From 3dd78e711e3ae1464c6b288acd91d28685b3bb9b Mon Sep 17 00:00:00 2001 From: Bhuminjay Soni Date: Tue, 21 Oct 2025 22:21:16 +0530 Subject: [PATCH 001/188] [syntax-errors] Name is parameter and global (#20426) ## Summary This PR implements a new semantic syntax error where name is parameter & global. ## Test Plan I have written inline test as directed in #17412 --------- Signed-off-by: 11happy Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- crates/ruff_linter/src/checkers/ast/mod.rs | 21 +++++++ crates/ruff_linter/src/linter.rs | 36 ++++++++++++ ...GlobalParameter_global_parameter_3.10.snap | 56 +++++++++++++++++++ .../ruff_python_parser/src/semantic_errors.rs | 23 ++++++++ crates/ruff_python_parser/tests/fixtures.rs | 4 ++ .../src/semantic_index/builder.rs | 3 + 6 files changed, 143 insertions(+) create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_GlobalParameter_global_parameter_3.10.snap diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index c6d4a5bf3d..7750d29f34 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -725,6 +725,7 @@ impl SemanticSyntaxContext for Checker<'_> { | SemanticSyntaxErrorKind::WriteToDebug(_) | SemanticSyntaxErrorKind::DifferentMatchPatternBindings | SemanticSyntaxErrorKind::InvalidExpression(..) + | SemanticSyntaxErrorKind::GlobalParameter(_) | SemanticSyntaxErrorKind::DuplicateMatchKey(_) | SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_) | SemanticSyntaxErrorKind::InvalidStarExpression @@ -846,6 +847,26 @@ impl SemanticSyntaxContext for Checker<'_> { } false } + + fn is_bound_parameter(&self, name: &str) -> bool { + for scope in self.semantic.current_scopes() { + match scope.kind { + ScopeKind::Class(_) => return false, + ScopeKind::Function(ast::StmtFunctionDef { parameters, .. }) + | ScopeKind::Lambda(ast::ExprLambda { + parameters: Some(parameters), + .. + }) => return parameters.includes(name), + ScopeKind::Lambda(_) + | ScopeKind::Generator { .. } + | ScopeKind::Module + | ScopeKind::Type + | ScopeKind::DunderClassCell => {} + } + } + + false + } } impl<'a> Visitor<'a> for Checker<'a> { diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 1386704920..3503a69c25 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1040,6 +1040,42 @@ mod tests { PythonVersion::PY310, "DuplicateMatchKey" )] + #[test_case( + "global_parameter", + " + def f(a): + global a + + def g(a): + if True: + global a + + def h(a): + def inner(): + global a + + def i(a): + try: + global a + except Exception: + pass + + def f(a): + a = 1 + global a + + def f(a): + a = 1 + a = 2 + global a + + def f(a): + class Inner: + global a # ok + ", + PythonVersion::PY310, + "GlobalParameter" + )] #[test_case( "duplicate_match_class_attribute", " diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_GlobalParameter_global_parameter_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_GlobalParameter_global_parameter_3.10.snap new file mode 100644 index 0000000000..c1c7fbd378 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_GlobalParameter_global_parameter_3.10.snap @@ -0,0 +1,56 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +invalid-syntax: name `a` is parameter and global + --> :3:12 + | +2 | def f(a): +3 | global a + | ^ +4 | +5 | def g(a): + | + +invalid-syntax: name `a` is parameter and global + --> :7:16 + | +5 | def g(a): +6 | if True: +7 | global a + | ^ +8 | +9 | def h(a): + | + +invalid-syntax: name `a` is parameter and global + --> :15:16 + | +13 | def i(a): +14 | try: +15 | global a + | ^ +16 | except Exception: +17 | pass + | + +invalid-syntax: name `a` is parameter and global + --> :21:12 + | +19 | def f(a): +20 | a = 1 +21 | global a + | ^ +22 | +23 | def f(a): + | + +invalid-syntax: name `a` is parameter and global + --> :26:12 + | +24 | a = 1 +25 | a = 2 +26 | global a + | ^ +27 | +28 | def f(a): + | diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index ac57426915..aba6e5ed7c 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -133,6 +133,17 @@ impl SemanticSyntaxChecker { } Self::duplicate_parameter_name(parameters, ctx); } + Stmt::Global(ast::StmtGlobal { names, .. }) => { + for name in names { + if ctx.is_bound_parameter(name) { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::GlobalParameter(name.to_string()), + name.range, + ); + } + } + } Stmt::ClassDef(ast::StmtClassDef { type_params, .. }) | Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. }) => { if let Some(type_params) = type_params { @@ -1137,6 +1148,9 @@ impl Display for SemanticSyntaxError { } SemanticSyntaxErrorKind::BreakOutsideLoop => f.write_str("`break` outside loop"), SemanticSyntaxErrorKind::ContinueOutsideLoop => f.write_str("`continue` outside loop"), + SemanticSyntaxErrorKind::GlobalParameter(name) => { + write!(f, "name `{name}` is parameter and global") + } SemanticSyntaxErrorKind::DifferentMatchPatternBindings => { write!(f, "alternative patterns bind different names") } @@ -1520,6 +1534,13 @@ pub enum SemanticSyntaxErrorKind { /// Represents the use of a `continue` statement outside of a loop. ContinueOutsideLoop, + /// Represents a function parameter that is also declared as `global`. + /// + /// Declaring a parameter as `global` is invalid, since parameters are already + /// bound in the local scope of the function. Using `global` on them introduces + /// ambiguity and will result in a `SyntaxError`. + GlobalParameter(String), + /// Represents the use of alternative patterns in a `match` statement that bind different names. /// /// Python requires all alternatives in an OR pattern (`|`) to bind the same set of names. @@ -2054,6 +2075,8 @@ pub trait SemanticSyntaxContext { fn report_semantic_error(&self, error: SemanticSyntaxError); fn in_loop_context(&self) -> bool; + + fn is_bound_parameter(&self, name: &str) -> bool; } /// Modified version of [`std::str::EscapeDefault`] that does not escape single or double quotes. diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index 9837d9d873..c646fe525b 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -575,6 +575,10 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { fn in_loop_context(&self) -> bool { true } + + fn is_bound_parameter(&self, _name: &str) -> bool { + false + } } impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> { diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 5bf8cbb3e7..8107f9c122 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -2789,6 +2789,9 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { fn in_loop_context(&self) -> bool { self.current_scope_info().current_loop.is_some() } + fn is_bound_parameter(&self, _name: &str) -> bool { + false + } } #[derive(Copy, Clone, Debug, PartialEq)] From 2dbca6370bb0940a8659b27e499bc6ad9fac7da6 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 21 Oct 2025 19:13:36 +0200 Subject: [PATCH 002/188] [ty] Avoid ever-growing default types (#20991) ## Summary We currently panic in the seemingly rare case where the type of a default value of a parameter depends on the callable itself: ```py class C: def f(self: C): self.x = lambda a=self.x: a ``` Types of default values are only used for display reasons, and it's unclear if we even want to track them (or if we should rather track the actual value). So it didn't seem to me that we should spend a lot of effort (and runtime) trying to achieve a theoretically correct type here (which would be infinite). Instead, we simply replace *nested* default types with `Unknown`, i.e. only if the type of the default value is a callable itself. closes https://github.com/astral-sh/ty/issues/1402 ## Test Plan Regression tests --- .../resources/mdtest/cycle.md | 73 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 25 ++++++- .../src/types/infer/builder.rs | 9 ++- .../src/types/signatures.rs | 28 ++++--- 4 files changed, 116 insertions(+), 19 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md index 0bd3b5b2a6..e52641a05c 100644 --- a/crates/ty_python_semantic/resources/mdtest/cycle.md +++ b/crates/ty_python_semantic/resources/mdtest/cycle.md @@ -31,3 +31,76 @@ p = Point() reveal_type(p.x) # revealed: Unknown | int reveal_type(p.y) # revealed: Unknown | int ``` + +## Parameter default values + +This is a regression test for . When a parameter has a +default value that references the callable itself, we currently prevent infinite recursion by simply +falling back to `Unknown` for the type of the default value, which does not have any practical +impact except for the displayed type. We could also consider inferring `Divergent` when we encounter +too many layers of nesting (instead of just one), but that would require a type traversal which +could have performance implications. So for now, we mainly make sure not to panic or stack overflow +for these seeminly rare cases. + +### Functions + +```py +class C: + def f(self: "C"): + def inner_a(positional=self.a): + return + self.a = inner_a + # revealed: def inner_a(positional=Unknown | (def inner_a(positional=Unknown) -> Unknown)) -> Unknown + reveal_type(inner_a) + + def inner_b(*, kw_only=self.b): + return + self.b = inner_b + # revealed: def inner_b(*, kw_only=Unknown | (def inner_b(*, kw_only=Unknown) -> Unknown)) -> Unknown + reveal_type(inner_b) + + def inner_c(positional_only=self.c, /): + return + self.c = inner_c + # revealed: def inner_c(positional_only=Unknown | (def inner_c(positional_only=Unknown, /) -> Unknown), /) -> Unknown + reveal_type(inner_c) + + def inner_d(*, kw_only=self.d): + return + self.d = inner_d + # revealed: def inner_d(*, kw_only=Unknown | (def inner_d(*, kw_only=Unknown) -> Unknown)) -> Unknown + reveal_type(inner_d) +``` + +We do, however, still check assignability of the default value to the parameter type: + +```py +class D: + def f(self: "D"): + # error: [invalid-parameter-default] "Default value of type `Unknown | (def inner_a(a: int = Unknown | (def inner_a(a: int = Unknown) -> Unknown)) -> Unknown)` is not assignable to annotated parameter type `int`" + def inner_a(a: int = self.a): ... + self.a = inner_a +``` + +### Lambdas + +```py +class C: + def f(self: "C"): + self.a = lambda positional=self.a: positional + self.b = lambda *, kw_only=self.b: kw_only + self.c = lambda positional_only=self.c, /: positional_only + self.d = lambda *, kw_only=self.d: kw_only + + # revealed: (positional=Unknown | ((positional=Unknown) -> Unknown)) -> Unknown + reveal_type(self.a) + + # revealed: (*, kw_only=Unknown | ((*, kw_only=Unknown) -> Unknown)) -> Unknown + reveal_type(self.b) + + # revealed: (positional_only=Unknown | ((positional_only=Unknown, /) -> Unknown), /) -> Unknown + reveal_type(self.c) + + # revealed: (*, kw_only=Unknown | ((*, kw_only=Unknown) -> Unknown)) -> Unknown + reveal_type(self.d) +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index d9a51ce5b9..aa13a13cd4 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6678,6 +6678,7 @@ impl<'db> Type<'db> { } } TypeMapping::PromoteLiterals + | TypeMapping::ReplaceParameterDefaults | TypeMapping::BindLegacyTypevars(_) => self, TypeMapping::Materialize(materialization_kind) => { Type::TypeVar(bound_typevar.materialize_impl(db, *materialization_kind, visitor)) @@ -6693,7 +6694,8 @@ impl<'db> Type<'db> { TypeMapping::PromoteLiterals | TypeMapping::BindSelf(_) | TypeMapping::ReplaceSelf { .. } | - TypeMapping::Materialize(_) => self, + TypeMapping::Materialize(_) | + TypeMapping::ReplaceParameterDefaults => self, } Type::FunctionLiteral(function) => { @@ -6807,7 +6809,8 @@ impl<'db> Type<'db> { TypeMapping::BindLegacyTypevars(_) | TypeMapping::BindSelf(_) | TypeMapping::ReplaceSelf { .. } | - TypeMapping::Materialize(_) => self, + TypeMapping::Materialize(_) | + TypeMapping::ReplaceParameterDefaults => self, TypeMapping::PromoteLiterals => self.promote_literals_impl(db, tcx) } @@ -6817,7 +6820,8 @@ impl<'db> Type<'db> { TypeMapping::BindLegacyTypevars(_) | TypeMapping::BindSelf(_) | TypeMapping::ReplaceSelf { .. } | - TypeMapping::PromoteLiterals => self, + TypeMapping::PromoteLiterals | + TypeMapping::ReplaceParameterDefaults => self, TypeMapping::Materialize(materialization_kind) => match materialization_kind { MaterializationKind::Top => Type::object(), MaterializationKind::Bottom => Type::Never, @@ -6993,6 +6997,15 @@ 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> { + self.apply_type_mapping( + db, + &TypeMapping::ReplaceParameterDefaults, + TypeContext::default(), + ) + } + /// Return the string representation of this type when converted to string as it would be /// provided by the `__str__` method. /// @@ -7369,6 +7382,9 @@ pub enum TypeMapping<'a, 'db> { ReplaceSelf { new_upper_bound: Type<'db> }, /// Create the top or bottom materialization of a type. Materialize(MaterializationKind), + /// Replace default types in parameters of callables with `Unknown`. This is used to avoid infinite + /// recursion when the type of the default value of a parameter depends on the callable itself. + ReplaceParameterDefaults, } impl<'db> TypeMapping<'_, 'db> { @@ -7383,7 +7399,8 @@ impl<'db> TypeMapping<'_, 'db> { | TypeMapping::PartialSpecialization(_) | TypeMapping::PromoteLiterals | TypeMapping::BindLegacyTypevars(_) - | TypeMapping::Materialize(_) => context, + | TypeMapping::Materialize(_) + | TypeMapping::ReplaceParameterDefaults => context, TypeMapping::BindSelf(_) => GenericContext::from_typevar_instances( db, context diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 9c6826e71e..821e650886 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -6508,7 +6508,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut parameter = Parameter::positional_only(Some(param.name().id.clone())); if let Some(default) = param.default() { parameter = parameter.with_default_type( - self.infer_expression(default, TypeContext::default()), + self.infer_expression(default, TypeContext::default()) + .replace_parameter_defaults(self.db()), ); } parameter @@ -6521,7 +6522,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut parameter = Parameter::positional_or_keyword(param.name().id.clone()); if let Some(default) = param.default() { parameter = parameter.with_default_type( - self.infer_expression(default, TypeContext::default()), + self.infer_expression(default, TypeContext::default()) + .replace_parameter_defaults(self.db()), ); } parameter @@ -6538,7 +6540,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut parameter = Parameter::keyword_only(param.name().id.clone()); if let Some(default) = param.default() { parameter = parameter.with_default_type( - self.infer_expression(default, TypeContext::default()), + self.infer_expression(default, TypeContext::default()) + .replace_parameter_defaults(self.db()), ); } parameter diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index a34473dc45..26c3aca6a1 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1267,9 +1267,9 @@ impl<'db> Parameters<'db> { } = parameters; let default_type = |param: &ast::ParameterWithDefault| { - param - .default() - .map(|default| definition_expression_type(db, definition, default)) + param.default().map(|default| { + definition_expression_type(db, definition, default).replace_parameter_defaults(db) + }) }; let method_info = infer_method_information(db, definition); @@ -1873,23 +1873,27 @@ impl<'db> ParameterKind<'db> { tcx: TypeContext<'db>, visitor: &ApplyTypeMappingVisitor<'db>, ) -> Self { + let apply_to_default_type = |default_type: &Option>| { + if type_mapping == &TypeMapping::ReplaceParameterDefaults && default_type.is_some() { + Some(Type::unknown()) + } else { + default_type + .as_ref() + .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + } + }; + match self { Self::PositionalOnly { default_type, name } => Self::PositionalOnly { - default_type: default_type - .as_ref() - .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)), + default_type: apply_to_default_type(default_type), name: name.clone(), }, Self::PositionalOrKeyword { default_type, name } => Self::PositionalOrKeyword { - default_type: default_type - .as_ref() - .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)), + default_type: apply_to_default_type(default_type), name: name.clone(), }, Self::KeywordOnly { default_type, name } => Self::KeywordOnly { - default_type: default_type - .as_ref() - .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)), + default_type: apply_to_default_type(default_type), name: name.clone(), }, Self::Variadic { .. } | Self::KeywordVariadic { .. } => self.clone(), From 9d1ffd605ca25aae5f473cfd05c75ad0ef0549cd Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 21 Oct 2025 19:25:41 +0200 Subject: [PATCH 003/188] [ty] Implement go-to for binary and unary operators (#21001) Co-authored-by: Alex Waygood --- crates/ruff_python_parser/src/token.rs | 10 +- crates/ty_ide/src/goto.rs | 116 +++++- crates/ty_ide/src/goto_definition.rs | 329 ++++++++++++++++-- crates/ty_ide/src/hover.rs | 119 +++++++ crates/ty_ide/src/references.rs | 7 +- crates/ty_python_semantic/src/lib.rs | 5 +- .../ty_python_semantic/src/semantic_model.rs | 2 +- crates/ty_python_semantic/src/types.rs | 8 + crates/ty_python_semantic/src/types/call.rs | 98 +++++- .../ty_python_semantic/src/types/call/bind.rs | 24 +- .../src/types/ide_support.rs | 100 +++++- .../src/types/infer/builder.rs | 77 +--- 12 files changed, 774 insertions(+), 121 deletions(-) diff --git a/crates/ruff_python_parser/src/token.rs b/crates/ruff_python_parser/src/token.rs index 1d9461a722..18b7648c4c 100644 --- a/crates/ruff_python_parser/src/token.rs +++ b/crates/ruff_python_parser/src/token.rs @@ -486,7 +486,7 @@ impl TokenKind { /// /// [`as_unary_operator`]: TokenKind::as_unary_operator #[inline] - pub(crate) const fn as_unary_arithmetic_operator(self) -> Option { + pub const fn as_unary_arithmetic_operator(self) -> Option { Some(match self { TokenKind::Plus => UnaryOp::UAdd, TokenKind::Minus => UnaryOp::USub, @@ -501,7 +501,7 @@ impl TokenKind { /// /// [`as_unary_arithmetic_operator`]: TokenKind::as_unary_arithmetic_operator #[inline] - pub(crate) const fn as_unary_operator(self) -> Option { + pub const fn as_unary_operator(self) -> Option { Some(match self { TokenKind::Plus => UnaryOp::UAdd, TokenKind::Minus => UnaryOp::USub, @@ -514,7 +514,7 @@ impl TokenKind { /// Returns the [`BoolOp`] that corresponds to this token kind, if it is a boolean operator, /// otherwise return [None]. #[inline] - pub(crate) const fn as_bool_operator(self) -> Option { + pub const fn as_bool_operator(self) -> Option { Some(match self { TokenKind::And => BoolOp::And, TokenKind::Or => BoolOp::Or, @@ -528,7 +528,7 @@ impl TokenKind { /// Use [`as_augmented_assign_operator`] to match against an augmented assignment token. /// /// [`as_augmented_assign_operator`]: TokenKind::as_augmented_assign_operator - pub(crate) const fn as_binary_operator(self) -> Option { + pub const fn as_binary_operator(self) -> Option { Some(match self { TokenKind::Plus => Operator::Add, TokenKind::Minus => Operator::Sub, @@ -550,7 +550,7 @@ impl TokenKind { /// Returns the [`Operator`] that corresponds to this token kind, if it is /// an augmented assignment operator, or [`None`] otherwise. #[inline] - pub(crate) const fn as_augmented_assign_operator(self) -> Option { + pub const fn as_augmented_assign_operator(self) -> Option { Some(match self { TokenKind::PlusEqual => Operator::Add, TokenKind::MinusEqual => Operator::Sub, diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index bda25506f2..d7a7091f94 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -8,19 +8,18 @@ use std::borrow::Cow; use crate::find_node::covering_node; use crate::stub_mapping::StubMapper; use ruff_db::parsed::ParsedModuleRef; -use ruff_python_ast::ExprCall; use ruff_python_ast::{self as ast, AnyNodeRef}; -use ruff_python_parser::TokenKind; +use ruff_python_parser::{TokenKind, Tokens}; use ruff_text_size::{Ranged, TextRange, TextSize}; -use ty_python_semantic::HasDefinition; -use ty_python_semantic::ImportAliasResolution; + use ty_python_semantic::ResolvedDefinition; use ty_python_semantic::types::Type; use ty_python_semantic::types::ide_support::{ call_signature_details, definitions_for_keyword_argument, }; use ty_python_semantic::{ - HasType, SemanticModel, definitions_for_imported_symbol, definitions_for_name, + HasDefinition, HasType, ImportAliasResolution, SemanticModel, definitions_for_imported_symbol, + definitions_for_name, }; #[derive(Clone, Debug)] @@ -30,6 +29,28 @@ pub(crate) enum GotoTarget<'a> { ClassDef(&'a ast::StmtClassDef), Parameter(&'a ast::Parameter), + /// Go to on the operator of a binary operation. + /// + /// ```py + /// a + b + /// ^ + /// ``` + BinOp { + expression: &'a ast::ExprBinOp, + operator_range: TextRange, + }, + + /// Go to where the operator of a unary operation is defined. + /// + /// ```py + /// -a + /// ^ + /// ``` + UnaryOp { + expression: &'a ast::ExprUnaryOp, + operator_range: TextRange, + }, + /// Multi-part module names /// Handles both `import foo.bar` and `from foo.bar import baz` cases /// ```py @@ -166,7 +187,7 @@ pub(crate) enum GotoTarget<'a> { /// The callable that can actually be selected by a cursor callable: ast::ExprRef<'a>, /// The call of the callable - call: &'a ExprCall, + call: &'a ast::ExprCall, }, } @@ -295,6 +316,16 @@ impl GotoTarget<'_> { | GotoTarget::TypeParamTypeVarTupleName(_) | GotoTarget::NonLocal { .. } | GotoTarget::Globals { .. } => return None, + GotoTarget::BinOp { expression, .. } => { + let (_, ty) = + ty_python_semantic::definitions_for_bin_op(model.db(), model, expression)?; + ty + } + GotoTarget::UnaryOp { expression, .. } => { + let (_, ty) = + ty_python_semantic::definitions_for_unary_op(model.db(), model, expression)?; + ty + } }; Some(ty) @@ -451,6 +482,23 @@ impl GotoTarget<'_> { } } + GotoTarget::BinOp { expression, .. } => { + let model = SemanticModel::new(db, file); + + let (definitions, _) = + ty_python_semantic::definitions_for_bin_op(db, &model, expression)?; + + Some(DefinitionsOrTargets::Definitions(definitions)) + } + + GotoTarget::UnaryOp { expression, .. } => { + let model = SemanticModel::new(db, file); + let (definitions, _) = + ty_python_semantic::definitions_for_unary_op(db, &model, expression)?; + + Some(DefinitionsOrTargets::Definitions(definitions)) + } + _ => None, } } @@ -524,6 +572,7 @@ impl GotoTarget<'_> { } GotoTarget::NonLocal { identifier, .. } => Some(Cow::Borrowed(identifier.as_str())), GotoTarget::Globals { identifier, .. } => Some(Cow::Borrowed(identifier.as_str())), + GotoTarget::BinOp { .. } | GotoTarget::UnaryOp { .. } => None, } } @@ -531,6 +580,7 @@ impl GotoTarget<'_> { pub(crate) fn from_covering_node<'a>( covering_node: &crate::find_node::CoveringNode<'a>, offset: TextSize, + tokens: &Tokens, ) -> Option> { tracing::trace!("Covering node is of kind {:?}", covering_node.node().kind()); @@ -690,6 +740,44 @@ impl GotoTarget<'_> { } }, + AnyNodeRef::ExprBinOp(binary) => { + if offset >= binary.left.end() && offset < binary.right.start() { + let between_operands = + tokens.in_range(TextRange::new(binary.left.end(), binary.right.start())); + if let Some(operator_token) = between_operands + .iter() + .find(|token| token.kind().as_binary_operator().is_some()) + && operator_token.range().contains_inclusive(offset) + { + return Some(GotoTarget::BinOp { + expression: binary, + operator_range: operator_token.range(), + }); + } + } + + Some(GotoTarget::Expression(binary.into())) + } + + AnyNodeRef::ExprUnaryOp(unary) => { + if offset >= unary.start() && offset < unary.operand.start() { + let before_operand = + tokens.in_range(TextRange::new(unary.start(), unary.operand.start())); + + if let Some(operator_token) = before_operand + .iter() + .find(|token| token.kind().as_unary_operator().is_some()) + && operator_token.range().contains_inclusive(offset) + { + return Some(GotoTarget::UnaryOp { + expression: unary, + operator_range: operator_token.range(), + }); + } + } + Some(GotoTarget::Expression(unary.into())) + } + node => { // Check if this is seemingly a callable being invoked (the `x` in `x(...)`) let parent = covering_node.parent(); @@ -737,6 +825,8 @@ impl Ranged for GotoTarget<'_> { GotoTarget::TypeParamTypeVarTupleName(tuple) => tuple.name.range, GotoTarget::NonLocal { identifier, .. } => identifier.range, GotoTarget::Globals { identifier, .. } => identifier.range, + GotoTarget::BinOp { operator_range, .. } + | GotoTarget::UnaryOp { operator_range, .. } => *operator_range, } } } @@ -794,7 +884,7 @@ fn definitions_for_expression<'db>( fn definitions_for_callable<'db>( db: &'db dyn crate::Db, file: ruff_db::files::File, - call: &ExprCall, + call: &ast::ExprCall, ) -> Vec> { let model = SemanticModel::new(db, file); // Attempt to refine to a specific call @@ -835,14 +925,24 @@ pub(crate) fn find_goto_target( | TokenKind::Complex | TokenKind::Float | TokenKind::Int => 1, + + TokenKind::Comment => -1, + + // if we have a+b`, prefer the `+` token (by respecting the token ordering) + // This matches VS Code's behavior where it sends the start of the clicked token as offset. + kind if kind.as_binary_operator().is_some() || kind.as_unary_operator().is_some() => 1, _ => 0, })?; + if token.kind().is_comment() { + return None; + } + let covering_node = covering_node(parsed.syntax().into(), token.range()) .find_first(|node| node.is_identifier() || node.is_expression()) .ok()?; - GotoTarget::from_covering_node(&covering_node, offset) + GotoTarget::from_covering_node(&covering_node, offset, parsed.tokens()) } /// Helper function to resolve a module name and create a navigation target. diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index fb165f61a0..6cc6d0c23d 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -798,26 +798,6 @@ my_func(my_other_func(ab=5, y=2), 0) "); } - impl CursorTest { - fn goto_definition(&self) -> String { - let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset) - else { - return "No goto target found".to_string(); - }; - - if targets.is_empty() { - return "No definitions found".to_string(); - } - - let source = targets.range; - self.render_diagnostics( - targets - .into_iter() - .map(|target| GotoDefinitionDiagnostic::new(source, &target)), - ) - } - } - #[test] fn goto_definition_overload_type_disambiguated1() { let test = CursorTest::builder() @@ -1130,6 +1110,315 @@ def ab(a: int, *, c: int): ... "#); } + #[test] + fn goto_definition_binary_operator() { + let test = CursorTest::builder() + .source( + "main.py", + " +class Test: + def __add__(self, other): + return Test() + + +a = Test() +b = Test() + +a + b +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __add__(self, other): + | ^^^^^^^ + 4 | return Test() + | + info: Source + --> main.py:10:3 + | + 8 | b = Test() + 9 | + 10 | a + b + | ^ + | + "); + } + + #[test] + fn goto_definition_binary_operator_reflected_dunder() { + let test = CursorTest::builder() + .source( + "main.py", + " +class A: + def __radd__(self, other) -> A: + return self + +class B: ... + +B() + A() +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Definition + --> main.py:3:9 + | + 2 | class A: + 3 | def __radd__(self, other) -> A: + | ^^^^^^^^ + 4 | return self + | + info: Source + --> main.py:8:5 + | + 6 | class B: ... + 7 | + 8 | B() + A() + | ^ + | + "); + } + + #[test] + fn goto_definition_binary_operator_no_spaces_before_operator() { + let test = CursorTest::builder() + .source( + "main.py", + " +class Test: + def __add__(self, other): + return Test() + + +a = Test() +b = Test() + +a+b +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __add__(self, other): + | ^^^^^^^ + 4 | return Test() + | + info: Source + --> main.py:10:2 + | + 8 | b = Test() + 9 | + 10 | a+b + | ^ + | + "); + } + + #[test] + fn goto_definition_binary_operator_no_spaces_after_operator() { + let test = CursorTest::builder() + .source( + "main.py", + " +class Test: + def __add__(self, other): + return Test() + + +a = Test() +b = Test() + +a+b +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Definition + --> main.py:8:1 + | + 7 | a = Test() + 8 | b = Test() + | ^ + 9 | + 10 | a+b + | + info: Source + --> main.py:10:3 + | + 8 | b = Test() + 9 | + 10 | a+b + | ^ + | + "); + } + + #[test] + fn goto_definition_binary_operator_comment() { + let test = CursorTest::builder() + .source( + "main.py", + " +class Test: + def __add__(self, other): + return Test() + + +( + Test() # comment + + Test() +) +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @"No goto target found"); + } + + #[test] + fn goto_definition_unary_operator() { + let test = CursorTest::builder() + .source( + "main.py", + " +class Test: + def __bool__(self) -> bool: ... + +a = Test() + +not a +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __bool__(self) -> bool: ... + | ^^^^^^^^ + 4 | + 5 | a = Test() + | + info: Source + --> main.py:7:1 + | + 5 | a = Test() + 6 | + 7 | not a + | ^^^ + | + "); + } + + #[test] + fn goto_definition_unary_after_operator() { + let test = CursorTest::builder() + .source( + "main.py", + " +class Test: + def __bool__(self) -> bool: ... + +a = Test() + +not a +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __bool__(self) -> bool: ... + | ^^^^^^^^ + 4 | + 5 | a = Test() + | + info: Source + --> main.py:7:1 + | + 5 | a = Test() + 6 | + 7 | not a + | ^^^ + | + "); + } + + #[test] + fn goto_definition_unary_between_operator_and_operand() { + let test = CursorTest::builder() + .source( + "main.py", + " +class Test: + def __bool__(self) -> bool: ... + +a = Test() + +-a +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Definition + --> main.py:5:1 + | + 3 | def __bool__(self) -> bool: ... + 4 | + 5 | a = Test() + | ^ + 6 | + 7 | -a + | + info: Source + --> main.py:7:2 + | + 5 | a = Test() + 6 | + 7 | -a + | ^ + | + "); + } + + impl CursorTest { + fn goto_definition(&self) -> String { + let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset) + else { + return "No goto target found".to_string(); + }; + + if targets.is_empty() { + return "No definitions found".to_string(); + } + + let source = targets.range; + self.render_diagnostics( + targets + .into_iter() + .map(|target| GotoDefinitionDiagnostic::new(source, &target)), + ) + } + } + struct GotoDefinitionDiagnostic { source: FileRange, target: FileRange, diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 241078c76f..b555295678 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -2514,6 +2514,125 @@ def ab(a: int, *, c: int): "); } + #[test] + fn hover_binary_operator_literal() { + let test = cursor_test( + r#" + result = 5 + 3 + "#, + ); + + assert_snapshot!(test.hover(), @r" + bound method int.__add__(value: int, /) -> int + --------------------------------------------- + Return self+value. + + --------------------------------------------- + ```python + bound method int.__add__(value: int, /) -> int + ``` + --- + ```text + Return self+value. + + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:12 + | + 2 | result = 5 + 3 + | - + | | + | source + | Cursor offset + | + "); + } + + #[test] + fn hover_binary_operator_overload() { + let test = cursor_test( + r#" + from __future__ import annotations + from typing import overload + + class Test: + @overload + def __add__(self, other: Test, /) -> Test: ... + @overload + def __add__(self, other: Other, /) -> Test: ... + def __add__(self, other: Test | Other, /) -> Test: + return self + + class Other: ... + + Test() + Test() + "#, + ); + + // TODO: We should only show the matching overload here. + // https://github.com/astral-sh/ty/issues/73 + assert_snapshot!(test.hover(), @r" + (other: Test, /) -> Test + (other: Other, /) -> Test + --------------------------------------------- + ```python + (other: Test, /) -> Test + (other: Other, /) -> Test + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:15:8 + | + 13 | class Other: ... + 14 | + 15 | Test() + Test() + | - + | | + | source + | Cursor offset + | + "); + } + + #[test] + fn hover_binary_operator_union() { + let test = cursor_test( + r#" + from __future__ import annotations + + class Test: + def __add__(self, other: Other, /) -> Other: + return other + + class Other: + def __add__(self, other: Other, /) -> Other: + return self + + def _(a: Test | Other): + a + Other() + "#, + ); + + assert_snapshot!(test.hover(), @r" + (bound method Test.__add__(other: Other, /) -> Other) | (bound method Other.__add__(other: Other, /) -> Other) + --------------------------------------------- + ```python + (bound method Test.__add__(other: Other, /) -> Other) | (bound method Other.__add__(other: Other, /) -> Other) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:13:7 + | + 12 | def _(a: Test | Other): + 13 | a + Other() + | ^- Cursor offset + | | + | source + | + "); + } + impl CursorTest { fn hover(&self) -> String { use std::fmt::Write; diff --git a/crates/ty_ide/src/references.rs b/crates/ty_ide/src/references.rs index 1dcd747418..62716fd6a5 100644 --- a/crates/ty_ide/src/references.rs +++ b/crates/ty_ide/src/references.rs @@ -18,6 +18,7 @@ use ruff_python_ast::{ self as ast, AnyNodeRef, visitor::source_order::{SourceOrderVisitor, TraversalSignal}, }; +use ruff_python_parser::Tokens; use ruff_text_size::{Ranged, TextRange}; use ty_python_semantic::ImportAliasResolution; @@ -127,6 +128,7 @@ fn references_for_file( target_definitions, references, mode, + tokens: module.tokens(), target_text, ancestors: Vec::new(), }; @@ -156,6 +158,7 @@ fn is_symbol_externally_visible(goto_target: &GotoTarget<'_>) -> bool { struct LocalReferencesFinder<'a> { db: &'a dyn Db, file: File, + tokens: &'a Tokens, target_definitions: &'a [NavigationTarget], references: &'a mut Vec, mode: ReferencesMode, @@ -282,7 +285,9 @@ impl LocalReferencesFinder<'_> { // where the identifier might be a multi-part module name. let offset = covering_node.node().start(); - if let Some(goto_target) = GotoTarget::from_covering_node(covering_node, offset) { + if let Some(goto_target) = + GotoTarget::from_covering_node(covering_node, offset, self.tokens) + { // Get the definitions for this goto target if let Some(current_definitions_nav) = goto_target .get_definition_targets(self.file, self.db, ImportAliasResolution::PreserveAliases) diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 5f41200522..ea0b492b7b 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -27,8 +27,9 @@ pub use semantic_model::{ pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin}; pub use types::DisplaySettings; pub use types::ide_support::{ - ImportAliasResolution, ResolvedDefinition, definitions_for_attribute, - definitions_for_imported_symbol, definitions_for_name, map_stub_definition, + ImportAliasResolution, ResolvedDefinition, definitions_for_attribute, definitions_for_bin_op, + definitions_for_imported_symbol, definitions_for_name, definitions_for_unary_op, + map_stub_definition, }; pub mod ast_node_ref; diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index beb2c7f968..a7db9d5698 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -27,7 +27,7 @@ impl<'db> SemanticModel<'db> { // TODO we don't actually want to expose the Db directly to lint rules, but we need to find a // solution for exposing information from types - pub fn db(&self) -> &dyn Db { + pub fn db(&self) -> &'db dyn Db { self.db } diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index aa13a13cd4..8fdc21e2b5 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -9987,6 +9987,14 @@ 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 { + Self::new(db, self.function(db), f(self.self_instance(db))) + } + #[salsa::tracked(cycle_fn=into_callable_type_cycle_recover, 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); diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs index 8c00ab3479..e2fb7dac96 100644 --- a/crates/ty_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -1,14 +1,87 @@ use super::context::InferContext; -use super::{Signature, Type}; +use super::{Signature, Type, TypeContext}; use crate::Db; use crate::types::PropertyInstanceType; use crate::types::call::bind::BindingError; +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}; +impl<'db> Type<'db> { + pub(crate) fn try_call_bin_op( + db: &'db dyn Db, + left_ty: Type<'db>, + op: ast::Operator, + right_ty: Type<'db>, + ) -> Result, CallBinOpError> { + // We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from + // the Python spec [1] is: + // + // - If rhs is a (proper) subclass of lhs, and it provides a different + // implementation of __rop__, use that. + // - Otherwise, if lhs implements __op__, use that. + // - Otherwise, if lhs and rhs are different types, and rhs implements __rop__, + // use that. + // + // [1] https://docs.python.org/3/reference/datamodel.html#object.__radd__ + + // Technically we don't have to check left_ty != right_ty here, since if the types + // are the same, they will trivially have the same implementation of the reflected + // dunder, and so we'll fail the inner check. But the type equality check will be + // faster for the common case, and allow us to skip the (two) class member lookups. + let left_class = left_ty.to_meta_type(db); + let right_class = right_ty.to_meta_type(db); + if left_ty != right_ty && right_ty.is_subtype_of(db, left_ty) { + let reflected_dunder = op.reflected_dunder(); + let rhs_reflected = right_class.member(db, reflected_dunder).place; + // TODO: if `rhs_reflected` is possibly unbound, we should union the two possible + // Bindings together + if !rhs_reflected.is_undefined() + && rhs_reflected != left_class.member(db, reflected_dunder).place + { + return Ok(right_ty + .try_call_dunder( + db, + reflected_dunder, + CallArguments::positional([left_ty]), + TypeContext::default(), + ) + .or_else(|_| { + left_ty.try_call_dunder( + db, + op.dunder(), + CallArguments::positional([right_ty]), + TypeContext::default(), + ) + })?); + } + } + + let call_on_left_instance = left_ty.try_call_dunder( + db, + op.dunder(), + CallArguments::positional([right_ty]), + TypeContext::default(), + ); + + call_on_left_instance.or_else(|_| { + if left_ty == right_ty { + Err(CallBinOpError::NotSupported) + } else { + Ok(right_ty.try_call_dunder( + db, + op.reflected_dunder(), + CallArguments::positional([left_ty]), + TypeContext::default(), + )?) + } + }) + } +} + /// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was /// unsuccessful. /// @@ -26,7 +99,7 @@ impl<'db> CallError<'db> { return None; } self.1 - .into_iter() + .iter() .flatten() .flat_map(bind::Binding::errors) .find_map(|error| match error { @@ -89,3 +162,24 @@ impl<'db> From> for CallDunderError<'db> { Self::CallError(kind, bindings) } } + +#[derive(Debug)] +pub(crate) 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. + CallError, + + NotSupported, +} + +impl From> for CallBinOpError { + fn from(value: CallDunderError<'_>) -> Self { + match value { + CallDunderError::CallError(_, _) => Self::CallError, + CallDunderError::MethodNotAvailable | CallDunderError::PossiblyUnbound(_) => { + CallBinOpError::NotSupported + } + } + } +} diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 9c4df3c22c..dd1b2215c6 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -96,6 +96,10 @@ impl<'db> Bindings<'db> { &self.argument_forms.values } + pub(crate) fn iter(&self) -> std::slice::Iter<'_, CallableBinding<'db>> { + self.elements.iter() + } + /// Match the arguments of a call site against the parameters of a collection of possibly /// unioned, possibly overloaded signatures. /// @@ -1178,7 +1182,16 @@ impl<'a, 'db> IntoIterator for &'a Bindings<'db> { type IntoIter = std::slice::Iter<'a, CallableBinding<'db>>; fn into_iter(self) -> Self::IntoIter { - self.elements.iter() + self.iter() + } +} + +impl<'db> IntoIterator for Bindings<'db> { + type Item = CallableBinding<'db>; + type IntoIter = smallvec::IntoIter<[CallableBinding<'db>; 1]>; + + fn into_iter(self) -> Self::IntoIter { + self.elements.into_iter() } } @@ -2106,6 +2119,15 @@ impl<'a, 'db> IntoIterator for &'a CallableBinding<'db> { } } +impl<'db> IntoIterator for CallableBinding<'db> { + type Item = Binding<'db>; + type IntoIter = smallvec::IntoIter<[Binding<'db>; 1]>; + + fn into_iter(self) -> Self::IntoIter { + self.overloads.into_iter() + } +} + #[derive(Debug, Copy, Clone)] enum OverloadCallReturnType<'db> { ArgumentTypeExpansion(Type<'db>), diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index d02011390b..b8dbc97a52 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -13,7 +13,7 @@ use crate::semantic_index::{ use crate::types::call::{CallArguments, MatchedArgument}; use crate::types::signatures::Signature; use crate::types::{ - ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type, + ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type, TypeContext, TypeVarBoundOrConstraints, class::CodeGeneratorKind, }; use crate::{Db, HasType, NameKind, SemanticModel}; @@ -908,18 +908,19 @@ pub fn call_signature_details<'db>( .into_iter() .flat_map(std::iter::IntoIterator::into_iter) .map(|binding| { - let signature = &binding.signature; + let argument_to_parameter_mapping = binding.argument_matches().to_vec(); + let signature = binding.signature; let display_details = signature.display(db).to_string_parts(); - let parameter_label_offsets = display_details.parameter_ranges.clone(); - let parameter_names = display_details.parameter_names.clone(); + let parameter_label_offsets = display_details.parameter_ranges; + let parameter_names = display_details.parameter_names; CallSignatureDetails { - signature: signature.clone(), + definition: signature.definition(), + signature, label: display_details.label, parameter_label_offsets, parameter_names, - definition: signature.definition(), - argument_to_parameter_mapping: binding.argument_matches().to_vec(), + argument_to_parameter_mapping, } }) .collect() @@ -929,6 +930,91 @@ pub fn call_signature_details<'db>( } } +/// Returns the definitions of the binary operation along with its callable type. +pub fn definitions_for_bin_op<'db>( + db: &'db dyn Db, + model: &SemanticModel<'db>, + binary_op: &ast::ExprBinOp, +) -> Option<(Vec>, Type<'db>)> { + let left_ty = binary_op.left.inferred_type(model); + let right_ty = binary_op.right.inferred_type(model); + + let Ok(bindings) = Type::try_call_bin_op(db, left_ty, binary_op.op, right_ty) else { + return None; + }; + + let callable_type = promote_literals_for_self(db, bindings.callable_type()); + + let definitions: Vec<_> = bindings + .into_iter() + .flat_map(std::iter::IntoIterator::into_iter) + .filter_map(|binding| { + Some(ResolvedDefinition::Definition( + binding.signature.definition?, + )) + }) + .collect(); + + Some((definitions, callable_type)) +} + +/// Returns the definitions for an unary operator along with their callable types. +pub fn definitions_for_unary_op<'db>( + db: &'db dyn Db, + model: &SemanticModel<'db>, + unary_op: &ast::ExprUnaryOp, +) -> Option<(Vec>, Type<'db>)> { + let operand_ty = unary_op.operand.inferred_type(model); + + let unary_dunder_method = match unary_op.op { + ast::UnaryOp::Invert => "__invert__", + ast::UnaryOp::UAdd => "__pos__", + ast::UnaryOp::USub => "__neg__", + ast::UnaryOp::Not => "__bool__", + }; + + let Ok(bindings) = operand_ty.try_call_dunder( + db, + unary_dunder_method, + CallArguments::none(), + TypeContext::default(), + ) else { + return None; + }; + + let callable_type = promote_literals_for_self(db, bindings.callable_type()); + + let definitions = bindings + .into_iter() + .flat_map(std::iter::IntoIterator::into_iter) + .filter_map(|binding| { + Some(ResolvedDefinition::Definition( + binding.signature.definition?, + )) + }) + .collect(); + + Some((definitions, callable_type)) +} + +/// Promotes literal types in `self` positions to their fallback instance types. +/// +/// 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> { + match ty { + Type::BoundMethod(method) => Type::BoundMethod(method.map_self_type(db, |self_ty| { + self_ty.literal_fallback_instance(db).unwrap_or(self_ty) + })), + Type::Union(elements) => elements.map(db, |ty| match ty { + Type::BoundMethod(method) => Type::BoundMethod(method.map_self_type(db, |self_ty| { + self_ty.literal_fallback_instance(db).unwrap_or(self_ty) + })), + _ => *ty, + }), + ty => ty, + } +} + /// Find the active signature index from `CallSignatureDetails`. /// The active signature is the first signature where all arguments present in the call /// have valid mappings to parameters (i.e., none of the mappings are None). diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 821e650886..d67a39dab0 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8216,80 +8216,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::TypeIs(_) | Type::TypedDict(_), op, - ) => { - // We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from - // the Python spec [1] is: - // - // - If rhs is a (proper) subclass of lhs, and it provides a different - // implementation of __rop__, use that. - // - Otherwise, if lhs implements __op__, use that. - // - Otherwise, if lhs and rhs are different types, and rhs implements __rop__, - // use that. - // - // [1] https://docs.python.org/3/reference/datamodel.html#object.__radd__ - - // Technically we don't have to check left_ty != right_ty here, since if the types - // are the same, they will trivially have the same implementation of the reflected - // dunder, and so we'll fail the inner check. But the type equality check will be - // faster for the common case, and allow us to skip the (two) class member lookups. - let left_class = left_ty.to_meta_type(self.db()); - let right_class = right_ty.to_meta_type(self.db()); - if left_ty != right_ty && right_ty.is_subtype_of(self.db(), left_ty) { - let reflected_dunder = op.reflected_dunder(); - let rhs_reflected = right_class.member(self.db(), reflected_dunder).place; - // TODO: if `rhs_reflected` is possibly unbound, we should union the two possible - // Bindings together - if !rhs_reflected.is_undefined() - && rhs_reflected != left_class.member(self.db(), reflected_dunder).place - { - return right_ty - .try_call_dunder( - self.db(), - reflected_dunder, - CallArguments::positional([left_ty]), - TypeContext::default(), - ) - .map(|outcome| outcome.return_type(self.db())) - .or_else(|_| { - left_ty - .try_call_dunder( - self.db(), - op.dunder(), - CallArguments::positional([right_ty]), - TypeContext::default(), - ) - .map(|outcome| outcome.return_type(self.db())) - }) - .ok(); - } - } - - let call_on_left_instance = left_ty - .try_call_dunder( - self.db(), - op.dunder(), - CallArguments::positional([right_ty]), - TypeContext::default(), - ) - .map(|outcome| outcome.return_type(self.db())) - .ok(); - - call_on_left_instance.or_else(|| { - if left_ty == right_ty { - None - } else { - right_ty - .try_call_dunder( - self.db(), - op.reflected_dunder(), - CallArguments::positional([left_ty]), - TypeContext::default(), - ) - .map(|outcome| outcome.return_type(self.db())) - .ok() - } - }) - } + ) => Type::try_call_bin_op(self.db(), left_ty, op, right_ty) + .map(|outcome| outcome.return_type(self.db())) + .ok(), } } From 2e13b13012cf561d1766fecabf05accd8017d9b1 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Tue, 21 Oct 2025 13:38:40 -0400 Subject: [PATCH 004/188] [ty] Support goto-definition on vendored typeshed stubs (#21020) This is an alternative to #21012 that more narrowly handles this logic in the stub-mapping machinery rather than pervasively allowing us to identify cached files as typeshed stubs. Much of the logic is the same (pulling the logic out of ty_server so it can be reused). I don't have a good sense for if one approach is "better" or "worse" in terms of like, semantics and Weird Bugs that this can cause. This one is just "less spooky in its broad consequences" and "less muddying of separation of concerns" and puts the extra logic on a much colder path. I won't be surprised if one day the previous implementation needs to be revisited for its more sweeping effects but for now this is good. Fixes https://github.com/astral-sh/ty/issues/1054 --- Cargo.lock | 2 +- crates/ty_ide/Cargo.toml | 1 + crates/ty_ide/src/lib.rs | 38 ++++++++++++++++++- crates/ty_ide/src/stub_mapping.rs | 14 ++++++- .../src/types/ide_support.rs | 33 ++++++++++++++-- crates/ty_server/Cargo.toml | 1 - crates/ty_server/src/system.rs | 16 +------- 7 files changed, 83 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7850570a20..3d917171a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4376,6 +4376,7 @@ dependencies = [ "tracing", "ty_project", "ty_python_semantic", + "ty_vendored", ] [[package]] @@ -4504,7 +4505,6 @@ dependencies = [ "ty_ide", "ty_project", "ty_python_semantic", - "ty_vendored", ] [[package]] diff --git a/crates/ty_ide/Cargo.toml b/crates/ty_ide/Cargo.toml index 3cbb298f9d..4be5d49fe2 100644 --- a/crates/ty_ide/Cargo.toml +++ b/crates/ty_ide/Cargo.toml @@ -25,6 +25,7 @@ ruff_source_file = { workspace = true } ruff_text_size = { workspace = true } ty_python_semantic = { workspace = true } ty_project = { workspace = true, features = ["testing"] } +ty_vendored = { workspace = true } get-size2 = { workspace = true } itertools = { workspace = true } diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 9febfb06ec..6a23302561 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -45,7 +45,11 @@ pub use signature_help::{ParameterDetails, SignatureDetails, SignatureHelpInfo, pub use symbols::{FlatSymbols, HierarchicalSymbols, SymbolId, SymbolInfo, SymbolKind}; pub use workspace_symbols::{WorkspaceSymbolInfo, workspace_symbols}; -use ruff_db::files::{File, FileRange}; +use ruff_db::{ + files::{File, FileRange}, + system::SystemPathBuf, + vendored::VendoredPath, +}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; use std::ops::{Deref, DerefMut}; @@ -287,6 +291,38 @@ impl HasNavigationTargets for TypeDefinition<'_> { } } +/// Get the cache-relative path where vendored paths should be written to. +pub fn relative_cached_vendored_root() -> SystemPathBuf { + // The vendored files are uniquely identified by the source commit. + SystemPathBuf::from(format!("vendored/typeshed/{}", ty_vendored::SOURCE_COMMIT)) +} + +/// Get the cached version of a vendored path in the cache, ensuring the file is written to disk. +pub fn cached_vendored_path( + db: &dyn ty_python_semantic::Db, + path: &VendoredPath, +) -> Option { + let writable = db.system().as_writable()?; + let mut relative_path = relative_cached_vendored_root(); + relative_path.push(path.as_str()); + + // Extract the vendored file onto the system. + writable + .get_or_cache(&relative_path, &|| db.vendored().read_to_string(path)) + .ok() + .flatten() +} + +/// Get the absolute root path of all cached vendored paths. +/// +/// This does not ensure that this path exists (this is only used for mapping cached paths +/// back to vendored ones, so this only matters if we've already been handed a path inside here). +pub fn cached_vendored_root(db: &dyn ty_python_semantic::Db) -> Option { + let writable = db.system().as_writable()?; + let relative_root = relative_cached_vendored_root(); + Some(writable.cache_dir()?.join(relative_root)) +} + #[cfg(test)] mod tests { use camino::Utf8Component; diff --git a/crates/ty_ide/src/stub_mapping.rs b/crates/ty_ide/src/stub_mapping.rs index d28823d145..5499fd5595 100644 --- a/crates/ty_ide/src/stub_mapping.rs +++ b/crates/ty_ide/src/stub_mapping.rs @@ -1,6 +1,9 @@ use itertools::Either; +use ruff_db::system::SystemPathBuf; use ty_python_semantic::{ResolvedDefinition, map_stub_definition}; +use crate::cached_vendored_root; + /// Maps `ResolvedDefinitions` from stub files to corresponding definitions in source files. /// /// This mapper is used to implement "Go To Definition" functionality that navigates from @@ -9,11 +12,16 @@ use ty_python_semantic::{ResolvedDefinition, map_stub_definition}; /// docstrings for functions that resolve to stubs. pub(crate) struct StubMapper<'db> { db: &'db dyn crate::Db, + cached_vendored_root: Option, } impl<'db> StubMapper<'db> { pub(crate) fn new(db: &'db dyn crate::Db) -> Self { - Self { db } + let cached_vendored_root = cached_vendored_root(db); + Self { + db, + cached_vendored_root, + } } /// Map a `ResolvedDefinition` from a stub file to corresponding definitions in source files. @@ -24,7 +32,9 @@ impl<'db> StubMapper<'db> { &self, def: ResolvedDefinition<'db>, ) -> impl Iterator> { - if let Some(definitions) = map_stub_definition(self.db, &def) { + if let Some(definitions) = + map_stub_definition(self.db, &def, self.cached_vendored_root.as_deref()) + { return Either::Left(definitions.into_iter()); } Either::Right(std::iter::once(def)) diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index b8dbc97a52..331d89a412 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -1138,8 +1138,10 @@ mod resolve_definition { } use indexmap::IndexSet; - use ruff_db::files::{File, FileRange}; + use ruff_db::files::{File, FileRange, vendored_path_to_file}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; + use ruff_db::system::SystemPath; + use ruff_db::vendored::VendoredPathBuf; use ruff_python_ast as ast; use rustc_hash::FxHashSet; use tracing::trace; @@ -1397,17 +1399,42 @@ mod resolve_definition { pub fn map_stub_definition<'db>( db: &'db dyn Db, def: &ResolvedDefinition<'db>, + cached_vendored_typeshed: Option<&SystemPath>, ) -> Option>> { - trace!("Stub mapping definition..."); // If the file isn't a stub, this is presumably the real definition let stub_file = def.file(db); + trace!("Stub mapping definition in: {}", stub_file.path(db)); if !stub_file.is_stub(db) { trace!("File isn't a stub, no stub mapping to do"); return None; } + // We write vendored typeshed stubs to disk in the cache, and consequently "forget" + // that they're typeshed when an IDE hands those paths back to us later. For most + // purposes this seemingly doesn't matter at all, and avoids issues with someone + // editing the cache by hand in their IDE and us getting confused about the contents + // of the file (hello and welcome to anyone who has found Bigger Issues this causes). + // + // The major exception is in exactly stub-mapping, where we need to "remember" that + // we're in typeshed to successfully stub-map to the Real Stdlib. So here we attempt + // to do just that. The resulting file must not be used for anything other than + // this module lookup, as the `ResolvedDefinition` we're handling isn't for that file. + let mut stub_file_for_module_lookup = stub_file; + if let Some(vendored_typeshed) = cached_vendored_typeshed + && let Some(stub_path) = stub_file.path(db).as_system_path() + && let Ok(rel_path) = stub_path.strip_prefix(vendored_typeshed) + && let Ok(typeshed_file) = + vendored_path_to_file(db, VendoredPathBuf::from(rel_path.as_str())) + { + trace!( + "Stub is cached vendored typeshed: {}", + typeshed_file.path(db) + ); + stub_file_for_module_lookup = typeshed_file; + } + // It's definitely a stub, so now rerun module resolution but with stubs disabled. - let stub_module = file_to_module(db, stub_file)?; + let stub_module = file_to_module(db, stub_file_for_module_lookup)?; trace!("Found stub module: {}", stub_module.name(db)); let real_module = resolve_real_module(db, stub_module.name(db))?; trace!("Found real module: {}", real_module.name(db)); diff --git a/crates/ty_server/Cargo.toml b/crates/ty_server/Cargo.toml index 3e7da397f6..4abb4b627b 100644 --- a/crates/ty_server/Cargo.toml +++ b/crates/ty_server/Cargo.toml @@ -22,7 +22,6 @@ ty_combine = { workspace = true } ty_ide = { workspace = true } ty_project = { workspace = true } ty_python_semantic = { workspace = true } -ty_vendored = { workspace = true } anyhow = { workspace = true } bitflags = { workspace = true } diff --git a/crates/ty_server/src/system.rs b/crates/ty_server/src/system.rs index 46011daa44..323e4a6846 100644 --- a/crates/ty_server/src/system.rs +++ b/crates/ty_server/src/system.rs @@ -13,6 +13,7 @@ use ruff_db::system::{ SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf, WritableSystem, }; use ruff_notebook::{Notebook, NotebookError}; +use ty_ide::cached_vendored_path; use ty_python_semantic::Db; use crate::DocumentQuery; @@ -25,20 +26,7 @@ pub(crate) fn file_to_url(db: &dyn Db, file: File) -> Option { FilePath::System(system) => Url::from_file_path(system.as_std_path()).ok(), FilePath::SystemVirtual(path) => Url::parse(path.as_str()).ok(), FilePath::Vendored(path) => { - let writable = db.system().as_writable()?; - - let system_path = SystemPathBuf::from(format!( - "vendored/typeshed/{}/{}", - // The vendored files are uniquely identified by the source commit. - ty_vendored::SOURCE_COMMIT, - path.as_str() - )); - - // Extract the vendored file onto the system. - let system_path = writable - .get_or_cache(&system_path, &|| db.vendored().read_to_string(path)) - .ok() - .flatten()?; + let system_path = cached_vendored_path(db, path)?; Url::from_file_path(system_path.as_std_path()).ok() } From 4b0fa5f2709b56b7c76b9cb864f250f54c1fe963 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:47:26 -0400 Subject: [PATCH 005/188] Render a diagnostic for syntax errors introduced in formatter tests (#21021) ## Summary I spun this out from #21005 because I thought it might be helpful separately. It just renders a nice `Diagnostic` for syntax errors pointing to the source of the error. This seemed a bit more helpful to me than just the byte offset when working on #21005, and we had most of the code around after #20443 anyway. ## Test Plan This doesn't actually affect any passing tests, but here's an example of the additional output I got when I broke the spacing after the `in` token: ``` error[internal-error]: Expected 'in', found name --> /home/brent/astral/ruff/crates/ruff_python_formatter/resources/test/fixtures/black/cases/cantfit.py:50:79 | 48 | need_more_to_make_the_line_long_enough, 49 | ) 50 | del ([], name_1, name_2), [(), [], name_4, name_3], name_1[[name_2 for name_1 inname_0]] | ^^^^^^^^ 51 | del () | ``` I just appended this to the other existing output for now. --- crates/ruff/src/commands/format.rs | 14 +-------- crates/ruff_python_formatter/src/lib.rs | 30 ++++++++++++++++++- .../ruff_python_formatter/tests/fixtures.rs | 12 ++++++-- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 20c000a89d..1f79e59339 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -879,19 +879,7 @@ impl From<&FormatCommandError> for Diagnostic { | FormatCommandError::Write(_, source_error) => { Diagnostic::new(DiagnosticId::Io, Severity::Error, source_error) } - FormatCommandError::Format(_, format_module_error) => match format_module_error { - FormatModuleError::ParseError(parse_error) => Diagnostic::new( - DiagnosticId::InternalError, - Severity::Error, - &parse_error.error, - ), - FormatModuleError::FormatError(format_error) => { - Diagnostic::new(DiagnosticId::InternalError, Severity::Error, format_error) - } - FormatModuleError::PrintError(print_error) => { - Diagnostic::new(DiagnosticId::InternalError, Severity::Error, print_error) - } - }, + FormatCommandError::Format(_, format_module_error) => format_module_error.into(), FormatCommandError::RangeFormatNotebook(_) => Diagnostic::new( DiagnosticId::InvalidCliOption, Severity::Error, diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 3dbef73807..e6b2f9e7b8 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -1,3 +1,4 @@ +use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; @@ -10,7 +11,7 @@ use ruff_formatter::{FormatError, Formatted, PrintError, Printed, SourceCode, fo use ruff_python_ast::{AnyNodeRef, Mod}; use ruff_python_parser::{ParseError, ParseOptions, Parsed, parse}; use ruff_python_trivia::CommentRanges; -use ruff_text_size::Ranged; +use ruff_text_size::{Ranged, TextRange}; use crate::comments::{ Comments, SourceComment, has_skip_comment, leading_comments, trailing_comments, @@ -117,6 +118,33 @@ pub enum FormatModuleError { PrintError(#[from] PrintError), } +impl FormatModuleError { + pub fn range(&self) -> Option { + match self { + FormatModuleError::ParseError(parse_error) => Some(parse_error.range()), + FormatModuleError::FormatError(_) | FormatModuleError::PrintError(_) => None, + } + } +} + +impl From<&FormatModuleError> for Diagnostic { + fn from(error: &FormatModuleError) -> Self { + match error { + FormatModuleError::ParseError(parse_error) => Diagnostic::new( + DiagnosticId::InternalError, + Severity::Error, + &parse_error.error, + ), + FormatModuleError::FormatError(format_error) => { + Diagnostic::new(DiagnosticId::InternalError, Severity::Error, format_error) + } + FormatModuleError::PrintError(print_error) => { + Diagnostic::new(DiagnosticId::InternalError, Severity::Error, print_error) + } + } + } +} + #[tracing::instrument(name = "format", level = Level::TRACE, skip_all)] pub fn format_module_source( source: &str, diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index 776b272d21..33741bd744 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -404,10 +404,18 @@ fn ensure_stability_when_formatting_twice( let reformatted = match format_module_source(formatted_code, options.clone()) { Ok(reformatted) => reformatted, Err(err) => { + let mut diag = Diagnostic::from(&err); + if let Some(range) = err.range() { + let file = + SourceFileBuilder::new(input_path.to_string_lossy(), formatted_code).finish(); + let span = Span::from(file).with_range(range); + diag.annotate(Annotation::primary(span)); + } panic!( "Expected formatted code of {} to be valid syntax: {err}:\ - \n---\n{formatted_code}---\n", - input_path.display() + \n---\n{formatted_code}---\n{}", + input_path.display(), + diag.display(&DummyFileResolver, &DisplayDiagnosticConfig::default()), ); } }; From a51a0f16e491e358f63cd587e98fa1bdcee4a29d Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Wed, 22 Oct 2025 05:58:48 +0900 Subject: [PATCH 006/188] [`flake8-simplify`] Skip `SIM911` when unknown arguments are present (#20697) ## Summary Fixes #18778 Prevent SIM911 from triggering when zip() is called on .keys()/.values() that take any positional or keyword arguments, so Ruff never suggests the lossy rewrite. ## Test Plan Added a test case to SIM911.py. --- .../test/fixtures/flake8_simplify/SIM911.py | 4 ++++ .../rules/zip_dict_keys_and_values.rs | 20 +++++++++++++------ ...ke8_simplify__tests__SIM911_SIM911.py.snap | 5 +++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM911.py b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM911.py index 3dd52b4139..ee5bed6854 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM911.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM911.py @@ -34,3 +34,7 @@ def foo(): # https://github.com/astral-sh/ruff/issues/18776 flag_stars = {} for country, stars in(zip)(flag_stars.keys(), flag_stars.values()):... + +# Regression test for https://github.com/astral-sh/ruff/issues/18778 +d = {} +for country, stars in zip(d.keys(*x), d.values("hello")):... diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/zip_dict_keys_and_values.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/zip_dict_keys_and_values.rs index 58f3a01a8a..28d123fab3 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/zip_dict_keys_and_values.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/zip_dict_keys_and_values.rs @@ -78,13 +78,18 @@ pub(crate) fn zip_dict_keys_and_values(checker: &Checker, expr: &ast::ExprCall) let [arg1, arg2] = &args[..] else { return; }; - let Some((var1, attr1)) = get_var_attr(arg1) else { + let Some((var1, attr1, args1)) = get_var_attr_args(arg1) else { return; }; - let Some((var2, attr2)) = get_var_attr(arg2) else { + let Some((var2, attr2, args2)) = get_var_attr_args(arg2) else { return; }; - if var1.id != var2.id || attr1 != "keys" || attr2 != "values" { + if var1.id != var2.id + || attr1 != "keys" + || attr2 != "values" + || !args1.is_empty() + || !args2.is_empty() + { return; } if !checker.semantic().match_builtin_expr(func, "zip") { @@ -122,8 +127,11 @@ pub(crate) fn zip_dict_keys_and_values(checker: &Checker, expr: &ast::ExprCall) ))); } -fn get_var_attr(expr: &Expr) -> Option<(&ExprName, &Identifier)> { - let Expr::Call(ast::ExprCall { func, .. }) = expr else { +fn get_var_attr_args(expr: &Expr) -> Option<(&ExprName, &Identifier, &Arguments)> { + let Expr::Call(ast::ExprCall { + func, arguments, .. + }) = expr + else { return None; }; let Expr::Attribute(ExprAttribute { value, attr, .. }) = func.as_ref() else { @@ -132,5 +140,5 @@ fn get_var_attr(expr: &Expr) -> Option<(&ExprName, &Identifier)> { let Expr::Name(var_name) = value.as_ref() else { return None; }; - Some((var_name, attr)) + Some((var_name, attr, arguments)) } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM911_SIM911.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM911_SIM911.py.snap index ffecf5ebeb..3b2f5bdf35 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM911_SIM911.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM911_SIM911.py.snap @@ -81,6 +81,8 @@ SIM911 [*] Use ` flag_stars.items()` instead of `(zip)(flag_stars.keys(), flag_s 35 | flag_stars = {} 36 | for country, stars in(zip)(flag_stars.keys(), flag_stars.values()):... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +37 | +38 | # Regression test for https://github.com/astral-sh/ruff/issues/18778 | help: Replace `(zip)(flag_stars.keys(), flag_stars.values())` with ` flag_stars.items()` 33 | @@ -88,3 +90,6 @@ help: Replace `(zip)(flag_stars.keys(), flag_stars.values())` with ` flag_stars. 35 | flag_stars = {} - for country, stars in(zip)(flag_stars.keys(), flag_stars.values()):... 36 + for country, stars in flag_stars.items():... +37 | +38 | # Regression test for https://github.com/astral-sh/ruff/issues/18778 +39 | d = {} From 6271fba1e1c01c9c49a360b1084217955145bce6 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Wed, 22 Oct 2025 15:24:34 +0900 Subject: [PATCH 007/188] [`ruff`] Auto generate ast Pattern nodes (#21024) --- crates/ruff_python_ast/ast.toml | 51 +- crates/ruff_python_ast/generate.py | 2 + crates/ruff_python_ast/src/generated.rs | 193 +++++++ crates/ruff_python_ast/src/node.rs | 114 ---- crates/ruff_python_ast/src/nodes.rs | 86 +-- ...alid_syntax@async_unexpected_token.py.snap | 2 +- ..._syntax@case_expect_indented_block.py.snap | 4 +- .../invalid_syntax@debug_shadow_match.py.snap | 2 +- ...x@different_match_pattern_bindings.py.snap | 150 ++--- ..._syntax@duplicate_match_class_attr.py.snap | 54 +- ...invalid_syntax@duplicate_match_key.py.snap | 100 ++-- ...id_syntax@irrefutable_case_pattern.py.snap | 22 +- .../invalid_syntax@match_before_py310.py.snap | 2 +- ...d_syntax@match_classify_as_keyword.py.snap | 2 +- ..._classify_as_keyword_or_identifier.py.snap | 2 +- ...nvalid_syntax@match_expected_colon.py.snap | 2 +- ...x@match_stmt_expect_indented_block.py.snap | 2 +- ...tax@match_stmt_expected_case_block.py.snap | 2 +- ...ntax@match_stmt_invalid_guard_expr.py.snap | 6 +- ...ax@match_stmt_invalid_subject_expr.py.snap | 6 +- ...ntax@match_stmt_missing_guard_expr.py.snap | 2 +- ..._syntax@match_stmt_missing_pattern.py.snap | 2 +- ...@match_stmt_no_newline_before_case.py.snap | 2 +- ...@match_stmt_single_starred_subject.py.snap | 2 +- ...ultiple_assignment_in_case_pattern.py.snap | 66 +-- ...ax@statements__match__as_pattern_0.py.snap | 6 +- ...ax@statements__match__as_pattern_1.py.snap | 2 +- ...ax@statements__match__as_pattern_2.py.snap | 4 +- ...ax@statements__match__as_pattern_3.py.snap | 6 +- ...ax@statements__match__as_pattern_4.py.snap | 6 +- ...ents__match__invalid_class_pattern.py.snap | 24 +- ..._match__invalid_lhs_or_rhs_pattern.py.snap | 36 +- ...ts__match__invalid_mapping_pattern.py.snap | 34 +- ...tements__match__star_pattern_usage.py.snap | 42 +- ...statements__match__unary_add_usage.py.snap | 30 +- ...ntax@class_keyword_in_case_pattern.py.snap | 4 +- ...x@different_match_pattern_bindings.py.snap | 60 +- ...id_syntax@duplicate_match_key_attr.py.snap | 6 +- ...valid_syntax@expressions__f_string.py.snap | 4 +- ...valid_syntax@expressions__t_string.py.snap | 4 +- ...ax@irrefutable_case_pattern_at_end.py.snap | 12 +- .../valid_syntax@match_after_py310.py.snap | 2 +- .../valid_syntax@match_as_pattern.py.snap | 4 +- ...ntax@match_as_pattern_soft_keyword.py.snap | 6 +- ...ax@match_attr_pattern_soft_keyword.py.snap | 8 +- ...syntax@match_classify_as_keyword_1.py.snap | 24 +- ...syntax@match_classify_as_keyword_2.py.snap | 12 +- ..._classify_as_keyword_or_identifier.py.snap | 6 +- ...nce_pattern_parentheses_terminator.py.snap | 12 +- ...@match_sequence_pattern_terminator.py.snap | 16 +- ...lid_syntax@match_stmt_subject_expr.py.snap | 8 +- ...syntax@match_stmt_valid_guard_expr.py.snap | 8 +- ...ultiple_assignment_in_case_pattern.py.snap | 12 +- .../valid_syntax@statement__match.py.snap | 542 +++++++++--------- 54 files changed, 928 insertions(+), 890 deletions(-) diff --git a/crates/ruff_python_ast/ast.toml b/crates/ruff_python_ast/ast.toml index 38a9415515..e2c6fd2844 100644 --- a/crates/ruff_python_ast/ast.toml +++ b/crates/ruff_python_ast/ast.toml @@ -559,15 +559,48 @@ InterpolatedStringLiteralElement = { variant = "Literal" } [Pattern] doc = "See also [pattern](https://docs.python.org/3/library/ast.html#ast.pattern)" -[Pattern.nodes] -PatternMatchValue = {} -PatternMatchSingleton = {} -PatternMatchSequence = {} -PatternMatchMapping = {} -PatternMatchClass = {} -PatternMatchStar = {} -PatternMatchAs = {} -PatternMatchOr = {} +[Pattern.nodes.PatternMatchValue] +doc = "See also [MatchValue](https://docs.python.org/3/library/ast.html#ast.MatchValue)" +fields = [{ name = "value", type = "Box" }] + +[Pattern.nodes.PatternMatchSingleton] +doc = "See also [MatchSingleton](https://docs.python.org/3/library/ast.html#ast.MatchSingleton)" +fields = [{ name = "value", type = "Singleton" }] + +[Pattern.nodes.PatternMatchSequence] +doc = "See also [MatchSequence](https://docs.python.org/3/library/ast.html#ast.MatchSequence)" +fields = [{ name = "patterns", type = "Pattern*" }] + +[Pattern.nodes.PatternMatchMapping] +doc = "See also [MatchMapping](https://docs.python.org/3/library/ast.html#ast.MatchMapping)" +fields = [ + { name = "keys", type = "Expr*" }, + { name = "patterns", type = "Pattern*" }, + { name = "rest", type = "Identifier?" }, +] +custom_source_order = true + +[Pattern.nodes.PatternMatchClass] +doc = "See also [MatchClass](https://docs.python.org/3/library/ast.html#ast.MatchClass)" +fields = [ + { name = "cls", type = "Box" }, + { name = "arguments", type = "PatternArguments" }, +] + +[Pattern.nodes.PatternMatchStar] +doc = "See also [MatchStar](https://docs.python.org/3/library/ast.html#ast.MatchStar)" +fields = [{ name = "name", type = "Identifier?" }] + +[Pattern.nodes.PatternMatchAs] +doc = "See also [MatchAs](https://docs.python.org/3/library/ast.html#ast.MatchAs)" +fields = [ + { name = "pattern", type = "Box?" }, + { name = "name", type = "Identifier?" }, +] + +[Pattern.nodes.PatternMatchOr] +doc = "See also [MatchOr](https://docs.python.org/3/library/ast.html#ast.MatchOr)" +fields = [{ name = "patterns", type = "Pattern*" }] [TypeParam] doc = "See also [type_param](https://docs.python.org/3/library/ast.html#ast.type_param)" diff --git a/crates/ruff_python_ast/generate.py b/crates/ruff_python_ast/generate.py index 2b8687b233..7ed8646f5b 100644 --- a/crates/ruff_python_ast/generate.py +++ b/crates/ruff_python_ast/generate.py @@ -38,6 +38,8 @@ types_requiring_crate_prefix = { "WithItem", "MatchCase", "Alias", + "Singleton", + "PatternArguments", } diff --git a/crates/ruff_python_ast/src/generated.rs b/crates/ruff_python_ast/src/generated.rs index 935595705d..8cf0ad9009 100644 --- a/crates/ruff_python_ast/src/generated.rs +++ b/crates/ruff_python_ast/src/generated.rs @@ -9637,6 +9637,82 @@ pub struct ExprIpyEscapeCommand { pub value: Box, } +/// See also [MatchValue](https://docs.python.org/3/library/ast.html#ast.MatchValue) +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct PatternMatchValue { + pub node_index: crate::AtomicNodeIndex, + pub range: ruff_text_size::TextRange, + pub value: Box, +} + +/// See also [MatchSingleton](https://docs.python.org/3/library/ast.html#ast.MatchSingleton) +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct PatternMatchSingleton { + pub node_index: crate::AtomicNodeIndex, + pub range: ruff_text_size::TextRange, + pub value: crate::Singleton, +} + +/// See also [MatchSequence](https://docs.python.org/3/library/ast.html#ast.MatchSequence) +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct PatternMatchSequence { + pub node_index: crate::AtomicNodeIndex, + pub range: ruff_text_size::TextRange, + pub patterns: Vec, +} + +/// See also [MatchMapping](https://docs.python.org/3/library/ast.html#ast.MatchMapping) +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct PatternMatchMapping { + pub node_index: crate::AtomicNodeIndex, + pub range: ruff_text_size::TextRange, + pub keys: Vec, + pub patterns: Vec, + pub rest: Option, +} + +/// See also [MatchClass](https://docs.python.org/3/library/ast.html#ast.MatchClass) +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct PatternMatchClass { + pub node_index: crate::AtomicNodeIndex, + pub range: ruff_text_size::TextRange, + pub cls: Box, + pub arguments: crate::PatternArguments, +} + +/// See also [MatchStar](https://docs.python.org/3/library/ast.html#ast.MatchStar) +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct PatternMatchStar { + pub node_index: crate::AtomicNodeIndex, + pub range: ruff_text_size::TextRange, + pub name: Option, +} + +/// See also [MatchAs](https://docs.python.org/3/library/ast.html#ast.MatchAs) +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct PatternMatchAs { + pub node_index: crate::AtomicNodeIndex, + pub range: ruff_text_size::TextRange, + pub pattern: Option>, + pub name: Option, +} + +/// See also [MatchOr](https://docs.python.org/3/library/ast.html#ast.MatchOr) +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct PatternMatchOr { + pub node_index: crate::AtomicNodeIndex, + pub range: ruff_text_size::TextRange, + pub patterns: Vec, +} + impl ModModule { pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) where @@ -10585,3 +10661,120 @@ impl ExprIpyEscapeCommand { } = self; } } + +impl PatternMatchValue { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let PatternMatchValue { + value, + range: _, + node_index: _, + } = self; + visitor.visit_expr(value); + } +} + +impl PatternMatchSingleton { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let PatternMatchSingleton { + value, + range: _, + node_index: _, + } = self; + visitor.visit_singleton(value); + } +} + +impl PatternMatchSequence { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let PatternMatchSequence { + patterns, + range: _, + node_index: _, + } = self; + + for elm in patterns { + visitor.visit_pattern(elm); + } + } +} + +impl PatternMatchClass { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let PatternMatchClass { + cls, + arguments, + range: _, + node_index: _, + } = self; + visitor.visit_expr(cls); + visitor.visit_pattern_arguments(arguments); + } +} + +impl PatternMatchStar { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let PatternMatchStar { + name, + range: _, + node_index: _, + } = self; + + if let Some(name) = name { + visitor.visit_identifier(name); + } + } +} + +impl PatternMatchAs { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let PatternMatchAs { + pattern, + name, + range: _, + node_index: _, + } = self; + + if let Some(pattern) = pattern { + visitor.visit_pattern(pattern); + } + + if let Some(name) = name { + visitor.visit_identifier(name); + } + } +} + +impl PatternMatchOr { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let PatternMatchOr { + patterns, + range: _, + node_index: _, + } = self; + + for elm in patterns { + visitor.visit_pattern(elm); + } + } +} diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index 58ad37925c..202315964d 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -235,50 +235,6 @@ impl ast::ExceptHandlerExceptHandler { } } -impl ast::PatternMatchValue { - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: SourceOrderVisitor<'a> + ?Sized, - { - let ast::PatternMatchValue { - value, - range: _, - node_index: _, - } = self; - visitor.visit_expr(value); - } -} - -impl ast::PatternMatchSingleton { - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: SourceOrderVisitor<'a> + ?Sized, - { - let ast::PatternMatchSingleton { - value, - range: _, - node_index: _, - } = self; - visitor.visit_singleton(value); - } -} - -impl ast::PatternMatchSequence { - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: SourceOrderVisitor<'a> + ?Sized, - { - let ast::PatternMatchSequence { - patterns, - range: _, - node_index: _, - } = self; - for pattern in patterns { - visitor.visit_pattern(pattern); - } - } -} - impl ast::PatternMatchMapping { pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) where @@ -311,76 +267,6 @@ impl ast::PatternMatchMapping { } } -impl ast::PatternMatchClass { - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: SourceOrderVisitor<'a> + ?Sized, - { - let ast::PatternMatchClass { - cls, - arguments: parameters, - range: _, - node_index: _, - } = self; - visitor.visit_expr(cls); - visitor.visit_pattern_arguments(parameters); - } -} - -impl ast::PatternMatchStar { - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: SourceOrderVisitor<'a> + ?Sized, - { - let ast::PatternMatchStar { - range: _, - node_index: _, - name, - } = self; - - if let Some(name) = name { - visitor.visit_identifier(name); - } - } -} - -impl ast::PatternMatchAs { - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: SourceOrderVisitor<'a> + ?Sized, - { - let ast::PatternMatchAs { - pattern, - range: _, - node_index: _, - name, - } = self; - if let Some(pattern) = pattern { - visitor.visit_pattern(pattern); - } - - if let Some(name) = name { - visitor.visit_identifier(name); - } - } -} - -impl ast::PatternMatchOr { - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: SourceOrderVisitor<'a> + ?Sized, - { - let ast::PatternMatchOr { - patterns, - range: _, - node_index: _, - } = self; - for pattern in patterns { - visitor.visit_pattern(pattern); - } - } -} - impl ast::PatternArguments { pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) where diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index 55c055e5bb..d26be06da5 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -3,7 +3,7 @@ use crate::AtomicNodeIndex; use crate::generated::{ ExprBytesLiteral, ExprDict, ExprFString, ExprList, ExprName, ExprSet, ExprStringLiteral, - ExprTString, ExprTuple, StmtClassDef, + ExprTString, ExprTuple, PatternMatchAs, PatternMatchOr, StmtClassDef, }; use std::borrow::Cow; use std::fmt; @@ -2848,58 +2848,10 @@ pub enum IrrefutablePatternKind { Wildcard, } -/// See also [MatchValue](https://docs.python.org/3/library/ast.html#ast.MatchValue) -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] -pub struct PatternMatchValue { - pub range: TextRange, - pub node_index: AtomicNodeIndex, - pub value: Box, -} - -/// See also [MatchSingleton](https://docs.python.org/3/library/ast.html#ast.MatchSingleton) -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] -pub struct PatternMatchSingleton { - pub range: TextRange, - pub node_index: AtomicNodeIndex, - pub value: Singleton, -} - -/// See also [MatchSequence](https://docs.python.org/3/library/ast.html#ast.MatchSequence) -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] -pub struct PatternMatchSequence { - pub range: TextRange, - pub node_index: AtomicNodeIndex, - pub patterns: Vec, -} - -/// See also [MatchMapping](https://docs.python.org/3/library/ast.html#ast.MatchMapping) -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] -pub struct PatternMatchMapping { - pub range: TextRange, - pub node_index: AtomicNodeIndex, - pub keys: Vec, - pub patterns: Vec, - pub rest: Option, -} - -/// See also [MatchClass](https://docs.python.org/3/library/ast.html#ast.MatchClass) -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] -pub struct PatternMatchClass { - pub range: TextRange, - pub node_index: AtomicNodeIndex, - pub cls: Box, - pub arguments: PatternArguments, -} - -/// An AST node to represent the arguments to a [`PatternMatchClass`], i.e., the +/// An AST node to represent the arguments to a [`crate::PatternMatchClass`], i.e., the /// parenthesized contents in `case Point(1, x=0, y=0)`. /// -/// Like [`Arguments`], but for [`PatternMatchClass`]. +/// Like [`Arguments`], but for [`crate::PatternMatchClass`]. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct PatternArguments { @@ -2909,10 +2861,10 @@ pub struct PatternArguments { pub keywords: Vec, } -/// An AST node to represent the keyword arguments to a [`PatternMatchClass`], i.e., the +/// An AST node to represent the keyword arguments to a [`crate::PatternMatchClass`], i.e., the /// `x=0` and `y=0` in `case Point(x=0, y=0)`. /// -/// Like [`Keyword`], but for [`PatternMatchClass`]. +/// Like [`Keyword`], but for [`crate::PatternMatchClass`]. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct PatternKeyword { @@ -2922,34 +2874,6 @@ pub struct PatternKeyword { pub pattern: Pattern, } -/// See also [MatchStar](https://docs.python.org/3/library/ast.html#ast.MatchStar) -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] -pub struct PatternMatchStar { - pub range: TextRange, - pub node_index: AtomicNodeIndex, - pub name: Option, -} - -/// See also [MatchAs](https://docs.python.org/3/library/ast.html#ast.MatchAs) -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] -pub struct PatternMatchAs { - pub range: TextRange, - pub node_index: AtomicNodeIndex, - pub pattern: Option>, - pub name: Option, -} - -/// See also [MatchOr](https://docs.python.org/3/library/ast.html#ast.MatchOr) -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] -pub struct PatternMatchOr { - pub range: TextRange, - pub node_index: AtomicNodeIndex, - pub patterns: Vec, -} - impl TypeParam { pub const fn name(&self) -> &Identifier { match self { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@async_unexpected_token.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@async_unexpected_token.py.snap index 7d5b8fd56a..2dd2bddfc4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@async_unexpected_token.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@async_unexpected_token.py.snap @@ -148,8 +148,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 109..110, node_index: NodeIndex(None), + range: 109..110, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@case_expect_indented_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@case_expect_indented_block.py.snap index 99c12452b7..952ba305f7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@case_expect_indented_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@case_expect_indented_block.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 24..25, node_index: NodeIndex(None), + range: 24..25, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -49,8 +49,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 36..37, node_index: NodeIndex(None), + range: 36..37, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_match.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_match.py.snap index 16b505fcf9..0fe9d38d82 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_match.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_match.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 18..27, node_index: NodeIndex(None), + range: 18..27, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@different_match_pattern_bindings.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@different_match_pattern_bindings.py.snap index a2ec2aa024..588dbe3cd8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@different_match_pattern_bindings.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@different_match_pattern_bindings.py.snap @@ -28,18 +28,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 18..27, node_index: NodeIndex(None), + range: 18..27, patterns: [ MatchSequence( PatternMatchSequence { - range: 18..21, node_index: NodeIndex(None), + range: 18..21, patterns: [ MatchAs( PatternMatchAs { - range: 19..20, node_index: NodeIndex(None), + range: 19..20, pattern: None, name: Some( Identifier { @@ -55,13 +55,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 24..27, node_index: NodeIndex(None), + range: 24..27, patterns: [ MatchAs( PatternMatchAs { - range: 25..26, node_index: NodeIndex(None), + range: 25..26, pattern: None, name: Some( Identifier { @@ -99,18 +99,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 42..50, node_index: NodeIndex(None), + range: 42..50, patterns: [ MatchSequence( PatternMatchSequence { - range: 42..45, node_index: NodeIndex(None), + range: 42..45, patterns: [ MatchAs( PatternMatchAs { - range: 43..44, node_index: NodeIndex(None), + range: 43..44, pattern: None, name: Some( Identifier { @@ -126,8 +126,8 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 48..50, node_index: NodeIndex(None), + range: 48..50, patterns: [], }, ), @@ -155,18 +155,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 65..78, node_index: NodeIndex(None), + range: 65..78, patterns: [ MatchSequence( PatternMatchSequence { - range: 65..71, node_index: NodeIndex(None), + range: 65..71, patterns: [ MatchAs( PatternMatchAs { - range: 66..67, node_index: NodeIndex(None), + range: 66..67, pattern: None, name: Some( Identifier { @@ -179,8 +179,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 69..70, node_index: NodeIndex(None), + range: 69..70, pattern: None, name: Some( Identifier { @@ -196,13 +196,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 74..78, node_index: NodeIndex(None), + range: 74..78, patterns: [ MatchAs( PatternMatchAs { - range: 75..76, node_index: NodeIndex(None), + range: 75..76, pattern: None, name: Some( Identifier { @@ -240,18 +240,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 93..108, node_index: NodeIndex(None), + range: 93..108, patterns: [ MatchSequence( PatternMatchSequence { - range: 93..99, node_index: NodeIndex(None), + range: 93..99, patterns: [ MatchAs( PatternMatchAs { - range: 94..95, node_index: NodeIndex(None), + range: 94..95, pattern: None, name: Some( Identifier { @@ -264,8 +264,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 97..98, node_index: NodeIndex(None), + range: 97..98, pattern: None, name: None, }, @@ -275,13 +275,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 102..108, node_index: NodeIndex(None), + range: 102..108, patterns: [ MatchAs( PatternMatchAs { - range: 103..104, node_index: NodeIndex(None), + range: 103..104, pattern: None, name: Some( Identifier { @@ -294,8 +294,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 106..107, node_index: NodeIndex(None), + range: 106..107, pattern: None, name: Some( Identifier { @@ -333,13 +333,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 123..135, node_index: NodeIndex(None), + range: 123..135, patterns: [ MatchAs( PatternMatchAs { - range: 124..125, node_index: NodeIndex(None), + range: 124..125, pattern: None, name: Some( Identifier { @@ -352,13 +352,13 @@ Module( ), MatchOr( PatternMatchOr { - range: 128..133, node_index: NodeIndex(None), + range: 128..133, patterns: [ MatchAs( PatternMatchAs { - range: 128..129, node_index: NodeIndex(None), + range: 128..129, pattern: None, name: Some( Identifier { @@ -371,8 +371,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 132..133, node_index: NodeIndex(None), + range: 132..133, pattern: None, name: Some( Identifier { @@ -410,18 +410,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 150..165, node_index: NodeIndex(None), + range: 150..165, patterns: [ MatchSequence( PatternMatchSequence { - range: 150..153, node_index: NodeIndex(None), + range: 150..153, patterns: [ MatchAs( PatternMatchAs { - range: 151..152, node_index: NodeIndex(None), + range: 151..152, pattern: None, name: Some( Identifier { @@ -437,13 +437,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 156..159, node_index: NodeIndex(None), + range: 156..159, patterns: [ MatchAs( PatternMatchAs { - range: 157..158, node_index: NodeIndex(None), + range: 157..158, pattern: None, name: Some( Identifier { @@ -459,13 +459,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 162..165, node_index: NodeIndex(None), + range: 162..165, patterns: [ MatchAs( PatternMatchAs { - range: 163..164, node_index: NodeIndex(None), + range: 163..164, pattern: None, name: Some( Identifier { @@ -503,25 +503,25 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 180..188, node_index: NodeIndex(None), + range: 180..188, patterns: [ MatchSequence( PatternMatchSequence { - range: 180..182, node_index: NodeIndex(None), + range: 180..182, patterns: [], }, ), MatchSequence( PatternMatchSequence { - range: 185..188, node_index: NodeIndex(None), + range: 185..188, patterns: [ MatchAs( PatternMatchAs { - range: 186..187, node_index: NodeIndex(None), + range: 186..187, pattern: None, name: Some( Identifier { @@ -559,18 +559,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 203..215, node_index: NodeIndex(None), + range: 203..215, patterns: [ MatchSequence( PatternMatchSequence { - range: 203..206, node_index: NodeIndex(None), + range: 203..206, patterns: [ MatchAs( PatternMatchAs { - range: 204..205, node_index: NodeIndex(None), + range: 204..205, pattern: None, name: Some( Identifier { @@ -586,13 +586,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 209..215, node_index: NodeIndex(None), + range: 209..215, patterns: [ MatchClass( PatternMatchClass { - range: 210..214, node_index: NodeIndex(None), + range: 210..214, cls: Name( ExprName { node_index: NodeIndex(None), @@ -607,8 +607,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 212..213, node_index: NodeIndex(None), + range: 212..213, pattern: None, name: Some( Identifier { @@ -651,23 +651,23 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 230..241, node_index: NodeIndex(None), + range: 230..241, patterns: [ MatchOr( PatternMatchOr { - range: 231..240, node_index: NodeIndex(None), + range: 231..240, patterns: [ MatchSequence( PatternMatchSequence { - range: 231..234, node_index: NodeIndex(None), + range: 231..234, patterns: [ MatchAs( PatternMatchAs { - range: 232..233, node_index: NodeIndex(None), + range: 232..233, pattern: None, name: Some( Identifier { @@ -683,13 +683,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 237..240, node_index: NodeIndex(None), + range: 237..240, patterns: [ MatchAs( PatternMatchAs { - range: 238..239, node_index: NodeIndex(None), + range: 238..239, pattern: None, name: Some( Identifier { @@ -730,18 +730,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 256..271, node_index: NodeIndex(None), + range: 256..271, patterns: [ MatchSequence( PatternMatchSequence { - range: 256..262, node_index: NodeIndex(None), + range: 256..262, patterns: [ MatchClass( PatternMatchClass { - range: 257..261, node_index: NodeIndex(None), + range: 257..261, cls: Name( ExprName { node_index: NodeIndex(None), @@ -756,8 +756,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 259..260, node_index: NodeIndex(None), + range: 259..260, pattern: None, name: Some( Identifier { @@ -778,13 +778,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 265..271, node_index: NodeIndex(None), + range: 265..271, patterns: [ MatchClass( PatternMatchClass { - range: 266..270, node_index: NodeIndex(None), + range: 266..270, cls: Name( ExprName { node_index: NodeIndex(None), @@ -799,8 +799,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 268..269, node_index: NodeIndex(None), + range: 268..269, pattern: None, name: Some( Identifier { @@ -843,18 +843,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 286..307, node_index: NodeIndex(None), + range: 286..307, patterns: [ MatchSequence( PatternMatchSequence { - range: 286..295, node_index: NodeIndex(None), + range: 286..295, patterns: [ MatchClass( PatternMatchClass { - range: 287..294, node_index: NodeIndex(None), + range: 287..294, cls: Name( ExprName { node_index: NodeIndex(None), @@ -869,8 +869,8 @@ Module( patterns: [ MatchClass( PatternMatchClass { - range: 289..293, node_index: NodeIndex(None), + range: 289..293, cls: Name( ExprName { node_index: NodeIndex(None), @@ -885,8 +885,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 291..292, node_index: NodeIndex(None), + range: 291..292, pattern: None, name: Some( Identifier { @@ -912,13 +912,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 298..307, node_index: NodeIndex(None), + range: 298..307, patterns: [ MatchClass( PatternMatchClass { - range: 299..306, node_index: NodeIndex(None), + range: 299..306, cls: Name( ExprName { node_index: NodeIndex(None), @@ -933,8 +933,8 @@ Module( patterns: [ MatchClass( PatternMatchClass { - range: 301..305, node_index: NodeIndex(None), + range: 301..305, cls: Name( ExprName { node_index: NodeIndex(None), @@ -949,8 +949,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 303..304, node_index: NodeIndex(None), + range: 303..304, pattern: None, name: Some( Identifier { @@ -998,23 +998,23 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 322..341, node_index: NodeIndex(None), + range: 322..341, patterns: [ MatchSequence( PatternMatchSequence { - range: 322..330, node_index: NodeIndex(None), + range: 322..330, patterns: [ MatchSequence( PatternMatchSequence { - range: 323..329, node_index: NodeIndex(None), + range: 323..329, patterns: [ MatchAs( PatternMatchAs { - range: 324..325, node_index: NodeIndex(None), + range: 324..325, pattern: None, name: Some( Identifier { @@ -1027,8 +1027,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 327..328, node_index: NodeIndex(None), + range: 327..328, pattern: None, name: Some( Identifier { @@ -1047,18 +1047,18 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 333..341, node_index: NodeIndex(None), + range: 333..341, patterns: [ MatchSequence( PatternMatchSequence { - range: 334..340, node_index: NodeIndex(None), + range: 334..340, patterns: [ MatchAs( PatternMatchAs { - range: 335..336, node_index: NodeIndex(None), + range: 335..336, pattern: None, name: Some( Identifier { @@ -1071,8 +1071,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 338..339, node_index: NodeIndex(None), + range: 338..339, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_class_attr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_class_attr.py.snap index 63067a4b3c..83e2d732da 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_class_attr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_class_attr.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 18..33, node_index: NodeIndex(None), + range: 18..33, cls: Name( ExprName { node_index: NodeIndex(None), @@ -53,8 +53,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 26..27, node_index: NodeIndex(None), + range: 26..27, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -77,8 +77,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 31..32, node_index: NodeIndex(None), + range: 31..32, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -116,13 +116,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 48..65, node_index: NodeIndex(None), + range: 48..65, patterns: [ MatchClass( PatternMatchClass { - range: 49..64, node_index: NodeIndex(None), + range: 49..64, cls: Name( ExprName { node_index: NodeIndex(None), @@ -146,8 +146,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 57..58, node_index: NodeIndex(None), + range: 57..58, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -170,8 +170,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 62..63, node_index: NodeIndex(None), + range: 62..63, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -212,8 +212,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 80..108, node_index: NodeIndex(None), + range: 80..108, keys: [ StringLiteral( ExprStringLiteral { @@ -261,8 +261,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 86..87, node_index: NodeIndex(None), + range: 86..87, pattern: None, name: Some( Identifier { @@ -275,8 +275,8 @@ Module( ), MatchClass( PatternMatchClass { - range: 94..107, node_index: NodeIndex(None), + range: 94..107, cls: Name( ExprName { node_index: NodeIndex(None), @@ -300,8 +300,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 100..101, node_index: NodeIndex(None), + range: 100..101, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -324,8 +324,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 105..106, node_index: NodeIndex(None), + range: 105..106, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -367,13 +367,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 123..157, node_index: NodeIndex(None), + range: 123..157, patterns: [ MatchMapping( PatternMatchMapping { - range: 124..126, node_index: NodeIndex(None), + range: 124..126, keys: [], patterns: [], rest: None, @@ -381,8 +381,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 128..156, node_index: NodeIndex(None), + range: 128..156, keys: [ StringLiteral( ExprStringLiteral { @@ -430,8 +430,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 134..135, node_index: NodeIndex(None), + range: 134..135, pattern: None, name: Some( Identifier { @@ -444,8 +444,8 @@ Module( ), MatchClass( PatternMatchClass { - range: 142..155, node_index: NodeIndex(None), + range: 142..155, cls: Name( ExprName { node_index: NodeIndex(None), @@ -469,8 +469,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 148..149, node_index: NodeIndex(None), + range: 148..149, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -493,8 +493,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 153..154, node_index: NodeIndex(None), + range: 153..154, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -539,8 +539,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 172..225, node_index: NodeIndex(None), + range: 172..225, cls: Name( ExprName { node_index: NodeIndex(None), @@ -564,8 +564,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 180..181, node_index: NodeIndex(None), + range: 180..181, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -588,8 +588,8 @@ Module( }, pattern: MatchMapping( PatternMatchMapping { - range: 185..201, node_index: NodeIndex(None), + range: 185..201, keys: [ StringLiteral( ExprStringLiteral { @@ -637,8 +637,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 191..192, node_index: NodeIndex(None), + range: 191..192, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -652,8 +652,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 199..200, node_index: NodeIndex(None), + range: 199..200, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -680,8 +680,8 @@ Module( }, pattern: MatchClass( PatternMatchClass { - range: 209..224, node_index: NodeIndex(None), + range: 209..224, cls: Name( ExprName { node_index: NodeIndex(None), @@ -705,8 +705,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 217..218, node_index: NodeIndex(None), + range: 217..218, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -729,8 +729,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 222..223, node_index: NodeIndex(None), + range: 222..223, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_key.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_key.py.snap index aec92ae959..9ebc6f8748 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_key.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_key.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 18..34, node_index: NodeIndex(None), + range: 18..34, keys: [ StringLiteral( ExprStringLiteral { @@ -77,8 +77,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 24..25, node_index: NodeIndex(None), + range: 24..25, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -92,8 +92,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 32..33, node_index: NodeIndex(None), + range: 32..33, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -130,8 +130,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 49..67, node_index: NodeIndex(None), + range: 49..67, keys: [ BytesLiteral( ExprBytesLiteral { @@ -183,8 +183,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 56..57, node_index: NodeIndex(None), + range: 56..57, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -198,8 +198,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 65..66, node_index: NodeIndex(None), + range: 65..66, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -236,8 +236,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 82..94, node_index: NodeIndex(None), + range: 82..94, keys: [ NumberLiteral( ExprNumberLiteral { @@ -261,8 +261,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 86..87, node_index: NodeIndex(None), + range: 86..87, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -276,8 +276,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 92..93, node_index: NodeIndex(None), + range: 92..93, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -314,8 +314,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 109..125, node_index: NodeIndex(None), + range: 109..125, keys: [ NumberLiteral( ExprNumberLiteral { @@ -339,8 +339,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 115..116, node_index: NodeIndex(None), + range: 115..116, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -354,8 +354,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 123..124, node_index: NodeIndex(None), + range: 123..124, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -392,8 +392,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 140..166, node_index: NodeIndex(None), + range: 140..166, keys: [ BinOp( ExprBinOp { @@ -451,8 +451,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 151..152, node_index: NodeIndex(None), + range: 151..152, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -466,8 +466,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 164..165, node_index: NodeIndex(None), + range: 164..165, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -504,8 +504,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 181..199, node_index: NodeIndex(None), + range: 181..199, keys: [ BooleanLiteral( ExprBooleanLiteral { @@ -525,8 +525,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 188..189, node_index: NodeIndex(None), + range: 188..189, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -540,8 +540,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 197..198, node_index: NodeIndex(None), + range: 197..198, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -578,8 +578,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 214..232, node_index: NodeIndex(None), + range: 214..232, keys: [ NoneLiteral( ExprNoneLiteral { @@ -597,8 +597,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 221..222, node_index: NodeIndex(None), + range: 221..222, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -612,8 +612,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 230..231, node_index: NodeIndex(None), + range: 230..231, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -650,8 +650,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 247..314, node_index: NodeIndex(None), + range: 247..314, keys: [ StringLiteral( ExprStringLiteral { @@ -699,8 +699,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 279..280, node_index: NodeIndex(None), + range: 279..280, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -714,8 +714,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 312..313, node_index: NodeIndex(None), + range: 312..313, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -752,8 +752,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 329..353, node_index: NodeIndex(None), + range: 329..353, keys: [ StringLiteral( ExprStringLiteral { @@ -822,8 +822,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 335..336, node_index: NodeIndex(None), + range: 335..336, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -837,8 +837,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 343..344, node_index: NodeIndex(None), + range: 343..344, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -852,8 +852,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 351..352, node_index: NodeIndex(None), + range: 351..352, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -890,8 +890,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 368..396, node_index: NodeIndex(None), + range: 368..396, keys: [ NumberLiteral( ExprNumberLiteral { @@ -957,8 +957,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 372..373, node_index: NodeIndex(None), + range: 372..373, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -972,8 +972,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 380..381, node_index: NodeIndex(None), + range: 380..381, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -987,8 +987,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 386..387, node_index: NodeIndex(None), + range: 386..387, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1002,8 +1002,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 394..395, node_index: NodeIndex(None), + range: 394..395, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1040,13 +1040,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 411..429, node_index: NodeIndex(None), + range: 411..429, patterns: [ MatchMapping( PatternMatchMapping { - range: 412..428, node_index: NodeIndex(None), + range: 412..428, keys: [ StringLiteral( ExprStringLiteral { @@ -1094,8 +1094,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 418..419, node_index: NodeIndex(None), + range: 418..419, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1109,8 +1109,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 426..427, node_index: NodeIndex(None), + range: 426..427, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1150,8 +1150,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 444..472, node_index: NodeIndex(None), + range: 444..472, cls: Name( ExprName { node_index: NodeIndex(None), @@ -1175,8 +1175,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 450..451, node_index: NodeIndex(None), + range: 450..451, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1199,8 +1199,8 @@ Module( }, pattern: MatchMapping( PatternMatchMapping { - range: 455..471, node_index: NodeIndex(None), + range: 455..471, keys: [ StringLiteral( ExprStringLiteral { @@ -1248,8 +1248,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 461..462, node_index: NodeIndex(None), + range: 461..462, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1263,8 +1263,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 469..470, node_index: NodeIndex(None), + range: 469..470, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1306,13 +1306,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 487..527, node_index: NodeIndex(None), + range: 487..527, patterns: [ MatchClass( PatternMatchClass { - range: 488..496, node_index: NodeIndex(None), + range: 488..496, cls: Name( ExprName { node_index: NodeIndex(None), @@ -1336,8 +1336,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 494..495, node_index: NodeIndex(None), + range: 494..495, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1356,8 +1356,8 @@ Module( ), MatchClass( PatternMatchClass { - range: 498..526, node_index: NodeIndex(None), + range: 498..526, cls: Name( ExprName { node_index: NodeIndex(None), @@ -1381,8 +1381,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 504..505, node_index: NodeIndex(None), + range: 504..505, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1405,8 +1405,8 @@ Module( }, pattern: MatchMapping( PatternMatchMapping { - range: 509..525, node_index: NodeIndex(None), + range: 509..525, keys: [ StringLiteral( ExprStringLiteral { @@ -1454,8 +1454,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 515..516, node_index: NodeIndex(None), + range: 515..516, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1469,8 +1469,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 523..524, node_index: NodeIndex(None), + range: 523..524, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@irrefutable_case_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@irrefutable_case_pattern.py.snap index 00d25b2f52..e92cdb2db1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@irrefutable_case_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@irrefutable_case_pattern.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 18..21, node_index: NodeIndex(None), + range: 18..21, pattern: None, name: Some( Identifier { @@ -61,8 +61,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 55..56, node_index: NodeIndex(None), + range: 55..56, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -111,8 +111,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 80..81, node_index: NodeIndex(None), + range: 80..81, pattern: None, name: None, }, @@ -138,8 +138,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 96..97, node_index: NodeIndex(None), + range: 96..97, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -188,13 +188,13 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 143..155, node_index: NodeIndex(None), + range: 143..155, pattern: Some( MatchAs( PatternMatchAs { - range: 143..147, node_index: NodeIndex(None), + range: 143..147, pattern: None, name: Some( Identifier { @@ -236,8 +236,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 216..217, node_index: NodeIndex(None), + range: 216..217, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -286,13 +286,13 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 241..259, node_index: NodeIndex(None), + range: 241..259, patterns: [ MatchValue( PatternMatchValue { - range: 241..253, node_index: NodeIndex(None), + range: 241..253, value: Attribute( ExprAttribute { node_index: NodeIndex(None), @@ -317,8 +317,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 256..259, node_index: NodeIndex(None), + range: 256..259, pattern: None, name: Some( Identifier { @@ -353,8 +353,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 310..311, node_index: NodeIndex(None), + range: 310..311, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_before_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_before_py310.py.snap index ee6bbd4bd0..391c87863e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_before_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_before_py310.py.snap @@ -29,8 +29,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 63..64, node_index: NodeIndex(None), + range: 63..64, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword.py.snap index f30f13e7be..47f410992c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword.py.snap @@ -36,8 +36,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 26..27, node_index: NodeIndex(None), + range: 26..27, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword_or_identifier.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword_or_identifier.py.snap index 531219c2b7..4696b90ac2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword_or_identifier.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword_or_identifier.py.snap @@ -35,8 +35,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 32..33, node_index: NodeIndex(None), + range: 32..33, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_expected_colon.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_expected_colon.py.snap index a3b0a5de38..f352512262 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_expected_colon.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_expected_colon.py.snap @@ -47,8 +47,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 22..23, node_index: NodeIndex(None), + range: 22..23, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expect_indented_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expect_indented_block.py.snap index 3f165716fd..60781de936 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expect_indented_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expect_indented_block.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 16..17, node_index: NodeIndex(None), + range: 16..17, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expected_case_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expected_case_block.py.snap index 491b242611..8aaaa11d01 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expected_case_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expected_case_block.py.snap @@ -83,8 +83,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 54..55, node_index: NodeIndex(None), + range: 54..55, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_guard_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_guard_expr.py.snap index 73bce3400e..967dfc2d2e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_guard_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_guard_expr.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 18..19, node_index: NodeIndex(None), + range: 18..19, pattern: None, name: Some( Identifier { @@ -93,8 +93,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 49..50, node_index: NodeIndex(None), + range: 49..50, pattern: None, name: Some( Identifier { @@ -158,8 +158,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 82..83, node_index: NodeIndex(None), + range: 82..83, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_subject_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_subject_expr.py.snap index 1db01735fb..537bf08d6b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_subject_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_subject_expr.py.snap @@ -35,8 +35,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 21..22, node_index: NodeIndex(None), + range: 21..22, pattern: None, name: None, }, @@ -120,8 +120,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 93..94, node_index: NodeIndex(None), + range: 93..94, pattern: None, name: None, }, @@ -171,8 +171,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 124..125, node_index: NodeIndex(None), + range: 124..125, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_guard_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_guard_expr.py.snap index ebaf34464b..bf66acae17 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_guard_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_guard_expr.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 18..19, node_index: NodeIndex(None), + range: 18..19, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_pattern.py.snap index ddc7872be1..84fad76972 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_pattern.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 17..17, node_index: NodeIndex(None), + range: 17..17, value: Name( ExprName { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap index b904a97ae4..324f3480ff 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 16..17, node_index: NodeIndex(None), + range: 16..17, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_single_starred_subject.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_single_starred_subject.py.snap index 8ad5469d41..67fa5c7067 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_single_starred_subject.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_single_starred_subject.py.snap @@ -35,8 +35,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 21..22, node_index: NodeIndex(None), + range: 21..22, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap index fbedd8e0ee..3aed113557 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap @@ -29,13 +29,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 18..27, node_index: NodeIndex(None), + range: 18..27, patterns: [ MatchAs( PatternMatchAs { - range: 19..20, node_index: NodeIndex(None), + range: 19..20, pattern: None, name: Some( Identifier { @@ -48,8 +48,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 22..23, node_index: NodeIndex(None), + range: 22..23, pattern: None, name: Some( Identifier { @@ -62,8 +62,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 25..26, node_index: NodeIndex(None), + range: 25..26, pattern: None, name: Some( Identifier { @@ -98,13 +98,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 59..69, node_index: NodeIndex(None), + range: 59..69, patterns: [ MatchAs( PatternMatchAs { - range: 60..61, node_index: NodeIndex(None), + range: 60..61, pattern: None, name: Some( Identifier { @@ -117,8 +117,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 63..64, node_index: NodeIndex(None), + range: 63..64, pattern: None, name: Some( Identifier { @@ -131,8 +131,8 @@ Module( ), MatchStar( PatternMatchStar { - range: 66..68, node_index: NodeIndex(None), + range: 66..68, name: Some( Identifier { id: Name("y"), @@ -166,13 +166,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 101..110, node_index: NodeIndex(None), + range: 101..110, patterns: [ MatchAs( PatternMatchAs { - range: 102..103, node_index: NodeIndex(None), + range: 102..103, pattern: None, name: Some( Identifier { @@ -185,8 +185,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 105..106, node_index: NodeIndex(None), + range: 105..106, pattern: None, name: Some( Identifier { @@ -199,8 +199,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 108..109, node_index: NodeIndex(None), + range: 108..109, pattern: None, name: Some( Identifier { @@ -235,8 +235,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 151..163, node_index: NodeIndex(None), + range: 151..163, keys: [ NumberLiteral( ExprNumberLiteral { @@ -260,8 +260,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 155..156, node_index: NodeIndex(None), + range: 155..156, pattern: None, name: Some( Identifier { @@ -274,8 +274,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 161..162, node_index: NodeIndex(None), + range: 161..162, pattern: None, name: Some( Identifier { @@ -311,8 +311,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 212..223, node_index: NodeIndex(None), + range: 212..223, keys: [ NumberLiteral( ExprNumberLiteral { @@ -327,8 +327,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 216..217, node_index: NodeIndex(None), + range: 216..217, pattern: None, name: Some( Identifier { @@ -370,8 +370,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 274..285, node_index: NodeIndex(None), + range: 274..285, cls: Name( ExprName { node_index: NodeIndex(None), @@ -386,8 +386,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 280..281, node_index: NodeIndex(None), + range: 280..281, pattern: None, name: Some( Identifier { @@ -400,8 +400,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 283..284, node_index: NodeIndex(None), + range: 283..284, pattern: None, name: Some( Identifier { @@ -438,8 +438,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 325..340, node_index: NodeIndex(None), + range: 325..340, cls: Name( ExprName { node_index: NodeIndex(None), @@ -463,8 +463,8 @@ Module( }, pattern: MatchAs( PatternMatchAs { - range: 333..334, node_index: NodeIndex(None), + range: 333..334, pattern: None, name: Some( Identifier { @@ -486,8 +486,8 @@ Module( }, pattern: MatchAs( PatternMatchAs { - range: 338..339, node_index: NodeIndex(None), + range: 338..339, pattern: None, name: Some( Identifier { @@ -524,18 +524,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 377..407, node_index: NodeIndex(None), + range: 377..407, patterns: [ MatchSequence( PatternMatchSequence { - range: 377..380, node_index: NodeIndex(None), + range: 377..380, patterns: [ MatchAs( PatternMatchAs { - range: 378..379, node_index: NodeIndex(None), + range: 378..379, pattern: None, name: Some( Identifier { @@ -551,8 +551,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 383..389, node_index: NodeIndex(None), + range: 383..389, keys: [ NumberLiteral( ExprNumberLiteral { @@ -567,8 +567,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 387..388, node_index: NodeIndex(None), + range: 387..388, pattern: None, name: Some( Identifier { @@ -585,8 +585,8 @@ Module( ), MatchClass( PatternMatchClass { - range: 392..407, node_index: NodeIndex(None), + range: 392..407, cls: Name( ExprName { node_index: NodeIndex(None), @@ -610,8 +610,8 @@ Module( }, pattern: MatchAs( PatternMatchAs { - range: 400..401, node_index: NodeIndex(None), + range: 400..401, pattern: None, name: Some( Identifier { @@ -633,8 +633,8 @@ Module( }, pattern: MatchAs( PatternMatchAs { - range: 405..406, node_index: NodeIndex(None), + range: 405..406, pattern: None, name: Some( Identifier { @@ -674,13 +674,13 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 433..439, node_index: NodeIndex(None), + range: 433..439, pattern: Some( MatchAs( PatternMatchAs { - range: 433..434, node_index: NodeIndex(None), + range: 433..434, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_0.py.snap index 27c349f705..d03bfe08c8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_0.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 132..146, node_index: NodeIndex(None), + range: 132..146, cls: Name( ExprName { node_index: NodeIndex(None), @@ -44,8 +44,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 141..142, node_index: NodeIndex(None), + range: 141..142, pattern: None, name: Some( Identifier { @@ -58,8 +58,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 144..145, node_index: NodeIndex(None), + range: 144..145, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_1.py.snap index 4ef2b99dcf..34809d2560 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_1.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 145..158, node_index: NodeIndex(None), + range: 145..158, value: BinOp( ExprBinOp { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap index b649717bab..34e3500f26 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap @@ -28,13 +28,13 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 164..170, node_index: NodeIndex(None), + range: 164..170, pattern: Some( MatchAs( PatternMatchAs { - range: 164..165, node_index: NodeIndex(None), + range: 164..165, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap index 74014165fd..07cd1a64e6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 108..117, node_index: NodeIndex(None), + range: 108..117, cls: Dict( ExprDict { node_index: NodeIndex(None), @@ -43,13 +43,13 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 110..116, node_index: NodeIndex(None), + range: 110..116, pattern: Some( MatchAs( PatternMatchAs { - range: 110..111, node_index: NodeIndex(None), + range: 110..111, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_4.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_4.py.snap index 4ec3c13511..771706eaed 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_4.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_4.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 161..172, node_index: NodeIndex(None), + range: 161..172, keys: [ Name( ExprName { @@ -51,8 +51,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 164..166, node_index: NodeIndex(None), + range: 164..166, pattern: None, name: Some( Identifier { @@ -65,8 +65,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 170..171, node_index: NodeIndex(None), + range: 170..171, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_class_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_class_pattern.py.snap index e5f771dfd8..67b1874089 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_class_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_class_pattern.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 68..83, node_index: NodeIndex(None), + range: 68..83, cls: Name( ExprName { node_index: NodeIndex(None), @@ -53,8 +53,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 81..82, node_index: NodeIndex(None), + range: 81..82, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -86,8 +86,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 107..121, node_index: NodeIndex(None), + range: 107..121, cls: Name( ExprName { node_index: NodeIndex(None), @@ -111,8 +111,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 119..120, node_index: NodeIndex(None), + range: 119..120, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -144,8 +144,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 145..160, node_index: NodeIndex(None), + range: 145..160, cls: Name( ExprName { node_index: NodeIndex(None), @@ -169,8 +169,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 158..159, node_index: NodeIndex(None), + range: 158..159, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -202,8 +202,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 184..203, node_index: NodeIndex(None), + range: 184..203, cls: Name( ExprName { node_index: NodeIndex(None), @@ -227,8 +227,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 201..202, node_index: NodeIndex(None), + range: 201..202, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -260,8 +260,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 227..235, node_index: NodeIndex(None), + range: 227..235, cls: Name( ExprName { node_index: NodeIndex(None), @@ -285,8 +285,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 233..234, node_index: NodeIndex(None), + range: 233..234, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -318,8 +318,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 259..271, node_index: NodeIndex(None), + range: 259..271, cls: Name( ExprName { node_index: NodeIndex(None), @@ -343,8 +343,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 269..270, node_index: NodeIndex(None), + range: 269..270, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_lhs_or_rhs_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_lhs_or_rhs_pattern.py.snap index 978a1633e1..4f3a4512a1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_lhs_or_rhs_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_lhs_or_rhs_pattern.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 36..46, node_index: NodeIndex(None), + range: 36..46, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -84,8 +84,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 70..76, node_index: NodeIndex(None), + range: 70..76, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -128,8 +128,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 100..106, node_index: NodeIndex(None), + range: 100..106, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -172,8 +172,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 130..142, node_index: NodeIndex(None), + range: 130..142, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -233,8 +233,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 166..177, node_index: NodeIndex(None), + range: 166..177, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -296,8 +296,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 201..215, node_index: NodeIndex(None), + range: 201..215, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -360,8 +360,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 239..246, node_index: NodeIndex(None), + range: 239..246, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -406,8 +406,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 270..278, node_index: NodeIndex(None), + range: 270..278, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -459,8 +459,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 302..318, node_index: NodeIndex(None), + range: 302..318, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -540,8 +540,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 370..379, node_index: NodeIndex(None), + range: 370..379, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -595,8 +595,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 403..408, node_index: NodeIndex(None), + range: 403..408, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -638,8 +638,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 432..437, node_index: NodeIndex(None), + range: 432..437, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -681,8 +681,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 461..472, node_index: NodeIndex(None), + range: 461..472, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -741,8 +741,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 496..506, node_index: NodeIndex(None), + range: 496..506, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -803,8 +803,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 530..543, node_index: NodeIndex(None), + range: 530..543, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -866,8 +866,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 567..572, node_index: NodeIndex(None), + range: 567..572, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -910,8 +910,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 596..611, node_index: NodeIndex(None), + range: 596..611, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -990,8 +990,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 667..680, node_index: NodeIndex(None), + range: 667..680, value: BinOp( ExprBinOp { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap index 67b66ad2ea..5592524488 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 85..91, node_index: NodeIndex(None), + range: 85..91, keys: [ Starred( ExprStarred { @@ -50,8 +50,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 90..90, node_index: NodeIndex(None), + range: 90..90, value: Name( ExprName { node_index: NodeIndex(None), @@ -81,8 +81,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 115..124, node_index: NodeIndex(None), + range: 115..124, keys: [ Starred( ExprStarred { @@ -103,8 +103,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 122..123, node_index: NodeIndex(None), + range: 122..123, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -135,8 +135,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 148..156, node_index: NodeIndex(None), + range: 148..156, keys: [ Starred( ExprStarred { @@ -157,8 +157,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 154..155, node_index: NodeIndex(None), + range: 154..155, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -189,8 +189,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 180..195, node_index: NodeIndex(None), + range: 180..195, keys: [ Starred( ExprStarred { @@ -217,8 +217,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 185..185, node_index: NodeIndex(None), + range: 185..185, value: Name( ExprName { node_index: NodeIndex(None), @@ -231,8 +231,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 193..194, node_index: NodeIndex(None), + range: 193..194, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -279,8 +279,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 329..346, node_index: NodeIndex(None), + range: 329..346, keys: [ NoneLiteral( ExprNoneLiteral { @@ -292,8 +292,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 344..345, node_index: NodeIndex(None), + range: 344..345, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -330,8 +330,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 370..397, node_index: NodeIndex(None), + range: 370..397, keys: [ NoneLiteral( ExprNoneLiteral { @@ -343,8 +343,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 395..396, node_index: NodeIndex(None), + range: 395..396, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -381,8 +381,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 421..448, node_index: NodeIndex(None), + range: 421..448, keys: [ NoneLiteral( ExprNoneLiteral { @@ -394,8 +394,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 437..438, node_index: NodeIndex(None), + range: 437..438, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -448,8 +448,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 488..504, node_index: NodeIndex(None), + range: 488..504, keys: [ Call( ExprCall { @@ -484,8 +484,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 502..503, node_index: NodeIndex(None), + range: 502..503, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__star_pattern_usage.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__star_pattern_usage.py.snap index 8db3ef5b5b..364d382ff2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__star_pattern_usage.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__star_pattern_usage.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchStar( PatternMatchStar { - range: 81..83, node_index: NodeIndex(None), + range: 81..83, name: None, }, ), @@ -48,13 +48,13 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 107..114, node_index: NodeIndex(None), + range: 107..114, pattern: Some( MatchStar( PatternMatchStar { - range: 107..109, node_index: NodeIndex(None), + range: 107..109, name: None, }, ), @@ -83,8 +83,8 @@ Module( node_index: NodeIndex(None), pattern: MatchStar( PatternMatchStar { - range: 138..142, node_index: NodeIndex(None), + range: 138..142, name: Some( Identifier { id: Name("foo"), @@ -109,13 +109,13 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 166..174, node_index: NodeIndex(None), + range: 166..174, patterns: [ MatchStar( PatternMatchStar { - range: 166..170, node_index: NodeIndex(None), + range: 166..170, name: Some( Identifier { id: Name("foo"), @@ -127,8 +127,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 173..174, node_index: NodeIndex(None), + range: 173..174, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -158,13 +158,13 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 198..206, node_index: NodeIndex(None), + range: 198..206, patterns: [ MatchValue( PatternMatchValue { - range: 198..199, node_index: NodeIndex(None), + range: 198..199, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -178,8 +178,8 @@ Module( ), MatchStar( PatternMatchStar { - range: 202..206, node_index: NodeIndex(None), + range: 202..206, name: Some( Identifier { id: Name("foo"), @@ -207,8 +207,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 230..237, node_index: NodeIndex(None), + range: 230..237, cls: Name( ExprName { node_index: NodeIndex(None), @@ -223,8 +223,8 @@ Module( patterns: [ MatchStar( PatternMatchStar { - range: 234..236, node_index: NodeIndex(None), + range: 234..236, name: None, }, ), @@ -248,8 +248,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 261..270, node_index: NodeIndex(None), + range: 261..270, cls: Name( ExprName { node_index: NodeIndex(None), @@ -273,8 +273,8 @@ Module( }, pattern: MatchStar( PatternMatchStar { - range: 267..269, node_index: NodeIndex(None), + range: 267..269, name: None, }, ), @@ -298,8 +298,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 294..298, node_index: NodeIndex(None), + range: 294..298, keys: [ Starred( ExprStarred { @@ -320,8 +320,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 297..297, node_index: NodeIndex(None), + range: 297..297, value: Name( ExprName { node_index: NodeIndex(None), @@ -351,8 +351,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 322..329, node_index: NodeIndex(None), + range: 322..329, keys: [ Starred( ExprStarred { @@ -373,8 +373,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 327..328, node_index: NodeIndex(None), + range: 327..328, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -405,8 +405,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 353..363, node_index: NodeIndex(None), + range: 353..363, keys: [ NoneLiteral( ExprNoneLiteral { @@ -418,8 +418,8 @@ Module( patterns: [ MatchStar( PatternMatchStar { - range: 360..362, node_index: NodeIndex(None), + range: 360..362, name: None, }, ), @@ -442,8 +442,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 387..393, node_index: NodeIndex(None), + range: 387..393, value: BinOp( ExprBinOp { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__unary_add_usage.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__unary_add_usage.py.snap index fe42f9c289..2fcc8cad93 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__unary_add_usage.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__unary_add_usage.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 98..100, node_index: NodeIndex(None), + range: 98..100, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -63,13 +63,13 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 124..135, node_index: NodeIndex(None), + range: 124..135, patterns: [ MatchValue( PatternMatchValue { - range: 124..125, node_index: NodeIndex(None), + range: 124..125, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -83,8 +83,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 128..130, node_index: NodeIndex(None), + range: 128..130, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -105,8 +105,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 133..135, node_index: NodeIndex(None), + range: 133..135, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -143,13 +143,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 159..170, node_index: NodeIndex(None), + range: 159..170, patterns: [ MatchValue( PatternMatchValue { - range: 160..161, node_index: NodeIndex(None), + range: 160..161, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -163,8 +163,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 163..165, node_index: NodeIndex(None), + range: 163..165, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -185,8 +185,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 167..169, node_index: NodeIndex(None), + range: 167..169, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -223,8 +223,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 194..209, node_index: NodeIndex(None), + range: 194..209, cls: Name( ExprName { node_index: NodeIndex(None), @@ -248,8 +248,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 200..202, node_index: NodeIndex(None), + range: 200..202, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -279,8 +279,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 206..208, node_index: NodeIndex(None), + range: 206..208, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -319,8 +319,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 233..254, node_index: NodeIndex(None), + range: 233..254, keys: [ BooleanLiteral( ExprBooleanLiteral { @@ -340,8 +340,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 240..242, node_index: NodeIndex(None), + range: 240..242, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -362,8 +362,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 251..253, node_index: NodeIndex(None), + range: 251..253, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_keyword_in_case_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_keyword_in_case_pattern.py.snap index 12e1125f1a..a732e866b3 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_keyword_in_case_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_keyword_in_case_pattern.py.snap @@ -29,8 +29,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 18..28, node_index: NodeIndex(None), + range: 18..28, cls: Name( ExprName { node_index: NodeIndex(None), @@ -54,8 +54,8 @@ Module( }, pattern: MatchAs( PatternMatchAs { - range: 26..27, node_index: NodeIndex(None), + range: 26..27, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@different_match_pattern_bindings.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@different_match_pattern_bindings.py.snap index 8f7ec2412e..2705218022 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@different_match_pattern_bindings.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@different_match_pattern_bindings.py.snap @@ -28,18 +28,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 18..27, node_index: NodeIndex(None), + range: 18..27, patterns: [ MatchSequence( PatternMatchSequence { - range: 18..21, node_index: NodeIndex(None), + range: 18..21, patterns: [ MatchAs( PatternMatchAs { - range: 19..20, node_index: NodeIndex(None), + range: 19..20, pattern: None, name: Some( Identifier { @@ -55,13 +55,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 24..27, node_index: NodeIndex(None), + range: 24..27, patterns: [ MatchAs( PatternMatchAs { - range: 25..26, node_index: NodeIndex(None), + range: 25..26, pattern: None, name: Some( Identifier { @@ -99,18 +99,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 42..57, node_index: NodeIndex(None), + range: 42..57, patterns: [ MatchSequence( PatternMatchSequence { - range: 42..48, node_index: NodeIndex(None), + range: 42..48, patterns: [ MatchAs( PatternMatchAs { - range: 43..44, node_index: NodeIndex(None), + range: 43..44, pattern: None, name: Some( Identifier { @@ -123,8 +123,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 46..47, node_index: NodeIndex(None), + range: 46..47, pattern: None, name: Some( Identifier { @@ -140,13 +140,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 51..57, node_index: NodeIndex(None), + range: 51..57, patterns: [ MatchAs( PatternMatchAs { - range: 52..53, node_index: NodeIndex(None), + range: 52..53, pattern: None, name: Some( Identifier { @@ -159,8 +159,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 55..56, node_index: NodeIndex(None), + range: 55..56, pattern: None, name: Some( Identifier { @@ -198,13 +198,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 72..84, node_index: NodeIndex(None), + range: 72..84, patterns: [ MatchAs( PatternMatchAs { - range: 73..74, node_index: NodeIndex(None), + range: 73..74, pattern: None, name: Some( Identifier { @@ -217,13 +217,13 @@ Module( ), MatchOr( PatternMatchOr { - range: 77..82, node_index: NodeIndex(None), + range: 77..82, patterns: [ MatchAs( PatternMatchAs { - range: 77..78, node_index: NodeIndex(None), + range: 77..78, pattern: None, name: Some( Identifier { @@ -236,8 +236,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 81..82, node_index: NodeIndex(None), + range: 81..82, pattern: None, name: Some( Identifier { @@ -275,18 +275,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 99..114, node_index: NodeIndex(None), + range: 99..114, patterns: [ MatchSequence( PatternMatchSequence { - range: 99..105, node_index: NodeIndex(None), + range: 99..105, patterns: [ MatchAs( PatternMatchAs { - range: 100..101, node_index: NodeIndex(None), + range: 100..101, pattern: None, name: Some( Identifier { @@ -299,8 +299,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 103..104, node_index: NodeIndex(None), + range: 103..104, pattern: None, name: None, }, @@ -310,13 +310,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 108..114, node_index: NodeIndex(None), + range: 108..114, patterns: [ MatchAs( PatternMatchAs { - range: 109..110, node_index: NodeIndex(None), + range: 109..110, pattern: None, name: Some( Identifier { @@ -329,8 +329,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 112..113, node_index: NodeIndex(None), + range: 112..113, pattern: None, name: None, }, @@ -362,18 +362,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 129..141, node_index: NodeIndex(None), + range: 129..141, patterns: [ MatchSequence( PatternMatchSequence { - range: 129..132, node_index: NodeIndex(None), + range: 129..132, patterns: [ MatchAs( PatternMatchAs { - range: 130..131, node_index: NodeIndex(None), + range: 130..131, pattern: None, name: Some( Identifier { @@ -389,13 +389,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 135..141, node_index: NodeIndex(None), + range: 135..141, patterns: [ MatchClass( PatternMatchClass { - range: 136..140, node_index: NodeIndex(None), + range: 136..140, cls: Name( ExprName { node_index: NodeIndex(None), @@ -410,8 +410,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 138..139, node_index: NodeIndex(None), + range: 138..139, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@duplicate_match_key_attr.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@duplicate_match_key_attr.py.snap index bb0ad14135..e090cde339 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@duplicate_match_key_attr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@duplicate_match_key_attr.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 18..34, node_index: NodeIndex(None), + range: 18..34, keys: [ Attribute( ExprAttribute { @@ -75,8 +75,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 24..25, node_index: NodeIndex(None), + range: 24..25, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -90,8 +90,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 32..33, node_index: NodeIndex(None), + range: 32..33, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap index e696d1c34a..a7a0f0c925 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap @@ -1077,8 +1077,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 274..279, node_index: NodeIndex(None), + range: 274..279, value: StringLiteral( ExprStringLiteral { node_index: NodeIndex(None), @@ -1117,8 +1117,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 303..331, node_index: NodeIndex(None), + range: 303..331, value: StringLiteral( ExprStringLiteral { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap index 0896a3ae47..ad30629e3a 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap @@ -1053,8 +1053,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 276..281, node_index: NodeIndex(None), + range: 276..281, value: StringLiteral( ExprStringLiteral { node_index: NodeIndex(None), @@ -1093,8 +1093,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 305..333, node_index: NodeIndex(None), + range: 305..333, value: StringLiteral( ExprStringLiteral { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@irrefutable_case_pattern_at_end.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@irrefutable_case_pattern_at_end.py.snap index 355e84ac03..ee291df133 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@irrefutable_case_pattern_at_end.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@irrefutable_case_pattern_at_end.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 18..19, node_index: NodeIndex(None), + range: 18..19, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -62,8 +62,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 34..37, node_index: NodeIndex(None), + range: 34..37, pattern: None, name: Some( Identifier { @@ -111,8 +111,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 61..62, node_index: NodeIndex(None), + range: 61..62, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -145,8 +145,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 77..78, node_index: NodeIndex(None), + range: 77..78, pattern: None, name: None, }, @@ -188,8 +188,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 102..105, node_index: NodeIndex(None), + range: 102..105, pattern: None, name: Some( Identifier { @@ -229,8 +229,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 169..170, node_index: NodeIndex(None), + range: 169..170, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_after_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_after_py310.py.snap index 155afee96f..8169fc8654 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_after_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_after_py310.py.snap @@ -29,8 +29,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 64..65, node_index: NodeIndex(None), + range: 64..65, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern.py.snap index f69f8a5947..2ae3ea1e81 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 20..27, node_index: NodeIndex(None), + range: 20..27, pattern: None, name: Some( Identifier { @@ -77,8 +77,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 53..54, node_index: NodeIndex(None), + range: 53..54, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern_soft_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern_soft_keyword.py.snap index 7795d1b45c..1d32832797 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern_soft_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern_soft_keyword.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 20..24, node_index: NodeIndex(None), + range: 20..24, pattern: None, name: Some( Identifier { @@ -77,8 +77,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 50..55, node_index: NodeIndex(None), + range: 50..55, pattern: None, name: Some( Identifier { @@ -126,8 +126,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 81..85, node_index: NodeIndex(None), + range: 81..85, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_attr_pattern_soft_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_attr_pattern_soft_keyword.py.snap index 0f911c5ce4..5050a08ea5 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_attr_pattern_soft_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_attr_pattern_soft_keyword.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 20..29, node_index: NodeIndex(None), + range: 20..29, value: Attribute( ExprAttribute { node_index: NodeIndex(None), @@ -73,8 +73,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 44..52, node_index: NodeIndex(None), + range: 44..52, value: Attribute( ExprAttribute { node_index: NodeIndex(None), @@ -118,8 +118,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 67..75, node_index: NodeIndex(None), + range: 67..75, value: Attribute( ExprAttribute { node_index: NodeIndex(None), @@ -163,8 +163,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 90..125, node_index: NodeIndex(None), + range: 90..125, value: Attribute( ExprAttribute { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap index e7e54f42bb..3ff055535e 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 20..21, node_index: NodeIndex(None), + range: 20..21, pattern: None, name: None, }, @@ -72,8 +72,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 45..46, node_index: NodeIndex(None), + range: 45..46, pattern: None, name: None, }, @@ -116,8 +116,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 72..73, node_index: NodeIndex(None), + range: 72..73, pattern: None, name: None, }, @@ -161,8 +161,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 98..99, node_index: NodeIndex(None), + range: 98..99, pattern: None, name: None, }, @@ -217,8 +217,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 127..128, node_index: NodeIndex(None), + range: 127..128, pattern: None, name: None, }, @@ -300,8 +300,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 161..162, node_index: NodeIndex(None), + range: 161..162, pattern: None, name: None, }, @@ -361,8 +361,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 191..192, node_index: NodeIndex(None), + range: 191..192, pattern: None, name: None, }, @@ -411,8 +411,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 219..220, node_index: NodeIndex(None), + range: 219..220, pattern: None, name: None, }, @@ -452,8 +452,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 246..247, node_index: NodeIndex(None), + range: 246..247, pattern: None, name: None, }, @@ -502,8 +502,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 277..278, node_index: NodeIndex(None), + range: 277..278, pattern: None, name: None, }, @@ -563,8 +563,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 312..313, node_index: NodeIndex(None), + range: 312..313, pattern: None, name: None, }, @@ -639,8 +639,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 351..352, node_index: NodeIndex(None), + range: 351..352, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_2.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_2.py.snap index 16ff76beb6..84a0f8c747 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_2.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 22..23, node_index: NodeIndex(None), + range: 22..23, pattern: None, name: None, }, @@ -71,8 +71,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 50..51, node_index: NodeIndex(None), + range: 50..51, pattern: None, name: None, }, @@ -114,8 +114,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 78..79, node_index: NodeIndex(None), + range: 78..79, pattern: None, name: None, }, @@ -155,8 +155,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 106..107, node_index: NodeIndex(None), + range: 106..107, pattern: None, name: None, }, @@ -197,8 +197,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 134..135, node_index: NodeIndex(None), + range: 134..135, pattern: None, name: None, }, @@ -239,8 +239,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 163..164, node_index: NodeIndex(None), + range: 163..164, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_or_identifier.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_or_identifier.py.snap index 71fa711fb7..40eb8aafef 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_or_identifier.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_or_identifier.py.snap @@ -93,8 +93,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 61..62, node_index: NodeIndex(None), + range: 61..62, pattern: None, name: None, }, @@ -195,8 +195,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 127..128, node_index: NodeIndex(None), + range: 127..128, pattern: None, name: None, }, @@ -303,8 +303,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 218..219, node_index: NodeIndex(None), + range: 218..219, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_parentheses_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_parentheses_terminator.py.snap index 4abf3285a6..4cc16363d2 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_parentheses_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_parentheses_terminator.py.snap @@ -28,13 +28,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 24..30, node_index: NodeIndex(None), + range: 24..30, patterns: [ MatchAs( PatternMatchAs { - range: 25..26, node_index: NodeIndex(None), + range: 25..26, pattern: None, name: Some( Identifier { @@ -47,8 +47,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 28..29, node_index: NodeIndex(None), + range: 28..29, pattern: None, name: Some( Identifier { @@ -83,13 +83,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 45..51, node_index: NodeIndex(None), + range: 45..51, patterns: [ MatchAs( PatternMatchAs { - range: 46..47, node_index: NodeIndex(None), + range: 46..47, pattern: None, name: Some( Identifier { @@ -102,8 +102,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 49..50, node_index: NodeIndex(None), + range: 49..50, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_terminator.py.snap index d980cea8b2..361c36bae4 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_terminator.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 24..25, node_index: NodeIndex(None), + range: 24..25, pattern: None, name: Some( Identifier { @@ -70,13 +70,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 45..49, node_index: NodeIndex(None), + range: 45..49, patterns: [ MatchAs( PatternMatchAs { - range: 45..46, node_index: NodeIndex(None), + range: 45..46, pattern: None, name: Some( Identifier { @@ -89,8 +89,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 48..49, node_index: NodeIndex(None), + range: 48..49, pattern: None, name: Some( Identifier { @@ -125,13 +125,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 64..68, node_index: NodeIndex(None), + range: 64..68, patterns: [ MatchAs( PatternMatchAs { - range: 64..65, node_index: NodeIndex(None), + range: 64..65, pattern: None, name: Some( Identifier { @@ -144,8 +144,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 67..68, node_index: NodeIndex(None), + range: 67..68, pattern: None, name: Some( Identifier { @@ -189,8 +189,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 88..89, node_index: NodeIndex(None), + range: 88..89, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_subject_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_subject_expr.py.snap index 43db08647c..de49fc65c0 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_subject_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_subject_expr.py.snap @@ -43,8 +43,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 23..24, node_index: NodeIndex(None), + range: 23..24, pattern: None, name: None, }, @@ -101,8 +101,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 55..56, node_index: NodeIndex(None), + range: 55..56, pattern: None, name: None, }, @@ -184,8 +184,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 147..148, node_index: NodeIndex(None), + range: 147..148, pattern: None, name: None, }, @@ -233,8 +233,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 178..179, node_index: NodeIndex(None), + range: 178..179, pattern: None, name: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_valid_guard_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_valid_guard_expr.py.snap index ed559fabac..643a0bac5b 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_valid_guard_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_valid_guard_expr.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 18..19, node_index: NodeIndex(None), + range: 18..19, pattern: None, name: Some( Identifier { @@ -101,8 +101,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 53..54, node_index: NodeIndex(None), + range: 53..54, pattern: None, name: Some( Identifier { @@ -180,8 +180,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 98..99, node_index: NodeIndex(None), + range: 98..99, pattern: None, name: Some( Identifier { @@ -271,8 +271,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 138..139, node_index: NodeIndex(None), + range: 138..139, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_assignment_in_case_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_assignment_in_case_pattern.py.snap index 1f7090f2c9..a37747203d 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_assignment_in_case_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_assignment_in_case_pattern.py.snap @@ -29,13 +29,13 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 18..36, node_index: NodeIndex(None), + range: 18..36, patterns: [ MatchClass( PatternMatchClass { - range: 18..26, node_index: NodeIndex(None), + range: 18..26, cls: Name( ExprName { node_index: NodeIndex(None), @@ -50,8 +50,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 24..25, node_index: NodeIndex(None), + range: 24..25, pattern: None, name: Some( Identifier { @@ -69,13 +69,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 29..32, node_index: NodeIndex(None), + range: 29..32, patterns: [ MatchAs( PatternMatchAs { - range: 30..31, node_index: NodeIndex(None), + range: 30..31, pattern: None, name: Some( Identifier { @@ -91,8 +91,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 35..36, node_index: NodeIndex(None), + range: 35..36, pattern: None, name: Some( Identifier { diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap index e10babe037..496319ea71 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap @@ -28,8 +28,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 85..88, node_index: NodeIndex(None), + range: 85..88, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -99,8 +99,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 144..152, node_index: NodeIndex(None), + range: 144..152, cls: Name( ExprName { node_index: NodeIndex(None), @@ -115,8 +115,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 150..151, node_index: NodeIndex(None), + range: 150..151, pattern: None, name: Some( Identifier { @@ -182,8 +182,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 208..209, node_index: NodeIndex(None), + range: 208..209, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -239,8 +239,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 239..240, node_index: NodeIndex(None), + range: 239..240, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -313,13 +313,13 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 301..314, node_index: NodeIndex(None), + range: 301..314, patterns: [ MatchValue( PatternMatchValue { - range: 301..302, node_index: NodeIndex(None), + range: 301..302, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -333,8 +333,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 305..306, node_index: NodeIndex(None), + range: 305..306, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -348,8 +348,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 309..310, node_index: NodeIndex(None), + range: 309..310, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -363,8 +363,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 313..314, node_index: NodeIndex(None), + range: 313..314, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -427,18 +427,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 373..388, node_index: NodeIndex(None), + range: 373..388, patterns: [ MatchSequence( PatternMatchSequence { - range: 373..379, node_index: NodeIndex(None), + range: 373..379, patterns: [ MatchValue( PatternMatchValue { - range: 374..375, node_index: NodeIndex(None), + range: 374..375, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -452,8 +452,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 377..378, node_index: NodeIndex(None), + range: 377..378, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -470,13 +470,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 382..388, node_index: NodeIndex(None), + range: 382..388, patterns: [ MatchValue( PatternMatchValue { - range: 383..384, node_index: NodeIndex(None), + range: 383..384, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -490,8 +490,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 386..387, node_index: NodeIndex(None), + range: 386..387, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -559,13 +559,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 463..467, node_index: NodeIndex(None), + range: 463..467, patterns: [ MatchStar( PatternMatchStar { - range: 464..466, node_index: NodeIndex(None), + range: 464..466, name: None, }, ), @@ -610,8 +610,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 499..501, node_index: NodeIndex(None), + range: 499..501, keys: [], patterns: [], rest: None, @@ -671,8 +671,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 564..579, node_index: NodeIndex(None), + range: 564..579, keys: [ NumberLiteral( ExprNumberLiteral { @@ -687,13 +687,13 @@ Module( patterns: [ MatchSequence( PatternMatchSequence { - range: 568..578, node_index: NodeIndex(None), + range: 568..578, patterns: [ MatchValue( PatternMatchValue { - range: 569..570, node_index: NodeIndex(None), + range: 569..570, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -707,8 +707,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 572..573, node_index: NodeIndex(None), + range: 572..573, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -722,8 +722,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 575..577, node_index: NodeIndex(None), + range: 575..577, keys: [], patterns: [], rest: None, @@ -770,13 +770,13 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 604..672, node_index: NodeIndex(None), + range: 604..672, patterns: [ MatchMapping( PatternMatchMapping { - range: 604..626, node_index: NodeIndex(None), + range: 604..626, keys: [ NumberLiteral( ExprNumberLiteral { @@ -791,18 +791,18 @@ Module( patterns: [ MatchOr( PatternMatchOr { - range: 608..625, node_index: NodeIndex(None), + range: 608..625, patterns: [ MatchSequence( PatternMatchSequence { - range: 608..618, node_index: NodeIndex(None), + range: 608..618, patterns: [ MatchValue( PatternMatchValue { - range: 609..610, node_index: NodeIndex(None), + range: 609..610, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -816,8 +816,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 612..613, node_index: NodeIndex(None), + range: 612..613, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -831,8 +831,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 615..617, node_index: NodeIndex(None), + range: 615..617, keys: [], patterns: [], rest: None, @@ -843,8 +843,8 @@ Module( ), MatchSingleton( PatternMatchSingleton { - range: 621..625, node_index: NodeIndex(None), + range: 621..625, value: True, }, ), @@ -857,8 +857,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 629..638, node_index: NodeIndex(None), + range: 629..638, keys: [ NumberLiteral( ExprNumberLiteral { @@ -873,13 +873,13 @@ Module( patterns: [ MatchSequence( PatternMatchSequence { - range: 633..637, node_index: NodeIndex(None), + range: 633..637, patterns: [ MatchSequence( PatternMatchSequence { - range: 634..636, node_index: NodeIndex(None), + range: 634..636, patterns: [], }, ), @@ -892,8 +892,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 641..656, node_index: NodeIndex(None), + range: 641..656, keys: [ NumberLiteral( ExprNumberLiteral { @@ -908,13 +908,13 @@ Module( patterns: [ MatchSequence( PatternMatchSequence { - range: 645..655, node_index: NodeIndex(None), + range: 645..655, patterns: [ MatchValue( PatternMatchValue { - range: 646..647, node_index: NodeIndex(None), + range: 646..647, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -928,8 +928,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 649..650, node_index: NodeIndex(None), + range: 649..650, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -943,8 +943,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 652..654, node_index: NodeIndex(None), + range: 652..654, keys: [], patterns: [], rest: None, @@ -959,15 +959,15 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 659..661, node_index: NodeIndex(None), + range: 659..661, patterns: [], }, ), MatchValue( PatternMatchValue { - range: 664..667, node_index: NodeIndex(None), + range: 664..667, value: StringLiteral( ExprStringLiteral { node_index: NodeIndex(None), @@ -993,8 +993,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 670..672, node_index: NodeIndex(None), + range: 670..672, keys: [], patterns: [], rest: None, @@ -1037,8 +1037,8 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 697..699, node_index: NodeIndex(None), + range: 697..699, patterns: [], }, ), @@ -1092,8 +1092,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 755..767, node_index: NodeIndex(None), + range: 755..767, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -1172,8 +1172,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 823..826, node_index: NodeIndex(None), + range: 823..826, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -1244,13 +1244,13 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 882..895, node_index: NodeIndex(None), + range: 882..895, patterns: [ MatchValue( PatternMatchValue { - range: 882..883, node_index: NodeIndex(None), + range: 882..883, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1264,8 +1264,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 886..887, node_index: NodeIndex(None), + range: 886..887, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1279,8 +1279,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 890..891, node_index: NodeIndex(None), + range: 890..891, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1294,8 +1294,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 894..895, node_index: NodeIndex(None), + range: 894..895, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1358,8 +1358,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 954..955, node_index: NodeIndex(None), + range: 954..955, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1430,8 +1430,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 1016..1022, node_index: NodeIndex(None), + range: 1016..1022, keys: [ NumberLiteral( ExprNumberLiteral { @@ -1446,8 +1446,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 1020..1021, node_index: NodeIndex(None), + range: 1020..1021, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1497,8 +1497,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 1047..1053, node_index: NodeIndex(None), + range: 1047..1053, keys: [ NumberLiteral( ExprNumberLiteral { @@ -1513,8 +1513,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 1051..1052, node_index: NodeIndex(None), + range: 1051..1052, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1564,8 +1564,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 1078..1083, node_index: NodeIndex(None), + range: 1078..1083, keys: [], patterns: [], rest: Some( @@ -1639,13 +1639,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 1143..1147, node_index: NodeIndex(None), + range: 1143..1147, patterns: [ MatchStar( PatternMatchStar { - range: 1144..1146, node_index: NodeIndex(None), + range: 1144..1146, name: None, }, ), @@ -1702,8 +1702,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 1203..1204, node_index: NodeIndex(None), + range: 1203..1204, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1749,8 +1749,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 1229..1230, node_index: NodeIndex(None), + range: 1229..1230, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1812,8 +1812,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 1286..1298, node_index: NodeIndex(None), + range: 1286..1298, keys: [ StringLiteral( ExprStringLiteral { @@ -1840,8 +1840,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 1294..1297, node_index: NodeIndex(None), + range: 1294..1297, pattern: None, name: Some( Identifier { @@ -1934,13 +1934,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 1364..1377, node_index: NodeIndex(None), + range: 1364..1377, patterns: [ MatchValue( PatternMatchValue { - range: 1365..1366, node_index: NodeIndex(None), + range: 1365..1366, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1954,8 +1954,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 1368..1369, node_index: NodeIndex(None), + range: 1368..1369, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -1969,8 +1969,8 @@ Module( ), MatchStar( PatternMatchStar { - range: 1371..1373, node_index: NodeIndex(None), + range: 1371..1373, name: Some( Identifier { id: Name("x"), @@ -1982,8 +1982,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 1375..1376, node_index: NodeIndex(None), + range: 1375..1376, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -2048,13 +2048,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 1433..1436, node_index: NodeIndex(None), + range: 1433..1436, patterns: [ MatchValue( PatternMatchValue { - range: 1434..1435, node_index: NodeIndex(None), + range: 1434..1435, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -2103,13 +2103,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 1461..1467, node_index: NodeIndex(None), + range: 1461..1467, patterns: [ MatchValue( PatternMatchValue { - range: 1462..1463, node_index: NodeIndex(None), + range: 1462..1463, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -2123,8 +2123,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 1465..1466, node_index: NodeIndex(None), + range: 1465..1466, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -2222,13 +2222,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 1508..1514, node_index: NodeIndex(None), + range: 1508..1514, patterns: [ MatchValue( PatternMatchValue { - range: 1509..1510, node_index: NodeIndex(None), + range: 1509..1510, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -2242,8 +2242,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 1512..1513, node_index: NodeIndex(None), + range: 1512..1513, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -2308,13 +2308,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 1570..1580, node_index: NodeIndex(None), + range: 1570..1580, patterns: [ MatchAs( PatternMatchAs { - range: 1571..1572, node_index: NodeIndex(None), + range: 1571..1572, pattern: None, name: Some( Identifier { @@ -2327,8 +2327,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 1574..1575, node_index: NodeIndex(None), + range: 1574..1575, pattern: None, name: Some( Identifier { @@ -2341,8 +2341,8 @@ Module( ), MatchStar( PatternMatchStar { - range: 1577..1579, node_index: NodeIndex(None), + range: 1577..1579, name: None, }, ), @@ -2399,8 +2399,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 1636..1649, node_index: NodeIndex(None), + range: 1636..1649, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -2496,13 +2496,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 1708..1711, node_index: NodeIndex(None), + range: 1708..1711, patterns: [ MatchAs( PatternMatchAs { - range: 1709..1710, node_index: NodeIndex(None), + range: 1709..1710, pattern: None, name: Some( Identifier { @@ -2566,8 +2566,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 1767..1774, node_index: NodeIndex(None), + range: 1767..1774, value: Attribute( ExprAttribute { node_index: NodeIndex(None), @@ -2664,8 +2664,8 @@ Module( node_index: NodeIndex(None), pattern: MatchSingleton( PatternMatchSingleton { - range: 1830..1834, node_index: NodeIndex(None), + range: 1830..1834, value: None, }, ), @@ -2719,8 +2719,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 1890..1891, node_index: NodeIndex(None), + range: 1890..1891, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -2782,8 +2782,8 @@ Module( node_index: NodeIndex(None), pattern: MatchSingleton( PatternMatchSingleton { - range: 1947..1952, node_index: NodeIndex(None), + range: 1947..1952, value: False, }, ), @@ -2837,8 +2837,8 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 2008..2010, node_index: NodeIndex(None), + range: 2008..2010, patterns: [], }, ), @@ -2876,13 +2876,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 2035..2039, node_index: NodeIndex(None), + range: 2035..2039, patterns: [ MatchValue( PatternMatchValue { - range: 2036..2038, node_index: NodeIndex(None), + range: 2036..2038, value: StringLiteral( ExprStringLiteral { node_index: NodeIndex(None), @@ -2943,8 +2943,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 2064..2066, node_index: NodeIndex(None), + range: 2064..2066, value: StringLiteral( ExprStringLiteral { node_index: NodeIndex(None), @@ -3018,8 +3018,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 2122..2123, node_index: NodeIndex(None), + range: 2122..2123, pattern: None, name: Some( Identifier { @@ -3080,13 +3080,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 2179..2192, node_index: NodeIndex(None), + range: 2179..2192, patterns: [ MatchAs( PatternMatchAs { - range: 2180..2181, node_index: NodeIndex(None), + range: 2180..2181, pattern: None, name: Some( Identifier { @@ -3099,8 +3099,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 2183..2184, node_index: NodeIndex(None), + range: 2183..2184, pattern: None, name: Some( Identifier { @@ -3113,8 +3113,8 @@ Module( ), MatchStar( PatternMatchStar { - range: 2186..2191, node_index: NodeIndex(None), + range: 2186..2191, name: Some( Identifier { id: Name("rest"), @@ -3177,18 +3177,18 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 2248..2278, node_index: NodeIndex(None), + range: 2248..2278, patterns: [ MatchAs( PatternMatchAs { - range: 2249..2255, node_index: NodeIndex(None), + range: 2249..2255, pattern: Some( MatchValue( PatternMatchValue { - range: 2249..2250, node_index: NodeIndex(None), + range: 2249..2250, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -3212,13 +3212,13 @@ Module( ), MatchAs( PatternMatchAs { - range: 2260..2266, node_index: NodeIndex(None), + range: 2260..2266, pattern: Some( MatchValue( PatternMatchValue { - range: 2260..2261, node_index: NodeIndex(None), + range: 2260..2261, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -3242,13 +3242,13 @@ Module( ), MatchAs( PatternMatchAs { - range: 2271..2277, node_index: NodeIndex(None), + range: 2271..2277, pattern: Some( MatchValue( PatternMatchValue { - range: 2271..2272, node_index: NodeIndex(None), + range: 2271..2272, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -3367,8 +3367,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 2348..2363, node_index: NodeIndex(None), + range: 2348..2363, keys: [ NumberLiteral( ExprNumberLiteral { @@ -3383,13 +3383,13 @@ Module( patterns: [ MatchSequence( PatternMatchSequence { - range: 2352..2362, node_index: NodeIndex(None), + range: 2352..2362, patterns: [ MatchValue( PatternMatchValue { - range: 2353..2354, node_index: NodeIndex(None), + range: 2353..2354, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -3403,8 +3403,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 2356..2357, node_index: NodeIndex(None), + range: 2356..2357, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -3418,8 +3418,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 2359..2361, node_index: NodeIndex(None), + range: 2359..2361, keys: [], patterns: [], rest: None, @@ -3466,13 +3466,13 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 2388..2457, node_index: NodeIndex(None), + range: 2388..2457, patterns: [ MatchMapping( PatternMatchMapping { - range: 2388..2411, node_index: NodeIndex(None), + range: 2388..2411, keys: [ NumberLiteral( ExprNumberLiteral { @@ -3487,18 +3487,18 @@ Module( patterns: [ MatchOr( PatternMatchOr { - range: 2392..2410, node_index: NodeIndex(None), + range: 2392..2410, patterns: [ MatchSequence( PatternMatchSequence { - range: 2392..2402, node_index: NodeIndex(None), + range: 2392..2402, patterns: [ MatchValue( PatternMatchValue { - range: 2393..2394, node_index: NodeIndex(None), + range: 2393..2394, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -3512,8 +3512,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 2396..2397, node_index: NodeIndex(None), + range: 2396..2397, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -3527,8 +3527,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 2399..2401, node_index: NodeIndex(None), + range: 2399..2401, keys: [], patterns: [], rest: None, @@ -3539,8 +3539,8 @@ Module( ), MatchSingleton( PatternMatchSingleton { - range: 2405..2410, node_index: NodeIndex(None), + range: 2405..2410, value: False, }, ), @@ -3553,8 +3553,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 2414..2423, node_index: NodeIndex(None), + range: 2414..2423, keys: [ NumberLiteral( ExprNumberLiteral { @@ -3569,13 +3569,13 @@ Module( patterns: [ MatchSequence( PatternMatchSequence { - range: 2418..2422, node_index: NodeIndex(None), + range: 2418..2422, patterns: [ MatchSequence( PatternMatchSequence { - range: 2419..2421, node_index: NodeIndex(None), + range: 2419..2421, patterns: [], }, ), @@ -3588,8 +3588,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 2426..2441, node_index: NodeIndex(None), + range: 2426..2441, keys: [ NumberLiteral( ExprNumberLiteral { @@ -3604,13 +3604,13 @@ Module( patterns: [ MatchSequence( PatternMatchSequence { - range: 2430..2440, node_index: NodeIndex(None), + range: 2430..2440, patterns: [ MatchValue( PatternMatchValue { - range: 2431..2432, node_index: NodeIndex(None), + range: 2431..2432, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -3624,8 +3624,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 2434..2435, node_index: NodeIndex(None), + range: 2434..2435, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -3639,8 +3639,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 2437..2439, node_index: NodeIndex(None), + range: 2437..2439, keys: [], patterns: [], rest: None, @@ -3655,15 +3655,15 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 2444..2446, node_index: NodeIndex(None), + range: 2444..2446, patterns: [], }, ), MatchValue( PatternMatchValue { - range: 2449..2452, node_index: NodeIndex(None), + range: 2449..2452, value: StringLiteral( ExprStringLiteral { node_index: NodeIndex(None), @@ -3689,8 +3689,8 @@ Module( ), MatchMapping( PatternMatchMapping { - range: 2455..2457, node_index: NodeIndex(None), + range: 2455..2457, keys: [], patterns: [], rest: None, @@ -3733,8 +3733,8 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 2482..2484, node_index: NodeIndex(None), + range: 2482..2484, patterns: [], }, ), @@ -3817,13 +3817,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 2548..2553, node_index: NodeIndex(None), + range: 2548..2553, patterns: [ MatchValue( PatternMatchValue { - range: 2548..2549, node_index: NodeIndex(None), + range: 2548..2549, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -3837,8 +3837,8 @@ Module( ), MatchStar( PatternMatchStar { - range: 2551..2553, node_index: NodeIndex(None), + range: 2551..2553, name: Some( Identifier { id: Name("x"), @@ -3930,13 +3930,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 2617..2623, node_index: NodeIndex(None), + range: 2617..2623, patterns: [ MatchStar( PatternMatchStar { - range: 2617..2619, node_index: NodeIndex(None), + range: 2617..2619, name: Some( Identifier { id: Name("x"), @@ -3948,8 +3948,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 2621..2622, node_index: NodeIndex(None), + range: 2621..2622, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -4024,13 +4024,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 2680..2682, node_index: NodeIndex(None), + range: 2680..2682, patterns: [ MatchAs( PatternMatchAs { - range: 2680..2681, node_index: NodeIndex(None), + range: 2680..2681, pattern: None, name: Some( Identifier { @@ -4112,13 +4112,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 2741..2745, node_index: NodeIndex(None), + range: 2741..2745, patterns: [ MatchAs( PatternMatchAs { - range: 2741..2742, node_index: NodeIndex(None), + range: 2741..2742, pattern: None, name: Some( Identifier { @@ -4131,8 +4131,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 2744..2745, node_index: NodeIndex(None), + range: 2744..2745, pattern: None, name: Some( Identifier { @@ -4220,18 +4220,18 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 2807..2814, node_index: NodeIndex(None), + range: 2807..2814, patterns: [ MatchAs( PatternMatchAs { - range: 2807..2813, node_index: NodeIndex(None), + range: 2807..2813, pattern: Some( MatchAs( PatternMatchAs { - range: 2807..2808, node_index: NodeIndex(None), + range: 2807..2808, pattern: None, name: Some( Identifier { @@ -4305,8 +4305,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 2932..2938, node_index: NodeIndex(None), + range: 2932..2938, value: FString( ExprFString { node_index: NodeIndex(None), @@ -4415,8 +4415,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 2981..3004, node_index: NodeIndex(None), + range: 2981..3004, keys: [], patterns: [], rest: Some( @@ -4534,8 +4534,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 3060..3107, node_index: NodeIndex(None), + range: 3060..3107, keys: [ StringLiteral( ExprStringLiteral { @@ -4562,18 +4562,18 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 3079..3100, node_index: NodeIndex(None), + range: 3079..3100, pattern: Some( MatchOr( PatternMatchOr { - range: 3079..3091, node_index: NodeIndex(None), + range: 3079..3091, patterns: [ MatchClass( PatternMatchClass { - range: 3079..3084, node_index: NodeIndex(None), + range: 3079..3084, cls: Name( ExprName { node_index: NodeIndex(None), @@ -4592,8 +4592,8 @@ Module( ), MatchSingleton( PatternMatchSingleton { - range: 3087..3091, node_index: NodeIndex(None), + range: 3087..3091, value: None, }, ), @@ -4674,13 +4674,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 3148..3155, node_index: NodeIndex(None), + range: 3148..3155, patterns: [ MatchValue( PatternMatchValue { - range: 3149..3150, node_index: NodeIndex(None), + range: 3149..3150, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -4694,8 +4694,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 3152..3153, node_index: NodeIndex(None), + range: 3152..3153, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -4760,13 +4760,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 3189..3196, node_index: NodeIndex(None), + range: 3189..3196, patterns: [ MatchValue( PatternMatchValue { - range: 3190..3191, node_index: NodeIndex(None), + range: 3190..3191, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -4780,8 +4780,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 3193..3194, node_index: NodeIndex(None), + range: 3193..3194, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -4846,13 +4846,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 3230..3234, node_index: NodeIndex(None), + range: 3230..3234, patterns: [ MatchValue( PatternMatchValue { - range: 3231..3232, node_index: NodeIndex(None), + range: 3231..3232, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -4927,8 +4927,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 3269..3270, node_index: NodeIndex(None), + range: 3269..3270, pattern: None, name: Some( Identifier { @@ -4988,8 +4988,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 3306..3307, node_index: NodeIndex(None), + range: 3306..3307, pattern: None, name: Some( Identifier { @@ -5049,8 +5049,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 3344..3345, node_index: NodeIndex(None), + range: 3344..3345, pattern: None, name: Some( Identifier { @@ -5092,8 +5092,8 @@ Module( node_index: NodeIndex(None), pattern: MatchSingleton( PatternMatchSingleton { - range: 3403..3407, node_index: NodeIndex(None), + range: 3403..3407, value: None, }, ), @@ -5118,8 +5118,8 @@ Module( node_index: NodeIndex(None), pattern: MatchSingleton( PatternMatchSingleton { - range: 3430..3434, node_index: NodeIndex(None), + range: 3430..3434, value: True, }, ), @@ -5144,8 +5144,8 @@ Module( node_index: NodeIndex(None), pattern: MatchSingleton( PatternMatchSingleton { - range: 3457..3462, node_index: NodeIndex(None), + range: 3457..3462, value: False, }, ), @@ -5186,8 +5186,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 3515..3518, node_index: NodeIndex(None), + range: 3515..3518, value: Attribute( ExprAttribute { node_index: NodeIndex(None), @@ -5231,8 +5231,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 3541..3546, node_index: NodeIndex(None), + range: 3541..3546, value: Attribute( ExprAttribute { node_index: NodeIndex(None), @@ -5288,8 +5288,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 3569..3571, node_index: NodeIndex(None), + range: 3569..3571, value: StringLiteral( ExprStringLiteral { node_index: NodeIndex(None), @@ -5334,8 +5334,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 3594..3597, node_index: NodeIndex(None), + range: 3594..3597, value: BytesLiteral( ExprBytesLiteral { node_index: NodeIndex(None), @@ -5380,8 +5380,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 3620..3621, node_index: NodeIndex(None), + range: 3620..3621, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -5414,8 +5414,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 3644..3647, node_index: NodeIndex(None), + range: 3644..3647, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -5448,8 +5448,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 3670..3674, node_index: NodeIndex(None), + range: 3670..3674, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -5483,8 +5483,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 3697..3703, node_index: NodeIndex(None), + range: 3697..3703, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -5534,8 +5534,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 3726..3728, node_index: NodeIndex(None), + range: 3726..3728, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -5575,8 +5575,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 3751..3754, node_index: NodeIndex(None), + range: 3751..3754, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -5616,8 +5616,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 3777..3782, node_index: NodeIndex(None), + range: 3777..3782, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -5657,8 +5657,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 3806..3807, node_index: NodeIndex(None), + range: 3806..3807, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -5707,13 +5707,13 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 3858..3863, node_index: NodeIndex(None), + range: 3858..3863, patterns: [ MatchValue( PatternMatchValue { - range: 3858..3859, node_index: NodeIndex(None), + range: 3858..3859, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -5727,8 +5727,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 3862..3863, node_index: NodeIndex(None), + range: 3862..3863, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -5764,13 +5764,13 @@ Module( node_index: NodeIndex(None), pattern: MatchOr( PatternMatchOr { - range: 3886..3914, node_index: NodeIndex(None), + range: 3886..3914, patterns: [ MatchValue( PatternMatchValue { - range: 3886..3888, node_index: NodeIndex(None), + range: 3886..3888, value: StringLiteral( ExprStringLiteral { node_index: NodeIndex(None), @@ -5796,8 +5796,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 3891..3894, node_index: NodeIndex(None), + range: 3891..3894, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -5811,8 +5811,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 3897..3899, node_index: NodeIndex(None), + range: 3897..3899, value: UnaryOp( ExprUnaryOp { node_index: NodeIndex(None), @@ -5833,8 +5833,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 3902..3908, node_index: NodeIndex(None), + range: 3902..3908, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -5865,8 +5865,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 3911..3914, node_index: NodeIndex(None), + range: 3911..3914, value: Attribute( ExprAttribute { node_index: NodeIndex(None), @@ -5929,8 +5929,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 3964..3965, node_index: NodeIndex(None), + range: 3964..3965, pattern: None, name: Some( Identifier { @@ -5978,13 +5978,13 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 3997..4003, node_index: NodeIndex(None), + range: 3997..4003, pattern: Some( MatchAs( PatternMatchAs { - range: 3997..3998, node_index: NodeIndex(None), + range: 3997..3998, pattern: None, name: Some( Identifier { @@ -6042,18 +6042,18 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 4035..4047, node_index: NodeIndex(None), + range: 4035..4047, pattern: Some( MatchOr( PatternMatchOr { - range: 4035..4040, node_index: NodeIndex(None), + range: 4035..4040, patterns: [ MatchValue( PatternMatchValue { - range: 4035..4036, node_index: NodeIndex(None), + range: 4035..4036, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6067,8 +6067,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4039..4040, node_index: NodeIndex(None), + range: 4039..4040, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6114,13 +6114,13 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 4070..4083, node_index: NodeIndex(None), + range: 4070..4083, pattern: Some( MatchValue( PatternMatchValue { - range: 4070..4076, node_index: NodeIndex(None), + range: 4070..4076, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -6180,13 +6180,13 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 4106..4115, node_index: NodeIndex(None), + range: 4106..4115, pattern: Some( MatchValue( PatternMatchValue { - range: 4106..4109, node_index: NodeIndex(None), + range: 4106..4109, value: Attribute( ExprAttribute { node_index: NodeIndex(None), @@ -6240,13 +6240,13 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 4138..4144, node_index: NodeIndex(None), + range: 4138..4144, pattern: Some( MatchAs( PatternMatchAs { - range: 4138..4139, node_index: NodeIndex(None), + range: 4138..4139, pattern: None, name: None, }, @@ -6298,8 +6298,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 4176..4177, node_index: NodeIndex(None), + range: 4176..4177, pattern: None, name: None, }, @@ -6341,13 +6341,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 4233..4240, node_index: NodeIndex(None), + range: 4233..4240, patterns: [ MatchValue( PatternMatchValue { - range: 4233..4234, node_index: NodeIndex(None), + range: 4233..4234, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6361,8 +6361,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4236..4237, node_index: NodeIndex(None), + range: 4236..4237, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6376,8 +6376,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4239..4240, node_index: NodeIndex(None), + range: 4239..4240, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6413,13 +6413,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 4263..4273, node_index: NodeIndex(None), + range: 4263..4273, patterns: [ MatchValue( PatternMatchValue { - range: 4264..4265, node_index: NodeIndex(None), + range: 4264..4265, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6433,8 +6433,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4267..4268, node_index: NodeIndex(None), + range: 4267..4268, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6448,8 +6448,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4270..4271, node_index: NodeIndex(None), + range: 4270..4271, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6485,13 +6485,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 4296..4318, node_index: NodeIndex(None), + range: 4296..4318, patterns: [ MatchValue( PatternMatchValue { - range: 4297..4303, node_index: NodeIndex(None), + range: 4297..4303, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -6522,8 +6522,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 4305..4306, node_index: NodeIndex(None), + range: 4305..4306, pattern: None, name: Some( Identifier { @@ -6536,15 +6536,15 @@ Module( ), MatchSingleton( PatternMatchSingleton { - range: 4308..4312, node_index: NodeIndex(None), + range: 4308..4312, value: None, }, ), MatchValue( PatternMatchValue { - range: 4314..4317, node_index: NodeIndex(None), + range: 4314..4317, value: Attribute( ExprAttribute { node_index: NodeIndex(None), @@ -6591,23 +6591,23 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 4341..4357, node_index: NodeIndex(None), + range: 4341..4357, pattern: Some( MatchSequence( PatternMatchSequence { - range: 4341..4352, node_index: NodeIndex(None), + range: 4341..4352, patterns: [ MatchAs( PatternMatchAs { - range: 4342..4348, node_index: NodeIndex(None), + range: 4342..4348, pattern: Some( MatchValue( PatternMatchValue { - range: 4342..4343, node_index: NodeIndex(None), + range: 4342..4343, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6631,8 +6631,8 @@ Module( ), MatchAs( PatternMatchAs { - range: 4350..4351, node_index: NodeIndex(None), + range: 4350..4351, pattern: None, name: Some( Identifier { @@ -6677,13 +6677,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 4380..4394, node_index: NodeIndex(None), + range: 4380..4394, patterns: [ MatchValue( PatternMatchValue { - range: 4381..4382, node_index: NodeIndex(None), + range: 4381..4382, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6697,8 +6697,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4384..4385, node_index: NodeIndex(None), + range: 4384..4385, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6712,8 +6712,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4387..4393, node_index: NodeIndex(None), + range: 4387..4393, value: BinOp( ExprBinOp { node_index: NodeIndex(None), @@ -6766,18 +6766,18 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 4417..4427, node_index: NodeIndex(None), + range: 4417..4427, patterns: [ MatchSequence( PatternMatchSequence { - range: 4418..4423, node_index: NodeIndex(None), + range: 4418..4423, patterns: [ MatchValue( PatternMatchValue { - range: 4419..4420, node_index: NodeIndex(None), + range: 4419..4420, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6791,8 +6791,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4421..4422, node_index: NodeIndex(None), + range: 4421..4422, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6809,8 +6809,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4425..4426, node_index: NodeIndex(None), + range: 4425..4426, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6846,13 +6846,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 4450..4453, node_index: NodeIndex(None), + range: 4450..4453, patterns: [ MatchValue( PatternMatchValue { - range: 4451..4452, node_index: NodeIndex(None), + range: 4451..4452, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6904,13 +6904,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 4505..4508, node_index: NodeIndex(None), + range: 4505..4508, patterns: [ MatchStar( PatternMatchStar { - range: 4505..4507, node_index: NodeIndex(None), + range: 4505..4507, name: Some( Identifier { id: Name("a"), @@ -6944,13 +6944,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 4531..4534, node_index: NodeIndex(None), + range: 4531..4534, patterns: [ MatchStar( PatternMatchStar { - range: 4531..4533, node_index: NodeIndex(None), + range: 4531..4533, name: None, }, ), @@ -6978,13 +6978,13 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 4557..4570, node_index: NodeIndex(None), + range: 4557..4570, patterns: [ MatchValue( PatternMatchValue { - range: 4558..4559, node_index: NodeIndex(None), + range: 4558..4559, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -6998,8 +6998,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4561..4562, node_index: NodeIndex(None), + range: 4561..4562, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7013,8 +7013,8 @@ Module( ), MatchStar( PatternMatchStar { - range: 4564..4569, node_index: NodeIndex(None), + range: 4564..4569, name: Some( Identifier { id: Name("rest"), @@ -7048,20 +7048,20 @@ Module( node_index: NodeIndex(None), pattern: MatchSequence( PatternMatchSequence { - range: 4593..4603, node_index: NodeIndex(None), + range: 4593..4603, patterns: [ MatchStar( PatternMatchStar { - range: 4594..4596, node_index: NodeIndex(None), + range: 4594..4596, name: None, }, ), MatchValue( PatternMatchValue { - range: 4598..4599, node_index: NodeIndex(None), + range: 4598..4599, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7075,8 +7075,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4601..4602, node_index: NodeIndex(None), + range: 4601..4602, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7128,8 +7128,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 4656..4663, node_index: NodeIndex(None), + range: 4656..4663, cls: Name( ExprName { node_index: NodeIndex(None), @@ -7167,8 +7167,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 4686..4697, node_index: NodeIndex(None), + range: 4686..4697, cls: Attribute( ExprAttribute { node_index: NodeIndex(None), @@ -7230,8 +7230,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 4720..4732, node_index: NodeIndex(None), + range: 4720..4732, cls: Name( ExprName { node_index: NodeIndex(None), @@ -7255,8 +7255,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 4730..4731, node_index: NodeIndex(None), + range: 4730..4731, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7294,8 +7294,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 4755..4773, node_index: NodeIndex(None), + range: 4755..4773, cls: Name( ExprName { node_index: NodeIndex(None), @@ -7319,8 +7319,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 4765..4766, node_index: NodeIndex(None), + range: 4765..4766, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7343,8 +7343,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 4770..4771, node_index: NodeIndex(None), + range: 4770..4771, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7382,8 +7382,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 4796..4809, node_index: NodeIndex(None), + range: 4796..4809, cls: Name( ExprName { node_index: NodeIndex(None), @@ -7398,8 +7398,8 @@ Module( patterns: [ MatchValue( PatternMatchValue { - range: 4804..4805, node_index: NodeIndex(None), + range: 4804..4805, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7413,8 +7413,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4807..4808, node_index: NodeIndex(None), + range: 4807..4808, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7452,8 +7452,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 4832..4852, node_index: NodeIndex(None), + range: 4832..4852, cls: Name( ExprName { node_index: NodeIndex(None), @@ -7468,13 +7468,13 @@ Module( patterns: [ MatchSequence( PatternMatchSequence { - range: 4840..4846, node_index: NodeIndex(None), + range: 4840..4846, patterns: [ MatchValue( PatternMatchValue { - range: 4841..4842, node_index: NodeIndex(None), + range: 4841..4842, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7488,8 +7488,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4844..4845, node_index: NodeIndex(None), + range: 4844..4845, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7516,8 +7516,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 4850..4851, node_index: NodeIndex(None), + range: 4850..4851, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7555,8 +7555,8 @@ Module( node_index: NodeIndex(None), pattern: MatchClass( PatternMatchClass { - range: 4875..4897, node_index: NodeIndex(None), + range: 4875..4897, cls: Name( ExprName { node_index: NodeIndex(None), @@ -7580,13 +7580,13 @@ Module( }, pattern: MatchSequence( PatternMatchSequence { - range: 4885..4891, node_index: NodeIndex(None), + range: 4885..4891, patterns: [ MatchValue( PatternMatchValue { - range: 4886..4887, node_index: NodeIndex(None), + range: 4886..4887, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7600,8 +7600,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 4889..4890, node_index: NodeIndex(None), + range: 4889..4890, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7627,8 +7627,8 @@ Module( }, pattern: MatchValue( PatternMatchValue { - range: 4895..4896, node_index: NodeIndex(None), + range: 4895..4896, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7696,8 +7696,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 4957..4963, node_index: NodeIndex(None), + range: 4957..4963, keys: [ NumberLiteral( ExprNumberLiteral { @@ -7712,8 +7712,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 4961..4962, node_index: NodeIndex(None), + range: 4961..4962, pattern: None, name: None, }, @@ -7743,8 +7743,8 @@ Module( node_index: NodeIndex(None), pattern: MatchMapping( PatternMatchMapping { - range: 4986..5015, node_index: NodeIndex(None), + range: 4986..5015, keys: [ StringLiteral( ExprStringLiteral { @@ -7777,8 +7777,8 @@ Module( patterns: [ MatchAs( PatternMatchAs { - range: 4991..4992, node_index: NodeIndex(None), + range: 4991..4992, pattern: None, name: Some( Identifier { @@ -7791,13 +7791,13 @@ Module( ), MatchSequence( PatternMatchSequence { - range: 5000..5006, node_index: NodeIndex(None), + range: 5000..5006, patterns: [ MatchValue( PatternMatchValue { - range: 5001..5002, node_index: NodeIndex(None), + range: 5001..5002, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7811,8 +7811,8 @@ Module( ), MatchValue( PatternMatchValue { - range: 5004..5005, node_index: NodeIndex(None), + range: 5004..5005, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -7874,8 +7874,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 5064..5065, node_index: NodeIndex(None), + range: 5064..5065, pattern: None, name: Some( Identifier { @@ -7930,8 +7930,8 @@ Module( node_index: NodeIndex(None), pattern: MatchAs( PatternMatchAs { - range: 5090..5091, node_index: NodeIndex(None), + range: 5090..5091, pattern: None, name: Some( Identifier { @@ -8763,8 +8763,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 5683..5684, node_index: NodeIndex(None), + range: 5683..5684, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), @@ -8791,8 +8791,8 @@ Module( node_index: NodeIndex(None), pattern: MatchValue( PatternMatchValue { - range: 5700..5701, node_index: NodeIndex(None), + range: 5700..5701, value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), From 40148d7b118adfe435b561c971f00fa7e89623bb Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 22 Oct 2025 12:07:01 +0100 Subject: [PATCH 008/188] [ty] Add assertions to ensure that we never call `KnownClass::Tuple.to_instance()` or similar (#21027) --- crates/ty_python_semantic/src/types/class.rs | 54 +++++++++++++++---- .../src/types/infer/builder.rs | 2 +- .../ty_python_semantic/src/types/instance.rs | 25 +++------ 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 56df9329ed..c7b96cfa7e 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -4614,7 +4614,13 @@ impl KnownClass { /// 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(crate) 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)) @@ -4623,7 +4629,13 @@ 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<'_> { + 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) @@ -4667,11 +4679,17 @@ 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<'db>( self, db: &'db dyn Db, specialization: impl IntoIterator>, ) -> Type<'db> { + 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) @@ -5566,11 +5584,19 @@ mod tests { }); for class in KnownClass::iter() { - assert_ne!( - class.to_instance(&db), - Type::unknown(), - "Unexpectedly fell back to `Unknown` for `{class:?}`" - ); + // 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:?}`" + ); + } } } @@ -5617,11 +5643,19 @@ mod tests { current_version = version_added; } - assert_ne!( - class.to_instance(&db), - Type::unknown(), - "Unexpectedly fell back to `Unknown` for `{class:?}` on Python {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/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index d67a39dab0..238b95a4a8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -5950,7 +5950,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .unwrap_or(InferableTypeVars::None); annotation.filter_disjoint_elements( self.db(), - KnownClass::Tuple.to_instance(self.db()), + Type::homogeneous_tuple(self.db(), Type::unknown()), inferable, ) }); diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index ac081ca02a..c294cde618 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -85,16 +85,6 @@ impl<'db> Type<'db> { Type::NominalInstance(NominalInstanceType(NominalInstanceInner::ExactTuple(tuple))) } - /// **Private** helper function to create a `Type::NominalInstance` from a class that - /// is known not to be `Any`, a protocol class, or a typed dict class. - fn non_tuple_instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { - if class.is_known(db, KnownClass::Object) { - Type::NominalInstance(NominalInstanceType(NominalInstanceInner::Object)) - } else { - Type::NominalInstance(NominalInstanceType(NominalInstanceInner::NonTuple(class))) - } - } - pub(crate) const fn into_nominal_instance(self) -> Option> { match self { Type::NominalInstance(instance_type) => Some(instance_type), @@ -353,9 +343,9 @@ impl<'db> NominalInstanceType<'db> { NominalInstanceInner::ExactTuple(tuple) => { Type::tuple(tuple.normalized_impl(db, visitor)) } - NominalInstanceInner::NonTuple(class) => { - Type::non_tuple_instance(db, class.normalized_impl(db, visitor)) - } + NominalInstanceInner::NonTuple(class) => Type::NominalInstance(NominalInstanceType( + NominalInstanceInner::NonTuple(class.normalized_impl(db, visitor)), + )), NominalInstanceInner::Object => Type::object(), } } @@ -488,10 +478,11 @@ impl<'db> NominalInstanceType<'db> { NominalInstanceInner::ExactTuple(tuple) => { Type::tuple(tuple.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) } - NominalInstanceInner::NonTuple(class) => Type::non_tuple_instance( - db, - class.apply_type_mapping_impl(db, type_mapping, tcx, visitor), - ), + NominalInstanceInner::NonTuple(class) => { + Type::NominalInstance(NominalInstanceType(NominalInstanceInner::NonTuple( + class.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + ))) + } NominalInstanceInner::Object => Type::object(), } } From 2c9433796ac7e73667fd6bede25ee79238dc6805 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Wed, 22 Oct 2025 21:06:24 +0900 Subject: [PATCH 009/188] [`ruff`] Autogenerate TypeParam nodes (#21028) --- crates/ruff_python_ast/ast.toml | 25 ++++- crates/ruff_python_ast/src/generated.rs | 93 +++++++++++++++++++ crates/ruff_python_ast/src/node.rs | 61 ------------ crates/ruff_python_ast/src/nodes.rs | 31 ------- ...class_def_unclosed_type_param_list.py.snap | 4 +- ...lid_syntax@class_type_params_py311.py.snap | 8 +- .../invalid_syntax@debug_shadow_class.py.snap | 2 +- ...valid_syntax@debug_shadow_function.py.snap | 2 +- ...lid_syntax@debug_shadow_type_alias.py.snap | 2 +- ...tax@duplicate_type_parameter_names.py.snap | 38 ++++---- ...ction_def_unclosed_type_param_list.py.snap | 4 +- ..._syntax@function_type_params_py311.py.snap | 2 +- ...id_syntax@invalid_annotation_class.py.snap | 14 +-- ...syntax@invalid_annotation_function.py.snap | 40 ++++---- ...ntax@invalid_annotation_type_alias.py.snap | 10 +- ...atements__function_type_parameters.py.snap | 16 ++-- ...id_syntax@type_param_default_py312.py.snap | 12 +-- ...ntax@type_param_invalid_bound_expr.py.snap | 10 +- ...id_syntax@type_param_missing_bound.py.snap | 6 +- ...syntax@type_param_param_spec_bound.py.snap | 2 +- ...am_param_spec_invalid_default_expr.py.snap | 12 +-- ...e_param_param_spec_missing_default.py.snap | 6 +- ...aram_type_var_invalid_default_expr.py.snap | 14 +-- ...ype_param_type_var_missing_default.py.snap | 8 +- ...ax@type_param_type_var_tuple_bound.py.snap | 2 +- ...ype_var_tuple_invalid_default_expr.py.snap | 12 +-- ...ram_type_var_tuple_missing_default.py.snap | 6 +- ...lid_syntax@class_type_params_py312.py.snap | 8 +- ..._syntax@function_type_params_py312.py.snap | 2 +- ...non_duplicate_type_parameter_names.py.snap | 24 ++--- .../valid_syntax@statement__class.py.snap | 36 +++---- .../valid_syntax@statement__function.py.snap | 18 ++-- .../valid_syntax@statement__type.py.snap | 48 +++++----- ...id_syntax@type_param_default_py313.py.snap | 6 +- ...valid_syntax@type_param_param_spec.py.snap | 12 +-- .../valid_syntax@type_param_type_var.py.snap | 12 +-- ...d_syntax@type_param_type_var_tuple.py.snap | 14 +-- 37 files changed, 320 insertions(+), 302 deletions(-) diff --git a/crates/ruff_python_ast/ast.toml b/crates/ruff_python_ast/ast.toml index e2c6fd2844..cbcde4e213 100644 --- a/crates/ruff_python_ast/ast.toml +++ b/crates/ruff_python_ast/ast.toml @@ -605,10 +605,27 @@ fields = [{ name = "patterns", type = "Pattern*" }] [TypeParam] doc = "See also [type_param](https://docs.python.org/3/library/ast.html#ast.type_param)" -[TypeParam.nodes] -TypeParamTypeVar = {} -TypeParamTypeVarTuple = {} -TypeParamParamSpec = {} +[TypeParam.nodes.TypeParamTypeVar] +doc = "See also [TypeVar](https://docs.python.org/3/library/ast.html#ast.TypeVar)" +fields = [ + { name = "name", type = "Identifier" }, + { name = "bound", type = "Box?" }, + { name = "default", type = "Box?" }, +] + +[TypeParam.nodes.TypeParamTypeVarTuple] +doc = "See also [TypeVarTuple](https://docs.python.org/3/library/ast.html#ast.TypeVarTuple)" +fields = [ + { name = "name", type = "Identifier" }, + { name = "default", type = "Box?" }, +] + +[TypeParam.nodes.TypeParamParamSpec] +doc = "See also [ParamSpec](https://docs.python.org/3/library/ast.html#ast.ParamSpec)" +fields = [ + { name = "name", type = "Identifier" }, + { name = "default", type = "Box?" }, +] [ungrouped.nodes] InterpolatedStringFormatSpec = {} diff --git a/crates/ruff_python_ast/src/generated.rs b/crates/ruff_python_ast/src/generated.rs index 8cf0ad9009..547c50d631 100644 --- a/crates/ruff_python_ast/src/generated.rs +++ b/crates/ruff_python_ast/src/generated.rs @@ -9713,6 +9713,37 @@ pub struct PatternMatchOr { pub patterns: Vec, } +/// See also [TypeVar](https://docs.python.org/3/library/ast.html#ast.TypeVar) +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct TypeParamTypeVar { + pub node_index: crate::AtomicNodeIndex, + pub range: ruff_text_size::TextRange, + pub name: crate::Identifier, + pub bound: Option>, + pub default: Option>, +} + +/// See also [TypeVarTuple](https://docs.python.org/3/library/ast.html#ast.TypeVarTuple) +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct TypeParamTypeVarTuple { + pub node_index: crate::AtomicNodeIndex, + pub range: ruff_text_size::TextRange, + pub name: crate::Identifier, + pub default: Option>, +} + +/// See also [ParamSpec](https://docs.python.org/3/library/ast.html#ast.ParamSpec) +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct TypeParamParamSpec { + pub node_index: crate::AtomicNodeIndex, + pub range: ruff_text_size::TextRange, + pub name: crate::Identifier, + pub default: Option>, +} + impl ModModule { pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) where @@ -10778,3 +10809,65 @@ impl PatternMatchOr { } } } + +impl TypeParamTypeVar { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let TypeParamTypeVar { + name, + bound, + default, + range: _, + node_index: _, + } = self; + visitor.visit_identifier(name); + + if let Some(bound) = bound { + visitor.visit_expr(bound); + } + + if let Some(default) = default { + visitor.visit_expr(default); + } + } +} + +impl TypeParamTypeVarTuple { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let TypeParamTypeVarTuple { + name, + default, + range: _, + node_index: _, + } = self; + visitor.visit_identifier(name); + + if let Some(default) = default { + visitor.visit_expr(default); + } + } +} + +impl TypeParamParamSpec { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let TypeParamParamSpec { + name, + default, + range: _, + node_index: _, + } = self; + visitor.visit_identifier(name); + + if let Some(default) = default { + visitor.visit_expr(default); + } + } +} diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index 202315964d..518d7520e7 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -506,67 +506,6 @@ impl ast::TypeParams { } } -impl ast::TypeParamTypeVar { - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: SourceOrderVisitor<'a> + ?Sized, - { - let ast::TypeParamTypeVar { - bound, - default, - name, - range: _, - node_index: _, - } = self; - - visitor.visit_identifier(name); - if let Some(expr) = bound { - visitor.visit_expr(expr); - } - if let Some(expr) = default { - visitor.visit_expr(expr); - } - } -} - -impl ast::TypeParamTypeVarTuple { - #[inline] - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: SourceOrderVisitor<'a> + ?Sized, - { - let ast::TypeParamTypeVarTuple { - range: _, - node_index: _, - name, - default, - } = self; - visitor.visit_identifier(name); - if let Some(expr) = default { - visitor.visit_expr(expr); - } - } -} - -impl ast::TypeParamParamSpec { - #[inline] - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: SourceOrderVisitor<'a> + ?Sized, - { - let ast::TypeParamParamSpec { - range: _, - node_index: _, - name, - default, - } = self; - visitor.visit_identifier(name); - if let Some(expr) = default { - visitor.visit_expr(expr); - } - } -} - impl ast::FString { pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) where diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index d26be06da5..f71f420d09 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -2892,37 +2892,6 @@ impl TypeParam { } } -/// See also [TypeVar](https://docs.python.org/3/library/ast.html#ast.TypeVar) -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] -pub struct TypeParamTypeVar { - pub range: TextRange, - pub node_index: AtomicNodeIndex, - pub name: Identifier, - pub bound: Option>, - pub default: Option>, -} - -/// See also [ParamSpec](https://docs.python.org/3/library/ast.html#ast.ParamSpec) -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] -pub struct TypeParamParamSpec { - pub range: TextRange, - pub node_index: AtomicNodeIndex, - pub name: Identifier, - pub default: Option>, -} - -/// See also [TypeVarTuple](https://docs.python.org/3/library/ast.html#ast.TypeVarTuple) -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] -pub struct TypeParamTypeVarTuple { - pub range: TextRange, - pub node_index: AtomicNodeIndex, - pub name: Identifier, - pub default: Option>, -} - /// See also [decorator](https://docs.python.org/3/library/ast.html#ast.decorator) #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unclosed_type_param_list.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unclosed_type_param_list.py.snap index 6f4c7aa2ed..3246bdb0ce 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unclosed_type_param_list.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unclosed_type_param_list.py.snap @@ -27,8 +27,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 10..12, node_index: NodeIndex(None), + range: 10..12, name: Identifier { id: Name("T1"), range: 10..12, @@ -40,8 +40,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 14..17, node_index: NodeIndex(None), + range: 14..17, name: Identifier { id: Name("T2"), range: 15..17, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap index 40c8001fee..69e2166048 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap @@ -27,8 +27,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 54..69, node_index: NodeIndex(None), + range: 54..69, name: Identifier { id: Name("S"), range: 54..55, @@ -67,8 +67,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 71..79, node_index: NodeIndex(None), + range: 71..79, name: Identifier { id: Name("T"), range: 71..72, @@ -89,8 +89,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 81..84, node_index: NodeIndex(None), + range: 81..84, name: Identifier { id: Name("Ts"), range: 82..84, @@ -101,8 +101,8 @@ Module( ), ParamSpec( TypeParamParamSpec { - range: 86..89, node_index: NodeIndex(None), + range: 86..89, name: Identifier { id: Name("P"), range: 88..89, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_class.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_class.py.snap index c69ff0c693..d3f8cd86fd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_class.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_class.py.snap @@ -55,8 +55,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 43..52, node_index: NodeIndex(None), + range: 43..52, name: Identifier { id: Name("__debug__"), range: 43..52, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_function.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_function.py.snap index 9dbb166ca4..710fc7ad51 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_function.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_function.py.snap @@ -66,8 +66,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 44..53, node_index: NodeIndex(None), + range: 44..53, name: Identifier { id: Name("__debug__"), range: 44..53, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_type_alias.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_type_alias.py.snap index a39ffdd665..89647fd3a3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_type_alias.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_type_alias.py.snap @@ -67,8 +67,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 78..87, node_index: NodeIndex(None), + range: 78..87, name: Identifier { id: Name("__debug__"), range: 78..87, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap index 852f2656e0..6630565e33 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 11..12, node_index: NodeIndex(None), + range: 11..12, name: Identifier { id: Name("T"), range: 11..12, @@ -42,8 +42,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 14..15, node_index: NodeIndex(None), + range: 14..15, name: Identifier { id: Name("T"), range: 14..15, @@ -82,8 +82,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 29..30, node_index: NodeIndex(None), + range: 29..30, name: Identifier { id: Name("T"), range: 29..30, @@ -95,8 +95,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 32..33, node_index: NodeIndex(None), + range: 32..33, name: Identifier { id: Name("T"), range: 32..33, @@ -177,8 +177,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 54..55, node_index: NodeIndex(None), + range: 54..55, name: Identifier { id: Name("T"), range: 54..55, @@ -190,8 +190,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 57..58, node_index: NodeIndex(None), + range: 57..58, name: Identifier { id: Name("T"), range: 57..58, @@ -240,8 +240,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 76..77, node_index: NodeIndex(None), + range: 76..77, name: Identifier { id: Name("T"), range: 76..77, @@ -253,8 +253,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 79..85, node_index: NodeIndex(None), + range: 79..85, name: Identifier { id: Name("U"), range: 79..80, @@ -275,8 +275,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 87..102, node_index: NodeIndex(None), + range: 87..102, name: Identifier { id: Name("V"), range: 87..88, @@ -315,8 +315,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 104..107, node_index: NodeIndex(None), + range: 104..107, name: Identifier { id: Name("Ts"), range: 105..107, @@ -327,8 +327,8 @@ Module( ), ParamSpec( TypeParamParamSpec { - range: 109..112, node_index: NodeIndex(None), + range: 109..112, name: Identifier { id: Name("P"), range: 111..112, @@ -339,8 +339,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 114..125, node_index: NodeIndex(None), + range: 114..125, name: Identifier { id: Name("T"), range: 114..115, @@ -388,8 +388,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 139..140, node_index: NodeIndex(None), + range: 139..140, name: Identifier { id: Name("T"), range: 139..140, @@ -401,8 +401,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 142..143, node_index: NodeIndex(None), + range: 142..143, name: Identifier { id: Name("T"), range: 142..143, @@ -414,8 +414,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 145..146, node_index: NodeIndex(None), + range: 145..146, name: Identifier { id: Name("T"), range: 145..146, @@ -472,8 +472,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 175..176, node_index: NodeIndex(None), + range: 175..176, name: Identifier { id: Name("T"), range: 175..176, @@ -485,8 +485,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 178..180, node_index: NodeIndex(None), + range: 178..180, name: Identifier { id: Name("T"), range: 179..180, @@ -542,8 +542,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 224..225, node_index: NodeIndex(None), + range: 224..225, name: Identifier { id: Name("T"), range: 224..225, @@ -555,8 +555,8 @@ Module( ), ParamSpec( TypeParamParamSpec { - range: 227..230, node_index: NodeIndex(None), + range: 227..230, name: Identifier { id: Name("T"), range: 229..230, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_type_param_list.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_type_param_list.py.snap index 78bc5e9f73..fa71509d1f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_type_param_list.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_type_param_list.py.snap @@ -28,8 +28,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 8..10, node_index: NodeIndex(None), + range: 8..10, name: Identifier { id: Name("T1"), range: 8..10, @@ -41,8 +41,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 12..15, node_index: NodeIndex(None), + range: 12..15, name: Identifier { id: Name("T2"), range: 13..15, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap index 259c43f1e3..5ab2a0d364 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap @@ -28,8 +28,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 52..53, node_index: NodeIndex(None), + range: 52..53, name: Identifier { id: Name("T"), range: 52..53, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_class.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_class.py.snap index 87faffde66..3c8495804d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_class.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_class.py.snap @@ -27,8 +27,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 8..9, node_index: NodeIndex(None), + range: 8..9, name: Identifier { id: Name("T"), range: 8..9, @@ -105,8 +105,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 35..36, node_index: NodeIndex(None), + range: 35..36, name: Identifier { id: Name("T"), range: 35..36, @@ -178,8 +178,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 62..63, node_index: NodeIndex(None), + range: 62..63, name: Identifier { id: Name("T"), range: 62..63, @@ -249,8 +249,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 94..106, node_index: NodeIndex(None), + range: 94..106, name: Identifier { id: Name("T"), range: 94..95, @@ -315,8 +315,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 145..156, node_index: NodeIndex(None), + range: 145..156, name: Identifier { id: Name("T"), range: 145..146, @@ -387,8 +387,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 201..202, node_index: NodeIndex(None), + range: 201..202, name: Identifier { id: Name("T"), range: 201..202, @@ -458,8 +458,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 228..240, node_index: NodeIndex(None), + range: 228..240, name: Identifier { id: Name("T"), range: 228..229, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap index 7f6d5bbebf..682cbac08f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap @@ -28,8 +28,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 6..7, node_index: NodeIndex(None), + range: 6..7, name: Identifier { id: Name("T"), range: 6..7, @@ -102,8 +102,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 35..36, node_index: NodeIndex(None), + range: 35..36, name: Identifier { id: Name("T"), range: 35..36, @@ -192,8 +192,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 65..66, node_index: NodeIndex(None), + range: 65..66, name: Identifier { id: Name("T"), range: 65..66, @@ -274,8 +274,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 93..94, node_index: NodeIndex(None), + range: 93..94, name: Identifier { id: Name("T"), range: 93..94, @@ -372,8 +372,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 122..123, node_index: NodeIndex(None), + range: 122..123, name: Identifier { id: Name("T"), range: 122..123, @@ -464,8 +464,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 150..151, node_index: NodeIndex(None), + range: 150..151, name: Identifier { id: Name("T"), range: 150..151, @@ -540,8 +540,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 179..180, node_index: NodeIndex(None), + range: 179..180, name: Identifier { id: Name("T"), range: 179..180, @@ -630,8 +630,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 212..213, node_index: NodeIndex(None), + range: 212..213, name: Identifier { id: Name("T"), range: 212..213, @@ -704,8 +704,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 246..258, node_index: NodeIndex(None), + range: 246..258, name: Identifier { id: Name("T"), range: 246..247, @@ -780,8 +780,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 303..316, node_index: NodeIndex(None), + range: 303..316, name: Identifier { id: Name("T"), range: 303..304, @@ -856,8 +856,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 362..377, node_index: NodeIndex(None), + range: 362..377, name: Identifier { id: Name("Ts"), range: 363..365, @@ -931,8 +931,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 426..442, node_index: NodeIndex(None), + range: 426..442, name: Identifier { id: Name("Ts"), range: 428..430, @@ -1006,8 +1006,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 487..498, node_index: NodeIndex(None), + range: 487..498, name: Identifier { id: Name("T"), range: 487..488, @@ -1088,8 +1088,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 549..561, node_index: NodeIndex(None), + range: 549..561, name: Identifier { id: Name("T"), range: 549..550, @@ -1170,8 +1170,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 613..627, node_index: NodeIndex(None), + range: 613..627, name: Identifier { id: Name("Ts"), range: 614..616, @@ -1251,8 +1251,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 682..697, node_index: NodeIndex(None), + range: 682..697, name: Identifier { id: Name("Ts"), range: 684..686, @@ -1332,8 +1332,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 748..760, node_index: NodeIndex(None), + range: 748..760, name: Identifier { id: Name("T"), range: 748..749, @@ -1406,8 +1406,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 806..819, node_index: NodeIndex(None), + range: 806..819, name: Identifier { id: Name("T"), range: 806..807, @@ -1480,8 +1480,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 866..881, node_index: NodeIndex(None), + range: 866..881, name: Identifier { id: Name("Ts"), range: 867..869, @@ -1553,8 +1553,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 931..947, node_index: NodeIndex(None), + range: 931..947, name: Identifier { id: Name("Ts"), range: 933..935, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_type_alias.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_type_alias.py.snap index 61e633131d..5a05ef64f6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_type_alias.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_type_alias.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 7..19, node_index: NodeIndex(None), + range: 7..19, name: Identifier { id: Name("T"), range: 7..8, @@ -90,8 +90,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 55..68, node_index: NodeIndex(None), + range: 55..68, name: Identifier { id: Name("T"), range: 55..56, @@ -151,8 +151,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 105..120, node_index: NodeIndex(None), + range: 105..120, name: Identifier { id: Name("Ts"), range: 106..108, @@ -211,8 +211,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 160..176, node_index: NodeIndex(None), + range: 160..176, name: Identifier { id: Name("Ts"), range: 162..164, @@ -341,8 +341,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 315..327, node_index: NodeIndex(None), + range: 315..327, name: Identifier { id: Name("T"), range: 315..316, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap index bafb792f6a..9618f4200b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap @@ -28,8 +28,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 808..809, node_index: NodeIndex(None), + range: 808..809, name: Identifier { id: Name("A"), range: 808..809, @@ -41,8 +41,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 811..816, node_index: NodeIndex(None), + range: 811..816, name: Identifier { id: Name("await"), range: 811..816, @@ -99,8 +99,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 847..848, node_index: NodeIndex(None), + range: 847..848, name: Identifier { id: Name("A"), range: 847..848, @@ -112,8 +112,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 853..854, node_index: NodeIndex(None), + range: 853..854, name: Identifier { id: Name("B"), range: 853..854, @@ -170,8 +170,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 884..885, node_index: NodeIndex(None), + range: 884..885, name: Identifier { id: Name("A"), range: 884..885, @@ -183,8 +183,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 887..888, node_index: NodeIndex(None), + range: 887..888, name: Identifier { id: Name("B"), range: 887..888, @@ -241,8 +241,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 927..928, node_index: NodeIndex(None), + range: 927..928, name: Identifier { id: Name("A"), range: 927..928, @@ -299,8 +299,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 973..974, node_index: NodeIndex(None), + range: 973..974, name: Identifier { id: Name("A"), range: 973..974, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap index 60ecaa07d2..996be97aa4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 51..58, node_index: NodeIndex(None), + range: 51..58, name: Identifier { id: Name("T"), range: 51..52, @@ -80,8 +80,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 72..79, node_index: NodeIndex(None), + range: 72..79, name: Identifier { id: Name("T"), range: 72..73, @@ -146,8 +146,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 96..103, node_index: NodeIndex(None), + range: 96..103, name: Identifier { id: Name("T"), range: 96..97, @@ -210,8 +210,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 120..121, node_index: NodeIndex(None), + range: 120..121, name: Identifier { id: Name("S"), range: 120..121, @@ -223,8 +223,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 123..130, node_index: NodeIndex(None), + range: 123..130, name: Identifier { id: Name("T"), range: 123..124, @@ -245,8 +245,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 132..140, node_index: NodeIndex(None), + range: 132..140, name: Identifier { id: Name("U"), range: 132..133, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap index 46a51aa71b..321704cd05 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 7..14, node_index: NodeIndex(None), + range: 7..14, name: Identifier { id: Name("T"), range: 7..8, @@ -88,8 +88,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 29..39, node_index: NodeIndex(None), + range: 29..39, name: Identifier { id: Name("T"), range: 29..30, @@ -148,8 +148,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 54..69, node_index: NodeIndex(None), + range: 54..69, name: Identifier { id: Name("T"), range: 54..55, @@ -206,8 +206,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 84..88, node_index: NodeIndex(None), + range: 84..88, name: Identifier { id: Name("T"), range: 84..85, @@ -228,8 +228,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 92..95, node_index: NodeIndex(None), + range: 92..95, name: Identifier { id: Name("int"), range: 92..95, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_bound.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_bound.py.snap index 91001fe28b..d029828b20 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_bound.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_bound.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 7..9, node_index: NodeIndex(None), + range: 7..9, name: Identifier { id: Name("T"), range: 7..8, @@ -72,8 +72,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 25..28, node_index: NodeIndex(None), + range: 25..28, name: Identifier { id: Name("T1"), range: 25..27, @@ -85,8 +85,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 31..33, node_index: NodeIndex(None), + range: 31..33, name: Identifier { id: Name("T2"), range: 31..33, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap index cdda1b6d5c..de9da4848d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 7..10, node_index: NodeIndex(None), + range: 7..10, name: Identifier { id: Name("T"), range: 9..10, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap index 01b78df9f6..dad7c709de 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 7..17, node_index: NodeIndex(None), + range: 7..17, name: Identifier { id: Name("P"), range: 9..10, @@ -87,8 +87,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 32..45, node_index: NodeIndex(None), + range: 32..45, name: Identifier { id: Name("P"), range: 34..35, @@ -146,8 +146,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 60..78, node_index: NodeIndex(None), + range: 60..78, name: Identifier { id: Name("P"), range: 62..63, @@ -203,8 +203,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 93..100, node_index: NodeIndex(None), + range: 93..100, name: Identifier { id: Name("P"), range: 95..96, @@ -224,8 +224,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 104..107, node_index: NodeIndex(None), + range: 104..107, name: Identifier { id: Name("int"), range: 104..107, @@ -267,8 +267,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 122..132, node_index: NodeIndex(None), + range: 122..132, name: Identifier { id: Name("P"), range: 124..125, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_default.py.snap index 1a9ac4c029..1036d8c59a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_default.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 7..12, node_index: NodeIndex(None), + range: 7..12, name: Identifier { id: Name("P"), range: 9..10, @@ -71,8 +71,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 27..32, node_index: NodeIndex(None), + range: 27..32, name: Identifier { id: Name("P"), range: 29..30, @@ -83,8 +83,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 34..36, node_index: NodeIndex(None), + range: 34..36, name: Identifier { id: Name("T2"), range: 34..36, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap index d8b0ce565c..2831009c20 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 7..15, node_index: NodeIndex(None), + range: 7..15, name: Identifier { id: Name("T"), range: 7..8, @@ -88,8 +88,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 30..41, node_index: NodeIndex(None), + range: 30..41, name: Identifier { id: Name("T"), range: 30..31, @@ -148,8 +148,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 56..69, node_index: NodeIndex(None), + range: 56..69, name: Identifier { id: Name("T"), range: 56..57, @@ -208,8 +208,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 84..100, node_index: NodeIndex(None), + range: 84..100, name: Identifier { id: Name("T"), range: 84..85, @@ -266,8 +266,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 115..120, node_index: NodeIndex(None), + range: 115..120, name: Identifier { id: Name("T"), range: 115..116, @@ -288,8 +288,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 124..127, node_index: NodeIndex(None), + range: 124..127, name: Identifier { id: Name("int"), range: 124..127, @@ -331,8 +331,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 142..155, node_index: NodeIndex(None), + range: 142..155, name: Identifier { id: Name("T"), range: 142..143, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_missing_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_missing_default.py.snap index 1ecc4bfb34..55290b5e18 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_missing_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_missing_default.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 7..10, node_index: NodeIndex(None), + range: 7..10, name: Identifier { id: Name("T"), range: 7..8, @@ -72,8 +72,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 25..33, node_index: NodeIndex(None), + range: 25..33, name: Identifier { id: Name("T"), range: 25..26, @@ -124,8 +124,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 48..52, node_index: NodeIndex(None), + range: 48..52, name: Identifier { id: Name("T1"), range: 48..50, @@ -137,8 +137,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 54..56, node_index: NodeIndex(None), + range: 54..56, name: Identifier { id: Name("T2"), range: 54..56, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap index 1f30abc277..e1693e1722 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 7..9, node_index: NodeIndex(None), + range: 7..9, name: Identifier { id: Name("T"), range: 8..9, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap index 8e8de2e274..9b2d1c6de9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 7..17, node_index: NodeIndex(None), + range: 7..17, name: Identifier { id: Name("Ts"), range: 8..10, @@ -87,8 +87,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 32..49, node_index: NodeIndex(None), + range: 32..49, name: Identifier { id: Name("Ts"), range: 33..35, @@ -162,8 +162,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 64..77, node_index: NodeIndex(None), + range: 64..77, name: Identifier { id: Name("Ts"), range: 65..67, @@ -221,8 +221,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 92..110, node_index: NodeIndex(None), + range: 92..110, name: Identifier { id: Name("Ts"), range: 93..95, @@ -278,8 +278,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 125..132, node_index: NodeIndex(None), + range: 125..132, name: Identifier { id: Name("Ts"), range: 126..128, @@ -299,8 +299,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 136..139, node_index: NodeIndex(None), + range: 136..139, name: Identifier { id: Name("int"), range: 136..139, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_missing_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_missing_default.py.snap index fb3c198af0..313733b6e5 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_missing_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_missing_default.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 7..12, node_index: NodeIndex(None), + range: 7..12, name: Identifier { id: Name("Ts"), range: 8..10, @@ -71,8 +71,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 27..32, node_index: NodeIndex(None), + range: 27..32, name: Identifier { id: Name("Ts"), range: 28..30, @@ -83,8 +83,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 34..36, node_index: NodeIndex(None), + range: 34..36, name: Identifier { id: Name("T2"), range: 34..36, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_type_params_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_type_params_py312.py.snap index d56ec292ba..85a9bc9f78 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_type_params_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_type_params_py312.py.snap @@ -27,8 +27,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 54..69, node_index: NodeIndex(None), + range: 54..69, name: Identifier { id: Name("S"), range: 54..55, @@ -67,8 +67,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 71..79, node_index: NodeIndex(None), + range: 71..79, name: Identifier { id: Name("T"), range: 71..72, @@ -89,8 +89,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 81..84, node_index: NodeIndex(None), + range: 81..84, name: Identifier { id: Name("Ts"), range: 82..84, @@ -101,8 +101,8 @@ Module( ), ParamSpec( TypeParamParamSpec { - range: 86..89, node_index: NodeIndex(None), + range: 86..89, name: Identifier { id: Name("P"), range: 88..89, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_type_params_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_type_params_py312.py.snap index 4dee727ea1..c60829f2b7 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_type_params_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_type_params_py312.py.snap @@ -28,8 +28,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 52..53, node_index: NodeIndex(None), + range: 52..53, name: Identifier { id: Name("T"), range: 52..53, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_duplicate_type_parameter_names.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_duplicate_type_parameter_names.py.snap index c1a5fcb445..91e9cabf55 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_duplicate_type_parameter_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_duplicate_type_parameter_names.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 11..12, node_index: NodeIndex(None), + range: 11..12, name: Identifier { id: Name("T"), range: 11..12, @@ -86,8 +86,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 30..31, node_index: NodeIndex(None), + range: 30..31, name: Identifier { id: Name("T"), range: 30..31, @@ -168,8 +168,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 52..53, node_index: NodeIndex(None), + range: 52..53, name: Identifier { id: Name("T"), range: 52..53, @@ -216,8 +216,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 68..69, node_index: NodeIndex(None), + range: 68..69, name: Identifier { id: Name("T"), range: 68..69, @@ -229,8 +229,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 71..72, node_index: NodeIndex(None), + range: 71..72, name: Identifier { id: Name("U"), range: 71..72, @@ -242,8 +242,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 74..75, node_index: NodeIndex(None), + range: 74..75, name: Identifier { id: Name("V"), range: 74..75, @@ -292,8 +292,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 93..94, node_index: NodeIndex(None), + range: 93..94, name: Identifier { id: Name("T"), range: 93..94, @@ -305,8 +305,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 96..102, node_index: NodeIndex(None), + range: 96..102, name: Identifier { id: Name("U"), range: 96..97, @@ -327,8 +327,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 104..119, node_index: NodeIndex(None), + range: 104..119, name: Identifier { id: Name("V"), range: 104..105, @@ -367,8 +367,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 121..124, node_index: NodeIndex(None), + range: 121..124, name: Identifier { id: Name("Ts"), range: 122..124, @@ -379,8 +379,8 @@ Module( ), ParamSpec( TypeParamParamSpec { - range: 126..129, node_index: NodeIndex(None), + range: 126..129, name: Identifier { id: Name("P"), range: 128..129, @@ -391,8 +391,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 131..142, node_index: NodeIndex(None), + range: 131..142, name: Identifier { id: Name("D"), range: 131..132, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__class.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__class.py.snap index 29935b6485..9ce6fb674c 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__class.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__class.py.snap @@ -468,8 +468,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 342..343, node_index: NodeIndex(None), + range: 342..343, name: Identifier { id: Name("T"), range: 342..343, @@ -523,8 +523,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 387..394, node_index: NodeIndex(None), + range: 387..394, name: Identifier { id: Name("T"), range: 387..388, @@ -587,8 +587,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 436..442, node_index: NodeIndex(None), + range: 436..442, name: Identifier { id: Name("T"), range: 436..437, @@ -651,8 +651,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 496..514, node_index: NodeIndex(None), + range: 496..514, name: Identifier { id: Name("T"), range: 496..497, @@ -739,8 +739,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 562..577, node_index: NodeIndex(None), + range: 562..577, name: Identifier { id: Name("T"), range: 562..563, @@ -821,8 +821,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 617..618, node_index: NodeIndex(None), + range: 617..618, name: Identifier { id: Name("T"), range: 617..618, @@ -834,8 +834,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 620..621, node_index: NodeIndex(None), + range: 620..621, name: Identifier { id: Name("U"), range: 620..621, @@ -889,8 +889,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 659..660, node_index: NodeIndex(None), + range: 659..660, name: Identifier { id: Name("T"), range: 659..660, @@ -902,8 +902,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 662..663, node_index: NodeIndex(None), + range: 662..663, name: Identifier { id: Name("U"), range: 662..663, @@ -957,8 +957,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 700..703, node_index: NodeIndex(None), + range: 700..703, name: Identifier { id: Name("Ts"), range: 701..703, @@ -1011,8 +1011,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 752..781, node_index: NodeIndex(None), + range: 752..781, name: Identifier { id: Name("Ts"), range: 753..755, @@ -1122,8 +1122,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 838..860, node_index: NodeIndex(None), + range: 838..860, name: Identifier { id: Name("Ts"), range: 839..841, @@ -1225,8 +1225,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 893..896, node_index: NodeIndex(None), + range: 893..896, name: Identifier { id: Name("P"), range: 895..896, @@ -1279,8 +1279,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 942..958, node_index: NodeIndex(None), + range: 942..958, name: Identifier { id: Name("P"), range: 944..945, @@ -1359,8 +1359,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 993..994, node_index: NodeIndex(None), + range: 993..994, name: Identifier { id: Name("X"), range: 993..994, @@ -1372,8 +1372,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 996..1002, node_index: NodeIndex(None), + range: 996..1002, name: Identifier { id: Name("Y"), range: 996..997, @@ -1394,8 +1394,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 1004..1006, node_index: NodeIndex(None), + range: 1004..1006, name: Identifier { id: Name("U"), range: 1005..1006, @@ -1406,8 +1406,8 @@ Module( ), ParamSpec( TypeParamParamSpec { - range: 1008..1011, node_index: NodeIndex(None), + range: 1008..1011, name: Identifier { id: Name("P"), range: 1010..1011, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__function.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__function.py.snap index 0ecafb822d..b115bcc409 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__function.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__function.py.snap @@ -2387,8 +2387,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 1712..1713, node_index: NodeIndex(None), + range: 1712..1713, name: Identifier { id: Name("T"), range: 1712..1713, @@ -2473,8 +2473,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 1747..1753, node_index: NodeIndex(None), + range: 1747..1753, name: Identifier { id: Name("T"), range: 1747..1748, @@ -2568,8 +2568,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 1787..1802, node_index: NodeIndex(None), + range: 1787..1802, name: Identifier { id: Name("T"), range: 1787..1788, @@ -2681,8 +2681,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 1836..1839, node_index: NodeIndex(None), + range: 1836..1839, name: Identifier { id: Name("Ts"), range: 1837..1839, @@ -2800,8 +2800,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 1885..1888, node_index: NodeIndex(None), + range: 1885..1888, name: Identifier { id: Name("P"), range: 1887..1888, @@ -2915,8 +2915,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 1946..1947, node_index: NodeIndex(None), + range: 1946..1947, name: Identifier { id: Name("T"), range: 1946..1947, @@ -2928,8 +2928,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 1949..1955, node_index: NodeIndex(None), + range: 1949..1955, name: Identifier { id: Name("U"), range: 1949..1950, @@ -2950,8 +2950,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 1957..1960, node_index: NodeIndex(None), + range: 1957..1960, name: Identifier { id: Name("Ts"), range: 1958..1960, @@ -2962,8 +2962,8 @@ Module( ), ParamSpec( TypeParamParamSpec { - range: 1962..1965, node_index: NodeIndex(None), + range: 1962..1965, name: Identifier { id: Name("P"), range: 1964..1965, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__type.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__type.py.snap index 608679f328..6b807b46b0 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__type.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__type.py.snap @@ -141,8 +141,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 68..69, node_index: NodeIndex(None), + range: 68..69, name: Identifier { id: Name("T"), range: 68..69, @@ -229,8 +229,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 108..109, node_index: NodeIndex(None), + range: 108..109, name: Identifier { id: Name("T"), range: 108..109, @@ -272,8 +272,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 124..125, node_index: NodeIndex(None), + range: 124..125, name: Identifier { id: Name("T"), range: 124..125, @@ -360,8 +360,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 153..154, node_index: NodeIndex(None), + range: 153..154, name: Identifier { id: Name("T"), range: 153..154, @@ -373,8 +373,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 156..159, node_index: NodeIndex(None), + range: 156..159, name: Identifier { id: Name("Ts"), range: 157..159, @@ -385,8 +385,8 @@ Module( ), ParamSpec( TypeParamParamSpec { - range: 161..164, node_index: NodeIndex(None), + range: 161..164, name: Identifier { id: Name("P"), range: 163..164, @@ -453,8 +453,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 186..192, node_index: NodeIndex(None), + range: 186..192, name: Identifier { id: Name("T"), range: 186..187, @@ -475,8 +475,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 194..197, node_index: NodeIndex(None), + range: 194..197, name: Identifier { id: Name("Ts"), range: 195..197, @@ -487,8 +487,8 @@ Module( ), ParamSpec( TypeParamParamSpec { - range: 199..202, node_index: NodeIndex(None), + range: 199..202, name: Identifier { id: Name("P"), range: 201..202, @@ -555,8 +555,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 224..237, node_index: NodeIndex(None), + range: 224..237, name: Identifier { id: Name("T"), range: 224..225, @@ -595,8 +595,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 239..242, node_index: NodeIndex(None), + range: 239..242, name: Identifier { id: Name("Ts"), range: 240..242, @@ -607,8 +607,8 @@ Module( ), ParamSpec( TypeParamParamSpec { - range: 244..247, node_index: NodeIndex(None), + range: 244..247, name: Identifier { id: Name("P"), range: 246..247, @@ -675,8 +675,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 269..276, node_index: NodeIndex(None), + range: 269..276, name: Identifier { id: Name("T"), range: 269..270, @@ -742,8 +742,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 295..313, node_index: NodeIndex(None), + range: 295..313, name: Identifier { id: Name("T"), range: 295..296, @@ -848,8 +848,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 338..360, node_index: NodeIndex(None), + range: 338..360, name: Identifier { id: Name("Ts"), range: 339..341, @@ -987,8 +987,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 392..408, node_index: NodeIndex(None), + range: 392..408, name: Identifier { id: Name("P"), range: 394..395, @@ -1318,8 +1318,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 687..688, node_index: NodeIndex(None), + range: 687..688, name: Identifier { id: Name("T"), range: 687..688, @@ -1361,8 +1361,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 708..709, node_index: NodeIndex(None), + range: 708..709, name: Identifier { id: Name("T"), range: 708..709, @@ -1404,8 +1404,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 722..723, node_index: NodeIndex(None), + range: 722..723, name: Identifier { id: Name("T"), range: 722..723, @@ -1611,8 +1611,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 865..866, node_index: NodeIndex(None), + range: 865..866, name: Identifier { id: Name("T"), range: 865..866, @@ -1687,8 +1687,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 895..898, node_index: NodeIndex(None), + range: 895..898, name: Identifier { id: Name("P"), range: 897..898, @@ -1762,8 +1762,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 950..953, node_index: NodeIndex(None), + range: 950..953, name: Identifier { id: Name("Ts"), range: 951..953, @@ -1844,8 +1844,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 1011..1022, node_index: NodeIndex(None), + range: 1011..1022, name: Identifier { id: Name("T"), range: 1011..1012, @@ -1911,8 +1911,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 1082..1095, node_index: NodeIndex(None), + range: 1082..1095, name: Identifier { id: Name("T"), range: 1082..1083, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_default_py313.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_default_py313.py.snap index f673f2901a..df2192d1fb 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_default_py313.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_default_py313.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 51..58, node_index: NodeIndex(None), + range: 51..58, name: Identifier { id: Name("T"), range: 51..52, @@ -80,8 +80,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 72..79, node_index: NodeIndex(None), + range: 72..79, name: Identifier { id: Name("T"), range: 72..73, @@ -146,8 +146,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 96..103, node_index: NodeIndex(None), + range: 96..103, name: Identifier { id: Name("T"), range: 96..97, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_param_spec.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_param_spec.py.snap index 8ce68efabf..abeecd8020 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_param_spec.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_param_spec.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 7..10, node_index: NodeIndex(None), + range: 7..10, name: Identifier { id: Name("P"), range: 9..10, @@ -71,8 +71,8 @@ Module( type_params: [ ParamSpec( TypeParamParamSpec { - range: 25..34, node_index: NodeIndex(None), + range: 25..34, name: Identifier { id: Name("P"), range: 27..28, @@ -122,8 +122,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 49..50, node_index: NodeIndex(None), + range: 49..50, name: Identifier { id: Name("T"), range: 49..50, @@ -135,8 +135,8 @@ Module( ), ParamSpec( TypeParamParamSpec { - range: 52..55, node_index: NodeIndex(None), + range: 52..55, name: Identifier { id: Name("P"), range: 54..55, @@ -177,8 +177,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 70..71, node_index: NodeIndex(None), + range: 70..71, name: Identifier { id: Name("T"), range: 70..71, @@ -190,8 +190,8 @@ Module( ), ParamSpec( TypeParamParamSpec { - range: 73..82, node_index: NodeIndex(None), + range: 73..82, name: Identifier { id: Name("P"), range: 75..76, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var.py.snap index 39808ecfd6..e3aad35342 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 7..8, node_index: NodeIndex(None), + range: 7..8, name: Identifier { id: Name("T"), range: 7..8, @@ -72,8 +72,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 23..30, node_index: NodeIndex(None), + range: 23..30, name: Identifier { id: Name("T"), range: 23..24, @@ -124,8 +124,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 45..57, node_index: NodeIndex(None), + range: 45..57, name: Identifier { id: Name("T"), range: 45..46, @@ -185,8 +185,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 72..91, node_index: NodeIndex(None), + range: 72..91, name: Identifier { id: Name("T"), range: 72..73, @@ -264,8 +264,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 106..118, node_index: NodeIndex(None), + range: 106..118, name: Identifier { id: Name("T"), range: 106..107, @@ -295,8 +295,8 @@ Module( ), TypeVar( TypeParamTypeVar { - range: 120..139, node_index: NodeIndex(None), + range: 120..139, name: Identifier { id: Name("U"), range: 120..121, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var_tuple.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var_tuple.py.snap index 5aab990b65..715499e6fd 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var_tuple.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var_tuple.py.snap @@ -29,8 +29,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 7..10, node_index: NodeIndex(None), + range: 7..10, name: Identifier { id: Name("Ts"), range: 8..10, @@ -71,8 +71,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 25..34, node_index: NodeIndex(None), + range: 25..34, name: Identifier { id: Name("Ts"), range: 26..28, @@ -122,8 +122,8 @@ Module( type_params: [ TypeVarTuple( TypeParamTypeVarTuple { - range: 49..59, node_index: NodeIndex(None), + range: 49..59, name: Identifier { id: Name("Ts"), range: 50..52, @@ -180,8 +180,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 74..75, node_index: NodeIndex(None), + range: 74..75, name: Identifier { id: Name("T"), range: 74..75, @@ -193,8 +193,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 77..80, node_index: NodeIndex(None), + range: 77..80, name: Identifier { id: Name("Ts"), range: 78..80, @@ -235,8 +235,8 @@ Module( type_params: [ TypeVar( TypeParamTypeVar { - range: 95..96, node_index: NodeIndex(None), + range: 95..96, name: Identifier { id: Name("T"), range: 95..96, @@ -248,8 +248,8 @@ Module( ), TypeVarTuple( TypeParamTypeVarTuple { - range: 98..107, node_index: NodeIndex(None), + range: 98..107, name: Identifier { id: Name("Ts"), range: 99..101, From 58a68f1bbd7aa23d156ad6aeec0a62104a5f753c Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 22 Oct 2025 14:29:10 +0200 Subject: [PATCH 010/188] [ty] Fall back to `Divergent` for deeply nested specializations (#20988) ## Summary Fall back to `C[Divergent]` if we are trying to specialize `C[T]` with a type that itself already contains deeply nested specialized generic classes. This is a way to prevent infinite recursion for cases like `self.x = [self.x]` where type inference for the implicit instance attribute would not converge. closes https://github.com/astral-sh/ty/issues/1383 closes https://github.com/astral-sh/ty/issues/837 ## Test Plan Regression tests. --- .../resources/mdtest/attributes.md | 95 ++++++++++- .../resources/mdtest/pep613_type_aliases.md | 17 ++ crates/ty_python_semantic/src/types.rs | 24 ++- crates/ty_python_semantic/src/types/class.rs | 16 +- .../ty_python_semantic/src/types/generics.rs | 19 +++ crates/ty_python_semantic/src/types/infer.rs | 2 +- .../src/types/infer/builder.rs | 9 +- .../types/infer/builder/type_expression.rs | 4 +- .../ty_python_semantic/src/types/instance.rs | 5 +- .../ty_python_semantic/src/types/visitor.rs | 152 +++++++++++++++++- 10 files changed, 317 insertions(+), 26 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 94edff19e6..e92eaec9ce 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2457,6 +2457,48 @@ class Counter: reveal_type(Counter().count) # revealed: Unknown | int ``` +We also handle infinitely nested generics: + +```py +class NestedLists: + def __init__(self: "NestedLists"): + self.x = 1 + + def f(self: "NestedLists"): + self.x = [self.x] + +reveal_type(NestedLists().x) # revealed: Unknown | Literal[1] | list[Divergent] + +class NestedMixed: + def f(self: "NestedMixed"): + self.x = [self.x] + + def g(self: "NestedMixed"): + self.x = {self.x} + + def h(self: "NestedMixed"): + self.x = {"a": self.x} + +reveal_type(NestedMixed().x) # revealed: Unknown | list[Divergent] | set[Divergent] | dict[Unknown | str, Divergent] +``` + +And cases where the types originate from annotations: + +```py +from typing import TypeVar + +T = TypeVar("T") + +def make_list(value: T) -> list[T]: + return [value] + +class NestedLists2: + def f(self: "NestedLists2"): + self.x = make_list(self.x) + +reveal_type(NestedLists2().x) # revealed: Unknown | list[Divergent] +``` + ### Builtin types attributes This test can probably be removed eventually, but we currently include it because we do not yet @@ -2551,13 +2593,54 @@ reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Unknown] ## Divergent inferred implicit instance attribute types ```py -# TODO: This test currently panics, see https://github.com/astral-sh/ty/issues/837 +class C: + def f(self, other: "C"): + self.x = (other.x, 1) -# class C: -# def f(self, other: "C"): -# self.x = (other.x, 1) -# -# reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]] +reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]] +``` + +This also works if the tuple is not constructed directly: + +```py +from typing import TypeVar, Literal + +T = TypeVar("T") + +def make_tuple(x: T) -> tuple[T, Literal[1]]: + return (x, 1) + +class D: + def f(self, other: "D"): + self.x = make_tuple(other.x) + +reveal_type(D().x) # revealed: Unknown | tuple[Divergent, Literal[1]] +``` + +The tuple type may also expand exponentially "in breadth": + +```py +def duplicate(x: T) -> tuple[T, T]: + return (x, x) + +class E: + def f(self: "E"): + self.x = duplicate(self.x) + +reveal_type(E().x) # revealed: Unknown | tuple[Divergent, Divergent] +``` + +And it also works for homogeneous tuples: + +```py +def make_homogeneous_tuple(x: T) -> tuple[T, ...]: + return (x, x) + +class E: + def f(self, other: "E"): + self.x = make_homogeneous_tuple(other.x) + +reveal_type(E().x) # revealed: Unknown | tuple[Divergent, ...] ``` ## Attributes of standard library modules that aren't yet defined diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md new file mode 100644 index 0000000000..de3851ddab --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -0,0 +1,17 @@ +# PEP 613 type aliases + +We do not support PEP 613 type aliases yet. For now, just make sure that we don't panic: + +```py +from typing import TypeAlias + +RecursiveTuple: TypeAlias = tuple[int | "RecursiveTuple", str] + +def _(rec: RecursiveTuple): + reveal_type(rec) # revealed: tuple[Divergent, str] + +RecursiveHomogeneousTuple: TypeAlias = tuple[int | "RecursiveHomogeneousTuple", ...] + +def _(rec: RecursiveHomogeneousTuple): + reveal_type(rec) # revealed: tuple[Divergent, ...] +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 8fdc21e2b5..465df24583 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -69,7 +69,7 @@ use crate::types::tuple::{TupleSpec, TupleSpecBuilder}; 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::types::visitor::{any_over_type, exceeds_max_specialization_depth}; use crate::unpack::EvaluationMode; use crate::{Db, FxOrderSet, Module, Program}; pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass}; @@ -827,10 +827,14 @@ impl<'db> Type<'db> { Self::Dynamic(DynamicType::Unknown) } - pub(crate) fn divergent(scope: ScopeId<'db>) -> Self { + pub(crate) fn divergent(scope: Option>) -> Self { Self::Dynamic(DynamicType::Divergent(DivergentType { scope })) } + pub(crate) const fn is_divergent(&self) -> bool { + matches!(self, Type::Dynamic(DynamicType::Divergent(_))) + } + pub const fn is_unknown(&self) -> bool { matches!(self, Type::Dynamic(DynamicType::Unknown)) } @@ -6652,7 +6656,7 @@ impl<'db> Type<'db> { match self { Type::TypeVar(bound_typevar) => match type_mapping { TypeMapping::Specialization(specialization) => { - specialization.get(db, bound_typevar).unwrap_or(self) + specialization.get(db, bound_typevar).unwrap_or(self).fallback_to_divergent(db) } TypeMapping::PartialSpecialization(partial) => { partial.get(db, bound_typevar).unwrap_or(self) @@ -7214,6 +7218,16 @@ impl<'db> Type<'db> { pub(super) fn has_divergent_type(self, db: &'db dyn Db, div: Type<'db>) -> bool { any_over_type(db, self, &|ty| ty == div, false) } + + /// If the specialization depth of `self` exceeds the maximum limit allowed, + /// return `Divergent`. Otherwise, return `self`. + pub(super) fn fallback_to_divergent(self, db: &'db dyn Db) -> Type<'db> { + if exceeds_max_specialization_depth(db, self) { + Type::divergent(None) + } else { + self + } + } } impl<'db> From<&Type<'db>> for Type<'db> { @@ -7659,7 +7673,7 @@ impl<'db> KnownInstanceType<'db> { #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)] pub struct DivergentType<'db> { /// The scope where this divergence was detected. - scope: ScopeId<'db>, + scope: Option>, } #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)] @@ -11772,7 +11786,7 @@ pub(crate) mod tests { let file_scope_id = FileScopeId::global(); let scope = file_scope_id.to_scope_id(&db, file); - let div = Type::Dynamic(DynamicType::Divergent(DivergentType { scope })); + let div = Type::Dynamic(DynamicType::Divergent(DivergentType { scope: Some(scope) })); // The `Divergent` type must not be eliminated in union with other dynamic types, // as this would prevent detection of divergent type inference using `Divergent`. diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index c7b96cfa7e..8d46f1b57a 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -37,7 +37,8 @@ use crate::types::{ IsDisjointVisitor, IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeContext, TypeMapping, TypeRelation, TypedDictParams, UnionBuilder, VarianceInferable, - declaration_type, determine_upper_bound, infer_definition_types, + declaration_type, determine_upper_bound, exceeds_max_specialization_depth, + infer_definition_types, }; use crate::{ Db, FxIndexMap, FxIndexSet, FxOrderSet, Program, @@ -1612,7 +1613,18 @@ impl<'db> ClassLiteral<'db> { match self.generic_context(db) { None => ClassType::NonGeneric(self), Some(generic_context) => { - let specialization = f(generic_context); + let mut specialization = f(generic_context); + + for (idx, ty) in specialization.types(db).iter().enumerate() { + if exceeds_max_specialization_depth(db, *ty) { + specialization = specialization.with_replaced_type( + db, + idx, + Type::divergent(Some(self.body_scope(db))), + ); + } + } + ClassType::Generic(GenericAlias::new(db, self, specialization)) } } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index f401e0df3f..249c406a1b 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1264,6 +1264,25 @@ impl<'db> Specialization<'db> { // A tuple's specialization will include all of its element types, so we don't need to also // look in `self.tuple`. } + + /// Returns a copy of this specialization with the type at a given index replaced. + pub(crate) fn with_replaced_type( + self, + db: &'db dyn Db, + index: usize, + new_type: Type<'db>, + ) -> Self { + let mut new_types: Box<[_]> = self.types(db).to_vec().into_boxed_slice(); + new_types[index] = new_type; + + Self::new( + db, + self.generic_context(db), + new_types, + self.materialization_kind(db), + self.tuple_inner(db), + ) + } } /// A mapping between type variables and types. diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index f2c256f304..1bd539c074 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -567,7 +567,7 @@ impl<'db> CycleRecovery<'db> { fn fallback_type(self) -> Type<'db> { match self { Self::Initial => Type::Never, - Self::Divergent(scope) => Type::divergent(scope), + Self::Divergent(scope) => Type::divergent(Some(scope)), } } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 238b95a4a8..99df77b71d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -5968,16 +5968,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut annotated_elt_tys = annotated_tuple.as_ref().map(Tuple::all_elements); let db = self.db(); - let divergent = Type::divergent(self.scope()); let element_types = elts.iter().map(|element| { let annotated_elt_ty = annotated_elt_tys.as_mut().and_then(Iterator::next).copied(); - let element_type = self.infer_expression(element, TypeContext::new(annotated_elt_ty)); - - if element_type.has_divergent_type(self.db(), divergent) { - divergent - } else { - element_type - } + self.infer_expression(element, TypeContext::new(annotated_elt_ty)) }); Type::heterogeneous_tuple(db, element_types) 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 69c6b8a165..3c7bdb5464 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 @@ -22,7 +22,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// Infer the type of a type expression. pub(super) fn infer_type_expression(&mut self, expression: &ast::Expr) -> Type<'db> { let mut ty = self.infer_type_expression_no_store(expression); - let divergent = Type::divergent(self.scope()); + let divergent = Type::divergent(Some(self.scope())); if ty.has_divergent_type(self.db(), divergent) { ty = divergent; } @@ -588,7 +588,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // TODO: emit a diagnostic } } else { - element_types.push(element_ty); + element_types.push(element_ty.fallback_to_divergent(self.db())); } } diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index c294cde618..b6d42caae3 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -72,7 +72,10 @@ impl<'db> Type<'db> { { Type::tuple(TupleType::heterogeneous( db, - elements.into_iter().map(Into::into), + elements + .into_iter() + .map(Into::into) + .map(|element| element.fallback_to_divergent(db)), )) } diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index 51b77432a4..d58bf046f1 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -1,3 +1,5 @@ +use rustc_hash::FxHashMap; + use crate::{ Db, FxIndexSet, types::{ @@ -16,7 +18,10 @@ use crate::{ walk_typed_dict_type, walk_typeis_type, walk_union, }, }; -use std::cell::{Cell, RefCell}; +use std::{ + cell::{Cell, RefCell}, + collections::hash_map::Entry, +}; /// A visitor trait that recurses into nested types. /// @@ -295,3 +300,148 @@ pub(super) fn any_over_type<'db>( visitor.visit_type(db, ty); visitor.found_matching_type.get() } + +/// Returns the maximum number of layers of generic specializations for a given type. +/// +/// For example, `int` has a depth of `0`, `list[int]` has a depth of `1`, and `list[set[int]]` +/// has a depth of `2`. A set-theoretic type like `list[int] | list[list[int]]` has a maximum +/// depth of `2`. +fn specialization_depth(db: &dyn Db, ty: Type<'_>) -> usize { + #[derive(Debug, Default)] + struct SpecializationDepthVisitor<'db> { + seen_types: RefCell, Option>>, + max_depth: Cell, + } + + impl<'db> TypeVisitor<'db> for SpecializationDepthVisitor<'db> { + fn should_visit_lazy_type_attributes(&self) -> bool { + false + } + + fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) { + match TypeKind::from(ty) { + TypeKind::Atomic => { + if ty.is_divergent() { + self.max_depth.set(usize::MAX); + } + } + TypeKind::NonAtomic(non_atomic_type) => { + match self.seen_types.borrow_mut().entry(non_atomic_type) { + Entry::Occupied(cached_depth) => { + self.max_depth + .update(|current| current.max(cached_depth.get().unwrap_or(0))); + return; + } + Entry::Vacant(entry) => { + entry.insert(None); + } + } + + let self_depth: usize = + matches!(non_atomic_type, NonAtomicType::GenericAlias(_)).into(); + + let previous_max_depth = self.max_depth.replace(0); + walk_non_atomic_type(db, non_atomic_type, self); + + self.max_depth.update(|max_child_depth| { + previous_max_depth.max(max_child_depth.saturating_add(self_depth)) + }); + + self.seen_types + .borrow_mut() + .insert(non_atomic_type, Some(self.max_depth.get())); + } + } + } + } + + let visitor = SpecializationDepthVisitor::default(); + visitor.visit_type(db, ty); + visitor.max_depth.get() +} + +pub(super) fn exceeds_max_specialization_depth(db: &dyn Db, ty: Type<'_>) -> bool { + // To prevent infinite recursion during type inference for infinite types, we fall back to + // `C[Divergent]` once a certain amount of levels of specialization have occurred. For + // example: + // + // ```py + // x = 1 + // while random_bool(): + // x = [x] + // + // reveal_type(x) # Unknown | Literal[1] | list[Divergent] + // ``` + const MAX_SPECIALIZATION_DEPTH: usize = 10; + + specialization_depth(db, ty) > MAX_SPECIALIZATION_DEPTH +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{db::tests::setup_db, types::KnownClass}; + + #[test] + fn test_generics_layering_depth() { + let db = setup_db(); + + let int = || KnownClass::Int.to_instance(&db); + let list = |element| KnownClass::List.to_specialized_instance(&db, [element]); + let dict = |key, value| KnownClass::Dict.to_specialized_instance(&db, [key, value]); + let set = |element| KnownClass::Set.to_specialized_instance(&db, [element]); + let str = || KnownClass::Str.to_instance(&db); + let bytes = || KnownClass::Bytes.to_instance(&db); + + let list_of_int = list(int()); + assert_eq!(specialization_depth(&db, list_of_int), 1); + + let list_of_list_of_int = list(list_of_int); + assert_eq!(specialization_depth(&db, list_of_list_of_int), 2); + + let list_of_list_of_list_of_int = list(list_of_list_of_int); + assert_eq!(specialization_depth(&db, list_of_list_of_list_of_int), 3); + + assert_eq!(specialization_depth(&db, set(dict(str(), list_of_int))), 3); + + assert_eq!( + specialization_depth( + &db, + UnionType::from_elements(&db, [list_of_list_of_list_of_int, list_of_list_of_int]) + ), + 3 + ); + + assert_eq!( + specialization_depth( + &db, + UnionType::from_elements(&db, [list_of_list_of_int, list_of_list_of_list_of_int]) + ), + 3 + ); + + assert_eq!( + specialization_depth( + &db, + Type::heterogeneous_tuple(&db, [Type::heterogeneous_tuple(&db, [int()])]) + ), + 2 + ); + + assert_eq!( + specialization_depth(&db, Type::heterogeneous_tuple(&db, [list_of_int, str()])), + 2 + ); + + assert_eq!( + specialization_depth( + &db, + list(UnionType::from_elements( + &db, + [list(int()), list(str()), list(bytes())] + )) + ), + 2 + ); + } +} From 20510e1d71737c82ec5e1ab0c3f01435fdc8bf9f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 22 Oct 2025 15:32:14 +0100 Subject: [PATCH 011/188] [ty] Set `INSTA_FORCE_PASS` and `INSTA_OUTPUT` environment variables from mdtest.py (#21029) --- crates/ty_python_semantic/mdtest.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/mdtest.py b/crates/ty_python_semantic/mdtest.py index c0795ed50c..8eb272f39d 100644 --- a/crates/ty_python_semantic/mdtest.py +++ b/crates/ty_python_semantic/mdtest.py @@ -106,7 +106,12 @@ class MDTestRunner: return subprocess.run( [self.mdtest_executable, *arguments], cwd=CRATE_ROOT, - env=dict(os.environ, CLICOLOR_FORCE="1"), + env=dict( + os.environ, + CLICOLOR_FORCE="1", + INSTA_FORCE_PASS="1", + INSTA_OUTPUT="none", + ), capture_output=capture_output, text=True, check=False, From 81c1d36088c1533282dae787e8ff3b470a18dbb4 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 22 Oct 2025 16:40:58 +0200 Subject: [PATCH 012/188] [ty] Make `attributes.md` mdtests faster (#21030) ## Summary That example was too extreme for debug mode. --- crates/ty_python_semantic/resources/mdtest/attributes.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index e92eaec9ce..f6c20d0c5f 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2476,10 +2476,7 @@ class NestedMixed: def g(self: "NestedMixed"): self.x = {self.x} - def h(self: "NestedMixed"): - self.x = {"a": self.x} - -reveal_type(NestedMixed().x) # revealed: Unknown | list[Divergent] | set[Divergent] | dict[Unknown | str, Divergent] +reveal_type(NestedMixed().x) # revealed: Unknown | list[Divergent] | set[Divergent] ``` And cases where the types originate from annotations: From 766ed5b5f394b3dae19dba4859aac797b48fbd00 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 22 Oct 2025 13:38:44 -0400 Subject: [PATCH 013/188] [ty] Some more simplifications when rendering constraint sets (#21009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds another useful simplification when rendering constraint sets: `T = int` instead of `T = int ∧ T ≠ str`. (The "smaller" constraint `T = int` implies the "larger" constraint `T ≠ str`. Constraint set clauses are intersections, and if one constraint in a clause implies another, we can throw away the "larger" constraint.) While we're here, we also normalize the bounds of a constraint, so that we equate e.g. `T ≤ int | str` with `T ≤ str | int`, and change the ordering of BDD variables so that all constraints with the same typevar are ordered adjacent to each other. Lastly, we also add a new `display_graph` helper method that prints out the full graph structure of a BDD. --------- Co-authored-by: Alex Waygood --- Cargo.lock | 2 + crates/ty_python_semantic/Cargo.toml | 2 + .../mdtest/type_properties/constraints.md | 37 +++ .../src/semantic_index/definition.rs | 5 + crates/ty_python_semantic/src/types.rs | 8 +- .../src/types/constraints.rs | 283 ++++++++++++++++-- 6 files changed, 314 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d917171a4..1bc41b0c9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4434,10 +4434,12 @@ dependencies = [ "glob", "hashbrown 0.16.0", "indexmap", + "indoc", "insta", "itertools 0.14.0", "memchr", "ordermap", + "pretty_assertions", "quickcheck", "quickcheck_macros", "ruff_annotate_snippets", diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml index edeafa821f..faf5c37881 100644 --- a/crates/ty_python_semantic/Cargo.toml +++ b/crates/ty_python_semantic/Cargo.toml @@ -63,7 +63,9 @@ ty_vendored = { workspace = true } anyhow = { workspace = true } dir-test = { workspace = true } glob = { workspace = true } +indoc = { workspace = true } insta = { workspace = true } +pretty_assertions = { workspace = true } tempfile = { workspace = true } quickcheck = { version = "1.0.3", default-features = false } quickcheck_macros = { version = "1.0.0" } diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md index 5791ba00de..607501a349 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md @@ -601,3 +601,40 @@ def _[T, U]() -> None: # revealed: ty_extensions.ConstraintSet[always] reveal_type(~union | union) ``` + +## Other simplifications + +When displaying a constraint set, we transform the internal BDD representation into a DNF formula +(i.e., the logical OR of several clauses, each of which is the logical AND of several constraints). +This section contains several examples that show that we simplify the DNF formula as much as we can +before displaying it. + +```py +from ty_extensions import range_constraint + +def f[T, U](): + t1 = range_constraint(str, T, str) + t2 = range_constraint(bool, T, bool) + u1 = range_constraint(str, U, str) + u2 = range_constraint(bool, U, bool) + + # revealed: ty_extensions.ConstraintSet[(T@f = bool) ∨ (T@f = str)] + reveal_type(t1 | t2) + # revealed: ty_extensions.ConstraintSet[(U@f = bool) ∨ (U@f = str)] + reveal_type(u1 | u2) + # revealed: ty_extensions.ConstraintSet[((T@f = bool) ∧ (U@f = bool)) ∨ ((T@f = bool) ∧ (U@f = str)) ∨ ((T@f = str) ∧ (U@f = bool)) ∨ ((T@f = str) ∧ (U@f = str))] + reveal_type((t1 | t2) & (u1 | u2)) +``` + +The lower and upper bounds of a constraint are normalized, so that we equate unions and +intersections whose elements appear in different orders. + +```py +from typing import Never + +def f[T](): + # revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)] + reveal_type(range_constraint(Never, T, str | int)) + # revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)] + reveal_type(range_constraint(Never, T, int | str)) +``` diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index 368994fd34..81af22d314 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -22,7 +22,12 @@ 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 465df24583..93c306ba8d 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -8461,7 +8461,9 @@ fn lazy_bound_or_constraints_cycle_initial<'db>( } /// Where a type variable is bound and usable. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +#[derive( + Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, 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>), @@ -8495,7 +8497,9 @@ impl<'db> BindingContext<'db> { /// 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, PartialEq, Eq, Hash, get_size2::GetSize, salsa::Update)] +#[derive( + Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, 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/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 3d2b23c09f..832aa71ab2 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -23,6 +23,9 @@ //! Note that all lower and upper bounds in a constraint must be fully static. We take the bottom //! and top materializations of the types to remove any gradual forms if needed. //! +//! Lower and upper bounds must also be normalized. This lets us identify, for instance, +//! two constraints with equivalent but differently ordered unions as their bounds. +//! //! NOTE: This module is currently in a transitional state. We've added the BDD [`ConstraintSet`] //! representation, and updated all of our property checks to build up a constraint set and then //! check whether it is ever or always satisfiable, as appropriate. We are not yet inferring @@ -58,6 +61,7 @@ use std::fmt::Display; use itertools::Itertools; use rustc_hash::FxHashSet; +use salsa::plumbing::AsId; use crate::Db; use crate::types::{BoundTypeVarIdentity, IntersectionType, Type, UnionType}; @@ -183,20 +187,20 @@ impl<'db> ConstraintSet<'db> { /// Updates this constraint set to hold the union of itself and another constraint set. pub(crate) fn union(&mut self, db: &'db dyn Db, other: Self) -> Self { - self.node = self.node.or(db, other.node).simplify(db); + self.node = self.node.or(db, other.node); *self } /// Updates this constraint set to hold the intersection of itself and another constraint set. pub(crate) fn intersect(&mut self, db: &'db dyn Db, other: Self) -> Self { - self.node = self.node.and(db, other.node).simplify(db); + self.node = self.node.and(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).simplify(db), + node: self.node.negate(db), } } @@ -256,7 +260,6 @@ impl From for ConstraintSet<'_> { /// 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)] -#[derive(PartialOrd, Ord)] pub(crate) struct ConstrainedTypeVar<'db> { typevar: BoundTypeVarIdentity<'db>, lower: Type<'db>, @@ -292,8 +295,11 @@ impl<'db> ConstrainedTypeVar<'db> { return Node::AlwaysTrue; } + let lower = lower.normalized(db); + let upper = upper.normalized(db); Node::new_constraint(db, ConstrainedTypeVar::new(db, typevar, lower, upper)) } + fn when_true(self) -> ConstraintAssignment<'db> { ConstraintAssignment::Positive(self) } @@ -310,11 +316,37 @@ impl<'db> ConstrainedTypeVar<'db> { && other.upper(db).is_subtype_of(db, self.upper(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 + /// it's consistent. However, different orderings can have very different _performance_ + /// characteristics. Many BDD libraries attempt to reorder variables on the fly while building + /// 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 compare the _typevars_ of each constraint first, so that all constraints + /// for a single typevar are guaranteed to be adjacent in the BDD structure. There are several + /// simplifications that we perform that operate on constraints with the same typevar, and this + /// ensures that we can find all candidate simplifications more easily. + fn ordering(self, db: &'db dyn Db) -> impl Ord { + (self.typevar(db), self.as_id()) + } + + /// Returns whether this constraint implies another — i.e., whether every type that + /// satisfies this constraint also satisfies `other`. + /// + /// This is used (among other places) to simplify how we display constraint sets, by removing + /// redundant constraints from a clause. + fn implies(self, db: &'db dyn Db, other: Self) -> bool { + other.contains(db, self) + } + /// Returns the intersection of two range constraints, or `None` if the intersection is empty. fn intersect(self, db: &'db dyn Db, other: Self) -> Option { // (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(db), other.upper(db)]); + let lower = UnionType::from_elements(db, [self.lower(db), other.lower(db)]).normalized(db); + let upper = + IntersectionType::from_elements(db, [self.upper(db), other.upper(db)]).normalized(db); // If `lower ≰ upper`, then the intersection is empty, since there is no type that is both // greater than `lower`, and less than `upper`. @@ -390,8 +422,8 @@ impl<'db> ConstrainedTypeVar<'db> { /// that point at the same node. /// /// 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`]s are interned, so we can use the IDs -/// that salsa assigns to define this order. +/// visits variables in the same order. [`ConstrainedTypeVar::ordering`] defines the variable +/// ordering that we use for constraint set BDDs. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)] enum Node<'db> { AlwaysFalse, @@ -407,13 +439,13 @@ impl<'db> Node<'db> { if_true: Node<'db>, if_false: Node<'db>, ) -> Self { + debug_assert!((if_true.root_constraint(db)).is_none_or(|root_constraint| { + root_constraint.ordering(db) > constraint.ordering(db) + })); debug_assert!( - (if_true.root_constraint(db)) - .is_none_or(|root_constraint| root_constraint > constraint) - ); - debug_assert!( - (if_false.root_constraint(db)) - .is_none_or(|root_constraint| root_constraint > constraint) + (if_false.root_constraint(db)).is_none_or(|root_constraint| { + root_constraint.ordering(db) > constraint.ordering(db) + }) ); if if_true == if_false { return if_true; @@ -762,14 +794,87 @@ impl<'db> Node<'db> { Node::AlwaysFalse => f.write_str("never"), Node::Interior(_) => { let mut clauses = self.node.satisfied_clauses(self.db); - clauses.simplify(); + clauses.simplify(self.db); clauses.display(self.db).fmt(f) } } } } - DisplayNode { node: self, db } + DisplayNode { + node: self.simplify(db), + db, + } + } + + /// Displays the full graph structure of this BDD. `prefix` will be output before each line + /// other than the first. Produces output like the following: + /// + /// ```text + /// (T@_ = str) + /// ┡━₁ (U@_ = str) + /// │ ┡━₁ always + /// │ └─₀ (U@_ = bool) + /// │ ┡━₁ always + /// │ └─₀ never + /// └─₀ (T@_ = bool) + /// ┡━₁ (U@_ = str) + /// │ ┡━₁ always + /// │ └─₀ (U@_ = bool) + /// │ ┡━₁ always + /// │ └─₀ never + /// └─₀ never + /// ``` + #[cfg_attr(not(test), expect(dead_code))] // Keep this around for debugging purposes + fn display_graph(self, db: &'db dyn Db, prefix: &dyn Display) -> impl Display { + struct DisplayNode<'a, 'db> { + db: &'db dyn Db, + node: Node<'db>, + prefix: &'a dyn Display, + } + + impl<'a, 'db> DisplayNode<'a, 'db> { + fn new(db: &'db dyn Db, node: Node<'db>, prefix: &'a dyn Display) -> Self { + Self { db, node, prefix } + } + } + + impl Display for DisplayNode<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.node { + Node::AlwaysTrue => write!(f, "always"), + Node::AlwaysFalse => write!(f, "never"), + Node::Interior(interior) => { + interior.constraint(self.db).display(self.db).fmt(f)?; + // Calling display_graph recursively here causes rustc to claim that the + // expect(unused) up above is unfulfilled! + write!( + f, + "\n{}┡━₁ {}", + self.prefix, + DisplayNode::new( + self.db, + interior.if_true(self.db), + &format_args!("{}│ ", self.prefix) + ), + )?; + write!( + f, + "\n{}└─₀ {}", + self.prefix, + DisplayNode::new( + self.db, + interior.if_false(self.db), + &format_args!("{} ", self.prefix) + ), + )?; + Ok(()) + } + } + } + } + + DisplayNode::new(db, self, prefix) } } @@ -800,7 +905,7 @@ impl<'db> InteriorNode<'db> { fn or(self, db: &'db dyn Db, other: Self) -> Node<'db> { let self_constraint = self.constraint(db); let other_constraint = other.constraint(db); - match self_constraint.cmp(&other_constraint) { + match (self_constraint.ordering(db)).cmp(&other_constraint.ordering(db)) { Ordering::Equal => Node::new( db, self_constraint, @@ -826,7 +931,7 @@ impl<'db> InteriorNode<'db> { fn and(self, db: &'db dyn Db, other: Self) -> Node<'db> { let self_constraint = self.constraint(db); let other_constraint = other.constraint(db); - match self_constraint.cmp(&other_constraint) { + match (self_constraint.ordering(db)).cmp(&other_constraint.ordering(db)) { Ordering::Equal => Node::new( db, self_constraint, @@ -852,7 +957,7 @@ impl<'db> InteriorNode<'db> { fn iff(self, db: &'db dyn Db, other: Self) -> Node<'db> { let self_constraint = self.constraint(db); let other_constraint = other.constraint(db); - match self_constraint.cmp(&other_constraint) { + match (self_constraint.ordering(db)).cmp(&other_constraint.ordering(db)) { Ordering::Equal => Node::new( db, self_constraint, @@ -884,7 +989,7 @@ impl<'db> InteriorNode<'db> { // 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() < self_constraint { + if assignment.constraint().ordering(db) < self_constraint.ordering(db) { return (Node::Interior(self), false); } @@ -1141,6 +1246,55 @@ impl<'db> ConstraintAssignment<'db> { *self = self.negated(); } + /// Returns whether this constraint implies another — i.e., whether every type that + /// satisfies this constraint also satisfies `other`. + /// + /// This is used (among other places) to simplify how we display constraint sets, by removing + /// redundant constraints from a clause. + fn implies(self, db: &'db dyn 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. + // + // ....|----other-----|.... + // ......|---self---|...... + ( + ConstraintAssignment::Positive(self_constraint), + ConstraintAssignment::Positive(other_constraint), + ) => self_constraint.implies(db, 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 + // with the smaller. + // + // |-----|...other...|-----| + // |---|.....self......|---| + ( + ConstraintAssignment::Negative(self_constraint), + ConstraintAssignment::Negative(other_constraint), + ) => other_constraint.implies(db, self_constraint), + + // For a positive and negative constraint, the ranges have to be disjoint, and the + // positive range implies the negative range. + // + // |---------------|...self...|---| + // ..|---other---|................| + ( + ConstraintAssignment::Positive(self_constraint), + ConstraintAssignment::Negative(other_constraint), + ) => self_constraint.intersect(db, other_constraint).is_none(), + + // It's theoretically possible for a negative constraint to imply a positive constraint + // if the positive constraint is always satisfied (`Never ≤ T ≤ object`). But we never + // create constraints of that form, so with our representation, a negative constraint + // can never imply a positive constraint. + // + // |------other-------| + // |---|...self...|---| + (ConstraintAssignment::Negative(_), ConstraintAssignment::Positive(_)) => false, + } + } + // Keep this for future debugging needs, even though it's not currently used when rendering // constraint sets. #[expect(dead_code)] @@ -1209,6 +1363,43 @@ impl<'db> SatisfiedClause<'db> { false } + /// Simplifies this clause by removing constraints that are implied by other constraints in the + /// clause. (Clauses are the intersection of constraints, so if two clauses are redundant, we + /// 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 { + let mut changes_made = false; + let mut i = 0; + // Loop through each constraint, comparing it with any constraints that appear later in the + // list. + '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 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. + // (We'll have swapped another element into place at that index, and want to + // make sure that we process it.) + self.constraints.swap_remove(i); + changes_made = true; + continue 'outer; + } else if self.constraints[i].implies(db, 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. + self.constraints.swap_remove(j); + changes_made = true; + } else { + j += 1; + } + } + i += 1; + } + changes_made + } + fn display(&self, db: &'db dyn Db) -> String { // This is a bit heavy-handed, but we need to output the constraints in a consistent order // even though Salsa IDs are assigned non-deterministically. This Display output is only @@ -1258,7 +1449,13 @@ impl<'db> SatisfiedClauses<'db> { /// 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) { + fn simplify(&mut self, db: &'db dyn 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); + } + while self.simplify_one_round() { // Keep going } @@ -1340,3 +1537,47 @@ impl<'db> SatisfiedClauses<'db> { clauses.join(" ∨ ") } } + +#[cfg(test)] +mod tests { + use super::*; + + use indoc::indoc; + use pretty_assertions::assert_eq; + + use crate::db::tests::setup_db; + use crate::types::{BoundTypeVarInstance, KnownClass, TypeVarVariance}; + + #[test] + fn test_display_graph_output() { + let expected = indoc! {r#" + (T = str) + ┡━₁ (U = str) + │ ┡━₁ always + │ └─₀ (U = bool) + │ ┡━₁ always + │ └─₀ never + └─₀ (T = bool) + ┡━₁ (U = str) + │ ┡━₁ always + │ └─₀ (U = bool) + │ ┡━₁ always + │ └─₀ never + └─₀ never + "#} + .trim_end(); + + let db = setup_db(); + let t = BoundTypeVarInstance::synthetic(&db, "T", TypeVarVariance::Invariant); + let u = BoundTypeVarInstance::synthetic(&db, "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.identity(&db), str_type); + let t_bool = ConstraintSet::range(&db, bool_type, t.identity(&db), bool_type); + let u_str = ConstraintSet::range(&db, str_type, u.identity(&db), str_type); + let u_bool = ConstraintSet::range(&db, bool_type, u.identity(&db), bool_type); + let constraints = (t_str.or(&db, || t_bool)).and(&db, || u_str.or(&db, || u_bool)); + let actual = constraints.node.display_graph(&db, &"").to_string(); + assert_eq!(actual, expected); + } +} From 7ba176d395755f0b19ca2657863cff48d485ee04 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 22 Oct 2025 17:48:17 -0400 Subject: [PATCH 014/188] ci: adjust zizmor config, bump dist (#20999) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Also bumps `cargo dist` to 0.30, and moves us back to the upstream copy of `dist` now that the latest version has integrated our fork's patches. ## Test Plan See what happens in CI 🙂 --------- Signed-off-by: William Woodruff --- .github/workflows/publish-playground.yml | 4 +++- .github/workflows/publish-ty-playground.yml | 1 + .github/workflows/release.yml | 5 ++--- .github/workflows/ty-ecosystem-analyzer.yaml | 3 +++ .github/workflows/ty-ecosystem-report.yaml | 3 +++ .github/zizmor.yml | 13 +++++++++---- .pre-commit-config.yaml | 4 ++-- dist-workspace.toml | 8 ++------ 8 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish-playground.yml b/.github/workflows/publish-playground.yml index d40850afeb..e05691179f 100644 --- a/.github/workflows/publish-playground.yml +++ b/.github/workflows/publish-playground.yml @@ -18,6 +18,8 @@ env: CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 +permissions: {} + jobs: publish: runs-on: ubuntu-latest @@ -32,7 +34,7 @@ jobs: - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: 22 - cache: "npm" + cache: "npm" # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact cache-dependency-path: playground/package-lock.json - uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0 - name: "Install Node dependencies" diff --git a/.github/workflows/publish-ty-playground.yml b/.github/workflows/publish-ty-playground.yml index e842ab6928..5945935952 100644 --- a/.github/workflows/publish-ty-playground.yml +++ b/.github/workflows/publish-ty-playground.yml @@ -38,6 +38,7 @@ jobs: - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: 22 + cache: "npm" # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact - uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0 - name: "Install Node dependencies" run: npm ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b53ce5a2d0..6261aed8ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,6 @@ -# This file was autogenerated by dist: https://github.com/astral-sh/cargo-dist +# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist # # Copyright 2022-2024, axodotdev -# Copyright 2025 Astral Software Inc. # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: @@ -69,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/astral-sh/cargo-dist/releases/download/v0.28.5-prerelease.1/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.0/cargo-dist-installer.sh | sh" - name: Cache dist uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index 4d85f7e78b..a59cc6c947 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -34,10 +34,13 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + with: + enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 with: workspaces: "ruff" + lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact - name: Install Rust toolchain run: rustup show diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 3c5e0f7797..30b3bc93ab 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -30,10 +30,13 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + with: + enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 with: workspaces: "ruff" + lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact - name: Install Rust toolchain run: rustup show diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 8eae6bd3f3..2dc7f7dba3 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -9,13 +9,18 @@ rules: cache-poisoning: ignore: - build-docker.yml - - publish-playground.yml - - ty-ecosystem-analyzer.yaml - - ty-ecosystem-report.yaml excessive-permissions: # it's hard to test what the impact of removing these ignores would be # without actually running the release workflow... ignore: - build-docker.yml - - publish-playground.yml - publish-docs.yml + secrets-inherit: + # `cargo dist` makes extensive use of `secrets: inherit`, + # and we can't easily fix that until an upstream release changes that. + disable: true + template-injection: + ignore: + # like with `secrets-inherit`, `cargo dist` introduces some + # template injections. We've manually audited these usages for safety. + - release.yml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc81f653f5..130aaa554f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -101,8 +101,8 @@ repos: # zizmor detects security vulnerabilities in GitHub Actions workflows. # Additional configuration for the tool is found in `.github/zizmor.yml` - - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.11.0 + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.15.2 hooks: - id: zizmor diff --git a/dist-workspace.toml b/dist-workspace.toml index 1f23f1118b..5d1b64992e 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.28.5-prerelease.1" +cargo-dist-version = "0.30.0" # Whether to consider the binaries in a package for distribution (defaults true) dist = false # CI backends to support @@ -54,11 +54,7 @@ local-artifacts-jobs = ["./build-binaries", "./build-docker"] # 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" -] +post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground"] # Custom permissions for GitHub Jobs github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } } # Whether to install an updater program From 6c18f18450bd59c7659373d0c944794a6504405a Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 23 Oct 2025 07:11:50 +0900 Subject: [PATCH 015/188] [`ruff`] Fix UP032 conversion for decimal ints with underscores (#21022) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #21017 Taught UP032’s parenthesize check to ignore underscores when inspecting decimal integer literals so the converter emits `f"{(1_2).real}"` instead of invalid syntax. ## Test Plan Added test cases to UP032_2.py. --- .../test/fixtures/pyupgrade/UP032_2.py | 5 ++ .../src/rules/pyupgrade/rules/f_strings.rs | 6 +- ...__rules__pyupgrade__tests__UP032_2.py.snap | 58 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_2.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_2.py index 2987164454..9dbc7f385a 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_2.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_2.py @@ -26,3 +26,8 @@ "{.real}".format({1, 2}) "{.real}".format({1: 2, 3: 4}) "{}".format((i for i in range(2))) + +# https://github.com/astral-sh/ruff/issues/21017 +"{.real}".format(1_2) +"{0.real}".format(1_2) +"{a.real}".format(a=1_2) 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 46379c8ef0..f92372f43a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs @@ -160,7 +160,11 @@ fn parenthesize(expr: &Expr, text: &str, context: FormatContext) -> bool { value: ast::Number::Int(..), .. }), - ) => text.chars().all(|c| c.is_ascii_digit()), + ) => text + .chars() + // Ignore digit separators so decimal literals like `1_2` still count as pure digits. + .filter(|c| *c != '_') + .all(|c| c.is_ascii_digit()), // E.g., `{x, y}` should be parenthesized in `f"{(x, y)}"`. ( _, diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_2.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_2.py.snap index f3324ea02b..835b1ea651 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_2.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_2.py.snap @@ -385,6 +385,7 @@ help: Convert to f-string 26 + f"{({1, 2}).real}" 27 | "{.real}".format({1: 2, 3: 4}) 28 | "{}".format((i for i in range(2))) +29 | UP032 [*] Use f-string instead of `format` call --> UP032_2.py:27:1 @@ -402,6 +403,8 @@ help: Convert to f-string - "{.real}".format({1: 2, 3: 4}) 27 + f"{({1: 2, 3: 4}).real}" 28 | "{}".format((i for i in range(2))) +29 | +30 | # https://github.com/astral-sh/ruff/issues/21017 UP032 [*] Use f-string instead of `format` call --> UP032_2.py:28:1 @@ -410,6 +413,8 @@ UP032 [*] Use f-string instead of `format` call 27 | "{.real}".format({1: 2, 3: 4}) 28 | "{}".format((i for i in range(2))) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +29 | +30 | # https://github.com/astral-sh/ruff/issues/21017 | help: Convert to f-string 25 | "{.real}".format([1, 2]) @@ -417,3 +422,56 @@ help: Convert to f-string 27 | "{.real}".format({1: 2, 3: 4}) - "{}".format((i for i in range(2))) 28 + f"{(i for i in range(2))}" +29 | +30 | # https://github.com/astral-sh/ruff/issues/21017 +31 | "{.real}".format(1_2) + +UP032 [*] Use f-string instead of `format` call + --> UP032_2.py:31:1 + | +30 | # https://github.com/astral-sh/ruff/issues/21017 +31 | "{.real}".format(1_2) + | ^^^^^^^^^^^^^^^^^^^^^ +32 | "{0.real}".format(1_2) +33 | "{a.real}".format(a=1_2) + | +help: Convert to f-string +28 | "{}".format((i for i in range(2))) +29 | +30 | # https://github.com/astral-sh/ruff/issues/21017 + - "{.real}".format(1_2) +31 + f"{(1_2).real}" +32 | "{0.real}".format(1_2) +33 | "{a.real}".format(a=1_2) + +UP032 [*] Use f-string instead of `format` call + --> UP032_2.py:32:1 + | +30 | # https://github.com/astral-sh/ruff/issues/21017 +31 | "{.real}".format(1_2) +32 | "{0.real}".format(1_2) + | ^^^^^^^^^^^^^^^^^^^^^^ +33 | "{a.real}".format(a=1_2) + | +help: Convert to f-string +29 | +30 | # https://github.com/astral-sh/ruff/issues/21017 +31 | "{.real}".format(1_2) + - "{0.real}".format(1_2) +32 + f"{(1_2).real}" +33 | "{a.real}".format(a=1_2) + +UP032 [*] Use f-string instead of `format` call + --> UP032_2.py:33:1 + | +31 | "{.real}".format(1_2) +32 | "{0.real}".format(1_2) +33 | "{a.real}".format(a=1_2) + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Convert to f-string +30 | # https://github.com/astral-sh/ruff/issues/21017 +31 | "{.real}".format(1_2) +32 | "{0.real}".format(1_2) + - "{a.real}".format(a=1_2) +33 + f"{(1_2).real}" From 76a55314e4afdb35f52e3df9ceec2514f4fccf73 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 23 Oct 2025 09:25:16 +0200 Subject: [PATCH 016/188] Fix rare multithreaded related hang (#21038) --- Cargo.lock | 6 +- Cargo.toml | 2 +- crates/ty_python_semantic/src/dunder_all.rs | 12 +- crates/ty_python_semantic/src/place.rs | 23 +--- .../src/semantic_index/re_exports.rs | 11 +- crates/ty_python_semantic/src/types.rs | 112 +-------------- crates/ty_python_semantic/src/types/class.rs | 127 ++---------------- crates/ty_python_semantic/src/types/enums.rs | 12 +- .../ty_python_semantic/src/types/function.rs | 28 +--- .../ty_python_semantic/src/types/generics.rs | 10 -- crates/ty_python_semantic/src/types/infer.rs | 60 ++------- .../ty_python_semantic/src/types/instance.rs | 13 +- crates/ty_python_semantic/src/types/narrow.rs | 22 --- .../src/types/protocol_class.rs | 11 +- crates/ty_python_semantic/src/types/tuple.rs | 11 +- fuzz/Cargo.toml | 2 +- 16 files changed, 42 insertions(+), 420 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1bc41b0c9b..80fdf8b809 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3563,7 +3563,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" version = "0.24.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051" +source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161" dependencies = [ "boxcar", "compact_str", @@ -3587,12 +3587,12 @@ dependencies = [ [[package]] name = "salsa-macro-rules" version = "0.24.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051" +source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161" [[package]] name = "salsa-macros" version = "0.24.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051" +source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5f1112913e..22f4a5cbfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" } rustc-hash = { version = "2.0.0" } rustc-stable-hash = { version = "0.1.2" } # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "ef9f9329be6923acd050c8dddd172e3bc93e8051", default-features = false, features = [ +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d38145c29574758de7ffbe8a13cd4584c3b09161", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", diff --git a/crates/ty_python_semantic/src/dunder_all.rs b/crates/ty_python_semantic/src/dunder_all.rs index 10eab9321a..7273bc975b 100644 --- a/crates/ty_python_semantic/src/dunder_all.rs +++ b/crates/ty_python_semantic/src/dunder_all.rs @@ -10,23 +10,13 @@ use crate::semantic_index::{SemanticIndex, semantic_index}; use crate::types::{Truthiness, Type, TypeContext, infer_expression_types}; use crate::{Db, ModuleName, resolve_module}; -#[allow(clippy::ref_option)] -fn dunder_all_names_cycle_recover( - _db: &dyn Db, - _value: &Option>, - _count: u32, - _file: File, -) -> salsa::CycleRecoveryAction>> { - salsa::CycleRecoveryAction::Iterate -} - fn dunder_all_names_cycle_initial(_db: &dyn Db, _file: File) -> Option> { None } /// 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_fn=dunder_all_names_cycle_recover, cycle_initial=dunder_all_names_cycle_initial, heap_size=ruff_memory_usage::heap_size)] +#[salsa::tracked(returns(as_ref), cycle_initial=dunder_all_names_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn dunder_all_names(db: &dyn Db, file: File) -> Option> { let _span = tracing::trace_span!("dunder_all_names", file=?file.path(db)).entered(); diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index d0fa8ba36c..99804ae5c8 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -695,18 +695,6 @@ impl<'db> From> for PlaceAndQualifiers<'db> { } } -fn place_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &PlaceAndQualifiers<'db>, - _count: u32, - _scope: ScopeId<'db>, - _place_id: ScopedPlaceId, - _requires_explicit_reexport: RequiresExplicitReExport, - _considered_definitions: ConsideredDefinitions, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn place_cycle_initial<'db>( _db: &'db dyn Db, _scope: ScopeId<'db>, @@ -717,7 +705,7 @@ fn place_cycle_initial<'db>( Place::bound(Type::Never).into() } -#[salsa::tracked(cycle_fn=place_cycle_recover, cycle_initial=place_cycle_initial, heap_size=ruff_memory_usage::heap_size)] +#[salsa::tracked(cycle_initial=place_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn place_by_id<'db>( db: &'db dyn Db, scope: ScopeId<'db>, @@ -1511,7 +1499,6 @@ mod implicit_globals { #[salsa::tracked( returns(deref), cycle_initial=module_type_symbols_initial, - cycle_fn=module_type_symbols_cycle_recover, heap_size=ruff_memory_usage::heap_size )] fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> { @@ -1545,14 +1532,6 @@ mod implicit_globals { smallvec::SmallVec::default() } - fn module_type_symbols_cycle_recover( - _db: &dyn Db, - _value: &smallvec::SmallVec<[ast::name::Name; 8]>, - _count: u32, - ) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate - } - #[cfg(test)] mod tests { use super::*; 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 9f2139637f..52f26ef4a0 100644 --- a/crates/ty_python_semantic/src/semantic_index/re_exports.rs +++ b/crates/ty_python_semantic/src/semantic_index/re_exports.rs @@ -30,20 +30,11 @@ use rustc_hash::FxHashMap; use crate::{Db, module_name::ModuleName, resolve_module}; -fn exports_cycle_recover( - _db: &dyn Db, - _value: &[Name], - _count: u32, - _file: File, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn exports_cycle_initial(_db: &dyn Db, _file: File) -> Box<[Name]> { Box::default() } -#[salsa::tracked(returns(deref), cycle_fn=exports_cycle_recover, cycle_initial=exports_cycle_initial, heap_size=ruff_memory_usage::heap_size)] +#[salsa::tracked(returns(deref), cycle_initial=exports_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(super) 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); diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 93c306ba8d..1c0355862d 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -369,17 +369,6 @@ impl Default for MemberLookupPolicy { } } -fn member_lookup_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &PlaceAndQualifiers<'db>, - _count: u32, - _self: Type<'db>, - _name: Name, - _policy: MemberLookupPolicy, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn member_lookup_cycle_initial<'db>( _db: &'db dyn Db, _self: Type<'db>, @@ -389,17 +378,6 @@ fn member_lookup_cycle_initial<'db>( Place::bound(Type::Never).into() } -fn class_lookup_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &PlaceAndQualifiers<'db>, - _count: u32, - _self: Type<'db>, - _name: Name, - _policy: MemberLookupPolicy, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn class_lookup_cycle_initial<'db>( _db: &'db dyn Db, _self: Type<'db>, @@ -409,21 +387,6 @@ fn class_lookup_cycle_initial<'db>( Place::bound(Type::Never).into() } -#[allow(clippy::trivially_copy_pass_by_ref)] -fn variance_cycle_recover<'db, T>( - _db: &'db dyn Db, - _value: &TypeVarVariance, - count: u32, - _self: T, - _typevar: BoundTypeVarInstance<'db>, -) -> salsa::CycleRecoveryAction { - assert!( - count <= 2, - "Should only be able to cycle at most twice: there are only three levels in the lattice, each cycle should move us one" - ); - salsa::CycleRecoveryAction::Iterate -} - fn variance_cycle_initial<'db, T>( _db: &'db dyn Db, _self: T, @@ -1582,7 +1545,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. - #[salsa::tracked(cycle_fn=is_redundant_with_cycle_recover, cycle_initial=is_redundant_with_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(cycle_initial=is_redundant_with_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn is_redundant_with(self, db: &'db dyn Db, other: Type<'db>) -> bool { self.has_relation_to(db, other, InferableTypeVars::None, TypeRelation::Redundancy) .is_always_satisfied() @@ -3628,7 +3591,7 @@ impl<'db> Type<'db> { self.class_member_with_policy(db, name, MemberLookupPolicy::default()) } - #[salsa::tracked(cycle_fn=class_lookup_cycle_recover, cycle_initial=class_lookup_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(cycle_initial=class_lookup_cycle_initial, heap_size=ruff_memory_usage::heap_size)] fn class_member_with_policy( self, db: &'db dyn Db, @@ -4093,7 +4056,7 @@ impl<'db> Type<'db> { /// Similar to [`Type::member`], but allows the caller to specify what policy should be used /// when looking up attributes. See [`MemberLookupPolicy`] for more information. - #[salsa::tracked(cycle_fn=member_lookup_cycle_recover, cycle_initial=member_lookup_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(cycle_initial=member_lookup_cycle_initial, heap_size=ruff_memory_usage::heap_size)] fn member_lookup_with_policy( self, db: &'db dyn Db, @@ -6616,7 +6579,7 @@ impl<'db> Type<'db> { /// Note that this does not specialize generic classes, functions, or type aliases! That is a /// different operation that is performed explicitly (via a subscript operation), or implicitly /// via a call to the generic object. - #[salsa::tracked(heap_size=ruff_memory_usage::heap_size, cycle_fn=apply_specialization_cycle_recover, cycle_initial=apply_specialization_cycle_initial)] + #[salsa::tracked(heap_size=ruff_memory_usage::heap_size, cycle_initial=apply_specialization_cycle_initial)] pub(crate) fn apply_specialization( self, db: &'db dyn Db, @@ -7336,16 +7299,6 @@ impl<'db> VarianceInferable<'db> for Type<'db> { } #[allow(clippy::trivially_copy_pass_by_ref)] -fn is_redundant_with_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &bool, - _count: u32, - _subtype: Type<'db>, - _supertype: Type<'db>, -) -> salsa::CycleRecoveryAction { - salsa::CycleRecoveryAction::Iterate -} - fn is_redundant_with_cycle_initial<'db>( _db: &'db dyn Db, _subtype: Type<'db>, @@ -7354,16 +7307,6 @@ fn is_redundant_with_cycle_initial<'db>( true } -fn apply_specialization_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Type<'db>, - _count: u32, - _self: Type<'db>, - _specialization: Specialization<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn apply_specialization_cycle_initial<'db>( _db: &'db dyn Db, _self: Type<'db>, @@ -8340,7 +8283,6 @@ impl<'db> TypeVarInstance<'db> { } #[salsa::tracked( - cycle_fn=lazy_bound_or_constraints_cycle_recover, cycle_initial=lazy_bound_or_constraints_cycle_initial, heap_size=ruff_memory_usage::heap_size )] @@ -8365,7 +8307,6 @@ impl<'db> TypeVarInstance<'db> { } #[salsa::tracked( - cycle_fn=lazy_bound_or_constraints_cycle_recover, cycle_initial=lazy_bound_or_constraints_cycle_initial, heap_size=ruff_memory_usage::heap_size )] @@ -8443,16 +8384,6 @@ impl<'db> TypeVarInstance<'db> { } } -#[allow(clippy::ref_option)] -fn lazy_bound_or_constraints_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Option>, - _count: u32, - _self: TypeVarInstance<'db>, -) -> salsa::CycleRecoveryAction>> { - salsa::CycleRecoveryAction::Iterate -} - fn lazy_bound_or_constraints_cycle_initial<'db>( _db: &'db dyn Db, _self: TypeVarInstance<'db>, @@ -9975,16 +9906,6 @@ fn walk_bound_method_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( visitor.visit_type(db, method.self_instance(db)); } -#[allow(clippy::trivially_copy_pass_by_ref)] -fn into_callable_type_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &CallableType<'db>, - _count: u32, - _self: BoundMethodType<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn into_callable_type_cycle_initial<'db>( db: &'db dyn Db, _self: BoundMethodType<'db>, @@ -10013,7 +9934,7 @@ impl<'db> BoundMethodType<'db> { Self::new(db, self.function(db), f(self.self_instance(db))) } - #[salsa::tracked(cycle_fn=into_callable_type_cycle_recover, cycle_initial=into_callable_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[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); @@ -10777,7 +10698,7 @@ impl<'db> PEP695TypeAliasType<'db> { } /// The RHS type of a PEP-695 style type alias with specialization applied. - #[salsa::tracked(cycle_fn=value_type_cycle_recover, cycle_initial=value_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(cycle_initial=value_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { let value_type = self.raw_value_type(db); @@ -10840,7 +10761,7 @@ impl<'db> PEP695TypeAliasType<'db> { self.specialization(db).is_some() } - #[salsa::tracked(cycle_fn=generic_context_cycle_recover, cycle_initial=generic_context_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(cycle_initial=generic_context_cycle_initial, 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); @@ -10863,16 +10784,6 @@ impl<'db> PEP695TypeAliasType<'db> { } } -#[allow(clippy::ref_option, clippy::trivially_copy_pass_by_ref)] -fn generic_context_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Option>, - _count: u32, - _self: PEP695TypeAliasType<'db>, -) -> salsa::CycleRecoveryAction>> { - salsa::CycleRecoveryAction::Iterate -} - fn generic_context_cycle_initial<'db>( _db: &'db dyn Db, _self: PEP695TypeAliasType<'db>, @@ -10880,15 +10791,6 @@ fn generic_context_cycle_initial<'db>( None } -fn value_type_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Type<'db>, - _count: u32, - _self: PEP695TypeAliasType<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn value_type_cycle_initial<'db>(_db: &'db dyn Db, _self: PEP695TypeAliasType<'db>) -> Type<'db> { Type::Never } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 8d46f1b57a..dd20393d44 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -69,15 +69,6 @@ use ruff_python_ast::{self as ast, PythonVersion}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; -fn explicit_bases_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &[Type<'db>], - _count: u32, - _self: ClassLiteral<'db>, -) -> salsa::CycleRecoveryAction]>> { - salsa::CycleRecoveryAction::Iterate -} - fn explicit_bases_cycle_initial<'db>( _db: &'db dyn Db, _self: ClassLiteral<'db>, @@ -85,16 +76,6 @@ fn explicit_bases_cycle_initial<'db>( Box::default() } -#[expect(clippy::ref_option, clippy::trivially_copy_pass_by_ref)] -fn inheritance_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Option, - _count: u32, - _self: ClassLiteral<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn inheritance_cycle_initial<'db>( _db: &'db dyn Db, _self: ClassLiteral<'db>, @@ -102,17 +83,6 @@ fn inheritance_cycle_initial<'db>( None } -fn implicit_attribute_recover<'db>( - _db: &'db dyn Db, - _value: &Member<'db>, - _count: u32, - _class_body_scope: ScopeId<'db>, - _name: String, - _target_method_decorator: MethodDecorator, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn implicit_attribute_initial<'db>( _db: &'db dyn Db, _class_body_scope: ScopeId<'db>, @@ -122,16 +92,6 @@ fn implicit_attribute_initial<'db>( Member::unbound() } -fn try_mro_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Result, MroError<'db>>, - _count: u32, - _self: ClassLiteral<'db>, - _specialization: Option>, -) -> salsa::CycleRecoveryAction, MroError<'db>>> { - salsa::CycleRecoveryAction::Iterate -} - fn try_mro_cycle_initial<'db>( db: &'db dyn Db, self_: ClassLiteral<'db>, @@ -143,32 +103,11 @@ fn try_mro_cycle_initial<'db>( )) } -#[allow(clippy::trivially_copy_pass_by_ref)] -fn is_typed_dict_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &bool, - _count: u32, - _self: ClassLiteral<'db>, -) -> salsa::CycleRecoveryAction { - salsa::CycleRecoveryAction::Iterate -} - #[allow(clippy::unnecessary_wraps)] fn is_typed_dict_cycle_initial<'db>(_db: &'db dyn Db, _self: ClassLiteral<'db>) -> bool { false } -fn try_metaclass_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Result<(Type<'db>, Option), MetaclassError<'db>>, - _count: u32, - _self: ClassLiteral<'db>, -) -> salsa::CycleRecoveryAction< - Result<(Type<'db>, Option>), MetaclassError<'db>>, -> { - salsa::CycleRecoveryAction::Iterate -} - #[allow(clippy::unnecessary_wraps)] fn try_metaclass_cycle_initial<'db>( _db: &'db dyn Db, @@ -196,9 +135,7 @@ impl<'db> CodeGeneratorKind<'db> { class: ClassLiteral<'db>, specialization: Option>, ) -> Option { - #[salsa::tracked( - cycle_fn=code_generator_of_class_recover, - cycle_initial=code_generator_of_class_initial, + #[salsa::tracked(cycle_initial=code_generator_of_class_initial, heap_size=ruff_memory_usage::heap_size )] fn code_generator_of_class<'db>( @@ -238,17 +175,6 @@ impl<'db> CodeGeneratorKind<'db> { None } - #[expect(clippy::ref_option)] - fn code_generator_of_class_recover<'db>( - _db: &'db dyn Db, - _value: &Option>, - _count: u32, - _class: ClassLiteral<'db>, - _specialization: Option>, - ) -> salsa::CycleRecoveryAction>> { - salsa::CycleRecoveryAction::Iterate - } - code_generator_of_class(db, class, specialization) } @@ -1106,7 +1032,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_fn=into_callable_cycle_recover, cycle_initial=into_callable_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[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) -> Type<'db> { let self_ty = Type::from(self); let metaclass_dunder_call_function_symbol = self_ty @@ -1268,16 +1194,6 @@ impl<'db> ClassType<'db> { } } -#[allow(clippy::trivially_copy_pass_by_ref)] -fn into_callable_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Type<'db>, - _count: u32, - _self: ClassType<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn into_callable_cycle_initial<'db>(_db: &'db dyn Db, _self: ClassType<'db>) -> Type<'db> { Type::Never } @@ -1427,17 +1343,6 @@ pub struct ClassLiteral<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for ClassLiteral<'_> {} -#[expect(clippy::ref_option)] -#[allow(clippy::trivially_copy_pass_by_ref)] -fn generic_context_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Option>, - _count: u32, - _self: ClassLiteral<'db>, -) -> salsa::CycleRecoveryAction>> { - salsa::CycleRecoveryAction::Iterate -} - fn generic_context_cycle_initial<'db>( _db: &'db dyn Db, _self: ClassLiteral<'db>, @@ -1478,9 +1383,7 @@ impl<'db> ClassLiteral<'db> { self.pep695_generic_context(db).is_some() } - #[salsa::tracked( - cycle_fn=generic_context_cycle_recover, - cycle_initial=generic_context_cycle_initial, + #[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> { @@ -1505,9 +1408,7 @@ impl<'db> ClassLiteral<'db> { }) } - #[salsa::tracked( - cycle_fn=generic_context_cycle_recover, - cycle_initial=generic_context_cycle_initial, + #[salsa::tracked(cycle_initial=generic_context_cycle_initial, heap_size=ruff_memory_usage::heap_size, )] pub(crate) fn inherited_legacy_generic_context( @@ -1691,7 +1592,7 @@ impl<'db> ClassLiteral<'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_fn=explicit_bases_cycle_recover, cycle_initial=explicit_bases_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(returns(deref), cycle_initial=explicit_bases_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(super) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { tracing::trace!("ClassLiteral::explicit_bases_query: {}", self.name(db)); @@ -1827,7 +1728,7 @@ impl<'db> ClassLiteral<'db> { /// 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_fn=try_mro_cycle_recover, cycle_initial=try_mro_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(returns(as_ref), cycle_initial=try_mro_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(super) fn try_mro( self, db: &'db dyn Db, @@ -1877,9 +1778,7 @@ impl<'db> ClassLiteral<'db> { /// Return `true` if this class constitutes a typed dict specification (inherits from /// `typing.TypedDict`, either directly or indirectly). - #[salsa::tracked( - cycle_fn=is_typed_dict_cycle_recover, - cycle_initial=is_typed_dict_cycle_initial, + #[salsa::tracked(cycle_initial=is_typed_dict_cycle_initial, heap_size=ruff_memory_usage::heap_size )] pub(super) fn is_typed_dict(self, db: &'db dyn Db) -> bool { @@ -1940,9 +1839,7 @@ impl<'db> ClassLiteral<'db> { } /// Return the metaclass of this class, or an error if the metaclass cannot be inferred. - #[salsa::tracked( - cycle_fn=try_metaclass_cycle_recover, - cycle_initial=try_metaclass_cycle_initial, + #[salsa::tracked(cycle_initial=try_metaclass_cycle_initial, heap_size=ruff_memory_usage::heap_size, )] pub(super) fn try_metaclass( @@ -3124,9 +3021,7 @@ impl<'db> ClassLiteral<'db> { ) } - #[salsa::tracked( - cycle_fn=implicit_attribute_recover, - cycle_initial=implicit_attribute_initial, + #[salsa::tracked(cycle_initial=implicit_attribute_initial, heap_size=ruff_memory_usage::heap_size, )] fn implicit_attribute_inner( @@ -3562,7 +3457,7 @@ impl<'db> ClassLiteral<'db> { /// /// A class definition like this will fail at runtime, /// but we must be resilient to it or we could panic. - #[salsa::tracked(cycle_fn=inheritance_cycle_recover, cycle_initial=inheritance_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(cycle_initial=inheritance_cycle_initial, 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. /// @@ -3654,7 +3549,7 @@ impl<'db> From> for ClassType<'db> { #[salsa::tracked] impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { - #[salsa::tracked(cycle_fn=crate::types::variance_cycle_recover, cycle_initial=crate::types::variance_cycle_initial)] + #[salsa::tracked(cycle_initial=crate::types::variance_cycle_initial)] fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { let typevar_in_generic_context = self .generic_context(db) diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index fc4e4855e3..daffbaebbb 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -36,16 +36,6 @@ impl EnumMetadata<'_> { } } -#[allow(clippy::ref_option, clippy::trivially_copy_pass_by_ref)] -fn enum_metadata_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Option>, - _count: u32, - _class: ClassLiteral<'db>, -) -> salsa::CycleRecoveryAction>> { - salsa::CycleRecoveryAction::Iterate -} - #[allow(clippy::unnecessary_wraps)] fn enum_metadata_cycle_initial<'db>( _db: &'db dyn Db, @@ -56,7 +46,7 @@ fn enum_metadata_cycle_initial<'db>( /// List all members of an enum. #[allow(clippy::ref_option, clippy::unnecessary_wraps)] -#[salsa::tracked(returns(as_ref), cycle_fn=enum_metadata_cycle_recover, cycle_initial=enum_metadata_cycle_initial, heap_size=ruff_memory_usage::heap_size)] +#[salsa::tracked(returns(as_ref), cycle_initial=enum_metadata_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn enum_metadata<'db>( db: &'db dyn Db, class: ClassLiteral<'db>, diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index d368a33099..ce1a1e43d0 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -898,7 +898,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_fn=signature_cycle_recover, cycle_initial=signature_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(returns(ref), cycle_initial=signature_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn signature(self, db: &'db dyn Db) -> CallableSignature<'db> { self.updated_signature(db) .cloned() @@ -915,9 +915,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_fn=last_definition_signature_cycle_recover, - cycle_initial=last_definition_signature_cycle_initial, + 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> { @@ -928,9 +926,7 @@ impl<'db> FunctionType<'db> { /// Typed externally-visible "raw" signature of the last overload or implementation of this function. #[salsa::tracked( - returns(ref), - cycle_fn=last_definition_signature_cycle_recover, - cycle_initial=last_definition_signature_cycle_initial, + 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> { @@ -1194,15 +1190,6 @@ fn is_mode_with_nontrivial_return_type<'db>(db: &'db dyn Db, mode: Type<'db>) -> }) } -fn signature_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &CallableSignature<'db>, - _count: u32, - _function: FunctionType<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn signature_cycle_initial<'db>( _db: &'db dyn Db, _function: FunctionType<'db>, @@ -1210,15 +1197,6 @@ fn signature_cycle_initial<'db>( CallableSignature::single(Signature::bottom()) } -fn last_definition_signature_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Signature<'db>, - _count: u32, - _function: FunctionType<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn last_definition_signature_cycle_initial<'db>( _db: &'db dyn Db, _function: FunctionType<'db>, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 249c406a1b..2fff6f01d5 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -323,7 +323,6 @@ impl<'db> GenericContext<'db> { #[salsa::tracked( returns(ref), - cycle_fn=inferable_typevars_cycle_recover, cycle_initial=inferable_typevars_cycle_initial, heap_size=ruff_memory_usage::heap_size, )] @@ -626,15 +625,6 @@ impl<'db> GenericContext<'db> { } } -fn inferable_typevars_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &FxHashSet>, - _count: u32, - _self: GenericContext<'db>, -) -> salsa::CycleRecoveryAction>> { - salsa::CycleRecoveryAction::Iterate -} - fn inferable_typevars_cycle_initial<'db>( _db: &'db dyn Db, _self: GenericContext<'db>, diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 1bd539c074..d244597e16 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -67,7 +67,7 @@ const ITERATIONS_BEFORE_FALLBACK: u32 = 10; /// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. /// Use when checking a scope, or needing to provide a type for an arbitrary expression in the /// scope. -#[salsa::tracked(returns(ref), cycle_fn=scope_cycle_recover, cycle_initial=scope_cycle_initial, heap_size=ruff_memory_usage::heap_size)] +#[salsa::tracked(returns(ref), cycle_initial=scope_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn infer_scope_types<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> ScopeInference<'db> { let file = scope.file(db); let _span = tracing::trace_span!("infer_scope_types", scope=?scope.as_id(), ?file).entered(); @@ -81,15 +81,6 @@ pub(crate) fn infer_scope_types<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Sc TypeInferenceBuilder::new(db, InferenceRegion::Scope(scope), index, &module).finish_scope() } -fn scope_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &ScopeInference<'db>, - _count: u32, - _scope: ScopeId<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn scope_cycle_initial<'db>(_db: &'db dyn Db, scope: ScopeId<'db>) -> ScopeInference<'db> { ScopeInference::cycle_initial(scope) } @@ -118,6 +109,8 @@ pub(crate) fn infer_definition_types<'db>( fn definition_cycle_recover<'db>( db: &'db dyn Db, + _id: salsa::Id, + _last_provisional_value: &DefinitionInference<'db>, _value: &DefinitionInference<'db>, count: u32, definition: Definition<'db>, @@ -142,7 +135,7 @@ fn definition_cycle_initial<'db>( /// /// Deferred expressions are type expressions (annotations, base classes, aliases...) in a stub /// file, or in a file with `from __future__ import annotations`, or stringified annotations. -#[salsa::tracked(returns(ref), cycle_fn=deferred_cycle_recover, cycle_initial=deferred_cycle_initial, heap_size=ruff_memory_usage::heap_size)] +#[salsa::tracked(returns(ref), cycle_initial=deferred_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn infer_deferred_types<'db>( db: &'db dyn Db, definition: Definition<'db>, @@ -163,15 +156,6 @@ pub(crate) fn infer_deferred_types<'db>( .finish_definition() } -fn deferred_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &DefinitionInference<'db>, - _count: u32, - _definition: Definition<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn deferred_cycle_initial<'db>( db: &'db dyn Db, definition: Definition<'db>, @@ -239,6 +223,8 @@ pub(crate) fn infer_isolated_expression<'db>( fn expression_cycle_recover<'db>( db: &'db dyn Db, + _id: salsa::Id, + _last_provisional_value: &ExpressionInference<'db>, _value: &ExpressionInference<'db>, count: u32, input: InferExpression<'db>, @@ -289,7 +275,7 @@ pub(crate) fn infer_expression_type<'db>( infer_expression_type_impl(db, InferExpression::new(db, expression, tcx)) } -#[salsa::tracked(cycle_fn=single_expression_cycle_recover, cycle_initial=single_expression_cycle_initial, heap_size=ruff_memory_usage::heap_size)] +#[salsa::tracked(cycle_initial=single_expression_cycle_initial, heap_size=ruff_memory_usage::heap_size)] fn infer_expression_type_impl<'db>(db: &'db dyn Db, input: InferExpression<'db>) -> Type<'db> { let file = input.expression(db).file(db); let module = parsed_module(db, file).load(db); @@ -299,15 +285,6 @@ fn infer_expression_type_impl<'db>(db: &'db dyn Db, input: InferExpression<'db>) inference.expression_type(input.expression(db).node_ref(db, &module)) } -fn single_expression_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Type<'db>, - _count: u32, - _input: InferExpression<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn single_expression_cycle_initial<'db>( _db: &'db dyn Db, _input: InferExpression<'db>, @@ -402,7 +379,7 @@ impl<'db> TypeContext<'db> { /// /// Returns [`Truthiness::Ambiguous`] in case any non-definitely bound places /// were encountered while inferring the type of the expression. -#[salsa::tracked(cycle_fn=static_expression_truthiness_cycle_recover, cycle_initial=static_expression_truthiness_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] +#[salsa::tracked(cycle_initial=static_expression_truthiness_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] pub(crate) fn static_expression_truthiness<'db>( db: &'db dyn Db, expression: Expression<'db>, @@ -420,16 +397,6 @@ pub(crate) fn static_expression_truthiness<'db>( inference.expression_type(node).bool(db) } -#[expect(clippy::trivially_copy_pass_by_ref)] -fn static_expression_truthiness_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Truthiness, - _count: u32, - _expression: Expression<'db>, -) -> salsa::CycleRecoveryAction { - salsa::CycleRecoveryAction::Iterate -} - fn static_expression_truthiness_cycle_initial<'db>( _db: &'db dyn Db, _expression: Expression<'db>, @@ -443,7 +410,7 @@ fn static_expression_truthiness_cycle_initial<'db>( /// involved in an unpacking operation. It returns a result-like object that can be used to get the /// type of the variables involved in this unpacking along with any violations that are detected /// during this unpacking. -#[salsa::tracked(returns(ref), cycle_fn=unpack_cycle_recover, cycle_initial=unpack_cycle_initial, heap_size=ruff_memory_usage::heap_size)] +#[salsa::tracked(returns(ref), cycle_initial=unpack_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(super) 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); @@ -455,15 +422,6 @@ pub(super) fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> U unpacker.finish() } -fn unpack_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &UnpackResult<'db>, - _count: u32, - _unpack: Unpack<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn unpack_cycle_initial<'db>(_db: &'db dyn Db, _unpack: Unpack<'db>) -> UnpackResult<'db> { UnpackResult::cycle_initial(Type::Never) } diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index b6d42caae3..c8aa42d726 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -648,7 +648,7 @@ impl<'db> ProtocolInstanceType<'db> { /// 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 { - #[salsa::tracked(cycle_fn=recover, cycle_initial=initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(cycle_initial=initial, heap_size=ruff_memory_usage::heap_size)] fn inner<'db>(db: &'db dyn Db, protocol: ProtocolInstanceType<'db>, _: ()) -> bool { Type::object() .satisfies_protocol( @@ -662,17 +662,6 @@ impl<'db> ProtocolInstanceType<'db> { .is_always_satisfied() } - #[expect(clippy::trivially_copy_pass_by_ref)] - fn recover<'db>( - _db: &'db dyn Db, - _result: &bool, - _count: u32, - _value: ProtocolInstanceType<'db>, - _: (), - ) -> salsa::CycleRecoveryAction { - salsa::CycleRecoveryAction::Iterate - } - fn initial<'db>(_db: &'db dyn Db, _value: ProtocolInstanceType<'db>, _: ()) -> bool { true } diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 4f4505e5a0..b797eb4764 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -83,7 +83,6 @@ fn all_narrowing_constraints_for_pattern<'db>( #[salsa::tracked( returns(as_ref), - cycle_fn=constraints_for_expression_cycle_recover, cycle_initial=constraints_for_expression_cycle_initial, heap_size=ruff_memory_usage::heap_size, )] @@ -98,7 +97,6 @@ fn all_narrowing_constraints_for_expression<'db>( #[salsa::tracked( returns(as_ref), - cycle_fn=negative_constraints_for_expression_cycle_recover, cycle_initial=negative_constraints_for_expression_cycle_initial, heap_size=ruff_memory_usage::heap_size, )] @@ -120,16 +118,6 @@ fn all_negative_narrowing_constraints_for_pattern<'db>( NarrowingConstraintsBuilder::new(db, &module, PredicateNode::Pattern(pattern), false).finish() } -#[expect(clippy::ref_option)] -fn constraints_for_expression_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Option>, - _count: u32, - _expression: Expression<'db>, -) -> salsa::CycleRecoveryAction>> { - salsa::CycleRecoveryAction::Iterate -} - fn constraints_for_expression_cycle_initial<'db>( _db: &'db dyn Db, _expression: Expression<'db>, @@ -137,16 +125,6 @@ fn constraints_for_expression_cycle_initial<'db>( None } -#[expect(clippy::ref_option)] -fn negative_constraints_for_expression_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Option>, - _count: u32, - _expression: Expression<'db>, -) -> salsa::CycleRecoveryAction>> { - salsa::CycleRecoveryAction::Iterate -} - fn negative_constraints_for_expression_cycle_initial<'db>( _db: &'db dyn Db, _expression: Expression<'db>, diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 904d9d97d0..46a1142d4c 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -766,7 +766,7 @@ impl BoundOnClass { } /// Inner Salsa query for [`ProtocolClassLiteral::interface`]. -#[salsa::tracked(cycle_fn=proto_interface_cycle_recover, cycle_initial=proto_interface_cycle_initial, heap_size=ruff_memory_usage::heap_size)] +#[salsa::tracked(cycle_initial=proto_interface_cycle_initial, heap_size=ruff_memory_usage::heap_size)] fn cached_protocol_interface<'db>( db: &'db dyn Db, class: ClassType<'db>, @@ -864,15 +864,6 @@ fn cached_protocol_interface<'db>( // If we use `expect(clippy::trivially_copy_pass_by_ref)` here, // the lint expectation is unfulfilled on WASM #[allow(clippy::trivially_copy_pass_by_ref)] -fn proto_interface_cycle_recover<'db>( - _db: &dyn Db, - _value: &ProtocolInterface<'db>, - _count: u32, - _class: ClassType<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn proto_interface_cycle_initial<'db>( db: &'db dyn Db, _class: ClassType<'db>, diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index f091c99ea5..fb08672de1 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -201,7 +201,7 @@ impl<'db> TupleType<'db> { // N.B. If this method is not Salsa-tracked, we take 10 minutes to check // `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_fn=to_class_type_cycle_recover, cycle_initial=to_class_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[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> { let tuple_class = KnownClass::Tuple .try_to_class_literal(db) @@ -290,15 +290,6 @@ impl<'db> TupleType<'db> { } } -fn to_class_type_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &ClassType<'db>, - _count: u32, - _self: TupleType<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - fn to_class_type_cycle_initial<'db>(db: &'db dyn Db, self_: TupleType<'db>) -> ClassType<'db> { let tuple_class = KnownClass::Tuple .try_to_class_literal(db) diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 016c7ea6b3..24034b4854 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -30,7 +30,7 @@ ty_python_semantic = { path = "../crates/ty_python_semantic" } ty_vendored = { path = "../crates/ty_vendored" } libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "ef9f9329be6923acd050c8dddd172e3bc93e8051", default-features = false, features = [ +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d38145c29574758de7ffbe8a13cd4584c3b09161", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", From 589e8ac0d92728b8f5aed7e3ad3469ce1993619e Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 23 Oct 2025 09:34:39 +0200 Subject: [PATCH 017/188] [ty] Infer type for implicit `self` parameters in method bodies (#20922) ## Summary Infer a type of `Self` for unannotated `self` parameters in methods of classes. part of https://github.com/astral-sh/ty/issues/159 closes https://github.com/astral-sh/ty/issues/1081 ## Conformance tests changes ```diff +enums_member_values.py:85:9: error[invalid-assignment] Object of type `int` is not assignable to attribute `_value_` of type `str` ``` A true positive :heavy_check_mark: ```diff -generics_self_advanced.py:35:9: error[type-assertion-failure] Argument does not have asserted type `Self@method2` -generics_self_basic.py:14:9: error[type-assertion-failure] Argument does not have asserted type `Self@set_scale ``` Two false positives going away :heavy_check_mark: ```diff +generics_syntax_infer_variance.py:82:9: error[invalid-assignment] Cannot assign to final attribute `x` on type `Self@__init__` ``` This looks like a true positive to me, even if it's not marked with `# E` :heavy_check_mark: ```diff +protocols_explicit.py:56:9: error[invalid-assignment] Object of type `tuple[int, int, str]` is not assignable to attribute `rgb` of type `tuple[int, int, int]` ``` True positive :heavy_check_mark: ``` +protocols_explicit.py:85:9: error[invalid-attribute-access] Cannot assign to ClassVar `cm1` from an instance of type `Self@__init__` ``` This looks like a true positive to me, even if it's not marked with `# E`. But this is consistent with our understanding of `ClassVar`, I think. :heavy_check_mark: ```py +qualifiers_final_annotation.py:52:9: error[invalid-assignment] Cannot assign to final attribute `ID4` on type `Self@__init__` +qualifiers_final_annotation.py:65:9: error[invalid-assignment] Cannot assign to final attribute `ID7` on type `Self@method1` ``` New true positives :heavy_check_mark: ```py +qualifiers_final_annotation.py:52:9: error[invalid-assignment] Cannot assign to final attribute `ID4` on type `Self@__init__` +qualifiers_final_annotation.py:57:13: error[invalid-assignment] Cannot assign to final attribute `ID6` on type `Self@__init__` +qualifiers_final_annotation.py:59:13: error[invalid-assignment] Cannot assign to final attribute `ID6` on type `Self@__init__` ``` This is a new false positive, but that's a pre-existing issue on main (if you annotate with `Self`): https://play.ty.dev/3ee1c56d-7e13-43bb-811a-7a81e236e6ab :x: => reported as https://github.com/astral-sh/ty/issues/1409 ## Ecosystem * There are 5931 new `unresolved-attribute` and 3292 new `possibly-missing-attribute` attribute errors, way too many to look at all of them. I randomly sampled 15 of these errors and found: * 13 instances where there was simply no such attribute that we could plausibly see. Sometimes [I didn't find it anywhere](https://github.com/internetarchive/openlibrary/blob/8644d886c6579a5f49faadc4cd1ba9992e603d7e/openlibrary/plugins/openlibrary/tests/test_listapi.py#L33). Sometimes it was set externally on the object. Sometimes there was some [`setattr` dynamicness going on](https://github.com/pypa/setuptools/blob/a49f6b927d83b97630b4fb030de8035ed32436fd/setuptools/wheel.py#L88-L94). I would consider all of them to be true positives. * 1 instance where [attribute was set on `obj` in `__new__`](https://github.com/sympy/sympy/blob/9e87b44fd43572b9c4cc95ec569a2f4b81d56499/sympy/tensor/array/array_comprehension.py#L45C1-L45C36), which we don't support yet * 1 instance [where the attribute was defined via `__slots__` ](https://github.com/spack/spack/blob/e250ec0fc81130b708a8abe1894f0cc926880210/lib/spack/spack/vendor/pyrsistent/_pdeque.py#L48C5-L48C14) * I see 44 instances [of the false positive above](https://github.com/astral-sh/ty/issues/1409) with `Final` instance attributes being set in `__init__`. I don't think this should block this PR. ## Test Plan New Markdown tests. --------- Co-authored-by: Shaygan Hooshyari --- crates/ruff_benchmark/benches/ty.rs | 4 +- crates/ruff_benchmark/benches/ty_walltime.rs | 4 +- crates/ty_ide/src/completion.rs | 36 ++- crates/ty_ide/src/semantic_tokens.rs | 8 +- .../resources/mdtest/annotations/self.md | 67 ++-- .../resources/mdtest/attributes.md | 28 +- .../resources/mdtest/call/dunder.md | 2 +- .../resources/mdtest/class/super.md | 13 +- .../resources/mdtest/descriptor_protocol.md | 1 + .../resources/mdtest/named_tuple.md | 3 +- ...licit_Super_Objec…_(f9e5e48e3a4a4c12).snap | 289 +++++++++--------- .../resources/mdtest/type_qualifiers/final.md | 7 + .../ty_python_semantic/src/types/builder.rs | 2 +- .../ty_python_semantic/src/types/generics.rs | 6 +- .../src/types/infer/builder.rs | 63 +++- python/py-fuzzer/fuzz.py | 2 +- 16 files changed, 325 insertions(+), 210 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index bceff924fb..e932aabd15 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -667,7 +667,7 @@ fn attrs(criterion: &mut Criterion) { max_dep_date: "2025-06-17", python_version: PythonVersion::PY313, }, - 100, + 110, ); bench_project(&benchmark, criterion); @@ -684,7 +684,7 @@ fn anyio(criterion: &mut Criterion) { max_dep_date: "2025-06-17", python_version: PythonVersion::PY313, }, - 100, + 150, ); bench_project(&benchmark, criterion); diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index dfa76d2b33..be6195d96a 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -210,7 +210,7 @@ static TANJUN: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY312, }, - 100, + 320, ); static STATIC_FRAME: Benchmark = Benchmark::new( @@ -226,7 +226,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new( max_dep_date: "2025-08-09", python_version: PythonVersion::PY311, }, - 630, + 750, ); #[track_caller] diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 915fa0c030..feb4e45fc6 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -1957,14 +1957,34 @@ class Quux: ", ); - // FIXME: This should list completions on `self`, which should - // include, at least, `foo` and `bar`. At time of writing - // (2025-06-04), the type of `self` is inferred as `Unknown` in - // this context. This in turn prevents us from getting a list - // of available attributes. - // - // See: https://github.com/astral-sh/ty/issues/159 - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.completions_without_builtins(), @r" + bar + baz + foo + __annotations__ + __class__ + __delattr__ + __dict__ + __dir__ + __doc__ + __eq__ + __format__ + __getattribute__ + __getstate__ + __hash__ + __init__ + __init_subclass__ + __module__ + __ne__ + __new__ + __reduce__ + __reduce_ex__ + __repr__ + __setattr__ + __sizeof__ + __str__ + __subclasshook__ + "); } #[test] diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index 63dcaa0cb6..4e66881f48 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -1798,23 +1798,23 @@ class BoundedContainer[T: int, U = str]: "T" @ 554..555: TypeParameter "value2" @ 557..563: Parameter "U" @ 565..566: TypeParameter - "self" @ 577..581: Variable + "self" @ 577..581: TypeParameter "value1" @ 582..588: Variable "T" @ 590..591: TypeParameter "value1" @ 594..600: Parameter - "self" @ 609..613: Variable + "self" @ 609..613: TypeParameter "value2" @ 614..620: Variable "U" @ 622..623: TypeParameter "value2" @ 626..632: Parameter "get_first" @ 642..651: Method [definition] "self" @ 652..656: SelfParameter "T" @ 661..662: TypeParameter - "self" @ 679..683: Variable + "self" @ 679..683: TypeParameter "value1" @ 684..690: Variable "get_second" @ 700..710: Method [definition] "self" @ 711..715: SelfParameter "U" @ 720..721: TypeParameter - "self" @ 738..742: Variable + "self" @ 738..742: TypeParameter "value2" @ 743..749: Variable "BoundedContainer" @ 798..814: Class [definition] "T" @ 815..816: TypeParameter [definition] diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 91a2bcaf07..621db497c4 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -56,7 +56,7 @@ In instance methods, the first parameter (regardless of its name) is assumed to ```toml [environment] -python-version = "3.11" +python-version = "3.12" ``` ```py @@ -64,16 +64,30 @@ from typing import Self class A: def implicit_self(self) -> Self: - # TODO: This should be Self@implicit_self - reveal_type(self) # revealed: Unknown + reveal_type(self) # revealed: Self@implicit_self return self - def a_method(self) -> int: - def first_arg_is_not_self(a: int) -> int: + def implicit_self_generic[T](self, x: T) -> T: + reveal_type(self) # revealed: Self@implicit_self_generic + + return x + + def method_a(self) -> None: + def first_param_is_not_self(a: int): reveal_type(a) # revealed: int - return a - return first_arg_is_not_self(1) + reveal_type(self) # revealed: Self@method_a + + def first_param_is_not_self_unannotated(a): + reveal_type(a) # revealed: Unknown + reveal_type(self) # revealed: Self@method_a + + def first_param_is_also_not_self(self) -> None: + reveal_type(self) # revealed: Unknown + + def first_param_is_explicit_self(this: Self) -> None: + reveal_type(this) # revealed: Self@method_a + reveal_type(self) # revealed: Self@method_a @classmethod def a_classmethod(cls) -> Self: @@ -127,19 +141,16 @@ The name `self` is not special in any way. ```py class B: def name_does_not_matter(this) -> Self: - # TODO: Should reveal Self@name_does_not_matter - reveal_type(this) # revealed: Unknown + reveal_type(this) # revealed: Self@name_does_not_matter return this def positional_only(self, /, x: int) -> Self: - # TODO: Should reveal Self@positional_only - reveal_type(self) # revealed: Unknown + reveal_type(self) # revealed: Self@positional_only return self def keyword_only(self, *, x: int) -> Self: - # TODO: Should reveal Self@keyword_only - reveal_type(self) # revealed: Unknown + reveal_type(self) # revealed: Self@keyword_only return self @property @@ -165,8 +176,7 @@ T = TypeVar("T") class G(Generic[T]): def id(self) -> Self: - # TODO: Should reveal Self@id - reveal_type(self) # revealed: Unknown + reveal_type(self) # revealed: Self@id return self @@ -252,6 +262,20 @@ class LinkedList: reveal_type(LinkedList().next()) # revealed: LinkedList ``` +Attributes can also refer to a generic parameter: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + foo: T + def method(self) -> None: + reveal_type(self) # revealed: Self@method + reveal_type(self.foo) # revealed: T@C +``` + ## Generic Classes ```py @@ -342,31 +366,28 @@ b: Self # TODO: "Self" cannot be used in a function with a `self` or `cls` parameter that has a type annotation other than "Self" class Foo: - # TODO: rejected Self because self has a different type + # TODO: This `self: T` annotation should be rejected because `T` is not `Self` def has_existing_self_annotation(self: T) -> Self: return self # error: [invalid-return-type] def return_concrete_type(self) -> Self: - # TODO: tell user to use "Foo" instead of "Self" + # TODO: We could emit a hint that suggests annotating with `Foo` instead of `Self` # error: [invalid-return-type] return Foo() @staticmethod - # TODO: reject because of staticmethod + # TODO: The usage of `Self` here should be rejected because this is a static method def make() -> Self: # error: [invalid-return-type] return Foo() -class Bar(Generic[T]): - foo: T - def bar(self) -> T: - return self.foo +class Bar(Generic[T]): ... # error: [invalid-type-form] class Baz(Bar[Self]): ... class MyMetaclass(type): - # TODO: rejected + # TODO: reject the Self usage. because self cannot be used within a metaclass. def __new__(cls) -> Self: return super().__new__(cls) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index f6c20d0c5f..82d7f0c57b 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -26,9 +26,7 @@ class C: c_instance = C(1) reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"] - -# TODO: Same here. This should be `Unknown | Literal[1, "a"]` -reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown +reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown | Literal[1, "a"] # There is no special handling of attributes that are (directly) assigned to a declared parameter, # which means we union with `Unknown` here, since the attribute itself is not declared. This is @@ -177,8 +175,7 @@ c_instance = C(1) reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"] -# TODO: Should be `Unknown | Literal[1, "a"]` -reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown +reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown | Literal[1, "a"] reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None @@ -399,9 +396,19 @@ class TupleIterable: class C: def __init__(self) -> None: + # TODO: Should not emit this diagnostic + # error: [unresolved-attribute] [... for self.a in IntIterable()] + # TODO: Should not emit this diagnostic + # error: [unresolved-attribute] + # error: [unresolved-attribute] [... for (self.b, self.c) in TupleIterable()] + # TODO: Should not emit this diagnostic + # error: [unresolved-attribute] + # error: [unresolved-attribute] [... for self.d in IntIterable() for self.e in IntIterable()] + # TODO: Should not emit this diagnostic + # error: [unresolved-attribute] [[... for self.f in IntIterable()] for _ in IntIterable()] [[... for self.g in IntIterable()] for self in [D()]] @@ -598,6 +605,8 @@ class C: self.c = c if False: def set_e(self, e: str) -> None: + # TODO: Should not emit this diagnostic + # error: [unresolved-attribute] self.e = e # TODO: this would ideally be `Unknown | Literal[1]` @@ -685,7 +694,7 @@ class C: pure_class_variable2: ClassVar = 1 def method(self): - # TODO: this should be an error + # error: [invalid-attribute-access] "Cannot assign to ClassVar `pure_class_variable1` from an instance of type `Self@method`" self.pure_class_variable1 = "value set through instance" reveal_type(C.pure_class_variable1) # revealed: str @@ -885,11 +894,9 @@ class Intermediate(Base): # TODO: This should be an error (violates Liskov) self.redeclared_in_method_with_wider_type: object = object() - # TODO: This should be an `invalid-assignment` error - self.overwritten_in_subclass_method = None + self.overwritten_in_subclass_method = None # error: [invalid-assignment] - # TODO: This should be an `invalid-assignment` error - self.pure_overwritten_in_subclass_method = None + self.pure_overwritten_in_subclass_method = None # error: [invalid-assignment] self.pure_undeclared = "intermediate" @@ -1839,6 +1846,7 @@ def external_getattribute(name) -> int: class ThisFails: def __init__(self): + # error: [invalid-assignment] "Implicit shadowing of function `__getattribute__`" self.__getattribute__ = external_getattribute # error: [unresolved-attribute] diff --git a/crates/ty_python_semantic/resources/mdtest/call/dunder.md b/crates/ty_python_semantic/resources/mdtest/call/dunder.md index 09b83f0035..721517eac4 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/dunder.md +++ b/crates/ty_python_semantic/resources/mdtest/call/dunder.md @@ -205,7 +205,7 @@ class C: return str(key) def f(self): - # TODO: This should emit an `invalid-assignment` diagnostic once we understand the type of `self` + # error: [invalid-assignment] "Implicit shadowing of function `__getitem__`" self.__getitem__ = None # This is still fine, and simply calls the `__getitem__` method on the class diff --git a/crates/ty_python_semantic/resources/mdtest/class/super.md b/crates/ty_python_semantic/resources/mdtest/class/super.md index 1862866764..d08c5777c1 100644 --- a/crates/ty_python_semantic/resources/mdtest/class/super.md +++ b/crates/ty_python_semantic/resources/mdtest/class/super.md @@ -163,14 +163,13 @@ class A: class B(A): def __init__(self, a: int): - # TODO: Once `Self` is supported, this should be `, B>` - reveal_type(super()) # revealed: , Unknown> + reveal_type(super()) # revealed: , B> reveal_type(super(object, super())) # revealed: , super> super().__init__(a) @classmethod def f(cls): - # TODO: Once `Self` is supported, this should be `, >` + # TODO: Once `cls` is supported, this should be `, >` reveal_type(super()) # revealed: , Unknown> super().f() @@ -358,15 +357,15 @@ from __future__ import annotations class A: def test(self): - reveal_type(super()) # revealed: , Unknown> + reveal_type(super()) # revealed: , A> class B: def test(self): - reveal_type(super()) # revealed: , Unknown> + reveal_type(super()) # revealed: , B> class C(A.B): def test(self): - reveal_type(super()) # revealed: , Unknown> + reveal_type(super()) # revealed: , C> def inner(t: C): reveal_type(super()) # revealed: , C> @@ -616,7 +615,7 @@ class A: class B(A): def __init__(self, a: int): super().__init__(a) - # TODO: Once `Self` is supported, this should raise `unresolved-attribute` error + # error: [unresolved-attribute] "Object of type `, B>` has no attribute `a`" super().a # error: [unresolved-attribute] "Object of type `, B>` has no attribute `a`" diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index f5e0f254d0..f4b5854ed7 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -170,6 +170,7 @@ def f1(flag: bool): attr = DataDescriptor() def f(self): + # error: [invalid-assignment] "Invalid assignment to data descriptor attribute `attr` on type `Self@f` with custom `__set__` method" self.attr = "normal" reveal_type(C1().attr) # revealed: Unknown | Literal["data", "normal"] diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index 3a2b7ce14b..f8c8a330f2 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -208,8 +208,7 @@ class SuperUser(User): def now_called_robert(self): self.name = "Robert" # fine because overridden with a mutable attribute - # TODO: this should cause us to emit an error as we're assigning to a read-only property - # inherited from the `NamedTuple` superclass (requires https://github.com/astral-sh/ty/issues/159) + # error: 9 [invalid-assignment] "Cannot assign to read-only property `nickname` on object of type `Self@now_called_robert`" self.nickname = "Bob" james = SuperUser(0, "James", 42, "Jimmy") diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap index b13937e6e5..c9fcea3d5a 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap @@ -21,144 +21,143 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md 7 | 8 | class B(A): 9 | def __init__(self, a: int): - 10 | # TODO: Once `Self` is supported, this should be `, B>` - 11 | reveal_type(super()) # revealed: , Unknown> - 12 | reveal_type(super(object, super())) # revealed: , super> - 13 | super().__init__(a) - 14 | - 15 | @classmethod - 16 | def f(cls): - 17 | # TODO: Once `Self` is supported, this should be `, >` - 18 | reveal_type(super()) # revealed: , Unknown> - 19 | super().f() - 20 | - 21 | super(B, B(42)).__init__(42) - 22 | super(B, B).f() - 23 | import enum - 24 | from typing import Any, Self, Never, Protocol, Callable - 25 | from ty_extensions import Intersection - 26 | - 27 | class BuilderMeta(type): - 28 | def __new__( - 29 | cls: type[Any], - 30 | name: str, - 31 | bases: tuple[type, ...], - 32 | dct: dict[str, Any], - 33 | ) -> BuilderMeta: - 34 | # revealed: , Any> - 35 | s = reveal_type(super()) - 36 | # revealed: Any - 37 | return reveal_type(s.__new__(cls, name, bases, dct)) - 38 | - 39 | class BuilderMeta2(type): - 40 | def __new__( - 41 | cls: type[BuilderMeta2], - 42 | name: str, - 43 | bases: tuple[type, ...], - 44 | dct: dict[str, Any], - 45 | ) -> BuilderMeta2: - 46 | # revealed: , > - 47 | s = reveal_type(super()) - 48 | # TODO: should be `BuilderMeta2` (needs https://github.com/astral-sh/ty/issues/501) - 49 | # revealed: Unknown - 50 | return reveal_type(s.__new__(cls, name, bases, dct)) - 51 | - 52 | class Foo[T]: - 53 | x: T - 54 | - 55 | def method(self: Any): - 56 | reveal_type(super()) # revealed: , Any> - 57 | - 58 | if isinstance(self, Foo): - 59 | reveal_type(super()) # revealed: , Any> - 60 | - 61 | def method2(self: Foo[T]): - 62 | # revealed: , Foo[T@Foo]> - 63 | reveal_type(super()) - 64 | - 65 | def method3(self: Foo): - 66 | # revealed: , Foo[Unknown]> - 67 | reveal_type(super()) - 68 | - 69 | def method4(self: Self): - 70 | # revealed: , Foo[T@Foo]> - 71 | reveal_type(super()) - 72 | - 73 | def method5[S: Foo[int]](self: S, other: S) -> S: - 74 | # revealed: , Foo[int]> - 75 | reveal_type(super()) - 76 | return self - 77 | - 78 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S: - 79 | # revealed: , Foo[int]> | , Foo[str]> - 80 | reveal_type(super()) - 81 | return self - 82 | - 83 | def method7[S](self: S, other: S) -> S: - 84 | # error: [invalid-super-argument] - 85 | # revealed: Unknown - 86 | reveal_type(super()) - 87 | return self - 88 | - 89 | def method8[S: int](self: S, other: S) -> S: - 90 | # error: [invalid-super-argument] - 91 | # revealed: Unknown - 92 | reveal_type(super()) - 93 | return self - 94 | - 95 | def method9[S: (int, str)](self: S, other: S) -> S: - 96 | # error: [invalid-super-argument] - 97 | # revealed: Unknown - 98 | reveal_type(super()) - 99 | return self -100 | -101 | def method10[S: Callable[..., str]](self: S, other: S) -> S: -102 | # error: [invalid-super-argument] -103 | # revealed: Unknown -104 | reveal_type(super()) -105 | return self -106 | -107 | type Alias = Bar -108 | -109 | class Bar: -110 | def method(self: Alias): -111 | # revealed: , Bar> -112 | reveal_type(super()) -113 | -114 | def pls_dont_call_me(self: Never): -115 | # revealed: , Unknown> -116 | reveal_type(super()) -117 | -118 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]): -119 | # revealed: , Bar> -120 | reveal_type(super()) -121 | -122 | class P(Protocol): -123 | def method(self: P): -124 | # revealed: , P> -125 | reveal_type(super()) -126 | -127 | class E(enum.Enum): -128 | X = 1 -129 | -130 | def method(self: E): -131 | match self: -132 | case E.X: -133 | # revealed: , E> -134 | reveal_type(super()) + 10 | reveal_type(super()) # revealed: , B> + 11 | reveal_type(super(object, super())) # revealed: , super> + 12 | super().__init__(a) + 13 | + 14 | @classmethod + 15 | def f(cls): + 16 | # TODO: Once `cls` is supported, this should be `, >` + 17 | reveal_type(super()) # revealed: , Unknown> + 18 | super().f() + 19 | + 20 | super(B, B(42)).__init__(42) + 21 | super(B, B).f() + 22 | import enum + 23 | from typing import Any, Self, Never, Protocol, Callable + 24 | from ty_extensions import Intersection + 25 | + 26 | class BuilderMeta(type): + 27 | def __new__( + 28 | cls: type[Any], + 29 | name: str, + 30 | bases: tuple[type, ...], + 31 | dct: dict[str, Any], + 32 | ) -> BuilderMeta: + 33 | # revealed: , Any> + 34 | s = reveal_type(super()) + 35 | # revealed: Any + 36 | return reveal_type(s.__new__(cls, name, bases, dct)) + 37 | + 38 | class BuilderMeta2(type): + 39 | def __new__( + 40 | cls: type[BuilderMeta2], + 41 | name: str, + 42 | bases: tuple[type, ...], + 43 | dct: dict[str, Any], + 44 | ) -> BuilderMeta2: + 45 | # revealed: , > + 46 | s = reveal_type(super()) + 47 | # TODO: should be `BuilderMeta2` (needs https://github.com/astral-sh/ty/issues/501) + 48 | # revealed: Unknown + 49 | return reveal_type(s.__new__(cls, name, bases, dct)) + 50 | + 51 | class Foo[T]: + 52 | x: T + 53 | + 54 | def method(self: Any): + 55 | reveal_type(super()) # revealed: , Any> + 56 | + 57 | if isinstance(self, Foo): + 58 | reveal_type(super()) # revealed: , Any> + 59 | + 60 | def method2(self: Foo[T]): + 61 | # revealed: , Foo[T@Foo]> + 62 | reveal_type(super()) + 63 | + 64 | def method3(self: Foo): + 65 | # revealed: , Foo[Unknown]> + 66 | reveal_type(super()) + 67 | + 68 | def method4(self: Self): + 69 | # revealed: , Foo[T@Foo]> + 70 | reveal_type(super()) + 71 | + 72 | def method5[S: Foo[int]](self: S, other: S) -> S: + 73 | # revealed: , Foo[int]> + 74 | reveal_type(super()) + 75 | return self + 76 | + 77 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S: + 78 | # revealed: , Foo[int]> | , Foo[str]> + 79 | reveal_type(super()) + 80 | return self + 81 | + 82 | def method7[S](self: S, other: S) -> S: + 83 | # error: [invalid-super-argument] + 84 | # revealed: Unknown + 85 | reveal_type(super()) + 86 | return self + 87 | + 88 | def method8[S: int](self: S, other: S) -> S: + 89 | # error: [invalid-super-argument] + 90 | # revealed: Unknown + 91 | reveal_type(super()) + 92 | return self + 93 | + 94 | def method9[S: (int, str)](self: S, other: S) -> S: + 95 | # error: [invalid-super-argument] + 96 | # revealed: Unknown + 97 | reveal_type(super()) + 98 | return self + 99 | +100 | def method10[S: Callable[..., str]](self: S, other: S) -> S: +101 | # error: [invalid-super-argument] +102 | # revealed: Unknown +103 | reveal_type(super()) +104 | return self +105 | +106 | type Alias = Bar +107 | +108 | class Bar: +109 | def method(self: Alias): +110 | # revealed: , Bar> +111 | reveal_type(super()) +112 | +113 | def pls_dont_call_me(self: Never): +114 | # revealed: , Unknown> +115 | reveal_type(super()) +116 | +117 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]): +118 | # revealed: , Bar> +119 | reveal_type(super()) +120 | +121 | class P(Protocol): +122 | def method(self: P): +123 | # revealed: , P> +124 | reveal_type(super()) +125 | +126 | class E(enum.Enum): +127 | X = 1 +128 | +129 | def method(self: E): +130 | match self: +131 | case E.X: +132 | # revealed: , E> +133 | reveal_type(super()) ``` # Diagnostics ``` error[invalid-super-argument]: `S@method7` is not an instance or subclass of `` in `super(, S@method7)` call - --> src/mdtest_snippet.py:86:21 + --> src/mdtest_snippet.py:85:21 | -84 | # error: [invalid-super-argument] -85 | # revealed: Unknown -86 | reveal_type(super()) +83 | # error: [invalid-super-argument] +84 | # revealed: Unknown +85 | reveal_type(super()) | ^^^^^^^ -87 | return self +86 | return self | info: Type variable `S` has `object` as its implicit upper bound info: `object` is not an instance or subclass of `` @@ -169,13 +168,13 @@ info: rule `invalid-super-argument` is enabled by default ``` error[invalid-super-argument]: `S@method8` is not an instance or subclass of `` in `super(, S@method8)` call - --> src/mdtest_snippet.py:92:21 + --> src/mdtest_snippet.py:91:21 | -90 | # error: [invalid-super-argument] -91 | # revealed: Unknown -92 | reveal_type(super()) +89 | # error: [invalid-super-argument] +90 | # revealed: Unknown +91 | reveal_type(super()) | ^^^^^^^ -93 | return self +92 | return self | info: Type variable `S` has upper bound `int` info: `int` is not an instance or subclass of `` @@ -185,13 +184,13 @@ info: rule `invalid-super-argument` is enabled by default ``` error[invalid-super-argument]: `S@method9` is not an instance or subclass of `` in `super(, S@method9)` call - --> src/mdtest_snippet.py:98:21 + --> src/mdtest_snippet.py:97:21 | -96 | # error: [invalid-super-argument] -97 | # revealed: Unknown -98 | reveal_type(super()) +95 | # error: [invalid-super-argument] +96 | # revealed: Unknown +97 | reveal_type(super()) | ^^^^^^^ -99 | return self +98 | return self | info: Type variable `S` has constraints `int, str` info: `int | str` is not an instance or subclass of `` @@ -201,13 +200,13 @@ info: rule `invalid-super-argument` is enabled by default ``` error[invalid-super-argument]: `S@method10` is a type variable with an abstract/structural type as its bounds or constraints, in `super(, S@method10)` call - --> src/mdtest_snippet.py:104:21 + --> src/mdtest_snippet.py:103:21 | -102 | # error: [invalid-super-argument] -103 | # revealed: Unknown -104 | reveal_type(super()) +101 | # error: [invalid-super-argument] +102 | # revealed: Unknown +103 | reveal_type(super()) | ^^^^^^^ -105 | return self +104 | return self | info: Type variable `S` has upper bound `(...) -> str` info: rule `invalid-super-argument` is enabled by default 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 7c68534e10..ce2879e248 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md @@ -88,6 +88,8 @@ class C: self.FINAL_C: Final[int] = 1 self.FINAL_D: Final = 1 self.FINAL_E: Final + # TODO: Should not be an error + # error: [invalid-assignment] "Cannot assign to final attribute `FINAL_E` on type `Self@__init__`" self.FINAL_E = 1 reveal_type(C.FINAL_A) # revealed: int @@ -184,6 +186,7 @@ class C(metaclass=Meta): self.INSTANCE_FINAL_A: Final[int] = 1 self.INSTANCE_FINAL_B: Final = 1 self.INSTANCE_FINAL_C: Final[int] + # error: [invalid-assignment] "Cannot assign to final attribute `INSTANCE_FINAL_C` on type `Self@__init__`" self.INSTANCE_FINAL_C = 1 # error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_A` on type ``" @@ -278,6 +281,8 @@ class C: def __init__(self): self.LEGAL_H: Final[int] = 1 self.LEGAL_I: Final[int] + # TODO: Should not be an error + # error: [invalid-assignment] self.LEGAL_I = 1 # error: [invalid-type-form] "`Final` is not allowed in function parameter annotations" @@ -390,6 +395,8 @@ class C: DEFINED_IN_INIT: Final[int] def __init__(self): + # TODO: should not be an error + # error: [invalid-assignment] self.DEFINED_IN_INIT = 1 ``` diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index 152f9ba325..de413c00a9 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -206,7 +206,7 @@ enum ReduceResult<'db> { // // For now (until we solve https://github.com/astral-sh/ty/issues/957), keep this number // below 200, which is the salsa fixpoint iteration limit. -const MAX_UNION_LITERALS: usize = 199; +const MAX_UNION_LITERALS: usize = 190; pub(crate) struct UnionBuilder<'db> { elements: Vec>, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 2fff6f01d5..e8c6305d06 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -80,11 +80,11 @@ pub(crate) fn bind_typevar<'db>( /// Create a `typing.Self` type variable for a given class. pub(crate) fn typing_self<'db>( db: &'db dyn Db, - scope_id: ScopeId, + function_scope_id: ScopeId, typevar_binding_context: Option>, class: ClassLiteral<'db>, ) -> Option> { - let index = semantic_index(db, scope_id.file(db)); + let index = semantic_index(db, function_scope_id.file(db)); let identity = TypeVarIdentity::new( db, @@ -110,7 +110,7 @@ pub(crate) fn typing_self<'db>( bind_typevar( db, index, - scope_id.file_scope_id(db), + function_scope_id.file_scope_id(db), typevar_binding_context, typevar, ) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 99df77b71d..00bf13db47 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -81,7 +81,7 @@ use crate::types::function::{ }; use crate::types::generics::{ GenericContext, InferableTypeVars, LegacyGenericBase, SpecializationBuilder, bind_typevar, - enclosing_generic_contexts, + enclosing_generic_contexts, typing_self, }; use crate::types::infer::nearest_enclosing_function; use crate::types::instance::SliceLiteral; @@ -2495,6 +2495,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { let ty = if let Some(default_ty) = default_ty { UnionType::from_elements(self.db(), [Type::unknown(), default_ty]) + } else if let Some(ty) = self.special_first_method_parameter_type(parameter) { + ty } else { Type::unknown() }; @@ -2535,6 +2537,65 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + /// 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 method = infer_definition_types(db, method_definition) + .declaration_type(method_definition) + .inner_type() + .as_function_literal()?; + + if method.is_classmethod(db) { + // TODO: set the type for `cls` argument + return None; + } else if method.is_staticmethod(db) { + return None; + } + + 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()?; + + typing_self(db, self.scope(), Some(method_definition), class_literal) + } + /// Set initial declared/inferred types for a `*args` variadic positional parameter. /// /// The annotated type is implicitly wrapped in a string-keyed dictionary. diff --git a/python/py-fuzzer/fuzz.py b/python/py-fuzzer/fuzz.py index 76cc52d2a1..e113d7e179 100644 --- a/python/py-fuzzer/fuzz.py +++ b/python/py-fuzzer/fuzz.py @@ -139,7 +139,7 @@ class FuzzResult: case Executable.TY: panic_message = f"The following code triggers a {new}ty panic:" case _ as unreachable: - assert_never(unreachable) # ty: ignore[type-assertion-failure] + assert_never(unreachable) print(colored(panic_message, "red")) print() From c3631c78bd94b7afbe4293d3e2555f0c88d0c4ba Mon Sep 17 00:00:00 2001 From: Eric Mark Martin Date: Thu, 23 Oct 2025 03:50:21 -0400 Subject: [PATCH 018/188] [ty] Add docstrings for `ty_extensions` functions (#21036) Co-authored-by: David Peter --- .../ty_extensions/ty_extensions.pyi | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi index def2cd0963..262ded8867 100644 --- a/crates/ty_vendored/ty_extensions/ty_extensions.pyi +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi @@ -1,3 +1,4 @@ +# ruff: noqa: PYI021 import sys from collections.abc import Iterable from enum import Enum @@ -58,12 +59,36 @@ def negated_range_constraint( # # Ideally, these would be annotated using `TypeForm`, but that has not been # standardized yet (https://peps.python.org/pep-0747). -def is_equivalent_to(type_a: Any, type_b: Any) -> ConstraintSet: ... -def is_subtype_of(type_a: Any, type_b: Any) -> ConstraintSet: ... -def is_assignable_to(type_a: Any, type_b: Any) -> ConstraintSet: ... -def is_disjoint_from(type_a: Any, type_b: Any) -> ConstraintSet: ... -def is_singleton(ty: Any) -> bool: ... -def is_single_valued(ty: Any) -> bool: ... +def is_equivalent_to(type_a: Any, type_b: Any) -> ConstraintSet: + """Returns a constraint that is satisfied when `type_a` and `type_b` are + `equivalent`_ types. + + .. _equivalent: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent + """ + +def is_subtype_of(ty: Any, of: Any) -> ConstraintSet: + """Returns a constraint that is satisfied when `ty` is a `subtype`_ of `of`. + + .. _subtype: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence + """ + +def is_assignable_to(ty: Any, to: Any) -> ConstraintSet: + """Returns a constraint that is satisfied when `ty` is `assignable`_ to `to`. + + .. _assignable: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation + """ + +def is_disjoint_from(type_a: Any, type_b: Any) -> ConstraintSet: + """Returns a constraint that is satisfied when `type_a` and `type_b` are disjoint types. + + Two types are disjoint if they have no inhabitants in common. + """ + +def is_singleton(ty: Any) -> bool: + """Returns `True` if `ty` is a singleton type with exactly one inhabitant.""" + +def is_single_valued(ty: Any) -> bool: + """Returns `True` if `ty` is non-empty and all inhabitants compare equal to each other.""" # Returns the generic context of a type as a tuple of typevars, or `None` if the # type is not generic. From e92fd51a2c14d42fa3c6ffc442ec3233f9ed2c19 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 23 Oct 2025 10:05:08 +0200 Subject: [PATCH 019/188] [ty] Add cycle handling to `lazy_default` (#20967) --- .../resources/mdtest/pep695_type_aliases.md | 17 +++++++++++++++++ .../ty_python_semantic/resources/primer/bad.txt | 2 +- crates/ty_python_semantic/src/types.rs | 9 ++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) 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 a0f1c3fd8b..167fe4025d 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -313,6 +313,23 @@ static_assert(is_subtype_of(Bottom[JsonDict], Bottom[JsonDict])) static_assert(is_subtype_of(Bottom[JsonDict], Top[JsonDict])) ``` +### Cyclic defaults + +```py +from typing_extensions import Protocol, TypeVar + +T = TypeVar("T", default="C", covariant=True) + +class P(Protocol[T]): + pass + +class C(P[T]): + pass + +reveal_type(C[int]()) # revealed: C[int] +reveal_type(C()) # revealed: C[Divergent] +``` + ### Union inside generic #### With old-style union diff --git a/crates/ty_python_semantic/resources/primer/bad.txt b/crates/ty_python_semantic/resources/primer/bad.txt index 1a4f3eed3e..45414db491 100644 --- a/crates/ty_python_semantic/resources/primer/bad.txt +++ b/crates/ty_python_semantic/resources/primer/bad.txt @@ -1,2 +1,2 @@ spark # too many iterations (in `exported_names` query), `should not be able to access instance member `spark` of type variable IndexOpsLike@astype in inferable position` -steam.py # dependency graph cycle when querying TypeVarInstance < 'db >::lazy_default_(Id(2e007)), set cycle_fn/cycle_initial to fixpoint iterate. +steam.py # too many iterations diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 1c0355862d..91c51c5e83 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -8344,7 +8344,7 @@ impl<'db> TypeVarInstance<'db> { Some(TypeVarBoundOrConstraints::Constraints(ty)) } - #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(cycle_initial=lazy_default_cycle_initial, heap_size=ruff_memory_usage::heap_size)] fn lazy_default(self, db: &'db dyn Db) -> Option> { let definition = self.definition(db)?; let module = parsed_module(db, definition.file(db)).load(db); @@ -8391,6 +8391,13 @@ fn lazy_bound_or_constraints_cycle_initial<'db>( None } +fn lazy_default_cycle_initial<'db>( + _db: &'db dyn Db, + _self: TypeVarInstance<'db>, +) -> Option> { + None +} + /// Where a type variable is bound and usable. #[derive( Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, salsa::Update, get_size2::GetSize, From 01695513ce33f1f1615309323ba145c42f4720c1 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 23 Oct 2025 11:51:29 +0200 Subject: [PATCH 020/188] Disable npm caching for playground (#21039) --- .github/workflows/publish-playground.yml | 3 +-- .github/workflows/publish-ty-playground.yml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-playground.yml b/.github/workflows/publish-playground.yml index e05691179f..24bf4b4fef 100644 --- a/.github/workflows/publish-playground.yml +++ b/.github/workflows/publish-playground.yml @@ -34,8 +34,7 @@ jobs: - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: 22 - cache: "npm" # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact - cache-dependency-path: playground/package-lock.json + package-manager-cache: false - uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0 - name: "Install Node dependencies" run: npm ci diff --git a/.github/workflows/publish-ty-playground.yml b/.github/workflows/publish-ty-playground.yml index 5945935952..f28086517c 100644 --- a/.github/workflows/publish-ty-playground.yml +++ b/.github/workflows/publish-ty-playground.yml @@ -38,7 +38,7 @@ jobs: - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: 22 - cache: "npm" # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact + package-manager-cache: false - uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0 - name: "Install Node dependencies" run: npm ci From dab3d4e917ae41cce338cc5025e3f39aca38570a Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 23 Oct 2025 14:16:21 +0100 Subject: [PATCH 021/188] [ty] Improve `invalid-argument-type` diagnostics where a union type was provided (#21044) --- .../resources/mdtest/call/function.md | 24 ++++ ...gnostics_for_unio…_(5396a8f9e7f88f71).snap | 135 ++++++++++++++++++ .../ty_python_semantic/src/types/call/bind.rs | 30 ++++ 3 files changed, 189 insertions(+) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_Wrong_argument_type_-_Diagnostics_for_unio…_(5396a8f9e7f88f71).snap diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index eca645c21d..a8526a453b 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -820,6 +820,30 @@ def f(x: int = 1, y: str = "foo") -> int: reveal_type(f(y=2, x="bar")) # revealed: int ``` +### Diagnostics for union types where the union is not assignable + + + +```py +from typing import Sized + +class Foo: ... +class Bar: ... +class Baz: ... + +def f(x: Sized): ... +def g( + a: str | Foo, + b: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo, + c: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo | Bar, + d: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo | Bar | Baz, +): + f(a) # error: [invalid-argument-type] + f(b) # error: [invalid-argument-type] + f(c) # error: [invalid-argument-type] + f(d) # error: [invalid-argument-type] +``` + ## Too many positional arguments ### One too many diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_Wrong_argument_type_-_Diagnostics_for_unio…_(5396a8f9e7f88f71).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_Wrong_argument_type_-_Diagnostics_for_unio…_(5396a8f9e7f88f71).snap new file mode 100644 index 0000000000..83d62e6d23 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_Wrong_argument_type_-_Diagnostics_for_unio…_(5396a8f9e7f88f71).snap @@ -0,0 +1,135 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: function.md - Call expression - Wrong argument type - Diagnostics for union types where the union is not assignable +mdtest path: crates/ty_python_semantic/resources/mdtest/call/function.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Sized + 2 | + 3 | class Foo: ... + 4 | class Bar: ... + 5 | class Baz: ... + 6 | + 7 | def f(x: Sized): ... + 8 | def g( + 9 | a: str | Foo, +10 | b: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo, +11 | c: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo | Bar, +12 | d: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo | Bar | Baz, +13 | ): +14 | f(a) # error: [invalid-argument-type] +15 | f(b) # error: [invalid-argument-type] +16 | f(c) # error: [invalid-argument-type] +17 | f(d) # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `f` is incorrect + --> src/mdtest_snippet.py:14:7 + | +12 | d: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo | Bar | Baz, +13 | ): +14 | f(a) # error: [invalid-argument-type] + | ^ Expected `Sized`, found `str | Foo` +15 | f(b) # error: [invalid-argument-type] +16 | f(c) # error: [invalid-argument-type] + | +info: Element `Foo` of this union is not assignable to `Sized` +info: Function defined here + --> src/mdtest_snippet.py:7:5 + | +5 | class Baz: ... +6 | +7 | def f(x: Sized): ... + | ^ -------- Parameter declared here +8 | def g( +9 | a: str | Foo, + | +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Argument to function `f` is incorrect + --> src/mdtest_snippet.py:15:7 + | +13 | ): +14 | f(a) # error: [invalid-argument-type] +15 | f(b) # error: [invalid-argument-type] + | ^ Expected `Sized`, found `list[str] | str | dict[str, str] | ... omitted 5 union elements` +16 | f(c) # error: [invalid-argument-type] +17 | f(d) # error: [invalid-argument-type] + | +info: Element `Foo` of this union is not assignable to `Sized` +info: Function defined here + --> src/mdtest_snippet.py:7:5 + | +5 | class Baz: ... +6 | +7 | def f(x: Sized): ... + | ^ -------- Parameter declared here +8 | def g( +9 | a: str | Foo, + | +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Argument to function `f` is incorrect + --> src/mdtest_snippet.py:16:7 + | +14 | f(a) # error: [invalid-argument-type] +15 | f(b) # error: [invalid-argument-type] +16 | f(c) # error: [invalid-argument-type] + | ^ Expected `Sized`, found `list[str] | str | dict[str, str] | ... omitted 6 union elements` +17 | f(d) # error: [invalid-argument-type] + | +info: Union elements `Foo` and `Bar` are not assignable to `Sized` +info: Function defined here + --> src/mdtest_snippet.py:7:5 + | +5 | class Baz: ... +6 | +7 | def f(x: Sized): ... + | ^ -------- Parameter declared here +8 | def g( +9 | a: str | Foo, + | +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Argument to function `f` is incorrect + --> src/mdtest_snippet.py:17:7 + | +15 | f(b) # error: [invalid-argument-type] +16 | f(c) # error: [invalid-argument-type] +17 | f(d) # error: [invalid-argument-type] + | ^ Expected `Sized`, found `list[str] | str | dict[str, str] | ... omitted 7 union elements` + | +info: Union element `Foo`, and 2 more union elements, are not assignable to `Sized` +info: Function defined here + --> src/mdtest_snippet.py:7:5 + | +5 | class Baz: ... +6 | +7 | def f(x: Sized): ... + | ^ -------- Parameter declared here +8 | def g( +9 | a: str | Foo, + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index dd1b2215c6..90a03478d5 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -3519,6 +3519,36 @@ impl<'db> BindingError<'db> { "Expected `{expected_ty_display}`, found `{provided_ty_display}`" )); + if let Type::Union(union) = provided_ty { + let union_elements = union.elements(context.db()); + let invalid_elements: Vec> = union + .elements(context.db()) + .iter() + .filter(|element| !element.is_assignable_to(context.db(), *expected_ty)) + .copied() + .collect(); + let first_invalid_element = invalid_elements[0].display(context.db()); + if invalid_elements.len() < union_elements.len() { + match &invalid_elements[1..] { + [] => diag.info(format_args!( + "Element `{first_invalid_element}` of this union \ + is not assignable to `{expected_ty_display}`", + )), + [single] => diag.info(format_args!( + "Union elements `{first_invalid_element}` and `{}` \ + are not assignable to `{expected_ty_display}`", + single.display(context.db()), + )), + rest => diag.info(format_args!( + "Union element `{first_invalid_element}`, \ + and {} more union elements, \ + are not assignable to `{expected_ty_display}`", + rest.len(), + )), + } + } + } + if let Some(matching_overload) = matching_overload { if let Some((name_span, parameter_span)) = matching_overload.get(context.db()).and_then(|overload| { From 4ca74593dd669b75f6d9cae5de1af4c0ee395e2a Mon Sep 17 00:00:00 2001 From: decorator-factory <42166884+decorator-factory@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:09:13 +0300 Subject: [PATCH 022/188] [ty] Consider `type_check_only` when ranking completions (#20910) --- .../completion-evaluation-tasks.csv | 5 ++ .../completion.toml | 2 + .../main.py | 12 +++ .../module.py | 20 +++++ .../pyproject.toml | 5 ++ .../uv.lock | 8 ++ crates/ty_ide/src/completion.rs | 80 ++++++++++++++++++- .../ty_python_semantic/src/semantic_model.rs | 6 ++ crates/ty_python_semantic/src/types.rs | 15 +++- .../ty_python_semantic/src/types/call/bind.rs | 1 + crates/ty_python_semantic/src/types/class.rs | 2 + .../ty_python_semantic/src/types/function.rs | 11 ++- .../src/types/infer/builder.rs | 15 ++++ 13 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/completion.toml create mode 100644 crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/main.py create mode 100644 crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/module.py create mode 100644 crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/pyproject.toml create mode 100644 crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/uv.lock diff --git a/crates/ty_completion_eval/completion-evaluation-tasks.csv b/crates/ty_completion_eval/completion-evaluation-tasks.csv index 00b612e217..cf73a817e1 100644 --- a/crates/ty_completion_eval/completion-evaluation-tasks.csv +++ b/crates/ty_completion_eval/completion-evaluation-tasks.csv @@ -4,6 +4,11 @@ higher-level-symbols-preferred,main.py,0, higher-level-symbols-preferred,main.py,1,1 import-deprioritizes-dunder,main.py,0,1 import-deprioritizes-sunder,main.py,0,1 +import-deprioritizes-type_check_only,main.py,0,1 +import-deprioritizes-type_check_only,main.py,1,1 +import-deprioritizes-type_check_only,main.py,2,1 +import-deprioritizes-type_check_only,main.py,3,2 +import-deprioritizes-type_check_only,main.py,4,3 internal-typeshed-hidden,main.py,0,4 none-completion,main.py,0,11 numpy-array,main.py,0, diff --git a/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/completion.toml b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/completion.toml new file mode 100644 index 0000000000..cbd5805f07 --- /dev/null +++ b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/completion.toml @@ -0,0 +1,2 @@ +[settings] +auto-import = true diff --git a/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/main.py b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/main.py new file mode 100644 index 0000000000..52dd9ee9f8 --- /dev/null +++ b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/main.py @@ -0,0 +1,12 @@ +from module import UniquePrefixA +from module import unique_prefix_ + +from module import Class + +Class.meth_ + +# TODO: bound methods don't preserve type-check-only-ness, this is a bug +Class().meth_ + +# TODO: auto-imports don't take type-check-only-ness into account, this is a bug +UniquePrefixA diff --git a/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/module.py b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/module.py new file mode 100644 index 0000000000..9fd5998768 --- /dev/null +++ b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/module.py @@ -0,0 +1,20 @@ +from typing import type_check_only + + +@type_check_only +class UniquePrefixApple: pass + +class UniquePrefixAzurous: pass + + +@type_check_only +def unique_prefix_apple() -> None: pass + +def unique_prefix_azurous() -> None: pass + + +class Class: + @type_check_only + def meth_apple(self) -> None: pass + + def meth_azurous(self) -> None: pass diff --git a/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/pyproject.toml b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/pyproject.toml new file mode 100644 index 0000000000..cd277d8097 --- /dev/null +++ b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "test" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [] diff --git a/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/uv.lock b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/uv.lock new file mode 100644 index 0000000000..a4937d10d3 --- /dev/null +++ b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "test" +version = "0.1.0" +source = { virtual = "." } diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index feb4e45fc6..dc3da14149 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -65,6 +65,9 @@ pub struct Completion<'db> { /// use it mainly in tests so that we can write less /// noisy tests. pub builtin: bool, + /// Whether this item only exists for type checking purposes and + /// will be missing at runtime + pub is_type_check_only: bool, /// The documentation associated with this item, if /// available. pub documentation: Option, @@ -79,6 +82,7 @@ impl<'db> Completion<'db> { .ty .and_then(|ty| DefinitionsOrTargets::from_ty(db, ty)); let documentation = definition.and_then(|def| def.docstring(db)); + let is_type_check_only = semantic.is_type_check_only(db); Completion { name: semantic.name, insert: None, @@ -87,6 +91,7 @@ impl<'db> Completion<'db> { module_name: None, import: None, builtin: semantic.builtin, + is_type_check_only, documentation, } } @@ -294,6 +299,7 @@ fn add_keyword_value_completions<'db>( kind: None, module_name: None, import: None, + is_type_check_only: false, builtin: true, documentation: None, }); @@ -339,6 +345,8 @@ fn add_unimported_completions<'db>( module_name: Some(symbol.module.name(db)), import: import_action.import().cloned(), builtin: false, + // TODO: `is_type_check_only` requires inferring the type of the symbol + is_type_check_only: false, documentation: None, }); } @@ -837,16 +845,21 @@ fn is_in_string(parsed: &ParsedModuleRef, offset: TextSize) -> bool { }) } -/// Order completions lexicographically, with these exceptions: +/// Order completions according to the following rules: /// -/// 1) A `_[^_]` prefix sorts last and -/// 2) A `__` prefix sorts last except before (1) +/// 1) Names with no underscore prefix +/// 2) Names starting with `_` but not dunders +/// 3) `__dunder__` names +/// +/// Among each category, type-check-only items are sorted last, +/// and otherwise completions are sorted lexicographically. /// /// This has the effect of putting all dunder attributes after "normal" /// attributes, and all single-underscore attributes after dunder attributes. fn compare_suggestions(c1: &Completion, c2: &Completion) -> Ordering { let (kind1, kind2) = (NameKind::classify(&c1.name), NameKind::classify(&c2.name)); - kind1.cmp(&kind2).then_with(|| c1.name.cmp(&c2.name)) + + (kind1, c1.is_type_check_only, &c1.name).cmp(&(kind2, c2.is_type_check_only, &c2.name)) } #[cfg(test)] @@ -3398,6 +3411,65 @@ from os. ); } + #[test] + fn import_type_check_only_lowers_ranking() { + let test = CursorTest::builder() + .source( + "main.py", + r#" + import foo + foo.A + "#, + ) + .source( + "foo/__init__.py", + r#" + from typing import type_check_only + + @type_check_only + class Apple: pass + + class Banana: pass + class Cat: pass + class Azorubine: pass + "#, + ) + .build(); + + let settings = CompletionSettings::default(); + let completions = completion(&test.db, &settings, test.cursor.file, test.cursor.offset); + + let [apple_pos, banana_pos, cat_pos, azo_pos, ann_pos] = + ["Apple", "Banana", "Cat", "Azorubine", "__annotations__"].map(|name| { + completions + .iter() + .position(|comp| comp.name == name) + .unwrap() + }); + + assert!(completions[apple_pos].is_type_check_only); + assert!(apple_pos > banana_pos.max(cat_pos).max(azo_pos)); + assert!(ann_pos > apple_pos); + } + + #[test] + fn type_check_only_is_type_check_only() { + // `@typing.type_check_only` is a function that's unavailable at runtime + // and so should be the last "non-underscore" completion in `typing` + let test = cursor_test("from typing import t"); + + let settings = CompletionSettings::default(); + let completions = completion(&test.db, &settings, test.cursor.file, test.cursor.offset); + let last_nonunderscore = completions + .into_iter() + .filter(|c| !c.name.starts_with('_')) + .next_back() + .unwrap(); + + assert_eq!(&last_nonunderscore.name, "type_check_only"); + assert!(last_nonunderscore.is_type_check_only); + } + #[test] fn regression_test_issue_642() { // Regression test for https://github.com/astral-sh/ty/issues/642 diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index a7db9d5698..6a71f7adfb 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -342,6 +342,12 @@ pub struct Completion<'db> { pub builtin: bool, } +impl<'db> Completion<'db> { + pub fn is_type_check_only(&self, db: &'db dyn Db) -> bool { + self.ty.is_some_and(|ty| ty.is_type_check_only(db)) + } +} + pub trait HasType { /// Returns the inferred type of `self`. /// diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 91c51c5e83..31cb931396 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -53,8 +53,8 @@ pub use crate::types::display::DisplaySettings; use crate::types::display::TupleSpecialization; use crate::types::enums::{enum_metadata, is_single_member_enum}; use crate::types::function::{ - DataclassTransformerFlags, DataclassTransformerParams, FunctionSpans, FunctionType, - KnownFunction, + DataclassTransformerFlags, DataclassTransformerParams, FunctionDecorators, FunctionSpans, + FunctionType, KnownFunction, }; pub(crate) use crate::types::generics::GenericContext; use crate::types::generics::{ @@ -868,6 +868,17 @@ impl<'db> Type<'db> { matches!(self, Type::Dynamic(_)) } + /// Is a value of this type only usable in typing contexts? + pub(crate) fn is_type_check_only(&self, db: &'db dyn Db) -> bool { + match self { + Type::ClassLiteral(class_literal) => class_literal.type_check_only(db), + Type::FunctionLiteral(f) => { + f.has_known_decorator(db, FunctionDecorators::TYPE_CHECK_ONLY) + } + _ => false, + } + } + // If the type is a specialized instance of the given `KnownClass`, returns the specialization. pub(crate) fn known_specialization( &self, diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 90a03478d5..ef1d8574cb 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1001,6 +1001,7 @@ impl<'db> Bindings<'db> { class_literal.body_scope(db), class_literal.known(db), class_literal.deprecated(db), + class_literal.type_check_only(db), Some(params), class_literal.dataclass_transformer_params(db), ))); diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index dd20393d44..ae577549e2 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1336,6 +1336,8 @@ pub struct ClassLiteral<'db> { /// 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>, } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index ce1a1e43d0..2b096c0990 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -121,6 +121,8 @@ bitflags! { const STATICMETHOD = 1 << 5; /// `@typing.override` const OVERRIDE = 1 << 6; + /// `@typing.type_check_only` + const TYPE_CHECK_ONLY = 1 << 7; } } @@ -135,6 +137,7 @@ impl FunctionDecorators { Some(KnownFunction::AbstractMethod) => FunctionDecorators::ABSTRACT_METHOD, Some(KnownFunction::Final) => FunctionDecorators::FINAL, Some(KnownFunction::Override) => FunctionDecorators::OVERRIDE, + Some(KnownFunction::TypeCheckOnly) => FunctionDecorators::TYPE_CHECK_ONLY, _ => FunctionDecorators::empty(), }, Type::ClassLiteral(class) => match class.known(db) { @@ -1256,6 +1259,8 @@ pub enum KnownFunction { DisjointBase, /// [`typing(_extensions).no_type_check`](https://typing.python.org/en/latest/spec/directives.html#no-type-check) NoTypeCheck, + /// `typing(_extensions).type_check_only` + TypeCheckOnly, /// `typing(_extensions).assert_type` AssertType, @@ -1340,7 +1345,7 @@ impl KnownFunction { .then_some(candidate) } - /// Return `true` if `self` is defined in `module` at runtime. + /// Return `true` if `self` is defined in `module` const fn check_module(self, module: KnownModule) -> bool { match self { Self::IsInstance @@ -1394,6 +1399,8 @@ impl KnownFunction { | Self::NegatedRangeConstraint | Self::AllMembers => module.is_ty_extensions(), Self::ImportModule => module.is_importlib(), + + Self::TypeCheckOnly => matches!(module, KnownModule::Typing), } } @@ -1799,6 +1806,8 @@ pub(crate) mod tests { | KnownFunction::DisjointBase | KnownFunction::NoTypeCheck => KnownModule::TypingExtensions, + KnownFunction::TypeCheckOnly => KnownModule::Typing, + KnownFunction::IsSingleton | KnownFunction::IsSubtypeOf | KnownFunction::GenericContext diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 00bf13db47..9eae531590 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2207,6 +2207,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { 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; + } + let body_scope = self .index .node_scope(NodeWithScopeRef::Function(function)) @@ -2649,6 +2654,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } = class_node; let mut deprecated = None; + let mut type_check_only = false; let mut dataclass_params = None; let mut dataclass_transformer_params = None; for decorator in decorator_list { @@ -2673,6 +2679,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } + if decorator_ty + .as_function_literal() + .is_some_and(|function| function.is_known(self.db(), KnownFunction::TypeCheckOnly)) + { + type_check_only = true; + 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 @@ -2721,6 +2735,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { body_scope, maybe_known_class, deprecated, + type_check_only, dataclass_params, dataclass_transformer_params, )), From 48f17718770aa0f503c2d5dcd7c757b6d6a5f9b0 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:14:30 +0900 Subject: [PATCH 023/188] [ty] fix infinite recursion with generic type aliases (#20969) Co-authored-by: Alex Waygood --- .../mdtest/generics/pep695/aliases.md | 61 ++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 64 ++++++++++++------- 2 files changed, 103 insertions(+), 22 deletions(-) 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 4ea9e7adf8..3191cf5683 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -170,3 +170,64 @@ type X[T: X] = T def _(x: X): assert x ``` + +## Recursive generic type aliases + +```py +type RecursiveList[T] = T | list[RecursiveList[T]] + +r1: RecursiveList[int] = 1 +r2: RecursiveList[int] = [1, [1, 2, 3]] +# error: [invalid-assignment] "Object of type `Literal["a"]` is not assignable to `RecursiveList[int]`" +r3: RecursiveList[int] = "a" +# error: [invalid-assignment] +r4: RecursiveList[int] = ["a"] +# TODO: this should be an error +r5: RecursiveList[int] = [1, ["a"]] + +def _(x: RecursiveList[int]): + if isinstance(x, list): + # TODO: should be `list[RecursiveList[int]] + reveal_type(x[0]) # revealed: int | list[Any] + if isinstance(x, list) and isinstance(x[0], list): + # TODO: should be `list[RecursiveList[int]]` + reveal_type(x[0]) # revealed: list[Any] +``` + +Assignment checks respect structural subtyping, i.e. type aliases with the same structure are +assignable to each other. + +```py +# This is structurally equivalent to RecursiveList[T]. +type RecursiveList2[T] = T | list[T | list[RecursiveList[T]]] +# This is not structurally equivalent to RecursiveList[T]. +type RecursiveList3[T] = T | list[list[RecursiveList[T]]] + +def _(x: RecursiveList[int], y: RecursiveList2[int]): + r1: RecursiveList2[int] = x + # error: [invalid-assignment] + r2: RecursiveList3[int] = x + + r3: RecursiveList[int] = y + # error: [invalid-assignment] + r4: RecursiveList3[int] = y +``` + +It is also possible to handle divergent type aliases that are not actually have instances. + +```py +# The type variable `T` has no meaning here, it's just to make sure it works correctly. +type DivergentList[T] = list[DivergentList[T]] + +d1: DivergentList[int] = [] +# error: [invalid-assignment] +d2: DivergentList[int] = [1] +# error: [invalid-assignment] +d3: DivergentList[int] = ["a"] +# TODO: this should be an error +d4: DivergentList[int] = [[1]] + +def _(x: DivergentList[int]): + d1: DivergentList[int] = [x] + d2: DivergentList[int] = x[0] +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 31cb931396..6cc7f20739 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6772,7 +6772,11 @@ impl<'db> Type<'db> { Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping, tcx)), Type::TypeAlias(alias) => { - visitor.visit(self, || alias.value_type(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + // Do not call `value_type` here. `value_type` does the specialization internally, so `apply_type_mapping` is performed without `visitor` inheritance. + // In the case of recursive type aliases, this leads to infinite recursion. + // Instead, call `raw_value_type` and perform the specialization after the `visitor` cache has been created. + let value_type = visitor.visit(self, || alias.raw_value_type(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + alias.apply_function_specialization(db, value_type).apply_type_mapping_impl(db, type_mapping, tcx, visitor) } Type::ModuleLiteral(_) @@ -10716,31 +10720,12 @@ impl<'db> PEP695TypeAliasType<'db> { } /// The RHS type of a PEP-695 style type alias with specialization applied. - #[salsa::tracked(cycle_initial=value_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { - let value_type = self.raw_value_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)); - - value_type.apply_specialization(db, specialization) - } else { - value_type - } + self.apply_function_specialization(db, self.raw_value_type(db)) } /// The RHS type of a PEP-695 style type alias with *no* specialization applied. - /// - /// ## Warning - /// - /// This uses the semantic index to find the definition of the type alias. This means that if the - /// calling query is not in the same file as this type alias is defined in, then this will create - /// a cross-module dependency directly on the full AST which will lead to cache - /// over-invalidation. - /// This method also calls the type inference functions, and since type aliases can have recursive structures, - /// we should be careful not to create infinite recursions in this method (or make it tracked if necessary). + #[salsa::tracked(cycle_initial=value_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) 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); @@ -10750,6 +10735,17 @@ impl<'db> PEP695TypeAliasType<'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, @@ -10939,6 +10935,13 @@ impl<'db> TypeAliasType<'db> { } } + 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, @@ -11799,6 +11802,9 @@ 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(); @@ -11829,5 +11835,19 @@ type BivariantAlias[T] = Bivariant[T] .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 + ); } } From 155fd603e8fda8083155ce859bf59b6fdb5935a5 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:48:41 -0400 Subject: [PATCH 024/188] Document when a rule was added (#21035) Summary -- Inspired by #20859, this PR adds the version a rule was added, and the file and line where it was defined, to `ViolationMetadata`. The file and line just use the standard `file!` and `line!` macros, while the more interesting version field uses a new `violation_metadata` attribute parsed by our `ViolationMetadata` derive macro. I moved the commit modifying all of the rule files to the end, so it should be a lot easier to review by omitting that one. As a curiosity and a bit of a sanity check, I also plotted the rule numbers over time: image I think this looks pretty reasonable and avoids some of the artifacts the earlier versions of the script ran into, such as the `rule` sub-command not being available or `--explain` requiring a file argument.
Script and summary data ```shell gawk --csv ' NR > 1 { split($2, a, ".") major = a[1]; minor = a[2]; micro = a[3] # sum the number of rules added per minor version versions[minor] += 1 } END { tot = 0 for (i = 0; i <= 14; i++) { tot += versions[i] print i, tot } } ' ruff_rules_metadata.csv > summary.dat ``` ``` 0 696 1 768 2 778 3 803 4 822 5 848 6 855 7 865 8 893 9 915 10 916 11 924 12 929 13 932 14 933 ```
Test Plan -- I built and viewed the documentation locally, and it looks pretty good! image The spacing seems a bit awkward following the `h1` at the top, so I'm wondering if this might look nicer as a footer in Ruff. The links work well too: - [v0.0.271](https://github.com/astral-sh/ruff/releases/tag/v0.0.271) - [Related issues](https://github.com/astral-sh/ruff/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20airflow-variable-name-task-id-mismatch) - [View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fruff_linter%2Fsrc%2Frules%2Fairflow%2Frules%2Ftask_variable_name.rs#L34) The last one even works on `main` now since it points to the `derive(ViolationMetadata)` line. In terms of binary size, this branch is a bit bigger than main with 38,654,520 bytes compared to 38,635,728 (+20 KB). I guess that's not _too_ much of an increase, but I wanted to check since we're generating a lot more code with macros. --------- Co-authored-by: GiGaGon <107241144+MeGaGiGaGon@users.noreply.github.com> --- crates/ruff/src/commands/rule.rs | 16 + crates/ruff/tests/integration_test.rs | 6 +- ...tegration_test__rule_f401_output_json.snap | 10 +- crates/ruff_dev/src/generate_docs.rs | 42 + crates/ruff_dev/src/generate_rules_table.rs | 18 +- crates/ruff_linter/src/codes.rs | 1917 +++++++++-------- crates/ruff_linter/src/rule_redirects.rs | 2 +- crates/ruff_linter/src/rule_selector.rs | 8 +- .../airflow/rules/dag_schedule_argument.rs | 1 + .../airflow/rules/moved_to_provider_in_3.rs | 1 + .../src/rules/airflow/rules/removal_in_3.rs | 1 + .../suggested_to_move_to_provider_in_3.rs | 1 + .../airflow/rules/suggested_to_update_3_0.rs | 1 + .../rules/airflow/rules/task_variable_name.rs | 1 + .../eradicate/rules/commented_out_code.rs | 1 + .../rules/fastapi_non_annotated_dependency.rs | 1 + .../rules/fastapi_redundant_response_model.rs | 1 + .../rules/fastapi_unused_path_parameter.rs | 1 + .../src/rules/flake8_2020/rules/compare.rs | 5 + .../flake8_2020/rules/name_or_attribute.rs | 1 + .../src/rules/flake8_2020/rules/subscript.rs | 4 + .../flake8_annotations/rules/definition.rs | 11 + .../flake8_async/rules/async_busy_wait.rs | 1 + .../rules/async_function_with_timeout.rs | 1 + .../flake8_async/rules/async_zero_sleep.rs | 1 + .../flake8_async/rules/blocking_http_call.rs | 1 + .../rules/blocking_http_call_httpx.rs | 1 + .../flake8_async/rules/blocking_input.rs | 1 + .../flake8_async/rules/blocking_open_call.rs | 1 + .../rules/blocking_path_methods.rs | 1 + .../rules/blocking_process_invocation.rs | 3 + .../flake8_async/rules/blocking_sleep.rs | 1 + .../rules/cancel_scope_no_checkpoint.rs | 1 + .../rules/long_sleep_not_forever.rs | 1 + .../src/rules/flake8_async/rules/sync_call.rs | 1 + .../rules/flake8_bandit/rules/assert_used.rs | 1 + .../rules/bad_file_permissions.rs | 1 + .../rules/flake8_bandit/rules/django_extra.rs | 1 + .../flake8_bandit/rules/django_raw_sql.rs | 1 + .../rules/flake8_bandit/rules/exec_used.rs | 1 + .../flake8_bandit/rules/flask_debug_true.rs | 1 + .../rules/hardcoded_bind_all_interfaces.rs | 1 + .../rules/hardcoded_password_default.rs | 1 + .../rules/hardcoded_password_func_arg.rs | 1 + .../rules/hardcoded_password_string.rs | 1 + .../rules/hardcoded_sql_expression.rs | 1 + .../rules/hardcoded_tmp_directory.rs | 1 + .../rules/hashlib_insecure_hash_functions.rs | 1 + .../rules/jinja2_autoescape_false.rs | 1 + .../rules/logging_config_insecure_listen.rs | 1 + .../flake8_bandit/rules/mako_templates.rs | 1 + .../flake8_bandit/rules/paramiko_calls.rs | 1 + .../rules/request_with_no_cert_validation.rs | 1 + .../rules/request_without_timeout.rs | 1 + .../flake8_bandit/rules/shell_injection.rs | 7 + .../rules/snmp_insecure_version.rs | 1 + .../rules/snmp_weak_cryptography.rs | 1 + .../rules/ssh_no_host_key_verification.rs | 1 + .../rules/ssl_insecure_version.rs | 1 + .../rules/ssl_with_bad_defaults.rs | 1 + .../rules/ssl_with_no_version.rs | 1 + .../rules/suspicious_function_call.rs | 21 + .../flake8_bandit/rules/suspicious_imports.rs | 14 + .../rules/tarfile_unsafe_members.rs | 1 + .../rules/try_except_continue.rs | 1 + .../flake8_bandit/rules/try_except_pass.rs | 1 + .../flake8_bandit/rules/unsafe_markup_use.rs | 1 + .../flake8_bandit/rules/unsafe_yaml_load.rs | 1 + .../rules/weak_cryptographic_key.rs | 1 + .../flake8_blind_except/rules/blind_except.rs | 1 + ...olean_default_value_positional_argument.rs | 1 + .../rules/boolean_positional_value_in_call.rs | 1 + .../boolean_type_hint_positional_argument.rs | 1 + .../rules/abstract_base_class.rs | 2 + .../flake8_bugbear/rules/assert_false.rs | 1 + .../rules/assert_raises_exception.rs | 1 + .../rules/assignment_to_os_environ.rs | 1 + .../rules/batched_without_explicit_strict.rs | 1 + .../rules/cached_instance_method.rs | 1 + .../rules/class_as_data_structure.rs | 1 + .../rules/duplicate_exceptions.rs | 2 + .../flake8_bugbear/rules/duplicate_value.rs | 1 + .../rules/except_with_empty_tuple.rs | 1 + .../except_with_non_exception_classes.rs | 1 + .../rules/f_string_docstring.rs | 1 + .../function_call_in_argument_default.rs | 1 + .../rules/function_uses_loop_variable.rs | 1 + .../rules/getattr_with_constant.rs | 1 + .../rules/jump_statement_in_finally.rs | 1 + .../rules/loop_iterator_mutation.rs | 1 + .../rules/loop_variable_overrides_iterator.rs | 1 + .../rules/map_without_explicit_strict.rs | 1 + .../rules/mutable_argument_default.rs | 1 + .../rules/mutable_contextvar_default.rs | 1 + .../rules/no_explicit_stacklevel.rs | 1 + .../flake8_bugbear/rules/raise_literal.rs | 1 + .../rules/raise_without_from_inside_except.rs | 1 + .../rules/re_sub_positional_args.rs | 1 + .../redundant_tuple_in_exception_handler.rs | 1 + .../rules/return_in_generator.rs | 1 + .../rules/reuse_of_groupby_generator.rs | 1 + .../rules/setattr_with_constant.rs | 1 + .../star_arg_unpacking_after_keyword_arg.rs | 1 + .../rules/static_key_dict_comprehension.rs | 1 + .../rules/strip_with_multi_characters.rs | 1 + .../rules/unary_prefix_increment_decrement.rs | 1 + .../rules/unintentional_type_annotation.rs | 1 + .../rules/unreliable_callable_check.rs | 1 + .../rules/unused_loop_control_variable.rs | 1 + .../rules/useless_comparison.rs | 1 + .../rules/useless_contextlib_suppress.rs | 1 + .../rules/useless_expression.rs | 1 + .../rules/zip_without_explicit_strict.rs | 1 + .../rules/builtin_argument_shadowing.rs | 1 + .../rules/builtin_attribute_shadowing.rs | 1 + .../rules/builtin_import_shadowing.rs | 1 + .../builtin_lambda_argument_shadowing.rs | 1 + .../rules/builtin_variable_shadowing.rs | 1 + .../rules/stdlib_module_shadowing.rs | 1 + .../flake8_commas/rules/trailing_commas.rs | 3 + .../rules/unnecessary_call_around_sorted.rs | 1 + .../rules/unnecessary_collection_call.rs | 1 + .../rules/unnecessary_comprehension.rs | 1 + .../unnecessary_comprehension_in_call.rs | 1 + ...cessary_dict_comprehension_for_iterable.rs | 1 + .../unnecessary_double_cast_or_process.rs | 1 + .../rules/unnecessary_generator_dict.rs | 1 + .../rules/unnecessary_generator_list.rs | 1 + .../rules/unnecessary_generator_set.rs | 1 + .../rules/unnecessary_list_call.rs | 1 + .../unnecessary_list_comprehension_dict.rs | 1 + .../unnecessary_list_comprehension_set.rs | 1 + .../rules/unnecessary_literal_dict.rs | 1 + .../rules/unnecessary_literal_set.rs | 1 + .../unnecessary_literal_within_dict_call.rs | 1 + .../unnecessary_literal_within_list_call.rs | 1 + .../unnecessary_literal_within_tuple_call.rs | 1 + .../rules/unnecessary_map.rs | 1 + .../rules/unnecessary_subscript_reversal.rs | 1 + .../rules/missing_copyright_notice.rs | 1 + .../rules/call_date_fromtimestamp.rs | 1 + .../flake8_datetimez/rules/call_date_today.rs | 1 + .../rules/call_datetime_fromtimestamp.rs | 1 + .../rules/call_datetime_now_without_tzinfo.rs | 1 + .../call_datetime_strptime_without_zone.rs | 1 + .../rules/call_datetime_today.rs | 1 + .../rules/call_datetime_utcfromtimestamp.rs | 1 + .../rules/call_datetime_utcnow.rs | 1 + .../rules/call_datetime_without_tzinfo.rs | 1 + .../rules/datetime_min_max.rs | 1 + .../rules/flake8_debugger/rules/debugger.rs | 1 + .../rules/all_with_model_form.rs | 1 + .../rules/exclude_with_model_form.rs | 1 + .../rules/locals_in_render_function.rs | 1 + .../rules/model_without_dunder_str.rs | 1 + .../rules/non_leading_receiver_decorator.rs | 1 + .../rules/nullable_model_string_field.rs | 1 + .../rules/unordered_body_content_in_model.rs | 1 + .../rules/string_in_exception.rs | 3 + .../rules/shebang_leading_whitespace.rs | 1 + .../rules/shebang_missing_executable_file.rs | 1 + .../rules/shebang_missing_python.rs | 1 + .../rules/shebang_not_executable.rs | 1 + .../rules/shebang_not_first_line.rs | 1 + .../src/rules/flake8_fixme/rules/todos.rs | 4 + .../rules/future_required_type_annotation.rs | 1 + .../future_rewritable_type_annotation.rs | 1 + .../rules/f_string_in_gettext_func_call.rs | 1 + .../rules/format_in_gettext_func_call.rs | 1 + .../rules/printf_in_gettext_func_call.rs | 1 + .../rules/explicit.rs | 1 + .../rules/implicit.rs | 2 + .../rules/banned_import_alias.rs | 1 + .../rules/banned_import_from.rs | 1 + .../rules/unconventional_import_alias.rs | 1 + .../rules/direct_logger_instantiation.rs | 1 + .../rules/exc_info_outside_except_handler.rs | 1 + .../rules/exception_without_exc_info.rs | 1 + .../rules/invalid_get_logger_argument.rs | 1 + .../log_exception_outside_except_handler.rs | 1 + .../flake8_logging/rules/root_logger_call.rs | 1 + .../flake8_logging/rules/undocumented_warn.rs | 1 + .../rules/flake8_logging_format/violations.rs | 8 + .../rules/implicit_namespace_package.rs | 1 + .../rules/duplicate_class_field_definition.rs | 1 + .../rules/multiple_starts_ends_with.rs | 1 + .../flake8_pie/rules/non_unique_enums.rs | 1 + .../rules/reimplemented_container_builtin.rs | 1 + .../rules/unnecessary_dict_kwargs.rs | 1 + .../rules/unnecessary_placeholder.rs | 1 + .../rules/unnecessary_range_start.rs | 1 + .../flake8_pie/rules/unnecessary_spread.rs | 1 + .../rules/flake8_print/rules/print_call.rs | 2 + .../flake8_pyi/rules/any_eq_ne_annotation.rs | 1 + .../rules/bad_generator_return_type.rs | 1 + .../rules/bad_version_info_comparison.rs | 2 + .../flake8_pyi/rules/bytestring_usage.rs | 1 + .../rules/collections_named_tuple.rs | 1 + .../rules/complex_assignment_in_stub.rs | 1 + .../rules/complex_if_statement_in_stub.rs | 1 + .../rules/custom_type_var_for_self.rs | 1 + .../flake8_pyi/rules/docstring_in_stubs.rs | 1 + .../rules/duplicate_literal_member.rs | 1 + .../rules/duplicate_union_member.rs | 1 + .../rules/ellipsis_in_non_empty_class_body.rs | 1 + .../flake8_pyi/rules/exit_annotations.rs | 1 + .../rules/future_annotations_in_stub.rs | 1 + .../rules/generic_not_last_base_class.rs | 1 + .../rules/iter_method_return_iterable.rs | 1 + .../rules/no_return_argument_annotation.rs | 1 + .../flake8_pyi/rules/non_empty_stub_body.rs | 1 + .../flake8_pyi/rules/non_self_return_type.rs | 1 + .../rules/numeric_literal_too_long.rs | 1 + .../flake8_pyi/rules/pass_in_class_body.rs | 1 + .../rules/pass_statement_stub_body.rs | 1 + .../rules/pre_pep570_positional_argument.rs | 1 + .../flake8_pyi/rules/prefix_type_params.rs | 1 + .../rules/quoted_annotation_in_stub.rs | 1 + .../rules/redundant_final_literal.rs | 1 + .../rules/redundant_literal_union.rs | 1 + .../rules/redundant_none_literal.rs | 1 + .../rules/redundant_numeric_union.rs | 1 + .../rules/flake8_pyi/rules/simple_defaults.rs | 6 + .../rules/str_or_repr_defined_in_stub.rs | 1 + .../rules/string_or_bytes_too_long.rs | 1 + .../rules/stub_body_multiple_statements.rs | 1 + .../flake8_pyi/rules/type_alias_naming.rs | 2 + .../flake8_pyi/rules/type_comment_in_stub.rs | 1 + .../unaliased_collections_abc_set_import.rs | 1 + .../rules/unnecessary_literal_union.rs | 1 + .../rules/unnecessary_type_union.rs | 1 + .../flake8_pyi/rules/unrecognized_platform.rs | 2 + .../rules/unrecognized_version_info.rs | 3 + .../rules/unsupported_method_call_on_all.rs | 1 + .../rules/unused_private_type_definition.rs | 4 + .../flake8_pytest_style/rules/assertion.rs | 5 + .../rules/flake8_pytest_style/rules/fail.rs | 1 + .../flake8_pytest_style/rules/fixture.rs | 11 + .../flake8_pytest_style/rules/imports.rs | 1 + .../rules/flake8_pytest_style/rules/marks.rs | 2 + .../flake8_pytest_style/rules/parametrize.rs | 3 + .../rules/flake8_pytest_style/rules/patch.rs | 1 + .../rules/flake8_pytest_style/rules/raises.rs | 3 + .../rules/test_functions.rs | 1 + .../rules/flake8_pytest_style/rules/warns.rs | 3 + .../rules/avoidable_escaped_quote.rs | 1 + .../rules/check_string_quotes.rs | 3 + .../rules/unnecessary_escaped_quote.rs | 1 + .../unnecessary_paren_on_raise_exception.rs | 1 + .../src/rules/flake8_return/rules/function.rs | 8 + .../rules/private_member_access.rs | 1 + .../flake8_simplify/rules/ast_bool_op.rs | 6 + .../rules/flake8_simplify/rules/ast_expr.rs | 2 + .../rules/flake8_simplify/rules/ast_ifexp.rs | 3 + .../flake8_simplify/rules/ast_unary_op.rs | 3 + .../rules/flake8_simplify/rules/ast_with.rs | 1 + .../flake8_simplify/rules/collapsible_if.rs | 1 + .../rules/enumerate_for_loop.rs | 1 + .../if_else_block_instead_of_dict_get.rs | 1 + .../if_else_block_instead_of_dict_lookup.rs | 1 + .../rules/if_else_block_instead_of_if_exp.rs | 1 + .../rules/if_with_same_arms.rs | 1 + .../flake8_simplify/rules/key_in_dict.rs | 1 + .../flake8_simplify/rules/needless_bool.rs | 1 + .../rules/open_file_with_context_handler.rs | 1 + .../rules/reimplemented_builtin.rs | 1 + .../rules/return_in_try_except_finally.rs | 1 + .../rules/split_static_string.rs | 1 + .../rules/suppressible_exception.rs | 1 + .../flake8_simplify/rules/yoda_conditions.rs | 1 + .../rules/zip_dict_keys_and_values.rs | 1 + .../rules/no_slots_in_namedtuple_subclass.rs | 1 + .../rules/no_slots_in_str_subclass.rs | 1 + .../rules/no_slots_in_tuple_subclass.rs | 1 + .../flake8_tidy_imports/rules/banned_api.rs | 1 + .../rules/banned_module_level_imports.rs | 1 + .../rules/relative_imports.rs | 1 + .../src/rules/flake8_todos/rules/todos.rs | 7 + .../rules/empty_type_checking_block.rs | 1 + .../rules/runtime_cast_value.rs | 1 + .../runtime_import_in_type_checking_block.rs | 1 + .../rules/runtime_string_union.rs | 1 + .../rules/type_alias_quotes.rs | 2 + .../rules/typing_only_runtime_import.rs | 3 + .../rules/unused_arguments.rs | 5 + .../flake8_use_pathlib/rules/builtin_open.rs | 1 + .../flake8_use_pathlib/rules/glob_rule.rs | 1 + .../rules/invalid_pathlib_with_suffix.rs | 1 + .../flake8_use_pathlib/rules/os_chmod.rs | 1 + .../flake8_use_pathlib/rules/os_getcwd.rs | 1 + .../flake8_use_pathlib/rules/os_makedirs.rs | 1 + .../flake8_use_pathlib/rules/os_mkdir.rs | 1 + .../rules/os_path_abspath.rs | 1 + .../rules/os_path_basename.rs | 1 + .../rules/os_path_dirname.rs | 1 + .../rules/os_path_exists.rs | 1 + .../rules/os_path_expanduser.rs | 1 + .../rules/os_path_getatime.rs | 1 + .../rules/os_path_getctime.rs | 1 + .../rules/os_path_getmtime.rs | 1 + .../rules/os_path_getsize.rs | 1 + .../flake8_use_pathlib/rules/os_path_isabs.rs | 1 + .../flake8_use_pathlib/rules/os_path_isdir.rs | 1 + .../rules/os_path_isfile.rs | 1 + .../rules/os_path_islink.rs | 1 + .../rules/os_path_samefile.rs | 1 + .../flake8_use_pathlib/rules/os_readlink.rs | 1 + .../flake8_use_pathlib/rules/os_remove.rs | 1 + .../flake8_use_pathlib/rules/os_rename.rs | 1 + .../flake8_use_pathlib/rules/os_replace.rs | 1 + .../flake8_use_pathlib/rules/os_rmdir.rs | 1 + .../flake8_use_pathlib/rules/os_sep_split.rs | 1 + .../flake8_use_pathlib/rules/os_symlink.rs | 1 + .../flake8_use_pathlib/rules/os_unlink.rs | 1 + .../path_constructor_current_directory.rs | 1 + .../rules/flake8_use_pathlib/violations.rs | 5 + .../flynt/rules/static_join_to_fstring.rs | 1 + .../rules/isort/rules/add_required_imports.rs | 1 + .../src/rules/isort/rules/organize_imports.rs | 1 + .../mccabe/rules/function_is_too_complex.rs | 1 + .../rules/numpy/rules/deprecated_function.rs | 1 + .../numpy/rules/deprecated_type_alias.rs | 1 + .../src/rules/numpy/rules/legacy_random.rs | 1 + .../numpy/rules/numpy_2_0_deprecation.rs | 1 + .../pandas_vet/rules/assignment_to_df.rs | 1 + .../src/rules/pandas_vet/rules/attr.rs | 1 + .../src/rules/pandas_vet/rules/call.rs | 4 + .../pandas_vet/rules/inplace_argument.rs | 1 + .../rules/nunique_constant_series_check.rs | 1 + .../src/rules/pandas_vet/rules/pd_merge.rs | 1 + .../src/rules/pandas_vet/rules/read_table.rs | 1 + .../src/rules/pandas_vet/rules/subscript.rs | 3 + .../rules/camelcase_imported_as_acronym.rs | 1 + .../rules/camelcase_imported_as_constant.rs | 1 + .../rules/camelcase_imported_as_lowercase.rs | 1 + .../constant_imported_as_non_constant.rs | 1 + .../pep8_naming/rules/dunder_function_name.rs | 1 + .../rules/error_suffix_on_exception_name.rs | 1 + .../rules/invalid_argument_name.rs | 1 + .../pep8_naming/rules/invalid_class_name.rs | 1 + .../rules/invalid_first_argument_name.rs | 2 + .../rules/invalid_function_name.rs | 1 + .../pep8_naming/rules/invalid_module_name.rs | 1 + .../lowercase_imported_as_non_lowercase.rs | 1 + .../mixed_case_variable_in_class_scope.rs | 1 + .../mixed_case_variable_in_global_scope.rs | 1 + .../non_lowercase_variable_in_function.rs | 1 + .../perflint/rules/incorrect_dict_iterator.rs | 1 + .../rules/manual_dict_comprehension.rs | 1 + .../rules/manual_list_comprehension.rs | 1 + .../rules/perflint/rules/manual_list_copy.rs | 1 + .../perflint/rules/try_except_in_loop.rs | 1 + .../perflint/rules/unnecessary_list_cast.rs | 1 + .../pycodestyle/rules/ambiguous_class_name.rs | 1 + .../rules/ambiguous_function_name.rs | 1 + .../rules/ambiguous_variable_name.rs | 1 + .../rules/pycodestyle/rules/bare_except.rs | 1 + .../rules/pycodestyle/rules/blank_lines.rs | 6 + .../pycodestyle/rules/compound_statements.rs | 3 + .../pycodestyle/rules/doc_line_too_long.rs | 1 + .../src/rules/pycodestyle/rules/errors.rs | 2 + .../rules/invalid_escape_sequence.rs | 1 + .../pycodestyle/rules/lambda_assignment.rs | 1 + .../rules/pycodestyle/rules/line_too_long.rs | 1 + .../pycodestyle/rules/literal_comparisons.rs | 2 + .../logical_lines/extraneous_whitespace.rs | 3 + .../rules/logical_lines/indentation.rs | 7 + .../rules/logical_lines/missing_whitespace.rs | 1 + .../missing_whitespace_after_keyword.rs | 1 + .../missing_whitespace_around_operator.rs | 4 + .../logical_lines/redundant_backslash.rs | 1 + .../logical_lines/space_around_operator.rs | 6 + .../whitespace_around_keywords.rs | 4 + ...hitespace_around_named_parameter_equals.rs | 2 + .../whitespace_before_comment.rs | 4 + .../whitespace_before_parameters.rs | 1 + .../rules/missing_newline_at_end_of_file.rs | 1 + .../rules/mixed_spaces_and_tabs.rs | 1 + .../rules/module_import_not_at_top_of_file.rs | 1 + .../rules/multiple_imports_on_one_line.rs | 1 + .../src/rules/pycodestyle/rules/not_tests.rs | 2 + .../pycodestyle/rules/tab_indentation.rs | 1 + .../rules/too_many_newlines_at_end_of_file.rs | 1 + .../pycodestyle/rules/trailing_whitespace.rs | 2 + .../pycodestyle/rules/type_comparison.rs | 1 + .../rules/whitespace_after_decorator.rs | 1 + .../rules/pydoclint/rules/check_docstring.rs | 7 + .../src/rules/pydocstyle/rules/backslashes.rs | 1 + .../pydocstyle/rules/blank_after_summary.rs | 1 + .../rules/blank_before_after_class.rs | 3 + .../rules/blank_before_after_function.rs | 2 + .../src/rules/pydocstyle/rules/capitalized.rs | 1 + .../pydocstyle/rules/ends_with_period.rs | 1 + .../pydocstyle/rules/ends_with_punctuation.rs | 1 + .../src/rules/pydocstyle/rules/if_needed.rs | 1 + .../src/rules/pydocstyle/rules/indent.rs | 3 + .../rules/multi_line_summary_start.rs | 2 + .../rules/newline_after_last_paragraph.rs | 1 + .../rules/pydocstyle/rules/no_signature.rs | 1 + .../rules/no_surrounding_whitespace.rs | 1 + .../pydocstyle/rules/non_imperative_mood.rs | 1 + .../src/rules/pydocstyle/rules/not_empty.rs | 1 + .../src/rules/pydocstyle/rules/not_missing.rs | 8 + .../src/rules/pydocstyle/rules/one_liner.rs | 1 + .../src/rules/pydocstyle/rules/sections.rs | 14 + .../pydocstyle/rules/starts_with_this.rs | 1 + .../rules/pydocstyle/rules/triple_quotes.rs | 1 + .../src/rules/pyflakes/rules/assert_tuple.rs | 1 + .../pyflakes/rules/break_outside_loop.rs | 1 + .../pyflakes/rules/continue_outside_loop.rs | 1 + .../pyflakes/rules/default_except_not_last.rs | 1 + .../rules/f_string_missing_placeholders.rs | 1 + .../rules/forward_annotation_syntax_error.rs | 1 + .../rules/future_feature_not_defined.rs | 1 + .../src/rules/pyflakes/rules/if_tuple.rs | 1 + .../src/rules/pyflakes/rules/imports.rs | 5 + .../rules/invalid_literal_comparisons.rs | 1 + .../pyflakes/rules/invalid_print_syntax.rs | 1 + .../pyflakes/rules/raise_not_implemented.rs | 1 + .../pyflakes/rules/redefined_while_unused.rs | 1 + .../src/rules/pyflakes/rules/repeated_keys.rs | 2 + .../pyflakes/rules/return_outside_function.rs | 1 + .../pyflakes/rules/starred_expressions.rs | 2 + .../src/rules/pyflakes/rules/strings.rs | 14 + .../rules/pyflakes/rules/undefined_export.rs | 1 + .../rules/pyflakes/rules/undefined_local.rs | 1 + .../rules/pyflakes/rules/undefined_name.rs | 1 + .../rules/pyflakes/rules/unused_annotation.rs | 1 + .../src/rules/pyflakes/rules/unused_import.rs | 1 + .../rules/pyflakes/rules/unused_variable.rs | 1 + .../pyflakes/rules/yield_outside_function.rs | 1 + .../rules/pygrep_hooks/rules/blanket_noqa.rs | 1 + .../pygrep_hooks/rules/blanket_type_ignore.rs | 1 + .../pygrep_hooks/rules/deprecated_log_warn.rs | 1 + .../pygrep_hooks/rules/invalid_mock_access.rs | 1 + .../src/rules/pygrep_hooks/rules/no_eval.rs | 1 + .../src/rules/pylint/rules/and_or_ternary.rs | 1 + .../pylint/rules/assert_on_string_literal.rs | 1 + .../rules/pylint/rules/await_outside_async.rs | 1 + .../pylint/rules/bad_dunder_method_name.rs | 1 + .../src/rules/pylint/rules/bad_open_mode.rs | 1 + .../pylint/rules/bad_staticmethod_argument.rs | 1 + .../rules/pylint/rules/bad_str_strip_call.rs | 1 + .../rules/bad_string_format_character.rs | 1 + .../pylint/rules/bad_string_format_type.rs | 1 + .../pylint/rules/bidirectional_unicode.rs | 1 + .../rules/pylint/rules/binary_op_exception.rs | 1 + .../rules/boolean_chained_comparison.rs | 1 + .../rules/pylint/rules/collapsible_else_if.rs | 1 + .../pylint/rules/compare_to_empty_string.rs | 1 + .../pylint/rules/comparison_of_constant.rs | 1 + .../pylint/rules/comparison_with_itself.rs | 1 + .../rules/pylint/rules/continue_in_finally.rs | 1 + .../pylint/rules/dict_index_missing_items.rs | 1 + .../pylint/rules/dict_iter_missing_items.rs | 1 + .../src/rules/pylint/rules/duplicate_bases.rs | 1 + .../src/rules/pylint/rules/empty_comment.rs | 1 + .../src/rules/pylint/rules/eq_without_hash.rs | 1 + .../pylint/rules/global_at_module_level.rs | 1 + .../rules/pylint/rules/global_statement.rs | 1 + .../rules/global_variable_not_assigned.rs | 1 + .../src/rules/pylint/rules/if_stmt_min_max.rs | 1 + .../pylint/rules/import_outside_top_level.rs | 1 + .../rules/pylint/rules/import_private_name.rs | 1 + .../src/rules/pylint/rules/import_self.rs | 1 + .../rules/pylint/rules/invalid_all_format.rs | 1 + .../rules/pylint/rules/invalid_all_object.rs | 1 + .../rules/pylint/rules/invalid_bool_return.rs | 1 + .../pylint/rules/invalid_bytes_return.rs | 1 + .../pylint/rules/invalid_envvar_default.rs | 1 + .../pylint/rules/invalid_envvar_value.rs | 1 + .../rules/pylint/rules/invalid_hash_return.rs | 1 + .../pylint/rules/invalid_index_return.rs | 1 + .../pylint/rules/invalid_length_return.rs | 1 + .../rules/pylint/rules/invalid_str_return.rs | 1 + .../pylint/rules/invalid_string_characters.rs | 5 + .../rules/pylint/rules/iteration_over_set.rs | 1 + .../src/rules/pylint/rules/len_test.rs | 1 + .../rules/pylint/rules/literal_membership.rs | 1 + .../rules/load_before_global_declaration.rs | 1 + .../src/rules/pylint/rules/logging.rs | 2 + .../pylint/rules/magic_value_comparison.rs | 1 + .../rules/pylint/rules/manual_import_from.rs | 1 + .../pylint/rules/misplaced_bare_raise.rs | 1 + .../pylint/rules/missing_maxsplit_arg.rs | 1 + .../pylint/rules/modified_iterating_set.rs | 1 + .../rules/named_expr_without_context.rs | 1 + .../src/rules/pylint/rules/nan_comparison.rs | 1 + .../src/rules/pylint/rules/nested_min_max.rs | 1 + .../rules/pylint/rules/no_method_decorator.rs | 2 + .../src/rules/pylint/rules/no_self_use.rs | 1 + .../pylint/rules/non_ascii_module_import.rs | 1 + .../src/rules/pylint/rules/non_ascii_name.rs | 1 + .../pylint/rules/non_augmented_assignment.rs | 1 + .../rules/pylint/rules/non_slot_assignment.rs | 1 + .../rules/pylint/rules/nonlocal_and_global.rs | 1 + .../pylint/rules/nonlocal_without_binding.rs | 1 + .../pylint/rules/potential_index_error.rs | 1 + .../pylint/rules/property_with_parameters.rs | 1 + .../pylint/rules/redeclared_assigned_name.rs | 1 + .../rules/redefined_argument_from_local.rs | 1 + .../rules/pylint/rules/redefined_loop_name.rs | 1 + .../rules/redefined_slots_in_subclass.rs | 1 + .../rules/repeated_equality_comparison.rs | 1 + .../pylint/rules/repeated_isinstance_calls.rs | 1 + .../pylint/rules/repeated_keyword_argument.rs | 1 + .../src/rules/pylint/rules/return_in_init.rs | 1 + .../pylint/rules/self_assigning_variable.rs | 1 + .../pylint/rules/self_or_cls_assignment.rs | 1 + .../pylint/rules/shallow_copy_environ.rs | 1 + .../rules/pylint/rules/single_string_slots.rs | 1 + .../pylint/rules/singledispatch_method.rs | 1 + .../rules/singledispatchmethod_function.rs | 1 + .../rules/subprocess_popen_preexec_fn.rs | 1 + .../rules/subprocess_run_without_check.rs | 1 + .../pylint/rules/super_without_brackets.rs | 1 + .../src/rules/pylint/rules/sys_exit_alias.rs | 1 + .../rules/pylint/rules/too_many_arguments.rs | 1 + .../rules/too_many_boolean_expressions.rs | 1 + .../rules/pylint/rules/too_many_branches.rs | 1 + .../src/rules/pylint/rules/too_many_locals.rs | 1 + .../pylint/rules/too_many_nested_blocks.rs | 1 + .../rules/too_many_positional_arguments.rs | 1 + .../pylint/rules/too_many_public_methods.rs | 1 + .../rules/too_many_return_statements.rs | 1 + .../rules/pylint/rules/too_many_statements.rs | 1 + .../src/rules/pylint/rules/type_bivariance.rs | 1 + .../rules/type_name_incorrect_variance.rs | 1 + .../pylint/rules/type_param_name_mismatch.rs | 1 + .../unexpected_special_method_signature.rs | 1 + .../rules/unnecessary_dict_index_lookup.rs | 1 + .../rules/unnecessary_direct_lambda_call.rs | 1 + .../pylint/rules/unnecessary_dunder_call.rs | 1 + .../rules/pylint/rules/unnecessary_lambda.rs | 1 + .../rules/unnecessary_list_index_lookup.rs | 1 + .../src/rules/pylint/rules/unreachable.rs | 1 + .../pylint/rules/unspecified_encoding.rs | 1 + .../pylint/rules/useless_else_on_loop.rs | 1 + .../rules/useless_exception_statement.rs | 1 + .../pylint/rules/useless_import_alias.rs | 1 + .../src/rules/pylint/rules/useless_return.rs | 1 + .../rules/pylint/rules/useless_with_lock.rs | 1 + .../rules/yield_from_in_async_function.rs | 1 + .../src/rules/pylint/rules/yield_in_init.rs | 1 + ...convert_named_tuple_functional_to_class.rs | 1 + .../convert_typed_dict_functional_to_class.rs | 1 + .../pyupgrade/rules/datetime_utc_alias.rs | 1 + .../rules/deprecated_c_element_tree.rs | 1 + .../pyupgrade/rules/deprecated_import.rs | 1 + .../pyupgrade/rules/deprecated_mock_import.rs | 1 + .../rules/deprecated_unittest_alias.rs | 1 + .../pyupgrade/rules/extraneous_parentheses.rs | 1 + .../src/rules/pyupgrade/rules/f_strings.rs | 1 + .../rules/pyupgrade/rules/format_literals.rs | 1 + .../rules/lru_cache_with_maxsize_none.rs | 1 + .../rules/lru_cache_without_parameters.rs | 1 + .../rules/pyupgrade/rules/native_literals.rs | 1 + .../pyupgrade/rules/non_pep646_unpack.rs | 1 + .../src/rules/pyupgrade/rules/open_alias.rs | 1 + .../rules/pyupgrade/rules/os_error_alias.rs | 1 + .../pyupgrade/rules/outdated_version_block.rs | 1 + .../rules/pep695/non_pep695_generic_class.rs | 1 + .../pep695/non_pep695_generic_function.rs | 1 + .../rules/pep695/non_pep695_type_alias.rs | 1 + .../rules/pep695/private_type_parameter.rs | 1 + .../rules/printf_string_formatting.rs | 1 + .../pyupgrade/rules/quoted_annotation.rs | 1 + .../pyupgrade/rules/redundant_open_modes.rs | 1 + .../pyupgrade/rules/replace_stdout_stderr.rs | 1 + .../rules/pyupgrade/rules/replace_str_enum.rs | 1 + .../rules/replace_universal_newlines.rs | 1 + .../rules/super_call_with_parameters.rs | 1 + .../pyupgrade/rules/timeout_error_alias.rs | 1 + .../pyupgrade/rules/type_of_primitive.rs | 1 + .../pyupgrade/rules/typing_text_str_alias.rs | 1 + .../pyupgrade/rules/unicode_kind_prefix.rs | 1 + .../rules/unnecessary_builtin_import.rs | 1 + .../rules/unnecessary_class_parentheses.rs | 1 + .../rules/unnecessary_coding_comment.rs | 1 + .../rules/unnecessary_default_type_args.rs | 1 + .../rules/unnecessary_encode_utf8.rs | 1 + .../rules/unnecessary_future_import.rs | 1 + .../rules/unpacked_list_comprehension.rs | 1 + .../pyupgrade/rules/use_pep585_annotation.rs | 1 + .../pyupgrade/rules/use_pep604_annotation.rs | 2 + .../pyupgrade/rules/use_pep604_isinstance.rs | 1 + .../rules/useless_class_metaclass_type.rs | 1 + .../pyupgrade/rules/useless_metaclass_type.rs | 1 + .../rules/useless_object_inheritance.rs | 1 + .../pyupgrade/rules/yield_in_for_loop.rs | 1 + .../src/rules/refurb/rules/bit_count.rs | 1 + .../refurb/rules/check_and_remove_from_set.rs | 1 + .../rules/refurb/rules/delete_full_slice.rs | 1 + .../refurb/rules/for_loop_set_mutations.rs | 1 + .../src/rules/refurb/rules/for_loop_writes.rs | 1 + .../refurb/rules/fromisoformat_replace_z.rs | 1 + .../refurb/rules/fstring_number_format.rs | 1 + .../refurb/rules/hardcoded_string_charset.rs | 1 + .../rules/refurb/rules/hashlib_digest_hex.rs | 1 + .../rules/if_exp_instead_of_or_operator.rs | 1 + .../src/rules/refurb/rules/if_expr_min_max.rs | 1 + .../src/rules/refurb/rules/implicit_cwd.rs | 1 + .../rules/refurb/rules/int_on_sliced_str.rs | 1 + .../refurb/rules/isinstance_type_none.rs | 1 + .../rules/refurb/rules/list_reverse_copy.rs | 1 + .../src/rules/refurb/rules/math_constant.rs | 1 + .../rules/refurb/rules/metaclass_abcmeta.rs | 1 + .../rules/refurb/rules/print_empty_string.rs | 1 + .../src/rules/refurb/rules/read_whole_file.rs | 1 + .../rules/refurb/rules/readlines_in_for.rs | 1 + .../rules/refurb/rules/redundant_log_base.rs | 1 + .../rules/refurb/rules/regex_flag_alias.rs | 1 + .../refurb/rules/reimplemented_operator.rs | 1 + .../refurb/rules/reimplemented_starmap.rs | 1 + .../src/rules/refurb/rules/repeated_append.rs | 1 + .../src/rules/refurb/rules/repeated_global.rs | 1 + .../rules/single_item_membership_test.rs | 1 + .../src/rules/refurb/rules/slice_copy.rs | 1 + .../rules/slice_to_remove_prefix_or_suffix.rs | 1 + .../src/rules/refurb/rules/sorted_min_max.rs | 1 + .../rules/refurb/rules/subclass_builtin.rs | 1 + .../refurb/rules/type_none_comparison.rs | 1 + .../refurb/rules/unnecessary_enumerate.rs | 1 + .../refurb/rules/unnecessary_from_float.rs | 1 + .../rules/verbose_decimal_constructor.rs | 1 + .../rules/refurb/rules/write_whole_file.rs | 1 + .../access_annotations_from_class_dict.rs | 1 + .../ruff/rules/ambiguous_unicode_character.rs | 3 + .../ruff/rules/assert_with_print_message.rs | 1 + .../rules/ruff/rules/assignment_in_assert.rs | 1 + .../rules/ruff/rules/asyncio_dangling_task.rs | 1 + .../ruff/rules/class_with_mixed_type_vars.rs | 1 + .../rules/collection_literal_concatenation.rs | 1 + .../src/rules/ruff/rules/dataclass_enum.rs | 1 + .../ruff/rules/decimal_from_float_literal.rs | 1 + .../rules/ruff/rules/default_factory_kwarg.rs | 1 + .../explicit_f_string_type_conversion.rs | 1 + .../ruff/rules/falsy_dict_get_fallback.rs | 1 + .../function_call_in_dataclass_default.rs | 1 + .../rules/ruff/rules/if_key_in_dict_del.rs | 1 + .../rules/implicit_classvar_in_dataclass.rs | 1 + .../src/rules/ruff/rules/implicit_optional.rs | 1 + .../rules/ruff/rules/in_empty_collection.rs | 1 + ...rectly_parenthesized_tuple_in_subscript.rs | 1 + .../rules/ruff/rules/indented_form_feed.rs | 1 + ...invalid_assert_message_literal_argument.rs | 1 + .../invalid_formatter_suppression_comment.rs | 1 + .../rules/ruff/rules/invalid_index_type.rs | 1 + .../ruff/rules/invalid_pyproject_toml.rs | 1 + .../src/rules/ruff/rules/invalid_rule_code.rs | 1 + .../ruff/rules/legacy_form_pytest_raises.rs | 1 + .../ruff/rules/logging_eager_conversion.rs | 1 + .../ruff/rules/map_int_version_parsing.rs | 1 + .../ruff/rules/missing_fstring_syntax.rs | 1 + .../rules/ruff/rules/mutable_class_default.rs | 1 + .../ruff/rules/mutable_dataclass_default.rs | 1 + .../ruff/rules/mutable_fromkeys_value.rs | 1 + .../src/rules/ruff/rules/needless_else.rs | 1 + .../src/rules/ruff/rules/never_union.rs | 1 + .../rules/ruff/rules/non_octal_permissions.rs | 1 + .../ruff/rules/none_not_at_end_of_union.rs | 1 + .../rules/parenthesize_chained_operators.rs | 1 + .../src/rules/ruff/rules/post_init_default.rs | 1 + .../rules/pytest_raises_ambiguous_pattern.rs | 1 + .../ruff/rules/quadratic_list_summation.rs | 1 + .../src/rules/ruff/rules/redirected_noqa.rs | 1 + .../ruff/rules/redundant_bool_literal.rs | 1 + .../src/rules/ruff/rules/sort_dunder_all.rs | 1 + .../src/rules/ruff/rules/sort_dunder_slots.rs | 1 + .../src/rules/ruff/rules/starmap_zip.rs | 1 + .../rules/static_key_dict_comprehension.rs | 1 + .../src/rules/ruff/rules/test_rules.rs | 13 + .../ruff/rules/unnecessary_cast_to_int.rs | 1 + ...y_iterable_allocation_for_first_element.rs | 1 + .../rules/ruff/rules/unnecessary_key_check.rs | 1 + .../unnecessary_literal_within_deque_call.rs | 1 + .../ruff/rules/unnecessary_nested_literal.rs | 1 + .../rules/unnecessary_regular_expression.rs | 1 + .../src/rules/ruff/rules/unnecessary_round.rs | 1 + .../src/rules/ruff/rules/unraw_re_pattern.rs | 1 + .../src/rules/ruff/rules/unsafe_markup_use.rs | 1 + .../src/rules/ruff/rules/unused_async.rs | 1 + .../src/rules/ruff/rules/unused_noqa.rs | 1 + .../ruff/rules/unused_unpacked_variable.rs | 1 + .../rules/ruff/rules/used_dummy_variable.rs | 1 + .../src/rules/ruff/rules/useless_if_else.rs | 1 + .../ruff/rules/zip_instead_of_pairwise.rs | 1 + .../rules/error_instead_of_exception.rs | 1 + .../tryceratops/rules/raise_vanilla_args.rs | 1 + .../tryceratops/rules/raise_vanilla_class.rs | 1 + .../tryceratops/rules/raise_within_try.rs | 1 + .../tryceratops/rules/reraise_no_cause.rs | 1 + .../tryceratops/rules/try_consider_else.rs | 1 + .../rules/type_check_without_type_error.rs | 1 + .../tryceratops/rules/useless_try_except.rs | 1 + .../tryceratops/rules/verbose_log_message.rs | 1 + .../rules/tryceratops/rules/verbose_raise.rs | 1 + crates/ruff_linter/src/violation.rs | 14 +- crates/ruff_macros/src/lib.rs | 2 +- crates/ruff_macros/src/map_codes.rs | 59 +- crates/ruff_macros/src/rule_code_prefix.rs | 4 +- crates/ruff_macros/src/violation_metadata.rs | 62 + scripts/add_rule.py | 3 +- 703 files changed, 2105 insertions(+), 1005 deletions(-) diff --git a/crates/ruff/src/commands/rule.rs b/crates/ruff/src/commands/rule.rs index 6d797f8744..366ee56703 100644 --- a/crates/ruff/src/commands/rule.rs +++ b/crates/ruff/src/commands/rule.rs @@ -25,6 +25,7 @@ struct Explanation<'a> { explanation: Option<&'a str>, preview: bool, status: RuleGroup, + source_location: SourceLocation, } impl<'a> Explanation<'a> { @@ -43,6 +44,10 @@ impl<'a> Explanation<'a> { explanation: rule.explanation(), preview: rule.is_preview(), status: rule.group(), + source_location: SourceLocation { + file: rule.file(), + line: rule.line(), + }, } } } @@ -127,3 +132,14 @@ pub(crate) fn rules(format: HelpFormat) -> Result<()> { } Ok(()) } + +/// The location of the rule's implementation in the Ruff source tree, relative to the repository +/// root. +/// +/// For most rules this will point to the `#[derive(ViolationMetadata)]` line above the rule's +/// struct. +#[derive(Serialize)] +struct SourceLocation { + file: &'static str, + line: u32, +} diff --git a/crates/ruff/tests/integration_test.rs b/crates/ruff/tests/integration_test.rs index 62221419fe..48bc689a08 100644 --- a/crates/ruff/tests/integration_test.rs +++ b/crates/ruff/tests/integration_test.rs @@ -953,7 +953,11 @@ fn rule_f401() { #[test] fn rule_f401_output_json() { - assert_cmd_snapshot!(ruff_cmd().args(["rule", "F401", "--output-format", "json"])); + insta::with_settings!({filters => vec![ + (r#"("file": ")[^"]+(",)"#, "$1$2"), + ]}, { + assert_cmd_snapshot!(ruff_cmd().args(["rule", "F401", "--output-format", "json"])); + }); } #[test] diff --git a/crates/ruff/tests/snapshots/integration_test__rule_f401_output_json.snap b/crates/ruff/tests/snapshots/integration_test__rule_f401_output_json.snap index 51dffdf4a7..c206f975d4 100644 --- a/crates/ruff/tests/snapshots/integration_test__rule_f401_output_json.snap +++ b/crates/ruff/tests/snapshots/integration_test__rule_f401_output_json.snap @@ -25,6 +25,14 @@ exit_code: 0 "fix_availability": "Sometimes", "explanation": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Preview\nWhen [preview] is enabled (and certain simplifying assumptions\nare met), we analyze all import statements for a given module\nwhen determining whether an import is used, rather than simply\nthe last of these statements. This can result in both different and\nmore import statements being marked as unused.\n\nFor example, if a module consists of\n\n```python\nimport a\nimport a.b\n```\n\nthen both statements are marked as unused under [preview], whereas\nonly the second is marked as unused under stable behavior.\n\nAs another example, if a module consists of\n\n```python\nimport a.b\nimport a\n\na.b.foo()\n```\n\nthen a diagnostic will only be emitted for the first line under [preview],\nwhereas a diagnostic would only be emitted for the second line under\nstable behavior.\n\nNote that this behavior is somewhat subjective and is designed\nto conform to the developer's intuition rather than Python's actual\nexecution. To wit, the statement `import a.b` automatically executes\n`import a`, so in some sense `import a` is _always_ redundant\nin the presence of `import a.b`.\n\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\nSee [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)\nfor more details on how Ruff\ndetermines whether an import is first or third-party.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n\n[preview]: https://docs.astral.sh/ruff/preview/\n", "preview": false, - "status": "Stable" + "status": { + "Stable": { + "since": "v0.0.18" + } + }, + "source_location": { + "file": "", + "line": 145 + } } ----- stderr ----- diff --git a/crates/ruff_dev/src/generate_docs.rs b/crates/ruff_dev/src/generate_docs.rs index 5f2309328e..b2d8feb550 100644 --- a/crates/ruff_dev/src/generate_docs.rs +++ b/crates/ruff_dev/src/generate_docs.rs @@ -8,6 +8,7 @@ use std::path::PathBuf; use anyhow::Result; use itertools::Itertools; use regex::{Captures, Regex}; +use ruff_linter::codes::RuleGroup; use strum::IntoEnumIterator; use ruff_linter::FixAvailability; @@ -31,6 +32,47 @@ pub(crate) fn main(args: &Args) -> Result<()> { let _ = writeln!(&mut output, "# {} ({})", rule.name(), rule.noqa_code()); + let status_text = match rule.group() { + RuleGroup::Stable { since } => { + format!( + r#"Added in {since}"# + ) + } + RuleGroup::Preview { since } => { + format!( + r#"Preview (since {since})"# + ) + } + RuleGroup::Deprecated { since } => { + format!( + r#"Deprecated (since {since})"# + ) + } + RuleGroup::Removed { since } => { + format!( + r#"Removed (since {since})"# + ) + } + }; + + let _ = writeln!( + &mut output, + r#" +{status_text} · +Related issues · +View source + + +"#, + encoded_name = + url::form_urlencoded::byte_serialize(rule.name().as_str().as_bytes()) + .collect::(), + rule_code = rule.noqa_code(), + file = + url::form_urlencoded::byte_serialize(rule.file().replace('\\', "/").as_bytes()) + .collect::(), + line = rule.line(), + ); let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap(); if linter.url().is_some() { let common_prefix: String = match linter.common_prefix() { diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index 3255f8f42b..1f6f890076 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -32,20 +32,24 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator, table_out.push('\n'); for rule in rules { let status_token = match rule.group() { - RuleGroup::Removed => { + RuleGroup::Removed { since } => { format!( - "{REMOVED_SYMBOL}" + "{REMOVED_SYMBOL}" ) } - RuleGroup::Deprecated => { + RuleGroup::Deprecated { since } => { format!( - "{WARNING_SYMBOL}" + "{WARNING_SYMBOL}" ) } - RuleGroup::Preview => { - format!("{PREVIEW_SYMBOL}") + RuleGroup::Preview { since } => { + format!( + "{PREVIEW_SYMBOL}" + ) + } + RuleGroup::Stable { since } => { + format!("") } - RuleGroup::Stable => format!(""), }; let fix_token = match rule.fixable() { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 8b762c2c72..eab00b66d8 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -77,15 +77,16 @@ impl serde::Serialize for NoqaCode { #[derive(Debug, Copy, Clone, Serialize)] pub enum RuleGroup { - /// The rule is stable. - Stable, - /// The rule is unstable, and preview mode must be enabled for usage. - Preview, - /// The rule has been deprecated, warnings will be displayed during selection in stable - /// and errors will be raised if used with preview mode enabled. - Deprecated, - /// The rule has been removed, errors will be displayed on use. - Removed, + /// The rule is stable since the provided Ruff version. + Stable { since: &'static str }, + /// The rule has been unstable since the provided Ruff version, and preview mode must be enabled + /// for usage. + Preview { since: &'static str }, + /// The rule has been deprecated since the provided Ruff version, warnings will be displayed + /// during selection in stable and errors will be raised if used with preview mode enabled. + Deprecated { since: &'static str }, + /// The rule was removed in the provided Ruff version, and errors will be displayed on use. + Removed { since: &'static str }, } #[ruff_macros::map_codes] @@ -96,1095 +97,1095 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { #[rustfmt::skip] Some(match (linter, code) { // pycodestyle errors - (Pycodestyle, "E101") => (RuleGroup::Stable, rules::pycodestyle::rules::MixedSpacesAndTabs), - (Pycodestyle, "E111") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultiple), - (Pycodestyle, "E112") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoIndentedBlock), - (Pycodestyle, "E113") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::UnexpectedIndentation), - (Pycodestyle, "E114") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultipleComment), - (Pycodestyle, "E115") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoIndentedBlockComment), - (Pycodestyle, "E116") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::UnexpectedIndentationComment), - (Pycodestyle, "E117") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::OverIndented), - (Pycodestyle, "E201") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceAfterOpenBracket), - (Pycodestyle, "E202") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket), - (Pycodestyle, "E203") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforePunctuation), - (Pycodestyle, "E204") => (RuleGroup::Preview, rules::pycodestyle::rules::WhitespaceAfterDecorator), - (Pycodestyle, "E211") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeParameters), - (Pycodestyle, "E221") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator), - (Pycodestyle, "E222") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator), - (Pycodestyle, "E223") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabBeforeOperator), - (Pycodestyle, "E224") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabAfterOperator), - (Pycodestyle, "E225") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundOperator), - (Pycodestyle, "E226") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundArithmeticOperator), - (Pycodestyle, "E227") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundBitwiseOrShiftOperator), - (Pycodestyle, "E228") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundModuloOperator), - (Pycodestyle, "E231") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespace), - (Pycodestyle, "E241") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterComma), - (Pycodestyle, "E242") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabAfterComma), - (Pycodestyle, "E251") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::UnexpectedSpacesAroundKeywordParameterEquals), - (Pycodestyle, "E252") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundParameterEquals), - (Pycodestyle, "E261") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TooFewSpacesBeforeInlineComment), - (Pycodestyle, "E262") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoSpaceAfterInlineComment), - (Pycodestyle, "E265") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoSpaceAfterBlockComment), - (Pycodestyle, "E266") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleLeadingHashesForBlockComment), - (Pycodestyle, "E271") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterKeyword), - (Pycodestyle, "E272") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeKeyword), - (Pycodestyle, "E273") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabAfterKeyword), - (Pycodestyle, "E274") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabBeforeKeyword), - (Pycodestyle, "E275") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword), - (Pycodestyle, "E301") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLineBetweenMethods), - (Pycodestyle, "E302") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesTopLevel), - (Pycodestyle, "E303") => (RuleGroup::Preview, rules::pycodestyle::rules::TooManyBlankLines), - (Pycodestyle, "E304") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLineAfterDecorator), - (Pycodestyle, "E305") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesAfterFunctionOrClass), - (Pycodestyle, "E306") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesBeforeNestedDefinition), - (Pycodestyle, "E401") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleImportsOnOneLine), - (Pycodestyle, "E402") => (RuleGroup::Stable, rules::pycodestyle::rules::ModuleImportNotAtTopOfFile), - (Pycodestyle, "E501") => (RuleGroup::Stable, rules::pycodestyle::rules::LineTooLong), - (Pycodestyle, "E502") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::RedundantBackslash), - (Pycodestyle, "E701") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleStatementsOnOneLineColon), - (Pycodestyle, "E702") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleStatementsOnOneLineSemicolon), - (Pycodestyle, "E703") => (RuleGroup::Stable, rules::pycodestyle::rules::UselessSemicolon), - (Pycodestyle, "E711") => (RuleGroup::Stable, rules::pycodestyle::rules::NoneComparison), - (Pycodestyle, "E712") => (RuleGroup::Stable, rules::pycodestyle::rules::TrueFalseComparison), - (Pycodestyle, "E713") => (RuleGroup::Stable, rules::pycodestyle::rules::NotInTest), - (Pycodestyle, "E714") => (RuleGroup::Stable, rules::pycodestyle::rules::NotIsTest), - (Pycodestyle, "E721") => (RuleGroup::Stable, rules::pycodestyle::rules::TypeComparison), - (Pycodestyle, "E722") => (RuleGroup::Stable, rules::pycodestyle::rules::BareExcept), - (Pycodestyle, "E731") => (RuleGroup::Stable, rules::pycodestyle::rules::LambdaAssignment), - (Pycodestyle, "E741") => (RuleGroup::Stable, rules::pycodestyle::rules::AmbiguousVariableName), - (Pycodestyle, "E742") => (RuleGroup::Stable, rules::pycodestyle::rules::AmbiguousClassName), - (Pycodestyle, "E743") => (RuleGroup::Stable, rules::pycodestyle::rules::AmbiguousFunctionName), - (Pycodestyle, "E902") => (RuleGroup::Stable, rules::pycodestyle::rules::IOError), + (Pycodestyle, "E101") => rules::pycodestyle::rules::MixedSpacesAndTabs, + (Pycodestyle, "E111") => rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultiple, + (Pycodestyle, "E112") => rules::pycodestyle::rules::logical_lines::NoIndentedBlock, + (Pycodestyle, "E113") => rules::pycodestyle::rules::logical_lines::UnexpectedIndentation, + (Pycodestyle, "E114") => rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultipleComment, + (Pycodestyle, "E115") => rules::pycodestyle::rules::logical_lines::NoIndentedBlockComment, + (Pycodestyle, "E116") => rules::pycodestyle::rules::logical_lines::UnexpectedIndentationComment, + (Pycodestyle, "E117") => rules::pycodestyle::rules::logical_lines::OverIndented, + (Pycodestyle, "E201") => rules::pycodestyle::rules::logical_lines::WhitespaceAfterOpenBracket, + (Pycodestyle, "E202") => rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket, + (Pycodestyle, "E203") => rules::pycodestyle::rules::logical_lines::WhitespaceBeforePunctuation, + (Pycodestyle, "E204") => rules::pycodestyle::rules::WhitespaceAfterDecorator, + (Pycodestyle, "E211") => rules::pycodestyle::rules::logical_lines::WhitespaceBeforeParameters, + (Pycodestyle, "E221") => rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator, + (Pycodestyle, "E222") => rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator, + (Pycodestyle, "E223") => rules::pycodestyle::rules::logical_lines::TabBeforeOperator, + (Pycodestyle, "E224") => rules::pycodestyle::rules::logical_lines::TabAfterOperator, + (Pycodestyle, "E225") => rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundOperator, + (Pycodestyle, "E226") => rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundArithmeticOperator, + (Pycodestyle, "E227") => rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundBitwiseOrShiftOperator, + (Pycodestyle, "E228") => rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundModuloOperator, + (Pycodestyle, "E231") => rules::pycodestyle::rules::logical_lines::MissingWhitespace, + (Pycodestyle, "E241") => rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterComma, + (Pycodestyle, "E242") => rules::pycodestyle::rules::logical_lines::TabAfterComma, + (Pycodestyle, "E251") => rules::pycodestyle::rules::logical_lines::UnexpectedSpacesAroundKeywordParameterEquals, + (Pycodestyle, "E252") => rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundParameterEquals, + (Pycodestyle, "E261") => rules::pycodestyle::rules::logical_lines::TooFewSpacesBeforeInlineComment, + (Pycodestyle, "E262") => rules::pycodestyle::rules::logical_lines::NoSpaceAfterInlineComment, + (Pycodestyle, "E265") => rules::pycodestyle::rules::logical_lines::NoSpaceAfterBlockComment, + (Pycodestyle, "E266") => rules::pycodestyle::rules::logical_lines::MultipleLeadingHashesForBlockComment, + (Pycodestyle, "E271") => rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterKeyword, + (Pycodestyle, "E272") => rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeKeyword, + (Pycodestyle, "E273") => rules::pycodestyle::rules::logical_lines::TabAfterKeyword, + (Pycodestyle, "E274") => rules::pycodestyle::rules::logical_lines::TabBeforeKeyword, + (Pycodestyle, "E275") => rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword, + (Pycodestyle, "E301") => rules::pycodestyle::rules::BlankLineBetweenMethods, + (Pycodestyle, "E302") => rules::pycodestyle::rules::BlankLinesTopLevel, + (Pycodestyle, "E303") => rules::pycodestyle::rules::TooManyBlankLines, + (Pycodestyle, "E304") => rules::pycodestyle::rules::BlankLineAfterDecorator, + (Pycodestyle, "E305") => rules::pycodestyle::rules::BlankLinesAfterFunctionOrClass, + (Pycodestyle, "E306") => rules::pycodestyle::rules::BlankLinesBeforeNestedDefinition, + (Pycodestyle, "E401") => rules::pycodestyle::rules::MultipleImportsOnOneLine, + (Pycodestyle, "E402") => rules::pycodestyle::rules::ModuleImportNotAtTopOfFile, + (Pycodestyle, "E501") => rules::pycodestyle::rules::LineTooLong, + (Pycodestyle, "E502") => rules::pycodestyle::rules::logical_lines::RedundantBackslash, + (Pycodestyle, "E701") => rules::pycodestyle::rules::MultipleStatementsOnOneLineColon, + (Pycodestyle, "E702") => rules::pycodestyle::rules::MultipleStatementsOnOneLineSemicolon, + (Pycodestyle, "E703") => rules::pycodestyle::rules::UselessSemicolon, + (Pycodestyle, "E711") => rules::pycodestyle::rules::NoneComparison, + (Pycodestyle, "E712") => rules::pycodestyle::rules::TrueFalseComparison, + (Pycodestyle, "E713") => rules::pycodestyle::rules::NotInTest, + (Pycodestyle, "E714") => rules::pycodestyle::rules::NotIsTest, + (Pycodestyle, "E721") => rules::pycodestyle::rules::TypeComparison, + (Pycodestyle, "E722") => rules::pycodestyle::rules::BareExcept, + (Pycodestyle, "E731") => rules::pycodestyle::rules::LambdaAssignment, + (Pycodestyle, "E741") => rules::pycodestyle::rules::AmbiguousVariableName, + (Pycodestyle, "E742") => rules::pycodestyle::rules::AmbiguousClassName, + (Pycodestyle, "E743") => rules::pycodestyle::rules::AmbiguousFunctionName, + (Pycodestyle, "E902") => rules::pycodestyle::rules::IOError, #[allow(deprecated)] - (Pycodestyle, "E999") => (RuleGroup::Removed, rules::pycodestyle::rules::SyntaxError), + (Pycodestyle, "E999") => rules::pycodestyle::rules::SyntaxError, // pycodestyle warnings - (Pycodestyle, "W191") => (RuleGroup::Stable, rules::pycodestyle::rules::TabIndentation), - (Pycodestyle, "W291") => (RuleGroup::Stable, rules::pycodestyle::rules::TrailingWhitespace), - (Pycodestyle, "W292") => (RuleGroup::Stable, rules::pycodestyle::rules::MissingNewlineAtEndOfFile), - (Pycodestyle, "W293") => (RuleGroup::Stable, rules::pycodestyle::rules::BlankLineWithWhitespace), - (Pycodestyle, "W391") => (RuleGroup::Preview, rules::pycodestyle::rules::TooManyNewlinesAtEndOfFile), - (Pycodestyle, "W505") => (RuleGroup::Stable, rules::pycodestyle::rules::DocLineTooLong), - (Pycodestyle, "W605") => (RuleGroup::Stable, rules::pycodestyle::rules::InvalidEscapeSequence), + (Pycodestyle, "W191") => rules::pycodestyle::rules::TabIndentation, + (Pycodestyle, "W291") => rules::pycodestyle::rules::TrailingWhitespace, + (Pycodestyle, "W292") => rules::pycodestyle::rules::MissingNewlineAtEndOfFile, + (Pycodestyle, "W293") => rules::pycodestyle::rules::BlankLineWithWhitespace, + (Pycodestyle, "W391") => rules::pycodestyle::rules::TooManyNewlinesAtEndOfFile, + (Pycodestyle, "W505") => rules::pycodestyle::rules::DocLineTooLong, + (Pycodestyle, "W605") => rules::pycodestyle::rules::InvalidEscapeSequence, // pyflakes - (Pyflakes, "401") => (RuleGroup::Stable, rules::pyflakes::rules::UnusedImport), - (Pyflakes, "402") => (RuleGroup::Stable, rules::pyflakes::rules::ImportShadowedByLoopVar), - (Pyflakes, "403") => (RuleGroup::Stable, rules::pyflakes::rules::UndefinedLocalWithImportStar), - (Pyflakes, "404") => (RuleGroup::Stable, rules::pyflakes::rules::LateFutureImport), - (Pyflakes, "405") => (RuleGroup::Stable, rules::pyflakes::rules::UndefinedLocalWithImportStarUsage), - (Pyflakes, "406") => (RuleGroup::Stable, rules::pyflakes::rules::UndefinedLocalWithNestedImportStarUsage), - (Pyflakes, "407") => (RuleGroup::Stable, rules::pyflakes::rules::FutureFeatureNotDefined), - (Pyflakes, "501") => (RuleGroup::Stable, rules::pyflakes::rules::PercentFormatInvalidFormat), - (Pyflakes, "502") => (RuleGroup::Stable, rules::pyflakes::rules::PercentFormatExpectedMapping), - (Pyflakes, "503") => (RuleGroup::Stable, rules::pyflakes::rules::PercentFormatExpectedSequence), - (Pyflakes, "504") => (RuleGroup::Stable, rules::pyflakes::rules::PercentFormatExtraNamedArguments), - (Pyflakes, "505") => (RuleGroup::Stable, rules::pyflakes::rules::PercentFormatMissingArgument), - (Pyflakes, "506") => (RuleGroup::Stable, rules::pyflakes::rules::PercentFormatMixedPositionalAndNamed), - (Pyflakes, "507") => (RuleGroup::Stable, rules::pyflakes::rules::PercentFormatPositionalCountMismatch), - (Pyflakes, "508") => (RuleGroup::Stable, rules::pyflakes::rules::PercentFormatStarRequiresSequence), - (Pyflakes, "509") => (RuleGroup::Stable, rules::pyflakes::rules::PercentFormatUnsupportedFormatCharacter), - (Pyflakes, "521") => (RuleGroup::Stable, rules::pyflakes::rules::StringDotFormatInvalidFormat), - (Pyflakes, "522") => (RuleGroup::Stable, rules::pyflakes::rules::StringDotFormatExtraNamedArguments), - (Pyflakes, "523") => (RuleGroup::Stable, rules::pyflakes::rules::StringDotFormatExtraPositionalArguments), - (Pyflakes, "524") => (RuleGroup::Stable, rules::pyflakes::rules::StringDotFormatMissingArguments), - (Pyflakes, "525") => (RuleGroup::Stable, rules::pyflakes::rules::StringDotFormatMixingAutomatic), - (Pyflakes, "541") => (RuleGroup::Stable, rules::pyflakes::rules::FStringMissingPlaceholders), - (Pyflakes, "601") => (RuleGroup::Stable, rules::pyflakes::rules::MultiValueRepeatedKeyLiteral), - (Pyflakes, "602") => (RuleGroup::Stable, rules::pyflakes::rules::MultiValueRepeatedKeyVariable), - (Pyflakes, "621") => (RuleGroup::Stable, rules::pyflakes::rules::ExpressionsInStarAssignment), - (Pyflakes, "622") => (RuleGroup::Stable, rules::pyflakes::rules::MultipleStarredExpressions), - (Pyflakes, "631") => (RuleGroup::Stable, rules::pyflakes::rules::AssertTuple), - (Pyflakes, "632") => (RuleGroup::Stable, rules::pyflakes::rules::IsLiteral), - (Pyflakes, "633") => (RuleGroup::Stable, rules::pyflakes::rules::InvalidPrintSyntax), - (Pyflakes, "634") => (RuleGroup::Stable, rules::pyflakes::rules::IfTuple), - (Pyflakes, "701") => (RuleGroup::Stable, rules::pyflakes::rules::BreakOutsideLoop), - (Pyflakes, "702") => (RuleGroup::Stable, rules::pyflakes::rules::ContinueOutsideLoop), - (Pyflakes, "704") => (RuleGroup::Stable, rules::pyflakes::rules::YieldOutsideFunction), - (Pyflakes, "706") => (RuleGroup::Stable, rules::pyflakes::rules::ReturnOutsideFunction), - (Pyflakes, "707") => (RuleGroup::Stable, rules::pyflakes::rules::DefaultExceptNotLast), - (Pyflakes, "722") => (RuleGroup::Stable, rules::pyflakes::rules::ForwardAnnotationSyntaxError), - (Pyflakes, "811") => (RuleGroup::Stable, rules::pyflakes::rules::RedefinedWhileUnused), - (Pyflakes, "821") => (RuleGroup::Stable, rules::pyflakes::rules::UndefinedName), - (Pyflakes, "822") => (RuleGroup::Stable, rules::pyflakes::rules::UndefinedExport), - (Pyflakes, "823") => (RuleGroup::Stable, rules::pyflakes::rules::UndefinedLocal), - (Pyflakes, "841") => (RuleGroup::Stable, rules::pyflakes::rules::UnusedVariable), - (Pyflakes, "842") => (RuleGroup::Stable, rules::pyflakes::rules::UnusedAnnotation), - (Pyflakes, "901") => (RuleGroup::Stable, rules::pyflakes::rules::RaiseNotImplemented), + (Pyflakes, "401") => rules::pyflakes::rules::UnusedImport, + (Pyflakes, "402") => rules::pyflakes::rules::ImportShadowedByLoopVar, + (Pyflakes, "403") => rules::pyflakes::rules::UndefinedLocalWithImportStar, + (Pyflakes, "404") => rules::pyflakes::rules::LateFutureImport, + (Pyflakes, "405") => rules::pyflakes::rules::UndefinedLocalWithImportStarUsage, + (Pyflakes, "406") => rules::pyflakes::rules::UndefinedLocalWithNestedImportStarUsage, + (Pyflakes, "407") => rules::pyflakes::rules::FutureFeatureNotDefined, + (Pyflakes, "501") => rules::pyflakes::rules::PercentFormatInvalidFormat, + (Pyflakes, "502") => rules::pyflakes::rules::PercentFormatExpectedMapping, + (Pyflakes, "503") => rules::pyflakes::rules::PercentFormatExpectedSequence, + (Pyflakes, "504") => rules::pyflakes::rules::PercentFormatExtraNamedArguments, + (Pyflakes, "505") => rules::pyflakes::rules::PercentFormatMissingArgument, + (Pyflakes, "506") => rules::pyflakes::rules::PercentFormatMixedPositionalAndNamed, + (Pyflakes, "507") => rules::pyflakes::rules::PercentFormatPositionalCountMismatch, + (Pyflakes, "508") => rules::pyflakes::rules::PercentFormatStarRequiresSequence, + (Pyflakes, "509") => rules::pyflakes::rules::PercentFormatUnsupportedFormatCharacter, + (Pyflakes, "521") => rules::pyflakes::rules::StringDotFormatInvalidFormat, + (Pyflakes, "522") => rules::pyflakes::rules::StringDotFormatExtraNamedArguments, + (Pyflakes, "523") => rules::pyflakes::rules::StringDotFormatExtraPositionalArguments, + (Pyflakes, "524") => rules::pyflakes::rules::StringDotFormatMissingArguments, + (Pyflakes, "525") => rules::pyflakes::rules::StringDotFormatMixingAutomatic, + (Pyflakes, "541") => rules::pyflakes::rules::FStringMissingPlaceholders, + (Pyflakes, "601") => rules::pyflakes::rules::MultiValueRepeatedKeyLiteral, + (Pyflakes, "602") => rules::pyflakes::rules::MultiValueRepeatedKeyVariable, + (Pyflakes, "621") => rules::pyflakes::rules::ExpressionsInStarAssignment, + (Pyflakes, "622") => rules::pyflakes::rules::MultipleStarredExpressions, + (Pyflakes, "631") => rules::pyflakes::rules::AssertTuple, + (Pyflakes, "632") => rules::pyflakes::rules::IsLiteral, + (Pyflakes, "633") => rules::pyflakes::rules::InvalidPrintSyntax, + (Pyflakes, "634") => rules::pyflakes::rules::IfTuple, + (Pyflakes, "701") => rules::pyflakes::rules::BreakOutsideLoop, + (Pyflakes, "702") => rules::pyflakes::rules::ContinueOutsideLoop, + (Pyflakes, "704") => rules::pyflakes::rules::YieldOutsideFunction, + (Pyflakes, "706") => rules::pyflakes::rules::ReturnOutsideFunction, + (Pyflakes, "707") => rules::pyflakes::rules::DefaultExceptNotLast, + (Pyflakes, "722") => rules::pyflakes::rules::ForwardAnnotationSyntaxError, + (Pyflakes, "811") => rules::pyflakes::rules::RedefinedWhileUnused, + (Pyflakes, "821") => rules::pyflakes::rules::UndefinedName, + (Pyflakes, "822") => rules::pyflakes::rules::UndefinedExport, + (Pyflakes, "823") => rules::pyflakes::rules::UndefinedLocal, + (Pyflakes, "841") => rules::pyflakes::rules::UnusedVariable, + (Pyflakes, "842") => rules::pyflakes::rules::UnusedAnnotation, + (Pyflakes, "901") => rules::pyflakes::rules::RaiseNotImplemented, // pylint - (Pylint, "C0105") => (RuleGroup::Stable, rules::pylint::rules::TypeNameIncorrectVariance), - (Pylint, "C0131") => (RuleGroup::Stable, rules::pylint::rules::TypeBivariance), - (Pylint, "C0132") => (RuleGroup::Stable, rules::pylint::rules::TypeParamNameMismatch), - (Pylint, "C0205") => (RuleGroup::Stable, rules::pylint::rules::SingleStringSlots), - (Pylint, "C0206") => (RuleGroup::Stable, rules::pylint::rules::DictIndexMissingItems), - (Pylint, "C0207") => (RuleGroup::Preview, rules::pylint::rules::MissingMaxsplitArg), - (Pylint, "C0208") => (RuleGroup::Stable, rules::pylint::rules::IterationOverSet), - (Pylint, "C0414") => (RuleGroup::Stable, rules::pylint::rules::UselessImportAlias), - (Pylint, "C0415") => (RuleGroup::Stable, rules::pylint::rules::ImportOutsideTopLevel), - (Pylint, "C1802") => (RuleGroup::Stable, rules::pylint::rules::LenTest), - (Pylint, "C1901") => (RuleGroup::Preview, rules::pylint::rules::CompareToEmptyString), - (Pylint, "C2401") => (RuleGroup::Stable, rules::pylint::rules::NonAsciiName), - (Pylint, "C2403") => (RuleGroup::Stable, rules::pylint::rules::NonAsciiImportName), - (Pylint, "C2701") => (RuleGroup::Preview, rules::pylint::rules::ImportPrivateName), - (Pylint, "C2801") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDunderCall), - (Pylint, "C3002") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryDirectLambdaCall), - (Pylint, "E0100") => (RuleGroup::Stable, rules::pylint::rules::YieldInInit), - (Pylint, "E0101") => (RuleGroup::Stable, rules::pylint::rules::ReturnInInit), - (Pylint, "E0115") => (RuleGroup::Stable, rules::pylint::rules::NonlocalAndGlobal), - (Pylint, "E0116") => (RuleGroup::Stable, rules::pylint::rules::ContinueInFinally), - (Pylint, "E0117") => (RuleGroup::Stable, rules::pylint::rules::NonlocalWithoutBinding), - (Pylint, "E0118") => (RuleGroup::Stable, rules::pylint::rules::LoadBeforeGlobalDeclaration), - (Pylint, "E0237") => (RuleGroup::Stable, rules::pylint::rules::NonSlotAssignment), - (Pylint, "E0241") => (RuleGroup::Stable, rules::pylint::rules::DuplicateBases), - (Pylint, "E0302") => (RuleGroup::Stable, rules::pylint::rules::UnexpectedSpecialMethodSignature), - (Pylint, "E0303") => (RuleGroup::Stable, rules::pylint::rules::InvalidLengthReturnType), - (Pylint, "E0304") => (RuleGroup::Preview, rules::pylint::rules::InvalidBoolReturnType), - (Pylint, "E0305") => (RuleGroup::Stable, rules::pylint::rules::InvalidIndexReturnType), - (Pylint, "E0307") => (RuleGroup::Stable, rules::pylint::rules::InvalidStrReturnType), - (Pylint, "E0308") => (RuleGroup::Stable, rules::pylint::rules::InvalidBytesReturnType), - (Pylint, "E0309") => (RuleGroup::Stable, rules::pylint::rules::InvalidHashReturnType), - (Pylint, "E0604") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllObject), - (Pylint, "E0605") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllFormat), - (Pylint, "E0643") => (RuleGroup::Stable, rules::pylint::rules::PotentialIndexError), - (Pylint, "E0704") => (RuleGroup::Stable, rules::pylint::rules::MisplacedBareRaise), - (Pylint, "E1132") => (RuleGroup::Stable, rules::pylint::rules::RepeatedKeywordArgument), - (Pylint, "E1141") => (RuleGroup::Preview, rules::pylint::rules::DictIterMissingItems), - (Pylint, "E1142") => (RuleGroup::Stable, rules::pylint::rules::AwaitOutsideAsync), - (Pylint, "E1205") => (RuleGroup::Stable, rules::pylint::rules::LoggingTooManyArgs), - (Pylint, "E1206") => (RuleGroup::Stable, rules::pylint::rules::LoggingTooFewArgs), - (Pylint, "E1300") => (RuleGroup::Stable, rules::pylint::rules::BadStringFormatCharacter), - (Pylint, "E1307") => (RuleGroup::Stable, rules::pylint::rules::BadStringFormatType), - (Pylint, "E1310") => (RuleGroup::Stable, rules::pylint::rules::BadStrStripCall), - (Pylint, "E1507") => (RuleGroup::Stable, rules::pylint::rules::InvalidEnvvarValue), - (Pylint, "E1519") => (RuleGroup::Stable, rules::pylint::rules::SingledispatchMethod), - (Pylint, "E1520") => (RuleGroup::Stable, rules::pylint::rules::SingledispatchmethodFunction), - (Pylint, "E1700") => (RuleGroup::Stable, rules::pylint::rules::YieldFromInAsyncFunction), - (Pylint, "E2502") => (RuleGroup::Stable, rules::pylint::rules::BidirectionalUnicode), - (Pylint, "E2510") => (RuleGroup::Stable, rules::pylint::rules::InvalidCharacterBackspace), - (Pylint, "E2512") => (RuleGroup::Stable, rules::pylint::rules::InvalidCharacterSub), - (Pylint, "E2513") => (RuleGroup::Stable, rules::pylint::rules::InvalidCharacterEsc), - (Pylint, "E2514") => (RuleGroup::Stable, rules::pylint::rules::InvalidCharacterNul), - (Pylint, "E2515") => (RuleGroup::Stable, rules::pylint::rules::InvalidCharacterZeroWidthSpace), - (Pylint, "E4703") => (RuleGroup::Preview, rules::pylint::rules::ModifiedIteratingSet), - (Pylint, "R0124") => (RuleGroup::Stable, rules::pylint::rules::ComparisonWithItself), - (Pylint, "R0133") => (RuleGroup::Stable, rules::pylint::rules::ComparisonOfConstant), - (Pylint, "R0202") => (RuleGroup::Preview, rules::pylint::rules::NoClassmethodDecorator), - (Pylint, "R0203") => (RuleGroup::Preview, rules::pylint::rules::NoStaticmethodDecorator), - (Pylint, "R0206") => (RuleGroup::Stable, rules::pylint::rules::PropertyWithParameters), - (Pylint, "R0402") => (RuleGroup::Stable, rules::pylint::rules::ManualFromImport), - (Pylint, "R0904") => (RuleGroup::Preview, rules::pylint::rules::TooManyPublicMethods), - (Pylint, "R0911") => (RuleGroup::Stable, rules::pylint::rules::TooManyReturnStatements), - (Pylint, "R0912") => (RuleGroup::Stable, rules::pylint::rules::TooManyBranches), - (Pylint, "R0913") => (RuleGroup::Stable, rules::pylint::rules::TooManyArguments), - (Pylint, "R0914") => (RuleGroup::Preview, rules::pylint::rules::TooManyLocals), - (Pylint, "R0915") => (RuleGroup::Stable, rules::pylint::rules::TooManyStatements), - (Pylint, "R0916") => (RuleGroup::Preview, rules::pylint::rules::TooManyBooleanExpressions), - (Pylint, "R0917") => (RuleGroup::Preview, rules::pylint::rules::TooManyPositionalArguments), - (Pylint, "R1701") => (RuleGroup::Removed, rules::pylint::rules::RepeatedIsinstanceCalls), - (Pylint, "R1702") => (RuleGroup::Preview, rules::pylint::rules::TooManyNestedBlocks), - (Pylint, "R1704") => (RuleGroup::Stable, rules::pylint::rules::RedefinedArgumentFromLocal), - (Pylint, "R1706") => (RuleGroup::Removed, rules::pylint::rules::AndOrTernary), - (Pylint, "R1711") => (RuleGroup::Stable, rules::pylint::rules::UselessReturn), - (Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison), - (Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias), - (Pylint, "R1730") => (RuleGroup::Stable, rules::pylint::rules::IfStmtMinMax), - (Pylint, "R1716") => (RuleGroup::Stable, rules::pylint::rules::BooleanChainedComparison), - (Pylint, "R1733") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryDictIndexLookup), - (Pylint, "R1736") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryListIndexLookup), - (Pylint, "R2004") => (RuleGroup::Stable, rules::pylint::rules::MagicValueComparison), - (Pylint, "R2044") => (RuleGroup::Stable, rules::pylint::rules::EmptyComment), - (Pylint, "R5501") => (RuleGroup::Stable, rules::pylint::rules::CollapsibleElseIf), - (Pylint, "R6104") => (RuleGroup::Preview, rules::pylint::rules::NonAugmentedAssignment), - (Pylint, "R6201") => (RuleGroup::Preview, rules::pylint::rules::LiteralMembership), - (Pylint, "R6301") => (RuleGroup::Preview, rules::pylint::rules::NoSelfUse), + (Pylint, "C0105") => rules::pylint::rules::TypeNameIncorrectVariance, + (Pylint, "C0131") => rules::pylint::rules::TypeBivariance, + (Pylint, "C0132") => rules::pylint::rules::TypeParamNameMismatch, + (Pylint, "C0205") => rules::pylint::rules::SingleStringSlots, + (Pylint, "C0206") => rules::pylint::rules::DictIndexMissingItems, + (Pylint, "C0207") => rules::pylint::rules::MissingMaxsplitArg, + (Pylint, "C0208") => rules::pylint::rules::IterationOverSet, + (Pylint, "C0414") => rules::pylint::rules::UselessImportAlias, + (Pylint, "C0415") => rules::pylint::rules::ImportOutsideTopLevel, + (Pylint, "C1802") => rules::pylint::rules::LenTest, + (Pylint, "C1901") => rules::pylint::rules::CompareToEmptyString, + (Pylint, "C2401") => rules::pylint::rules::NonAsciiName, + (Pylint, "C2403") => rules::pylint::rules::NonAsciiImportName, + (Pylint, "C2701") => rules::pylint::rules::ImportPrivateName, + (Pylint, "C2801") => rules::pylint::rules::UnnecessaryDunderCall, + (Pylint, "C3002") => rules::pylint::rules::UnnecessaryDirectLambdaCall, + (Pylint, "E0100") => rules::pylint::rules::YieldInInit, + (Pylint, "E0101") => rules::pylint::rules::ReturnInInit, + (Pylint, "E0115") => rules::pylint::rules::NonlocalAndGlobal, + (Pylint, "E0116") => rules::pylint::rules::ContinueInFinally, + (Pylint, "E0117") => rules::pylint::rules::NonlocalWithoutBinding, + (Pylint, "E0118") => rules::pylint::rules::LoadBeforeGlobalDeclaration, + (Pylint, "E0237") => rules::pylint::rules::NonSlotAssignment, + (Pylint, "E0241") => rules::pylint::rules::DuplicateBases, + (Pylint, "E0302") => rules::pylint::rules::UnexpectedSpecialMethodSignature, + (Pylint, "E0303") => rules::pylint::rules::InvalidLengthReturnType, + (Pylint, "E0304") => rules::pylint::rules::InvalidBoolReturnType, + (Pylint, "E0305") => rules::pylint::rules::InvalidIndexReturnType, + (Pylint, "E0307") => rules::pylint::rules::InvalidStrReturnType, + (Pylint, "E0308") => rules::pylint::rules::InvalidBytesReturnType, + (Pylint, "E0309") => rules::pylint::rules::InvalidHashReturnType, + (Pylint, "E0604") => rules::pylint::rules::InvalidAllObject, + (Pylint, "E0605") => rules::pylint::rules::InvalidAllFormat, + (Pylint, "E0643") => rules::pylint::rules::PotentialIndexError, + (Pylint, "E0704") => rules::pylint::rules::MisplacedBareRaise, + (Pylint, "E1132") => rules::pylint::rules::RepeatedKeywordArgument, + (Pylint, "E1141") => rules::pylint::rules::DictIterMissingItems, + (Pylint, "E1142") => rules::pylint::rules::AwaitOutsideAsync, + (Pylint, "E1205") => rules::pylint::rules::LoggingTooManyArgs, + (Pylint, "E1206") => rules::pylint::rules::LoggingTooFewArgs, + (Pylint, "E1300") => rules::pylint::rules::BadStringFormatCharacter, + (Pylint, "E1307") => rules::pylint::rules::BadStringFormatType, + (Pylint, "E1310") => rules::pylint::rules::BadStrStripCall, + (Pylint, "E1507") => rules::pylint::rules::InvalidEnvvarValue, + (Pylint, "E1519") => rules::pylint::rules::SingledispatchMethod, + (Pylint, "E1520") => rules::pylint::rules::SingledispatchmethodFunction, + (Pylint, "E1700") => rules::pylint::rules::YieldFromInAsyncFunction, + (Pylint, "E2502") => rules::pylint::rules::BidirectionalUnicode, + (Pylint, "E2510") => rules::pylint::rules::InvalidCharacterBackspace, + (Pylint, "E2512") => rules::pylint::rules::InvalidCharacterSub, + (Pylint, "E2513") => rules::pylint::rules::InvalidCharacterEsc, + (Pylint, "E2514") => rules::pylint::rules::InvalidCharacterNul, + (Pylint, "E2515") => rules::pylint::rules::InvalidCharacterZeroWidthSpace, + (Pylint, "E4703") => rules::pylint::rules::ModifiedIteratingSet, + (Pylint, "R0124") => rules::pylint::rules::ComparisonWithItself, + (Pylint, "R0133") => rules::pylint::rules::ComparisonOfConstant, + (Pylint, "R0202") => rules::pylint::rules::NoClassmethodDecorator, + (Pylint, "R0203") => rules::pylint::rules::NoStaticmethodDecorator, + (Pylint, "R0206") => rules::pylint::rules::PropertyWithParameters, + (Pylint, "R0402") => rules::pylint::rules::ManualFromImport, + (Pylint, "R0904") => rules::pylint::rules::TooManyPublicMethods, + (Pylint, "R0911") => rules::pylint::rules::TooManyReturnStatements, + (Pylint, "R0912") => rules::pylint::rules::TooManyBranches, + (Pylint, "R0913") => rules::pylint::rules::TooManyArguments, + (Pylint, "R0914") => rules::pylint::rules::TooManyLocals, + (Pylint, "R0915") => rules::pylint::rules::TooManyStatements, + (Pylint, "R0916") => rules::pylint::rules::TooManyBooleanExpressions, + (Pylint, "R0917") => rules::pylint::rules::TooManyPositionalArguments, + (Pylint, "R1701") => rules::pylint::rules::RepeatedIsinstanceCalls, + (Pylint, "R1702") => rules::pylint::rules::TooManyNestedBlocks, + (Pylint, "R1704") => rules::pylint::rules::RedefinedArgumentFromLocal, + (Pylint, "R1706") => rules::pylint::rules::AndOrTernary, + (Pylint, "R1711") => rules::pylint::rules::UselessReturn, + (Pylint, "R1714") => rules::pylint::rules::RepeatedEqualityComparison, + (Pylint, "R1722") => rules::pylint::rules::SysExitAlias, + (Pylint, "R1730") => rules::pylint::rules::IfStmtMinMax, + (Pylint, "R1716") => rules::pylint::rules::BooleanChainedComparison, + (Pylint, "R1733") => rules::pylint::rules::UnnecessaryDictIndexLookup, + (Pylint, "R1736") => rules::pylint::rules::UnnecessaryListIndexLookup, + (Pylint, "R2004") => rules::pylint::rules::MagicValueComparison, + (Pylint, "R2044") => rules::pylint::rules::EmptyComment, + (Pylint, "R5501") => rules::pylint::rules::CollapsibleElseIf, + (Pylint, "R6104") => rules::pylint::rules::NonAugmentedAssignment, + (Pylint, "R6201") => rules::pylint::rules::LiteralMembership, + (Pylint, "R6301") => rules::pylint::rules::NoSelfUse, #[cfg(any(feature = "test-rules", test))] - (Pylint, "W0101") => (RuleGroup::Preview, rules::pylint::rules::UnreachableCode), - (Pylint, "W0108") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryLambda), - (Pylint, "W0177") => (RuleGroup::Stable, rules::pylint::rules::NanComparison), - (Pylint, "W0120") => (RuleGroup::Stable, rules::pylint::rules::UselessElseOnLoop), - (Pylint, "W0127") => (RuleGroup::Stable, rules::pylint::rules::SelfAssigningVariable), - (Pylint, "W0128") => (RuleGroup::Stable, rules::pylint::rules::RedeclaredAssignedName), - (Pylint, "W0129") => (RuleGroup::Stable, rules::pylint::rules::AssertOnStringLiteral), - (Pylint, "W0131") => (RuleGroup::Stable, rules::pylint::rules::NamedExprWithoutContext), - (Pylint, "W0133") => (RuleGroup::Stable, rules::pylint::rules::UselessExceptionStatement), - (Pylint, "W0211") => (RuleGroup::Stable, rules::pylint::rules::BadStaticmethodArgument), - (Pylint, "W0244") => (RuleGroup::Preview, rules::pylint::rules::RedefinedSlotsInSubclass), - (Pylint, "W0245") => (RuleGroup::Stable, rules::pylint::rules::SuperWithoutBrackets), - (Pylint, "W0406") => (RuleGroup::Stable, rules::pylint::rules::ImportSelf), - (Pylint, "W0602") => (RuleGroup::Stable, rules::pylint::rules::GlobalVariableNotAssigned), - (Pylint, "W0603") => (RuleGroup::Stable, rules::pylint::rules::GlobalStatement), - (Pylint, "W0604") => (RuleGroup::Stable, rules::pylint::rules::GlobalAtModuleLevel), - (Pylint, "W0642") => (RuleGroup::Stable, rules::pylint::rules::SelfOrClsAssignment), - (Pylint, "W0711") => (RuleGroup::Stable, rules::pylint::rules::BinaryOpException), - (Pylint, "W1501") => (RuleGroup::Stable, rules::pylint::rules::BadOpenMode), - (Pylint, "W1507") => (RuleGroup::Stable, rules::pylint::rules::ShallowCopyEnviron), - (Pylint, "W1508") => (RuleGroup::Stable, rules::pylint::rules::InvalidEnvvarDefault), - (Pylint, "W1509") => (RuleGroup::Stable, rules::pylint::rules::SubprocessPopenPreexecFn), - (Pylint, "W1510") => (RuleGroup::Stable, rules::pylint::rules::SubprocessRunWithoutCheck), - (Pylint, "W1514") => (RuleGroup::Preview, rules::pylint::rules::UnspecifiedEncoding), - (Pylint, "W1641") => (RuleGroup::Stable, rules::pylint::rules::EqWithoutHash), - (Pylint, "W2101") => (RuleGroup::Stable, rules::pylint::rules::UselessWithLock), - (Pylint, "W2901") => (RuleGroup::Stable, rules::pylint::rules::RedefinedLoopName), - (Pylint, "W3201") => (RuleGroup::Preview, rules::pylint::rules::BadDunderMethodName), - (Pylint, "W3301") => (RuleGroup::Stable, rules::pylint::rules::NestedMinMax), + (Pylint, "W0101") => rules::pylint::rules::UnreachableCode, + (Pylint, "W0108") => rules::pylint::rules::UnnecessaryLambda, + (Pylint, "W0177") => rules::pylint::rules::NanComparison, + (Pylint, "W0120") => rules::pylint::rules::UselessElseOnLoop, + (Pylint, "W0127") => rules::pylint::rules::SelfAssigningVariable, + (Pylint, "W0128") => rules::pylint::rules::RedeclaredAssignedName, + (Pylint, "W0129") => rules::pylint::rules::AssertOnStringLiteral, + (Pylint, "W0131") => rules::pylint::rules::NamedExprWithoutContext, + (Pylint, "W0133") => rules::pylint::rules::UselessExceptionStatement, + (Pylint, "W0211") => rules::pylint::rules::BadStaticmethodArgument, + (Pylint, "W0244") => rules::pylint::rules::RedefinedSlotsInSubclass, + (Pylint, "W0245") => rules::pylint::rules::SuperWithoutBrackets, + (Pylint, "W0406") => rules::pylint::rules::ImportSelf, + (Pylint, "W0602") => rules::pylint::rules::GlobalVariableNotAssigned, + (Pylint, "W0603") => rules::pylint::rules::GlobalStatement, + (Pylint, "W0604") => rules::pylint::rules::GlobalAtModuleLevel, + (Pylint, "W0642") => rules::pylint::rules::SelfOrClsAssignment, + (Pylint, "W0711") => rules::pylint::rules::BinaryOpException, + (Pylint, "W1501") => rules::pylint::rules::BadOpenMode, + (Pylint, "W1507") => rules::pylint::rules::ShallowCopyEnviron, + (Pylint, "W1508") => rules::pylint::rules::InvalidEnvvarDefault, + (Pylint, "W1509") => rules::pylint::rules::SubprocessPopenPreexecFn, + (Pylint, "W1510") => rules::pylint::rules::SubprocessRunWithoutCheck, + (Pylint, "W1514") => rules::pylint::rules::UnspecifiedEncoding, + (Pylint, "W1641") => rules::pylint::rules::EqWithoutHash, + (Pylint, "W2101") => rules::pylint::rules::UselessWithLock, + (Pylint, "W2901") => rules::pylint::rules::RedefinedLoopName, + (Pylint, "W3201") => rules::pylint::rules::BadDunderMethodName, + (Pylint, "W3301") => rules::pylint::rules::NestedMinMax, // flake8-async - (Flake8Async, "100") => (RuleGroup::Stable, rules::flake8_async::rules::CancelScopeNoCheckpoint), - (Flake8Async, "105") => (RuleGroup::Stable, rules::flake8_async::rules::TrioSyncCall), - (Flake8Async, "109") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncFunctionWithTimeout), - (Flake8Async, "110") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncBusyWait), - (Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncZeroSleep), - (Flake8Async, "116") => (RuleGroup::Stable, rules::flake8_async::rules::LongSleepNotForever), - (Flake8Async, "210") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction), - (Flake8Async, "212") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingHttpCallHttpxInAsyncFunction), - (Flake8Async, "220") => (RuleGroup::Stable, rules::flake8_async::rules::CreateSubprocessInAsyncFunction), - (Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction), - (Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction), - (Flake8Async, "230") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingOpenCallInAsyncFunction), - (Flake8Async, "240") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingPathMethodInAsyncFunction), - (Flake8Async, "250") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingInputInAsyncFunction), - (Flake8Async, "251") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingSleepInAsyncFunction), + (Flake8Async, "100") => rules::flake8_async::rules::CancelScopeNoCheckpoint, + (Flake8Async, "105") => rules::flake8_async::rules::TrioSyncCall, + (Flake8Async, "109") => rules::flake8_async::rules::AsyncFunctionWithTimeout, + (Flake8Async, "110") => rules::flake8_async::rules::AsyncBusyWait, + (Flake8Async, "115") => rules::flake8_async::rules::AsyncZeroSleep, + (Flake8Async, "116") => rules::flake8_async::rules::LongSleepNotForever, + (Flake8Async, "210") => rules::flake8_async::rules::BlockingHttpCallInAsyncFunction, + (Flake8Async, "212") => rules::flake8_async::rules::BlockingHttpCallHttpxInAsyncFunction, + (Flake8Async, "220") => rules::flake8_async::rules::CreateSubprocessInAsyncFunction, + (Flake8Async, "221") => rules::flake8_async::rules::RunProcessInAsyncFunction, + (Flake8Async, "222") => rules::flake8_async::rules::WaitForProcessInAsyncFunction, + (Flake8Async, "230") => rules::flake8_async::rules::BlockingOpenCallInAsyncFunction, + (Flake8Async, "240") => rules::flake8_async::rules::BlockingPathMethodInAsyncFunction, + (Flake8Async, "250") => rules::flake8_async::rules::BlockingInputInAsyncFunction, + (Flake8Async, "251") => rules::flake8_async::rules::BlockingSleepInAsyncFunction, // flake8-builtins - (Flake8Builtins, "001") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinVariableShadowing), - (Flake8Builtins, "002") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinArgumentShadowing), - (Flake8Builtins, "003") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinAttributeShadowing), - (Flake8Builtins, "004") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinImportShadowing), - (Flake8Builtins, "005") => (RuleGroup::Stable, rules::flake8_builtins::rules::StdlibModuleShadowing), - (Flake8Builtins, "006") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinLambdaArgumentShadowing), + (Flake8Builtins, "001") => rules::flake8_builtins::rules::BuiltinVariableShadowing, + (Flake8Builtins, "002") => rules::flake8_builtins::rules::BuiltinArgumentShadowing, + (Flake8Builtins, "003") => rules::flake8_builtins::rules::BuiltinAttributeShadowing, + (Flake8Builtins, "004") => rules::flake8_builtins::rules::BuiltinImportShadowing, + (Flake8Builtins, "005") => rules::flake8_builtins::rules::StdlibModuleShadowing, + (Flake8Builtins, "006") => rules::flake8_builtins::rules::BuiltinLambdaArgumentShadowing, // flake8-bugbear - (Flake8Bugbear, "002") => (RuleGroup::Stable, rules::flake8_bugbear::rules::UnaryPrefixIncrementDecrement), - (Flake8Bugbear, "003") => (RuleGroup::Stable, rules::flake8_bugbear::rules::AssignmentToOsEnviron), - (Flake8Bugbear, "004") => (RuleGroup::Stable, rules::flake8_bugbear::rules::UnreliableCallableCheck), - (Flake8Bugbear, "005") => (RuleGroup::Stable, rules::flake8_bugbear::rules::StripWithMultiCharacters), - (Flake8Bugbear, "006") => (RuleGroup::Stable, rules::flake8_bugbear::rules::MutableArgumentDefault), - (Flake8Bugbear, "007") => (RuleGroup::Stable, rules::flake8_bugbear::rules::UnusedLoopControlVariable), - (Flake8Bugbear, "008") => (RuleGroup::Stable, rules::flake8_bugbear::rules::FunctionCallInDefaultArgument), - (Flake8Bugbear, "009") => (RuleGroup::Stable, rules::flake8_bugbear::rules::GetAttrWithConstant), - (Flake8Bugbear, "010") => (RuleGroup::Stable, rules::flake8_bugbear::rules::SetAttrWithConstant), - (Flake8Bugbear, "011") => (RuleGroup::Stable, rules::flake8_bugbear::rules::AssertFalse), - (Flake8Bugbear, "012") => (RuleGroup::Stable, rules::flake8_bugbear::rules::JumpStatementInFinally), - (Flake8Bugbear, "013") => (RuleGroup::Stable, rules::flake8_bugbear::rules::RedundantTupleInExceptionHandler), - (Flake8Bugbear, "014") => (RuleGroup::Stable, rules::flake8_bugbear::rules::DuplicateHandlerException), - (Flake8Bugbear, "015") => (RuleGroup::Stable, rules::flake8_bugbear::rules::UselessComparison), - (Flake8Bugbear, "016") => (RuleGroup::Stable, rules::flake8_bugbear::rules::RaiseLiteral), - (Flake8Bugbear, "017") => (RuleGroup::Stable, rules::flake8_bugbear::rules::AssertRaisesException), - (Flake8Bugbear, "018") => (RuleGroup::Stable, rules::flake8_bugbear::rules::UselessExpression), - (Flake8Bugbear, "019") => (RuleGroup::Stable, rules::flake8_bugbear::rules::CachedInstanceMethod), - (Flake8Bugbear, "020") => (RuleGroup::Stable, rules::flake8_bugbear::rules::LoopVariableOverridesIterator), - (Flake8Bugbear, "021") => (RuleGroup::Stable, rules::flake8_bugbear::rules::FStringDocstring), - (Flake8Bugbear, "022") => (RuleGroup::Stable, rules::flake8_bugbear::rules::UselessContextlibSuppress), - (Flake8Bugbear, "023") => (RuleGroup::Stable, rules::flake8_bugbear::rules::FunctionUsesLoopVariable), - (Flake8Bugbear, "024") => (RuleGroup::Stable, rules::flake8_bugbear::rules::AbstractBaseClassWithoutAbstractMethod), - (Flake8Bugbear, "025") => (RuleGroup::Stable, rules::flake8_bugbear::rules::DuplicateTryBlockException), - (Flake8Bugbear, "026") => (RuleGroup::Stable, rules::flake8_bugbear::rules::StarArgUnpackingAfterKeywordArg), - (Flake8Bugbear, "027") => (RuleGroup::Stable, rules::flake8_bugbear::rules::EmptyMethodWithoutAbstractDecorator), - (Flake8Bugbear, "028") => (RuleGroup::Stable, rules::flake8_bugbear::rules::NoExplicitStacklevel), - (Flake8Bugbear, "029") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ExceptWithEmptyTuple), - (Flake8Bugbear, "030") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ExceptWithNonExceptionClasses), - (Flake8Bugbear, "031") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ReuseOfGroupbyGenerator), - (Flake8Bugbear, "032") => (RuleGroup::Stable, rules::flake8_bugbear::rules::UnintentionalTypeAnnotation), - (Flake8Bugbear, "033") => (RuleGroup::Stable, rules::flake8_bugbear::rules::DuplicateValue), - (Flake8Bugbear, "034") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ReSubPositionalArgs), - (Flake8Bugbear, "035") => (RuleGroup::Stable, rules::flake8_bugbear::rules::StaticKeyDictComprehension), - (Flake8Bugbear, "039") => (RuleGroup::Stable, rules::flake8_bugbear::rules::MutableContextvarDefault), - (Flake8Bugbear, "901") => (RuleGroup::Preview, rules::flake8_bugbear::rules::ReturnInGenerator), - (Flake8Bugbear, "903") => (RuleGroup::Preview, rules::flake8_bugbear::rules::ClassAsDataStructure), - (Flake8Bugbear, "904") => (RuleGroup::Stable, rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept), - (Flake8Bugbear, "905") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict), - (Flake8Bugbear, "909") => (RuleGroup::Preview, rules::flake8_bugbear::rules::LoopIteratorMutation), - (Flake8Bugbear, "911") => (RuleGroup::Stable, rules::flake8_bugbear::rules::BatchedWithoutExplicitStrict), - (Flake8Bugbear, "912") => (RuleGroup::Preview, rules::flake8_bugbear::rules::MapWithoutExplicitStrict), + (Flake8Bugbear, "002") => rules::flake8_bugbear::rules::UnaryPrefixIncrementDecrement, + (Flake8Bugbear, "003") => rules::flake8_bugbear::rules::AssignmentToOsEnviron, + (Flake8Bugbear, "004") => rules::flake8_bugbear::rules::UnreliableCallableCheck, + (Flake8Bugbear, "005") => rules::flake8_bugbear::rules::StripWithMultiCharacters, + (Flake8Bugbear, "006") => rules::flake8_bugbear::rules::MutableArgumentDefault, + (Flake8Bugbear, "007") => rules::flake8_bugbear::rules::UnusedLoopControlVariable, + (Flake8Bugbear, "008") => rules::flake8_bugbear::rules::FunctionCallInDefaultArgument, + (Flake8Bugbear, "009") => rules::flake8_bugbear::rules::GetAttrWithConstant, + (Flake8Bugbear, "010") => rules::flake8_bugbear::rules::SetAttrWithConstant, + (Flake8Bugbear, "011") => rules::flake8_bugbear::rules::AssertFalse, + (Flake8Bugbear, "012") => rules::flake8_bugbear::rules::JumpStatementInFinally, + (Flake8Bugbear, "013") => rules::flake8_bugbear::rules::RedundantTupleInExceptionHandler, + (Flake8Bugbear, "014") => rules::flake8_bugbear::rules::DuplicateHandlerException, + (Flake8Bugbear, "015") => rules::flake8_bugbear::rules::UselessComparison, + (Flake8Bugbear, "016") => rules::flake8_bugbear::rules::RaiseLiteral, + (Flake8Bugbear, "017") => rules::flake8_bugbear::rules::AssertRaisesException, + (Flake8Bugbear, "018") => rules::flake8_bugbear::rules::UselessExpression, + (Flake8Bugbear, "019") => rules::flake8_bugbear::rules::CachedInstanceMethod, + (Flake8Bugbear, "020") => rules::flake8_bugbear::rules::LoopVariableOverridesIterator, + (Flake8Bugbear, "021") => rules::flake8_bugbear::rules::FStringDocstring, + (Flake8Bugbear, "022") => rules::flake8_bugbear::rules::UselessContextlibSuppress, + (Flake8Bugbear, "023") => rules::flake8_bugbear::rules::FunctionUsesLoopVariable, + (Flake8Bugbear, "024") => rules::flake8_bugbear::rules::AbstractBaseClassWithoutAbstractMethod, + (Flake8Bugbear, "025") => rules::flake8_bugbear::rules::DuplicateTryBlockException, + (Flake8Bugbear, "026") => rules::flake8_bugbear::rules::StarArgUnpackingAfterKeywordArg, + (Flake8Bugbear, "027") => rules::flake8_bugbear::rules::EmptyMethodWithoutAbstractDecorator, + (Flake8Bugbear, "028") => rules::flake8_bugbear::rules::NoExplicitStacklevel, + (Flake8Bugbear, "029") => rules::flake8_bugbear::rules::ExceptWithEmptyTuple, + (Flake8Bugbear, "030") => rules::flake8_bugbear::rules::ExceptWithNonExceptionClasses, + (Flake8Bugbear, "031") => rules::flake8_bugbear::rules::ReuseOfGroupbyGenerator, + (Flake8Bugbear, "032") => rules::flake8_bugbear::rules::UnintentionalTypeAnnotation, + (Flake8Bugbear, "033") => rules::flake8_bugbear::rules::DuplicateValue, + (Flake8Bugbear, "034") => rules::flake8_bugbear::rules::ReSubPositionalArgs, + (Flake8Bugbear, "035") => rules::flake8_bugbear::rules::StaticKeyDictComprehension, + (Flake8Bugbear, "039") => rules::flake8_bugbear::rules::MutableContextvarDefault, + (Flake8Bugbear, "901") => rules::flake8_bugbear::rules::ReturnInGenerator, + (Flake8Bugbear, "903") => rules::flake8_bugbear::rules::ClassAsDataStructure, + (Flake8Bugbear, "904") => rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept, + (Flake8Bugbear, "905") => rules::flake8_bugbear::rules::ZipWithoutExplicitStrict, + (Flake8Bugbear, "909") => rules::flake8_bugbear::rules::LoopIteratorMutation, + (Flake8Bugbear, "911") => rules::flake8_bugbear::rules::BatchedWithoutExplicitStrict, + (Flake8Bugbear, "912") => rules::flake8_bugbear::rules::MapWithoutExplicitStrict, // flake8-blind-except - (Flake8BlindExcept, "001") => (RuleGroup::Stable, rules::flake8_blind_except::rules::BlindExcept), + (Flake8BlindExcept, "001") => rules::flake8_blind_except::rules::BlindExcept, // flake8-comprehensions - (Flake8Comprehensions, "00") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryGeneratorList), - (Flake8Comprehensions, "01") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryGeneratorSet), - (Flake8Comprehensions, "02") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryGeneratorDict), - (Flake8Comprehensions, "03") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryListComprehensionSet), - (Flake8Comprehensions, "04") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryListComprehensionDict), - (Flake8Comprehensions, "05") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryLiteralSet), - (Flake8Comprehensions, "06") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryLiteralDict), - (Flake8Comprehensions, "08") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryCollectionCall), - (Flake8Comprehensions, "09") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryLiteralWithinTupleCall), - (Flake8Comprehensions, "10") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryLiteralWithinListCall), - (Flake8Comprehensions, "11") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryListCall), - (Flake8Comprehensions, "13") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryCallAroundSorted), - (Flake8Comprehensions, "14") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryDoubleCastOrProcess), - (Flake8Comprehensions, "15") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessarySubscriptReversal), - (Flake8Comprehensions, "16") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryComprehension), - (Flake8Comprehensions, "17") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryMap), - (Flake8Comprehensions, "18") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryLiteralWithinDictCall), - (Flake8Comprehensions, "19") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryComprehensionInCall), - (Flake8Comprehensions, "20") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryDictComprehensionForIterable), + (Flake8Comprehensions, "00") => rules::flake8_comprehensions::rules::UnnecessaryGeneratorList, + (Flake8Comprehensions, "01") => rules::flake8_comprehensions::rules::UnnecessaryGeneratorSet, + (Flake8Comprehensions, "02") => rules::flake8_comprehensions::rules::UnnecessaryGeneratorDict, + (Flake8Comprehensions, "03") => rules::flake8_comprehensions::rules::UnnecessaryListComprehensionSet, + (Flake8Comprehensions, "04") => rules::flake8_comprehensions::rules::UnnecessaryListComprehensionDict, + (Flake8Comprehensions, "05") => rules::flake8_comprehensions::rules::UnnecessaryLiteralSet, + (Flake8Comprehensions, "06") => rules::flake8_comprehensions::rules::UnnecessaryLiteralDict, + (Flake8Comprehensions, "08") => rules::flake8_comprehensions::rules::UnnecessaryCollectionCall, + (Flake8Comprehensions, "09") => rules::flake8_comprehensions::rules::UnnecessaryLiteralWithinTupleCall, + (Flake8Comprehensions, "10") => rules::flake8_comprehensions::rules::UnnecessaryLiteralWithinListCall, + (Flake8Comprehensions, "11") => rules::flake8_comprehensions::rules::UnnecessaryListCall, + (Flake8Comprehensions, "13") => rules::flake8_comprehensions::rules::UnnecessaryCallAroundSorted, + (Flake8Comprehensions, "14") => rules::flake8_comprehensions::rules::UnnecessaryDoubleCastOrProcess, + (Flake8Comprehensions, "15") => rules::flake8_comprehensions::rules::UnnecessarySubscriptReversal, + (Flake8Comprehensions, "16") => rules::flake8_comprehensions::rules::UnnecessaryComprehension, + (Flake8Comprehensions, "17") => rules::flake8_comprehensions::rules::UnnecessaryMap, + (Flake8Comprehensions, "18") => rules::flake8_comprehensions::rules::UnnecessaryLiteralWithinDictCall, + (Flake8Comprehensions, "19") => rules::flake8_comprehensions::rules::UnnecessaryComprehensionInCall, + (Flake8Comprehensions, "20") => rules::flake8_comprehensions::rules::UnnecessaryDictComprehensionForIterable, // flake8-debugger - (Flake8Debugger, "0") => (RuleGroup::Stable, rules::flake8_debugger::rules::Debugger), + (Flake8Debugger, "0") => rules::flake8_debugger::rules::Debugger, // mccabe - (McCabe, "1") => (RuleGroup::Stable, rules::mccabe::rules::ComplexStructure), + (McCabe, "1") => rules::mccabe::rules::ComplexStructure, // flake8-tidy-imports - (Flake8TidyImports, "251") => (RuleGroup::Stable, rules::flake8_tidy_imports::rules::BannedApi), - (Flake8TidyImports, "252") => (RuleGroup::Stable, rules::flake8_tidy_imports::rules::RelativeImports), - (Flake8TidyImports, "253") => (RuleGroup::Stable, rules::flake8_tidy_imports::rules::BannedModuleLevelImports), + (Flake8TidyImports, "251") => rules::flake8_tidy_imports::rules::BannedApi, + (Flake8TidyImports, "252") => rules::flake8_tidy_imports::rules::RelativeImports, + (Flake8TidyImports, "253") => rules::flake8_tidy_imports::rules::BannedModuleLevelImports, // flake8-return - (Flake8Return, "501") => (RuleGroup::Stable, rules::flake8_return::rules::UnnecessaryReturnNone), - (Flake8Return, "502") => (RuleGroup::Stable, rules::flake8_return::rules::ImplicitReturnValue), - (Flake8Return, "503") => (RuleGroup::Stable, rules::flake8_return::rules::ImplicitReturn), - (Flake8Return, "504") => (RuleGroup::Stable, rules::flake8_return::rules::UnnecessaryAssign), - (Flake8Return, "505") => (RuleGroup::Stable, rules::flake8_return::rules::SuperfluousElseReturn), - (Flake8Return, "506") => (RuleGroup::Stable, rules::flake8_return::rules::SuperfluousElseRaise), - (Flake8Return, "507") => (RuleGroup::Stable, rules::flake8_return::rules::SuperfluousElseContinue), - (Flake8Return, "508") => (RuleGroup::Stable, rules::flake8_return::rules::SuperfluousElseBreak), + (Flake8Return, "501") => rules::flake8_return::rules::UnnecessaryReturnNone, + (Flake8Return, "502") => rules::flake8_return::rules::ImplicitReturnValue, + (Flake8Return, "503") => rules::flake8_return::rules::ImplicitReturn, + (Flake8Return, "504") => rules::flake8_return::rules::UnnecessaryAssign, + (Flake8Return, "505") => rules::flake8_return::rules::SuperfluousElseReturn, + (Flake8Return, "506") => rules::flake8_return::rules::SuperfluousElseRaise, + (Flake8Return, "507") => rules::flake8_return::rules::SuperfluousElseContinue, + (Flake8Return, "508") => rules::flake8_return::rules::SuperfluousElseBreak, // flake8-gettext - (Flake8GetText, "001") => (RuleGroup::Stable, rules::flake8_gettext::rules::FStringInGetTextFuncCall), - (Flake8GetText, "002") => (RuleGroup::Stable, rules::flake8_gettext::rules::FormatInGetTextFuncCall), - (Flake8GetText, "003") => (RuleGroup::Stable, rules::flake8_gettext::rules::PrintfInGetTextFuncCall), + (Flake8GetText, "001") => rules::flake8_gettext::rules::FStringInGetTextFuncCall, + (Flake8GetText, "002") => rules::flake8_gettext::rules::FormatInGetTextFuncCall, + (Flake8GetText, "003") => rules::flake8_gettext::rules::PrintfInGetTextFuncCall, // flake8-implicit-str-concat - (Flake8ImplicitStrConcat, "001") => (RuleGroup::Stable, rules::flake8_implicit_str_concat::rules::SingleLineImplicitStringConcatenation), - (Flake8ImplicitStrConcat, "002") => (RuleGroup::Stable, rules::flake8_implicit_str_concat::rules::MultiLineImplicitStringConcatenation), - (Flake8ImplicitStrConcat, "003") => (RuleGroup::Stable, rules::flake8_implicit_str_concat::rules::ExplicitStringConcatenation), + (Flake8ImplicitStrConcat, "001") => rules::flake8_implicit_str_concat::rules::SingleLineImplicitStringConcatenation, + (Flake8ImplicitStrConcat, "002") => rules::flake8_implicit_str_concat::rules::MultiLineImplicitStringConcatenation, + (Flake8ImplicitStrConcat, "003") => rules::flake8_implicit_str_concat::rules::ExplicitStringConcatenation, // flake8-print - (Flake8Print, "1") => (RuleGroup::Stable, rules::flake8_print::rules::Print), - (Flake8Print, "3") => (RuleGroup::Stable, rules::flake8_print::rules::PPrint), + (Flake8Print, "1") => rules::flake8_print::rules::Print, + (Flake8Print, "3") => rules::flake8_print::rules::PPrint, // flake8-quotes - (Flake8Quotes, "000") => (RuleGroup::Stable, rules::flake8_quotes::rules::BadQuotesInlineString), - (Flake8Quotes, "001") => (RuleGroup::Stable, rules::flake8_quotes::rules::BadQuotesMultilineString), - (Flake8Quotes, "002") => (RuleGroup::Stable, rules::flake8_quotes::rules::BadQuotesDocstring), - (Flake8Quotes, "003") => (RuleGroup::Stable, rules::flake8_quotes::rules::AvoidableEscapedQuote), - (Flake8Quotes, "004") => (RuleGroup::Stable, rules::flake8_quotes::rules::UnnecessaryEscapedQuote), + (Flake8Quotes, "000") => rules::flake8_quotes::rules::BadQuotesInlineString, + (Flake8Quotes, "001") => rules::flake8_quotes::rules::BadQuotesMultilineString, + (Flake8Quotes, "002") => rules::flake8_quotes::rules::BadQuotesDocstring, + (Flake8Quotes, "003") => rules::flake8_quotes::rules::AvoidableEscapedQuote, + (Flake8Quotes, "004") => rules::flake8_quotes::rules::UnnecessaryEscapedQuote, // flake8-annotations - (Flake8Annotations, "001") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeFunctionArgument), - (Flake8Annotations, "002") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeArgs), - (Flake8Annotations, "003") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeKwargs), + (Flake8Annotations, "001") => rules::flake8_annotations::rules::MissingTypeFunctionArgument, + (Flake8Annotations, "002") => rules::flake8_annotations::rules::MissingTypeArgs, + (Flake8Annotations, "003") => rules::flake8_annotations::rules::MissingTypeKwargs, #[allow(deprecated)] - (Flake8Annotations, "101") => (RuleGroup::Removed, rules::flake8_annotations::rules::MissingTypeSelf), + (Flake8Annotations, "101") => rules::flake8_annotations::rules::MissingTypeSelf, #[allow(deprecated)] - (Flake8Annotations, "102") => (RuleGroup::Removed, rules::flake8_annotations::rules::MissingTypeCls), - (Flake8Annotations, "201") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypeUndocumentedPublicFunction), - (Flake8Annotations, "202") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypePrivateFunction), - (Flake8Annotations, "204") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypeSpecialMethod), - (Flake8Annotations, "205") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypeStaticMethod), - (Flake8Annotations, "206") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingReturnTypeClassMethod), - (Flake8Annotations, "401") => (RuleGroup::Stable, rules::flake8_annotations::rules::AnyType), + (Flake8Annotations, "102") => rules::flake8_annotations::rules::MissingTypeCls, + (Flake8Annotations, "201") => rules::flake8_annotations::rules::MissingReturnTypeUndocumentedPublicFunction, + (Flake8Annotations, "202") => rules::flake8_annotations::rules::MissingReturnTypePrivateFunction, + (Flake8Annotations, "204") => rules::flake8_annotations::rules::MissingReturnTypeSpecialMethod, + (Flake8Annotations, "205") => rules::flake8_annotations::rules::MissingReturnTypeStaticMethod, + (Flake8Annotations, "206") => rules::flake8_annotations::rules::MissingReturnTypeClassMethod, + (Flake8Annotations, "401") => rules::flake8_annotations::rules::AnyType, // flake8-future-annotations - (Flake8FutureAnnotations, "100") => (RuleGroup::Stable, rules::flake8_future_annotations::rules::FutureRewritableTypeAnnotation), - (Flake8FutureAnnotations, "102") => (RuleGroup::Stable, rules::flake8_future_annotations::rules::FutureRequiredTypeAnnotation), + (Flake8FutureAnnotations, "100") => rules::flake8_future_annotations::rules::FutureRewritableTypeAnnotation, + (Flake8FutureAnnotations, "102") => rules::flake8_future_annotations::rules::FutureRequiredTypeAnnotation, // flake8-2020 - (Flake82020, "101") => (RuleGroup::Stable, rules::flake8_2020::rules::SysVersionSlice3), - (Flake82020, "102") => (RuleGroup::Stable, rules::flake8_2020::rules::SysVersion2), - (Flake82020, "103") => (RuleGroup::Stable, rules::flake8_2020::rules::SysVersionCmpStr3), - (Flake82020, "201") => (RuleGroup::Stable, rules::flake8_2020::rules::SysVersionInfo0Eq3), - (Flake82020, "202") => (RuleGroup::Stable, rules::flake8_2020::rules::SixPY3), - (Flake82020, "203") => (RuleGroup::Stable, rules::flake8_2020::rules::SysVersionInfo1CmpInt), - (Flake82020, "204") => (RuleGroup::Stable, rules::flake8_2020::rules::SysVersionInfoMinorCmpInt), - (Flake82020, "301") => (RuleGroup::Stable, rules::flake8_2020::rules::SysVersion0), - (Flake82020, "302") => (RuleGroup::Stable, rules::flake8_2020::rules::SysVersionCmpStr10), - (Flake82020, "303") => (RuleGroup::Stable, rules::flake8_2020::rules::SysVersionSlice1), + (Flake82020, "101") => rules::flake8_2020::rules::SysVersionSlice3, + (Flake82020, "102") => rules::flake8_2020::rules::SysVersion2, + (Flake82020, "103") => rules::flake8_2020::rules::SysVersionCmpStr3, + (Flake82020, "201") => rules::flake8_2020::rules::SysVersionInfo0Eq3, + (Flake82020, "202") => rules::flake8_2020::rules::SixPY3, + (Flake82020, "203") => rules::flake8_2020::rules::SysVersionInfo1CmpInt, + (Flake82020, "204") => rules::flake8_2020::rules::SysVersionInfoMinorCmpInt, + (Flake82020, "301") => rules::flake8_2020::rules::SysVersion0, + (Flake82020, "302") => rules::flake8_2020::rules::SysVersionCmpStr10, + (Flake82020, "303") => rules::flake8_2020::rules::SysVersionSlice1, // flake8-simplify - (Flake8Simplify, "101") => (RuleGroup::Stable, rules::flake8_simplify::rules::DuplicateIsinstanceCall), - (Flake8Simplify, "102") => (RuleGroup::Stable, rules::flake8_simplify::rules::CollapsibleIf), - (Flake8Simplify, "103") => (RuleGroup::Stable, rules::flake8_simplify::rules::NeedlessBool), - (Flake8Simplify, "105") => (RuleGroup::Stable, rules::flake8_simplify::rules::SuppressibleException), - (Flake8Simplify, "107") => (RuleGroup::Stable, rules::flake8_simplify::rules::ReturnInTryExceptFinally), - (Flake8Simplify, "108") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfElseBlockInsteadOfIfExp), - (Flake8Simplify, "109") => (RuleGroup::Stable, rules::flake8_simplify::rules::CompareWithTuple), - (Flake8Simplify, "110") => (RuleGroup::Stable, rules::flake8_simplify::rules::ReimplementedBuiltin), - (Flake8Simplify, "112") => (RuleGroup::Stable, rules::flake8_simplify::rules::UncapitalizedEnvironmentVariables), - (Flake8Simplify, "113") => (RuleGroup::Stable, rules::flake8_simplify::rules::EnumerateForLoop), - (Flake8Simplify, "114") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfWithSameArms), - (Flake8Simplify, "115") => (RuleGroup::Stable, rules::flake8_simplify::rules::OpenFileWithContextHandler), - (Flake8Simplify, "116") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictLookup), - (Flake8Simplify, "117") => (RuleGroup::Stable, rules::flake8_simplify::rules::MultipleWithStatements), - (Flake8Simplify, "118") => (RuleGroup::Stable, rules::flake8_simplify::rules::InDictKeys), - (Flake8Simplify, "201") => (RuleGroup::Stable, rules::flake8_simplify::rules::NegateEqualOp), - (Flake8Simplify, "202") => (RuleGroup::Stable, rules::flake8_simplify::rules::NegateNotEqualOp), - (Flake8Simplify, "208") => (RuleGroup::Stable, rules::flake8_simplify::rules::DoubleNegation), - (Flake8Simplify, "210") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfExprWithTrueFalse), - (Flake8Simplify, "211") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfExprWithFalseTrue), - (Flake8Simplify, "212") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfExprWithTwistedArms), - (Flake8Simplify, "220") => (RuleGroup::Stable, rules::flake8_simplify::rules::ExprAndNotExpr), - (Flake8Simplify, "221") => (RuleGroup::Stable, rules::flake8_simplify::rules::ExprOrNotExpr), - (Flake8Simplify, "222") => (RuleGroup::Stable, rules::flake8_simplify::rules::ExprOrTrue), - (Flake8Simplify, "223") => (RuleGroup::Stable, rules::flake8_simplify::rules::ExprAndFalse), - (Flake8Simplify, "300") => (RuleGroup::Stable, rules::flake8_simplify::rules::YodaConditions), - (Flake8Simplify, "401") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictGet), - (Flake8Simplify, "905") => (RuleGroup::Stable, rules::flake8_simplify::rules::SplitStaticString), - (Flake8Simplify, "910") => (RuleGroup::Stable, rules::flake8_simplify::rules::DictGetWithNoneDefault), - (Flake8Simplify, "911") => (RuleGroup::Stable, rules::flake8_simplify::rules::ZipDictKeysAndValues), + (Flake8Simplify, "101") => rules::flake8_simplify::rules::DuplicateIsinstanceCall, + (Flake8Simplify, "102") => rules::flake8_simplify::rules::CollapsibleIf, + (Flake8Simplify, "103") => rules::flake8_simplify::rules::NeedlessBool, + (Flake8Simplify, "105") => rules::flake8_simplify::rules::SuppressibleException, + (Flake8Simplify, "107") => rules::flake8_simplify::rules::ReturnInTryExceptFinally, + (Flake8Simplify, "108") => rules::flake8_simplify::rules::IfElseBlockInsteadOfIfExp, + (Flake8Simplify, "109") => rules::flake8_simplify::rules::CompareWithTuple, + (Flake8Simplify, "110") => rules::flake8_simplify::rules::ReimplementedBuiltin, + (Flake8Simplify, "112") => rules::flake8_simplify::rules::UncapitalizedEnvironmentVariables, + (Flake8Simplify, "113") => rules::flake8_simplify::rules::EnumerateForLoop, + (Flake8Simplify, "114") => rules::flake8_simplify::rules::IfWithSameArms, + (Flake8Simplify, "115") => rules::flake8_simplify::rules::OpenFileWithContextHandler, + (Flake8Simplify, "116") => rules::flake8_simplify::rules::IfElseBlockInsteadOfDictLookup, + (Flake8Simplify, "117") => rules::flake8_simplify::rules::MultipleWithStatements, + (Flake8Simplify, "118") => rules::flake8_simplify::rules::InDictKeys, + (Flake8Simplify, "201") => rules::flake8_simplify::rules::NegateEqualOp, + (Flake8Simplify, "202") => rules::flake8_simplify::rules::NegateNotEqualOp, + (Flake8Simplify, "208") => rules::flake8_simplify::rules::DoubleNegation, + (Flake8Simplify, "210") => rules::flake8_simplify::rules::IfExprWithTrueFalse, + (Flake8Simplify, "211") => rules::flake8_simplify::rules::IfExprWithFalseTrue, + (Flake8Simplify, "212") => rules::flake8_simplify::rules::IfExprWithTwistedArms, + (Flake8Simplify, "220") => rules::flake8_simplify::rules::ExprAndNotExpr, + (Flake8Simplify, "221") => rules::flake8_simplify::rules::ExprOrNotExpr, + (Flake8Simplify, "222") => rules::flake8_simplify::rules::ExprOrTrue, + (Flake8Simplify, "223") => rules::flake8_simplify::rules::ExprAndFalse, + (Flake8Simplify, "300") => rules::flake8_simplify::rules::YodaConditions, + (Flake8Simplify, "401") => rules::flake8_simplify::rules::IfElseBlockInsteadOfDictGet, + (Flake8Simplify, "905") => rules::flake8_simplify::rules::SplitStaticString, + (Flake8Simplify, "910") => rules::flake8_simplify::rules::DictGetWithNoneDefault, + (Flake8Simplify, "911") => rules::flake8_simplify::rules::ZipDictKeysAndValues, // flake8-copyright - (Flake8Copyright, "001") => (RuleGroup::Preview, rules::flake8_copyright::rules::MissingCopyrightNotice), + (Flake8Copyright, "001") => rules::flake8_copyright::rules::MissingCopyrightNotice, // pyupgrade - (Pyupgrade, "001") => (RuleGroup::Stable, rules::pyupgrade::rules::UselessMetaclassType), - (Pyupgrade, "003") => (RuleGroup::Stable, rules::pyupgrade::rules::TypeOfPrimitive), - (Pyupgrade, "004") => (RuleGroup::Stable, rules::pyupgrade::rules::UselessObjectInheritance), - (Pyupgrade, "005") => (RuleGroup::Stable, rules::pyupgrade::rules::DeprecatedUnittestAlias), - (Pyupgrade, "006") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP585Annotation), - (Pyupgrade, "007") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP604AnnotationUnion), - (Pyupgrade, "008") => (RuleGroup::Stable, rules::pyupgrade::rules::SuperCallWithParameters), - (Pyupgrade, "009") => (RuleGroup::Stable, rules::pyupgrade::rules::UTF8EncodingDeclaration), - (Pyupgrade, "010") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryFutureImport), - (Pyupgrade, "011") => (RuleGroup::Stable, rules::pyupgrade::rules::LRUCacheWithoutParameters), - (Pyupgrade, "012") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryEncodeUTF8), - (Pyupgrade, "013") => (RuleGroup::Stable, rules::pyupgrade::rules::ConvertTypedDictFunctionalToClass), - (Pyupgrade, "014") => (RuleGroup::Stable, rules::pyupgrade::rules::ConvertNamedTupleFunctionalToClass), - (Pyupgrade, "015") => (RuleGroup::Stable, rules::pyupgrade::rules::RedundantOpenModes), - (Pyupgrade, "017") => (RuleGroup::Stable, rules::pyupgrade::rules::DatetimeTimezoneUTC), - (Pyupgrade, "018") => (RuleGroup::Stable, rules::pyupgrade::rules::NativeLiterals), - (Pyupgrade, "019") => (RuleGroup::Stable, rules::pyupgrade::rules::TypingTextStrAlias), - (Pyupgrade, "020") => (RuleGroup::Stable, rules::pyupgrade::rules::OpenAlias), - (Pyupgrade, "021") => (RuleGroup::Stable, rules::pyupgrade::rules::ReplaceUniversalNewlines), - (Pyupgrade, "022") => (RuleGroup::Stable, rules::pyupgrade::rules::ReplaceStdoutStderr), - (Pyupgrade, "023") => (RuleGroup::Stable, rules::pyupgrade::rules::DeprecatedCElementTree), - (Pyupgrade, "024") => (RuleGroup::Stable, rules::pyupgrade::rules::OSErrorAlias), - (Pyupgrade, "025") => (RuleGroup::Stable, rules::pyupgrade::rules::UnicodeKindPrefix), - (Pyupgrade, "026") => (RuleGroup::Stable, rules::pyupgrade::rules::DeprecatedMockImport), - (Pyupgrade, "027") => (RuleGroup::Removed, rules::pyupgrade::rules::UnpackedListComprehension), - (Pyupgrade, "028") => (RuleGroup::Stable, rules::pyupgrade::rules::YieldInForLoop), - (Pyupgrade, "029") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryBuiltinImport), - (Pyupgrade, "030") => (RuleGroup::Stable, rules::pyupgrade::rules::FormatLiterals), - (Pyupgrade, "031") => (RuleGroup::Stable, rules::pyupgrade::rules::PrintfStringFormatting), - (Pyupgrade, "032") => (RuleGroup::Stable, rules::pyupgrade::rules::FString), - (Pyupgrade, "033") => (RuleGroup::Stable, rules::pyupgrade::rules::LRUCacheWithMaxsizeNone), - (Pyupgrade, "034") => (RuleGroup::Stable, rules::pyupgrade::rules::ExtraneousParentheses), - (Pyupgrade, "035") => (RuleGroup::Stable, rules::pyupgrade::rules::DeprecatedImport), - (Pyupgrade, "036") => (RuleGroup::Stable, rules::pyupgrade::rules::OutdatedVersionBlock), - (Pyupgrade, "037") => (RuleGroup::Stable, rules::pyupgrade::rules::QuotedAnnotation), - (Pyupgrade, "038") => (RuleGroup::Removed, rules::pyupgrade::rules::NonPEP604Isinstance), - (Pyupgrade, "039") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryClassParentheses), - (Pyupgrade, "040") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695TypeAlias), - (Pyupgrade, "041") => (RuleGroup::Stable, rules::pyupgrade::rules::TimeoutErrorAlias), - (Pyupgrade, "042") => (RuleGroup::Preview, rules::pyupgrade::rules::ReplaceStrEnum), - (Pyupgrade, "043") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryDefaultTypeArgs), - (Pyupgrade, "044") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP646Unpack), - (Pyupgrade, "045") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP604AnnotationOptional), - (Pyupgrade, "046") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695GenericClass), - (Pyupgrade, "047") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695GenericFunction), - (Pyupgrade, "049") => (RuleGroup::Stable, rules::pyupgrade::rules::PrivateTypeParameter), - (Pyupgrade, "050") => (RuleGroup::Stable, rules::pyupgrade::rules::UselessClassMetaclassType), + (Pyupgrade, "001") => rules::pyupgrade::rules::UselessMetaclassType, + (Pyupgrade, "003") => rules::pyupgrade::rules::TypeOfPrimitive, + (Pyupgrade, "004") => rules::pyupgrade::rules::UselessObjectInheritance, + (Pyupgrade, "005") => rules::pyupgrade::rules::DeprecatedUnittestAlias, + (Pyupgrade, "006") => rules::pyupgrade::rules::NonPEP585Annotation, + (Pyupgrade, "007") => rules::pyupgrade::rules::NonPEP604AnnotationUnion, + (Pyupgrade, "008") => rules::pyupgrade::rules::SuperCallWithParameters, + (Pyupgrade, "009") => rules::pyupgrade::rules::UTF8EncodingDeclaration, + (Pyupgrade, "010") => rules::pyupgrade::rules::UnnecessaryFutureImport, + (Pyupgrade, "011") => rules::pyupgrade::rules::LRUCacheWithoutParameters, + (Pyupgrade, "012") => rules::pyupgrade::rules::UnnecessaryEncodeUTF8, + (Pyupgrade, "013") => rules::pyupgrade::rules::ConvertTypedDictFunctionalToClass, + (Pyupgrade, "014") => rules::pyupgrade::rules::ConvertNamedTupleFunctionalToClass, + (Pyupgrade, "015") => rules::pyupgrade::rules::RedundantOpenModes, + (Pyupgrade, "017") => rules::pyupgrade::rules::DatetimeTimezoneUTC, + (Pyupgrade, "018") => rules::pyupgrade::rules::NativeLiterals, + (Pyupgrade, "019") => rules::pyupgrade::rules::TypingTextStrAlias, + (Pyupgrade, "020") => rules::pyupgrade::rules::OpenAlias, + (Pyupgrade, "021") => rules::pyupgrade::rules::ReplaceUniversalNewlines, + (Pyupgrade, "022") => rules::pyupgrade::rules::ReplaceStdoutStderr, + (Pyupgrade, "023") => rules::pyupgrade::rules::DeprecatedCElementTree, + (Pyupgrade, "024") => rules::pyupgrade::rules::OSErrorAlias, + (Pyupgrade, "025") => rules::pyupgrade::rules::UnicodeKindPrefix, + (Pyupgrade, "026") => rules::pyupgrade::rules::DeprecatedMockImport, + (Pyupgrade, "027") => rules::pyupgrade::rules::UnpackedListComprehension, + (Pyupgrade, "028") => rules::pyupgrade::rules::YieldInForLoop, + (Pyupgrade, "029") => rules::pyupgrade::rules::UnnecessaryBuiltinImport, + (Pyupgrade, "030") => rules::pyupgrade::rules::FormatLiterals, + (Pyupgrade, "031") => rules::pyupgrade::rules::PrintfStringFormatting, + (Pyupgrade, "032") => rules::pyupgrade::rules::FString, + (Pyupgrade, "033") => rules::pyupgrade::rules::LRUCacheWithMaxsizeNone, + (Pyupgrade, "034") => rules::pyupgrade::rules::ExtraneousParentheses, + (Pyupgrade, "035") => rules::pyupgrade::rules::DeprecatedImport, + (Pyupgrade, "036") => rules::pyupgrade::rules::OutdatedVersionBlock, + (Pyupgrade, "037") => rules::pyupgrade::rules::QuotedAnnotation, + (Pyupgrade, "038") => rules::pyupgrade::rules::NonPEP604Isinstance, + (Pyupgrade, "039") => rules::pyupgrade::rules::UnnecessaryClassParentheses, + (Pyupgrade, "040") => rules::pyupgrade::rules::NonPEP695TypeAlias, + (Pyupgrade, "041") => rules::pyupgrade::rules::TimeoutErrorAlias, + (Pyupgrade, "042") => rules::pyupgrade::rules::ReplaceStrEnum, + (Pyupgrade, "043") => rules::pyupgrade::rules::UnnecessaryDefaultTypeArgs, + (Pyupgrade, "044") => rules::pyupgrade::rules::NonPEP646Unpack, + (Pyupgrade, "045") => rules::pyupgrade::rules::NonPEP604AnnotationOptional, + (Pyupgrade, "046") => rules::pyupgrade::rules::NonPEP695GenericClass, + (Pyupgrade, "047") => rules::pyupgrade::rules::NonPEP695GenericFunction, + (Pyupgrade, "049") => rules::pyupgrade::rules::PrivateTypeParameter, + (Pyupgrade, "050") => rules::pyupgrade::rules::UselessClassMetaclassType, // pydocstyle - (Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule), - (Pydocstyle, "101") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicClass), - (Pydocstyle, "102") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicMethod), - (Pydocstyle, "103") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicFunction), - (Pydocstyle, "104") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicPackage), - (Pydocstyle, "105") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedMagicMethod), - (Pydocstyle, "106") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicNestedClass), - (Pydocstyle, "107") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicInit), - (Pydocstyle, "200") => (RuleGroup::Stable, rules::pydocstyle::rules::UnnecessaryMultilineDocstring), - (Pydocstyle, "201") => (RuleGroup::Stable, rules::pydocstyle::rules::BlankLineBeforeFunction), - (Pydocstyle, "202") => (RuleGroup::Stable, rules::pydocstyle::rules::BlankLineAfterFunction), - (Pydocstyle, "203") => (RuleGroup::Stable, rules::pydocstyle::rules::IncorrectBlankLineBeforeClass), - (Pydocstyle, "204") => (RuleGroup::Stable, rules::pydocstyle::rules::IncorrectBlankLineAfterClass), - (Pydocstyle, "205") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingBlankLineAfterSummary), - (Pydocstyle, "206") => (RuleGroup::Stable, rules::pydocstyle::rules::DocstringTabIndentation), - (Pydocstyle, "207") => (RuleGroup::Stable, rules::pydocstyle::rules::UnderIndentation), - (Pydocstyle, "208") => (RuleGroup::Stable, rules::pydocstyle::rules::OverIndentation), - (Pydocstyle, "209") => (RuleGroup::Stable, rules::pydocstyle::rules::NewLineAfterLastParagraph), - (Pydocstyle, "210") => (RuleGroup::Stable, rules::pydocstyle::rules::SurroundingWhitespace), - (Pydocstyle, "211") => (RuleGroup::Stable, rules::pydocstyle::rules::BlankLineBeforeClass), - (Pydocstyle, "212") => (RuleGroup::Stable, rules::pydocstyle::rules::MultiLineSummaryFirstLine), - (Pydocstyle, "213") => (RuleGroup::Stable, rules::pydocstyle::rules::MultiLineSummarySecondLine), - (Pydocstyle, "214") => (RuleGroup::Stable, rules::pydocstyle::rules::OverindentedSection), - (Pydocstyle, "215") => (RuleGroup::Stable, rules::pydocstyle::rules::OverindentedSectionUnderline), - (Pydocstyle, "300") => (RuleGroup::Stable, rules::pydocstyle::rules::TripleSingleQuotes), - (Pydocstyle, "301") => (RuleGroup::Stable, rules::pydocstyle::rules::EscapeSequenceInDocstring), - (Pydocstyle, "400") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingTrailingPeriod), - (Pydocstyle, "401") => (RuleGroup::Stable, rules::pydocstyle::rules::NonImperativeMood), - (Pydocstyle, "402") => (RuleGroup::Stable, rules::pydocstyle::rules::SignatureInDocstring), - (Pydocstyle, "403") => (RuleGroup::Stable, rules::pydocstyle::rules::FirstWordUncapitalized), - (Pydocstyle, "404") => (RuleGroup::Stable, rules::pydocstyle::rules::DocstringStartsWithThis), - (Pydocstyle, "405") => (RuleGroup::Stable, rules::pydocstyle::rules::NonCapitalizedSectionName), - (Pydocstyle, "406") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingNewLineAfterSectionName), - (Pydocstyle, "407") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingDashedUnderlineAfterSection), - (Pydocstyle, "408") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingSectionUnderlineAfterName), - (Pydocstyle, "409") => (RuleGroup::Stable, rules::pydocstyle::rules::MismatchedSectionUnderlineLength), - (Pydocstyle, "410") => (RuleGroup::Stable, rules::pydocstyle::rules::NoBlankLineAfterSection), - (Pydocstyle, "411") => (RuleGroup::Stable, rules::pydocstyle::rules::NoBlankLineBeforeSection), - (Pydocstyle, "412") => (RuleGroup::Stable, rules::pydocstyle::rules::BlankLinesBetweenHeaderAndContent), - (Pydocstyle, "413") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingBlankLineAfterLastSection), - (Pydocstyle, "414") => (RuleGroup::Stable, rules::pydocstyle::rules::EmptyDocstringSection), - (Pydocstyle, "415") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingTerminalPunctuation), - (Pydocstyle, "416") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingSectionNameColon), - (Pydocstyle, "417") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedParam), - (Pydocstyle, "418") => (RuleGroup::Stable, rules::pydocstyle::rules::OverloadWithDocstring), - (Pydocstyle, "419") => (RuleGroup::Stable, rules::pydocstyle::rules::EmptyDocstring), + (Pydocstyle, "100") => rules::pydocstyle::rules::UndocumentedPublicModule, + (Pydocstyle, "101") => rules::pydocstyle::rules::UndocumentedPublicClass, + (Pydocstyle, "102") => rules::pydocstyle::rules::UndocumentedPublicMethod, + (Pydocstyle, "103") => rules::pydocstyle::rules::UndocumentedPublicFunction, + (Pydocstyle, "104") => rules::pydocstyle::rules::UndocumentedPublicPackage, + (Pydocstyle, "105") => rules::pydocstyle::rules::UndocumentedMagicMethod, + (Pydocstyle, "106") => rules::pydocstyle::rules::UndocumentedPublicNestedClass, + (Pydocstyle, "107") => rules::pydocstyle::rules::UndocumentedPublicInit, + (Pydocstyle, "200") => rules::pydocstyle::rules::UnnecessaryMultilineDocstring, + (Pydocstyle, "201") => rules::pydocstyle::rules::BlankLineBeforeFunction, + (Pydocstyle, "202") => rules::pydocstyle::rules::BlankLineAfterFunction, + (Pydocstyle, "203") => rules::pydocstyle::rules::IncorrectBlankLineBeforeClass, + (Pydocstyle, "204") => rules::pydocstyle::rules::IncorrectBlankLineAfterClass, + (Pydocstyle, "205") => rules::pydocstyle::rules::MissingBlankLineAfterSummary, + (Pydocstyle, "206") => rules::pydocstyle::rules::DocstringTabIndentation, + (Pydocstyle, "207") => rules::pydocstyle::rules::UnderIndentation, + (Pydocstyle, "208") => rules::pydocstyle::rules::OverIndentation, + (Pydocstyle, "209") => rules::pydocstyle::rules::NewLineAfterLastParagraph, + (Pydocstyle, "210") => rules::pydocstyle::rules::SurroundingWhitespace, + (Pydocstyle, "211") => rules::pydocstyle::rules::BlankLineBeforeClass, + (Pydocstyle, "212") => rules::pydocstyle::rules::MultiLineSummaryFirstLine, + (Pydocstyle, "213") => rules::pydocstyle::rules::MultiLineSummarySecondLine, + (Pydocstyle, "214") => rules::pydocstyle::rules::OverindentedSection, + (Pydocstyle, "215") => rules::pydocstyle::rules::OverindentedSectionUnderline, + (Pydocstyle, "300") => rules::pydocstyle::rules::TripleSingleQuotes, + (Pydocstyle, "301") => rules::pydocstyle::rules::EscapeSequenceInDocstring, + (Pydocstyle, "400") => rules::pydocstyle::rules::MissingTrailingPeriod, + (Pydocstyle, "401") => rules::pydocstyle::rules::NonImperativeMood, + (Pydocstyle, "402") => rules::pydocstyle::rules::SignatureInDocstring, + (Pydocstyle, "403") => rules::pydocstyle::rules::FirstWordUncapitalized, + (Pydocstyle, "404") => rules::pydocstyle::rules::DocstringStartsWithThis, + (Pydocstyle, "405") => rules::pydocstyle::rules::NonCapitalizedSectionName, + (Pydocstyle, "406") => rules::pydocstyle::rules::MissingNewLineAfterSectionName, + (Pydocstyle, "407") => rules::pydocstyle::rules::MissingDashedUnderlineAfterSection, + (Pydocstyle, "408") => rules::pydocstyle::rules::MissingSectionUnderlineAfterName, + (Pydocstyle, "409") => rules::pydocstyle::rules::MismatchedSectionUnderlineLength, + (Pydocstyle, "410") => rules::pydocstyle::rules::NoBlankLineAfterSection, + (Pydocstyle, "411") => rules::pydocstyle::rules::NoBlankLineBeforeSection, + (Pydocstyle, "412") => rules::pydocstyle::rules::BlankLinesBetweenHeaderAndContent, + (Pydocstyle, "413") => rules::pydocstyle::rules::MissingBlankLineAfterLastSection, + (Pydocstyle, "414") => rules::pydocstyle::rules::EmptyDocstringSection, + (Pydocstyle, "415") => rules::pydocstyle::rules::MissingTerminalPunctuation, + (Pydocstyle, "416") => rules::pydocstyle::rules::MissingSectionNameColon, + (Pydocstyle, "417") => rules::pydocstyle::rules::UndocumentedParam, + (Pydocstyle, "418") => rules::pydocstyle::rules::OverloadWithDocstring, + (Pydocstyle, "419") => rules::pydocstyle::rules::EmptyDocstring, // pep8-naming - (PEP8Naming, "801") => (RuleGroup::Stable, rules::pep8_naming::rules::InvalidClassName), - (PEP8Naming, "802") => (RuleGroup::Stable, rules::pep8_naming::rules::InvalidFunctionName), - (PEP8Naming, "803") => (RuleGroup::Stable, rules::pep8_naming::rules::InvalidArgumentName), - (PEP8Naming, "804") => (RuleGroup::Stable, rules::pep8_naming::rules::InvalidFirstArgumentNameForClassMethod), - (PEP8Naming, "805") => (RuleGroup::Stable, rules::pep8_naming::rules::InvalidFirstArgumentNameForMethod), - (PEP8Naming, "806") => (RuleGroup::Stable, rules::pep8_naming::rules::NonLowercaseVariableInFunction), - (PEP8Naming, "807") => (RuleGroup::Stable, rules::pep8_naming::rules::DunderFunctionName), - (PEP8Naming, "811") => (RuleGroup::Stable, rules::pep8_naming::rules::ConstantImportedAsNonConstant), - (PEP8Naming, "812") => (RuleGroup::Stable, rules::pep8_naming::rules::LowercaseImportedAsNonLowercase), - (PEP8Naming, "813") => (RuleGroup::Stable, rules::pep8_naming::rules::CamelcaseImportedAsLowercase), - (PEP8Naming, "814") => (RuleGroup::Stable, rules::pep8_naming::rules::CamelcaseImportedAsConstant), - (PEP8Naming, "815") => (RuleGroup::Stable, rules::pep8_naming::rules::MixedCaseVariableInClassScope), - (PEP8Naming, "816") => (RuleGroup::Stable, rules::pep8_naming::rules::MixedCaseVariableInGlobalScope), - (PEP8Naming, "817") => (RuleGroup::Stable, rules::pep8_naming::rules::CamelcaseImportedAsAcronym), - (PEP8Naming, "818") => (RuleGroup::Stable, rules::pep8_naming::rules::ErrorSuffixOnExceptionName), - (PEP8Naming, "999") => (RuleGroup::Stable, rules::pep8_naming::rules::InvalidModuleName), + (PEP8Naming, "801") => rules::pep8_naming::rules::InvalidClassName, + (PEP8Naming, "802") => rules::pep8_naming::rules::InvalidFunctionName, + (PEP8Naming, "803") => rules::pep8_naming::rules::InvalidArgumentName, + (PEP8Naming, "804") => rules::pep8_naming::rules::InvalidFirstArgumentNameForClassMethod, + (PEP8Naming, "805") => rules::pep8_naming::rules::InvalidFirstArgumentNameForMethod, + (PEP8Naming, "806") => rules::pep8_naming::rules::NonLowercaseVariableInFunction, + (PEP8Naming, "807") => rules::pep8_naming::rules::DunderFunctionName, + (PEP8Naming, "811") => rules::pep8_naming::rules::ConstantImportedAsNonConstant, + (PEP8Naming, "812") => rules::pep8_naming::rules::LowercaseImportedAsNonLowercase, + (PEP8Naming, "813") => rules::pep8_naming::rules::CamelcaseImportedAsLowercase, + (PEP8Naming, "814") => rules::pep8_naming::rules::CamelcaseImportedAsConstant, + (PEP8Naming, "815") => rules::pep8_naming::rules::MixedCaseVariableInClassScope, + (PEP8Naming, "816") => rules::pep8_naming::rules::MixedCaseVariableInGlobalScope, + (PEP8Naming, "817") => rules::pep8_naming::rules::CamelcaseImportedAsAcronym, + (PEP8Naming, "818") => rules::pep8_naming::rules::ErrorSuffixOnExceptionName, + (PEP8Naming, "999") => rules::pep8_naming::rules::InvalidModuleName, // isort - (Isort, "001") => (RuleGroup::Stable, rules::isort::rules::UnsortedImports), - (Isort, "002") => (RuleGroup::Stable, rules::isort::rules::MissingRequiredImport), + (Isort, "001") => rules::isort::rules::UnsortedImports, + (Isort, "002") => rules::isort::rules::MissingRequiredImport, // eradicate - (Eradicate, "001") => (RuleGroup::Stable, rules::eradicate::rules::CommentedOutCode), + (Eradicate, "001") => rules::eradicate::rules::CommentedOutCode, // flake8-bandit - (Flake8Bandit, "101") => (RuleGroup::Stable, rules::flake8_bandit::rules::Assert), - (Flake8Bandit, "102") => (RuleGroup::Stable, rules::flake8_bandit::rules::ExecBuiltin), - (Flake8Bandit, "103") => (RuleGroup::Stable, rules::flake8_bandit::rules::BadFilePermissions), - (Flake8Bandit, "104") => (RuleGroup::Stable, rules::flake8_bandit::rules::HardcodedBindAllInterfaces), - (Flake8Bandit, "105") => (RuleGroup::Stable, rules::flake8_bandit::rules::HardcodedPasswordString), - (Flake8Bandit, "106") => (RuleGroup::Stable, rules::flake8_bandit::rules::HardcodedPasswordFuncArg), - (Flake8Bandit, "107") => (RuleGroup::Stable, rules::flake8_bandit::rules::HardcodedPasswordDefault), - (Flake8Bandit, "108") => (RuleGroup::Stable, rules::flake8_bandit::rules::HardcodedTempFile), - (Flake8Bandit, "110") => (RuleGroup::Stable, rules::flake8_bandit::rules::TryExceptPass), - (Flake8Bandit, "112") => (RuleGroup::Stable, rules::flake8_bandit::rules::TryExceptContinue), - (Flake8Bandit, "113") => (RuleGroup::Stable, rules::flake8_bandit::rules::RequestWithoutTimeout), - (Flake8Bandit, "201") => (RuleGroup::Stable, rules::flake8_bandit::rules::FlaskDebugTrue), - (Flake8Bandit, "202") => (RuleGroup::Stable, rules::flake8_bandit::rules::TarfileUnsafeMembers), - (Flake8Bandit, "301") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousPickleUsage), - (Flake8Bandit, "302") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousMarshalUsage), - (Flake8Bandit, "303") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousInsecureHashUsage), - (Flake8Bandit, "304") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousInsecureCipherUsage), - (Flake8Bandit, "305") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousInsecureCipherModeUsage), - (Flake8Bandit, "306") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousMktempUsage), - (Flake8Bandit, "307") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousEvalUsage), - (Flake8Bandit, "308") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousMarkSafeUsage), - (Flake8Bandit, "310") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousURLOpenUsage), - (Flake8Bandit, "311") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousNonCryptographicRandomUsage), - (Flake8Bandit, "312") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousTelnetUsage), - (Flake8Bandit, "313") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousXMLCElementTreeUsage), - (Flake8Bandit, "314") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousXMLElementTreeUsage), - (Flake8Bandit, "315") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousXMLExpatReaderUsage), - (Flake8Bandit, "316") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousXMLExpatBuilderUsage), - (Flake8Bandit, "317") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousXMLSaxUsage), - (Flake8Bandit, "318") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousXMLMiniDOMUsage), - (Flake8Bandit, "319") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousXMLPullDOMUsage), - (Flake8Bandit, "320") => (RuleGroup::Removed, rules::flake8_bandit::rules::SuspiciousXMLETreeUsage), - (Flake8Bandit, "321") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousFTPLibUsage), - (Flake8Bandit, "323") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousUnverifiedContextUsage), - (Flake8Bandit, "324") => (RuleGroup::Stable, rules::flake8_bandit::rules::HashlibInsecureHashFunction), - (Flake8Bandit, "401") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousTelnetlibImport), - (Flake8Bandit, "402") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousFtplibImport), - (Flake8Bandit, "403") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPickleImport), - (Flake8Bandit, "404") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousSubprocessImport), - (Flake8Bandit, "405") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlEtreeImport), - (Flake8Bandit, "406") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlSaxImport), - (Flake8Bandit, "407") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlExpatImport), - (Flake8Bandit, "408") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlMinidomImport), - (Flake8Bandit, "409") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlPulldomImport), - (Flake8Bandit, "410") => (RuleGroup::Removed, rules::flake8_bandit::rules::SuspiciousLxmlImport), - (Flake8Bandit, "411") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousXmlrpcImport), - (Flake8Bandit, "412") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousHttpoxyImport), - (Flake8Bandit, "413") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPycryptoImport), - (Flake8Bandit, "415") => (RuleGroup::Preview, rules::flake8_bandit::rules::SuspiciousPyghmiImport), - (Flake8Bandit, "501") => (RuleGroup::Stable, rules::flake8_bandit::rules::RequestWithNoCertValidation), - (Flake8Bandit, "502") => (RuleGroup::Stable, rules::flake8_bandit::rules::SslInsecureVersion), - (Flake8Bandit, "503") => (RuleGroup::Stable, rules::flake8_bandit::rules::SslWithBadDefaults), - (Flake8Bandit, "504") => (RuleGroup::Stable, rules::flake8_bandit::rules::SslWithNoVersion), - (Flake8Bandit, "505") => (RuleGroup::Stable, rules::flake8_bandit::rules::WeakCryptographicKey), - (Flake8Bandit, "506") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnsafeYAMLLoad), - (Flake8Bandit, "507") => (RuleGroup::Stable, rules::flake8_bandit::rules::SSHNoHostKeyVerification), - (Flake8Bandit, "508") => (RuleGroup::Stable, rules::flake8_bandit::rules::SnmpInsecureVersion), - (Flake8Bandit, "509") => (RuleGroup::Stable, rules::flake8_bandit::rules::SnmpWeakCryptography), - (Flake8Bandit, "601") => (RuleGroup::Stable, rules::flake8_bandit::rules::ParamikoCall), - (Flake8Bandit, "602") => (RuleGroup::Stable, rules::flake8_bandit::rules::SubprocessPopenWithShellEqualsTrue), - (Flake8Bandit, "603") => (RuleGroup::Stable, rules::flake8_bandit::rules::SubprocessWithoutShellEqualsTrue), - (Flake8Bandit, "604") => (RuleGroup::Stable, rules::flake8_bandit::rules::CallWithShellEqualsTrue), - (Flake8Bandit, "605") => (RuleGroup::Stable, rules::flake8_bandit::rules::StartProcessWithAShell), - (Flake8Bandit, "606") => (RuleGroup::Stable, rules::flake8_bandit::rules::StartProcessWithNoShell), - (Flake8Bandit, "607") => (RuleGroup::Stable, rules::flake8_bandit::rules::StartProcessWithPartialPath), - (Flake8Bandit, "608") => (RuleGroup::Stable, rules::flake8_bandit::rules::HardcodedSQLExpression), - (Flake8Bandit, "609") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnixCommandWildcardInjection), - (Flake8Bandit, "610") => (RuleGroup::Stable, rules::flake8_bandit::rules::DjangoExtra), - (Flake8Bandit, "611") => (RuleGroup::Stable, rules::flake8_bandit::rules::DjangoRawSql), - (Flake8Bandit, "612") => (RuleGroup::Stable, rules::flake8_bandit::rules::LoggingConfigInsecureListen), - (Flake8Bandit, "701") => (RuleGroup::Stable, rules::flake8_bandit::rules::Jinja2AutoescapeFalse), - (Flake8Bandit, "702") => (RuleGroup::Stable, rules::flake8_bandit::rules::MakoTemplates), - (Flake8Bandit, "704") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnsafeMarkupUse), + (Flake8Bandit, "101") => rules::flake8_bandit::rules::Assert, + (Flake8Bandit, "102") => rules::flake8_bandit::rules::ExecBuiltin, + (Flake8Bandit, "103") => rules::flake8_bandit::rules::BadFilePermissions, + (Flake8Bandit, "104") => rules::flake8_bandit::rules::HardcodedBindAllInterfaces, + (Flake8Bandit, "105") => rules::flake8_bandit::rules::HardcodedPasswordString, + (Flake8Bandit, "106") => rules::flake8_bandit::rules::HardcodedPasswordFuncArg, + (Flake8Bandit, "107") => rules::flake8_bandit::rules::HardcodedPasswordDefault, + (Flake8Bandit, "108") => rules::flake8_bandit::rules::HardcodedTempFile, + (Flake8Bandit, "110") => rules::flake8_bandit::rules::TryExceptPass, + (Flake8Bandit, "112") => rules::flake8_bandit::rules::TryExceptContinue, + (Flake8Bandit, "113") => rules::flake8_bandit::rules::RequestWithoutTimeout, + (Flake8Bandit, "201") => rules::flake8_bandit::rules::FlaskDebugTrue, + (Flake8Bandit, "202") => rules::flake8_bandit::rules::TarfileUnsafeMembers, + (Flake8Bandit, "301") => rules::flake8_bandit::rules::SuspiciousPickleUsage, + (Flake8Bandit, "302") => rules::flake8_bandit::rules::SuspiciousMarshalUsage, + (Flake8Bandit, "303") => rules::flake8_bandit::rules::SuspiciousInsecureHashUsage, + (Flake8Bandit, "304") => rules::flake8_bandit::rules::SuspiciousInsecureCipherUsage, + (Flake8Bandit, "305") => rules::flake8_bandit::rules::SuspiciousInsecureCipherModeUsage, + (Flake8Bandit, "306") => rules::flake8_bandit::rules::SuspiciousMktempUsage, + (Flake8Bandit, "307") => rules::flake8_bandit::rules::SuspiciousEvalUsage, + (Flake8Bandit, "308") => rules::flake8_bandit::rules::SuspiciousMarkSafeUsage, + (Flake8Bandit, "310") => rules::flake8_bandit::rules::SuspiciousURLOpenUsage, + (Flake8Bandit, "311") => rules::flake8_bandit::rules::SuspiciousNonCryptographicRandomUsage, + (Flake8Bandit, "312") => rules::flake8_bandit::rules::SuspiciousTelnetUsage, + (Flake8Bandit, "313") => rules::flake8_bandit::rules::SuspiciousXMLCElementTreeUsage, + (Flake8Bandit, "314") => rules::flake8_bandit::rules::SuspiciousXMLElementTreeUsage, + (Flake8Bandit, "315") => rules::flake8_bandit::rules::SuspiciousXMLExpatReaderUsage, + (Flake8Bandit, "316") => rules::flake8_bandit::rules::SuspiciousXMLExpatBuilderUsage, + (Flake8Bandit, "317") => rules::flake8_bandit::rules::SuspiciousXMLSaxUsage, + (Flake8Bandit, "318") => rules::flake8_bandit::rules::SuspiciousXMLMiniDOMUsage, + (Flake8Bandit, "319") => rules::flake8_bandit::rules::SuspiciousXMLPullDOMUsage, + (Flake8Bandit, "320") => rules::flake8_bandit::rules::SuspiciousXMLETreeUsage, + (Flake8Bandit, "321") => rules::flake8_bandit::rules::SuspiciousFTPLibUsage, + (Flake8Bandit, "323") => rules::flake8_bandit::rules::SuspiciousUnverifiedContextUsage, + (Flake8Bandit, "324") => rules::flake8_bandit::rules::HashlibInsecureHashFunction, + (Flake8Bandit, "401") => rules::flake8_bandit::rules::SuspiciousTelnetlibImport, + (Flake8Bandit, "402") => rules::flake8_bandit::rules::SuspiciousFtplibImport, + (Flake8Bandit, "403") => rules::flake8_bandit::rules::SuspiciousPickleImport, + (Flake8Bandit, "404") => rules::flake8_bandit::rules::SuspiciousSubprocessImport, + (Flake8Bandit, "405") => rules::flake8_bandit::rules::SuspiciousXmlEtreeImport, + (Flake8Bandit, "406") => rules::flake8_bandit::rules::SuspiciousXmlSaxImport, + (Flake8Bandit, "407") => rules::flake8_bandit::rules::SuspiciousXmlExpatImport, + (Flake8Bandit, "408") => rules::flake8_bandit::rules::SuspiciousXmlMinidomImport, + (Flake8Bandit, "409") => rules::flake8_bandit::rules::SuspiciousXmlPulldomImport, + (Flake8Bandit, "410") => rules::flake8_bandit::rules::SuspiciousLxmlImport, + (Flake8Bandit, "411") => rules::flake8_bandit::rules::SuspiciousXmlrpcImport, + (Flake8Bandit, "412") => rules::flake8_bandit::rules::SuspiciousHttpoxyImport, + (Flake8Bandit, "413") => rules::flake8_bandit::rules::SuspiciousPycryptoImport, + (Flake8Bandit, "415") => rules::flake8_bandit::rules::SuspiciousPyghmiImport, + (Flake8Bandit, "501") => rules::flake8_bandit::rules::RequestWithNoCertValidation, + (Flake8Bandit, "502") => rules::flake8_bandit::rules::SslInsecureVersion, + (Flake8Bandit, "503") => rules::flake8_bandit::rules::SslWithBadDefaults, + (Flake8Bandit, "504") => rules::flake8_bandit::rules::SslWithNoVersion, + (Flake8Bandit, "505") => rules::flake8_bandit::rules::WeakCryptographicKey, + (Flake8Bandit, "506") => rules::flake8_bandit::rules::UnsafeYAMLLoad, + (Flake8Bandit, "507") => rules::flake8_bandit::rules::SSHNoHostKeyVerification, + (Flake8Bandit, "508") => rules::flake8_bandit::rules::SnmpInsecureVersion, + (Flake8Bandit, "509") => rules::flake8_bandit::rules::SnmpWeakCryptography, + (Flake8Bandit, "601") => rules::flake8_bandit::rules::ParamikoCall, + (Flake8Bandit, "602") => rules::flake8_bandit::rules::SubprocessPopenWithShellEqualsTrue, + (Flake8Bandit, "603") => rules::flake8_bandit::rules::SubprocessWithoutShellEqualsTrue, + (Flake8Bandit, "604") => rules::flake8_bandit::rules::CallWithShellEqualsTrue, + (Flake8Bandit, "605") => rules::flake8_bandit::rules::StartProcessWithAShell, + (Flake8Bandit, "606") => rules::flake8_bandit::rules::StartProcessWithNoShell, + (Flake8Bandit, "607") => rules::flake8_bandit::rules::StartProcessWithPartialPath, + (Flake8Bandit, "608") => rules::flake8_bandit::rules::HardcodedSQLExpression, + (Flake8Bandit, "609") => rules::flake8_bandit::rules::UnixCommandWildcardInjection, + (Flake8Bandit, "610") => rules::flake8_bandit::rules::DjangoExtra, + (Flake8Bandit, "611") => rules::flake8_bandit::rules::DjangoRawSql, + (Flake8Bandit, "612") => rules::flake8_bandit::rules::LoggingConfigInsecureListen, + (Flake8Bandit, "701") => rules::flake8_bandit::rules::Jinja2AutoescapeFalse, + (Flake8Bandit, "702") => rules::flake8_bandit::rules::MakoTemplates, + (Flake8Bandit, "704") => rules::flake8_bandit::rules::UnsafeMarkupUse, // flake8-boolean-trap - (Flake8BooleanTrap, "001") => (RuleGroup::Stable, rules::flake8_boolean_trap::rules::BooleanTypeHintPositionalArgument), - (Flake8BooleanTrap, "002") => (RuleGroup::Stable, rules::flake8_boolean_trap::rules::BooleanDefaultValuePositionalArgument), - (Flake8BooleanTrap, "003") => (RuleGroup::Stable, rules::flake8_boolean_trap::rules::BooleanPositionalValueInCall), + (Flake8BooleanTrap, "001") => rules::flake8_boolean_trap::rules::BooleanTypeHintPositionalArgument, + (Flake8BooleanTrap, "002") => rules::flake8_boolean_trap::rules::BooleanDefaultValuePositionalArgument, + (Flake8BooleanTrap, "003") => rules::flake8_boolean_trap::rules::BooleanPositionalValueInCall, // flake8-unused-arguments - (Flake8UnusedArguments, "001") => (RuleGroup::Stable, rules::flake8_unused_arguments::rules::UnusedFunctionArgument), - (Flake8UnusedArguments, "002") => (RuleGroup::Stable, rules::flake8_unused_arguments::rules::UnusedMethodArgument), - (Flake8UnusedArguments, "003") => (RuleGroup::Stable, rules::flake8_unused_arguments::rules::UnusedClassMethodArgument), - (Flake8UnusedArguments, "004") => (RuleGroup::Stable, rules::flake8_unused_arguments::rules::UnusedStaticMethodArgument), - (Flake8UnusedArguments, "005") => (RuleGroup::Stable, rules::flake8_unused_arguments::rules::UnusedLambdaArgument), + (Flake8UnusedArguments, "001") => rules::flake8_unused_arguments::rules::UnusedFunctionArgument, + (Flake8UnusedArguments, "002") => rules::flake8_unused_arguments::rules::UnusedMethodArgument, + (Flake8UnusedArguments, "003") => rules::flake8_unused_arguments::rules::UnusedClassMethodArgument, + (Flake8UnusedArguments, "004") => rules::flake8_unused_arguments::rules::UnusedStaticMethodArgument, + (Flake8UnusedArguments, "005") => rules::flake8_unused_arguments::rules::UnusedLambdaArgument, // flake8-import-conventions - (Flake8ImportConventions, "001") => (RuleGroup::Stable, rules::flake8_import_conventions::rules::UnconventionalImportAlias), - (Flake8ImportConventions, "002") => (RuleGroup::Stable, rules::flake8_import_conventions::rules::BannedImportAlias), - (Flake8ImportConventions, "003") => (RuleGroup::Stable, rules::flake8_import_conventions::rules::BannedImportFrom), + (Flake8ImportConventions, "001") => rules::flake8_import_conventions::rules::UnconventionalImportAlias, + (Flake8ImportConventions, "002") => rules::flake8_import_conventions::rules::BannedImportAlias, + (Flake8ImportConventions, "003") => rules::flake8_import_conventions::rules::BannedImportFrom, // flake8-datetimez - (Flake8Datetimez, "001") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDatetimeWithoutTzinfo), - (Flake8Datetimez, "002") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDatetimeToday), - (Flake8Datetimez, "003") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDatetimeUtcnow), - (Flake8Datetimez, "004") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDatetimeUtcfromtimestamp), - (Flake8Datetimez, "005") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDatetimeNowWithoutTzinfo), - (Flake8Datetimez, "006") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDatetimeFromtimestamp), - (Flake8Datetimez, "007") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDatetimeStrptimeWithoutZone), - (Flake8Datetimez, "011") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDateToday), - (Flake8Datetimez, "012") => (RuleGroup::Stable, rules::flake8_datetimez::rules::CallDateFromtimestamp), - (Flake8Datetimez, "901") => (RuleGroup::Stable, rules::flake8_datetimez::rules::DatetimeMinMax), + (Flake8Datetimez, "001") => rules::flake8_datetimez::rules::CallDatetimeWithoutTzinfo, + (Flake8Datetimez, "002") => rules::flake8_datetimez::rules::CallDatetimeToday, + (Flake8Datetimez, "003") => rules::flake8_datetimez::rules::CallDatetimeUtcnow, + (Flake8Datetimez, "004") => rules::flake8_datetimez::rules::CallDatetimeUtcfromtimestamp, + (Flake8Datetimez, "005") => rules::flake8_datetimez::rules::CallDatetimeNowWithoutTzinfo, + (Flake8Datetimez, "006") => rules::flake8_datetimez::rules::CallDatetimeFromtimestamp, + (Flake8Datetimez, "007") => rules::flake8_datetimez::rules::CallDatetimeStrptimeWithoutZone, + (Flake8Datetimez, "011") => rules::flake8_datetimez::rules::CallDateToday, + (Flake8Datetimez, "012") => rules::flake8_datetimez::rules::CallDateFromtimestamp, + (Flake8Datetimez, "901") => rules::flake8_datetimez::rules::DatetimeMinMax, // pygrep-hooks - (PygrepHooks, "001") => (RuleGroup::Removed, rules::pygrep_hooks::rules::Eval), - (PygrepHooks, "002") => (RuleGroup::Removed, rules::pygrep_hooks::rules::DeprecatedLogWarn), - (PygrepHooks, "003") => (RuleGroup::Stable, rules::pygrep_hooks::rules::BlanketTypeIgnore), - (PygrepHooks, "004") => (RuleGroup::Stable, rules::pygrep_hooks::rules::BlanketNOQA), - (PygrepHooks, "005") => (RuleGroup::Stable, rules::pygrep_hooks::rules::InvalidMockAccess), + (PygrepHooks, "001") => rules::pygrep_hooks::rules::Eval, + (PygrepHooks, "002") => rules::pygrep_hooks::rules::DeprecatedLogWarn, + (PygrepHooks, "003") => rules::pygrep_hooks::rules::BlanketTypeIgnore, + (PygrepHooks, "004") => rules::pygrep_hooks::rules::BlanketNOQA, + (PygrepHooks, "005") => rules::pygrep_hooks::rules::InvalidMockAccess, // pandas-vet - (PandasVet, "002") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfInplaceArgument), - (PandasVet, "003") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfDotIsNull), - (PandasVet, "004") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfDotNotNull), - (PandasVet, "007") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfDotIx), - (PandasVet, "008") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfDotAt), - (PandasVet, "009") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfDotIat), - (PandasVet, "010") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfDotPivotOrUnstack), - (PandasVet, "011") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfDotValues), - (PandasVet, "012") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfDotReadTable), - (PandasVet, "013") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfDotStack), - (PandasVet, "015") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfPdMerge), - (PandasVet, "101") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasNuniqueConstantSeriesCheck), - (PandasVet, "901") => (RuleGroup::Removed, rules::pandas_vet::rules::PandasDfVariableName), + (PandasVet, "002") => rules::pandas_vet::rules::PandasUseOfInplaceArgument, + (PandasVet, "003") => rules::pandas_vet::rules::PandasUseOfDotIsNull, + (PandasVet, "004") => rules::pandas_vet::rules::PandasUseOfDotNotNull, + (PandasVet, "007") => rules::pandas_vet::rules::PandasUseOfDotIx, + (PandasVet, "008") => rules::pandas_vet::rules::PandasUseOfDotAt, + (PandasVet, "009") => rules::pandas_vet::rules::PandasUseOfDotIat, + (PandasVet, "010") => rules::pandas_vet::rules::PandasUseOfDotPivotOrUnstack, + (PandasVet, "011") => rules::pandas_vet::rules::PandasUseOfDotValues, + (PandasVet, "012") => rules::pandas_vet::rules::PandasUseOfDotReadTable, + (PandasVet, "013") => rules::pandas_vet::rules::PandasUseOfDotStack, + (PandasVet, "015") => rules::pandas_vet::rules::PandasUseOfPdMerge, + (PandasVet, "101") => rules::pandas_vet::rules::PandasNuniqueConstantSeriesCheck, + (PandasVet, "901") => rules::pandas_vet::rules::PandasDfVariableName, // flake8-errmsg - (Flake8ErrMsg, "101") => (RuleGroup::Stable, rules::flake8_errmsg::rules::RawStringInException), - (Flake8ErrMsg, "102") => (RuleGroup::Stable, rules::flake8_errmsg::rules::FStringInException), - (Flake8ErrMsg, "103") => (RuleGroup::Stable, rules::flake8_errmsg::rules::DotFormatInException), + (Flake8ErrMsg, "101") => rules::flake8_errmsg::rules::RawStringInException, + (Flake8ErrMsg, "102") => rules::flake8_errmsg::rules::FStringInException, + (Flake8ErrMsg, "103") => rules::flake8_errmsg::rules::DotFormatInException, // flake8-pyi - (Flake8Pyi, "001") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnprefixedTypeParam), - (Flake8Pyi, "002") => (RuleGroup::Stable, rules::flake8_pyi::rules::ComplexIfStatementInStub), - (Flake8Pyi, "003") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnrecognizedVersionInfoCheck), - (Flake8Pyi, "004") => (RuleGroup::Stable, rules::flake8_pyi::rules::PatchVersionComparison), - (Flake8Pyi, "005") => (RuleGroup::Stable, rules::flake8_pyi::rules::WrongTupleLengthVersionComparison), - (Flake8Pyi, "006") => (RuleGroup::Stable, rules::flake8_pyi::rules::BadVersionInfoComparison), - (Flake8Pyi, "007") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnrecognizedPlatformCheck), - (Flake8Pyi, "008") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnrecognizedPlatformName), - (Flake8Pyi, "009") => (RuleGroup::Stable, rules::flake8_pyi::rules::PassStatementStubBody), - (Flake8Pyi, "010") => (RuleGroup::Stable, rules::flake8_pyi::rules::NonEmptyStubBody), - (Flake8Pyi, "011") => (RuleGroup::Stable, rules::flake8_pyi::rules::TypedArgumentDefaultInStub), - (Flake8Pyi, "012") => (RuleGroup::Stable, rules::flake8_pyi::rules::PassInClassBody), - (Flake8Pyi, "013") => (RuleGroup::Stable, rules::flake8_pyi::rules::EllipsisInNonEmptyClassBody), - (Flake8Pyi, "014") => (RuleGroup::Stable, rules::flake8_pyi::rules::ArgumentDefaultInStub), - (Flake8Pyi, "015") => (RuleGroup::Stable, rules::flake8_pyi::rules::AssignmentDefaultInStub), - (Flake8Pyi, "016") => (RuleGroup::Stable, rules::flake8_pyi::rules::DuplicateUnionMember), - (Flake8Pyi, "017") => (RuleGroup::Stable, rules::flake8_pyi::rules::ComplexAssignmentInStub), - (Flake8Pyi, "018") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnusedPrivateTypeVar), - (Flake8Pyi, "019") => (RuleGroup::Stable, rules::flake8_pyi::rules::CustomTypeVarForSelf), - (Flake8Pyi, "020") => (RuleGroup::Stable, rules::flake8_pyi::rules::QuotedAnnotationInStub), - (Flake8Pyi, "021") => (RuleGroup::Stable, rules::flake8_pyi::rules::DocstringInStub), - (Flake8Pyi, "024") => (RuleGroup::Stable, rules::flake8_pyi::rules::CollectionsNamedTuple), - (Flake8Pyi, "025") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnaliasedCollectionsAbcSetImport), - (Flake8Pyi, "026") => (RuleGroup::Stable, rules::flake8_pyi::rules::TypeAliasWithoutAnnotation), - (Flake8Pyi, "029") => (RuleGroup::Stable, rules::flake8_pyi::rules::StrOrReprDefinedInStub), - (Flake8Pyi, "030") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnnecessaryLiteralUnion), - (Flake8Pyi, "032") => (RuleGroup::Stable, rules::flake8_pyi::rules::AnyEqNeAnnotation), - (Flake8Pyi, "033") => (RuleGroup::Stable, rules::flake8_pyi::rules::TypeCommentInStub), - (Flake8Pyi, "034") => (RuleGroup::Stable, rules::flake8_pyi::rules::NonSelfReturnType), - (Flake8Pyi, "035") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnassignedSpecialVariableInStub), - (Flake8Pyi, "036") => (RuleGroup::Stable, rules::flake8_pyi::rules::BadExitAnnotation), - (Flake8Pyi, "041") => (RuleGroup::Stable, rules::flake8_pyi::rules::RedundantNumericUnion), - (Flake8Pyi, "042") => (RuleGroup::Stable, rules::flake8_pyi::rules::SnakeCaseTypeAlias), - (Flake8Pyi, "043") => (RuleGroup::Stable, rules::flake8_pyi::rules::TSuffixedTypeAlias), - (Flake8Pyi, "044") => (RuleGroup::Stable, rules::flake8_pyi::rules::FutureAnnotationsInStub), - (Flake8Pyi, "045") => (RuleGroup::Stable, rules::flake8_pyi::rules::IterMethodReturnIterable), - (Flake8Pyi, "046") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnusedPrivateProtocol), - (Flake8Pyi, "047") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnusedPrivateTypeAlias), - (Flake8Pyi, "048") => (RuleGroup::Stable, rules::flake8_pyi::rules::StubBodyMultipleStatements), - (Flake8Pyi, "049") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnusedPrivateTypedDict), - (Flake8Pyi, "050") => (RuleGroup::Stable, rules::flake8_pyi::rules::NoReturnArgumentAnnotationInStub), - (Flake8Pyi, "051") => (RuleGroup::Stable, rules::flake8_pyi::rules::RedundantLiteralUnion), - (Flake8Pyi, "052") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnannotatedAssignmentInStub), - (Flake8Pyi, "054") => (RuleGroup::Stable, rules::flake8_pyi::rules::NumericLiteralTooLong), - (Flake8Pyi, "053") => (RuleGroup::Stable, rules::flake8_pyi::rules::StringOrBytesTooLong), - (Flake8Pyi, "055") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnnecessaryTypeUnion), - (Flake8Pyi, "056") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnsupportedMethodCallOnAll), - (Flake8Pyi, "058") => (RuleGroup::Stable, rules::flake8_pyi::rules::GeneratorReturnFromIterMethod), - (Flake8Pyi, "057") => (RuleGroup::Stable, rules::flake8_pyi::rules::ByteStringUsage), - (Flake8Pyi, "059") => (RuleGroup::Stable, rules::flake8_pyi::rules::GenericNotLastBaseClass), - (Flake8Pyi, "061") => (RuleGroup::Stable, rules::flake8_pyi::rules::RedundantNoneLiteral), - (Flake8Pyi, "062") => (RuleGroup::Stable, rules::flake8_pyi::rules::DuplicateLiteralMember), - (Flake8Pyi, "063") => (RuleGroup::Stable, rules::flake8_pyi::rules::Pep484StylePositionalOnlyParameter), - (Flake8Pyi, "064") => (RuleGroup::Stable, rules::flake8_pyi::rules::RedundantFinalLiteral), - (Flake8Pyi, "066") => (RuleGroup::Stable, rules::flake8_pyi::rules::BadVersionInfoOrder), + (Flake8Pyi, "001") => rules::flake8_pyi::rules::UnprefixedTypeParam, + (Flake8Pyi, "002") => rules::flake8_pyi::rules::ComplexIfStatementInStub, + (Flake8Pyi, "003") => rules::flake8_pyi::rules::UnrecognizedVersionInfoCheck, + (Flake8Pyi, "004") => rules::flake8_pyi::rules::PatchVersionComparison, + (Flake8Pyi, "005") => rules::flake8_pyi::rules::WrongTupleLengthVersionComparison, + (Flake8Pyi, "006") => rules::flake8_pyi::rules::BadVersionInfoComparison, + (Flake8Pyi, "007") => rules::flake8_pyi::rules::UnrecognizedPlatformCheck, + (Flake8Pyi, "008") => rules::flake8_pyi::rules::UnrecognizedPlatformName, + (Flake8Pyi, "009") => rules::flake8_pyi::rules::PassStatementStubBody, + (Flake8Pyi, "010") => rules::flake8_pyi::rules::NonEmptyStubBody, + (Flake8Pyi, "011") => rules::flake8_pyi::rules::TypedArgumentDefaultInStub, + (Flake8Pyi, "012") => rules::flake8_pyi::rules::PassInClassBody, + (Flake8Pyi, "013") => rules::flake8_pyi::rules::EllipsisInNonEmptyClassBody, + (Flake8Pyi, "014") => rules::flake8_pyi::rules::ArgumentDefaultInStub, + (Flake8Pyi, "015") => rules::flake8_pyi::rules::AssignmentDefaultInStub, + (Flake8Pyi, "016") => rules::flake8_pyi::rules::DuplicateUnionMember, + (Flake8Pyi, "017") => rules::flake8_pyi::rules::ComplexAssignmentInStub, + (Flake8Pyi, "018") => rules::flake8_pyi::rules::UnusedPrivateTypeVar, + (Flake8Pyi, "019") => rules::flake8_pyi::rules::CustomTypeVarForSelf, + (Flake8Pyi, "020") => rules::flake8_pyi::rules::QuotedAnnotationInStub, + (Flake8Pyi, "021") => rules::flake8_pyi::rules::DocstringInStub, + (Flake8Pyi, "024") => rules::flake8_pyi::rules::CollectionsNamedTuple, + (Flake8Pyi, "025") => rules::flake8_pyi::rules::UnaliasedCollectionsAbcSetImport, + (Flake8Pyi, "026") => rules::flake8_pyi::rules::TypeAliasWithoutAnnotation, + (Flake8Pyi, "029") => rules::flake8_pyi::rules::StrOrReprDefinedInStub, + (Flake8Pyi, "030") => rules::flake8_pyi::rules::UnnecessaryLiteralUnion, + (Flake8Pyi, "032") => rules::flake8_pyi::rules::AnyEqNeAnnotation, + (Flake8Pyi, "033") => rules::flake8_pyi::rules::TypeCommentInStub, + (Flake8Pyi, "034") => rules::flake8_pyi::rules::NonSelfReturnType, + (Flake8Pyi, "035") => rules::flake8_pyi::rules::UnassignedSpecialVariableInStub, + (Flake8Pyi, "036") => rules::flake8_pyi::rules::BadExitAnnotation, + (Flake8Pyi, "041") => rules::flake8_pyi::rules::RedundantNumericUnion, + (Flake8Pyi, "042") => rules::flake8_pyi::rules::SnakeCaseTypeAlias, + (Flake8Pyi, "043") => rules::flake8_pyi::rules::TSuffixedTypeAlias, + (Flake8Pyi, "044") => rules::flake8_pyi::rules::FutureAnnotationsInStub, + (Flake8Pyi, "045") => rules::flake8_pyi::rules::IterMethodReturnIterable, + (Flake8Pyi, "046") => rules::flake8_pyi::rules::UnusedPrivateProtocol, + (Flake8Pyi, "047") => rules::flake8_pyi::rules::UnusedPrivateTypeAlias, + (Flake8Pyi, "048") => rules::flake8_pyi::rules::StubBodyMultipleStatements, + (Flake8Pyi, "049") => rules::flake8_pyi::rules::UnusedPrivateTypedDict, + (Flake8Pyi, "050") => rules::flake8_pyi::rules::NoReturnArgumentAnnotationInStub, + (Flake8Pyi, "051") => rules::flake8_pyi::rules::RedundantLiteralUnion, + (Flake8Pyi, "052") => rules::flake8_pyi::rules::UnannotatedAssignmentInStub, + (Flake8Pyi, "054") => rules::flake8_pyi::rules::NumericLiteralTooLong, + (Flake8Pyi, "053") => rules::flake8_pyi::rules::StringOrBytesTooLong, + (Flake8Pyi, "055") => rules::flake8_pyi::rules::UnnecessaryTypeUnion, + (Flake8Pyi, "056") => rules::flake8_pyi::rules::UnsupportedMethodCallOnAll, + (Flake8Pyi, "058") => rules::flake8_pyi::rules::GeneratorReturnFromIterMethod, + (Flake8Pyi, "057") => rules::flake8_pyi::rules::ByteStringUsage, + (Flake8Pyi, "059") => rules::flake8_pyi::rules::GenericNotLastBaseClass, + (Flake8Pyi, "061") => rules::flake8_pyi::rules::RedundantNoneLiteral, + (Flake8Pyi, "062") => rules::flake8_pyi::rules::DuplicateLiteralMember, + (Flake8Pyi, "063") => rules::flake8_pyi::rules::Pep484StylePositionalOnlyParameter, + (Flake8Pyi, "064") => rules::flake8_pyi::rules::RedundantFinalLiteral, + (Flake8Pyi, "066") => rules::flake8_pyi::rules::BadVersionInfoOrder, // flake8-pytest-style - (Flake8PytestStyle, "001") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle), - (Flake8PytestStyle, "002") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestFixturePositionalArgs), - (Flake8PytestStyle, "003") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestExtraneousScopeFunction), + (Flake8PytestStyle, "001") => rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle, + (Flake8PytestStyle, "002") => rules::flake8_pytest_style::rules::PytestFixturePositionalArgs, + (Flake8PytestStyle, "003") => rules::flake8_pytest_style::rules::PytestExtraneousScopeFunction, #[allow(deprecated)] - (Flake8PytestStyle, "004") => (RuleGroup::Removed, rules::flake8_pytest_style::rules::PytestMissingFixtureNameUnderscore), + (Flake8PytestStyle, "004") => rules::flake8_pytest_style::rules::PytestMissingFixtureNameUnderscore, #[allow(deprecated)] - (Flake8PytestStyle, "005") => (RuleGroup::Removed, rules::flake8_pytest_style::rules::PytestIncorrectFixtureNameUnderscore), - (Flake8PytestStyle, "006") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestParametrizeNamesWrongType), - (Flake8PytestStyle, "007") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestParametrizeValuesWrongType), - (Flake8PytestStyle, "008") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestPatchWithLambda), - (Flake8PytestStyle, "009") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestUnittestAssertion), - (Flake8PytestStyle, "010") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestRaisesWithoutException), - (Flake8PytestStyle, "011") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestRaisesTooBroad), - (Flake8PytestStyle, "012") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestRaisesWithMultipleStatements), - (Flake8PytestStyle, "013") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestIncorrectPytestImport), - (Flake8PytestStyle, "014") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestDuplicateParametrizeTestCases), - (Flake8PytestStyle, "015") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestAssertAlwaysFalse), - (Flake8PytestStyle, "016") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestFailWithoutMessage), - (Flake8PytestStyle, "017") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestAssertInExcept), - (Flake8PytestStyle, "018") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestCompositeAssertion), - (Flake8PytestStyle, "019") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestFixtureParamWithoutValue), - (Flake8PytestStyle, "020") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestDeprecatedYieldFixture), - (Flake8PytestStyle, "021") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestFixtureFinalizerCallback), - (Flake8PytestStyle, "022") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestUselessYieldFixture), - (Flake8PytestStyle, "023") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestIncorrectMarkParenthesesStyle), - (Flake8PytestStyle, "024") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestUnnecessaryAsyncioMarkOnFixture), - (Flake8PytestStyle, "025") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestErroneousUseFixturesOnFixture), - (Flake8PytestStyle, "026") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestUseFixturesWithoutParameters), - (Flake8PytestStyle, "027") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestUnittestRaisesAssertion), - (Flake8PytestStyle, "028") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestParameterWithDefaultArgument), - (Flake8PytestStyle, "029") => (RuleGroup::Preview, rules::flake8_pytest_style::rules::PytestWarnsWithoutWarning), - (Flake8PytestStyle, "030") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestWarnsTooBroad), - (Flake8PytestStyle, "031") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestWarnsWithMultipleStatements), + (Flake8PytestStyle, "005") => rules::flake8_pytest_style::rules::PytestIncorrectFixtureNameUnderscore, + (Flake8PytestStyle, "006") => rules::flake8_pytest_style::rules::PytestParametrizeNamesWrongType, + (Flake8PytestStyle, "007") => rules::flake8_pytest_style::rules::PytestParametrizeValuesWrongType, + (Flake8PytestStyle, "008") => rules::flake8_pytest_style::rules::PytestPatchWithLambda, + (Flake8PytestStyle, "009") => rules::flake8_pytest_style::rules::PytestUnittestAssertion, + (Flake8PytestStyle, "010") => rules::flake8_pytest_style::rules::PytestRaisesWithoutException, + (Flake8PytestStyle, "011") => rules::flake8_pytest_style::rules::PytestRaisesTooBroad, + (Flake8PytestStyle, "012") => rules::flake8_pytest_style::rules::PytestRaisesWithMultipleStatements, + (Flake8PytestStyle, "013") => rules::flake8_pytest_style::rules::PytestIncorrectPytestImport, + (Flake8PytestStyle, "014") => rules::flake8_pytest_style::rules::PytestDuplicateParametrizeTestCases, + (Flake8PytestStyle, "015") => rules::flake8_pytest_style::rules::PytestAssertAlwaysFalse, + (Flake8PytestStyle, "016") => rules::flake8_pytest_style::rules::PytestFailWithoutMessage, + (Flake8PytestStyle, "017") => rules::flake8_pytest_style::rules::PytestAssertInExcept, + (Flake8PytestStyle, "018") => rules::flake8_pytest_style::rules::PytestCompositeAssertion, + (Flake8PytestStyle, "019") => rules::flake8_pytest_style::rules::PytestFixtureParamWithoutValue, + (Flake8PytestStyle, "020") => rules::flake8_pytest_style::rules::PytestDeprecatedYieldFixture, + (Flake8PytestStyle, "021") => rules::flake8_pytest_style::rules::PytestFixtureFinalizerCallback, + (Flake8PytestStyle, "022") => rules::flake8_pytest_style::rules::PytestUselessYieldFixture, + (Flake8PytestStyle, "023") => rules::flake8_pytest_style::rules::PytestIncorrectMarkParenthesesStyle, + (Flake8PytestStyle, "024") => rules::flake8_pytest_style::rules::PytestUnnecessaryAsyncioMarkOnFixture, + (Flake8PytestStyle, "025") => rules::flake8_pytest_style::rules::PytestErroneousUseFixturesOnFixture, + (Flake8PytestStyle, "026") => rules::flake8_pytest_style::rules::PytestUseFixturesWithoutParameters, + (Flake8PytestStyle, "027") => rules::flake8_pytest_style::rules::PytestUnittestRaisesAssertion, + (Flake8PytestStyle, "028") => rules::flake8_pytest_style::rules::PytestParameterWithDefaultArgument, + (Flake8PytestStyle, "029") => rules::flake8_pytest_style::rules::PytestWarnsWithoutWarning, + (Flake8PytestStyle, "030") => rules::flake8_pytest_style::rules::PytestWarnsTooBroad, + (Flake8PytestStyle, "031") => rules::flake8_pytest_style::rules::PytestWarnsWithMultipleStatements, // flake8-pie - (Flake8Pie, "790") => (RuleGroup::Stable, rules::flake8_pie::rules::UnnecessaryPlaceholder), - (Flake8Pie, "794") => (RuleGroup::Stable, rules::flake8_pie::rules::DuplicateClassFieldDefinition), - (Flake8Pie, "796") => (RuleGroup::Stable, rules::flake8_pie::rules::NonUniqueEnums), - (Flake8Pie, "800") => (RuleGroup::Stable, rules::flake8_pie::rules::UnnecessarySpread), - (Flake8Pie, "804") => (RuleGroup::Stable, rules::flake8_pie::rules::UnnecessaryDictKwargs), - (Flake8Pie, "807") => (RuleGroup::Stable, rules::flake8_pie::rules::ReimplementedContainerBuiltin), - (Flake8Pie, "808") => (RuleGroup::Stable, rules::flake8_pie::rules::UnnecessaryRangeStart), - (Flake8Pie, "810") => (RuleGroup::Stable, rules::flake8_pie::rules::MultipleStartsEndsWith), + (Flake8Pie, "790") => rules::flake8_pie::rules::UnnecessaryPlaceholder, + (Flake8Pie, "794") => rules::flake8_pie::rules::DuplicateClassFieldDefinition, + (Flake8Pie, "796") => rules::flake8_pie::rules::NonUniqueEnums, + (Flake8Pie, "800") => rules::flake8_pie::rules::UnnecessarySpread, + (Flake8Pie, "804") => rules::flake8_pie::rules::UnnecessaryDictKwargs, + (Flake8Pie, "807") => rules::flake8_pie::rules::ReimplementedContainerBuiltin, + (Flake8Pie, "808") => rules::flake8_pie::rules::UnnecessaryRangeStart, + (Flake8Pie, "810") => rules::flake8_pie::rules::MultipleStartsEndsWith, // flake8-commas - (Flake8Commas, "812") => (RuleGroup::Stable, rules::flake8_commas::rules::MissingTrailingComma), - (Flake8Commas, "818") => (RuleGroup::Stable, rules::flake8_commas::rules::TrailingCommaOnBareTuple), - (Flake8Commas, "819") => (RuleGroup::Stable, rules::flake8_commas::rules::ProhibitedTrailingComma), + (Flake8Commas, "812") => rules::flake8_commas::rules::MissingTrailingComma, + (Flake8Commas, "818") => rules::flake8_commas::rules::TrailingCommaOnBareTuple, + (Flake8Commas, "819") => rules::flake8_commas::rules::ProhibitedTrailingComma, // flake8-no-pep420 - (Flake8NoPep420, "001") => (RuleGroup::Stable, rules::flake8_no_pep420::rules::ImplicitNamespacePackage), + (Flake8NoPep420, "001") => rules::flake8_no_pep420::rules::ImplicitNamespacePackage, // flake8-executable - (Flake8Executable, "001") => (RuleGroup::Stable, rules::flake8_executable::rules::ShebangNotExecutable), - (Flake8Executable, "002") => (RuleGroup::Stable, rules::flake8_executable::rules::ShebangMissingExecutableFile), - (Flake8Executable, "003") => (RuleGroup::Stable, rules::flake8_executable::rules::ShebangMissingPython), - (Flake8Executable, "004") => (RuleGroup::Stable, rules::flake8_executable::rules::ShebangLeadingWhitespace), - (Flake8Executable, "005") => (RuleGroup::Stable, rules::flake8_executable::rules::ShebangNotFirstLine), + (Flake8Executable, "001") => rules::flake8_executable::rules::ShebangNotExecutable, + (Flake8Executable, "002") => rules::flake8_executable::rules::ShebangMissingExecutableFile, + (Flake8Executable, "003") => rules::flake8_executable::rules::ShebangMissingPython, + (Flake8Executable, "004") => rules::flake8_executable::rules::ShebangLeadingWhitespace, + (Flake8Executable, "005") => rules::flake8_executable::rules::ShebangNotFirstLine, // flake8-type-checking - (Flake8TypeChecking, "001") => (RuleGroup::Stable, rules::flake8_type_checking::rules::TypingOnlyFirstPartyImport), - (Flake8TypeChecking, "002") => (RuleGroup::Stable, rules::flake8_type_checking::rules::TypingOnlyThirdPartyImport), - (Flake8TypeChecking, "003") => (RuleGroup::Stable, rules::flake8_type_checking::rules::TypingOnlyStandardLibraryImport), - (Flake8TypeChecking, "004") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock), - (Flake8TypeChecking, "005") => (RuleGroup::Stable, rules::flake8_type_checking::rules::EmptyTypeCheckingBlock), - (Flake8TypeChecking, "006") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeCastValue), - (Flake8TypeChecking, "007") => (RuleGroup::Stable, rules::flake8_type_checking::rules::UnquotedTypeAlias), - (Flake8TypeChecking, "008") => (RuleGroup::Preview, rules::flake8_type_checking::rules::QuotedTypeAlias), - (Flake8TypeChecking, "010") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeStringUnion), + (Flake8TypeChecking, "001") => rules::flake8_type_checking::rules::TypingOnlyFirstPartyImport, + (Flake8TypeChecking, "002") => rules::flake8_type_checking::rules::TypingOnlyThirdPartyImport, + (Flake8TypeChecking, "003") => rules::flake8_type_checking::rules::TypingOnlyStandardLibraryImport, + (Flake8TypeChecking, "004") => rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock, + (Flake8TypeChecking, "005") => rules::flake8_type_checking::rules::EmptyTypeCheckingBlock, + (Flake8TypeChecking, "006") => rules::flake8_type_checking::rules::RuntimeCastValue, + (Flake8TypeChecking, "007") => rules::flake8_type_checking::rules::UnquotedTypeAlias, + (Flake8TypeChecking, "008") => rules::flake8_type_checking::rules::QuotedTypeAlias, + (Flake8TypeChecking, "010") => rules::flake8_type_checking::rules::RuntimeStringUnion, // tryceratops - (Tryceratops, "002") => (RuleGroup::Stable, rules::tryceratops::rules::RaiseVanillaClass), - (Tryceratops, "003") => (RuleGroup::Stable, rules::tryceratops::rules::RaiseVanillaArgs), - (Tryceratops, "004") => (RuleGroup::Stable, rules::tryceratops::rules::TypeCheckWithoutTypeError), - (Tryceratops, "200") => (RuleGroup::Removed, rules::tryceratops::rules::ReraiseNoCause), - (Tryceratops, "201") => (RuleGroup::Stable, rules::tryceratops::rules::VerboseRaise), - (Tryceratops, "203") => (RuleGroup::Stable, rules::tryceratops::rules::UselessTryExcept), - (Tryceratops, "300") => (RuleGroup::Stable, rules::tryceratops::rules::TryConsiderElse), - (Tryceratops, "301") => (RuleGroup::Stable, rules::tryceratops::rules::RaiseWithinTry), - (Tryceratops, "400") => (RuleGroup::Stable, rules::tryceratops::rules::ErrorInsteadOfException), - (Tryceratops, "401") => (RuleGroup::Stable, rules::tryceratops::rules::VerboseLogMessage), + (Tryceratops, "002") => rules::tryceratops::rules::RaiseVanillaClass, + (Tryceratops, "003") => rules::tryceratops::rules::RaiseVanillaArgs, + (Tryceratops, "004") => rules::tryceratops::rules::TypeCheckWithoutTypeError, + (Tryceratops, "200") => rules::tryceratops::rules::ReraiseNoCause, + (Tryceratops, "201") => rules::tryceratops::rules::VerboseRaise, + (Tryceratops, "203") => rules::tryceratops::rules::UselessTryExcept, + (Tryceratops, "300") => rules::tryceratops::rules::TryConsiderElse, + (Tryceratops, "301") => rules::tryceratops::rules::RaiseWithinTry, + (Tryceratops, "400") => rules::tryceratops::rules::ErrorInsteadOfException, + (Tryceratops, "401") => rules::tryceratops::rules::VerboseLogMessage, // flake8-use-pathlib - (Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathAbspath), - (Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsChmod), - (Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsMkdir), - (Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsMakedirs), - (Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRename), - (Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsReplace), - (Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRmdir), - (Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRemove), - (Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsUnlink), - (Flake8UsePathlib, "109") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsGetcwd), - (Flake8UsePathlib, "110") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExists), - (Flake8UsePathlib, "111") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExpanduser), - (Flake8UsePathlib, "112") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsdir), - (Flake8UsePathlib, "113") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsfile), - (Flake8UsePathlib, "114") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIslink), - (Flake8UsePathlib, "115") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsReadlink), - (Flake8UsePathlib, "116") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsStat), - (Flake8UsePathlib, "117") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsabs), - (Flake8UsePathlib, "118") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathJoin), - (Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathBasename), - (Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname), - (Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathSamefile), - (Flake8UsePathlib, "122") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSplitext), - (Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::BuiltinOpen), - (Flake8UsePathlib, "124") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::PyPath), - (Flake8UsePathlib, "201") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::PathConstructorCurrentDirectory), - (Flake8UsePathlib, "202") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathGetsize), - (Flake8UsePathlib, "202") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathGetsize), - (Flake8UsePathlib, "203") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathGetatime), - (Flake8UsePathlib, "204") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathGetmtime), - (Flake8UsePathlib, "205") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathGetctime), - (Flake8UsePathlib, "206") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsSepSplit), - (Flake8UsePathlib, "207") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::Glob), - (Flake8UsePathlib, "208") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsListdir), - (Flake8UsePathlib, "210") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::InvalidPathlibWithSuffix), - (Flake8UsePathlib, "211") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsSymlink), + (Flake8UsePathlib, "100") => rules::flake8_use_pathlib::rules::OsPathAbspath, + (Flake8UsePathlib, "101") => rules::flake8_use_pathlib::rules::OsChmod, + (Flake8UsePathlib, "102") => rules::flake8_use_pathlib::rules::OsMkdir, + (Flake8UsePathlib, "103") => rules::flake8_use_pathlib::rules::OsMakedirs, + (Flake8UsePathlib, "104") => rules::flake8_use_pathlib::rules::OsRename, + (Flake8UsePathlib, "105") => rules::flake8_use_pathlib::rules::OsReplace, + (Flake8UsePathlib, "106") => rules::flake8_use_pathlib::rules::OsRmdir, + (Flake8UsePathlib, "107") => rules::flake8_use_pathlib::rules::OsRemove, + (Flake8UsePathlib, "108") => rules::flake8_use_pathlib::rules::OsUnlink, + (Flake8UsePathlib, "109") => rules::flake8_use_pathlib::rules::OsGetcwd, + (Flake8UsePathlib, "110") => rules::flake8_use_pathlib::rules::OsPathExists, + (Flake8UsePathlib, "111") => rules::flake8_use_pathlib::rules::OsPathExpanduser, + (Flake8UsePathlib, "112") => rules::flake8_use_pathlib::rules::OsPathIsdir, + (Flake8UsePathlib, "113") => rules::flake8_use_pathlib::rules::OsPathIsfile, + (Flake8UsePathlib, "114") => rules::flake8_use_pathlib::rules::OsPathIslink, + (Flake8UsePathlib, "115") => rules::flake8_use_pathlib::rules::OsReadlink, + (Flake8UsePathlib, "116") => rules::flake8_use_pathlib::violations::OsStat, + (Flake8UsePathlib, "117") => rules::flake8_use_pathlib::rules::OsPathIsabs, + (Flake8UsePathlib, "118") => rules::flake8_use_pathlib::violations::OsPathJoin, + (Flake8UsePathlib, "119") => rules::flake8_use_pathlib::rules::OsPathBasename, + (Flake8UsePathlib, "120") => rules::flake8_use_pathlib::rules::OsPathDirname, + (Flake8UsePathlib, "121") => rules::flake8_use_pathlib::rules::OsPathSamefile, + (Flake8UsePathlib, "122") => rules::flake8_use_pathlib::violations::OsPathSplitext, + (Flake8UsePathlib, "123") => rules::flake8_use_pathlib::rules::BuiltinOpen, + (Flake8UsePathlib, "124") => rules::flake8_use_pathlib::violations::PyPath, + (Flake8UsePathlib, "201") => rules::flake8_use_pathlib::rules::PathConstructorCurrentDirectory, + (Flake8UsePathlib, "202") => rules::flake8_use_pathlib::rules::OsPathGetsize, + (Flake8UsePathlib, "202") => rules::flake8_use_pathlib::rules::OsPathGetsize, + (Flake8UsePathlib, "203") => rules::flake8_use_pathlib::rules::OsPathGetatime, + (Flake8UsePathlib, "204") => rules::flake8_use_pathlib::rules::OsPathGetmtime, + (Flake8UsePathlib, "205") => rules::flake8_use_pathlib::rules::OsPathGetctime, + (Flake8UsePathlib, "206") => rules::flake8_use_pathlib::rules::OsSepSplit, + (Flake8UsePathlib, "207") => rules::flake8_use_pathlib::rules::Glob, + (Flake8UsePathlib, "208") => rules::flake8_use_pathlib::violations::OsListdir, + (Flake8UsePathlib, "210") => rules::flake8_use_pathlib::rules::InvalidPathlibWithSuffix, + (Flake8UsePathlib, "211") => rules::flake8_use_pathlib::rules::OsSymlink, // flake8-logging-format - (Flake8LoggingFormat, "001") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingStringFormat), - (Flake8LoggingFormat, "002") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingPercentFormat), - (Flake8LoggingFormat, "003") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingStringConcat), - (Flake8LoggingFormat, "004") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingFString), - (Flake8LoggingFormat, "010") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingWarn), - (Flake8LoggingFormat, "101") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingExtraAttrClash), - (Flake8LoggingFormat, "201") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingExcInfo), - (Flake8LoggingFormat, "202") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingRedundantExcInfo), + (Flake8LoggingFormat, "001") => rules::flake8_logging_format::violations::LoggingStringFormat, + (Flake8LoggingFormat, "002") => rules::flake8_logging_format::violations::LoggingPercentFormat, + (Flake8LoggingFormat, "003") => rules::flake8_logging_format::violations::LoggingStringConcat, + (Flake8LoggingFormat, "004") => rules::flake8_logging_format::violations::LoggingFString, + (Flake8LoggingFormat, "010") => rules::flake8_logging_format::violations::LoggingWarn, + (Flake8LoggingFormat, "101") => rules::flake8_logging_format::violations::LoggingExtraAttrClash, + (Flake8LoggingFormat, "201") => rules::flake8_logging_format::violations::LoggingExcInfo, + (Flake8LoggingFormat, "202") => rules::flake8_logging_format::violations::LoggingRedundantExcInfo, // flake8-raise - (Flake8Raise, "102") => (RuleGroup::Stable, rules::flake8_raise::rules::UnnecessaryParenOnRaiseException), + (Flake8Raise, "102") => rules::flake8_raise::rules::UnnecessaryParenOnRaiseException, // flake8-self - (Flake8Self, "001") => (RuleGroup::Stable, rules::flake8_self::rules::PrivateMemberAccess), + (Flake8Self, "001") => rules::flake8_self::rules::PrivateMemberAccess, // numpy - (Numpy, "001") => (RuleGroup::Stable, rules::numpy::rules::NumpyDeprecatedTypeAlias), - (Numpy, "002") => (RuleGroup::Stable, rules::numpy::rules::NumpyLegacyRandom), - (Numpy, "003") => (RuleGroup::Stable, rules::numpy::rules::NumpyDeprecatedFunction), - (Numpy, "201") => (RuleGroup::Stable, rules::numpy::rules::Numpy2Deprecation), + (Numpy, "001") => rules::numpy::rules::NumpyDeprecatedTypeAlias, + (Numpy, "002") => rules::numpy::rules::NumpyLegacyRandom, + (Numpy, "003") => rules::numpy::rules::NumpyDeprecatedFunction, + (Numpy, "201") => rules::numpy::rules::Numpy2Deprecation, // fastapi - (FastApi, "001") => (RuleGroup::Stable, rules::fastapi::rules::FastApiRedundantResponseModel), - (FastApi, "002") => (RuleGroup::Stable, rules::fastapi::rules::FastApiNonAnnotatedDependency), - (FastApi, "003") => (RuleGroup::Stable, rules::fastapi::rules::FastApiUnusedPathParameter), + (FastApi, "001") => rules::fastapi::rules::FastApiRedundantResponseModel, + (FastApi, "002") => rules::fastapi::rules::FastApiNonAnnotatedDependency, + (FastApi, "003") => rules::fastapi::rules::FastApiUnusedPathParameter, // pydoclint - (Pydoclint, "102") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousParameter), - (Pydoclint, "201") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingReturns), - (Pydoclint, "202") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousReturns), - (Pydoclint, "402") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingYields), - (Pydoclint, "403") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousYields), - (Pydoclint, "501") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingException), - (Pydoclint, "502") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousException), + (Pydoclint, "102") => rules::pydoclint::rules::DocstringExtraneousParameter, + (Pydoclint, "201") => rules::pydoclint::rules::DocstringMissingReturns, + (Pydoclint, "202") => rules::pydoclint::rules::DocstringExtraneousReturns, + (Pydoclint, "402") => rules::pydoclint::rules::DocstringMissingYields, + (Pydoclint, "403") => rules::pydoclint::rules::DocstringExtraneousYields, + (Pydoclint, "501") => rules::pydoclint::rules::DocstringMissingException, + (Pydoclint, "502") => rules::pydoclint::rules::DocstringExtraneousException, // ruff - (Ruff, "001") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterString), - (Ruff, "002") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterDocstring), - (Ruff, "003") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterComment), - (Ruff, "005") => (RuleGroup::Stable, rules::ruff::rules::CollectionLiteralConcatenation), - (Ruff, "006") => (RuleGroup::Stable, rules::ruff::rules::AsyncioDanglingTask), - (Ruff, "007") => (RuleGroup::Stable, rules::ruff::rules::ZipInsteadOfPairwise), - (Ruff, "008") => (RuleGroup::Stable, rules::ruff::rules::MutableDataclassDefault), - (Ruff, "009") => (RuleGroup::Stable, rules::ruff::rules::FunctionCallInDataclassDefaultArgument), - (Ruff, "010") => (RuleGroup::Stable, rules::ruff::rules::ExplicitFStringTypeConversion), - (Ruff, "011") => (RuleGroup::Removed, rules::ruff::rules::RuffStaticKeyDictComprehension), - (Ruff, "012") => (RuleGroup::Stable, rules::ruff::rules::MutableClassDefault), - (Ruff, "013") => (RuleGroup::Stable, rules::ruff::rules::ImplicitOptional), - (Ruff, "015") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement), - (Ruff, "016") => (RuleGroup::Stable, rules::ruff::rules::InvalidIndexType), - (Ruff, "017") => (RuleGroup::Stable, rules::ruff::rules::QuadraticListSummation), - (Ruff, "018") => (RuleGroup::Stable, rules::ruff::rules::AssignmentInAssert), - (Ruff, "019") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryKeyCheck), - (Ruff, "020") => (RuleGroup::Stable, rules::ruff::rules::NeverUnion), - (Ruff, "021") => (RuleGroup::Stable, rules::ruff::rules::ParenthesizeChainedOperators), - (Ruff, "022") => (RuleGroup::Stable, rules::ruff::rules::UnsortedDunderAll), - (Ruff, "023") => (RuleGroup::Stable, rules::ruff::rules::UnsortedDunderSlots), - (Ruff, "024") => (RuleGroup::Stable, rules::ruff::rules::MutableFromkeysValue), - (Ruff, "026") => (RuleGroup::Stable, rules::ruff::rules::DefaultFactoryKwarg), - (Ruff, "027") => (RuleGroup::Preview, rules::ruff::rules::MissingFStringSyntax), - (Ruff, "028") => (RuleGroup::Stable, rules::ruff::rules::InvalidFormatterSuppressionComment), - (Ruff, "029") => (RuleGroup::Preview, rules::ruff::rules::UnusedAsync), - (Ruff, "030") => (RuleGroup::Stable, rules::ruff::rules::AssertWithPrintMessage), - (Ruff, "031") => (RuleGroup::Preview, rules::ruff::rules::IncorrectlyParenthesizedTupleInSubscript), - (Ruff, "032") => (RuleGroup::Stable, rules::ruff::rules::DecimalFromFloatLiteral), - (Ruff, "033") => (RuleGroup::Stable, rules::ruff::rules::PostInitDefault), - (Ruff, "034") => (RuleGroup::Stable, rules::ruff::rules::UselessIfElse), - (Ruff, "035") => (RuleGroup::Removed, rules::ruff::rules::RuffUnsafeMarkupUse), - (Ruff, "036") => (RuleGroup::Preview, rules::ruff::rules::NoneNotAtEndOfUnion), - (Ruff, "037") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryEmptyIterableWithinDequeCall), - (Ruff, "038") => (RuleGroup::Preview, rules::ruff::rules::RedundantBoolLiteral), - (Ruff, "039") => (RuleGroup::Preview, rules::ruff::rules::UnrawRePattern), - (Ruff, "040") => (RuleGroup::Stable, rules::ruff::rules::InvalidAssertMessageLiteralArgument), - (Ruff, "041") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryNestedLiteral), - (Ruff, "043") => (RuleGroup::Stable, rules::ruff::rules::PytestRaisesAmbiguousPattern), - (Ruff, "045") => (RuleGroup::Preview, rules::ruff::rules::ImplicitClassVarInDataclass), - (Ruff, "046") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryCastToInt), - (Ruff, "047") => (RuleGroup::Preview, rules::ruff::rules::NeedlessElse), - (Ruff, "048") => (RuleGroup::Stable, rules::ruff::rules::MapIntVersionParsing), - (Ruff, "049") => (RuleGroup::Stable, rules::ruff::rules::DataclassEnum), - (Ruff, "051") => (RuleGroup::Stable, rules::ruff::rules::IfKeyInDictDel), - (Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable), - (Ruff, "053") => (RuleGroup::Stable, rules::ruff::rules::ClassWithMixedTypeVars), - (Ruff, "054") => (RuleGroup::Preview, rules::ruff::rules::IndentedFormFeed), - (Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression), - (Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback), - (Ruff, "057") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryRound), - (Ruff, "058") => (RuleGroup::Stable, rules::ruff::rules::StarmapZip), - (Ruff, "059") => (RuleGroup::Stable, rules::ruff::rules::UnusedUnpackedVariable), - (Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection), - (Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises), - (Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AccessAnnotationsFromClassDict), - (Ruff, "064") => (RuleGroup::Preview, rules::ruff::rules::NonOctalPermissions), - (Ruff, "065") => (RuleGroup::Preview, rules::ruff::rules::LoggingEagerConversion), + (Ruff, "001") => rules::ruff::rules::AmbiguousUnicodeCharacterString, + (Ruff, "002") => rules::ruff::rules::AmbiguousUnicodeCharacterDocstring, + (Ruff, "003") => rules::ruff::rules::AmbiguousUnicodeCharacterComment, + (Ruff, "005") => rules::ruff::rules::CollectionLiteralConcatenation, + (Ruff, "006") => rules::ruff::rules::AsyncioDanglingTask, + (Ruff, "007") => rules::ruff::rules::ZipInsteadOfPairwise, + (Ruff, "008") => rules::ruff::rules::MutableDataclassDefault, + (Ruff, "009") => rules::ruff::rules::FunctionCallInDataclassDefaultArgument, + (Ruff, "010") => rules::ruff::rules::ExplicitFStringTypeConversion, + (Ruff, "011") => rules::ruff::rules::RuffStaticKeyDictComprehension, + (Ruff, "012") => rules::ruff::rules::MutableClassDefault, + (Ruff, "013") => rules::ruff::rules::ImplicitOptional, + (Ruff, "015") => rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement, + (Ruff, "016") => rules::ruff::rules::InvalidIndexType, + (Ruff, "017") => rules::ruff::rules::QuadraticListSummation, + (Ruff, "018") => rules::ruff::rules::AssignmentInAssert, + (Ruff, "019") => rules::ruff::rules::UnnecessaryKeyCheck, + (Ruff, "020") => rules::ruff::rules::NeverUnion, + (Ruff, "021") => rules::ruff::rules::ParenthesizeChainedOperators, + (Ruff, "022") => rules::ruff::rules::UnsortedDunderAll, + (Ruff, "023") => rules::ruff::rules::UnsortedDunderSlots, + (Ruff, "024") => rules::ruff::rules::MutableFromkeysValue, + (Ruff, "026") => rules::ruff::rules::DefaultFactoryKwarg, + (Ruff, "027") => rules::ruff::rules::MissingFStringSyntax, + (Ruff, "028") => rules::ruff::rules::InvalidFormatterSuppressionComment, + (Ruff, "029") => rules::ruff::rules::UnusedAsync, + (Ruff, "030") => rules::ruff::rules::AssertWithPrintMessage, + (Ruff, "031") => rules::ruff::rules::IncorrectlyParenthesizedTupleInSubscript, + (Ruff, "032") => rules::ruff::rules::DecimalFromFloatLiteral, + (Ruff, "033") => rules::ruff::rules::PostInitDefault, + (Ruff, "034") => rules::ruff::rules::UselessIfElse, + (Ruff, "035") => rules::ruff::rules::RuffUnsafeMarkupUse, + (Ruff, "036") => rules::ruff::rules::NoneNotAtEndOfUnion, + (Ruff, "037") => rules::ruff::rules::UnnecessaryEmptyIterableWithinDequeCall, + (Ruff, "038") => rules::ruff::rules::RedundantBoolLiteral, + (Ruff, "039") => rules::ruff::rules::UnrawRePattern, + (Ruff, "040") => rules::ruff::rules::InvalidAssertMessageLiteralArgument, + (Ruff, "041") => rules::ruff::rules::UnnecessaryNestedLiteral, + (Ruff, "043") => rules::ruff::rules::PytestRaisesAmbiguousPattern, + (Ruff, "045") => rules::ruff::rules::ImplicitClassVarInDataclass, + (Ruff, "046") => rules::ruff::rules::UnnecessaryCastToInt, + (Ruff, "047") => rules::ruff::rules::NeedlessElse, + (Ruff, "048") => rules::ruff::rules::MapIntVersionParsing, + (Ruff, "049") => rules::ruff::rules::DataclassEnum, + (Ruff, "051") => rules::ruff::rules::IfKeyInDictDel, + (Ruff, "052") => rules::ruff::rules::UsedDummyVariable, + (Ruff, "053") => rules::ruff::rules::ClassWithMixedTypeVars, + (Ruff, "054") => rules::ruff::rules::IndentedFormFeed, + (Ruff, "055") => rules::ruff::rules::UnnecessaryRegularExpression, + (Ruff, "056") => rules::ruff::rules::FalsyDictGetFallback, + (Ruff, "057") => rules::ruff::rules::UnnecessaryRound, + (Ruff, "058") => rules::ruff::rules::StarmapZip, + (Ruff, "059") => rules::ruff::rules::UnusedUnpackedVariable, + (Ruff, "060") => rules::ruff::rules::InEmptyCollection, + (Ruff, "061") => rules::ruff::rules::LegacyFormPytestRaises, + (Ruff, "063") => rules::ruff::rules::AccessAnnotationsFromClassDict, + (Ruff, "064") => rules::ruff::rules::NonOctalPermissions, + (Ruff, "065") => rules::ruff::rules::LoggingEagerConversion, - (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), - (Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA), - (Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode), + (Ruff, "100") => rules::ruff::rules::UnusedNOQA, + (Ruff, "101") => rules::ruff::rules::RedirectedNOQA, + (Ruff, "102") => rules::ruff::rules::InvalidRuleCode, - (Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml), + (Ruff, "200") => rules::ruff::rules::InvalidPyprojectToml, #[cfg(any(feature = "test-rules", test))] - (Ruff, "900") => (RuleGroup::Stable, rules::ruff::rules::StableTestRule), + (Ruff, "900") => rules::ruff::rules::StableTestRule, #[cfg(any(feature = "test-rules", test))] - (Ruff, "901") => (RuleGroup::Stable, rules::ruff::rules::StableTestRuleSafeFix), + (Ruff, "901") => rules::ruff::rules::StableTestRuleSafeFix, #[cfg(any(feature = "test-rules", test))] - (Ruff, "902") => (RuleGroup::Stable, rules::ruff::rules::StableTestRuleUnsafeFix), + (Ruff, "902") => rules::ruff::rules::StableTestRuleUnsafeFix, #[cfg(any(feature = "test-rules", test))] - (Ruff, "903") => (RuleGroup::Stable, rules::ruff::rules::StableTestRuleDisplayOnlyFix), + (Ruff, "903") => rules::ruff::rules::StableTestRuleDisplayOnlyFix, #[cfg(any(feature = "test-rules", test))] - (Ruff, "911") => (RuleGroup::Preview, rules::ruff::rules::PreviewTestRule), + (Ruff, "911") => rules::ruff::rules::PreviewTestRule, #[cfg(any(feature = "test-rules", test))] - (Ruff, "920") => (RuleGroup::Deprecated, rules::ruff::rules::DeprecatedTestRule), + (Ruff, "920") => rules::ruff::rules::DeprecatedTestRule, #[cfg(any(feature = "test-rules", test))] - (Ruff, "921") => (RuleGroup::Deprecated, rules::ruff::rules::AnotherDeprecatedTestRule), + (Ruff, "921") => rules::ruff::rules::AnotherDeprecatedTestRule, #[cfg(any(feature = "test-rules", test))] - (Ruff, "930") => (RuleGroup::Removed, rules::ruff::rules::RemovedTestRule), + (Ruff, "930") => rules::ruff::rules::RemovedTestRule, #[cfg(any(feature = "test-rules", test))] - (Ruff, "931") => (RuleGroup::Removed, rules::ruff::rules::AnotherRemovedTestRule), + (Ruff, "931") => rules::ruff::rules::AnotherRemovedTestRule, #[cfg(any(feature = "test-rules", test))] - (Ruff, "940") => (RuleGroup::Removed, rules::ruff::rules::RedirectedFromTestRule), + (Ruff, "940") => rules::ruff::rules::RedirectedFromTestRule, #[cfg(any(feature = "test-rules", test))] - (Ruff, "950") => (RuleGroup::Stable, rules::ruff::rules::RedirectedToTestRule), + (Ruff, "950") => rules::ruff::rules::RedirectedToTestRule, #[cfg(any(feature = "test-rules", test))] - (Ruff, "960") => (RuleGroup::Removed, rules::ruff::rules::RedirectedFromPrefixTestRule), + (Ruff, "960") => rules::ruff::rules::RedirectedFromPrefixTestRule, #[cfg(any(feature = "test-rules", test))] - (Ruff, "990") => (RuleGroup::Preview, rules::ruff::rules::PanicyTestRule), + (Ruff, "990") => rules::ruff::rules::PanicyTestRule, // flake8-django - (Flake8Django, "001") => (RuleGroup::Stable, rules::flake8_django::rules::DjangoNullableModelStringField), - (Flake8Django, "003") => (RuleGroup::Stable, rules::flake8_django::rules::DjangoLocalsInRenderFunction), - (Flake8Django, "006") => (RuleGroup::Stable, rules::flake8_django::rules::DjangoExcludeWithModelForm), - (Flake8Django, "007") => (RuleGroup::Stable, rules::flake8_django::rules::DjangoAllWithModelForm), - (Flake8Django, "008") => (RuleGroup::Stable, rules::flake8_django::rules::DjangoModelWithoutDunderStr), - (Flake8Django, "012") => (RuleGroup::Stable, rules::flake8_django::rules::DjangoUnorderedBodyContentInModel), - (Flake8Django, "013") => (RuleGroup::Stable, rules::flake8_django::rules::DjangoNonLeadingReceiverDecorator), + (Flake8Django, "001") => rules::flake8_django::rules::DjangoNullableModelStringField, + (Flake8Django, "003") => rules::flake8_django::rules::DjangoLocalsInRenderFunction, + (Flake8Django, "006") => rules::flake8_django::rules::DjangoExcludeWithModelForm, + (Flake8Django, "007") => rules::flake8_django::rules::DjangoAllWithModelForm, + (Flake8Django, "008") => rules::flake8_django::rules::DjangoModelWithoutDunderStr, + (Flake8Django, "012") => rules::flake8_django::rules::DjangoUnorderedBodyContentInModel, + (Flake8Django, "013") => rules::flake8_django::rules::DjangoNonLeadingReceiverDecorator, // flynt - // Reserved: (Flynt, "001") => (RuleGroup::Stable, Rule: :StringConcatenationToFString), - (Flynt, "002") => (RuleGroup::Stable, rules::flynt::rules::StaticJoinToFString), + // Reserved: (Flynt, "001") => Rule: :StringConcatenationToFString, + (Flynt, "002") => rules::flynt::rules::StaticJoinToFString, // flake8-todos - (Flake8Todos, "001") => (RuleGroup::Stable, rules::flake8_todos::rules::InvalidTodoTag), - (Flake8Todos, "002") => (RuleGroup::Stable, rules::flake8_todos::rules::MissingTodoAuthor), - (Flake8Todos, "003") => (RuleGroup::Stable, rules::flake8_todos::rules::MissingTodoLink), - (Flake8Todos, "004") => (RuleGroup::Stable, rules::flake8_todos::rules::MissingTodoColon), - (Flake8Todos, "005") => (RuleGroup::Stable, rules::flake8_todos::rules::MissingTodoDescription), - (Flake8Todos, "006") => (RuleGroup::Stable, rules::flake8_todos::rules::InvalidTodoCapitalization), - (Flake8Todos, "007") => (RuleGroup::Stable, rules::flake8_todos::rules::MissingSpaceAfterTodoColon), + (Flake8Todos, "001") => rules::flake8_todos::rules::InvalidTodoTag, + (Flake8Todos, "002") => rules::flake8_todos::rules::MissingTodoAuthor, + (Flake8Todos, "003") => rules::flake8_todos::rules::MissingTodoLink, + (Flake8Todos, "004") => rules::flake8_todos::rules::MissingTodoColon, + (Flake8Todos, "005") => rules::flake8_todos::rules::MissingTodoDescription, + (Flake8Todos, "006") => rules::flake8_todos::rules::InvalidTodoCapitalization, + (Flake8Todos, "007") => rules::flake8_todos::rules::MissingSpaceAfterTodoColon, // airflow - (Airflow, "001") => (RuleGroup::Stable, rules::airflow::rules::AirflowVariableNameTaskIdMismatch), - (Airflow, "002") => (RuleGroup::Stable, rules::airflow::rules::AirflowDagNoScheduleArgument), - (Airflow, "301") => (RuleGroup::Stable, rules::airflow::rules::Airflow3Removal), - (Airflow, "302") => (RuleGroup::Stable, rules::airflow::rules::Airflow3MovedToProvider), - (Airflow, "311") => (RuleGroup::Stable, rules::airflow::rules::Airflow3SuggestedUpdate), - (Airflow, "312") => (RuleGroup::Stable, rules::airflow::rules::Airflow3SuggestedToMoveToProvider), + (Airflow, "001") => rules::airflow::rules::AirflowVariableNameTaskIdMismatch, + (Airflow, "002") => rules::airflow::rules::AirflowDagNoScheduleArgument, + (Airflow, "301") => rules::airflow::rules::Airflow3Removal, + (Airflow, "302") => rules::airflow::rules::Airflow3MovedToProvider, + (Airflow, "311") => rules::airflow::rules::Airflow3SuggestedUpdate, + (Airflow, "312") => rules::airflow::rules::Airflow3SuggestedToMoveToProvider, // perflint - (Perflint, "101") => (RuleGroup::Stable, rules::perflint::rules::UnnecessaryListCast), - (Perflint, "102") => (RuleGroup::Stable, rules::perflint::rules::IncorrectDictIterator), - (Perflint, "203") => (RuleGroup::Stable, rules::perflint::rules::TryExceptInLoop), - (Perflint, "401") => (RuleGroup::Stable, rules::perflint::rules::ManualListComprehension), - (Perflint, "402") => (RuleGroup::Stable, rules::perflint::rules::ManualListCopy), - (Perflint, "403") => (RuleGroup::Stable, rules::perflint::rules::ManualDictComprehension), + (Perflint, "101") => rules::perflint::rules::UnnecessaryListCast, + (Perflint, "102") => rules::perflint::rules::IncorrectDictIterator, + (Perflint, "203") => rules::perflint::rules::TryExceptInLoop, + (Perflint, "401") => rules::perflint::rules::ManualListComprehension, + (Perflint, "402") => rules::perflint::rules::ManualListCopy, + (Perflint, "403") => rules::perflint::rules::ManualDictComprehension, // flake8-fixme - (Flake8Fixme, "001") => (RuleGroup::Stable, rules::flake8_fixme::rules::LineContainsFixme), - (Flake8Fixme, "002") => (RuleGroup::Stable, rules::flake8_fixme::rules::LineContainsTodo), - (Flake8Fixme, "003") => (RuleGroup::Stable, rules::flake8_fixme::rules::LineContainsXxx), - (Flake8Fixme, "004") => (RuleGroup::Stable, rules::flake8_fixme::rules::LineContainsHack), + (Flake8Fixme, "001") => rules::flake8_fixme::rules::LineContainsFixme, + (Flake8Fixme, "002") => rules::flake8_fixme::rules::LineContainsTodo, + (Flake8Fixme, "003") => rules::flake8_fixme::rules::LineContainsXxx, + (Flake8Fixme, "004") => rules::flake8_fixme::rules::LineContainsHack, // flake8-slots - (Flake8Slots, "000") => (RuleGroup::Stable, rules::flake8_slots::rules::NoSlotsInStrSubclass), - (Flake8Slots, "001") => (RuleGroup::Stable, rules::flake8_slots::rules::NoSlotsInTupleSubclass), - (Flake8Slots, "002") => (RuleGroup::Stable, rules::flake8_slots::rules::NoSlotsInNamedtupleSubclass), + (Flake8Slots, "000") => rules::flake8_slots::rules::NoSlotsInStrSubclass, + (Flake8Slots, "001") => rules::flake8_slots::rules::NoSlotsInTupleSubclass, + (Flake8Slots, "002") => rules::flake8_slots::rules::NoSlotsInNamedtupleSubclass, // refurb - (Refurb, "101") => (RuleGroup::Preview, rules::refurb::rules::ReadWholeFile), - (Refurb, "103") => (RuleGroup::Preview, rules::refurb::rules::WriteWholeFile), - (Refurb, "105") => (RuleGroup::Stable, rules::refurb::rules::PrintEmptyString), - (Refurb, "110") => (RuleGroup::Preview, rules::refurb::rules::IfExpInsteadOfOrOperator), - (Refurb, "113") => (RuleGroup::Preview, rules::refurb::rules::RepeatedAppend), - (Refurb, "116") => (RuleGroup::Stable, rules::refurb::rules::FStringNumberFormat), - (Refurb, "118") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedOperator), - (Refurb, "122") => (RuleGroup::Stable, rules::refurb::rules::ForLoopWrites), - (Refurb, "129") => (RuleGroup::Stable, rules::refurb::rules::ReadlinesInFor), - (Refurb, "131") => (RuleGroup::Preview, rules::refurb::rules::DeleteFullSlice), - (Refurb, "132") => (RuleGroup::Stable, rules::refurb::rules::CheckAndRemoveFromSet), - (Refurb, "136") => (RuleGroup::Stable, rules::refurb::rules::IfExprMinMax), - (Refurb, "140") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedStarmap), - (Refurb, "142") => (RuleGroup::Preview, rules::refurb::rules::ForLoopSetMutations), - (Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy), - (Refurb, "148") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryEnumerate), - (Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant), - (Refurb, "154") => (RuleGroup::Preview, rules::refurb::rules::RepeatedGlobal), - (Refurb, "156") => (RuleGroup::Preview, rules::refurb::rules::HardcodedStringCharset), - (Refurb, "157") => (RuleGroup::Stable, rules::refurb::rules::VerboseDecimalConstructor), - (Refurb, "161") => (RuleGroup::Stable, rules::refurb::rules::BitCount), - (Refurb, "162") => (RuleGroup::Stable, rules::refurb::rules::FromisoformatReplaceZ), - (Refurb, "163") => (RuleGroup::Stable, rules::refurb::rules::RedundantLogBase), - (Refurb, "164") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryFromFloat), - (Refurb, "166") => (RuleGroup::Stable, rules::refurb::rules::IntOnSlicedStr), - (Refurb, "167") => (RuleGroup::Stable, rules::refurb::rules::RegexFlagAlias), - (Refurb, "168") => (RuleGroup::Stable, rules::refurb::rules::IsinstanceTypeNone), - (Refurb, "169") => (RuleGroup::Stable, rules::refurb::rules::TypeNoneComparison), - (Refurb, "171") => (RuleGroup::Preview, rules::refurb::rules::SingleItemMembershipTest), - (Refurb, "177") => (RuleGroup::Stable, rules::refurb::rules::ImplicitCwd), - (Refurb, "180") => (RuleGroup::Preview, rules::refurb::rules::MetaClassABCMeta), - (Refurb, "181") => (RuleGroup::Stable, rules::refurb::rules::HashlibDigestHex), - (Refurb, "187") => (RuleGroup::Stable, rules::refurb::rules::ListReverseCopy), - (Refurb, "188") => (RuleGroup::Stable, rules::refurb::rules::SliceToRemovePrefixOrSuffix), - (Refurb, "189") => (RuleGroup::Preview, rules::refurb::rules::SubclassBuiltin), - (Refurb, "192") => (RuleGroup::Preview, rules::refurb::rules::SortedMinMax), + (Refurb, "101") => rules::refurb::rules::ReadWholeFile, + (Refurb, "103") => rules::refurb::rules::WriteWholeFile, + (Refurb, "105") => rules::refurb::rules::PrintEmptyString, + (Refurb, "110") => rules::refurb::rules::IfExpInsteadOfOrOperator, + (Refurb, "113") => rules::refurb::rules::RepeatedAppend, + (Refurb, "116") => rules::refurb::rules::FStringNumberFormat, + (Refurb, "118") => rules::refurb::rules::ReimplementedOperator, + (Refurb, "122") => rules::refurb::rules::ForLoopWrites, + (Refurb, "129") => rules::refurb::rules::ReadlinesInFor, + (Refurb, "131") => rules::refurb::rules::DeleteFullSlice, + (Refurb, "132") => rules::refurb::rules::CheckAndRemoveFromSet, + (Refurb, "136") => rules::refurb::rules::IfExprMinMax, + (Refurb, "140") => rules::refurb::rules::ReimplementedStarmap, + (Refurb, "142") => rules::refurb::rules::ForLoopSetMutations, + (Refurb, "145") => rules::refurb::rules::SliceCopy, + (Refurb, "148") => rules::refurb::rules::UnnecessaryEnumerate, + (Refurb, "152") => rules::refurb::rules::MathConstant, + (Refurb, "154") => rules::refurb::rules::RepeatedGlobal, + (Refurb, "156") => rules::refurb::rules::HardcodedStringCharset, + (Refurb, "157") => rules::refurb::rules::VerboseDecimalConstructor, + (Refurb, "161") => rules::refurb::rules::BitCount, + (Refurb, "162") => rules::refurb::rules::FromisoformatReplaceZ, + (Refurb, "163") => rules::refurb::rules::RedundantLogBase, + (Refurb, "164") => rules::refurb::rules::UnnecessaryFromFloat, + (Refurb, "166") => rules::refurb::rules::IntOnSlicedStr, + (Refurb, "167") => rules::refurb::rules::RegexFlagAlias, + (Refurb, "168") => rules::refurb::rules::IsinstanceTypeNone, + (Refurb, "169") => rules::refurb::rules::TypeNoneComparison, + (Refurb, "171") => rules::refurb::rules::SingleItemMembershipTest, + (Refurb, "177") => rules::refurb::rules::ImplicitCwd, + (Refurb, "180") => rules::refurb::rules::MetaClassABCMeta, + (Refurb, "181") => rules::refurb::rules::HashlibDigestHex, + (Refurb, "187") => rules::refurb::rules::ListReverseCopy, + (Refurb, "188") => rules::refurb::rules::SliceToRemovePrefixOrSuffix, + (Refurb, "189") => rules::refurb::rules::SubclassBuiltin, + (Refurb, "192") => rules::refurb::rules::SortedMinMax, // flake8-logging - (Flake8Logging, "001") => (RuleGroup::Stable, rules::flake8_logging::rules::DirectLoggerInstantiation), - (Flake8Logging, "002") => (RuleGroup::Stable, rules::flake8_logging::rules::InvalidGetLoggerArgument), - (Flake8Logging, "004") => (RuleGroup::Preview, rules::flake8_logging::rules::LogExceptionOutsideExceptHandler), - (Flake8Logging, "007") => (RuleGroup::Stable, rules::flake8_logging::rules::ExceptionWithoutExcInfo), - (Flake8Logging, "009") => (RuleGroup::Stable, rules::flake8_logging::rules::UndocumentedWarn), - (Flake8Logging, "014") => (RuleGroup::Stable, rules::flake8_logging::rules::ExcInfoOutsideExceptHandler), - (Flake8Logging, "015") => (RuleGroup::Stable, rules::flake8_logging::rules::RootLoggerCall), + (Flake8Logging, "001") => rules::flake8_logging::rules::DirectLoggerInstantiation, + (Flake8Logging, "002") => rules::flake8_logging::rules::InvalidGetLoggerArgument, + (Flake8Logging, "004") => rules::flake8_logging::rules::LogExceptionOutsideExceptHandler, + (Flake8Logging, "007") => rules::flake8_logging::rules::ExceptionWithoutExcInfo, + (Flake8Logging, "009") => rules::flake8_logging::rules::UndocumentedWarn, + (Flake8Logging, "014") => rules::flake8_logging::rules::ExcInfoOutsideExceptHandler, + (Flake8Logging, "015") => rules::flake8_logging::rules::RootLoggerCall, _ => return None, }) diff --git a/crates/ruff_linter/src/rule_redirects.rs b/crates/ruff_linter/src/rule_redirects.rs index 953074e10d..b85523c6a1 100644 --- a/crates/ruff_linter/src/rule_redirects.rs +++ b/crates/ruff_linter/src/rule_redirects.rs @@ -150,7 +150,7 @@ mod tests { for rule in Rule::iter() { let (code, group) = (rule.noqa_code(), rule.group()); - if matches!(group, RuleGroup::Removed) { + if matches!(group, RuleGroup::Removed { .. }) { continue; } diff --git a/crates/ruff_linter/src/rule_selector.rs b/crates/ruff_linter/src/rule_selector.rs index b0417f4c1d..b3eee0d837 100644 --- a/crates/ruff_linter/src/rule_selector.rs +++ b/crates/ruff_linter/src/rule_selector.rs @@ -209,15 +209,15 @@ impl RuleSelector { self.all_rules().filter(move |rule| { match rule.group() { // Always include stable rules - RuleGroup::Stable => true, + RuleGroup::Stable { .. } => true, // Enabling preview includes all preview rules unless explicit selection is turned on - RuleGroup::Preview => { + RuleGroup::Preview { .. } => { preview_enabled && (self.is_exact() || !preview_require_explicit) } // Deprecated rules are excluded by default unless explicitly selected - RuleGroup::Deprecated => !preview_enabled && self.is_exact(), + RuleGroup::Deprecated { .. } => !preview_enabled && self.is_exact(), // Removed rules are included if explicitly selected but will error downstream - RuleGroup::Removed => self.is_exact(), + RuleGroup::Removed { .. } => self.is_exact(), } }) } diff --git a/crates/ruff_linter/src/rules/airflow/rules/dag_schedule_argument.rs b/crates/ruff_linter/src/rules/airflow/rules/dag_schedule_argument.rs index efb7a69f13..c06c86a955 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/dag_schedule_argument.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/dag_schedule_argument.rs @@ -41,6 +41,7 @@ use crate::checkers::ast::Checker; /// dag = DAG(dag_id="my_dag", schedule=timedelta(days=1)) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.13.0")] pub(crate) struct AirflowDagNoScheduleArgument; impl Violation for AirflowDagNoScheduleArgument { diff --git a/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs index 94a0583626..d420c25a6d 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs @@ -35,6 +35,7 @@ use crate::{FixAvailability, Violation}; /// fab_auth_manager_app = FabAuthManager().get_fastapi_app() /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.13.0")] pub(crate) struct Airflow3MovedToProvider<'a> { deprecated: QualifiedName<'a>, replacement: ProviderReplacement, 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 41defac3d0..df09b37e81 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 @@ -41,6 +41,7 @@ use ruff_text_size::TextRange; /// yesterday = today - timedelta(days=1) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.13.0")] pub(crate) struct Airflow3Removal { deprecated: String, replacement: Replacement, diff --git a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs index 5de270e6b8..bb31ef7b98 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs @@ -51,6 +51,7 @@ use ruff_text_size::TextRange; /// ) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.13.0")] pub(crate) struct Airflow3SuggestedToMoveToProvider<'a> { deprecated: QualifiedName<'a>, replacement: ProviderReplacement, diff --git a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs index c4f29f4b5a..e939387bda 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs @@ -37,6 +37,7 @@ use ruff_text_size::TextRange; /// Asset(uri="test://test/") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.13.0")] pub(crate) struct Airflow3SuggestedUpdate { deprecated: String, replacement: Replacement, diff --git a/crates/ruff_linter/src/rules/airflow/rules/task_variable_name.rs b/crates/ruff_linter/src/rules/airflow/rules/task_variable_name.rs index 8fcd6da8c1..af30a832e3 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/task_variable_name.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/task_variable_name.rs @@ -32,6 +32,7 @@ use crate::checkers::ast::Checker; /// my_task = PythonOperator(task_id="my_task") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct AirflowVariableNameTaskIdMismatch { task_id: String, } diff --git a/crates/ruff_linter/src/rules/eradicate/rules/commented_out_code.rs b/crates/ruff_linter/src/rules/eradicate/rules/commented_out_code.rs index a6a93a4514..a26d64d0ea 100644 --- a/crates/ruff_linter/src/rules/eradicate/rules/commented_out_code.rs +++ b/crates/ruff_linter/src/rules/eradicate/rules/commented_out_code.rs @@ -30,6 +30,7 @@ use crate::rules::eradicate::detection::comment_contains_code; /// /// [#4845]: https://github.com/astral-sh/ruff/issues/4845 #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.145")] pub(crate) struct CommentedOutCode; impl Violation for CommentedOutCode { diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs index 9baf036ad7..3de94ad67e 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs @@ -79,6 +79,7 @@ use ruff_python_ast::PythonVersion; /// [typing-annotated]: https://docs.python.org/3/library/typing.html#typing.Annotated /// [typing-extensions]: https://typing-extensions.readthedocs.io/en/stable/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct FastApiNonAnnotatedDependency { py_version: PythonVersion, } diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs index a6707d1ea1..440be901a7 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs @@ -59,6 +59,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// return item /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct FastApiRedundantResponseModel; impl AlwaysFixableViolation for FastApiRedundantResponseModel { 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 1dda686c00..92a04f11e5 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 @@ -64,6 +64,7 @@ use crate::{FixAvailability, Violation}; /// This rule's fix is marked as unsafe, as modifying a function signature can /// change the behavior of the code. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct FastApiUnusedPathParameter { arg_name: String, function_name: String, diff --git a/crates/ruff_linter/src/rules/flake8_2020/rules/compare.rs b/crates/ruff_linter/src/rules/flake8_2020/rules/compare.rs index 12c18a2dd7..0fb5e5ce8d 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/rules/compare.rs +++ b/crates/ruff_linter/src/rules/flake8_2020/rules/compare.rs @@ -41,6 +41,7 @@ use crate::rules::flake8_2020::helpers::is_sys; /// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.113")] pub(crate) struct SysVersionCmpStr3; impl Violation for SysVersionCmpStr3 { @@ -91,6 +92,7 @@ impl Violation for SysVersionCmpStr3 { /// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.113")] pub(crate) struct SysVersionInfo0Eq3 { eq: bool, } @@ -137,6 +139,7 @@ impl Violation for SysVersionInfo0Eq3 { /// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.113")] pub(crate) struct SysVersionInfo1CmpInt; impl Violation for SysVersionInfo1CmpInt { @@ -179,6 +182,7 @@ impl Violation for SysVersionInfo1CmpInt { /// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.113")] pub(crate) struct SysVersionInfoMinorCmpInt; impl Violation for SysVersionInfoMinorCmpInt { @@ -222,6 +226,7 @@ impl Violation for SysVersionInfoMinorCmpInt { /// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.113")] pub(crate) struct SysVersionCmpStr10; impl Violation for SysVersionCmpStr10 { diff --git a/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs b/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs index e310dd24b5..10fc95a7c4 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs +++ b/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs @@ -36,6 +36,7 @@ use crate::checkers::ast::Checker; /// - [Six documentation: `six.PY2`](https://six.readthedocs.io/#six.PY2) /// - [Six documentation: `six.PY3`](https://six.readthedocs.io/#six.PY3) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.113")] pub(crate) struct SixPY3; impl Violation for SixPY3 { diff --git a/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs b/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs index 5b5d4d6e00..591b8a529c 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs +++ b/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs @@ -38,6 +38,7 @@ use crate::rules::flake8_2020::helpers::is_sys; /// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.113")] pub(crate) struct SysVersionSlice3; impl Violation for SysVersionSlice3 { @@ -78,6 +79,7 @@ impl Violation for SysVersionSlice3 { /// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.113")] pub(crate) struct SysVersion2; impl Violation for SysVersion2 { @@ -118,6 +120,7 @@ impl Violation for SysVersion2 { /// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.113")] pub(crate) struct SysVersion0; impl Violation for SysVersion0 { @@ -158,6 +161,7 @@ impl Violation for SysVersion0 { /// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.113")] pub(crate) struct SysVersionSlice1; impl Violation for SysVersionSlice1 { diff --git a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs index b0d78bb417..a70659f99e 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs @@ -38,6 +38,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## Options /// - `lint.flake8-annotations.suppress-dummy-args` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.105")] pub(crate) struct MissingTypeFunctionArgument { name: String, } @@ -73,6 +74,7 @@ impl Violation for MissingTypeFunctionArgument { /// ## Options /// - `lint.flake8-annotations.suppress-dummy-args` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.105")] pub(crate) struct MissingTypeArgs { name: String, } @@ -108,6 +110,7 @@ impl Violation for MissingTypeArgs { /// ## Options /// - `lint.flake8-annotations.suppress-dummy-args` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.105")] pub(crate) struct MissingTypeKwargs { name: String, } @@ -149,6 +152,7 @@ impl Violation for MissingTypeKwargs { /// ``` #[derive(ViolationMetadata)] #[deprecated(note = "ANN101 has been removed")] +#[violation_metadata(removed_since = "0.8.0")] pub(crate) struct MissingTypeSelf; #[expect(deprecated)] @@ -193,6 +197,7 @@ impl Violation for MissingTypeSelf { /// ``` #[derive(ViolationMetadata)] #[deprecated(note = "ANN102 has been removed")] +#[violation_metadata(removed_since = "0.8.0")] pub(crate) struct MissingTypeCls; #[expect(deprecated)] @@ -236,6 +241,7 @@ impl Violation for MissingTypeCls { /// /// - `lint.typing-extensions` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.105")] pub(crate) struct MissingReturnTypeUndocumentedPublicFunction { name: String, annotation: Option, @@ -289,6 +295,7 @@ impl Violation for MissingReturnTypeUndocumentedPublicFunction { /// /// - `lint.typing-extensions` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.105")] pub(crate) struct MissingReturnTypePrivateFunction { name: String, annotation: Option, @@ -345,6 +352,7 @@ impl Violation for MissingReturnTypePrivateFunction { /// self.x = x /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.105")] pub(crate) struct MissingReturnTypeSpecialMethod { name: String, annotation: Option, @@ -392,6 +400,7 @@ impl Violation for MissingReturnTypeSpecialMethod { /// return 1 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.105")] pub(crate) struct MissingReturnTypeStaticMethod { name: String, annotation: Option, @@ -439,6 +448,7 @@ impl Violation for MissingReturnTypeStaticMethod { /// return 1 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.105")] pub(crate) struct MissingReturnTypeClassMethod { name: String, annotation: Option, @@ -508,6 +518,7 @@ impl Violation for MissingReturnTypeClassMethod { /// - [Python documentation: `typing.Any`](https://docs.python.org/3/library/typing.html#typing.Any) /// - [Mypy documentation: The Any type](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#the-any-type) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.108")] pub(crate) struct AnyType { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/async_busy_wait.rs b/crates/ruff_linter/src/rules/flake8_async/rules/async_busy_wait.rs index 9b4fab75c4..3cbb3e992b 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/async_busy_wait.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/async_busy_wait.rs @@ -42,6 +42,7 @@ use crate::rules::flake8_async::helpers::AsyncModule; /// - [`anyio` events](https://anyio.readthedocs.io/en/latest/api.html#anyio.Event) /// - [`trio` events](https://trio.readthedocs.io/en/latest/reference-core.html#trio.Event) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct AsyncBusyWait { module: AsyncModule, } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs b/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs index 74c19a734a..0593f2e1e8 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs @@ -65,6 +65,7 @@ use ruff_python_ast::PythonVersion; /// /// ["structured concurrency"]: https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/#timeouts-and-cancellation #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct AsyncFunctionWithTimeout { module: AsyncModule, } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/async_zero_sleep.rs b/crates/ruff_linter/src/rules/flake8_async/rules/async_zero_sleep.rs index ec4a70d266..aee5788ae4 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/async_zero_sleep.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/async_zero_sleep.rs @@ -49,6 +49,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct AsyncZeroSleep { module: AsyncModule, } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call.rs index b6e9d045ad..7f68504da4 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call.rs @@ -38,6 +38,7 @@ use crate::checkers::ast::Checker; /// ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct BlockingHttpCallInAsyncFunction; impl Violation for BlockingHttpCallInAsyncFunction { diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call_httpx.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call_httpx.rs index e46905774e..ee36eb4bd4 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call_httpx.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call_httpx.rs @@ -37,6 +37,7 @@ use crate::checkers::ast::Checker; /// response = await client.get(...) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.12.11")] pub(crate) struct BlockingHttpCallHttpxInAsyncFunction { name: String, call: String, diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_input.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_input.rs index 3c5a70f92e..c4c2cc85d6 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_input.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_input.rs @@ -33,6 +33,7 @@ use crate::checkers::ast::Checker; /// username = await loop.run_in_executor(None, input, "Username:") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.12.12")] pub(crate) struct BlockingInputInAsyncFunction; impl Violation for BlockingInputInAsyncFunction { diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs index daf84df349..0b1427c781 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs @@ -34,6 +34,7 @@ use crate::checkers::ast::Checker; /// contents = await f.read() /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct BlockingOpenCallInAsyncFunction; impl Violation for BlockingOpenCallInAsyncFunction { diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_path_methods.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_path_methods.rs index 14827c5b43..c158e4a309 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_path_methods.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_path_methods.rs @@ -47,6 +47,7 @@ use ruff_text_size::Ranged; /// new_path = os.path.join("/tmp/src/", path) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.13.2")] pub(crate) struct BlockingPathMethodInAsyncFunction { path_library: String, } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_process_invocation.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_process_invocation.rs index 2995ab0e3c..8ea20eb2b0 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_process_invocation.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_process_invocation.rs @@ -37,6 +37,7 @@ use crate::checkers::ast::Checker; /// asyncio.create_subprocess_shell(cmd) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct CreateSubprocessInAsyncFunction; impl Violation for CreateSubprocessInAsyncFunction { @@ -76,6 +77,7 @@ impl Violation for CreateSubprocessInAsyncFunction { /// asyncio.create_subprocess_shell(cmd) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct RunProcessInAsyncFunction; impl Violation for RunProcessInAsyncFunction { @@ -120,6 +122,7 @@ impl Violation for RunProcessInAsyncFunction { /// await asyncio.loop.run_in_executor(None, wait_for_process) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct WaitForProcessInAsyncFunction; impl Violation for WaitForProcessInAsyncFunction { diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_sleep.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_sleep.rs index a3ef530bc0..a06ed76f6c 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_sleep.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_sleep.rs @@ -35,6 +35,7 @@ use crate::checkers::ast::Checker; /// await asyncio.sleep(1) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct BlockingSleepInAsyncFunction; impl Violation for BlockingSleepInAsyncFunction { diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/cancel_scope_no_checkpoint.rs b/crates/ruff_linter/src/rules/flake8_async/rules/cancel_scope_no_checkpoint.rs index 80d9101948..6eedc05514 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/cancel_scope_no_checkpoint.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/cancel_scope_no_checkpoint.rs @@ -45,6 +45,7 @@ use crate::rules::flake8_async::helpers::MethodName; /// - [`anyio` timeouts](https://anyio.readthedocs.io/en/stable/cancellation.html) /// - [`trio` timeouts](https://trio.readthedocs.io/en/stable/reference-core.html#cancellation-and-timeouts) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.269")] pub(crate) struct CancelScopeNoCheckpoint { method_name: MethodName, } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs b/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs index 8680dce16b..e64e9644a3 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs @@ -39,6 +39,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// This fix is marked as unsafe as it changes program behavior. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.13.0")] pub(crate) struct LongSleepNotForever { module: AsyncModule, } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/sync_call.rs b/crates/ruff_linter/src/rules/flake8_async/rules/sync_call.rs index dc4e40e68e..48a8f573a2 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/sync_call.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/sync_call.rs @@ -38,6 +38,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// This rule's fix is marked as unsafe, as adding an `await` to a function /// call changes its semantics and runtime behavior. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct TrioSyncCall { method_name: MethodName, } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/assert_used.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/assert_used.rs index 1366cb7ac7..606630bccb 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/assert_used.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/assert_used.rs @@ -33,6 +33,7 @@ use crate::checkers::ast::Checker; /// raise ValueError("Expected positive value.") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.116")] pub(crate) struct Assert; impl Violation for Assert { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/bad_file_permissions.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/bad_file_permissions.rs index dab899b43a..511c7b4341 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/bad_file_permissions.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/bad_file_permissions.rs @@ -35,6 +35,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `stat`](https://docs.python.org/3/library/stat.html) /// - [Common Weakness Enumeration: CWE-732](https://cwe.mitre.org/data/definitions/732.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.211")] pub(crate) struct BadFilePermissions { reason: Reason, } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/django_extra.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/django_extra.rs index 0ad0de23d5..3c33e38e9f 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/django_extra.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/django_extra.rs @@ -34,6 +34,7 @@ use crate::checkers::ast::Checker; /// - [Django documentation: SQL injection protection](https://docs.djangoproject.com/en/dev/topics/security/#sql-injection-protection) /// - [Common Weakness Enumeration: CWE-89](https://cwe.mitre.org/data/definitions/89.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct DjangoExtra; impl Violation for DjangoExtra { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/django_raw_sql.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/django_raw_sql.rs index 13c3556d23..16611b8e38 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/django_raw_sql.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/django_raw_sql.rs @@ -25,6 +25,7 @@ use crate::checkers::ast::Checker; /// - [Django documentation: SQL injection protection](https://docs.djangoproject.com/en/dev/topics/security/#sql-injection-protection) /// - [Common Weakness Enumeration: CWE-89](https://cwe.mitre.org/data/definitions/89.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct DjangoRawSql; impl Violation for DjangoRawSql { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/exec_used.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/exec_used.rs index f66347d6f4..442e322b93 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/exec_used.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/exec_used.rs @@ -22,6 +22,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `exec`](https://docs.python.org/3/library/functions.html#exec) /// - [Common Weakness Enumeration: CWE-78](https://cwe.mitre.org/data/definitions/78.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.116")] pub(crate) struct ExecBuiltin; impl Violation for ExecBuiltin { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/flask_debug_true.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/flask_debug_true.rs index 9f147c6eec..df51a33599 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/flask_debug_true.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/flask_debug_true.rs @@ -39,6 +39,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Flask documentation: Debug Mode](https://flask.palletsprojects.com/en/latest/quickstart/#debug-mode) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct FlaskDebugTrue; impl Violation for FlaskDebugTrue { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs index 843dd5c3b6..91994a042e 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs @@ -27,6 +27,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Common Weakness Enumeration: CWE-200](https://cwe.mitre.org/data/definitions/200.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.116")] pub(crate) struct HardcodedBindAllInterfaces; impl Violation for HardcodedBindAllInterfaces { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_default.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_default.rs index 872ef4b7a9..db23388252 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_default.rs @@ -39,6 +39,7 @@ use crate::rules::flake8_bandit::helpers::{matches_password_name, string_literal /// ## References /// - [Common Weakness Enumeration: CWE-259](https://cwe.mitre.org/data/definitions/259.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.116")] pub(crate) struct HardcodedPasswordDefault { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs index f3cf884655..995c80e656 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs @@ -35,6 +35,7 @@ use crate::rules::flake8_bandit::helpers::{matches_password_name, string_literal /// ## References /// - [Common Weakness Enumeration: CWE-259](https://cwe.mitre.org/data/definitions/259.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.116")] pub(crate) struct HardcodedPasswordFuncArg { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_string.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_string.rs index 3711376801..e3c0b27ed6 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_string.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_string.rs @@ -34,6 +34,7 @@ use crate::rules::flake8_bandit::helpers::{matches_password_name, string_literal /// ## References /// - [Common Weakness Enumeration: CWE-259](https://cwe.mitre.org/data/definitions/259.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.116")] pub(crate) struct HardcodedPasswordString { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs index 76baa9c479..34134d96c7 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs @@ -45,6 +45,7 @@ static SQL_REGEX: LazyLock = LazyLock::new(|| { /// - [B608: Test for SQL injection](https://bandit.readthedocs.io/en/latest/plugins/b608_hardcoded_sql_expressions.html) /// - [psycopg3: Server-side binding](https://www.psycopg.org/psycopg3/docs/basic/from_pg2.html#server-side-binding) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.245")] pub(crate) struct HardcodedSQLExpression; impl Violation for HardcodedSQLExpression { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs index 77893289e3..055f16a5f6 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs @@ -40,6 +40,7 @@ use crate::checkers::ast::Checker; /// - [Common Weakness Enumeration: CWE-379](https://cwe.mitre.org/data/definitions/379.html) /// - [Python documentation: `tempfile`](https://docs.python.org/3/library/tempfile.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.211")] pub(crate) struct HardcodedTempFile { string: String, } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs index a64f66a399..9268e39e2f 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs @@ -74,6 +74,7 @@ use crate::rules::flake8_bandit::helpers::string_literal; /// - [Common Weakness Enumeration: CWE-328](https://cwe.mitre.org/data/definitions/328.html) /// - [Common Weakness Enumeration: CWE-916](https://cwe.mitre.org/data/definitions/916.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.212")] pub(crate) struct HashlibInsecureHashFunction { library: String, string: String, diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs index 475a24dfae..17aca8105f 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs @@ -35,6 +35,7 @@ use crate::checkers::ast::Checker; /// - [Jinja documentation: API](https://jinja.palletsprojects.com/en/latest/api/#autoescaping) /// - [Common Weakness Enumeration: CWE-94](https://cwe.mitre.org/data/definitions/94.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.220")] pub(crate) struct Jinja2AutoescapeFalse { value: bool, } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs index 16d9b307b5..54ad45d645 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs @@ -25,6 +25,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `logging.config.listen()`](https://docs.python.org/3/library/logging.config.html#logging.config.listen) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct LoggingConfigInsecureListen; impl Violation for LoggingConfigInsecureListen { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs index b62a0b39e7..32f695263c 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs @@ -33,6 +33,7 @@ use crate::checkers::ast::Checker; /// - [OpenStack security: Cross site scripting XSS](https://security.openstack.org/guidelines/dg_cross-site-scripting-xss.html) /// - [Common Weakness Enumeration: CWE-80](https://cwe.mitre.org/data/definitions/80.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct MakoTemplates; impl Violation for MakoTemplates { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/paramiko_calls.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/paramiko_calls.rs index 8eb8ec0bfc..ea85c1ea20 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/paramiko_calls.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/paramiko_calls.rs @@ -26,6 +26,7 @@ use crate::checkers::ast::Checker; /// - [Common Weakness Enumeration: CWE-78](https://cwe.mitre.org/data/definitions/78.html) /// - [Paramiko documentation: `SSHClient.exec_command()`](https://docs.paramiko.org/en/stable/api/client.html#paramiko.client.SSHClient.exec_command) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.270")] pub(crate) struct ParamikoCall; impl Violation for ParamikoCall { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs index c22bf55168..dadd8bde40 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs @@ -31,6 +31,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Common Weakness Enumeration: CWE-295](https://cwe.mitre.org/data/definitions/295.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.213")] pub(crate) struct RequestWithNoCertValidation { string: String, } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/request_without_timeout.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/request_without_timeout.rs index b6d3352177..f9ae386b02 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/request_without_timeout.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/request_without_timeout.rs @@ -33,6 +33,7 @@ use crate::checkers::ast::Checker; /// - [Requests documentation: Timeouts](https://requests.readthedocs.io/en/latest/user/advanced/#timeouts) /// - [httpx documentation: Timeouts](https://www.python-httpx.org/advanced/timeouts/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.213")] pub(crate) struct RequestWithoutTimeout { implicit: bool, module: String, diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs index 1e3a1c6fd1..038d1492bc 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs @@ -37,6 +37,7 @@ use crate::{ /// - [Python documentation: `subprocess` — Subprocess management](https://docs.python.org/3/library/subprocess.html) /// - [Common Weakness Enumeration: CWE-78](https://cwe.mitre.org/data/definitions/78.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.262")] pub(crate) struct SubprocessPopenWithShellEqualsTrue { safety: Safety, is_exact: bool, @@ -79,6 +80,7 @@ impl Violation for SubprocessPopenWithShellEqualsTrue { /// /// [#4045]: https://github.com/astral-sh/ruff/issues/4045 #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.262")] pub(crate) struct SubprocessWithoutShellEqualsTrue; impl Violation for SubprocessWithoutShellEqualsTrue { @@ -117,6 +119,7 @@ impl Violation for SubprocessWithoutShellEqualsTrue { /// ## References /// - [Python documentation: Security Considerations](https://docs.python.org/3/library/subprocess.html#security-considerations) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.262")] pub(crate) struct CallWithShellEqualsTrue { is_exact: bool, } @@ -169,6 +172,7 @@ impl Violation for CallWithShellEqualsTrue { /// ## References /// - [Python documentation: `subprocess`](https://docs.python.org/3/library/subprocess.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.262")] pub(crate) struct StartProcessWithAShell { safety: Safety, } @@ -210,6 +214,7 @@ impl Violation for StartProcessWithAShell { /// /// [S605]: https://docs.astral.sh/ruff/rules/start-process-with-a-shell #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.262")] pub(crate) struct StartProcessWithNoShell; impl Violation for StartProcessWithNoShell { @@ -245,6 +250,7 @@ impl Violation for StartProcessWithNoShell { /// - [Python documentation: `subprocess.Popen()`](https://docs.python.org/3/library/subprocess.html#subprocess.Popen) /// - [Common Weakness Enumeration: CWE-426](https://cwe.mitre.org/data/definitions/426.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.262")] pub(crate) struct StartProcessWithPartialPath; impl Violation for StartProcessWithPartialPath { @@ -278,6 +284,7 @@ impl Violation for StartProcessWithPartialPath { /// ## References /// - [Common Weakness Enumeration: CWE-78](https://cwe.mitre.org/data/definitions/78.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct UnixCommandWildcardInjection; impl Violation for UnixCommandWildcardInjection { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs index c7e6affb48..b490efdfc5 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs @@ -31,6 +31,7 @@ use crate::checkers::ast::Checker; /// - [Cybersecurity and Infrastructure Security Agency (CISA): Alert TA17-156A](https://www.cisa.gov/news-events/alerts/2017/06/05/reducing-risk-snmp-abuse) /// - [Common Weakness Enumeration: CWE-319](https://cwe.mitre.org/data/definitions/319.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.218")] pub(crate) struct SnmpInsecureVersion; impl Violation for SnmpInsecureVersion { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs index f7453cae1e..9f067e2c4e 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs @@ -29,6 +29,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Common Weakness Enumeration: CWE-319](https://cwe.mitre.org/data/definitions/319.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.218")] pub(crate) struct SnmpWeakCryptography; impl Violation for SnmpWeakCryptography { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssh_no_host_key_verification.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssh_no_host_key_verification.rs index b09cd8e4ac..626da50782 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssh_no_host_key_verification.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssh_no_host_key_verification.rs @@ -34,6 +34,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Paramiko documentation: set_missing_host_key_policy](https://docs.paramiko.org/en/latest/api/client.html#paramiko.client.SSHClient.set_missing_host_key_policy) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct SSHNoHostKeyVerification; impl Violation for SSHNoHostKeyVerification { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_insecure_version.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_insecure_version.rs index d022d1711a..a423fe5233 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_insecure_version.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_insecure_version.rs @@ -35,6 +35,7 @@ use crate::checkers::ast::Checker; /// ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct SslInsecureVersion { protocol: String, } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_bad_defaults.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_bad_defaults.rs index 1acb8d4ef4..4d8f8f3045 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_bad_defaults.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_bad_defaults.rs @@ -35,6 +35,7 @@ use crate::checkers::ast::Checker; /// def func(version=ssl.PROTOCOL_TLSv1_2): ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct SslWithBadDefaults { protocol: String, } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_no_version.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_no_version.rs index 20cea65ce7..a5cfccaf6c 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_no_version.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_no_version.rs @@ -26,6 +26,7 @@ use crate::checkers::ast::Checker; /// ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct SslWithNoVersion; impl Violation for SslWithNoVersion { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs index fed37def9a..0cc1ebe4ed 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs @@ -51,6 +51,7 @@ use crate::preview::is_suspicious_function_reference_enabled; /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousPickleUsage; impl Violation for SuspiciousPickleUsage { @@ -100,6 +101,7 @@ impl Violation for SuspiciousPickleUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousMarshalUsage; impl Violation for SuspiciousMarshalUsage { @@ -150,6 +152,7 @@ impl Violation for SuspiciousMarshalUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousInsecureHashUsage; impl Violation for SuspiciousInsecureHashUsage { @@ -192,6 +195,7 @@ impl Violation for SuspiciousInsecureHashUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousInsecureCipherUsage; impl Violation for SuspiciousInsecureCipherUsage { @@ -236,6 +240,7 @@ impl Violation for SuspiciousInsecureCipherUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousInsecureCipherModeUsage; impl Violation for SuspiciousInsecureCipherModeUsage { @@ -285,6 +290,7 @@ impl Violation for SuspiciousInsecureCipherModeUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousMktempUsage; impl Violation for SuspiciousMktempUsage { @@ -325,6 +331,7 @@ impl Violation for SuspiciousMktempUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousEvalUsage; impl Violation for SuspiciousEvalUsage { @@ -378,6 +385,7 @@ impl Violation for SuspiciousEvalUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousMarkSafeUsage; impl Violation for SuspiciousMarkSafeUsage { @@ -430,6 +438,7 @@ impl Violation for SuspiciousMarkSafeUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousURLOpenUsage; impl Violation for SuspiciousURLOpenUsage { @@ -472,6 +481,7 @@ impl Violation for SuspiciousURLOpenUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousNonCryptographicRandomUsage; impl Violation for SuspiciousNonCryptographicRandomUsage { @@ -516,6 +526,7 @@ impl Violation for SuspiciousNonCryptographicRandomUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousXMLCElementTreeUsage; impl Violation for SuspiciousXMLCElementTreeUsage { @@ -560,6 +571,7 @@ impl Violation for SuspiciousXMLCElementTreeUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousXMLElementTreeUsage; impl Violation for SuspiciousXMLElementTreeUsage { @@ -604,6 +616,7 @@ impl Violation for SuspiciousXMLElementTreeUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousXMLExpatReaderUsage; impl Violation for SuspiciousXMLExpatReaderUsage { @@ -648,6 +661,7 @@ impl Violation for SuspiciousXMLExpatReaderUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousXMLExpatBuilderUsage; impl Violation for SuspiciousXMLExpatBuilderUsage { @@ -692,6 +706,7 @@ impl Violation for SuspiciousXMLExpatBuilderUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousXMLSaxUsage; impl Violation for SuspiciousXMLSaxUsage { @@ -736,6 +751,7 @@ impl Violation for SuspiciousXMLSaxUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousXMLMiniDOMUsage; impl Violation for SuspiciousXMLMiniDOMUsage { @@ -780,6 +796,7 @@ impl Violation for SuspiciousXMLMiniDOMUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousXMLPullDOMUsage; impl Violation for SuspiciousXMLPullDOMUsage { @@ -821,6 +838,7 @@ impl Violation for SuspiciousXMLPullDOMUsage { /// [preview]: https://docs.astral.sh/ruff/preview/ /// [deprecated]: https://pypi.org/project/defusedxml/0.8.0rc2/#defusedxml-lxml #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "0.12.0")] pub(crate) struct SuspiciousXMLETreeUsage; impl Violation for SuspiciousXMLETreeUsage { @@ -867,6 +885,7 @@ impl Violation for SuspiciousXMLETreeUsage { /// [PEP 476]: https://peps.python.org/pep-0476/ /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousUnverifiedContextUsage; impl Violation for SuspiciousUnverifiedContextUsage { @@ -892,6 +911,7 @@ impl Violation for SuspiciousUnverifiedContextUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousTelnetUsage; impl Violation for SuspiciousTelnetUsage { @@ -917,6 +937,7 @@ impl Violation for SuspiciousTelnetUsage { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct SuspiciousFTPLibUsage; impl Violation for SuspiciousFTPLibUsage { 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 f83087c43e..acc978d054 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 @@ -25,6 +25,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `telnetlib` - Telnet client](https://docs.python.org/3.12/library/telnetlib.html#module-telnetlib) /// - [PEP 594: `telnetlib`](https://peps.python.org/pep-0594/#telnetlib) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct SuspiciousTelnetlibImport; impl Violation for SuspiciousTelnetlibImport { @@ -49,6 +50,7 @@ impl Violation for SuspiciousTelnetlibImport { /// ## References /// - [Python documentation: `ftplib` - FTP protocol client](https://docs.python.org/3/library/ftplib.html) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct SuspiciousFtplibImport; impl Violation for SuspiciousFtplibImport { @@ -74,6 +76,7 @@ impl Violation for SuspiciousFtplibImport { /// ## References /// - [Python documentation: `pickle` — Python object serialization](https://docs.python.org/3/library/pickle.html) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct SuspiciousPickleImport; impl Violation for SuspiciousPickleImport { @@ -95,6 +98,7 @@ impl Violation for SuspiciousPickleImport { /// import subprocess /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct SuspiciousSubprocessImport; impl Violation for SuspiciousSubprocessImport { @@ -118,6 +122,7 @@ impl Violation for SuspiciousSubprocessImport { /// import xml.etree.cElementTree /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct SuspiciousXmlEtreeImport; impl Violation for SuspiciousXmlEtreeImport { @@ -141,6 +146,7 @@ impl Violation for SuspiciousXmlEtreeImport { /// import xml.sax /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct SuspiciousXmlSaxImport; impl Violation for SuspiciousXmlSaxImport { @@ -164,6 +170,7 @@ impl Violation for SuspiciousXmlSaxImport { /// import xml.dom.expatbuilder /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct SuspiciousXmlExpatImport; impl Violation for SuspiciousXmlExpatImport { @@ -187,6 +194,7 @@ impl Violation for SuspiciousXmlExpatImport { /// import xml.dom.minidom /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct SuspiciousXmlMinidomImport; impl Violation for SuspiciousXmlMinidomImport { @@ -210,6 +218,7 @@ impl Violation for SuspiciousXmlMinidomImport { /// import xml.dom.pulldom /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct SuspiciousXmlPulldomImport; impl Violation for SuspiciousXmlPulldomImport { @@ -240,6 +249,7 @@ impl Violation for SuspiciousXmlPulldomImport { /// /// [deprecated]: https://github.com/tiran/defusedxml/blob/c7445887f5e1bcea470a16f61369d29870cfcfe1/README.md#defusedxmllxml #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "v0.3.0")] pub(crate) struct SuspiciousLxmlImport; impl Violation for SuspiciousLxmlImport { @@ -263,6 +273,7 @@ impl Violation for SuspiciousLxmlImport { /// import xmlrpc /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct SuspiciousXmlrpcImport; impl Violation for SuspiciousXmlrpcImport { @@ -289,6 +300,7 @@ impl Violation for SuspiciousXmlrpcImport { /// ## References /// - [httpoxy website](https://httpoxy.org/) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct SuspiciousHttpoxyImport; impl Violation for SuspiciousHttpoxyImport { @@ -314,6 +326,7 @@ impl Violation for SuspiciousHttpoxyImport { /// ## References /// - [Buffer Overflow Issue](https://github.com/pycrypto/pycrypto/issues/176) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct SuspiciousPycryptoImport; impl Violation for SuspiciousPycryptoImport { @@ -339,6 +352,7 @@ impl Violation for SuspiciousPycryptoImport { /// ## References /// - [Buffer Overflow Issue](https://github.com/pycrypto/pycrypto/issues/176) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct SuspiciousPyghmiImport; impl Violation for SuspiciousPyghmiImport { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs index 8e816ee21d..92d58e8448 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs @@ -38,6 +38,7 @@ use crate::checkers::ast::Checker; /// /// [PEP 706]: https://peps.python.org/pep-0706/#backporting-forward-compatibility #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct TarfileUnsafeMembers; impl Violation for TarfileUnsafeMembers { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_continue.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_continue.rs index 5bd4632853..85e7e4cdb6 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_continue.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_continue.rs @@ -45,6 +45,7 @@ use crate::rules::flake8_bandit::helpers::is_untyped_exception; /// - [Common Weakness Enumeration: CWE-703](https://cwe.mitre.org/data/definitions/703.html) /// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.245")] pub(crate) struct TryExceptContinue; impl Violation for TryExceptContinue { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_pass.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_pass.rs index 56dcaff1e0..383f51f957 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_pass.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_pass.rs @@ -41,6 +41,7 @@ use crate::rules::flake8_bandit::helpers::is_untyped_exception; /// - [Common Weakness Enumeration: CWE-703](https://cwe.mitre.org/data/definitions/703.html) /// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.237")] pub(crate) struct TryExceptPass; impl Violation for TryExceptPass { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_markup_use.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_markup_use.rs index 25cd68d58b..7dd043ea11 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_markup_use.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_markup_use.rs @@ -75,6 +75,7 @@ use crate::{checkers::ast::Checker, settings::LinterSettings}; /// [markupsafe-markup]: https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup /// [flake8-markupsafe]: https://github.com/vmagamedov/flake8-markupsafe #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct UnsafeMarkupUse { name: String, } 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 dae9564053..593f5e01da 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 @@ -35,6 +35,7 @@ use crate::checkers::ast::Checker; /// - [PyYAML documentation: Loading YAML](https://pyyaml.org/wiki/PyYAMLDocumentation) /// - [Common Weakness Enumeration: CWE-20](https://cwe.mitre.org/data/definitions/20.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.212")] pub(crate) struct UnsafeYAMLLoad { pub loader: Option, } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/weak_cryptographic_key.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/weak_cryptographic_key.rs index 63b709fc32..956d90f2ae 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/weak_cryptographic_key.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/weak_cryptographic_key.rs @@ -33,6 +33,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [CSRC: Transitioning the Use of Cryptographic Algorithms and Key Lengths](https://csrc.nist.gov/pubs/sp/800/131/a/r2/final) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct WeakCryptographicKey { cryptographic_key: CryptographicKey, } diff --git a/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs b/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs index ac512dee04..704b628abf 100644 --- a/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs +++ b/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs @@ -63,6 +63,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) /// - [PEP 8: Programming Recommendations on bare `except`](https://peps.python.org/pep-0008/#programming-recommendations) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.127")] pub(crate) struct BlindExcept { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs index 84f47e1e96..361f1df069 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs @@ -90,6 +90,7 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def; /// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) /// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.127")] pub(crate) struct BooleanDefaultValuePositionalArgument; impl Violation for BooleanDefaultValuePositionalArgument { diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_positional_value_in_call.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_positional_value_in_call.rs index b3a97c1d9a..4a62e65923 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_positional_value_in_call.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_positional_value_in_call.rs @@ -42,6 +42,7 @@ use crate::rules::flake8_boolean_trap::helpers::allow_boolean_trap; /// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) /// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.127")] pub(crate) struct BooleanPositionalValueInCall; impl Violation for BooleanPositionalValueInCall { diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs index 24dff35099..4bc72d0f26 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs @@ -94,6 +94,7 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def; /// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) /// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.127")] pub(crate) struct BooleanTypeHintPositionalArgument; impl Violation for BooleanTypeHintPositionalArgument { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs index 2d14a917e7..2b57ee35e1 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs @@ -54,6 +54,7 @@ use crate::registry::Rule; /// - [Python documentation: `abc`](https://docs.python.org/3/library/abc.html) /// - [Python documentation: `typing.ClassVar`](https://docs.python.org/3/library/typing.html#typing.ClassVar) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.118")] pub(crate) struct AbstractBaseClassWithoutAbstractMethod { name: String, } @@ -99,6 +100,7 @@ impl Violation for AbstractBaseClassWithoutAbstractMethod { /// ## References /// - [Python documentation: `abc`](https://docs.python.org/3/library/abc.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.118")] pub(crate) struct EmptyMethodWithoutAbstractDecorator { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs index d1ba8edcdb..d114cc2432 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs @@ -35,6 +35,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `assert`](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.67")] pub(crate) struct AssertFalse; impl AlwaysFixableViolation for AssertFalse { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs index 9702cad266..5eab2482ec 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs @@ -29,6 +29,7 @@ use crate::checkers::ast::Checker; /// self.assertRaises(SomeSpecificException, foo) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.83")] pub(crate) struct AssertRaisesException { exception: ExceptionKind, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs index b96a5d91fa..f0ca244999 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs @@ -40,6 +40,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `os.environ`](https://docs.python.org/3/library/os.html#os.environ) /// - [Python documentation: `subprocess.Popen`](https://docs.python.org/3/library/subprocess.html#subprocess.Popen) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.102")] pub(crate) struct AssignmentToOsEnviron; impl Violation for AssignmentToOsEnviron { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs index 78e29661b3..4091b13c80 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs @@ -49,6 +49,7 @@ use crate::{FixAvailability, Violation}; /// ## References /// - [Python documentation: `batched`](https://docs.python.org/3/library/itertools.html#batched) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct BatchedWithoutExplicitStrict; impl Violation for BatchedWithoutExplicitStrict { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs index 9849cdf816..c059d9b5b9 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs @@ -63,6 +63,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) /// - [don't lru_cache methods!](https://www.youtube.com/watch?v=sVjtp6tGo0g) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.114")] pub(crate) struct CachedInstanceMethod; impl Violation for CachedInstanceMethod { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs index 1ed8395147..5a0e2025b4 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs @@ -34,6 +34,7 @@ use ruff_python_ast::PythonVersion; /// y: float /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.9.0")] pub(crate) struct ClassAsDataStructure; impl Violation for ClassAsDataStructure { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs index 7747f07df7..04a0c3c552 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs @@ -40,6 +40,7 @@ use crate::{Edit, Fix}; /// ## References /// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.67")] pub(crate) struct DuplicateTryBlockException { name: String, is_star: bool, @@ -87,6 +88,7 @@ impl Violation for DuplicateTryBlockException { /// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) /// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.67")] pub(crate) struct DuplicateHandlerException { pub names: Vec, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_value.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_value.rs index 643b2c0c5e..2d6c79e804 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_value.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_value.rs @@ -29,6 +29,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// {1, 2, 3} /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct DuplicateValue { value: String, existing: String, diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs index d3f5aba2bb..0609669a68 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs @@ -34,6 +34,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.250")] pub(crate) struct ExceptWithEmptyTuple { is_star: bool, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs index 66df64700d..3da20df439 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs @@ -35,6 +35,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) /// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.255")] pub(crate) struct ExceptWithNonExceptionClasses { is_star: bool, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs index 8b4d9527f9..bfccc9f0d6 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs @@ -31,6 +31,7 @@ use crate::checkers::ast::Checker; /// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) /// - [Python documentation: Formatted string literals](https://docs.python.org/3/reference/lexical_analysis.html#f-strings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.116")] pub(crate) struct FStringDocstring; impl Violation for FStringDocstring { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs index 54faca1174..47b57d56fd 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs @@ -62,6 +62,7 @@ use crate::checkers::ast::Checker; /// ## Options /// - `lint.flake8-bugbear.extend-immutable-calls` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.102")] pub(crate) struct FunctionCallInDefaultArgument { name: Option, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs index a4cbba7cc7..f19a43e633 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs @@ -42,6 +42,7 @@ use crate::checkers::ast::Checker; /// - [The Hitchhiker's Guide to Python: Late Binding Closures](https://docs.python-guide.org/writing/gotchas/#late-binding-closures) /// - [Python documentation: `functools.partial`](https://docs.python.org/3/library/functools.html#functools.partial) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.139")] pub(crate) struct FunctionUsesLoopVariable { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/getattr_with_constant.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/getattr_with_constant.rs index ef108b77f5..7905de14ca 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/getattr_with_constant.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/getattr_with_constant.rs @@ -32,6 +32,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `getattr`](https://docs.python.org/3/library/functions.html#getattr) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.110")] pub(crate) struct GetAttrWithConstant; impl AlwaysFixableViolation for GetAttrWithConstant { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs index 87fc151c90..944efa0432 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs @@ -41,6 +41,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `try` statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.116")] pub(crate) struct JumpStatementInFinally { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs index 6a617a147d..14456024bb 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs @@ -36,6 +36,7 @@ use crate::fix::snippet::SourceCodeSnippet; /// ## References /// - [Python documentation: Mutable Sequence Types](https://docs.python.org/3/library/stdtypes.html#typesseq-mutable) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.3.7")] pub(crate) struct LoopIteratorMutation { name: Option, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs index f0a12e755a..1ac5e45f1c 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs @@ -37,6 +37,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `for` statement](https://docs.python.org/3/reference/compound_stmts.html#the-for-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.121")] pub(crate) struct LoopVariableOverridesIterator { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs index 0eba117469..cd268f610b 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs @@ -43,6 +43,7 @@ use crate::{AlwaysFixableViolation, Applicability, Fix}; /// - [Python documentation: `map`](https://docs.python.org/3/library/functions.html#map) /// - [What’s New in Python 3.14](https://docs.python.org/dev/whatsnew/3.14.html) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.13.2")] pub(crate) struct MapWithoutExplicitStrict; impl AlwaysFixableViolation for MapWithoutExplicitStrict { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs index a20680a03f..94098831b8 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs @@ -79,6 +79,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.92")] pub(crate) struct MutableArgumentDefault; impl Violation for MutableArgumentDefault { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_contextvar_default.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_contextvar_default.rs index a6d3879d42..a81e1b2c3f 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_contextvar_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_contextvar_default.rs @@ -54,6 +54,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `contextvars` — Context Variables](https://docs.python.org/3/library/contextvars.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct MutableContextvarDefault; impl Violation for MutableContextvarDefault { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs index ed2b3e98cd..f737e781ef 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs @@ -41,6 +41,7 @@ use crate::{checkers::ast::Checker, fix::edits::add_argument}; /// ## References /// - [Python documentation: `warnings.warn`](https://docs.python.org/3/library/warnings.html#warnings.warn) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.257")] pub(crate) struct NoExplicitStacklevel; impl AlwaysFixableViolation for NoExplicitStacklevel { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_literal.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_literal.rs index bfe131f98e..1fd6b3123a 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_literal.rs @@ -27,6 +27,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.102")] pub(crate) struct RaiseLiteral; impl Violation for RaiseLiteral { 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 a9b6c674f7..41bc2ebdf8 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 @@ -47,6 +47,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.138")] pub(crate) struct RaiseWithoutFromInsideExcept { is_star: bool, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs index 4f38947c77..0b7845fa39 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs @@ -40,6 +40,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `re.subn`](https://docs.python.org/3/library/re.html#re.subn) /// - [Python documentation: `re.split`](https://docs.python.org/3/library/re.html#re.split) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.278")] pub(crate) struct ReSubPositionalArgs { method: Method, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs index 3d5e383430..2ace373d4e 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs @@ -36,6 +36,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.89")] pub(crate) struct RedundantTupleInExceptionHandler { name: String, } 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 8b8f529df4..f7584dd4bb 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 @@ -79,6 +79,7 @@ use crate::checkers::ast::Checker; /// yield from dir_path.glob(f"*.{file_type}") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.4.8")] pub(crate) struct ReturnInGenerator; impl Violation for ReturnInGenerator { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs index 7643b2846e..6e88ccca40 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs @@ -34,6 +34,7 @@ use crate::checkers::ast::Checker; /// do_something_with_the_group(values) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.260")] pub(crate) struct ReuseOfGroupbyGenerator; impl Violation for ReuseOfGroupbyGenerator { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/setattr_with_constant.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/setattr_with_constant.rs index 7e378a14ba..d3ba5b953e 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/setattr_with_constant.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/setattr_with_constant.rs @@ -31,6 +31,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `setattr`](https://docs.python.org/3/library/functions.html#setattr) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.111")] pub(crate) struct SetAttrWithConstant; impl AlwaysFixableViolation for SetAttrWithConstant { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs index 570ac00660..cf79b66502 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs @@ -46,6 +46,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) /// - [Disallow iterable argument unpacking after a keyword argument?](https://github.com/python/cpython/issues/82741) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.109")] pub(crate) struct StarArgUnpackingAfterKeywordArg; impl Violation for StarArgUnpackingAfterKeywordArg { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/static_key_dict_comprehension.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/static_key_dict_comprehension.rs index ace25f1f55..7847820525 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/static_key_dict_comprehension.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/static_key_dict_comprehension.rs @@ -31,6 +31,7 @@ use crate::fix::snippet::SourceCodeSnippet; /// {value: value.upper() for value in data} /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct StaticKeyDictComprehension { key: SourceCodeSnippet, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs index 4c6ef9ef5e..e50661e8b9 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs @@ -45,6 +45,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `str.strip`](https://docs.python.org/3/library/stdtypes.html#str.strip) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.106")] pub(crate) struct StripWithMultiCharacters; impl Violation for StripWithMultiCharacters { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unary_prefix_increment_decrement.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unary_prefix_increment_decrement.rs index f9c868226b..3bfcb06125 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unary_prefix_increment_decrement.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unary_prefix_increment_decrement.rs @@ -31,6 +31,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: Unary arithmetic and bitwise operations](https://docs.python.org/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations) /// - [Python documentation: Augmented assignment statements](https://docs.python.org/3/reference/simple_stmts.html#augmented-assignment-statements) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.83")] pub(crate) struct UnaryPrefixIncrementDecrement { operator: UnaryPrefixOperatorType, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs index 5588b4c343..d58d345a12 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs @@ -23,6 +23,7 @@ use crate::checkers::ast::Checker; /// a["b"] = 1 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.250")] pub(crate) struct UnintentionalTypeAnnotation; impl Violation for UnintentionalTypeAnnotation { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs index 4aa582533e..b3bcbf5ba9 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs @@ -67,6 +67,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `__getattr__`](https://docs.python.org/3/reference/datamodel.html#object.__getattr__) /// - [Python documentation: `__call__`](https://docs.python.org/3/reference/datamodel.html#object.__call__) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.106")] pub(crate) struct UnreliableCallableCheck; impl Violation for UnreliableCallableCheck { 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 4b0a48ac16..e530b5d794 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 @@ -35,6 +35,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [PEP 8: Naming Conventions](https://peps.python.org/pep-0008/#naming-conventions) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.84")] pub(crate) struct UnusedLoopControlVariable { /// The name of the loop control variable. name: String, diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_comparison.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_comparison.rs index 25d373357c..00f4beb283 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_comparison.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_comparison.rs @@ -34,6 +34,7 @@ use crate::rules::flake8_bugbear::helpers::at_last_top_level_expression_in_cell; /// ## References /// - [Python documentation: `assert` statement](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.102")] pub(crate) struct UselessComparison { at: ComparisonLocationAt, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs index 4ec3e85d1f..8e07bbd96c 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs @@ -37,6 +37,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `contextlib.suppress`](https://docs.python.org/3/library/contextlib.html#contextlib.suppress) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.118")] pub(crate) struct UselessContextlibSuppress; impl Violation for UselessContextlibSuppress { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_expression.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_expression.rs index 2c9f74dc75..1bcd6f638b 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_expression.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_expression.rs @@ -51,6 +51,7 @@ use crate::rules::flake8_bugbear::helpers::at_last_top_level_expression_in_cell; /// _ = obj.attribute /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.100")] pub(crate) struct UselessExpression { kind: Kind, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs index 102d6b0a57..136715b981 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs @@ -39,6 +39,7 @@ use crate::{AlwaysFixableViolation, Applicability, Fix}; /// ## References /// - [Python documentation: `zip`](https://docs.python.org/3/library/functions.html#zip) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.167")] pub(crate) struct ZipWithoutExplicitStrict; impl AlwaysFixableViolation for ZipWithoutExplicitStrict { diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs index c600059c11..766e4b4561 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs @@ -49,6 +49,7 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin; /// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide) /// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.48")] pub(crate) struct BuiltinArgumentShadowing { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs index 948e0c892d..20f599484f 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs @@ -57,6 +57,7 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin; /// ## Options /// - `lint.flake8-builtins.ignorelist` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.48")] pub(crate) struct BuiltinAttributeShadowing { kind: Kind, name: String, diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_import_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_import_shadowing.rs index 6dae6ea8bd..29bcf1d034 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_import_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_import_shadowing.rs @@ -41,6 +41,7 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin; /// - `lint.flake8-builtins.ignorelist` /// - `target-version` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct BuiltinImportShadowing { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_lambda_argument_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_lambda_argument_shadowing.rs index ef9ef4a5d0..b9314e842f 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_lambda_argument_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_lambda_argument_shadowing.rs @@ -21,6 +21,7 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin; /// ## Options /// - `lint.flake8-builtins.ignorelist` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.9.0")] pub(crate) struct BuiltinLambdaArgumentShadowing { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs index 31708157dd..5b8f937a2a 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs @@ -44,6 +44,7 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin; /// ## References /// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.48")] pub(crate) struct BuiltinVariableShadowing { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs index 66f9e6f657..55ea5e72d8 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs @@ -53,6 +53,7 @@ use crate::settings::LinterSettings; /// - `lint.flake8-builtins.allowed-modules` /// - `lint.flake8-builtins.strict-checking` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.9.0")] pub(crate) struct StdlibModuleShadowing { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_commas/rules/trailing_commas.rs b/crates/ruff_linter/src/rules/flake8_commas/rules/trailing_commas.rs index 71c422de68..220c15fbed 100644 --- a/crates/ruff_linter/src/rules/flake8_commas/rules/trailing_commas.rs +++ b/crates/ruff_linter/src/rules/flake8_commas/rules/trailing_commas.rs @@ -153,6 +153,7 @@ impl Context { /// /// [formatter]:https://docs.astral.sh/ruff/formatter/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.223")] pub(crate) struct MissingTrailingComma; impl AlwaysFixableViolation for MissingTrailingComma { @@ -198,6 +199,7 @@ impl AlwaysFixableViolation for MissingTrailingComma { /// foo = (json.dumps({"bar": 1}),) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.223")] pub(crate) struct TrailingCommaOnBareTuple; impl Violation for TrailingCommaOnBareTuple { @@ -230,6 +232,7 @@ impl Violation for TrailingCommaOnBareTuple { /// /// [formatter]:https://docs.astral.sh/ruff/formatter/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.223")] pub(crate) struct ProhibitedTrailingComma; impl AlwaysFixableViolation for ProhibitedTrailingComma { diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs index ad3f1c5b11..a36ef72ec0 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs @@ -42,6 +42,7 @@ use crate::rules::flake8_comprehensions::fixes; /// The fix is marked as safe for `list()` cases, as removing `list()` around /// `sorted()` does not change the behavior. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.73")] pub(crate) struct UnnecessaryCallAroundSorted { func: UnnecessaryFunction, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs index ac9f666410..ca01f07e54 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs @@ -40,6 +40,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## Options /// - `lint.flake8-comprehensions.allow-dict-calls-with-keyword-arguments` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.61")] pub(crate) struct UnnecessaryCollectionCall { kind: Collection, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs index 558f2ace4f..f384b32e6e 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs @@ -57,6 +57,7 @@ use crate::rules::flake8_comprehensions::fixes; /// /// Additionally, this fix may drop comments when rewriting the comprehension. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.73")] pub(crate) struct UnnecessaryComprehension { kind: ComprehensionKind, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs index 6565131796..5be7f7f974 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs @@ -67,6 +67,7 @@ use crate::{Edit, Fix, Violation}; /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.262")] pub(crate) struct UnnecessaryComprehensionInCall { comprehension_kind: ComprehensionKind, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs index 2618c4765a..09c0c3dcbd 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs @@ -49,6 +49,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `dict.fromkeys`](https://docs.python.org/3/library/stdtypes.html#dict.fromkeys) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct UnnecessaryDictComprehensionForIterable { is_value_none_literal: bool, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs index c0811cdb06..8a94a0f423 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs @@ -48,6 +48,7 @@ use crate::rules::flake8_comprehensions::fixes; /// This rule's fix is marked as unsafe, as it may occasionally drop comments /// when rewriting the call. In most cases, though, comments will be preserved. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct UnnecessaryDoubleCastOrProcess { inner: String, outer: String, diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs index 7cf15ff401..547f5bd8e0 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs @@ -32,6 +32,7 @@ use crate::rules::flake8_comprehensions::helpers; /// This rule's fix is marked as unsafe, as it may occasionally drop comments /// when rewriting the call. In most cases, though, comments will be preserved. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.61")] pub(crate) struct UnnecessaryGeneratorDict; impl AlwaysFixableViolation for UnnecessaryGeneratorDict { diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs index eba4c76c60..d2b088fadf 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs @@ -42,6 +42,7 @@ use crate::rules::flake8_comprehensions::helpers; /// This rule's fix is marked as unsafe, as it may occasionally drop comments /// when rewriting the call. In most cases, though, comments will be preserved. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.61")] pub(crate) struct UnnecessaryGeneratorList { short_circuit: bool, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs index 0cbad1d806..b2ed05c925 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs @@ -43,6 +43,7 @@ use crate::rules::flake8_comprehensions::helpers; /// This rule's fix is marked as unsafe, as it may occasionally drop comments /// when rewriting the call. In most cases, though, comments will be preserved. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.61")] pub(crate) struct UnnecessaryGeneratorSet { short_circuit: bool, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs index 422081814a..00ed691551 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs @@ -29,6 +29,7 @@ use crate::rules::flake8_comprehensions::helpers; /// This rule's fix is marked as unsafe, as it may occasionally drop comments /// when rewriting the call. In most cases, though, comments will be preserved. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.73")] pub(crate) struct UnnecessaryListCall; impl AlwaysFixableViolation for UnnecessaryListCall { diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs index 6ea4747748..e0b523c17b 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs @@ -29,6 +29,7 @@ use crate::rules::flake8_comprehensions::helpers; /// This rule's fix is marked as unsafe, as it may occasionally drop comments /// when rewriting the call. In most cases, though, comments will be preserved. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.58")] pub(crate) struct UnnecessaryListComprehensionDict; impl AlwaysFixableViolation for UnnecessaryListComprehensionDict { diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs index 966b51c388..fd171a58b2 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs @@ -31,6 +31,7 @@ use crate::rules::flake8_comprehensions::helpers; /// This rule's fix is marked as unsafe, as it may occasionally drop comments /// when rewriting the call. In most cases, though, comments will be preserved. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.58")] pub(crate) struct UnnecessaryListComprehensionSet; impl AlwaysFixableViolation for UnnecessaryListComprehensionSet { diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs index d23a74582b..cea5f9c2f9 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs @@ -33,6 +33,7 @@ use crate::rules::flake8_comprehensions::helpers; /// This rule's fix is marked as unsafe, as it may occasionally drop comments /// when rewriting the call. In most cases, though, comments will be preserved. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.61")] pub(crate) struct UnnecessaryLiteralDict { obj_type: LiteralKind, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs index e89f3e2fda..f06964f33d 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs @@ -34,6 +34,7 @@ use crate::rules::flake8_comprehensions::helpers; /// This rule's fix is marked as unsafe, as it may occasionally drop comments /// when rewriting the call. In most cases, though, comments will be preserved. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.61")] pub(crate) struct UnnecessaryLiteralSet { kind: UnnecessaryLiteral, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs index 1f1c04ccc7..e6f374f88d 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs @@ -35,6 +35,7 @@ use crate::rules::flake8_comprehensions::helpers; /// This rule's fix is marked as unsafe, as it may occasionally drop comments /// when rewriting the call. In most cases, though, comments will be preserved. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.262")] pub(crate) struct UnnecessaryLiteralWithinDictCall { kind: DictKind, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs index 5d4a9d7a4c..2861e19174 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs @@ -35,6 +35,7 @@ use crate::rules::flake8_comprehensions::helpers; /// This rule's fix is marked as unsafe, as it may occasionally drop comments /// when rewriting the call. In most cases, though, comments will be preserved. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.66")] pub(crate) struct UnnecessaryLiteralWithinListCall { kind: LiteralKind, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs index 30a250e7ce..c932878d1b 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs @@ -48,6 +48,7 @@ use crate::rules::flake8_comprehensions::helpers; /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.66")] pub(crate) struct UnnecessaryLiteralWithinTupleCall { literal_kind: TupleLiteralKind, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_map.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_map.rs index 362c03b68c..64b373c6ba 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_map.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_map.rs @@ -44,6 +44,7 @@ use crate::{FixAvailability, Violation}; /// This rule's fix is marked as unsafe, as it may occasionally drop comments /// when rewriting the call. In most cases, though, comments will be preserved. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.74")] pub(crate) struct UnnecessaryMap { object_type: ObjectType, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs index 5357daf99f..72e11829ec 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs @@ -27,6 +27,7 @@ use crate::checkers::ast::Checker; /// iterable /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.64")] pub(crate) struct UnnecessarySubscriptReversal { func: String, } diff --git a/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs b/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs index f2c578036c..b3f70a7f88 100644 --- a/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs +++ b/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs @@ -20,6 +20,7 @@ use crate::settings::LinterSettings; /// - `lint.flake8-copyright.min-file-size` /// - `lint.flake8-copyright.notice-rgx` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.273")] pub(crate) struct MissingCopyrightNotice; impl Violation for MissingCopyrightNotice { diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs index 8ea11704b2..8cdc1a7a46 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs @@ -45,6 +45,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct CallDateFromtimestamp; impl Violation for CallDateFromtimestamp { diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_today.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_today.rs index 1010fb6c17..8b84a68625 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_today.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_today.rs @@ -45,6 +45,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct CallDateToday; impl Violation for CallDateToday { diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs index 7e443efe74..36c8d9831d 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs @@ -48,6 +48,7 @@ use crate::rules::flake8_datetimez::helpers::{self, DatetimeModuleAntipattern}; /// ## References /// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct CallDatetimeFromtimestamp(DatetimeModuleAntipattern); impl Violation for CallDatetimeFromtimestamp { diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs index ceea7e2d84..5827cad81c 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs @@ -46,6 +46,7 @@ use crate::rules::flake8_datetimez::helpers::{self, DatetimeModuleAntipattern}; /// ## References /// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct CallDatetimeNowWithoutTzinfo(DatetimeModuleAntipattern); impl Violation for CallDatetimeNowWithoutTzinfo { diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs index 93e2a37b90..534ca9cb4a 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs @@ -51,6 +51,7 @@ use crate::rules::flake8_datetimez::helpers::DatetimeModuleAntipattern; /// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects) /// - [Python documentation: `strftime()` and `strptime()` Behavior](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct CallDatetimeStrptimeWithoutZone(DatetimeModuleAntipattern); impl Violation for CallDatetimeStrptimeWithoutZone { diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs index c902d72200..4545302ba0 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs @@ -46,6 +46,7 @@ use crate::rules::flake8_datetimez::helpers; /// ## References /// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct CallDatetimeToday; impl Violation for CallDatetimeToday { diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs index 4f6210e026..3441ecd6b5 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs @@ -47,6 +47,7 @@ use crate::rules::flake8_datetimez::helpers; /// ## References /// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct CallDatetimeUtcfromtimestamp; impl Violation for CallDatetimeUtcfromtimestamp { diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs index 552ebbce5f..c1dfd21a42 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs @@ -46,6 +46,7 @@ use crate::rules::flake8_datetimez::helpers; /// ## References /// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct CallDatetimeUtcnow; impl Violation for CallDatetimeUtcnow { diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs index 7fe640462f..a621ad202b 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs @@ -45,6 +45,7 @@ use crate::rules::flake8_datetimez::helpers::{self, DatetimeModuleAntipattern}; /// ## References /// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct CallDatetimeWithoutTzinfo(DatetimeModuleAntipattern); impl Violation for CallDatetimeWithoutTzinfo { diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/datetime_min_max.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/datetime_min_max.rs index dafa96df78..a1428c9a77 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/datetime_min_max.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/datetime_min_max.rs @@ -42,6 +42,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct DatetimeMinMax { min_max: MinMax, } diff --git a/crates/ruff_linter/src/rules/flake8_debugger/rules/debugger.rs b/crates/ruff_linter/src/rules/flake8_debugger/rules/debugger.rs index a068ff4387..f7ce0eb580 100644 --- a/crates/ruff_linter/src/rules/flake8_debugger/rules/debugger.rs +++ b/crates/ruff_linter/src/rules/flake8_debugger/rules/debugger.rs @@ -31,6 +31,7 @@ use crate::rules::flake8_debugger::types::DebuggerUsingType; /// - [Python documentation: `pdb` — The Python Debugger](https://docs.python.org/3/library/pdb.html) /// - [Python documentation: `logging` — Logging facility for Python](https://docs.python.org/3/library/logging.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.141")] pub(crate) struct Debugger { using_type: DebuggerUsingType, } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/all_with_model_form.rs b/crates/ruff_linter/src/rules/flake8_django/rules/all_with_model_form.rs index 13a1209f84..9a752afd92 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/all_with_model_form.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/all_with_model_form.rs @@ -38,6 +38,7 @@ use crate::rules::flake8_django::helpers::is_model_form; /// fields = ["title", "content"] /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.253")] pub(crate) struct DjangoAllWithModelForm; impl Violation for DjangoAllWithModelForm { diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/exclude_with_model_form.rs b/crates/ruff_linter/src/rules/flake8_django/rules/exclude_with_model_form.rs index 4f5fe15f7a..2b1142f7bb 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/exclude_with_model_form.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/exclude_with_model_form.rs @@ -36,6 +36,7 @@ use crate::rules::flake8_django::helpers::is_model_form; /// fields = ["title", "content"] /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.253")] pub(crate) struct DjangoExcludeWithModelForm; impl Violation for DjangoExcludeWithModelForm { diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs b/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs index 4f5ad2ca42..6f239ffe65 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs @@ -34,6 +34,7 @@ use crate::checkers::ast::Checker; /// return render(request, "app/index.html", context) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.253")] pub(crate) struct DjangoLocalsInRenderFunction; impl Violation for DjangoLocalsInRenderFunction { diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/model_without_dunder_str.rs b/crates/ruff_linter/src/rules/flake8_django/rules/model_without_dunder_str.rs index 0141c77f22..e1cf9fda3f 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/model_without_dunder_str.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/model_without_dunder_str.rs @@ -41,6 +41,7 @@ use crate::rules::flake8_django::helpers; /// return f"{self.field}" /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.246")] pub(crate) struct DjangoModelWithoutDunderStr; impl Violation for DjangoModelWithoutDunderStr { diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs b/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs index 3a854e9756..289b4a45e2 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs @@ -41,6 +41,7 @@ use crate::checkers::ast::Checker; /// pass /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.246")] pub(crate) struct DjangoNonLeadingReceiverDecorator; impl Violation for DjangoNonLeadingReceiverDecorator { diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs b/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs index 3157e6f4fb..47e463d5df 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs @@ -41,6 +41,7 @@ use crate::rules::flake8_django::helpers; /// field = models.CharField(max_length=255, default="") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.246")] pub(crate) struct DjangoNullableModelStringField { field_name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/unordered_body_content_in_model.rs b/crates/ruff_linter/src/rules/flake8_django/rules/unordered_body_content_in_model.rs index b6492136ac..b3a0127343 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/unordered_body_content_in_model.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/unordered_body_content_in_model.rs @@ -63,6 +63,7 @@ use crate::rules::flake8_django::helpers; /// /// [Django Style Guide]: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#model-style #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct DjangoUnorderedBodyContentInModel { element_type: ContentType, prev_element_type: ContentType, diff --git a/crates/ruff_linter/src/rules/flake8_errmsg/rules/string_in_exception.rs b/crates/ruff_linter/src/rules/flake8_errmsg/rules/string_in_exception.rs index d5ea4f13fc..474ff585ea 100644 --- a/crates/ruff_linter/src/rules/flake8_errmsg/rules/string_in_exception.rs +++ b/crates/ruff_linter/src/rules/flake8_errmsg/rules/string_in_exception.rs @@ -48,6 +48,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// RuntimeError: 'Some value' is incorrect /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.183")] pub(crate) struct RawStringInException; impl Violation for RawStringInException { @@ -103,6 +104,7 @@ impl Violation for RawStringInException { /// RuntimeError: 'Some value' is incorrect /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.183")] pub(crate) struct FStringInException; impl Violation for FStringInException { @@ -159,6 +161,7 @@ impl Violation for FStringInException { /// RuntimeError: 'Some value' is incorrect /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.183")] pub(crate) struct DotFormatInException; impl Violation for DotFormatInException { diff --git a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_leading_whitespace.rs b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_leading_whitespace.rs index 5fe7a072d2..8285746933 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_leading_whitespace.rs +++ b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_leading_whitespace.rs @@ -31,6 +31,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.229")] pub(crate) struct ShebangLeadingWhitespace; impl AlwaysFixableViolation for ShebangLeadingWhitespace { diff --git a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_executable_file.rs b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_executable_file.rs index 1d13b3815f..301ea999d0 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_executable_file.rs +++ b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_executable_file.rs @@ -34,6 +34,7 @@ use crate::rules::flake8_executable::helpers::is_executable; /// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) /// - [Git documentation: `git update-index --chmod`](https://git-scm.com/docs/git-update-index#Documentation/git-update-index.txt---chmod-x) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.233")] pub(crate) struct ShebangMissingExecutableFile; impl Violation for ShebangMissingExecutableFile { diff --git a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_python.rs b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_python.rs index 13d6b13158..968e5340ca 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_python.rs +++ b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_python.rs @@ -32,6 +32,7 @@ use crate::comments::shebang::ShebangDirective; /// ## References /// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.229")] pub(crate) struct ShebangMissingPython; impl Violation for ShebangMissingPython { diff --git a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_executable.rs b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_executable.rs index afff2a41f3..c9ea74d86b 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_executable.rs +++ b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_executable.rs @@ -38,6 +38,7 @@ use crate::rules::flake8_executable::helpers::is_executable; /// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) /// - [Git documentation: `git update-index --chmod`](https://git-scm.com/docs/git-update-index#Documentation/git-update-index.txt---chmod-x) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.233")] pub(crate) struct ShebangNotExecutable; impl Violation for ShebangNotExecutable { diff --git a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_first_line.rs b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_first_line.rs index 4f5168e24d..4d1732cd7c 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_first_line.rs +++ b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_first_line.rs @@ -33,6 +33,7 @@ use crate::checkers::ast::LintContext; /// ## References /// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.229")] pub(crate) struct ShebangNotFirstLine; impl Violation for ShebangNotFirstLine { diff --git a/crates/ruff_linter/src/rules/flake8_fixme/rules/todos.rs b/crates/ruff_linter/src/rules/flake8_fixme/rules/todos.rs index e17deced80..5ca86e42b9 100644 --- a/crates/ruff_linter/src/rules/flake8_fixme/rules/todos.rs +++ b/crates/ruff_linter/src/rules/flake8_fixme/rules/todos.rs @@ -23,6 +23,7 @@ use crate::directives::{TodoComment, TodoDirectiveKind}; /// return f"Hello, {name}!" # TODO: Add support for custom greetings. /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.272")] pub(crate) struct LineContainsTodo; impl Violation for LineContainsTodo { #[derive_message_formats] @@ -49,6 +50,7 @@ impl Violation for LineContainsTodo { /// return distance / time # FIXME: Raises ZeroDivisionError for time = 0. /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.272")] pub(crate) struct LineContainsFixme; impl Violation for LineContainsFixme { #[derive_message_formats] @@ -72,6 +74,7 @@ impl Violation for LineContainsFixme { /// return distance / time # XXX: Raises ZeroDivisionError for time = 0. /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.272")] pub(crate) struct LineContainsXxx; impl Violation for LineContainsXxx { #[derive_message_formats] @@ -107,6 +110,7 @@ impl Violation for LineContainsXxx { /// return False /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.272")] pub(crate) struct LineContainsHack; impl Violation for LineContainsHack { #[derive_message_formats] diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_required_type_annotation.rs b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_required_type_annotation.rs index 99571bb802..593a42b368 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_required_type_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_required_type_annotation.rs @@ -49,6 +49,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// ## Options /// - `target-version` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct FutureRequiredTypeAnnotation { reason: Reason, } diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs index 66ca69cafa..26ae39ffce 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs @@ -68,6 +68,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// ## Options /// - `target-version` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.269")] pub(crate) struct FutureRewritableTypeAnnotation { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs b/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs index 13f3544768..2cc9ca51a1 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs @@ -41,6 +41,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `gettext` — Multilingual internationalization services](https://docs.python.org/3/library/gettext.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.260")] pub(crate) struct FStringInGetTextFuncCall; impl Violation for FStringInGetTextFuncCall { diff --git a/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs b/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs index 9d53d48abf..ad584804d0 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs @@ -41,6 +41,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `gettext` — Multilingual internationalization services](https://docs.python.org/3/library/gettext.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.260")] pub(crate) struct FormatInGetTextFuncCall; impl Violation for FormatInGetTextFuncCall { diff --git a/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs b/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs index 22c8a4b737..22172a8005 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs @@ -40,6 +40,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `gettext` — Multilingual internationalization services](https://docs.python.org/3/library/gettext.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.260")] pub(crate) struct PrintfInGetTextFuncCall; impl Violation for PrintfInGetTextFuncCall { diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs index 7a6e33aebc..60a65d46e5 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs @@ -33,6 +33,7 @@ use crate::{Edit, Fix}; /// ) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.201")] pub(crate) struct ExplicitStringConcatenation; impl AlwaysFixableViolation for ExplicitStringConcatenation { diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/implicit.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/implicit.rs index 7961f22b6e..c9cc873667 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/implicit.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/implicit.rs @@ -35,6 +35,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// z = "The quick brown fox." /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.201")] pub(crate) struct SingleLineImplicitStringConcatenation; impl Violation for SingleLineImplicitStringConcatenation { @@ -92,6 +93,7 @@ impl Violation for SingleLineImplicitStringConcatenation { /// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length /// [formatter]:https://docs.astral.sh/ruff/formatter/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.201")] pub(crate) struct MultiLineImplicitStringConcatenation; impl Violation for MultiLineImplicitStringConcatenation { diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_alias.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_alias.rs index 6fc205699a..080e5f3977 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_alias.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_alias.rs @@ -34,6 +34,7 @@ use crate::rules::flake8_import_conventions::settings::BannedAliases; /// ## Options /// - `lint.flake8-import-conventions.banned-aliases` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.262")] pub(crate) struct BannedImportAlias { name: String, asname: String, diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_from.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_from.rs index c5a64ea82a..e27123fe71 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_from.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_from.rs @@ -33,6 +33,7 @@ use crate::{Violation, checkers::ast::Checker}; /// ## Options /// - `lint.flake8-import-conventions.banned-from` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.263")] pub(crate) struct BannedImportFrom { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs index 90970ae343..e0684056dc 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs @@ -35,6 +35,7 @@ use crate::renamer::Renamer; /// - `lint.flake8-import-conventions.aliases` /// - `lint.flake8-import-conventions.extend-aliases` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.166")] pub(crate) struct UnconventionalImportAlias { name: String, asname: String, diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs index 501953d980..29519be884 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs @@ -42,6 +42,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [Logger Objects]: https://docs.python.org/3/library/logging.html#logger-objects #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct DirectLoggerInstantiation; impl Violation for DirectLoggerInstantiation { diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs index d1768b4254..1172e40893 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs @@ -44,6 +44,7 @@ use crate::{Fix, FixAvailability, Violation}; /// ## Fix safety /// The fix is always marked as unsafe, as it changes runtime behavior. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct ExcInfoOutsideExceptHandler; impl Violation for ExcInfoOutsideExceptHandler { diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/exception_without_exc_info.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/exception_without_exc_info.rs index 4724239d56..8d5c954277 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/exception_without_exc_info.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/exception_without_exc_info.rs @@ -31,6 +31,7 @@ use crate::checkers::ast::Checker; /// logging.error("...") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct ExceptionWithoutExcInfo; impl Violation for ExceptionWithoutExcInfo { diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs index 368bdfa5d0..948d55f16d 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs @@ -45,6 +45,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [logging documentation]: https://docs.python.org/3/library/logging.html#logger-objects #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct InvalidGetLoggerArgument; impl Violation for InvalidGetLoggerArgument { diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs index 00c099014e..4f2b852a2b 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs @@ -44,6 +44,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [The documentation]: https://docs.python.org/3/library/logging.html#logging.exception #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.9.5")] pub(crate) struct LogExceptionOutsideExceptHandler; impl Violation for LogExceptionOutsideExceptHandler { diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/root_logger_call.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/root_logger_call.rs index 6c174b39e3..f7b6a55706 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/root_logger_call.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/root_logger_call.rs @@ -28,6 +28,7 @@ use crate::checkers::ast::Checker; /// logger.info("Foobar") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct RootLoggerCall { attr: String, } diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs index 2287a217b3..213e9f7c60 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs @@ -33,6 +33,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// logging.basicConfig(level=logging.WARNING) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct UndocumentedWarn; impl Violation for UndocumentedWarn { diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/violations.rs b/crates/ruff_linter/src/rules/flake8_logging_format/violations.rs index 21e22bf2f7..9a2c378e57 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/violations.rs +++ b/crates/ruff_linter/src/rules/flake8_logging_format/violations.rs @@ -75,6 +75,7 @@ use crate::{AlwaysFixableViolation, Violation}; /// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) /// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.236")] pub(crate) struct LoggingStringFormat; impl Violation for LoggingStringFormat { @@ -159,6 +160,7 @@ impl Violation for LoggingStringFormat { /// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) /// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.236")] pub(crate) struct LoggingPercentFormat; impl Violation for LoggingPercentFormat { @@ -242,6 +244,7 @@ impl Violation for LoggingPercentFormat { /// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) /// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.236")] pub(crate) struct LoggingStringConcat; impl Violation for LoggingStringConcat { @@ -324,6 +327,7 @@ impl Violation for LoggingStringConcat { /// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) /// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.236")] pub(crate) struct LoggingFString; impl Violation for LoggingFString { @@ -381,6 +385,7 @@ impl Violation for LoggingFString { /// - [Python documentation: `logging.warning`](https://docs.python.org/3/library/logging.html#logging.warning) /// - [Python documentation: `logging.Logger.warning`](https://docs.python.org/3/library/logging.html#logging.Logger.warning) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.236")] pub(crate) struct LoggingWarn; impl AlwaysFixableViolation for LoggingWarn { @@ -448,6 +453,7 @@ impl AlwaysFixableViolation for LoggingWarn { /// ## References /// - [Python documentation: LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.236")] pub(crate) struct LoggingExtraAttrClash(pub String); impl Violation for LoggingExtraAttrClash { @@ -510,6 +516,7 @@ impl Violation for LoggingExtraAttrClash { /// - [Python documentation: `logging.error`](https://docs.python.org/3/library/logging.html#logging.error) /// - [Python documentation: `error`](https://docs.python.org/3/library/logging.html#logging.Logger.error) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.236")] pub(crate) struct LoggingExcInfo; impl Violation for LoggingExcInfo { @@ -572,6 +579,7 @@ impl Violation for LoggingExcInfo { /// - [Python documentation: `logging.error`](https://docs.python.org/3/library/logging.html#logging.error) /// - [Python documentation: `error`](https://docs.python.org/3/library/logging.html#logging.Logger.error) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.236")] pub(crate) struct LoggingRedundantExcInfo; impl Violation for LoggingRedundantExcInfo { diff --git a/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs b/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs index 82cdbda753..92a7a211cc 100644 --- a/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs +++ b/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs @@ -32,6 +32,7 @@ use crate::package::PackageRoot; /// ## Options /// - `namespace-packages` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.225")] pub(crate) struct ImplicitNamespacePackage { filename: String, parent: Option, diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs index 7049a2814d..ed6777c0d4 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs @@ -35,6 +35,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// This fix is always marked as unsafe since we cannot know /// for certain which assignment was intended. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct DuplicateClassFieldDefinition { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs index 2df842b862..e39693d754 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs @@ -49,6 +49,7 @@ use crate::{Edit, Fix}; /// - [Python documentation: `str.startswith`](https://docs.python.org/3/library/stdtypes.html#str.startswith) /// - [Python documentation: `str.endswith`](https://docs.python.org/3/library/stdtypes.html#str.endswith) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.243")] pub(crate) struct MultipleStartsEndsWith { attr: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/non_unique_enums.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/non_unique_enums.rs index 21464f148f..c36234283b 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/non_unique_enums.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/non_unique_enums.rs @@ -41,6 +41,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `enum.Enum`](https://docs.python.org/3/library/enum.html#enum.Enum) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.224")] pub(crate) struct NonUniqueEnums { value: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/reimplemented_container_builtin.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/reimplemented_container_builtin.rs index ad817929c9..4de59dd383 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/reimplemented_container_builtin.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/reimplemented_container_builtin.rs @@ -38,6 +38,7 @@ use crate::{FixAvailability, Violation}; /// ## References /// - [Python documentation: `list`](https://docs.python.org/3/library/functions.html#func-list) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct ReimplementedContainerBuiltin { container: Container, } diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs index 09bf37474c..cc436d258a 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs @@ -70,6 +70,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: Dictionary displays](https://docs.python.org/3/reference/expressions.html#dictionary-displays) /// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct UnnecessaryDictKwargs; impl Violation for UnnecessaryDictKwargs { diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_placeholder.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_placeholder.rs index 1a64d0f971..3d446f68b0 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_placeholder.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_placeholder.rs @@ -59,6 +59,7 @@ use crate::{Edit, Fix}; /// ## References /// - [Python documentation: The `pass` statement](https://docs.python.org/3/reference/simple_stmts.html#the-pass-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct UnnecessaryPlaceholder { kind: Placeholder, } diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs index 5c9d4c4be6..6e6b10b206 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs @@ -27,6 +27,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// ## References /// - [Python documentation: `range`](https://docs.python.org/3/library/stdtypes.html#range) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.286")] pub(crate) struct UnnecessaryRangeStart; impl AlwaysFixableViolation for UnnecessaryRangeStart { diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_spread.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_spread.rs index b5aefbfa5e..4b99f4e887 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_spread.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_spread.rs @@ -28,6 +28,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: Dictionary displays](https://docs.python.org/3/reference/expressions.html#dictionary-displays) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct UnnecessarySpread; impl Violation for UnnecessarySpread { diff --git a/crates/ruff_linter/src/rules/flake8_print/rules/print_call.rs b/crates/ruff_linter/src/rules/flake8_print/rules/print_call.rs index c5a851aaf8..82db18a2fc 100644 --- a/crates/ruff_linter/src/rules/flake8_print/rules/print_call.rs +++ b/crates/ruff_linter/src/rules/flake8_print/rules/print_call.rs @@ -48,6 +48,7 @@ use crate::{Fix, FixAvailability, Violation}; /// This rule's fix is marked as unsafe, as it will remove `print` statements /// that are used beyond debugging purposes. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.57")] pub(crate) struct Print; impl Violation for Print { @@ -97,6 +98,7 @@ impl Violation for Print { /// This rule's fix is marked as unsafe, as it will remove `pprint` statements /// that are used beyond debugging purposes. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.57")] pub(crate) struct PPrint; impl Violation for PPrint { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs index bebf9aee83..aeff638af2 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs @@ -44,6 +44,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// - [Python documentation: The `Any` type](https://docs.python.org/3/library/typing.html#the-any-type) /// - [Mypy documentation: Any vs. object](https://mypy.readthedocs.io/en/latest/dynamic_typing.html#any-vs-object) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct AnyEqNeAnnotation { method_name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_generator_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_generator_return_type.rs index 0524b69af6..b836c4de7f 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_generator_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_generator_return_type.rs @@ -59,6 +59,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// unsafe for any `__iter__` or `__aiter__` method in a `.py` file that has /// more than two statements (including docstrings) in its body. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct GeneratorReturnFromIterMethod { return_type: Iterator, method: Method, diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs index 6ac4235e64..630e267142 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs @@ -51,6 +51,7 @@ use crate::registry::Rule; /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.254")] pub(crate) struct BadVersionInfoComparison; impl Violation for BadVersionInfoComparison { @@ -100,6 +101,7 @@ impl Violation for BadVersionInfoComparison { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct BadVersionInfoOrder; impl Violation for BadVersionInfoOrder { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/bytestring_usage.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/bytestring_usage.rs index 59d9173860..c87c669ab7 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/bytestring_usage.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/bytestring_usage.rs @@ -28,6 +28,7 @@ use crate::{FixAvailability, Violation}; /// ## References /// - [Python documentation: The `ByteString` type](https://docs.python.org/3/library/typing.html#typing.ByteString) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.6.0")] pub(crate) struct ByteStringUsage { origin: ByteStringOrigin, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs index 5c2a4de600..e6735a6921 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs @@ -36,6 +36,7 @@ use crate::checkers::ast::Checker; /// age: int /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct CollectionsNamedTuple; impl Violation for CollectionsNamedTuple { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_assignment_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_assignment_in_stub.rs index 0f38dfe8e6..f5dd22a140 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_assignment_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_assignment_in_stub.rs @@ -42,6 +42,7 @@ use crate::checkers::ast::Checker; /// X: TypeAlias = int /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.279")] pub(crate) struct ComplexAssignmentInStub; impl Violation for ComplexAssignmentInStub { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs index 010753a0bc..d097df3b34 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs @@ -32,6 +32,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Typing documentation: Version and platform checking](https://typing.python.org/en/latest/spec/directives.html#version-and-platform-checks) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.276")] pub(crate) struct ComplexIfStatementInStub; impl Violation for ComplexIfStatementInStub { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs index aaa182b4e0..b22a050ad0 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs @@ -89,6 +89,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// [typing_TypeVar]: https://docs.python.org/3/library/typing.html#typing.TypeVar /// [typing_extensions]: https://typing-extensions.readthedocs.io/en/latest/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.283")] pub(crate) struct CustomTypeVarForSelf { typevar_name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/docstring_in_stubs.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/docstring_in_stubs.rs index 6db0029643..abaf7035b5 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/docstring_in_stubs.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/docstring_in_stubs.rs @@ -27,6 +27,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// def func(param: int) -> str: ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.253")] pub(crate) struct DocstringInStub; impl AlwaysFixableViolation for DocstringInStub { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_literal_member.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_literal_member.rs index 1f4c6210e4..9ccdfd92db 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_literal_member.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_literal_member.rs @@ -40,6 +40,7 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## References /// - [Python documentation: `typing.Literal`](https://docs.python.org/3/library/typing.html#typing.Literal) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.6.0")] pub(crate) struct DuplicateLiteralMember { duplicate_name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs index 47d26ca850..cc909a7267 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs @@ -37,6 +37,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `typing.Union`](https://docs.python.org/3/library/typing.html#typing.Union) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.262")] pub(crate) struct DuplicateUnionMember { duplicate_name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs index 4c1497fe93..d261c9aef7 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs @@ -28,6 +28,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// value: int /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.270")] pub(crate) struct EllipsisInNonEmptyClassBody; impl Violation for EllipsisInNonEmptyClassBody { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs index 3127f3d411..670ad6b891 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs @@ -47,6 +47,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ) -> None: ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.279")] pub(crate) struct BadExitAnnotation { func_kind: FuncKind, error_kind: ErrorKind, diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs index b85059e756..cd3703c718 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs @@ -18,6 +18,7 @@ use crate::{checkers::ast::Checker, fix}; /// ## References /// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#language-features) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.273")] pub(crate) struct FutureAnnotationsInStub; impl Violation for FutureAnnotationsInStub { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs index 19565abd56..4cd5035693 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs @@ -84,6 +84,7 @@ use crate::{Fix, FixAvailability, Violation}; /// [1]: https://github.com/python/cpython/issues/106102 /// [MRO]: https://docs.python.org/3/glossary.html#term-method-resolution-order #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.13.0")] pub(crate) struct GenericNotLastBaseClass; impl Violation for GenericNotLastBaseClass { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs index b72a0ce0cc..b6f6359014 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs @@ -69,6 +69,7 @@ use crate::checkers::ast::Checker; /// def __iter__(self) -> collections.abc.Iterator[str]: ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct IterMethodReturnIterable { is_async: bool, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs index c129ae33f7..1fa1d645ba 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs @@ -41,6 +41,7 @@ use ruff_python_ast::PythonVersion; /// /// [bottom type]: https://en.wikipedia.org/wiki/Bottom_type #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.272")] pub(crate) struct NoReturnArgumentAnnotationInStub { module: TypingModule, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_empty_stub_body.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_empty_stub_body.rs index 3e3efd8c66..36d2fc11b5 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_empty_stub_body.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_empty_stub_body.rs @@ -28,6 +28,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Typing documentation - Writing and Maintaining Stub Files](https://typing.python.org/en/latest/guides/writing_stubs.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.253")] pub(crate) struct NonEmptyStubBody; impl AlwaysFixableViolation for NonEmptyStubBody { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs index 9e823d11d5..59d01ff834 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -88,6 +88,7 @@ use ruff_text_size::Ranged; /// ## References /// - [Python documentation: `typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct NonSelfReturnType { class_name: String, method_name: String, diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/numeric_literal_too_long.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/numeric_literal_too_long.rs index 739778f4cf..83b48e677a 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/numeric_literal_too_long.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/numeric_literal_too_long.rs @@ -30,6 +30,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// def foo(arg: int = ...) -> None: ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct NumericLiteralTooLong; impl AlwaysFixableViolation for NumericLiteralTooLong { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_in_class_body.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_in_class_body.rs index 2177f92e78..336d2664f7 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_in_class_body.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_in_class_body.rs @@ -27,6 +27,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// x: int /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.260")] pub(crate) struct PassInClassBody; impl AlwaysFixableViolation for PassInClassBody { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs index f4a7f64538..6aa512ca15 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs @@ -25,6 +25,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Typing documentation - Writing and Maintaining Stub Files](https://typing.python.org/en/latest/guides/writing_stubs.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.253")] pub(crate) struct PassStatementStubBody; impl AlwaysFixableViolation for PassStatementStubBody { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs index e481a82dc2..507a1b9808 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs @@ -40,6 +40,7 @@ use ruff_python_ast::PythonVersion; /// [PEP 484]: https://peps.python.org/pep-0484/#positional-only-arguments /// [PEP 570]: https://peps.python.org/pep-0570 #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct Pep484StylePositionalOnlyParameter; impl Violation for Pep484StylePositionalOnlyParameter { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs index 152c54671b..22f14f043e 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs @@ -46,6 +46,7 @@ impl fmt::Display for VarKind { /// _T = TypeVar("_T") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.245")] pub(crate) struct UnprefixedTypeParam { kind: VarKind, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs index 63b610408b..86bdf32a98 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs @@ -29,6 +29,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Typing documentation - Writing and Maintaining Stub Files](https://typing.python.org/en/latest/guides/writing_stubs.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.265")] pub(crate) struct QuotedAnnotationInStub; impl AlwaysFixableViolation for QuotedAnnotationInStub { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_final_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_final_literal.rs index d710b4823f..8b44eab1ce 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_final_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_final_literal.rs @@ -34,6 +34,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// y: Final = 42 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct RedundantFinalLiteral { literal: SourceCodeSnippet, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_literal_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_literal_union.rs index 512849b70d..d4d018c748 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_literal_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_literal_union.rs @@ -37,6 +37,7 @@ use crate::fix::snippet::SourceCodeSnippet; /// x: Literal[b"B"] | str /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.283")] pub(crate) struct RedundantLiteralUnion { literal: SourceCodeSnippet, builtin_type: ExprType, diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 78441d1f8b..14145229fc 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -49,6 +49,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Typing documentation: Legal parameters for `Literal` at type check time](https://typing.python.org/en/latest/spec/literal.html#legal-parameters-for-literal-at-type-check-time) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.13.0")] pub(crate) struct RedundantNoneLiteral { union_kind: UnionKind, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs index 1197e70ea6..1e4583f312 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs @@ -53,6 +53,7 @@ use super::generate_union_fix; /// /// [typing specification]: https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.279")] pub(crate) struct RedundantNumericUnion { redundancy: Redundancy, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs index 32059594db..32b83754be 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -41,6 +41,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix, Violation}; /// ## References /// - [`flake8-pyi`](https://github.com/PyCQA/flake8-pyi/blob/main/ERRORCODES.md) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.253")] pub(crate) struct TypedArgumentDefaultInStub; impl AlwaysFixableViolation for TypedArgumentDefaultInStub { @@ -87,6 +88,7 @@ impl AlwaysFixableViolation for TypedArgumentDefaultInStub { /// ## References /// - [`flake8-pyi`](https://github.com/PyCQA/flake8-pyi/blob/main/ERRORCODES.md) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.253")] pub(crate) struct ArgumentDefaultInStub; impl AlwaysFixableViolation for ArgumentDefaultInStub { @@ -131,6 +133,7 @@ impl AlwaysFixableViolation for ArgumentDefaultInStub { /// ## References /// - [`flake8-pyi`](https://github.com/PyCQA/flake8-pyi/blob/main/ERRORCODES.md) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.260")] pub(crate) struct AssignmentDefaultInStub; impl AlwaysFixableViolation for AssignmentDefaultInStub { @@ -151,6 +154,7 @@ impl AlwaysFixableViolation for AssignmentDefaultInStub { /// Stub files exist to provide type hints, and are never executed. As such, /// all assignments in stub files should be annotated with a type. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.269")] pub(crate) struct UnannotatedAssignmentInStub { name: String, } @@ -182,6 +186,7 @@ impl Violation for UnannotatedAssignmentInStub { /// __all__: list[str] = ["foo", "bar"] /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct UnassignedSpecialVariableInStub { name: String, } @@ -230,6 +235,7 @@ impl Violation for UnassignedSpecialVariableInStub { /// /// - `lint.typing-extensions` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.279")] pub(crate) struct TypeAliasWithoutAnnotation { module: TypingModule, name: String, diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs index 6cb7ceaa2e..0bc8f55eec 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs @@ -24,6 +24,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// def __repr__(self) -> str: ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct StrOrReprDefinedInStub { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs index a739f91d36..0b0580663b 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs @@ -33,6 +33,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// def foo(arg: str = ...) -> None: ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct StringOrBytesTooLong; impl AlwaysFixableViolation for StringOrBytesTooLong { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs index 06b428d7d3..e0e26b055d 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs @@ -29,6 +29,7 @@ use crate::checkers::ast::Checker; /// def function(): ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct StubBodyMultipleStatements; impl Violation for StubBodyMultipleStatements { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/type_alias_naming.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/type_alias_naming.rs index 7a2aba860b..a7d5d4b387 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/type_alias_naming.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/type_alias_naming.rs @@ -26,6 +26,7 @@ use crate::checkers::ast::Checker; /// TypeAliasName: TypeAlias = int /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.265")] pub(crate) struct SnakeCaseTypeAlias { name: String, } @@ -65,6 +66,7 @@ impl Violation for SnakeCaseTypeAlias { /// ## References /// - [PEP 484: Type Aliases](https://peps.python.org/pep-0484/#type-aliases) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.265")] pub(crate) struct TSuffixedTypeAlias { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/type_comment_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/type_comment_in_stub.rs index 2fe79461ed..de345707b9 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/type_comment_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/type_comment_in_stub.rs @@ -28,6 +28,7 @@ use crate::checkers::ast::LintContext; /// x: int = 1 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.254")] pub(crate) struct TypeCommentInStub; impl Violation for TypeCommentInStub { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs index ba3c63ac2f..47b111f97e 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs @@ -40,6 +40,7 @@ use crate::{Applicability, Fix, FixAvailability, Violation}; /// `import foo as foo` alias, or are imported via a `*` import. As such, the /// fix is marked as safe in more cases for `.pyi` files. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct UnaliasedCollectionsAbcSetImport; impl Violation for UnaliasedCollectionsAbcSetImport { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs index 3945afde33..cf5cd71932 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs @@ -47,6 +47,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `typing.Literal`](https://docs.python.org/3/library/typing.html#typing.Literal) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.278")] pub(crate) struct UnnecessaryLiteralUnion { members: Vec, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_type_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_type_union.rs index 5906e7f411..2e07aeb84d 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_type_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_type_union.rs @@ -32,6 +32,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// Note that while the fix may flatten nested unions into a single top-level union, /// the semantics of the annotation will remain unchanged. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.283")] pub(crate) struct UnnecessaryTypeUnion { members: Vec, union_kind: UnionKind, diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs index d9183c0966..d7a22d0940 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs @@ -46,6 +46,7 @@ use crate::registry::Rule; /// ## References /// - [Typing documentation: Version and Platform checking](https://typing.python.org/en/latest/spec/directives.html#version-and-platform-checks) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.246")] pub(crate) struct UnrecognizedPlatformCheck; impl Violation for UnrecognizedPlatformCheck { @@ -84,6 +85,7 @@ impl Violation for UnrecognizedPlatformCheck { /// ## References /// - [Typing documentation: Version and Platform checking](https://typing.python.org/en/latest/spec/directives.html#version-and-platform-checks) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.246")] pub(crate) struct UnrecognizedPlatformName { platform: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs index 9188e865a8..3fe1fa58ff 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs @@ -33,6 +33,7 @@ use crate::registry::Rule; /// ## References /// - [Typing documentation: Version and Platform checking](https://typing.python.org/en/latest/spec/directives.html#version-and-platform-checks) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.276")] pub(crate) struct UnrecognizedVersionInfoCheck; impl Violation for UnrecognizedVersionInfoCheck { @@ -72,6 +73,7 @@ impl Violation for UnrecognizedVersionInfoCheck { /// ## References /// - [Typing documentation: Version and Platform checking](https://typing.python.org/en/latest/spec/directives.html#version-and-platform-checks) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.276")] pub(crate) struct PatchVersionComparison; impl Violation for PatchVersionComparison { @@ -108,6 +110,7 @@ impl Violation for PatchVersionComparison { /// ## References /// - [Typing documentation: Version and Platform checking](https://typing.python.org/en/latest/spec/directives.html#version-and-platform-checks) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.276")] pub(crate) struct WrongTupleLengthVersionComparison { expected_length: usize, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unsupported_method_call_on_all.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unsupported_method_call_on_all.rs index e745f67210..fb55360533 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unsupported_method_call_on_all.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unsupported_method_call_on_all.rs @@ -41,6 +41,7 @@ use crate::checkers::ast::Checker; /// __all__ += ["C"] /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.281")] pub(crate) struct UnsupportedMethodCallOnAll { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs index d5e6645642..82f3d93519 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs @@ -30,6 +30,7 @@ use crate::{Fix, FixAvailability, Violation}; /// The fix is always marked as unsafe, as it would break your code if the type /// variable is imported by another module. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.281")] pub(crate) struct UnusedPrivateTypeVar { type_var_like_name: String, type_var_like_kind: String, @@ -86,6 +87,7 @@ impl Violation for UnusedPrivateTypeVar { /// def func(arg: _PrivateProtocol) -> None: ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.281")] pub(crate) struct UnusedPrivateProtocol { name: String, } @@ -124,6 +126,7 @@ impl Violation for UnusedPrivateProtocol { /// def func(arg: _UsedTypeAlias) -> _UsedTypeAlias: ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.281")] pub(crate) struct UnusedPrivateTypeAlias { name: String, } @@ -164,6 +167,7 @@ impl Violation for UnusedPrivateTypeAlias { /// def func(arg: _UsedPrivateTypedDict) -> _UsedPrivateTypedDict: ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.281")] pub(crate) struct UnusedPrivateTypedDict { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs index 599decd57b..545372dd6c 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs @@ -61,6 +61,7 @@ use super::unittest_assert::UnittestAssert; /// assert not something_else /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestCompositeAssertion; impl Violation for PytestCompositeAssertion { @@ -108,6 +109,7 @@ impl Violation for PytestCompositeAssertion { /// ## References /// - [`pytest` documentation: `pytest.raises`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestAssertInExcept { name: String, } @@ -149,6 +151,7 @@ impl Violation for PytestAssertInExcept { /// ## References /// - [`pytest` documentation: `pytest.fail`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fail) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestAssertAlwaysFalse; impl Violation for PytestAssertAlwaysFalse { @@ -188,6 +191,7 @@ impl Violation for PytestAssertAlwaysFalse { /// ## References /// - [`pytest` documentation: Assertion introspection details](https://docs.pytest.org/en/7.1.x/how-to/assert.html#assertion-introspection-details) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestUnittestAssertion { assertion: String, } @@ -343,6 +347,7 @@ pub(crate) fn unittest_assertion( /// ## References /// - [`pytest` documentation: Assertions about expected exceptions](https://docs.pytest.org/en/latest/how-to/assert.html#assertions-about-expected-exceptions) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.285")] pub(crate) struct PytestUnittestRaisesAssertion { assertion: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fail.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fail.rs index cc95dd837a..e30fcee3bc 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fail.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fail.rs @@ -46,6 +46,7 @@ use crate::rules::flake8_pytest_style::helpers::{is_empty_or_null_string, is_pyt /// ## References /// - [`pytest` documentation: `pytest.fail`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fail) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestFailWithoutMessage; impl Violation for PytestFailWithoutMessage { diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs index 6c0558636c..49be564b48 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs @@ -80,6 +80,7 @@ use crate::rules::flake8_pytest_style::helpers::{ /// ## References /// - [`pytest` documentation: API Reference: Fixtures](https://docs.pytest.org/en/latest/reference/reference.html#fixtures-api) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestFixtureIncorrectParenthesesStyle { expected: Parentheses, actual: Parentheses, @@ -131,6 +132,7 @@ impl AlwaysFixableViolation for PytestFixtureIncorrectParenthesesStyle { /// ## References /// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestFixturePositionalArgs { function: String, } @@ -172,6 +174,7 @@ impl Violation for PytestFixturePositionalArgs { /// ## References /// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestExtraneousScopeFunction; impl AlwaysFixableViolation for PytestExtraneousScopeFunction { @@ -235,6 +238,7 @@ impl AlwaysFixableViolation for PytestExtraneousScopeFunction { /// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture) #[derive(ViolationMetadata)] #[deprecated(note = "PT004 has been removed")] +#[violation_metadata(removed_since = "0.8.0")] pub(crate) struct PytestMissingFixtureNameUnderscore; #[expect(deprecated)] @@ -300,6 +304,7 @@ impl Violation for PytestMissingFixtureNameUnderscore { /// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture) #[derive(ViolationMetadata)] #[deprecated(note = "PT005 has been removed")] +#[violation_metadata(removed_since = "0.8.0")] pub(crate) struct PytestIncorrectFixtureNameUnderscore; #[expect(deprecated)] @@ -359,6 +364,7 @@ impl Violation for PytestIncorrectFixtureNameUnderscore { /// ## References /// - [`pytest` documentation: `pytest.mark.usefixtures`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-mark-usefixtures) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestFixtureParamWithoutValue { name: String, } @@ -407,6 +413,7 @@ impl Violation for PytestFixtureParamWithoutValue { /// ## References /// - [`pytest` documentation: `yield_fixture` functions](https://docs.pytest.org/en/latest/yieldfixture.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestDeprecatedYieldFixture; impl Violation for PytestDeprecatedYieldFixture { @@ -466,6 +473,7 @@ impl Violation for PytestDeprecatedYieldFixture { /// - [`pytest` documentation: Adding finalizers directly](https://docs.pytest.org/en/latest/how-to/fixtures.html#adding-finalizers-directly) /// - [`pytest` documentation: Factories as fixtures](https://docs.pytest.org/en/latest/how-to/fixtures.html#factories-as-fixtures) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestFixtureFinalizerCallback; impl Violation for PytestFixtureFinalizerCallback { @@ -514,6 +522,7 @@ impl Violation for PytestFixtureFinalizerCallback { /// ## References /// - [`pytest` documentation: Teardown/Cleanup](https://docs.pytest.org/en/latest/how-to/fixtures.html#teardown-cleanup-aka-fixture-finalization) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestUselessYieldFixture { name: String, } @@ -571,6 +580,7 @@ impl AlwaysFixableViolation for PytestUselessYieldFixture { /// ## References /// - [`pytest` documentation: `pytest.mark.usefixtures`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-mark-usefixtures) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestErroneousUseFixturesOnFixture; impl AlwaysFixableViolation for PytestErroneousUseFixturesOnFixture { @@ -614,6 +624,7 @@ impl AlwaysFixableViolation for PytestErroneousUseFixturesOnFixture { /// ## References /// - [PyPI: `pytest-asyncio`](https://pypi.org/project/pytest-asyncio/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestUnnecessaryAsyncioMarkOnFixture; impl AlwaysFixableViolation for PytestUnnecessaryAsyncioMarkOnFixture { diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/imports.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/imports.rs index 2b6c126185..e1cff65749 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/imports.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/imports.rs @@ -23,6 +23,7 @@ use crate::{Violation, checkers::ast::Checker}; /// import pytest /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestIncorrectPytestImport; impl Violation for PytestIncorrectPytestImport { diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs index 4931a24c9d..1981f8e586 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs @@ -64,6 +64,7 @@ use crate::rules::flake8_pytest_style::helpers::{Parentheses, get_mark_decorator /// ## References /// - [`pytest` documentation: Marks](https://docs.pytest.org/en/latest/reference/reference.html#marks) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestIncorrectMarkParenthesesStyle { mark_name: String, expected_parens: Parentheses, @@ -119,6 +120,7 @@ impl AlwaysFixableViolation for PytestIncorrectMarkParenthesesStyle { /// ## References /// - [`pytest` documentation: `pytest.mark.usefixtures`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-mark-usefixtures) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestUseFixturesWithoutParameters; impl AlwaysFixableViolation for PytestUseFixturesWithoutParameters { diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs index 8087730352..20b14399f9 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs @@ -66,6 +66,7 @@ use crate::rules::flake8_pytest_style::types; /// ## References /// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestParametrizeNamesWrongType { single_argument: bool, expected: types::ParametrizeNameType, @@ -200,6 +201,7 @@ impl Violation for PytestParametrizeNamesWrongType { /// ## References /// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestParametrizeValuesWrongType { values: types::ParametrizeValuesType, row: types::ParametrizeValuesRowType, @@ -264,6 +266,7 @@ impl Violation for PytestParametrizeValuesWrongType { /// ## References /// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.285")] pub(crate) struct PytestDuplicateParametrizeTestCases { index: usize, } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs index fef3f695eb..8fa6dc83fd 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs @@ -42,6 +42,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `unittest.mock.patch`](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch) /// - [PyPI: `pytest-mock`](https://pypi.org/project/pytest-mock/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestPatchWithLambda; impl Violation for PytestPatchWithLambda { diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs index feacfbe937..04c9a8c372 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs @@ -51,6 +51,7 @@ use crate::rules::flake8_pytest_style::helpers::is_empty_or_null_string; /// ## References /// - [`pytest` documentation: `pytest.raises`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestRaisesWithMultipleStatements; impl Violation for PytestRaisesWithMultipleStatements { @@ -102,6 +103,7 @@ impl Violation for PytestRaisesWithMultipleStatements { /// ## References /// - [`pytest` documentation: `pytest.raises`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestRaisesTooBroad { exception: String, } @@ -147,6 +149,7 @@ impl Violation for PytestRaisesTooBroad { /// ## References /// - [`pytest` documentation: `pytest.raises`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestRaisesWithoutException; impl Violation for PytestRaisesWithoutException { diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/test_functions.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/test_functions.rs index 810e783a2c..f092b67ff7 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/test_functions.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/test_functions.rs @@ -31,6 +31,7 @@ use ruff_text_size::Ranged; /// ## References /// - [Original Pytest issue](https://github.com/pytest-dev/pytest/issues/12693) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct PytestParameterWithDefaultArgument { parameter_name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/warns.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/warns.rs index 55e1a7bb48..3ee1436118 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/warns.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/warns.rs @@ -50,6 +50,7 @@ use crate::rules::flake8_pytest_style::helpers::is_empty_or_null_string; /// ## References /// - [`pytest` documentation: `pytest.warns`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-warns) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct PytestWarnsWithMultipleStatements; impl Violation for PytestWarnsWithMultipleStatements { @@ -101,6 +102,7 @@ impl Violation for PytestWarnsWithMultipleStatements { /// ## References /// - [`pytest` documentation: `pytest.warns`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-warns) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct PytestWarnsTooBroad { warning: String, } @@ -146,6 +148,7 @@ impl Violation for PytestWarnsTooBroad { /// ## References /// - [`pytest` documentation: `pytest.warns`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-warns) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.9.2")] pub(crate) struct PytestWarnsWithoutWarning; impl Violation for PytestWarnsWithoutWarning { diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs index 3a15f45154..59dfdf857e 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs @@ -34,6 +34,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// /// [formatter]: https://docs.astral.sh/ruff/formatter #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.88")] pub(crate) struct AvoidableEscapedQuote; impl AlwaysFixableViolation for AvoidableEscapedQuote { diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index aae3f18480..43fbf7ad6d 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs @@ -37,6 +37,7 @@ use crate::rules::flake8_quotes::settings::Quote; /// /// [formatter]: https://docs.astral.sh/ruff/formatter #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.88")] pub(crate) struct BadQuotesInlineString { preferred_quote: Quote, } @@ -94,6 +95,7 @@ impl Violation for BadQuotesInlineString { /// /// [formatter]: https://docs.astral.sh/ruff/formatter #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.88")] pub(crate) struct BadQuotesMultilineString { preferred_quote: Quote, } @@ -149,6 +151,7 @@ impl AlwaysFixableViolation for BadQuotesMultilineString { /// /// [formatter]: https://docs.astral.sh/ruff/formatter #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.88")] pub(crate) struct BadQuotesDocstring { preferred_quote: Quote, } diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs index 4100097e03..6b76f4b617 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs @@ -33,6 +33,7 @@ use crate::rules::flake8_quotes::helpers::{contains_escaped_quote, raw_contents, /// /// [formatter]: https://docs.astral.sh/ruff/formatter #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct UnnecessaryEscapedQuote; impl AlwaysFixableViolation for UnnecessaryEscapedQuote { diff --git a/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs b/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs index e3a66cf35b..07ca59a9a2 100644 --- a/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs +++ b/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs @@ -43,6 +43,7 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## References /// - [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.239")] pub(crate) struct UnnecessaryParenOnRaiseException; impl AlwaysFixableViolation for UnnecessaryParenOnRaiseException { diff --git a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs index 8fd3ae0b44..018ddd925b 100644 --- a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs @@ -56,6 +56,7 @@ use crate::rules::flake8_return::visitor::{ReturnVisitor, Stack}; /// This rule's fix is marked as unsafe for cases in which comments would be /// dropped from the `return` statement. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.154")] pub(crate) struct UnnecessaryReturnNone; impl AlwaysFixableViolation for UnnecessaryReturnNone { @@ -97,6 +98,7 @@ impl AlwaysFixableViolation for UnnecessaryReturnNone { /// return 1 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.154")] pub(crate) struct ImplicitReturnValue; impl AlwaysFixableViolation for ImplicitReturnValue { @@ -135,6 +137,7 @@ impl AlwaysFixableViolation for ImplicitReturnValue { /// return None /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.154")] pub(crate) struct ImplicitReturn; impl AlwaysFixableViolation for ImplicitReturn { @@ -170,6 +173,7 @@ impl AlwaysFixableViolation for ImplicitReturn { /// return 1 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.154")] pub(crate) struct UnnecessaryAssign { name: String, } @@ -212,6 +216,7 @@ impl AlwaysFixableViolation for UnnecessaryAssign { /// return baz /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.154")] pub(crate) struct SuperfluousElseReturn { branch: Branch, } @@ -256,6 +261,7 @@ impl Violation for SuperfluousElseReturn { /// raise Exception(baz) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.154")] pub(crate) struct SuperfluousElseRaise { branch: Branch, } @@ -302,6 +308,7 @@ impl Violation for SuperfluousElseRaise { /// x = 0 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.154")] pub(crate) struct SuperfluousElseContinue { branch: Branch, } @@ -348,6 +355,7 @@ impl Violation for SuperfluousElseContinue { /// x = 0 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.154")] pub(crate) struct SuperfluousElseBreak { branch: Branch, } diff --git a/crates/ruff_linter/src/rules/flake8_self/rules/private_member_access.rs b/crates/ruff_linter/src/rules/flake8_self/rules/private_member_access.rs index 2326e24ea3..1b2db08b00 100644 --- a/crates/ruff_linter/src/rules/flake8_self/rules/private_member_access.rs +++ b/crates/ruff_linter/src/rules/flake8_self/rules/private_member_access.rs @@ -55,6 +55,7 @@ use crate::rules::pylint::helpers::is_dunder_operator_method; /// ## References /// - [_What is the meaning of single or double underscores before an object name?_](https://stackoverflow.com/questions/1301346/what-is-the-meaning-of-single-and-double-underscore-before-an-object-name) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.240")] pub(crate) struct PrivateMemberAccess { access: String, } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs index 398bd63860..0b53a271f4 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs @@ -45,6 +45,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `isinstance`](https://docs.python.org/3/library/functions.html#isinstance) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.212")] pub(crate) struct DuplicateIsinstanceCall { name: Option, } @@ -93,6 +94,7 @@ impl Violation for DuplicateIsinstanceCall { /// ## References /// - [Python documentation: Membership test operations](https://docs.python.org/3/reference/expressions.html#membership-test-operations) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.213")] pub(crate) struct CompareWithTuple { replacement: String, } @@ -126,6 +128,7 @@ impl AlwaysFixableViolation for CompareWithTuple { /// ## References /// - [Python documentation: Boolean operations](https://docs.python.org/3/reference/expressions.html#boolean-operations) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.211")] pub(crate) struct ExprAndNotExpr { name: String, } @@ -158,6 +161,7 @@ impl AlwaysFixableViolation for ExprAndNotExpr { /// ## References /// - [Python documentation: Boolean operations](https://docs.python.org/3/reference/expressions.html#boolean-operations) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.211")] pub(crate) struct ExprOrNotExpr { name: String, } @@ -210,6 +214,7 @@ pub(crate) enum ContentAround { /// a = x or [1] /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct ExprOrTrue { expr: String, remove: ContentAround, @@ -262,6 +267,7 @@ impl AlwaysFixableViolation for ExprOrTrue { /// a = x and [] /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct ExprAndFalse { expr: String, remove: ContentAround, diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs index d48598fda7..58860dc56e 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs @@ -41,6 +41,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `os.environ`](https://docs.python.org/3/library/os.html#os.environ) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.218")] pub(crate) struct UncapitalizedEnvironmentVariables { expected: SourceCodeSnippet, actual: SourceCodeSnippet, @@ -91,6 +92,7 @@ impl Violation for UncapitalizedEnvironmentVariables { /// ## References /// - [Python documentation: `dict.get`](https://docs.python.org/3/library/stdtypes.html#dict.get) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.261")] pub(crate) struct DictGetWithNoneDefault { expected: SourceCodeSnippet, actual: SourceCodeSnippet, diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs index 1f4c12179f..15b2f54bf8 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs @@ -37,6 +37,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: Truth Value Testing](https://docs.python.org/3/library/stdtypes.html#truth-value-testing) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.214")] pub(crate) struct IfExprWithTrueFalse { is_compare: bool, } @@ -85,6 +86,7 @@ impl Violation for IfExprWithTrueFalse { /// ## References /// - [Python documentation: Truth Value Testing](https://docs.python.org/3/library/stdtypes.html#truth-value-testing) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.214")] pub(crate) struct IfExprWithFalseTrue; impl AlwaysFixableViolation for IfExprWithFalseTrue { @@ -118,6 +120,7 @@ impl AlwaysFixableViolation for IfExprWithFalseTrue { /// ## References /// - [Python documentation: Truth Value Testing](https://docs.python.org/3/library/stdtypes.html#truth-value-testing) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.214")] pub(crate) struct IfExprWithTwistedArms { expr_body: String, expr_else: String, diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_unary_op.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_unary_op.rs index d00393aa66..f7977066cf 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_unary_op.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_unary_op.rs @@ -33,6 +33,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.213")] pub(crate) struct NegateEqualOp { left: String, right: String, @@ -75,6 +76,7 @@ impl AlwaysFixableViolation for NegateEqualOp { /// ## References /// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.213")] pub(crate) struct NegateNotEqualOp { left: String, right: String, @@ -112,6 +114,7 @@ impl AlwaysFixableViolation for NegateNotEqualOp { /// ## References /// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.213")] pub(crate) struct DoubleNegation { expr: String, } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_with.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_with.rs index 973adba1a8..c115122453 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_with.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_with.rs @@ -47,6 +47,7 @@ use crate::{FixAvailability, Violation}; /// ## References /// - [Python documentation: The `with` statement](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.211")] pub(crate) struct MultipleWithStatements; impl Violation for MultipleWithStatements { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs index 9131a0369c..f0d726cc52 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs @@ -46,6 +46,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: The `if` statement](https://docs.python.org/3/reference/compound_stmts.html#the-if-statement) /// - [Python documentation: Boolean operations](https://docs.python.org/3/reference/expressions.html#boolean-operations) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.211")] pub(crate) struct CollapsibleIf; impl Violation for CollapsibleIf { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/enumerate_for_loop.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/enumerate_for_loop.rs index 4f4d0c3968..6739fa3868 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/enumerate_for_loop.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/enumerate_for_loop.rs @@ -36,6 +36,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `enumerate`](https://docs.python.org/3/library/functions.html#enumerate) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct EnumerateForLoop { index: String, } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs index 1441c87154..91f837647f 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs @@ -53,6 +53,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: Mapping Types](https://docs.python.org/3/library/stdtypes.html#mapping-types-dict) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.219")] pub(crate) struct IfElseBlockInsteadOfDictGet { contents: String, } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_lookup.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_lookup.rs index e3e7a41b63..8b0454355b 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_lookup.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_lookup.rs @@ -36,6 +36,7 @@ use crate::checkers::ast::Checker; /// return phrases.get(x, "Goodnight") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.250")] pub(crate) struct IfElseBlockInsteadOfDictLookup; impl Violation for IfElseBlockInsteadOfDictLookup { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs index db904d6dd1..f69098f697 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs @@ -61,6 +61,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// [code coverage]: https://github.com/nedbat/coveragepy/issues/509 /// [pycodestyle.max-line-length]: https://docs.astral.sh/ruff/settings/#lint_pycodestyle_max-line-length #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.213")] pub(crate) struct IfElseBlockInsteadOfIfExp { /// The ternary or binary expression to replace the `if`-`else`-block. contents: String, diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs index 404c78e260..92182b383a 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs @@ -36,6 +36,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// print("Hello") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.246")] pub(crate) struct IfWithSameArms; impl Violation for IfWithSameArms { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs index da647235e1..5ced08a673 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs @@ -38,6 +38,7 @@ use crate::{Applicability, Edit}; /// ## References /// - [Python documentation: Mapping Types](https://docs.python.org/3/library/stdtypes.html#mapping-types-dict) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.176")] pub(crate) struct InDictKeys { operator: String, } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/needless_bool.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/needless_bool.rs index 1d64db5fc8..4b2dbb7d9a 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/needless_bool.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/needless_bool.rs @@ -56,6 +56,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: Truth Value Testing](https://docs.python.org/3/library/stdtypes.html#truth-value-testing) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.214")] pub(crate) struct NeedlessBool { condition: Option, negate: bool, diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs index 33c789455f..a21af6475f 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs @@ -34,6 +34,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.219")] pub(crate) struct OpenFileWithContextHandler; impl Violation for OpenFileWithContextHandler { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs index 5ad99330e8..9c216311ed 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -44,6 +44,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `any`](https://docs.python.org/3/library/functions.html#any) /// - [Python documentation: `all`](https://docs.python.org/3/library/functions.html#all) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.211")] pub(crate) struct ReimplementedBuiltin { replacement: String, } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs index 636b490673..e574f51adc 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs @@ -41,6 +41,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: Defining Clean-up Actions](https://docs.python.org/3/tutorial/errors.html#defining-clean-up-actions) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.211")] pub(crate) struct ReturnInTryExceptFinally; impl Violation for ReturnInTryExceptFinally { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/split_static_string.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/split_static_string.rs index 05e5eddf4f..ffd4ee30c5 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/split_static_string.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/split_static_string.rs @@ -47,6 +47,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `str.split`](https://docs.python.org/3/library/stdtypes.html#str.split) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct SplitStaticString { method: Method, } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/suppressible_exception.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/suppressible_exception.rs index d3b93e3dbb..2dc4a778f4 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/suppressible_exception.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/suppressible_exception.rs @@ -43,6 +43,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `try` statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement) /// - [a simpler `try`/`except` (and why maybe shouldn't)](https://www.youtube.com/watch?v=MZAJ8qnC7mk) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.211")] pub(crate) struct SuppressibleException { exception: String, } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/yoda_conditions.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/yoda_conditions.rs index a21b960af0..687729c20c 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/yoda_conditions.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/yoda_conditions.rs @@ -48,6 +48,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons) /// - [Python documentation: Assignment statements](https://docs.python.org/3/reference/simple_stmts.html#assignment-statements) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.207")] pub(crate) struct YodaConditions { suggestion: Option, } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/zip_dict_keys_and_values.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/zip_dict_keys_and_values.rs index 28d123fab3..19169b72c7 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/zip_dict_keys_and_values.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/zip_dict_keys_and_values.rs @@ -33,6 +33,7 @@ use crate::{checkers::ast::Checker, fix::snippet::SourceCodeSnippet}; /// ## References /// - [Python documentation: `dict.items`](https://docs.python.org/3/library/stdtypes.html#dict.items) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct ZipDictKeysAndValues { expected: SourceCodeSnippet, actual: SourceCodeSnippet, diff --git a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs index 37d2084c96..142233a4bf 100644 --- a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs +++ b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs @@ -47,6 +47,7 @@ use crate::rules::flake8_slots::helpers::has_slots; /// ## References /// - [Python documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.273")] pub(crate) struct NoSlotsInNamedtupleSubclass(NamedTupleKind); impl Violation for NoSlotsInNamedtupleSubclass { diff --git a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs index 198071aaa4..6ab13988e7 100644 --- a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs +++ b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs @@ -39,6 +39,7 @@ use crate::rules::flake8_slots::helpers::has_slots; /// ## References /// - [Python documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.273")] pub(crate) struct NoSlotsInStrSubclass; impl Violation for NoSlotsInStrSubclass { diff --git a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs index dd9cea0ad3..addc7bd421 100644 --- a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs +++ b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs @@ -40,6 +40,7 @@ use crate::rules::flake8_slots::helpers::has_slots; /// ## References /// - [Python documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.273")] pub(crate) struct NoSlotsInTupleSubclass; impl Violation for NoSlotsInTupleSubclass { diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs index dc352e3e4e..6ada015222 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs @@ -25,6 +25,7 @@ use crate::rules::flake8_tidy_imports::matchers::NameMatchPolicy; /// ## Options /// - `lint.flake8-tidy-imports.banned-api` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.201")] pub(crate) struct BannedApi { name: String, message: String, diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_module_level_imports.rs b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_module_level_imports.rs index 6527d8d522..23d250f7a9 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_module_level_imports.rs +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_module_level_imports.rs @@ -43,6 +43,7 @@ use crate::rules::flake8_tidy_imports::matchers::{MatchName, MatchNameOrParent, /// ## Options /// - `lint.flake8-tidy-imports.banned-module-level-imports` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.285")] pub(crate) struct BannedModuleLevelImports { name: String, } 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 e88906787b..bc77a55716 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 @@ -46,6 +46,7 @@ use crate::rules::flake8_tidy_imports::settings::Strictness; /// /// [PEP 8]: https://peps.python.org/pep-0008/#imports #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.169")] pub(crate) struct RelativeImports { strictness: Strictness, } diff --git a/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs b/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs index 24125023a6..b46e4d44aa 100644 --- a/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs +++ b/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs @@ -31,6 +31,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix, Violation}; /// # TODO(ruff): this is now fixed! /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.269")] pub(crate) struct InvalidTodoTag { pub tag: String, } @@ -61,6 +62,7 @@ impl Violation for InvalidTodoTag { /// # TODO(charlie): now an author is assigned /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.269")] pub(crate) struct MissingTodoAuthor; impl Violation for MissingTodoAuthor { @@ -102,6 +104,7 @@ impl Violation for MissingTodoAuthor { /// # SIXCHR-003 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.269")] pub(crate) struct MissingTodoLink; impl Violation for MissingTodoLink { @@ -131,6 +134,7 @@ impl Violation for MissingTodoLink { /// # TODO(charlie): colon fixed /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.269")] pub(crate) struct MissingTodoColon; impl Violation for MissingTodoColon { @@ -158,6 +162,7 @@ impl Violation for MissingTodoColon { /// # TODO(charlie): fix some issue /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.269")] pub(crate) struct MissingTodoDescription; impl Violation for MissingTodoDescription { @@ -185,6 +190,7 @@ impl Violation for MissingTodoDescription { /// # TODO(charlie): this is capitalized /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.269")] pub(crate) struct InvalidTodoCapitalization { tag: String, } @@ -222,6 +228,7 @@ impl AlwaysFixableViolation for InvalidTodoCapitalization { /// # TODO(charlie): fix this /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.269")] pub(crate) struct MissingSpaceAfterTodoColon; impl Violation for MissingSpaceAfterTodoColon { diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs index c6b935929d..4299a91804 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs @@ -33,6 +33,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// ## References /// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct EmptyTypeCheckingBlock; impl AlwaysFixableViolation for EmptyTypeCheckingBlock { diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_cast_value.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_cast_value.rs index 1db267f677..40c210dee1 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_cast_value.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_cast_value.rs @@ -43,6 +43,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// This fix is safe as long as the type expression doesn't span multiple /// lines and includes comments on any of the lines apart from the last one. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct RuntimeCastValue; impl AlwaysFixableViolation for RuntimeCastValue { diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index e3cbf17a02..e96b3b7bc6 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -54,6 +54,7 @@ use crate::{Fix, FixAvailability, Violation}; /// ## References /// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct RuntimeImportInTypeCheckingBlock { qualified_name: String, strategy: Strategy, diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_string_union.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_string_union.rs index 1c103e0db8..40c6a066b6 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_string_union.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_string_union.rs @@ -53,6 +53,7 @@ use crate::checkers::ast::Checker; /// /// [PEP 604]: https://peps.python.org/pep-0604/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct RuntimeStringUnion; impl Violation for RuntimeStringUnion { diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index 53c1d004c3..776ce1486e 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -49,6 +49,7 @@ use ruff_python_ast::parenthesize::parenthesized_range; /// /// [PEP 613]: https://peps.python.org/pep-0613/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct UnquotedTypeAlias; impl Violation for UnquotedTypeAlias { @@ -133,6 +134,7 @@ impl Violation for UnquotedTypeAlias { /// [PYI020]: https://docs.astral.sh/ruff/rules/quoted-annotation-in-stub/ /// [UP037]: https://docs.astral.sh/ruff/rules/quoted-annotation/ #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.8.1")] pub(crate) struct QuotedTypeAlias; impl AlwaysFixableViolation for QuotedTypeAlias { diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 9327f6803c..f1918596b1 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -81,6 +81,7 @@ use crate::{Fix, FixAvailability, Violation}; /// ## References /// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct TypingOnlyFirstPartyImport { qualified_name: String, } @@ -163,6 +164,7 @@ impl Violation for TypingOnlyFirstPartyImport { /// ## References /// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct TypingOnlyThirdPartyImport { qualified_name: String, } @@ -245,6 +247,7 @@ impl Violation for TypingOnlyThirdPartyImport { /// ## References /// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct TypingOnlyStandardLibraryImport { qualified_name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs b/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs index 2aff59aa32..18d0a96377 100644 --- a/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs +++ b/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs @@ -36,6 +36,7 @@ use crate::registry::Rule; /// ## Options /// - `lint.dummy-variable-rgx` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.168")] pub(crate) struct UnusedFunctionArgument { name: String, } @@ -76,6 +77,7 @@ impl Violation for UnusedFunctionArgument { /// ## Options /// - `lint.dummy-variable-rgx` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.168")] pub(crate) struct UnusedMethodArgument { name: String, } @@ -118,6 +120,7 @@ impl Violation for UnusedMethodArgument { /// ## Options /// - `lint.dummy-variable-rgx` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.168")] pub(crate) struct UnusedClassMethodArgument { name: String, } @@ -160,6 +163,7 @@ impl Violation for UnusedClassMethodArgument { /// ## Options /// - `lint.dummy-variable-rgx` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.168")] pub(crate) struct UnusedStaticMethodArgument { name: String, } @@ -199,6 +203,7 @@ impl Violation for UnusedStaticMethodArgument { /// ## Options /// - `lint.dummy-variable-rgx` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.168")] pub(crate) struct UnusedLambdaArgument { name: String, } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/builtin_open.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/builtin_open.rs index e6802131c2..c88519c844 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/builtin_open.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/builtin_open.rs @@ -50,6 +50,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct BuiltinOpen; impl Violation for BuiltinOpen { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/glob_rule.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/glob_rule.rs index 3c329bf20d..b131679ff4 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/glob_rule.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/glob_rule.rs @@ -51,6 +51,7 @@ use crate::Violation; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.281")] pub(crate) struct Glob { pub function: String, } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/invalid_pathlib_with_suffix.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/invalid_pathlib_with_suffix.rs index 3bd4c5a091..fc66c33855 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/invalid_pathlib_with_suffix.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/invalid_pathlib_with_suffix.rs @@ -57,6 +57,7 @@ use ruff_text_size::Ranged; /// /// No fix is offered if the suffix `"."` is given, since the intent is unclear. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct InvalidPathlibWithSuffix { single_dot: bool, } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_chmod.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_chmod.rs index 26366c8808..a6f851ca89 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_chmod.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_chmod.rs @@ -51,6 +51,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsChmod; impl Violation for OsChmod { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_getcwd.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_getcwd.rs index 33e45b488b..7bb533246d 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_getcwd.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_getcwd.rs @@ -47,6 +47,7 @@ use ruff_text_size::Ranged; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsGetcwd; impl Violation for OsGetcwd { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_makedirs.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_makedirs.rs index f2c668c9b0..27aec3e66c 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_makedirs.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_makedirs.rs @@ -50,6 +50,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsMakedirs; impl Violation for OsMakedirs { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_mkdir.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_mkdir.rs index c744d3ebd2..86eec34df9 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_mkdir.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_mkdir.rs @@ -51,6 +51,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsMkdir; impl Violation for OsMkdir { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_abspath.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_abspath.rs index 419c05bccc..9b58561e88 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_abspath.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_abspath.rs @@ -53,6 +53,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsPathAbspath; impl Violation for OsPathAbspath { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_basename.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_basename.rs index b517d00a7b..ca69d07ce3 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_basename.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_basename.rs @@ -55,6 +55,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsPathBasename; impl Violation for OsPathBasename { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_dirname.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_dirname.rs index e8628647a5..d3175c2035 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_dirname.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_dirname.rs @@ -55,6 +55,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsPathDirname; impl Violation for OsPathDirname { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_exists.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_exists.rs index 53e441883d..f3fe32a641 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_exists.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_exists.rs @@ -45,6 +45,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsPathExists; impl Violation for OsPathExists { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_expanduser.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_expanduser.rs index f1110f7d9e..d544acde39 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_expanduser.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_expanduser.rs @@ -49,6 +49,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsPathExpanduser; impl Violation for OsPathExpanduser { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getatime.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getatime.rs index 7797ea5745..0f148f4033 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getatime.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getatime.rs @@ -47,6 +47,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.279")] pub(crate) struct OsPathGetatime; impl Violation for OsPathGetatime { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getctime.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getctime.rs index 873a229865..86bce28aed 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getctime.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getctime.rs @@ -47,6 +47,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.279")] pub(crate) struct OsPathGetctime; impl Violation for OsPathGetctime { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getmtime.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getmtime.rs index 0d3cda75cd..42e77e3fe9 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getmtime.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getmtime.rs @@ -47,6 +47,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.279")] pub(crate) struct OsPathGetmtime; impl Violation for OsPathGetmtime { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getsize.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getsize.rs index fe3baf4241..a945b2224c 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getsize.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getsize.rs @@ -47,6 +47,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.279")] pub(crate) struct OsPathGetsize; impl Violation for OsPathGetsize { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isabs.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isabs.rs index 355c6987a4..b1c8cb33c3 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isabs.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isabs.rs @@ -44,6 +44,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsPathIsabs; impl Violation for OsPathIsabs { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isdir.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isdir.rs index 3c8ee3f7a6..a2c1b8620f 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isdir.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isdir.rs @@ -45,6 +45,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsPathIsdir; impl Violation for OsPathIsdir { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isfile.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isfile.rs index a1ed2a5601..d31e39eef7 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isfile.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isfile.rs @@ -45,6 +45,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsPathIsfile; impl Violation for OsPathIsfile { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_islink.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_islink.rs index 5b6c879de9..d958a2c19c 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_islink.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_islink.rs @@ -45,6 +45,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsPathIslink; impl Violation for OsPathIslink { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_samefile.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_samefile.rs index e0d563be22..cbf6d7a034 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_samefile.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_samefile.rs @@ -46,6 +46,7 @@ use ruff_python_ast::ExprCall; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsPathSamefile; impl Violation for OsPathSamefile { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_readlink.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_readlink.rs index 3cf990b035..d1df572ed5 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_readlink.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_readlink.rs @@ -47,6 +47,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsReadlink; impl Violation for OsReadlink { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_remove.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_remove.rs index 56975ddc3d..43852e11e2 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_remove.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_remove.rs @@ -47,6 +47,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsRemove; impl Violation for OsRemove { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_rename.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_rename.rs index ada81a0146..c5f2293ee9 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_rename.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_rename.rs @@ -47,6 +47,7 @@ use ruff_python_ast::ExprCall; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsRename; impl Violation for OsRename { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_replace.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_replace.rs index 6670e724ef..ef60099467 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_replace.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_replace.rs @@ -50,6 +50,7 @@ use ruff_python_ast::ExprCall; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsReplace; impl Violation for OsReplace { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_rmdir.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_rmdir.rs index 0e0320b6eb..a044e541b9 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_rmdir.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_rmdir.rs @@ -47,6 +47,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsRmdir; impl Violation for OsRmdir { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_sep_split.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_sep_split.rs index f6e01d037c..8a74dd1106 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_sep_split.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_sep_split.rs @@ -53,6 +53,7 @@ use crate::checkers::ast::Checker; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.281")] pub(crate) struct OsSepSplit; impl Violation for OsSepSplit { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_symlink.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_symlink.rs index 2ba322ab8f..6e54acabb4 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_symlink.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_symlink.rs @@ -48,6 +48,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.13.0")] pub(crate) struct OsSymlink; impl Violation for OsSymlink { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_unlink.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_unlink.rs index d071b3cebc..9f49025465 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_unlink.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_unlink.rs @@ -47,6 +47,7 @@ use crate::{FixAvailability, Violation}; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsUnlink; impl Violation for OsUnlink { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs index c909c312ac..bf5a4ed8b7 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs @@ -41,6 +41,7 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## References /// - [Python documentation: `Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.279")] pub(crate) struct PathConstructorCurrentDirectory; impl AlwaysFixableViolation for PathConstructorCurrentDirectory { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs index 6108ff3a27..b5bcfdb1e3 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs @@ -47,6 +47,7 @@ use crate::Violation; /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsStat; impl Violation for OsStat { @@ -93,6 +94,7 @@ impl Violation for OsStat { /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsPathJoin { pub(crate) module: String, pub(crate) joiner: Joiner, @@ -164,6 +166,7 @@ pub(crate) enum Joiner { /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct OsPathSplitext; impl Violation for OsPathSplitext { @@ -200,6 +203,7 @@ impl Violation for OsPathSplitext { /// - [Python documentation: `Pathlib`](https://docs.python.org/3/library/pathlib.html) /// - [Path repository](https://github.com/jaraco/path) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct PyPath; impl Violation for PyPath { @@ -258,6 +262,7 @@ impl Violation for PyPath { /// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) /// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct OsListdir; impl Violation for OsListdir { diff --git a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs index e446654457..bdc7340974 100644 --- a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs +++ b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs @@ -39,6 +39,7 @@ use crate::rules::flynt::helpers; /// ## References /// - [Python documentation: f-strings](https://docs.python.org/3/reference/lexical_analysis.html#f-strings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.266")] pub(crate) struct StaticJoinToFString { expression: SourceCodeSnippet, } 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 e674c8fe9a..b887e1c2a0 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 @@ -39,6 +39,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// ## Options /// - `lint.isort.required-imports` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.218")] pub(crate) struct MissingRequiredImport(pub String); impl AlwaysFixableViolation for MissingRequiredImport { diff --git a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs index a8d5d68ffd..febe9fc425 100644 --- a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs @@ -39,6 +39,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ``` /// #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.110")] pub(crate) struct UnsortedImports; impl Violation for UnsortedImports { diff --git a/crates/ruff_linter/src/rules/mccabe/rules/function_is_too_complex.rs b/crates/ruff_linter/src/rules/mccabe/rules/function_is_too_complex.rs index 3648c48239..bc1ca1e278 100644 --- a/crates/ruff_linter/src/rules/mccabe/rules/function_is_too_complex.rs +++ b/crates/ruff_linter/src/rules/mccabe/rules/function_is_too_complex.rs @@ -48,6 +48,7 @@ use crate::checkers::ast::Checker; /// ## Options /// - `lint.mccabe.max-complexity` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.127")] pub(crate) struct ComplexStructure { name: String, complexity: usize, diff --git a/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs b/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs index 1cbae5066d..b945ba8df0 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs @@ -31,6 +31,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// np.all([True, False]) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.276")] pub(crate) struct NumpyDeprecatedFunction { existing: String, replacement: String, diff --git a/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs b/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs index 60967e12b4..c2744dfbe1 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs @@ -31,6 +31,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// int /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.247")] pub(crate) struct NumpyDeprecatedTypeAlias { type_name: String, } diff --git a/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs b/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs index cbe58e2122..625a29829f 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs @@ -46,6 +46,7 @@ use crate::checkers::ast::Checker; /// [Random Sampling]: https://numpy.org/doc/stable/reference/random/index.html#random-quick-start /// [NEP 19]: https://numpy.org/neps/nep-0019-rng-policy.html #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.248")] pub(crate) struct NumpyLegacyRandom { method_name: String, } diff --git a/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs b/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs index bcc712b18d..313e0cc9a8 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs @@ -50,6 +50,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// np.round(arr2) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct Numpy2Deprecation { existing: String, migration_guide: Option, diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/assignment_to_df.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/assignment_to_df.rs index 51c846fd55..fb7ae9713b 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/assignment_to_df.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/assignment_to_df.rs @@ -32,6 +32,7 @@ use crate::{Violation, checkers::ast::Checker}; /// animals = pd.read_csv("animals.csv") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "0.13.0")] pub(crate) struct PandasDfVariableName; impl Violation for PandasDfVariableName { diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/attr.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/attr.rs index ab6b55104b..e867edc5c0 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/attr.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/attr.rs @@ -34,6 +34,7 @@ use crate::rules::pandas_vet::helpers::{Resolution, test_expression}; /// ## References /// - [Pandas documentation: Accessing the values in a Series or Index](https://pandas.pydata.org/pandas-docs/stable/whatsnew/v0.24.0.html#accessing-the-values-in-a-series-or-index) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct PandasUseOfDotValues; impl Violation for PandasUseOfDotValues { diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/call.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/call.rs index 8b7619cf3e..407f689b69 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/call.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/call.rs @@ -40,6 +40,7 @@ use crate::rules::pandas_vet::helpers::{Resolution, test_expression}; /// - [Pandas documentation: `isnull`](https://pandas.pydata.org/docs/reference/api/pandas.isnull.html#pandas.isnull) /// - [Pandas documentation: `isna`](https://pandas.pydata.org/docs/reference/api/pandas.isna.html#pandas.isna) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct PandasUseOfDotIsNull; impl Violation for PandasUseOfDotIsNull { @@ -80,6 +81,7 @@ impl Violation for PandasUseOfDotIsNull { /// - [Pandas documentation: `notnull`](https://pandas.pydata.org/docs/reference/api/pandas.notnull.html#pandas.notnull) /// - [Pandas documentation: `notna`](https://pandas.pydata.org/docs/reference/api/pandas.notna.html#pandas.notna) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct PandasUseOfDotNotNull; impl Violation for PandasUseOfDotNotNull { @@ -116,6 +118,7 @@ impl Violation for PandasUseOfDotNotNull { /// - [Pandas documentation: Reshaping and pivot tables](https://pandas.pydata.org/docs/user_guide/reshaping.html) /// - [Pandas documentation: `pivot_table`](https://pandas.pydata.org/docs/reference/api/pandas.pivot_table.html#pandas.pivot_table) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct PandasUseOfDotPivotOrUnstack; impl Violation for PandasUseOfDotPivotOrUnstack { @@ -153,6 +156,7 @@ impl Violation for PandasUseOfDotPivotOrUnstack { /// - [Pandas documentation: `melt`](https://pandas.pydata.org/docs/reference/api/pandas.melt.html) /// - [Pandas documentation: `stack`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.stack.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct PandasUseOfDotStack; impl Violation for PandasUseOfDotStack { diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs index 100a688b79..0fa448f7c5 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs @@ -36,6 +36,7 @@ use ruff_python_semantic::Modules; /// ## References /// - [_Why You Should Probably Never Use pandas `inplace=True`_](https://towardsdatascience.com/why-you-should-probably-never-use-pandas-inplace-true-9f9f211849e4) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct PandasUseOfInplaceArgument; impl Violation for PandasUseOfInplaceArgument { diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/nunique_constant_series_check.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/nunique_constant_series_check.rs index 2d7ceba14b..279e0a9d69 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/nunique_constant_series_check.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/nunique_constant_series_check.rs @@ -51,6 +51,7 @@ use crate::rules::pandas_vet::helpers::{Resolution, test_expression}; /// - [Pandas Cookbook: "Constant Series"](https://pandas.pydata.org/docs/user_guide/cookbook.html#constant-series) /// - [Pandas documentation: `nunique`](https://pandas.pydata.org/docs/reference/api/pandas.Series.nunique.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.279")] pub(crate) struct PandasNuniqueConstantSeriesCheck; impl Violation for PandasNuniqueConstantSeriesCheck { diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/pd_merge.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/pd_merge.rs index d8c90b74ac..34ca700a13 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/pd_merge.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/pd_merge.rs @@ -44,6 +44,7 @@ use crate::checkers::ast::Checker; /// - [Pandas documentation: `merge`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.merge.html#pandas.DataFrame.merge) /// - [Pandas documentation: `pd.merge`](https://pandas.pydata.org/docs/reference/api/pandas.merge.html#pandas.merge) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct PandasUseOfPdMerge; impl Violation for PandasUseOfPdMerge { diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs index 776e738bbb..79025d2b8b 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs @@ -36,6 +36,7 @@ use crate::checkers::ast::Checker; /// - [Pandas documentation: `read_csv`](https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html#pandas.read_csv) /// - [Pandas documentation: `read_table`](https://pandas.pydata.org/docs/reference/api/pandas.read_table.html#pandas.read_table) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct PandasUseOfDotReadTable; impl Violation for PandasUseOfDotReadTable { diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/subscript.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/subscript.rs index cda65b6554..7ebb5ba0d0 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/subscript.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/subscript.rs @@ -40,6 +40,7 @@ use crate::rules::pandas_vet::helpers::{Resolution, test_expression}; /// - [Pandas documentation: `loc`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.loc.html) /// - [Pandas documentation: `iloc`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.iloc.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct PandasUseOfDotIx; impl Violation for PandasUseOfDotIx { @@ -82,6 +83,7 @@ impl Violation for PandasUseOfDotIx { /// - [Pandas documentation: `loc`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.loc.html) /// - [Pandas documentation: `at`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.at.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct PandasUseOfDotAt; impl Violation for PandasUseOfDotAt { @@ -133,6 +135,7 @@ impl Violation for PandasUseOfDotAt { /// - [Pandas documentation: `iloc`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.iloc.html) /// - [Pandas documentation: `iat`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.iat.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.188")] pub(crate) struct PandasUseOfDotIat; impl Violation for PandasUseOfDotIat { diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs index 6c547ff5c8..6e5c75886a 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs @@ -42,6 +42,7 @@ use crate::rules::pep8_naming::helpers; /// /// [PEP 8]: https://peps.python.org/pep-0008/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.82")] pub(crate) struct CamelcaseImportedAsAcronym { name: String, asname: String, diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs index 609cd61fc0..09f5f2deb0 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs @@ -51,6 +51,7 @@ use crate::rules::pep8_naming::settings::IgnoreNames; /// /// [PEP 8]: https://peps.python.org/pep-0008/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.82")] pub(crate) struct CamelcaseImportedAsConstant { name: String, asname: String, diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs index c81884c045..eb3cf80bdf 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs @@ -36,6 +36,7 @@ use crate::rules::pep8_naming::settings::IgnoreNames; /// /// [PEP 8]: https://peps.python.org/pep-0008/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.82")] pub(crate) struct CamelcaseImportedAsLowercase { name: String, asname: String, diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs index 26e72de928..f88853faab 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs @@ -49,6 +49,7 @@ use crate::rules::pep8_naming::{helpers, settings::IgnoreNames}; /// /// [PEP 8]: https://peps.python.org/pep-0008/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.82")] pub(crate) struct ConstantImportedAsNonConstant { name: String, asname: String, diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/dunder_function_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/dunder_function_name.rs index c4b8f86299..84ec885347 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/dunder_function_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/dunder_function_name.rs @@ -38,6 +38,7 @@ use crate::rules::pep8_naming::settings::IgnoreNames; /// /// [PEP 8]: https://peps.python.org/pep-0008/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.82")] pub(crate) struct DunderFunctionName; impl Violation for DunderFunctionName { diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs index ecaa71ee20..bb990df5ce 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs @@ -35,6 +35,7 @@ use crate::rules::pep8_naming::settings::IgnoreNames; /// /// [PEP 8]: https://peps.python.org/pep-0008/#exception-names #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.89")] pub(crate) struct ErrorSuffixOnExceptionName { name: String, } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_argument_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_argument_name.rs index 5bab2edfc7..a490f34702 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_argument_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_argument_name.rs @@ -44,6 +44,7 @@ use crate::checkers::ast::Checker; /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.77")] pub(crate) struct InvalidArgumentName { name: String, } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_class_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_class_name.rs index c220d4d41a..cf996876c8 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_class_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_class_name.rs @@ -41,6 +41,7 @@ use crate::rules::pep8_naming::settings::IgnoreNames; /// /// [PEP 8]: https://peps.python.org/pep-0008/#class-names #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.77")] pub(crate) struct InvalidClassName { name: String, } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs index ac43d779cc..b174329467 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs @@ -59,6 +59,7 @@ use crate::{Fix, Violation}; /// /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.77")] pub(crate) struct InvalidFirstArgumentNameForMethod { argument_name: String, } @@ -129,6 +130,7 @@ impl Violation for InvalidFirstArgumentNameForMethod { /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments /// [PLW0211]: https://docs.astral.sh/ruff/rules/bad-staticmethod-argument/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.77")] pub(crate) struct InvalidFirstArgumentNameForClassMethod { argument_name: String, // Whether the method is `__new__` diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_function_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_function_name.rs index 5deb83247e..546b41c014 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_function_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_function_name.rs @@ -42,6 +42,7 @@ use crate::rules::pep8_naming::settings::IgnoreNames; /// /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-variable-names #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.77")] pub(crate) struct InvalidFunctionName { name: String, } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_module_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_module_name.rs index 6e7e699282..cbc9b54ae2 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_module_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_module_name.rs @@ -38,6 +38,7 @@ use crate::rules::pep8_naming::settings::IgnoreNames; /// /// [PEP 8]: https://peps.python.org/pep-0008/#package-and-module-names #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.248")] pub(crate) struct InvalidModuleName { name: String, } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs index 6d2df2e8f1..ae246a1f5b 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs @@ -35,6 +35,7 @@ use crate::rules::pep8_naming::settings::IgnoreNames; /// /// [PEP 8]: https://peps.python.org/pep-0008/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.82")] pub(crate) struct LowercaseImportedAsNonLowercase { name: String, asname: String, diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs index 1ad5f88381..8e3842a0ef 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs @@ -41,6 +41,7 @@ use crate::rules::pep8_naming::helpers; /// /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.89")] pub(crate) struct MixedCaseVariableInClassScope { name: String, } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs index e7f87f4c4e..ae4b159bf2 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs @@ -52,6 +52,7 @@ use crate::rules::pep8_naming::helpers; /// /// [PEP 8]: https://peps.python.org/pep-0008/#global-variable-names #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.89")] pub(crate) struct MixedCaseVariableInGlobalScope { name: String, } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs index d066363122..afcf42309a 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs @@ -39,6 +39,7 @@ use crate::rules::pep8_naming::helpers; /// /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-variable-names #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.89")] pub(crate) struct NonLowercaseVariableInFunction { name: String, } 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 b26e7b309b..b24e8fa793 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 @@ -44,6 +44,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// (e.g., if it is missing a `.keys()` or `.values()` method, or if those /// methods behave differently than they do on standard mapping types). #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.273")] pub(crate) struct IncorrectDictIterator { subset: DictSubset, } 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 99f34f620a..560aac29a8 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 @@ -46,6 +46,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// result.update({x: y for x, y in pairs if y % 2}) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct ManualDictComprehension { fix_type: DictComprehensionType, is_async: bool, 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 d09e42db8d..84b7290e51 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 @@ -49,6 +49,7 @@ use ruff_text_size::{Ranged, TextRange}; /// filtered.extend(x for x in original if x % 2) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.276")] pub(crate) struct ManualListComprehension { is_async: bool, comprehension_type: Option, diff --git a/crates/ruff_linter/src/rules/perflint/rules/manual_list_copy.rs b/crates/ruff_linter/src/rules/perflint/rules/manual_list_copy.rs index 3322830aca..44122f25b3 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/manual_list_copy.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/manual_list_copy.rs @@ -35,6 +35,7 @@ use crate::checkers::ast::Checker; /// filtered = list(original) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.276")] pub(crate) struct ManualListCopy; impl Violation for ManualListCopy { diff --git a/crates/ruff_linter/src/rules/perflint/rules/try_except_in_loop.rs b/crates/ruff_linter/src/rules/perflint/rules/try_except_in_loop.rs index 87f89bf18e..9fdcc29446 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/try_except_in_loop.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/try_except_in_loop.rs @@ -77,6 +77,7 @@ use crate::checkers::ast::Checker; /// ## Options /// - `target-version` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.276")] pub(crate) struct TryExceptInLoop; impl Violation for TryExceptInLoop { diff --git a/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs b/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs index 8949c0997d..f616e0f541 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs @@ -51,6 +51,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// print(i) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.276")] pub(crate) struct UnnecessaryListCast; impl AlwaysFixableViolation for UnnecessaryListCast { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_class_name.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_class_name.rs index 6bf061ede5..eb2bb786ba 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_class_name.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_class_name.rs @@ -26,6 +26,7 @@ use crate::rules::pycodestyle::helpers::is_ambiguous_name; /// class Integer(object): ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.35")] pub(crate) struct AmbiguousClassName(pub String); impl Violation for AmbiguousClassName { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_function_name.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_function_name.rs index 1cd8770c86..f9d53f25b8 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_function_name.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_function_name.rs @@ -26,6 +26,7 @@ use crate::rules::pycodestyle::helpers::is_ambiguous_name; /// def long_name(x): ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.35")] pub(crate) struct AmbiguousFunctionName(pub String); impl Violation for AmbiguousFunctionName { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_variable_name.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_variable_name.rs index 156b336a47..d618e3d116 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_variable_name.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_variable_name.rs @@ -33,6 +33,7 @@ use crate::rules::pycodestyle::helpers::is_ambiguous_name; /// i = 42 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.34")] pub(crate) struct AmbiguousVariableName(pub String); impl Violation for AmbiguousVariableName { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/bare_except.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/bare_except.rs index 3040240166..82bbea5beb 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/bare_except.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/bare_except.rs @@ -45,6 +45,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) /// - [Google Python Style Guide: "Exceptions"](https://google.github.io/styleguide/pyguide.html#24-exceptions) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.36")] pub(crate) struct BareExcept; impl Violation for BareExcept { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs index 5e44676ca4..978806ee95 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs @@ -62,6 +62,7 @@ const BLANK_LINES_NESTED_LEVEL: u32 = 1; /// - [Flake 8 rule](https://www.flake8rules.com/rules/E301.html) /// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.2.2")] pub(crate) struct BlankLineBetweenMethods; impl AlwaysFixableViolation for BlankLineBetweenMethods { @@ -115,6 +116,7 @@ impl AlwaysFixableViolation for BlankLineBetweenMethods { /// - [Flake 8 rule](https://www.flake8rules.com/rules/E302.html) /// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.2.2")] pub(crate) struct BlankLinesTopLevel { actual_blank_lines: u32, expected_blank_lines: u32, @@ -182,6 +184,7 @@ impl AlwaysFixableViolation for BlankLinesTopLevel { /// - [Flake 8 rule](https://www.flake8rules.com/rules/E303.html) /// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.2.2")] pub(crate) struct TooManyBlankLines { actual_blank_lines: u32, } @@ -228,6 +231,7 @@ impl AlwaysFixableViolation for TooManyBlankLines { /// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines) /// - [Flake 8 rule](https://www.flake8rules.com/rules/E304.html) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.2.2")] pub(crate) struct BlankLineAfterDecorator { actual_blank_lines: u32, } @@ -279,6 +283,7 @@ impl AlwaysFixableViolation for BlankLineAfterDecorator { /// - [Flake 8 rule](https://www.flake8rules.com/rules/E305.html) /// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.2.2")] pub(crate) struct BlankLinesAfterFunctionOrClass { actual_blank_lines: u32, } @@ -333,6 +338,7 @@ impl AlwaysFixableViolation for BlankLinesAfterFunctionOrClass { /// - [Flake 8 rule](https://www.flake8rules.com/rules/E306.html) /// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.2.2")] pub(crate) struct BlankLinesBeforeNestedDefinition; impl AlwaysFixableViolation for BlankLinesBeforeNestedDefinition { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/compound_statements.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/compound_statements.rs index 49a2fb1db5..8fd4889f3b 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/compound_statements.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/compound_statements.rs @@ -29,6 +29,7 @@ use crate::{Edit, Fix}; /// /// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.245")] pub(crate) struct MultipleStatementsOnOneLineColon; impl Violation for MultipleStatementsOnOneLineColon { @@ -59,6 +60,7 @@ impl Violation for MultipleStatementsOnOneLineColon { /// /// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.245")] pub(crate) struct MultipleStatementsOnOneLineSemicolon; impl Violation for MultipleStatementsOnOneLineSemicolon { @@ -84,6 +86,7 @@ impl Violation for MultipleStatementsOnOneLineSemicolon { /// do_four() /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.245")] pub(crate) struct UselessSemicolon; impl AlwaysFixableViolation for UselessSemicolon { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/doc_line_too_long.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/doc_line_too_long.rs index c9f0ae0753..9e59ce5a79 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/doc_line_too_long.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/doc_line_too_long.rs @@ -72,6 +72,7 @@ use crate::settings::LinterSettings; /// /// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.219")] pub(crate) struct DocLineTooLong(usize, usize); impl Violation for DocLineTooLong { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs index 58baf660bc..ad74b42169 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs @@ -25,6 +25,7 @@ use crate::Violation; /// - [UNIX Permissions introduction](https://mason.gmu.edu/~montecin/UNIXpermiss.htm) /// - [Command Line Basics: Symbolic Links](https://www.digitalocean.com/community/tutorials/workflow-symbolic-links) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.28")] pub struct IOError { pub message: String, } @@ -65,6 +66,7 @@ impl Violation for IOError { /// - [Python documentation: Syntax Errors](https://docs.python.org/3/tutorial/errors.html#syntax-errors) #[derive(ViolationMetadata)] #[deprecated(note = "E999 has been removed")] +#[violation_metadata(removed_since = "0.8.0")] pub(crate) struct SyntaxError; #[expect(deprecated)] diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index 1e0fe8507a..0d6ea9c1a6 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -41,6 +41,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: String and Bytes literals](https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.85")] pub(crate) struct InvalidEscapeSequence { ch: char, fix_title: FixTitle, diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs index 35839f78f7..a42473386b 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -36,6 +36,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// /// [PEP 8]: https://peps.python.org/pep-0008/#programming-recommendations #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.28")] pub(crate) struct LambdaAssignment { name: String, } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/line_too_long.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/line_too_long.rs index 9a03e0f98a..b81cbdf7b1 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/line_too_long.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/line_too_long.rs @@ -70,6 +70,7 @@ use crate::settings::LinterSettings; /// /// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.18")] pub(crate) struct LineTooLong(usize, usize); impl Violation for LineTooLong { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs index baf229ada8..6ae6cea817 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs @@ -58,6 +58,7 @@ impl EqCmpOp { /// [PEP 8]: https://peps.python.org/pep-0008/#programming-recommendations /// [this issue]: https://github.com/astral-sh/ruff/issues/4560 #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.28")] pub(crate) struct NoneComparison(EqCmpOp); impl AlwaysFixableViolation for NoneComparison { @@ -120,6 +121,7 @@ impl AlwaysFixableViolation for NoneComparison { /// [PEP 8]: https://peps.python.org/pep-0008/#programming-recommendations /// [this issue]: https://github.com/astral-sh/ruff/issues/4560 #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.28")] pub(crate) struct TrueFalseComparison { value: bool, op: EqCmpOp, diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs index 6dd9b49356..ff57fca1fe 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs @@ -31,6 +31,7 @@ use super::{LogicalLine, Whitespace}; /// /// [PEP 8]: https://peps.python.org/pep-0008/#pet-peeves #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct WhitespaceAfterOpenBracket { symbol: char, } @@ -70,6 +71,7 @@ impl AlwaysFixableViolation for WhitespaceAfterOpenBracket { /// /// [PEP 8]: https://peps.python.org/pep-0008/#pet-peeves #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct WhitespaceBeforeCloseBracket { symbol: char, } @@ -107,6 +109,7 @@ impl AlwaysFixableViolation for WhitespaceBeforeCloseBracket { /// /// [PEP 8]: https://peps.python.org/pep-0008/#pet-peeves #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct WhitespaceBeforePunctuation { symbol: char, } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/indentation.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/indentation.rs index 000fc967d0..5c351f695f 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/indentation.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/indentation.rs @@ -38,6 +38,7 @@ use super::LogicalLine; /// [PEP 8]: https://peps.python.org/pep-0008/#indentation /// [formatter]:https://docs.astral.sh/ruff/formatter/ #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct IndentationWithInvalidMultiple { indent_width: usize, } @@ -83,6 +84,7 @@ impl Violation for IndentationWithInvalidMultiple { /// [PEP 8]: https://peps.python.org/pep-0008/#indentation /// [formatter]:https://docs.astral.sh/ruff/formatter/ #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct IndentationWithInvalidMultipleComment { indent_width: usize, } @@ -116,6 +118,7 @@ impl Violation for IndentationWithInvalidMultipleComment { /// /// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct NoIndentedBlock; impl Violation for NoIndentedBlock { @@ -148,6 +151,7 @@ impl Violation for NoIndentedBlock { /// /// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct NoIndentedBlockComment; impl Violation for NoIndentedBlockComment { @@ -177,6 +181,7 @@ impl Violation for NoIndentedBlockComment { /// /// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct UnexpectedIndentation; impl Violation for UnexpectedIndentation { @@ -206,6 +211,7 @@ impl Violation for UnexpectedIndentation { /// /// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct UnexpectedIndentationComment; impl Violation for UnexpectedIndentationComment { @@ -242,6 +248,7 @@ impl Violation for UnexpectedIndentationComment { /// [PEP 8]: https://peps.python.org/pep-0008/#indentation /// [formatter]:https://docs.astral.sh/ruff/formatter/ #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct OverIndented { is_comment: bool, } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs index 7b9e82c4f7..759bbb29d6 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs @@ -24,6 +24,7 @@ use super::{DefinitionState, LogicalLine}; /// a = (1, 2) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct MissingWhitespace { token: TokenKind, } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs index f0d4600038..c767be0dc8 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs @@ -27,6 +27,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: Keywords](https://docs.python.org/3/reference/lexical_analysis.html#keywords) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct MissingWhitespaceAfterKeyword; impl AlwaysFixableViolation for MissingWhitespaceAfterKeyword { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs index 2b7fdadb10..408575fa23 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs @@ -30,6 +30,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// [PEP 8]: https://peps.python.org/pep-0008/#pet-peeves // E225 #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct MissingWhitespaceAroundOperator; impl AlwaysFixableViolation for MissingWhitespaceAroundOperator { @@ -69,6 +70,7 @@ impl AlwaysFixableViolation for MissingWhitespaceAroundOperator { /// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations // E226 #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct MissingWhitespaceAroundArithmeticOperator; impl AlwaysFixableViolation for MissingWhitespaceAroundArithmeticOperator { @@ -108,6 +110,7 @@ impl AlwaysFixableViolation for MissingWhitespaceAroundArithmeticOperator { /// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations // E227 #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct MissingWhitespaceAroundBitwiseOrShiftOperator; impl AlwaysFixableViolation for MissingWhitespaceAroundBitwiseOrShiftOperator { @@ -147,6 +150,7 @@ impl AlwaysFixableViolation for MissingWhitespaceAroundBitwiseOrShiftOperator { /// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations // E228 #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct MissingWhitespaceAroundModuloOperator; impl AlwaysFixableViolation for MissingWhitespaceAroundModuloOperator { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/redundant_backslash.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/redundant_backslash.rs index b8f8439bec..2092b63716 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/redundant_backslash.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/redundant_backslash.rs @@ -30,6 +30,7 @@ use super::LogicalLine; /// /// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.3.3")] pub(crate) struct RedundantBackslash; impl AlwaysFixableViolation for RedundantBackslash { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs index 7a73fc7795..c806ba46f4 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs @@ -26,6 +26,7 @@ use super::{LogicalLine, Whitespace}; /// /// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct TabBeforeOperator; impl AlwaysFixableViolation for TabBeforeOperator { @@ -58,6 +59,7 @@ impl AlwaysFixableViolation for TabBeforeOperator { /// /// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct MultipleSpacesBeforeOperator; impl AlwaysFixableViolation for MultipleSpacesBeforeOperator { @@ -90,6 +92,7 @@ impl AlwaysFixableViolation for MultipleSpacesBeforeOperator { /// /// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct TabAfterOperator; impl AlwaysFixableViolation for TabAfterOperator { @@ -122,6 +125,7 @@ impl AlwaysFixableViolation for TabAfterOperator { /// /// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct MultipleSpacesAfterOperator; impl AlwaysFixableViolation for MultipleSpacesAfterOperator { @@ -152,6 +156,7 @@ impl AlwaysFixableViolation for MultipleSpacesAfterOperator { /// ``` /// #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.281")] pub(crate) struct TabAfterComma; impl AlwaysFixableViolation for TabAfterComma { @@ -182,6 +187,7 @@ impl AlwaysFixableViolation for TabAfterComma { /// a = 4, 5 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.281")] pub(crate) struct MultipleSpacesAfterComma; impl AlwaysFixableViolation for MultipleSpacesAfterComma { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs index af7a415275..69ef1174d4 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs @@ -22,6 +22,7 @@ use super::{LogicalLine, Whitespace}; /// True and False /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct MultipleSpacesAfterKeyword; impl AlwaysFixableViolation for MultipleSpacesAfterKeyword { @@ -51,6 +52,7 @@ impl AlwaysFixableViolation for MultipleSpacesAfterKeyword { /// x and y /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct MultipleSpacesBeforeKeyword; impl AlwaysFixableViolation for MultipleSpacesBeforeKeyword { @@ -80,6 +82,7 @@ impl AlwaysFixableViolation for MultipleSpacesBeforeKeyword { /// True and False /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct TabAfterKeyword; impl AlwaysFixableViolation for TabAfterKeyword { @@ -109,6 +112,7 @@ impl AlwaysFixableViolation for TabAfterKeyword { /// True and False /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct TabBeforeKeyword; impl AlwaysFixableViolation for TabBeforeKeyword { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs index 3e11ca5a8c..bed7da83e1 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs @@ -32,6 +32,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// /// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct UnexpectedSpacesAroundKeywordParameterEquals; impl AlwaysFixableViolation for UnexpectedSpacesAroundKeywordParameterEquals { @@ -71,6 +72,7 @@ impl AlwaysFixableViolation for UnexpectedSpacesAroundKeywordParameterEquals { /// /// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct MissingWhitespaceAroundParameterEquals; impl AlwaysFixableViolation for MissingWhitespaceAroundParameterEquals { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs index 49f85613b2..dc9e91dac2 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs @@ -31,6 +31,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// /// [PEP 8]: https://peps.python.org/pep-0008/#comments #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct TooFewSpacesBeforeInlineComment; impl AlwaysFixableViolation for TooFewSpacesBeforeInlineComment { @@ -67,6 +68,7 @@ impl AlwaysFixableViolation for TooFewSpacesBeforeInlineComment { /// /// [PEP 8]: https://peps.python.org/pep-0008/#comments #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct NoSpaceAfterInlineComment; impl AlwaysFixableViolation for NoSpaceAfterInlineComment { @@ -104,6 +106,7 @@ impl AlwaysFixableViolation for NoSpaceAfterInlineComment { /// /// [PEP 8]: https://peps.python.org/pep-0008/#comments #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct NoSpaceAfterBlockComment; impl AlwaysFixableViolation for NoSpaceAfterBlockComment { @@ -150,6 +153,7 @@ impl AlwaysFixableViolation for NoSpaceAfterBlockComment { /// /// [PEP 8]: https://peps.python.org/pep-0008/#comments #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct MultipleLeadingHashesForBlockComment; impl AlwaysFixableViolation for MultipleLeadingHashesForBlockComment { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs index 4f180f2632..e176455718 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs @@ -26,6 +26,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// /// [PEP 8]: https://peps.python.org/pep-0008/#pet-peeves #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.269")] pub(crate) struct WhitespaceBeforeParameters { bracket: TokenKind, } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs index f8301cbc94..de7fbd042d 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs @@ -24,6 +24,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// spam(1)\n /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.61")] pub(crate) struct MissingNewlineAtEndOfFile; impl AlwaysFixableViolation for MissingNewlineAtEndOfFile { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/mixed_spaces_and_tabs.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/mixed_spaces_and_tabs.rs index 649fc972fc..b62d4e008b 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/mixed_spaces_and_tabs.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/mixed_spaces_and_tabs.rs @@ -27,6 +27,7 @@ use crate::{Violation, checkers::ast::LintContext}; /// if a == 0:\n a = 1\n b = 1 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.229")] pub(crate) struct MixedSpacesAndTabs; impl Violation for MixedSpacesAndTabs { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/module_import_not_at_top_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/module_import_not_at_top_of_file.rs index 066de7316f..3da13717a3 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/module_import_not_at_top_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/module_import_not_at_top_of_file.rs @@ -40,6 +40,7 @@ use crate::checkers::ast::Checker; /// /// [PEP 8]: https://peps.python.org/pep-0008/#imports #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.28")] pub(crate) struct ModuleImportNotAtTopOfFile { source_type: PySourceType, } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/multiple_imports_on_one_line.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/multiple_imports_on_one_line.rs index dd7dc69b5b..aa5301fb49 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/multiple_imports_on_one_line.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/multiple_imports_on_one_line.rs @@ -31,6 +31,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [PEP 8]: https://peps.python.org/pep-0008/#imports #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.191")] pub(crate) struct MultipleImportsOnOneLine; impl Violation for MultipleImportsOnOneLine { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs index a431cb0368..a4234093bc 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs @@ -28,6 +28,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// pass /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.28")] pub(crate) struct NotInTest; impl AlwaysFixableViolation for NotInTest { @@ -64,6 +65,7 @@ impl AlwaysFixableViolation for NotInTest { /// /// [PEP8]: https://peps.python.org/pep-0008/#programming-recommendations #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.28")] pub(crate) struct NotIsTest; impl AlwaysFixableViolation for NotIsTest { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/tab_indentation.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/tab_indentation.rs index 40d272336d..581f2a1eaa 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/tab_indentation.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/tab_indentation.rs @@ -24,6 +24,7 @@ use crate::checkers::ast::LintContext; /// [PEP 8]: https://peps.python.org/pep-0008/#tabs-or-spaces /// [formatter]: https://docs.astral.sh/ruff/formatter #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.254")] pub(crate) struct TabIndentation; impl Violation for TabIndentation { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs index 79ccdc6978..3a84aad979 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -29,6 +29,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix, checkers::ast::LintContext}; /// spam(1)\n /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.3.3")] pub(crate) struct TooManyNewlinesAtEndOfFile { num_trailing_newlines: u32, in_notebook: bool, diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/trailing_whitespace.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/trailing_whitespace.rs index 78dabff4df..c72825da72 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/trailing_whitespace.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/trailing_whitespace.rs @@ -32,6 +32,7 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// /// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.253")] pub(crate) struct TrailingWhitespace; impl AlwaysFixableViolation for TrailingWhitespace { @@ -69,6 +70,7 @@ impl AlwaysFixableViolation for TrailingWhitespace { /// /// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.253")] pub(crate) struct BlankLineWithWhitespace; impl AlwaysFixableViolation for BlankLineWithWhitespace { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/type_comparison.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/type_comparison.rs index b7a0c0ca86..152e5e808e 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/type_comparison.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/type_comparison.rs @@ -49,6 +49,7 @@ use crate::checkers::ast::Checker; /// pass /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.39")] pub(crate) struct TypeComparison; impl Violation for TypeComparison { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/whitespace_after_decorator.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/whitespace_after_decorator.rs index 63517ae803..1ce7f2c0a8 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/whitespace_after_decorator.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/whitespace_after_decorator.rs @@ -30,6 +30,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// /// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.5.1")] pub(crate) struct WhitespaceAfterDecorator; impl AlwaysFixableViolation for WhitespaceAfterDecorator { diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index dd4f8ee8a7..adcdcc5dec 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -58,6 +58,7 @@ use crate::rules::pydocstyle::settings::Convention; /// return distance / time /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.14.1")] pub(crate) struct DocstringExtraneousParameter { id: String, } @@ -112,6 +113,7 @@ impl Violation for DocstringExtraneousParameter { /// return distance / time /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.5.6")] pub(crate) struct DocstringMissingReturns; impl Violation for DocstringMissingReturns { @@ -163,6 +165,7 @@ impl Violation for DocstringMissingReturns { /// print("Hello!") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.5.6")] pub(crate) struct DocstringExtraneousReturns; impl Violation for DocstringExtraneousReturns { @@ -215,6 +218,7 @@ impl Violation for DocstringExtraneousReturns { /// yield i /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.5.7")] pub(crate) struct DocstringMissingYields; impl Violation for DocstringMissingYields { @@ -266,6 +270,7 @@ impl Violation for DocstringMissingYields { /// print("Hello!") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.5.7")] pub(crate) struct DocstringExtraneousYields; impl Violation for DocstringExtraneousYields { @@ -337,6 +342,7 @@ impl Violation for DocstringExtraneousYields { /// raise FasterThanLightError from exc /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.5.5")] pub(crate) struct DocstringMissingException { id: String, } @@ -404,6 +410,7 @@ impl Violation for DocstringMissingException { /// could possibly raise, even those which are not explicitly raised using /// `raise` statements in the function body. #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.5.5")] pub(crate) struct DocstringExtraneousException { ids: Vec, } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/backslashes.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/backslashes.rs index a062d173bd..e7fdf4f37b 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/backslashes.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/backslashes.rs @@ -42,6 +42,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) /// - [Python documentation: String and Bytes literals](https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.172")] pub(crate) struct EscapeSequenceInDocstring; impl Violation for EscapeSequenceInDocstring { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_after_summary.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_after_summary.rs index 50e6a75d3d..52e0282b6e 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_after_summary.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_after_summary.rs @@ -41,6 +41,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [PEP 257]: https://peps.python.org/pep-0257/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.68")] pub(crate) struct MissingBlankLineAfterSummary { num_lines: usize, } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_class.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_class.rs index 1196a42a75..16cdb26a13 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_class.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_class.rs @@ -43,6 +43,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// /// [D211]: https://docs.astral.sh/ruff/rules/blank-line-before-class #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct IncorrectBlankLineBeforeClass; impl AlwaysFixableViolation for IncorrectBlankLineBeforeClass { @@ -95,6 +96,7 @@ impl AlwaysFixableViolation for IncorrectBlankLineBeforeClass { /// /// [PEP 257]: https://peps.python.org/pep-0257/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct IncorrectBlankLineAfterClass; impl AlwaysFixableViolation for IncorrectBlankLineAfterClass { @@ -142,6 +144,7 @@ impl AlwaysFixableViolation for IncorrectBlankLineAfterClass { /// /// [D203]: https://docs.astral.sh/ruff/rules/incorrect-blank-line-before-class #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct BlankLineBeforeClass; impl AlwaysFixableViolation for BlankLineBeforeClass { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_function.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_function.rs index bf124a12d1..83efa3adc6 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_function.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_function.rs @@ -38,6 +38,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct BlankLineBeforeFunction { num_lines: usize, } @@ -84,6 +85,7 @@ impl Violation for BlankLineBeforeFunction { /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct BlankLineAfterFunction { num_lines: usize, } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs index ccc082ba38..32cfa89406 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs @@ -30,6 +30,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.69")] pub(crate) struct FirstWordUncapitalized { first_word: String, capitalized_word: String, diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_period.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_period.rs index 11f7915c33..6b1c7f4cf1 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_period.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_period.rs @@ -45,6 +45,7 @@ use crate::rules::pydocstyle::helpers::logical_line; /// /// [PEP 257]: https://peps.python.org/pep-0257/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.68")] pub(crate) struct MissingTrailingPeriod; impl Violation for MissingTrailingPeriod { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_punctuation.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_punctuation.rs index 6b4d25fa56..2e1174a72e 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_punctuation.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_punctuation.rs @@ -44,6 +44,7 @@ use crate::rules::pydocstyle::helpers::logical_line; /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.69")] pub(crate) struct MissingTerminalPunctuation; impl Violation for MissingTerminalPunctuation { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/if_needed.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/if_needed.rs index 0ad276b816..7fb88ffc34 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/if_needed.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/if_needed.rs @@ -67,6 +67,7 @@ use crate::docstrings::Docstring; /// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) /// - [Python documentation: `typing.overload`](https://docs.python.org/3/library/typing.html#typing.overload) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.71")] pub(crate) struct OverloadWithDocstring; impl Violation for OverloadWithDocstring { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/indent.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/indent.rs index 2a8f9b721f..ebb1c72cc9 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/indent.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/indent.rs @@ -52,6 +52,7 @@ use crate::{Edit, Fix}; /// [PEP 8]: https://peps.python.org/pep-0008/#tabs-or-spaces /// [formatter]: https://docs.astral.sh/ruff/formatter #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.75")] pub(crate) struct DocstringTabIndentation; impl Violation for DocstringTabIndentation { @@ -100,6 +101,7 @@ impl Violation for DocstringTabIndentation { /// [PEP 257]: https://peps.python.org/pep-0257/ /// [formatter]: https://docs.astral.sh/ruff/formatter/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.75")] pub(crate) struct UnderIndentation; impl AlwaysFixableViolation for UnderIndentation { @@ -152,6 +154,7 @@ impl AlwaysFixableViolation for UnderIndentation { /// [PEP 257]: https://peps.python.org/pep-0257/ /// [formatter]:https://docs.astral.sh/ruff/formatter/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.75")] pub(crate) struct OverIndentation; impl AlwaysFixableViolation for OverIndentation { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/multi_line_summary_start.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/multi_line_summary_start.rs index 4d2e6e596f..b02c06af01 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/multi_line_summary_start.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/multi_line_summary_start.rs @@ -61,6 +61,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// [D213]: https://docs.astral.sh/ruff/rules/multi-line-summary-second-line /// [PEP 257]: https://peps.python.org/pep-0257 #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.69")] pub(crate) struct MultiLineSummaryFirstLine; impl AlwaysFixableViolation for MultiLineSummaryFirstLine { @@ -124,6 +125,7 @@ impl AlwaysFixableViolation for MultiLineSummaryFirstLine { /// [D212]: https://docs.astral.sh/ruff/rules/multi-line-summary-first-line /// [PEP 257]: https://peps.python.org/pep-0257 #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.69")] pub(crate) struct MultiLineSummarySecondLine; impl AlwaysFixableViolation for MultiLineSummarySecondLine { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs index afdaf68348..43c32de6c1 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs @@ -44,6 +44,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// /// [PEP 257]: https://peps.python.org/pep-0257/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.68")] pub(crate) struct NewLineAfterLastParagraph; impl AlwaysFixableViolation for NewLineAfterLastParagraph { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/no_signature.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/no_signature.rs index 5a06ae6022..4ffc8be1dd 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/no_signature.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/no_signature.rs @@ -41,6 +41,7 @@ use crate::docstrings::Docstring; /// /// [PEP 257]: https://peps.python.org/pep-0257/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct SignatureInDocstring; impl Violation for SignatureInDocstring { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs index fd9ee2798e..4233f596ed 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs @@ -32,6 +32,7 @@ use crate::rules::pydocstyle::helpers::ends_with_backslash; /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.68")] pub(crate) struct SurroundingWhitespace; impl Violation for SurroundingWhitespace { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/non_imperative_mood.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/non_imperative_mood.rs index 6369e355de..9597b17b92 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/non_imperative_mood.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/non_imperative_mood.rs @@ -51,6 +51,7 @@ static MOOD: LazyLock = LazyLock::new(Mood::new); /// /// [PEP 257]: https://peps.python.org/pep-0257/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.228")] pub(crate) struct NonImperativeMood { first_line: String, } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/not_empty.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/not_empty.rs index 4c69d9bd4f..a16e0a191d 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/not_empty.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/not_empty.rs @@ -29,6 +29,7 @@ use crate::docstrings::Docstring; /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.68")] pub(crate) struct EmptyDocstring; impl Violation for EmptyDocstring { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/not_missing.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/not_missing.rs index 87d4f876aa..b82098169f 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/not_missing.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/not_missing.rs @@ -61,6 +61,7 @@ use crate::checkers::ast::Checker; /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct UndocumentedPublicModule; impl Violation for UndocumentedPublicModule { @@ -144,6 +145,7 @@ impl Violation for UndocumentedPublicModule { /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct UndocumentedPublicClass; impl Violation for UndocumentedPublicClass { @@ -226,6 +228,7 @@ impl Violation for UndocumentedPublicClass { /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct UndocumentedPublicMethod; impl Violation for UndocumentedPublicMethod { @@ -316,6 +319,7 @@ impl Violation for UndocumentedPublicMethod { /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct UndocumentedPublicFunction; impl Violation for UndocumentedPublicFunction { @@ -359,6 +363,7 @@ impl Violation for UndocumentedPublicFunction { /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct UndocumentedPublicPackage; impl Violation for UndocumentedPublicPackage { @@ -416,6 +421,7 @@ impl Violation for UndocumentedPublicPackage { /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct UndocumentedMagicMethod; impl Violation for UndocumentedMagicMethod { @@ -471,6 +477,7 @@ impl Violation for UndocumentedMagicMethod { /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct UndocumentedPublicNestedClass; impl Violation for UndocumentedPublicNestedClass { @@ -519,6 +526,7 @@ impl Violation for UndocumentedPublicNestedClass { /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.70")] pub(crate) struct UndocumentedPublicInit; impl Violation for UndocumentedPublicInit { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/one_liner.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/one_liner.rs index 158092f110..3fe7e92bae 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/one_liner.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/one_liner.rs @@ -37,6 +37,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [PEP 257]: https://peps.python.org/pep-0257/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.68")] pub(crate) struct UnnecessaryMultilineDocstring; impl Violation for UnnecessaryMultilineDocstring { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs index 0f4e51717a..7b9fc80ba8 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -89,6 +89,7 @@ use crate::{Edit, Fix}; /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.73")] pub(crate) struct OverindentedSection { name: String, } @@ -192,6 +193,7 @@ impl AlwaysFixableViolation for OverindentedSection { /// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.73")] pub(crate) struct OverindentedSectionUnderline { name: String, } @@ -275,6 +277,7 @@ impl AlwaysFixableViolation for OverindentedSectionUnderline { /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.71")] pub(crate) struct NonCapitalizedSectionName { name: String, } @@ -373,6 +376,7 @@ impl AlwaysFixableViolation for NonCapitalizedSectionName { /// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.71")] pub(crate) struct MissingNewLineAfterSectionName { name: String, } @@ -476,6 +480,7 @@ impl AlwaysFixableViolation for MissingNewLineAfterSectionName { /// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.71")] pub(crate) struct MissingDashedUnderlineAfterSection { name: String, } @@ -582,6 +587,7 @@ impl AlwaysFixableViolation for MissingDashedUnderlineAfterSection { /// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.71")] pub(crate) struct MissingSectionUnderlineAfterName { name: String, } @@ -686,6 +692,7 @@ impl AlwaysFixableViolation for MissingSectionUnderlineAfterName { /// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.71")] pub(crate) struct MismatchedSectionUnderlineLength { name: String, } @@ -783,6 +790,7 @@ impl AlwaysFixableViolation for MismatchedSectionUnderlineLength { /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.71")] pub(crate) struct NoBlankLineAfterSection { name: String, } @@ -876,6 +884,7 @@ impl AlwaysFixableViolation for NoBlankLineAfterSection { /// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.71")] pub(crate) struct NoBlankLineBeforeSection { name: String, } @@ -971,6 +980,7 @@ impl AlwaysFixableViolation for NoBlankLineBeforeSection { /// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.71")] pub(crate) struct MissingBlankLineAfterLastSection { name: String, } @@ -1060,6 +1070,7 @@ impl AlwaysFixableViolation for MissingBlankLineAfterLastSection { /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.71")] pub(crate) struct EmptyDocstringSection { name: String, } @@ -1137,6 +1148,7 @@ impl Violation for EmptyDocstringSection { /// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) /// - [Google Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.74")] pub(crate) struct MissingSectionNameColon { name: String, } @@ -1222,6 +1234,7 @@ impl AlwaysFixableViolation for MissingSectionNameColon { /// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.73")] pub(crate) struct UndocumentedParam { /// The name of the function being documented. definition: String, @@ -1304,6 +1317,7 @@ impl Violation for UndocumentedParam { /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.71")] pub(crate) struct BlankLinesBetweenHeaderAndContent { name: String, } 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 1bbcc9ab4c..d3e9465918 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 @@ -40,6 +40,7 @@ use crate::rules::pydocstyle::helpers::normalize_word; /// /// [PEP 257]: https://peps.python.org/pep-0257/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.71")] pub(crate) struct DocstringStartsWithThis; impl Violation for DocstringStartsWithThis { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/triple_quotes.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/triple_quotes.rs index 374ac69369..17dc5eac92 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/triple_quotes.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/triple_quotes.rs @@ -38,6 +38,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [formatter]: https://docs.astral.sh/ruff/formatter/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.69")] pub(crate) struct TripleSingleQuotes { expected_quote: Quote, } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/assert_tuple.rs b/crates/ruff_linter/src/rules/pyflakes/rules/assert_tuple.rs index 798f9bb990..1884a2e6cf 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/assert_tuple.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/assert_tuple.rs @@ -28,6 +28,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `assert` statement](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.28")] pub(crate) struct AssertTuple; impl Violation for AssertTuple { diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/break_outside_loop.rs b/crates/ruff_linter/src/rules/pyflakes/rules/break_outside_loop.rs index 0309c0047e..834d63f730 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/break_outside_loop.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/break_outside_loop.rs @@ -18,6 +18,7 @@ use crate::Violation; /// ## References /// - [Python documentation: `break`](https://docs.python.org/3/reference/simple_stmts.html#the-break-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.36")] pub(crate) struct BreakOutsideLoop; impl Violation for BreakOutsideLoop { diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/continue_outside_loop.rs b/crates/ruff_linter/src/rules/pyflakes/rules/continue_outside_loop.rs index 334c64c2a7..4e775f3ccd 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/continue_outside_loop.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/continue_outside_loop.rs @@ -18,6 +18,7 @@ use crate::Violation; /// ## References /// - [Python documentation: `continue`](https://docs.python.org/3/reference/simple_stmts.html#the-continue-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.36")] pub(crate) struct ContinueOutsideLoop; impl Violation for ContinueOutsideLoop { diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/default_except_not_last.rs b/crates/ruff_linter/src/rules/pyflakes/rules/default_except_not_last.rs index 42a45f3bf4..0cfa0a1950 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/default_except_not_last.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/default_except_not_last.rs @@ -45,6 +45,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.28")] pub(crate) struct DefaultExceptNotLast; impl Violation for DefaultExceptNotLast { diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs b/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs index 3e15f4cfe3..3538e596e1 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs @@ -54,6 +54,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [PEP 498 – Literal String Interpolation](https://peps.python.org/pep-0498/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.18")] pub(crate) struct FStringMissingPlaceholders; impl AlwaysFixableViolation for FStringMissingPlaceholders { diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs b/crates/ruff_linter/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs index 41c3796563..d552c3f3d9 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs @@ -24,6 +24,7 @@ use crate::Violation; /// ## References /// - [PEP 563 – Postponed Evaluation of Annotations](https://peps.python.org/pep-0563/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.39")] pub(crate) struct ForwardAnnotationSyntaxError { pub parse_error: String, } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/future_feature_not_defined.rs b/crates/ruff_linter/src/rules/pyflakes/rules/future_feature_not_defined.rs index 9d5886f04a..59e2bf79a1 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/future_feature_not_defined.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/future_feature_not_defined.rs @@ -13,6 +13,7 @@ use crate::Violation; /// ## References /// - [Python documentation: `__future__`](https://docs.python.org/3/library/__future__.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.34")] pub(crate) struct FutureFeatureNotDefined { pub name: String, } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/if_tuple.rs b/crates/ruff_linter/src/rules/pyflakes/rules/if_tuple.rs index 75fb7ce65a..47f6ea9fd7 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/if_tuple.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/if_tuple.rs @@ -29,6 +29,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `if` statement](https://docs.python.org/3/reference/compound_stmts.html#the-if-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.18")] pub(crate) struct IfTuple; impl Violation for IfTuple { diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs b/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs index 3b3c33d0fe..7a97aa09ab 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs @@ -34,6 +34,7 @@ use crate::checkers::ast::Checker; /// print(filename) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.44")] pub(crate) struct ImportShadowedByLoopVar { pub(crate) name: String, pub(crate) row: SourceRow, @@ -117,6 +118,7 @@ pub(crate) fn import_shadowed_by_loop_var(checker: &Checker, scope_id: ScopeId, /// /// [PEP 8]: https://peps.python.org/pep-0008/#imports #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.18")] pub(crate) struct UndefinedLocalWithImportStar { pub(crate) name: String, } @@ -155,6 +157,7 @@ impl Violation for UndefinedLocalWithImportStar { /// ## References /// - [Python documentation: Future statements](https://docs.python.org/3/reference/simple_stmts.html#future) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.34")] pub(crate) struct LateFutureImport; impl Violation for LateFutureImport { @@ -199,6 +202,7 @@ impl Violation for LateFutureImport { /// return pi * radius**2 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.44")] pub(crate) struct UndefinedLocalWithImportStarUsage { pub(crate) name: String, } @@ -240,6 +244,7 @@ impl Violation for UndefinedLocalWithImportStarUsage { /// /// [PEP 8]: https://peps.python.org/pep-0008/#imports #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.37")] pub(crate) struct UndefinedLocalWithNestedImportStarUsage { pub(crate) name: String, } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs b/crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs index 6a6fa72517..d09d6cd006 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs @@ -51,6 +51,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// - [Python documentation: Value comparisons](https://docs.python.org/3/reference/expressions.html#value-comparisons) /// - [_Why does Python log a SyntaxWarning for ‘is’ with literals?_ by Adam Johnson](https://adamj.eu/tech/2020/01/21/why-does-python-3-8-syntaxwarning-for-is-literal/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.39")] pub(crate) struct IsLiteral { cmp_op: IsCmpOp, } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/invalid_print_syntax.rs b/crates/ruff_linter/src/rules/pyflakes/rules/invalid_print_syntax.rs index 146917dd45..72ccffb266 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/invalid_print_syntax.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/invalid_print_syntax.rs @@ -47,6 +47,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `print`](https://docs.python.org/3/library/functions.html#print) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.39")] pub(crate) struct InvalidPrintSyntax; impl Violation for InvalidPrintSyntax { diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/raise_not_implemented.rs b/crates/ruff_linter/src/rules/pyflakes/rules/raise_not_implemented.rs index e19fd97b5d..0bfd4f9933 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/raise_not_implemented.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/raise_not_implemented.rs @@ -35,6 +35,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `NotImplemented`](https://docs.python.org/3/library/constants.html#NotImplemented) /// - [Python documentation: `NotImplementedError`](https://docs.python.org/3/library/exceptions.html#NotImplementedError) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.18")] pub(crate) struct RaiseNotImplemented; impl Violation for RaiseNotImplemented { 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 3c13941f6a..01439bc764 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 @@ -31,6 +31,7 @@ use rustc_hash::FxHashMap; /// import bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.171")] pub(crate) struct RedefinedWhileUnused { pub name: String, pub row: SourceRow, diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs index 0771b2ec72..1acdc90138 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs @@ -49,6 +49,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: Dictionaries](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.30")] pub(crate) struct MultiValueRepeatedKeyLiteral { name: SourceCodeSnippet, existing: SourceCodeSnippet, @@ -121,6 +122,7 @@ impl Violation for MultiValueRepeatedKeyLiteral { /// ## References /// - [Python documentation: Dictionaries](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.30")] pub(crate) struct MultiValueRepeatedKeyVariable { name: SourceCodeSnippet, } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/return_outside_function.rs b/crates/ruff_linter/src/rules/pyflakes/rules/return_outside_function.rs index bd004db454..dc67da1e58 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/return_outside_function.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/return_outside_function.rs @@ -18,6 +18,7 @@ use crate::Violation; /// ## References /// - [Python documentation: `return`](https://docs.python.org/3/reference/simple_stmts.html#the-return-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.18")] pub(crate) struct ReturnOutsideFunction; impl Violation for ReturnOutsideFunction { diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs b/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs index 9fe9ec063f..70f25fb689 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs @@ -18,6 +18,7 @@ use crate::{Violation, checkers::ast::Checker}; /// ## References /// - [PEP 3132 – Extended Iterable Unpacking](https://peps.python.org/pep-3132/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.32")] pub(crate) struct ExpressionsInStarAssignment; impl Violation for ExpressionsInStarAssignment { @@ -44,6 +45,7 @@ impl Violation for ExpressionsInStarAssignment { /// ## References /// - [PEP 3132 – Extended Iterable Unpacking](https://peps.python.org/pep-3132/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.32")] pub(crate) struct MultipleStarredExpressions; impl Violation for MultipleStarredExpressions { diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/strings.rs b/crates/ruff_linter/src/rules/pyflakes/rules/strings.rs index 785e7266b3..9df61638c4 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/strings.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/strings.rs @@ -39,6 +39,7 @@ use crate::rules::pyflakes::format::FormatSummary; /// ## References /// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.142")] pub(crate) struct PercentFormatInvalidFormat { pub(crate) message: String, } @@ -78,6 +79,7 @@ impl Violation for PercentFormatInvalidFormat { /// ## References /// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.142")] pub(crate) struct PercentFormatExpectedMapping; impl Violation for PercentFormatExpectedMapping { @@ -114,6 +116,7 @@ impl Violation for PercentFormatExpectedMapping { /// ## References /// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.142")] pub(crate) struct PercentFormatExpectedSequence; impl Violation for PercentFormatExpectedSequence { @@ -153,6 +156,7 @@ impl Violation for PercentFormatExpectedSequence { /// ## References /// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.142")] pub(crate) struct PercentFormatExtraNamedArguments { missing: Vec, } @@ -193,6 +197,7 @@ impl AlwaysFixableViolation for PercentFormatExtraNamedArguments { /// ## References /// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.142")] pub(crate) struct PercentFormatMissingArgument { missing: Vec, } @@ -233,6 +238,7 @@ impl Violation for PercentFormatMissingArgument { /// ## References /// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.142")] pub(crate) struct PercentFormatMixedPositionalAndNamed; impl Violation for PercentFormatMixedPositionalAndNamed { @@ -263,6 +269,7 @@ impl Violation for PercentFormatMixedPositionalAndNamed { /// ## References /// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.142")] pub(crate) struct PercentFormatPositionalCountMismatch { wanted: usize, got: usize, @@ -301,6 +308,7 @@ impl Violation for PercentFormatPositionalCountMismatch { /// ## References /// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.142")] pub(crate) struct PercentFormatStarRequiresSequence; impl Violation for PercentFormatStarRequiresSequence { @@ -331,6 +339,7 @@ impl Violation for PercentFormatStarRequiresSequence { /// ## References /// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.142")] pub(crate) struct PercentFormatUnsupportedFormatCharacter { pub(crate) char: char, } @@ -362,6 +371,7 @@ impl Violation for PercentFormatUnsupportedFormatCharacter { /// ## References /// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.138")] pub(crate) struct StringDotFormatInvalidFormat { pub(crate) message: String, } @@ -404,6 +414,7 @@ impl Violation for StringDotFormatInvalidFormat { /// ## References /// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.139")] pub(crate) struct StringDotFormatExtraNamedArguments { missing: Vec, } @@ -455,6 +466,7 @@ impl Violation for StringDotFormatExtraNamedArguments { /// ## References /// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.139")] pub(crate) struct StringDotFormatExtraPositionalArguments { missing: Vec, } @@ -498,6 +510,7 @@ impl Violation for StringDotFormatExtraPositionalArguments { /// ## References /// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.139")] pub(crate) struct StringDotFormatMissingArguments { missing: Vec, } @@ -536,6 +549,7 @@ impl Violation for StringDotFormatMissingArguments { /// ## References /// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.139")] pub(crate) struct StringDotFormatMixingAutomatic; impl Violation for StringDotFormatMixingAutomatic { diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/undefined_export.rs b/crates/ruff_linter/src/rules/pyflakes/rules/undefined_export.rs index 18c9ddc1d8..1a1648f92c 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/undefined_export.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/undefined_export.rs @@ -40,6 +40,7 @@ use crate::Violation; /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.25")] pub(crate) struct UndefinedExport { pub name: String, } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/undefined_local.rs b/crates/ruff_linter/src/rules/pyflakes/rules/undefined_local.rs index 0af1d5f4fb..a46205a72a 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/undefined_local.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/undefined_local.rs @@ -33,6 +33,7 @@ use crate::checkers::ast::Checker; /// x += 1 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.24")] pub(crate) struct UndefinedLocal { name: String, } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/undefined_name.rs b/crates/ruff_linter/src/rules/pyflakes/rules/undefined_name.rs index f1666bf9b3..314488cb21 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/undefined_name.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/undefined_name.rs @@ -27,6 +27,7 @@ use crate::Violation; /// ## References /// - [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.20")] pub(crate) struct UndefinedName { pub(crate) name: String, pub(crate) minor_version_builtin_added: Option, diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_annotation.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_annotation.rs index 041a5a0211..98692373bf 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_annotation.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_annotation.rs @@ -21,6 +21,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [PEP 484 – Type Hints](https://peps.python.org/pep-0484/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.172")] pub(crate) struct UnusedAnnotation { name: String, } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs index 22bcd0cf0e..37530f1a60 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs @@ -143,6 +143,7 @@ use crate::{Applicability, Fix, FixAvailability, Violation}; /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.18")] pub(crate) struct UnusedImport { /// Qualified name of the import name: String, diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs index b52e01142b..aa5610620e 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs @@ -53,6 +53,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [RUF059]: https://docs.astral.sh/ruff/rules/unused-unpacked-variable/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.22")] pub(crate) struct UnusedVariable { pub name: String, } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/yield_outside_function.rs b/crates/ruff_linter/src/rules/pyflakes/rules/yield_outside_function.rs index 7838c267db..38be7d1167 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/yield_outside_function.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/yield_outside_function.rs @@ -54,6 +54,7 @@ impl From for DeferralKeyword { /// /// [autoawait]: https://ipython.readthedocs.io/en/stable/interactive/autoawait.html #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.22")] pub(crate) struct YieldOutsideFunction { keyword: DeferralKeyword, } diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_noqa.rs b/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_noqa.rs index e60fd42072..1c5bdf4339 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_noqa.rs +++ b/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_noqa.rs @@ -39,6 +39,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Ruff documentation](https://docs.astral.sh/ruff/configuration/#error-suppression) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.200")] pub(crate) struct BlanketNOQA { missing_colon: bool, file_exemption: bool, diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs b/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs index efd12c7b3b..1386ca0f61 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs +++ b/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs @@ -42,6 +42,7 @@ use crate::checkers::ast::LintContext; /// enable_error_code = ["ignore-without-code"] /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.187")] pub(crate) struct BlanketTypeIgnore; impl Violation for BlanketTypeIgnore { diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs b/crates/ruff_linter/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs index 648702121c..87319784a8 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs +++ b/crates/ruff_linter/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs @@ -34,6 +34,7 @@ use crate::{FixAvailability, Violation}; /// /// [G010]: https://docs.astral.sh/ruff/rules/logging-warn/ #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "v0.2.0")] pub(crate) struct DeprecatedLogWarn; /// PGH002 diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/rules/invalid_mock_access.rs b/crates/ruff_linter/src/rules/pygrep_hooks/rules/invalid_mock_access.rs index ec33040df1..afcc230966 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/rules/invalid_mock_access.rs +++ b/crates/ruff_linter/src/rules/pygrep_hooks/rules/invalid_mock_access.rs @@ -33,6 +33,7 @@ enum Reason { /// my_mock.assert_called() /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.266")] pub(crate) struct InvalidMockAccess { reason: Reason, } diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/rules/no_eval.rs b/crates/ruff_linter/src/rules/pygrep_hooks/rules/no_eval.rs index 8615e4ae51..1818aaf79a 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/rules/no_eval.rs +++ b/crates/ruff_linter/src/rules/pygrep_hooks/rules/no_eval.rs @@ -31,6 +31,7 @@ use crate::Violation; /// /// [S307]: https://docs.astral.sh/ruff/rules/suspicious-eval-usage/ #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "v0.2.0")] pub(crate) struct Eval; /// PGH001 diff --git a/crates/ruff_linter/src/rules/pylint/rules/and_or_ternary.rs b/crates/ruff_linter/src/rules/pylint/rules/and_or_ternary.rs index bc8bed77c7..e93e3a1c34 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/and_or_ternary.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/and_or_ternary.rs @@ -29,6 +29,7 @@ use crate::Violation; /// maximum = x if x >= y else y /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "v0.2.0")] pub(crate) struct AndOrTernary; /// PLR1706 diff --git a/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs b/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs index 64db3f1e40..60dbad3e8c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs @@ -26,6 +26,7 @@ enum Kind { /// assert "always true" /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct AssertOnStringLiteral { kind: Kind, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs b/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs index e41275a1ca..78ad99fd07 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs @@ -36,6 +36,7 @@ use crate::Violation; /// /// [autoawait]: https://ipython.readthedocs.io/en/stable/interactive/autoawait.html #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.150")] pub(crate) struct AwaitOutsideAsync; impl Violation for AwaitOutsideAsync { diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs index b4ee0a4332..200ec4dbe2 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs @@ -43,6 +43,7 @@ use crate::rules::pylint::helpers::is_known_dunder_method; /// ## Options /// - `lint.pylint.allow-dunder-method-names` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.285")] pub(crate) struct BadDunderMethodName { name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_open_mode.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_open_mode.rs index b89cbf1425..c949b7baef 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_open_mode.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_open_mode.rs @@ -37,6 +37,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct BadOpenMode { mode: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs index 4108cd8c74..bc488411fd 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs @@ -37,6 +37,7 @@ use crate::checkers::ast::Checker; /// /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.6.0")] pub(crate) struct BadStaticmethodArgument { argument_name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs index b251bab323..41620db47b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs @@ -48,6 +48,7 @@ use ruff_python_ast::PythonVersion; /// ## References /// - [Python documentation: `str.strip`](https://docs.python.org/3/library/stdtypes.html?highlight=strip#str.strip) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.242")] pub(crate) struct BadStrStripCall { strip: StripKind, removal: Option, diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs index a94e8290e9..28a24babde 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs @@ -27,6 +27,7 @@ use crate::checkers::ast::Checker; /// print("{:z}".format("1")) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.283")] pub(crate) struct BadStringFormatCharacter { format_char: char, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_type.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_type.rs index a747b9b12a..3822b76b7a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_type.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_type.rs @@ -28,6 +28,7 @@ use crate::checkers::ast::Checker; /// print("%d" % 1) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.245")] pub(crate) struct BadStringFormatType; impl Violation for BadStringFormatType { diff --git a/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs b/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs index 11ac152258..3e6f2eff6f 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs @@ -50,6 +50,7 @@ const BIDI_UNICODE: [char; 11] = [ /// ## References /// - [PEP 672: Bidirectional Marks, Embeddings, Overrides and Isolates](https://peps.python.org/pep-0672/#bidirectional-marks-embeddings-overrides-and-isolates) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.244")] pub(crate) struct BidirectionalUnicode; impl Violation for BidirectionalUnicode { diff --git a/crates/ruff_linter/src/rules/pylint/rules/binary_op_exception.rs b/crates/ruff_linter/src/rules/pylint/rules/binary_op_exception.rs index eefae02792..533d44cc5a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/binary_op_exception.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/binary_op_exception.rs @@ -47,6 +47,7 @@ impl From<&ast::BoolOp> for BoolOp { /// pass /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.258")] pub(crate) struct BinaryOpException { op: BoolOp, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs index 1795e7ed57..27d6d49ad5 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs @@ -35,6 +35,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// pass /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.9.0")] pub(crate) struct BooleanChainedComparison; impl AlwaysFixableViolation for BooleanChainedComparison { diff --git a/crates/ruff_linter/src/rules/pylint/rules/collapsible_else_if.rs b/crates/ruff_linter/src/rules/pylint/rules/collapsible_else_if.rs index 72b15426a7..1e68c246fb 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/collapsible_else_if.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/collapsible_else_if.rs @@ -46,6 +46,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `if` Statements](https://docs.python.org/3/tutorial/controlflow.html#if-statements) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.253")] pub(crate) struct CollapsibleElseIf; impl Violation for CollapsibleElseIf { diff --git a/crates/ruff_linter/src/rules/pylint/rules/compare_to_empty_string.rs b/crates/ruff_linter/src/rules/pylint/rules/compare_to_empty_string.rs index 94217d05ce..421dcac4be 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/compare_to_empty_string.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/compare_to_empty_string.rs @@ -41,6 +41,7 @@ use crate::checkers::ast::Checker; /// /// [#4282]: https://github.com/astral-sh/ruff/issues/4282 #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.255")] pub(crate) struct CompareToEmptyString { existing: String, replacement: String, diff --git a/crates/ruff_linter/src/rules/pylint/rules/comparison_of_constant.rs b/crates/ruff_linter/src/rules/pylint/rules/comparison_of_constant.rs index 8eab156203..57abf6f9e3 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/comparison_of_constant.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/comparison_of_constant.rs @@ -28,6 +28,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.221")] pub(crate) struct ComparisonOfConstant { left_constant: String, op: CmpOp, diff --git a/crates/ruff_linter/src/rules/pylint/rules/comparison_with_itself.rs b/crates/ruff_linter/src/rules/pylint/rules/comparison_with_itself.rs index 60697e17cd..9c38b10839 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/comparison_with_itself.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/comparison_with_itself.rs @@ -31,6 +31,7 @@ use crate::fix::snippet::SourceCodeSnippet; /// ## References /// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.273")] pub(crate) struct ComparisonWithItself { actual: SourceCodeSnippet, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/continue_in_finally.rs b/crates/ruff_linter/src/rules/pylint/rules/continue_in_finally.rs index a89baf379e..476dae54ed 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/continue_in_finally.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/continue_in_finally.rs @@ -37,6 +37,7 @@ use crate::checkers::ast::Checker; /// ## Options /// - `target-version` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.257")] pub(crate) struct ContinueInFinally; impl Violation for ContinueInFinally { diff --git a/crates/ruff_linter/src/rules/pylint/rules/dict_index_missing_items.rs b/crates/ruff_linter/src/rules/pylint/rules/dict_index_missing_items.rs index c30ac6883e..a7f718c1b9 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/dict_index_missing_items.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/dict_index_missing_items.rs @@ -46,6 +46,7 @@ use crate::checkers::ast::Checker; /// print(f"{instrument}: {section}") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct DictIndexMissingItems; impl Violation for DictIndexMissingItems { diff --git a/crates/ruff_linter/src/rules/pylint/rules/dict_iter_missing_items.rs b/crates/ruff_linter/src/rules/pylint/rules/dict_iter_missing_items.rs index 60c7b91d04..e973f1ce9c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/dict_iter_missing_items.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/dict_iter_missing_items.rs @@ -51,6 +51,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## Fix safety /// Due to the known problem with tuple keys, this fix is unsafe. #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.3.0")] pub(crate) struct DictIterMissingItems; impl AlwaysFixableViolation for DictIterMissingItems { diff --git a/crates/ruff_linter/src/rules/pylint/rules/duplicate_bases.rs b/crates/ruff_linter/src/rules/pylint/rules/duplicate_bases.rs index e4a5192df2..2724d9a5e0 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/duplicate_bases.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/duplicate_bases.rs @@ -55,6 +55,7 @@ use crate::{Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: Class definitions](https://docs.python.org/3/reference/compound_stmts.html#class-definitions) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.269")] pub(crate) struct DuplicateBases { base: String, class: String, diff --git a/crates/ruff_linter/src/rules/pylint/rules/empty_comment.rs b/crates/ruff_linter/src/rules/pylint/rules/empty_comment.rs index fb67f85505..004d50e5f4 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/empty_comment.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/empty_comment.rs @@ -30,6 +30,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Pylint documentation](https://pylint.pycqa.org/en/latest/user_guide/messages/refactor/empty-comment.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct EmptyComment; impl Violation for EmptyComment { diff --git a/crates/ruff_linter/src/rules/pylint/rules/eq_without_hash.rs b/crates/ruff_linter/src/rules/pylint/rules/eq_without_hash.rs index 69401e854d..4c63c8b2a3 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/eq_without_hash.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/eq_without_hash.rs @@ -64,6 +64,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `object.__hash__`](https://docs.python.org/3/reference/datamodel.html#object.__hash__) /// - [Python glossary: hashable](https://docs.python.org/3/glossary.html#term-hashable) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct EqWithoutHash; impl Violation for EqWithoutHash { diff --git a/crates/ruff_linter/src/rules/pylint/rules/global_at_module_level.rs b/crates/ruff_linter/src/rules/pylint/rules/global_at_module_level.rs index c92593740f..681bdc5a21 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/global_at_module_level.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/global_at_module_level.rs @@ -15,6 +15,7 @@ use crate::checkers::ast::Checker; /// At the module level, all names are global by default, so the `global` /// keyword is redundant. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct GlobalAtModuleLevel; impl Violation for GlobalAtModuleLevel { diff --git a/crates/ruff_linter/src/rules/pylint/rules/global_statement.rs b/crates/ruff_linter/src/rules/pylint/rules/global_statement.rs index dfa63cd427..345357ecdd 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/global_statement.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/global_statement.rs @@ -40,6 +40,7 @@ use crate::checkers::ast::Checker; /// print(var) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.253")] pub(crate) struct GlobalStatement { name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/global_variable_not_assigned.rs b/crates/ruff_linter/src/rules/pylint/rules/global_variable_not_assigned.rs index a5421541f3..26c6312e96 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/global_variable_not_assigned.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/global_variable_not_assigned.rs @@ -40,6 +40,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `global` statement](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.174")] pub(crate) struct GlobalVariableNotAssigned { name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs b/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs index 4fc2bf484e..247f1495df 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs @@ -46,6 +46,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `max`](https://docs.python.org/3/library/functions.html#max) /// - [Python documentation: `min`](https://docs.python.org/3/library/functions.html#min) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.6.0")] pub(crate) struct IfStmtMinMax { min_max: MinMax, replacement: SourceCodeSnippet, diff --git a/crates/ruff_linter/src/rules/pylint/rules/import_outside_top_level.rs b/crates/ruff_linter/src/rules/pylint/rules/import_outside_top_level.rs index 57b7fd2b27..d2a953c816 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/import_outside_top_level.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/import_outside_top_level.rs @@ -53,6 +53,7 @@ use crate::{ /// [TID253]: https://docs.astral.sh/ruff/rules/banned-module-level-imports/ /// [PEP 8]: https://peps.python.org/pep-0008/#imports #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct ImportOutsideTopLevel; impl Violation for ImportOutsideTopLevel { diff --git a/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs b/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs index dab7b51793..e60ad913fc 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs @@ -50,6 +50,7 @@ use crate::package::PackageRoot; /// [PEP 8]: https://peps.python.org/pep-0008/ /// [PEP 420]: https://peps.python.org/pep-0420/ #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.14")] pub(crate) struct ImportPrivateName { name: String, module: Option, diff --git a/crates/ruff_linter/src/rules/pylint/rules/import_self.rs b/crates/ruff_linter/src/rules/pylint/rules/import_self.rs index 03ba035f3e..d755117246 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/import_self.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/import_self.rs @@ -23,6 +23,7 @@ use crate::{Violation, checkers::ast::Checker}; /// def foo(): ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.265")] pub(crate) struct ImportSelf { name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_all_format.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_all_format.rs index e338280462..cd0cf491eb 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_all_format.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_all_format.rs @@ -27,6 +27,7 @@ use crate::{Violation, checkers::ast::Checker}; /// ## References /// - [Python documentation: The `import` statement](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.237")] pub(crate) struct InvalidAllFormat; impl Violation for InvalidAllFormat { diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_all_object.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_all_object.rs index 99d83fe98b..047d652f80 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_all_object.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_all_object.rs @@ -27,6 +27,7 @@ use crate::{Violation, checkers::ast::Checker}; /// ## References /// - [Python documentation: The `import` statement](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.237")] pub(crate) struct InvalidAllObject; impl Violation for InvalidAllObject { diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs index cb099e399a..ab7970ddce 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs @@ -35,6 +35,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `__bool__` method](https://docs.python.org/3/reference/datamodel.html#object.__bool__) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.3.3")] pub(crate) struct InvalidBoolReturnType; impl Violation for InvalidBoolReturnType { diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_bytes_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_bytes_return.rs index 9b5004e6bb..ca2126ef2a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_bytes_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_bytes_return.rs @@ -35,6 +35,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `__bytes__` method](https://docs.python.org/3/reference/datamodel.html#object.__bytes__) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.6.0")] pub(crate) struct InvalidBytesReturnType; impl Violation for InvalidBytesReturnType { diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_default.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_default.rs index 37d899b653..2d84a1b9da 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_default.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_default.rs @@ -33,6 +33,7 @@ use crate::checkers::ast::Checker; /// int(os.getenv("FOO", "1")) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.255")] pub(crate) struct InvalidEnvvarDefault; impl Violation for InvalidEnvvarDefault { diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_value.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_value.rs index 9a986e41d7..3733211a38 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_value.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_value.rs @@ -30,6 +30,7 @@ use crate::checkers::ast::Checker; /// os.getenv("1") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.255")] pub(crate) struct InvalidEnvvarValue; impl Violation for InvalidEnvvarValue { diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_hash_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_hash_return.rs index a1ecd71cf1..4ed2f1a470 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_hash_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_hash_return.rs @@ -39,6 +39,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `__hash__` method](https://docs.python.org/3/reference/datamodel.html#object.__hash__) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.6.0")] pub(crate) struct InvalidHashReturnType; impl Violation for InvalidHashReturnType { diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_index_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_index_return.rs index dca29deb76..c863e1011d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_index_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_index_return.rs @@ -41,6 +41,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `__index__` method](https://docs.python.org/3/reference/datamodel.html#object.__index__) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.6.0")] pub(crate) struct InvalidIndexReturnType; impl Violation for InvalidIndexReturnType { diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_length_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_length_return.rs index 4e155a4aab..cf659ffa2b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_length_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_length_return.rs @@ -40,6 +40,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `__len__` method](https://docs.python.org/3/reference/datamodel.html#object.__len__) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.6.0")] pub(crate) struct InvalidLengthReturnType; impl Violation for InvalidLengthReturnType { diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_str_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_str_return.rs index 2484b2f74f..ecc67de402 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_str_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_str_return.rs @@ -35,6 +35,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `__str__` method](https://docs.python.org/3/reference/datamodel.html#object.__str__) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct InvalidStrReturnType; impl Violation for InvalidStrReturnType { diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs index 6b621fe0cd..05688f67c7 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs @@ -26,6 +26,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// x = "\b" /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.257")] pub(crate) struct InvalidCharacterBackspace; impl Violation for InvalidCharacterBackspace { @@ -61,6 +62,7 @@ impl Violation for InvalidCharacterBackspace { /// x = "\x1a" /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.257")] pub(crate) struct InvalidCharacterSub; impl Violation for InvalidCharacterSub { @@ -96,6 +98,7 @@ impl Violation for InvalidCharacterSub { /// x = "\x1b" /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.257")] pub(crate) struct InvalidCharacterEsc; impl Violation for InvalidCharacterEsc { @@ -131,6 +134,7 @@ impl Violation for InvalidCharacterEsc { /// x = "\0" /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.257")] pub(crate) struct InvalidCharacterNul; impl Violation for InvalidCharacterNul { @@ -165,6 +169,7 @@ impl Violation for InvalidCharacterNul { /// x = "Dear Sir\u200b/\u200bMadam" # zero width space /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.257")] pub(crate) struct InvalidCharacterZeroWidthSpace; impl Violation for InvalidCharacterZeroWidthSpace { diff --git a/crates/ruff_linter/src/rules/pylint/rules/iteration_over_set.rs b/crates/ruff_linter/src/rules/pylint/rules/iteration_over_set.rs index 92d18485ae..0e99fc0f7c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/iteration_over_set.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/iteration_over_set.rs @@ -31,6 +31,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `set`](https://docs.python.org/3/library/stdtypes.html#set) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct IterationOverSet; impl AlwaysFixableViolation for IterationOverSet { diff --git a/crates/ruff_linter/src/rules/pylint/rules/len_test.rs b/crates/ruff_linter/src/rules/pylint/rules/len_test.rs index 1a664eadda..e64bcd12e5 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/len_test.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/len_test.rs @@ -59,6 +59,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// [PEP 8: Programming Recommendations](https://peps.python.org/pep-0008/#programming-recommendations) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct LenTest { expression: SourceCodeSnippet, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/literal_membership.rs b/crates/ruff_linter/src/rules/pylint/rules/literal_membership.rs index f8a2d91752..b11870aa63 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/literal_membership.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/literal_membership.rs @@ -34,6 +34,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [What’s New In Python 3.2](https://docs.python.org/3/whatsnew/3.2.html#optimizations) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.1")] pub(crate) struct LiteralMembership; impl AlwaysFixableViolation for LiteralMembership { diff --git a/crates/ruff_linter/src/rules/pylint/rules/load_before_global_declaration.rs b/crates/ruff_linter/src/rules/pylint/rules/load_before_global_declaration.rs index 5d93d51e50..575239ec12 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/load_before_global_declaration.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/load_before_global_declaration.rs @@ -37,6 +37,7 @@ use crate::Violation; /// ## References /// - [Python documentation: The `global` statement](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.174")] pub(crate) struct LoadBeforeGlobalDeclaration { pub(crate) name: String, pub(crate) row: SourceRow, diff --git a/crates/ruff_linter/src/rules/pylint/rules/logging.rs b/crates/ruff_linter/src/rules/pylint/rules/logging.rs index d8a13cfdf3..b126b85f77 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/logging.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/logging.rs @@ -37,6 +37,7 @@ use crate::rules::pyflakes::cformat::CFormatSummary; /// raise /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.252")] pub(crate) struct LoggingTooFewArgs; impl Violation for LoggingTooFewArgs { @@ -74,6 +75,7 @@ impl Violation for LoggingTooFewArgs { /// raise /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.252")] pub(crate) struct LoggingTooManyArgs; impl Violation for LoggingTooManyArgs { diff --git a/crates/ruff_linter/src/rules/pylint/rules/magic_value_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/magic_value_comparison.rs index a469e7b40c..c3d6c2fe5e 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/magic_value_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/magic_value_comparison.rs @@ -46,6 +46,7 @@ use crate::rules::pylint::settings::ConstantType; /// /// [PEP 8]: https://peps.python.org/pep-0008/#constants #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.221")] pub(crate) struct MagicValueComparison { value: String, } 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 8de9c13f9e..c0aab3657b 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 @@ -27,6 +27,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: Submodules](https://docs.python.org/3/reference/import.html#submodules) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct ManualFromImport { module: String, name: String, diff --git a/crates/ruff_linter/src/rules/pylint/rules/misplaced_bare_raise.rs b/crates/ruff_linter/src/rules/pylint/rules/misplaced_bare_raise.rs index 3856d2e649..98777b218c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/misplaced_bare_raise.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/misplaced_bare_raise.rs @@ -41,6 +41,7 @@ use crate::rules::pylint::helpers::in_dunder_method; /// raise ValueError("`obj` cannot be `None`") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct MisplacedBareRaise; impl Violation for MisplacedBareRaise { diff --git a/crates/ruff_linter/src/rules/pylint/rules/missing_maxsplit_arg.rs b/crates/ruff_linter/src/rules/pylint/rules/missing_maxsplit_arg.rs index e047e9b019..7843f8f925 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/missing_maxsplit_arg.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/missing_maxsplit_arg.rs @@ -41,6 +41,7 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// This rule's fix is marked as unsafe for `split()`/`rsplit()` calls that contain `*args` or `**kwargs` arguments, as /// adding a `maxsplit` argument to such a call may lead to duplicated arguments. #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.11.12")] pub(crate) struct MissingMaxsplitArg { actual_split_type: String, suggested_split_type: String, diff --git a/crates/ruff_linter/src/rules/pylint/rules/modified_iterating_set.rs b/crates/ruff_linter/src/rules/pylint/rules/modified_iterating_set.rs index 6d99e42069..b01ad881d8 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/modified_iterating_set.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/modified_iterating_set.rs @@ -46,6 +46,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `set`](https://docs.python.org/3/library/stdtypes.html#set) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.3.5")] pub(crate) struct ModifiedIteratingSet { name: Name, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/named_expr_without_context.rs b/crates/ruff_linter/src/rules/pylint/rules/named_expr_without_context.rs index 5b306bd547..3e45e26a49 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/named_expr_without_context.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/named_expr_without_context.rs @@ -25,6 +25,7 @@ use crate::checkers::ast::Checker; /// a = 42 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.270")] pub(crate) struct NamedExprWithoutContext; impl Violation for NamedExprWithoutContext { diff --git a/crates/ruff_linter/src/rules/pylint/rules/nan_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/nan_comparison.rs index 3ff711e0fd..d387f65c06 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/nan_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/nan_comparison.rs @@ -33,6 +33,7 @@ use crate::linter::float::as_nan_float_string_literal; /// pass /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct NanComparison { nan: Nan, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/nested_min_max.rs b/crates/ruff_linter/src/rules/pylint/rules/nested_min_max.rs index 8b50510a87..34322705ee 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/nested_min_max.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/nested_min_max.rs @@ -75,6 +75,7 @@ pub(crate) enum MinMax { /// - [Python documentation: `min`](https://docs.python.org/3/library/functions.html#min) /// - [Python documentation: `max`](https://docs.python.org/3/library/functions.html#max) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.266")] pub(crate) struct NestedMinMax { func: MinMax, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/no_method_decorator.rs b/crates/ruff_linter/src/rules/pylint/rules/no_method_decorator.rs index 99016e24c2..334cd47bc8 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/no_method_decorator.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/no_method_decorator.rs @@ -33,6 +33,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// def bar(cls): ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.7")] pub(crate) struct NoClassmethodDecorator; impl AlwaysFixableViolation for NoClassmethodDecorator { @@ -69,6 +70,7 @@ impl AlwaysFixableViolation for NoClassmethodDecorator { /// def bar(arg1, arg2): ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.7")] pub(crate) struct NoStaticmethodDecorator; impl AlwaysFixableViolation for NoStaticmethodDecorator { diff --git a/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs b/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs index c46a268005..fd6e24528c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs @@ -39,6 +39,7 @@ use crate::rules::flake8_unused_arguments::rules::is_not_implemented_stub_with_v /// print("Greetings friend!") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.286")] pub(crate) struct NoSelfUse { method_name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_ascii_module_import.rs b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_module_import.rs index 75046c8ffe..be493bb7de 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_ascii_module_import.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_module_import.rs @@ -30,6 +30,7 @@ use crate::checkers::ast::Checker; /// /// [PEP 672]: https://peps.python.org/pep-0672/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct NonAsciiImportName { name: String, kind: Kind, diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs index 6d66ac2345..9be3a4e377 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs @@ -26,6 +26,7 @@ use crate::checkers::ast::Checker; /// /// [PEP 672]: https://peps.python.org/pep-0672/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct NonAsciiName { name: String, kind: Kind, diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs b/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs index 1562783af2..7423c2dc76 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs @@ -69,6 +69,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// assert (foo, bar) == ([1, 2], [1, 2]) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.3.7")] pub(crate) struct NonAugmentedAssignment { operator: AugmentedOperator, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs b/crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs index 3c331c47ad..42964f1efa 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs @@ -47,6 +47,7 @@ use crate::checkers::ast::Checker; /// pass /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.1.15")] pub(crate) struct NonSlotAssignment { name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_and_global.rs b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_and_global.rs index bb8b10a2ea..fd8d54441a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_and_global.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_and_global.rs @@ -41,6 +41,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: The `global` statement](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) /// - [Python documentation: The `nonlocal` statement](https://docs.python.org/3/reference/simple_stmts.html#nonlocal) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct NonlocalAndGlobal { pub(crate) name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs index ff2bd1a2ab..90f5e0dde5 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs @@ -34,6 +34,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: The `nonlocal` statement](https://docs.python.org/3/reference/simple_stmts.html#nonlocal) /// - [PEP 3104 – Access to Names in Outer Scopes](https://peps.python.org/pep-3104/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.174")] pub(crate) struct NonlocalWithoutBinding { pub(crate) name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/potential_index_error.rs b/crates/ruff_linter/src/rules/pylint/rules/potential_index_error.rs index 4a3f29e090..2a67e6a83b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/potential_index_error.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/potential_index_error.rs @@ -19,6 +19,7 @@ use crate::checkers::ast::Checker; /// print([0, 1, 2][3]) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct PotentialIndexError; impl Violation for PotentialIndexError { diff --git a/crates/ruff_linter/src/rules/pylint/rules/property_with_parameters.rs b/crates/ruff_linter/src/rules/pylint/rules/property_with_parameters.rs index a0c2c9db75..9c77508943 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/property_with_parameters.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/property_with_parameters.rs @@ -35,6 +35,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `property`](https://docs.python.org/3/library/functions.html#property) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.153")] pub(crate) struct PropertyWithParameters; impl Violation for PropertyWithParameters { diff --git a/crates/ruff_linter/src/rules/pylint/rules/redeclared_assigned_name.rs b/crates/ruff_linter/src/rules/pylint/rules/redeclared_assigned_name.rs index b85a58b1da..aeac5463d3 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/redeclared_assigned_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/redeclared_assigned_name.rs @@ -28,6 +28,7 @@ use crate::checkers::ast::Checker; /// print(a) # 3 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct RedeclaredAssignedName { name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/redefined_argument_from_local.rs b/crates/ruff_linter/src/rules/pylint/rules/redefined_argument_from_local.rs index 856447d810..67799df816 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/redefined_argument_from_local.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/redefined_argument_from_local.rs @@ -33,6 +33,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Pylint documentation](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/redefined-argument-from-local.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct RedefinedArgumentFromLocal { pub(crate) name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs b/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs index bbdf17e8e7..3abd675316 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs @@ -49,6 +49,7 @@ use crate::checkers::ast::Checker; /// print(f.readline()) # prints a line from path2 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.252")] pub(crate) struct RedefinedLoopName { name: String, outer_kind: OuterBindingKind, diff --git a/crates/ruff_linter/src/rules/pylint/rules/redefined_slots_in_subclass.rs b/crates/ruff_linter/src/rules/pylint/rules/redefined_slots_in_subclass.rs index 43b4b5eaa3..21ccdc83da 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/redefined_slots_in_subclass.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/redefined_slots_in_subclass.rs @@ -38,6 +38,7 @@ use crate::checkers::ast::Checker; /// __slots__ = "d" /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.9.3")] pub(crate) struct RedefinedSlotsInSubclass { base: String, slot_name: String, diff --git a/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs index c9c0c00e1c..441ea068c6 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs @@ -53,6 +53,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// - [Python documentation: Membership test operations](https://docs.python.org/3/reference/expressions.html#membership-test-operations) /// - [Python documentation: `set`](https://docs.python.org/3/library/stdtypes.html#set) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.279")] pub(crate) struct RepeatedEqualityComparison { expression: SourceCodeSnippet, all_hashable: bool, diff --git a/crates/ruff_linter/src/rules/pylint/rules/repeated_isinstance_calls.rs b/crates/ruff_linter/src/rules/pylint/rules/repeated_isinstance_calls.rs index 9c71a32469..093c3fa8d3 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/repeated_isinstance_calls.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/repeated_isinstance_calls.rs @@ -49,6 +49,7 @@ use crate::fix::snippet::SourceCodeSnippet; /// /// [SIM101]: https://docs.astral.sh/ruff/rules/duplicate-isinstance-call/ #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "0.5.0")] pub(crate) struct RepeatedIsinstanceCalls { expression: SourceCodeSnippet, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/repeated_keyword_argument.rs b/crates/ruff_linter/src/rules/pylint/rules/repeated_keyword_argument.rs index b2033a5e8f..ab2db3f95d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/repeated_keyword_argument.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/repeated_keyword_argument.rs @@ -23,6 +23,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: Argument](https://docs.python.org/3/glossary.html#term-argument) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct RepeatedKeywordArgument { duplicate_keyword: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/return_in_init.rs b/crates/ruff_linter/src/rules/pylint/rules/return_in_init.rs index a80913635c..b27ee93b59 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/return_in_init.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/return_in_init.rs @@ -35,6 +35,7 @@ use crate::rules::pylint::helpers::in_dunder_method; /// ## References /// - [CodeQL: `py-explicit-return-in-init`](https://codeql.github.com/codeql-query-help/python/py-explicit-return-in-init/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.248")] pub(crate) struct ReturnInInit; impl Violation for ReturnInInit { diff --git a/crates/ruff_linter/src/rules/pylint/rules/self_assigning_variable.rs b/crates/ruff_linter/src/rules/pylint/rules/self_assigning_variable.rs index 2d9d194c6e..4c10078f8a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/self_assigning_variable.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/self_assigning_variable.rs @@ -24,6 +24,7 @@ use crate::checkers::ast::Checker; /// country = "Poland" /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.281")] pub(crate) struct SelfAssigningVariable { name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/self_or_cls_assignment.rs b/crates/ruff_linter/src/rules/pylint/rules/self_or_cls_assignment.rs index d6611acd21..e5c90407e1 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/self_or_cls_assignment.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/self_or_cls_assignment.rs @@ -46,6 +46,7 @@ use crate::checkers::ast::Checker; /// return supercls /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.6.0")] pub(crate) struct SelfOrClsAssignment { method_type: MethodType, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/shallow_copy_environ.rs b/crates/ruff_linter/src/rules/pylint/rules/shallow_copy_environ.rs index 2cfe984f86..94846decb2 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/shallow_copy_environ.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/shallow_copy_environ.rs @@ -43,6 +43,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// /// [BPO 15373]: https://bugs.python.org/issue15373 #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct ShallowCopyEnviron; impl AlwaysFixableViolation for ShallowCopyEnviron { diff --git a/crates/ruff_linter/src/rules/pylint/rules/single_string_slots.rs b/crates/ruff_linter/src/rules/pylint/rules/single_string_slots.rs index 3cf41a00db..0c6f94a483 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/single_string_slots.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/single_string_slots.rs @@ -48,6 +48,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.276")] pub(crate) struct SingleStringSlots; impl Violation for SingleStringSlots { diff --git a/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs b/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs index 91d280fb73..9096a22dc8 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs @@ -43,6 +43,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// This rule's fix is marked as unsafe, as migrating from `@singledispatch` to /// `@singledispatchmethod` may change the behavior of the code. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.6.0")] pub(crate) struct SingledispatchMethod; impl Violation for SingledispatchMethod { diff --git a/crates/ruff_linter/src/rules/pylint/rules/singledispatchmethod_function.rs b/crates/ruff_linter/src/rules/pylint/rules/singledispatchmethod_function.rs index eb2e82c258..e658b19a0d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/singledispatchmethod_function.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/singledispatchmethod_function.rs @@ -41,6 +41,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// This rule's fix is marked as unsafe, as migrating from `@singledispatchmethod` to /// `@singledispatch` may change the behavior of the code. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.6.0")] pub(crate) struct SingledispatchmethodFunction; impl Violation for SingledispatchmethodFunction { diff --git a/crates/ruff_linter/src/rules/pylint/rules/subprocess_popen_preexec_fn.rs b/crates/ruff_linter/src/rules/pylint/rules/subprocess_popen_preexec_fn.rs index b07d0bef0d..b27f592506 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/subprocess_popen_preexec_fn.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/subprocess_popen_preexec_fn.rs @@ -40,6 +40,7 @@ use crate::checkers::ast::Checker; /// /// [targeted for deprecation]: https://github.com/python/cpython/issues/82616 #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.281")] pub(crate) struct SubprocessPopenPreexecFn; impl Violation for SubprocessPopenPreexecFn { diff --git a/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs b/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs index 9d4848ff67..0ed569eef8 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs @@ -46,6 +46,7 @@ use crate::{AlwaysFixableViolation, Applicability, Fix}; /// ## References /// - [Python documentation: `subprocess.run`](https://docs.python.org/3/library/subprocess.html#subprocess.run) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.285")] pub(crate) struct SubprocessRunWithoutCheck; impl AlwaysFixableViolation for SubprocessRunWithoutCheck { diff --git a/crates/ruff_linter/src/rules/pylint/rules/super_without_brackets.rs b/crates/ruff_linter/src/rules/pylint/rules/super_without_brackets.rs index 510fc836c5..5791ee77ff 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/super_without_brackets.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/super_without_brackets.rs @@ -47,6 +47,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// return f"{original_speak} But as a dog, it barks!" /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct SuperWithoutBrackets; impl AlwaysFixableViolation for SuperWithoutBrackets { diff --git a/crates/ruff_linter/src/rules/pylint/rules/sys_exit_alias.rs b/crates/ruff_linter/src/rules/pylint/rules/sys_exit_alias.rs index c0a1a0445a..1fb4bd4792 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/sys_exit_alias.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/sys_exit_alias.rs @@ -50,6 +50,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: Constants added by the `site` module](https://docs.python.org/3/library/constants.html#constants-added-by-the-site-module) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.156")] pub(crate) struct SysExitAlias { name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs index 7fb946ea83..9242ad8342 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs @@ -44,6 +44,7 @@ use crate::checkers::ast::Checker; /// ## Options /// - `lint.pylint.max-args` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.238")] pub(crate) struct TooManyArguments { c_args: usize, max_args: usize, diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_boolean_expressions.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_boolean_expressions.rs index 76bf947411..61f0dfa442 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_boolean_expressions.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_boolean_expressions.rs @@ -26,6 +26,7 @@ use crate::checkers::ast::Checker; /// ## Options /// - `lint.pylint.max-bool-expr` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.1")] pub(crate) struct TooManyBooleanExpressions { expressions: usize, max_expressions: usize, diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_branches.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_branches.rs index d5c82b95eb..fd9d90cd38 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_branches.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_branches.rs @@ -145,6 +145,7 @@ use crate::checkers::ast::Checker; /// ## Options /// - `lint.pylint.max-branches` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.242")] pub(crate) struct TooManyBranches { branches: usize, max_branches: usize, diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_locals.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_locals.rs index 56ac5cf986..0914278ae9 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_locals.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_locals.rs @@ -20,6 +20,7 @@ use crate::checkers::ast::Checker; /// ## Options /// - `lint.pylint.max-locals` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.9")] pub(crate) struct TooManyLocals { current_amount: usize, max_amount: usize, diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_nested_blocks.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_nested_blocks.rs index 0e4ee0dea3..b07828832d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_nested_blocks.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_nested_blocks.rs @@ -19,6 +19,7 @@ use crate::checkers::ast::Checker; /// ## Options /// - `lint.pylint.max-nested-blocks` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.15")] pub(crate) struct TooManyNestedBlocks { nested_blocks: usize, max_nested_blocks: usize, diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs index 2e0e02908c..f9f979658c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs @@ -42,6 +42,7 @@ use crate::checkers::ast::Checker; /// ## Options /// - `lint.pylint.max-positional-args` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.7")] pub(crate) struct TooManyPositionalArguments { c_pos: usize, max_pos: usize, diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_public_methods.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_public_methods.rs index 53decdc547..bc1802926b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_public_methods.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_public_methods.rs @@ -83,6 +83,7 @@ use crate::checkers::ast::Checker; /// ## Options /// - `lint.pylint.max-public-methods` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.290")] pub(crate) struct TooManyPublicMethods { methods: usize, max_methods: usize, diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_return_statements.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_return_statements.rs index f4fb169653..f414f6931b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_return_statements.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_return_statements.rs @@ -52,6 +52,7 @@ use crate::{Violation, checkers::ast::Checker}; /// ## Options /// - `lint.pylint.max-returns` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.242")] pub(crate) struct TooManyReturnStatements { returns: usize, max_returns: usize, diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_statements.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_statements.rs index 7033a71d83..7be3f6ac0c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_statements.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_statements.rs @@ -48,6 +48,7 @@ use crate::checkers::ast::Checker; /// ## Options /// - `lint.pylint.max-statements` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.240")] pub(crate) struct TooManyStatements { statements: usize, max_statements: usize, diff --git a/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs b/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs index b4cd0a11f4..05e109907b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs @@ -54,6 +54,7 @@ use crate::rules::pylint::helpers::type_param_name; /// - [PEP 483 – The Theory of Type Hints: Covariance and Contravariance](https://peps.python.org/pep-0483/#covariance-and-contravariance) /// - [PEP 484 – Type Hints: Covariance and contravariance](https://peps.python.org/pep-0484/#covariance-and-contravariance) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.278")] pub(crate) struct TypeBivariance { kind: VarKind, param_name: Option, diff --git a/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs b/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs index f73b12fdad..4d1591abaa 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs @@ -43,6 +43,7 @@ use crate::rules::pylint::helpers::type_param_name; /// /// [PEP 484]: https://peps.python.org/pep-0484/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.278")] pub(crate) struct TypeNameIncorrectVariance { kind: VarKind, param_name: String, diff --git a/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs b/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs index 2d1b33bd89..1a936c95d1 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs @@ -39,6 +39,7 @@ use crate::rules::pylint::helpers::type_param_name; /// /// [PEP 484]:https://peps.python.org/pep-0484/#generics #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.277")] pub(crate) struct TypeParamNameMismatch { kind: VarKind, var_name: String, diff --git a/crates/ruff_linter/src/rules/pylint/rules/unexpected_special_method_signature.rs b/crates/ruff_linter/src/rules/pylint/rules/unexpected_special_method_signature.rs index b6bc8c255c..ea6ddb31c4 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unexpected_special_method_signature.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unexpected_special_method_signature.rs @@ -110,6 +110,7 @@ impl ExpectedParams { /// ## References /// - [Python documentation: Data model](https://docs.python.org/3/reference/datamodel.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.263")] pub(crate) struct UnexpectedSpecialMethodSignature { method_name: String, expected_params: ExpectedParams, diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dict_index_lookup.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dict_index_lookup.rs index 9e1be524b2..f108291958 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dict_index_lookup.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dict_index_lookup.rs @@ -31,6 +31,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// print(fruit_count) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct UnnecessaryDictIndexLookup; impl AlwaysFixableViolation for UnnecessaryDictIndexLookup { diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs index 17d0ab7663..dd40fa11c5 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs @@ -26,6 +26,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: Lambdas](https://docs.python.org/3/reference/expressions.html#lambda) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.153")] pub(crate) struct UnnecessaryDirectLambdaCall; impl Violation for UnnecessaryDirectLambdaCall { diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs index 62f69fad7b..f47c6d4e22 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs @@ -64,6 +64,7 @@ use ruff_python_ast::PythonVersion; /// return x > 2 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.12")] pub(crate) struct UnnecessaryDunderCall { method: String, replacement: Option, diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_lambda.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_lambda.rs index 783063ccda..2d4fae9dc4 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_lambda.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_lambda.rs @@ -44,6 +44,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// name, as in: `foo(x=1, y=2)`. Since `func` does not define the arguments /// `x` and `y`, unlike the lambda, the call would raise a `TypeError`. #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.2")] pub(crate) struct UnnecessaryLambda; impl Violation for UnnecessaryLambda { diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_list_index_lookup.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_list_index_lookup.rs index 991d25bac6..49481b1634 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_list_index_lookup.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_list_index_lookup.rs @@ -32,6 +32,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// print(letter) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct UnnecessaryListIndexLookup; impl AlwaysFixableViolation for UnnecessaryListIndexLookup { diff --git a/crates/ruff_linter/src/rules/pylint/rules/unreachable.rs b/crates/ruff_linter/src/rules/pylint/rules/unreachable.rs index b590573b4c..efd0a0db3a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unreachable.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unreachable.rs @@ -30,6 +30,7 @@ use crate::checkers::ast::Checker; /// return "reachable" /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.0.0")] pub(crate) struct UnreachableCode { name: String, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs index a730ffd567..7d9cd12506 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs @@ -55,6 +55,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// /// [PEP 597]: https://peps.python.org/pep-0597/ #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.1")] pub(crate) struct UnspecifiedEncoding { function_name: String, mode: ModeArgument, diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_else_on_loop.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_else_on_loop.rs index 5dc74c156a..e1a1a20505 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_else_on_loop.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_else_on_loop.rs @@ -47,6 +47,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `break` and `continue` Statements, and `else` Clauses on Loops](https://docs.python.org/3/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.156")] pub(crate) struct UselessElseOnLoop; impl Violation for UselessElseOnLoop { diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs index 4d7be33a71..4608297683 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs @@ -34,6 +34,7 @@ use ruff_python_ast::PythonVersion; /// This rule's fix is marked as unsafe, as converting a useless exception /// statement to a `raise` statement will change the program's behavior. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct UselessExceptionStatement; impl Violation for UselessExceptionStatement { diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs index d858fc3f76..63f53fe8fb 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs @@ -34,6 +34,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// import numpy /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.156")] pub(crate) struct UselessImportAlias { required_import_conflict: bool, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_return.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_return.rs index c33acea2d4..003b32a0e7 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_return.rs @@ -29,6 +29,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// print(5) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.257")] pub(crate) struct UselessReturn; impl AlwaysFixableViolation for UselessReturn { diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_with_lock.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_with_lock.rs index 77931841f0..a45f02cb13 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_with_lock.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_with_lock.rs @@ -48,6 +48,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `Lock Objects`](https://docs.python.org/3/library/threading.html#lock-objects) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct UselessWithLock; impl Violation for UselessWithLock { diff --git a/crates/ruff_linter/src/rules/pylint/rules/yield_from_in_async_function.rs b/crates/ruff_linter/src/rules/pylint/rules/yield_from_in_async_function.rs index fd9e2ee618..246870d5c0 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/yield_from_in_async_function.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/yield_from_in_async_function.rs @@ -24,6 +24,7 @@ use crate::Violation; /// yield number /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct YieldFromInAsyncFunction; impl Violation for YieldFromInAsyncFunction { diff --git a/crates/ruff_linter/src/rules/pylint/rules/yield_in_init.rs b/crates/ruff_linter/src/rules/pylint/rules/yield_in_init.rs index 67236eed6b..622129ef58 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/yield_in_init.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/yield_in_init.rs @@ -29,6 +29,7 @@ use crate::rules::pylint::helpers::in_dunder_method; /// ## References /// - [CodeQL: `py-init-method-is-generator`](https://codeql.github.com/codeql-query-help/python/py-init-method-is-generator/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.245")] pub(crate) struct YieldInInit; impl Violation for YieldInInit { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs index 0974d66bb7..46004e4024 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs @@ -50,6 +50,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct ConvertNamedTupleFunctionalToClass { name: String, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index 28b4b08a8e..af99187e6d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -61,6 +61,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// [Python keywords]: https://docs.python.org/3/reference/lexical_analysis.html#keywords /// [Dunder names]: https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct ConvertTypedDictFunctionalToClass { name: String, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/datetime_utc_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/datetime_utc_alias.rs index b67f312699..c37281831f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/datetime_utc_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/datetime_utc_alias.rs @@ -34,6 +34,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `datetime.UTC`](https://docs.python.org/3/library/datetime.html#datetime.UTC) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.192")] pub(crate) struct DatetimeTimezoneUTC; impl Violation for DatetimeTimezoneUTC { 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 e154084268..1ce34e2f15 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 @@ -25,6 +25,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `xml.etree.ElementTree`](https://docs.python.org/3/library/xml.etree.elementtree.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.199")] pub(crate) struct DeprecatedCElementTree; impl AlwaysFixableViolation for DeprecatedCElementTree { 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 46d317d292..9b15ac0b5b 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs @@ -64,6 +64,7 @@ enum Deprecation { /// from collections.abc import Sequence /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.239")] pub(crate) struct DeprecatedImport { deprecation: Deprecation, } 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 654dcad96d..5c13bbd07f 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 @@ -49,6 +49,7 @@ pub(crate) enum MockReference { /// - [Python documentation: `unittest.mock`](https://docs.python.org/3/library/unittest.mock.html) /// - [PyPI: `mock`](https://pypi.org/project/mock/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.206")] pub(crate) struct DeprecatedMockImport { reference_type: MockReference, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs index 2a336e9b8c..5804d543d4 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs @@ -39,6 +39,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python 3.11 documentation: Deprecated aliases](https://docs.python.org/3.11/library/unittest.html#deprecated-aliases) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct DeprecatedUnittestAlias { alias: String, target: String, diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/extraneous_parentheses.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/extraneous_parentheses.rs index c6ba676d82..9fe0324c1c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/extraneous_parentheses.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/extraneous_parentheses.rs @@ -25,6 +25,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// print("Hello, world") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.228")] pub(crate) struct ExtraneousParentheses; impl AlwaysFixableViolation for ExtraneousParentheses { 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 f92372f43a..b889c66d8c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs @@ -40,6 +40,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: f-strings](https://docs.python.org/3/reference/lexical_analysis.html#f-strings) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.224")] pub(crate) struct FString; impl Violation for FString { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/format_literals.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/format_literals.rs index 971a56ec1d..c60a9efa14 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/format_literals.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/format_literals.rs @@ -47,6 +47,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: Format String Syntax](https://docs.python.org/3/library/string.html#format-string-syntax) /// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.218")] pub(crate) struct FormatLiterals; impl Violation for FormatLiterals { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs index 9b1fd26801..1d66e806db 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs @@ -41,6 +41,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `@functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.225")] pub(crate) struct LRUCacheWithMaxsizeNone; impl AlwaysFixableViolation for LRUCacheWithMaxsizeNone { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs index 8941677219..27aaa84e31 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs @@ -39,6 +39,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// - [Python documentation: `@functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache) /// - [Let lru_cache be used as a decorator with no arguments](https://github.com/python/cpython/issues/80953) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct LRUCacheWithoutParameters; impl AlwaysFixableViolation for LRUCacheWithoutParameters { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs index 0f1a301f19..c1b6d0d540 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs @@ -128,6 +128,7 @@ impl fmt::Display for LiteralType { /// - [Python documentation: `float`](https://docs.python.org/3/library/functions.html#float) /// - [Python documentation: `bool`](https://docs.python.org/3/library/functions.html#bool) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.193")] pub(crate) struct NativeLiterals { literal_type: LiteralType, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/non_pep646_unpack.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/non_pep646_unpack.rs index 79ab523ad3..36487d7926 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/non_pep646_unpack.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/non_pep646_unpack.rs @@ -36,6 +36,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [PEP 646]: https://peps.python.org/pep-0646/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct NonPEP646Unpack; impl Violation for NonPEP646Unpack { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/open_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/open_alias.rs index 4d74273a28..563658fa66 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/open_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/open_alias.rs @@ -30,6 +30,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `io.open`](https://docs.python.org/3/library/io.html#io.open) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.196")] pub(crate) struct OpenAlias; impl Violation for OpenAlias { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/os_error_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/os_error_alias.rs index aeb01069f9..a4486b93c2 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/os_error_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/os_error_alias.rs @@ -35,6 +35,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `OSError`](https://docs.python.org/3/library/exceptions.html#OSError) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.206")] pub(crate) struct OSErrorAlias { name: Option, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs index 52e4cd9771..dfa945fab1 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -52,6 +52,7 @@ use ruff_python_semantic::SemanticModel; /// ## References /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.240")] pub(crate) struct OutdatedVersionBlock { reason: Reason, } 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 0d9f8ad2d6..b0d273b7c4 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 @@ -86,6 +86,7 @@ use super::{ /// [UP049]: https://docs.astral.sh/ruff/rules/private-type-parameter/ /// [fail]: https://github.com/python/mypy/issues/18507 #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct NonPEP695GenericClass { name: String, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs index 93bd368f45..e59b3905ce 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs @@ -78,6 +78,7 @@ use super::{DisplayTypeVars, TypeVarReferenceVisitor, check_type_vars, in_nested /// [UP049]: https://docs.astral.sh/ruff/rules/private-type-parameter/ /// [fail]: https://github.com/python/mypy/issues/18507 #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct NonPEP695GenericFunction { name: String, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs index 54390c998c..43e3ec8536 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs @@ -84,6 +84,7 @@ use super::{ /// [UP047]: https://docs.astral.sh/ruff/rules/non-pep695-generic-function/ /// [UP049]: https://docs.astral.sh/ruff/rules/private-type-parameter/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.283")] pub(crate) struct NonPEP695TypeAlias { name: String, type_alias_kind: TypeAliasKind, diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/private_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/private_type_parameter.rs index d581cc64dc..c5a20c83dc 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/private_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/private_type_parameter.rs @@ -66,6 +66,7 @@ use crate::{ /// [UP046]: https://docs.astral.sh/ruff/rules/non-pep695-generic-class /// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct PrivateTypeParameter { kind: ParamKind, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/printf_string_formatting.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/printf_string_formatting.rs index 1faffd5a71..d6de04d355 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/printf_string_formatting.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/printf_string_formatting.rs @@ -75,6 +75,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#old-string-formatting) /// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.229")] pub(crate) struct PrintfStringFormatting; impl Violation for PrintfStringFormatting { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs index e641553f84..25a85e0a18 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs @@ -87,6 +87,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// [TC008]: https://docs.astral.sh/ruff/rules/quoted-type-alias/ /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.242")] pub(crate) struct QuotedAnnotation; impl AlwaysFixableViolation for QuotedAnnotation { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/redundant_open_modes.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/redundant_open_modes.rs index 13deff4941..cf87abc039 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/redundant_open_modes.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/redundant_open_modes.rs @@ -30,6 +30,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct RedundantOpenModes { replacement: String, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs index 17de6c99fa..7c5dd2f027 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -44,6 +44,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python 3.7 release notes](https://docs.python.org/3/whatsnew/3.7.html#subprocess) /// - [Python documentation: `subprocess.run`](https://docs.python.org/3/library/subprocess.html#subprocess.run) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.199")] pub(crate) struct ReplaceStdoutStderr; impl Violation for ReplaceStdoutStderr { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs index 7bdcdad798..1adab1d52d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs @@ -77,6 +77,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [breaking change]: https://blog.pecar.me/python-enum #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.3.6")] pub(crate) struct ReplaceStrEnum { name: String, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs index 9769dde244..97000fa75f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs @@ -35,6 +35,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// - [Python 3.7 release notes](https://docs.python.org/3/whatsnew/3.7.html#subprocess) /// - [Python documentation: `subprocess.run`](https://docs.python.org/3/library/subprocess.html#subprocess.run) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.196")] pub(crate) struct ReplaceUniversalNewlines; impl AlwaysFixableViolation for ReplaceUniversalNewlines { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs index 9f7f31a2f8..cc7cbef8e0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs @@ -57,6 +57,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `super`](https://docs.python.org/3/library/functions.html#super) /// - [super/MRO, Python's most misunderstood feature.](https://www.youtube.com/watch?v=X1PQ7zzltz4) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct SuperCallWithParameters; impl Violation for SuperCallWithParameters { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs index eadfdf8eb8..85ca344d9d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs @@ -40,6 +40,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `TimeoutError`](https://docs.python.org/3/library/exceptions.html#TimeoutError) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct TimeoutErrorAlias { name: Option, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/type_of_primitive.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/type_of_primitive.rs index 251cd37dbc..08acc9850d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/type_of_primitive.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/type_of_primitive.rs @@ -30,6 +30,7 @@ use crate::rules::pyupgrade::types::Primitive; /// - [Python documentation: `type()`](https://docs.python.org/3/library/functions.html#type) /// - [Python documentation: Built-in types](https://docs.python.org/3/library/stdtypes.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct TypeOfPrimitive { primitive: Primitive, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs index 2310880eed..4e69c888b6 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs @@ -34,6 +34,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `typing.Text`](https://docs.python.org/3/library/typing.html#typing.Text) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.195")] pub(crate) struct TypingTextStrAlias { module: TypingModule, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unicode_kind_prefix.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unicode_kind_prefix.rs index 7d62237e1f..8e162fea30 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unicode_kind_prefix.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unicode_kind_prefix.rs @@ -25,6 +25,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: Unicode HOWTO](https://docs.python.org/3/howto/unicode.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.201")] pub(crate) struct UnicodeKindPrefix; impl AlwaysFixableViolation for UnicodeKindPrefix { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs index c2675200e0..41e73a3096 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs @@ -46,6 +46,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// ## References /// - [Python documentation: The Python Standard Library](https://docs.python.org/3/library/index.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.211")] pub(crate) struct UnnecessaryBuiltinImport { pub names: Vec, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs index 10649d493e..d2459b8411 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs @@ -25,6 +25,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.273")] pub(crate) struct UnnecessaryClassParentheses; impl AlwaysFixableViolation for UnnecessaryClassParentheses { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs index e9d7b90777..e2e8f10f60 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs @@ -32,6 +32,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// /// [PEP 3120]: https://peps.python.org/pep-3120/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct UTF8EncodingDeclaration; impl AlwaysFixableViolation for UTF8EncodingDeclaration { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs index e5d739d5a6..c61cee5cb8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs @@ -63,6 +63,7 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct UnnecessaryDefaultTypeArgs; impl AlwaysFixableViolation for UnnecessaryDefaultTypeArgs { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs index 7eea6a4613..12ca46ed4b 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs @@ -30,6 +30,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `str.encode`](https://docs.python.org/3/library/stdtypes.html#str.encode) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct UnnecessaryEncodeUTF8 { reason: Reason, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs index 62eea80f2b..8b5bd03bef 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs @@ -44,6 +44,7 @@ use crate::{AlwaysFixableViolation, Applicability, Fix}; /// ## References /// - [Python documentation: `__future__` — Future statement definitions](https://docs.python.org/3/library/__future__.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct UnnecessaryFutureImport { pub names: Vec, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs index 1958a23120..7cb86fe030 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs @@ -28,6 +28,7 @@ use crate::Violation; /// - [Python documentation: Generator expressions](https://docs.python.org/3/reference/expressions.html#generator-expressions) /// - [Python documentation: List comprehensions](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions) #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "0.8.0")] pub(crate) struct UnpackedListComprehension; impl Violation for UnpackedListComprehension { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs index 044c21f258..420768f1ed 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -55,6 +55,7 @@ use ruff_python_ast::PythonVersion; /// /// [PEP 585]: https://peps.python.org/pep-0585/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct NonPEP585Annotation { from: String, to: String, 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 784bc5c65e..aabbb65d15 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 @@ -54,6 +54,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// /// [PEP 604]: https://peps.python.org/pep-0604/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct NonPEP604AnnotationUnion; impl Violation for NonPEP604AnnotationUnion { @@ -110,6 +111,7 @@ impl Violation for NonPEP604AnnotationUnion { /// /// [PEP 604]: https://peps.python.org/pep-0604/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct NonPEP604AnnotationOptional; impl Violation for NonPEP604AnnotationOptional { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_isinstance.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_isinstance.rs index 17f50ab99a..df8c492c96 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_isinstance.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_isinstance.rs @@ -72,6 +72,7 @@ impl CallKind { /// [PEP 604]: https://peps.python.org/pep-0604/ /// [PEP 695]: https://peps.python.org/pep-0695/ #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "0.13.0")] pub(crate) struct NonPEP604Isinstance { kind: CallKind, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs index 09e993b2fb..75bbe20eca 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs @@ -29,6 +29,7 @@ use ruff_text_size::Ranged; /// ## References /// - [PEP 3115 – Metaclasses in Python 3000](https://peps.python.org/pep-3115/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.13.0")] pub(crate) struct UselessClassMetaclassType { name: String, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_metaclass_type.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_metaclass_type.rs index b221b2e7cd..2d2d7ac868 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_metaclass_type.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_metaclass_type.rs @@ -29,6 +29,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// ## References /// - [PEP 3115 – Metaclasses in Python 3000](https://peps.python.org/pep-3115/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct UselessMetaclassType; impl AlwaysFixableViolation for UselessMetaclassType { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs index cb1fe0152e..4a5789c78f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -32,6 +32,7 @@ use crate::{AlwaysFixableViolation, Fix}; /// ## References /// - [PEP 3115 – Metaclasses in Python 3000](https://peps.python.org/pep-3115/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct UselessObjectInheritance { name: String, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs index 8fa6118e4c..7fec2b7d79 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs @@ -51,6 +51,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: The `yield` statement](https://docs.python.org/3/reference/simple_stmts.html#the-yield-statement) /// - [PEP 380 – Syntax for Delegating to a Subgenerator](https://peps.python.org/pep-0380/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.210")] pub(crate) struct YieldInForLoop; impl Violation for YieldInForLoop { diff --git a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs index 1dd8d9c89e..b86c6e9d9e 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs @@ -39,6 +39,7 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## References /// - [Python documentation:`int.bit_count`](https://docs.python.org/3/library/stdtypes.html#int.bit_count) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct BitCount { existing: SourceCodeSnippet, replacement: SourceCodeSnippet, diff --git a/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs b/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs index 7aa11f94ba..b28b06d4a9 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs @@ -40,6 +40,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `set.discard()`](https://docs.python.org/3/library/stdtypes.html?highlight=list#frozenset.discard) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct CheckAndRemoveFromSet { element: SourceCodeSnippet, set: String, diff --git a/crates/ruff_linter/src/rules/refurb/rules/delete_full_slice.rs b/crates/ruff_linter/src/rules/refurb/rules/delete_full_slice.rs index 2fce83de33..08ffd45463 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/delete_full_slice.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/delete_full_slice.rs @@ -44,6 +44,7 @@ use crate::rules::refurb::helpers::generate_method_call; /// - [Python documentation: Mutable Sequence Types](https://docs.python.org/3/library/stdtypes.html?highlight=list#mutable-sequence-types) /// - [Python documentation: `dict.clear()`](https://docs.python.org/3/library/stdtypes.html?highlight=list#dict.clear) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.287")] pub(crate) struct DeleteFullSlice; impl Violation for DeleteFullSlice { diff --git a/crates/ruff_linter/src/rules/refurb/rules/for_loop_set_mutations.rs b/crates/ruff_linter/src/rules/refurb/rules/for_loop_set_mutations.rs index 609c0dd260..202f509557 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/for_loop_set_mutations.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/for_loop_set_mutations.rs @@ -44,6 +44,7 @@ use crate::rules::refurb::helpers::parenthesize_loop_iter_if_necessary; /// ## References /// - [Python documentation: `set`](https://docs.python.org/3/library/stdtypes.html#set) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.3.5")] pub(crate) struct ForLoopSetMutations { method_name: &'static str, batch_method_name: &'static str, diff --git a/crates/ruff_linter/src/rules/refurb/rules/for_loop_writes.rs b/crates/ruff_linter/src/rules/refurb/rules/for_loop_writes.rs index 8f20877817..23d24788ca 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/for_loop_writes.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/for_loop_writes.rs @@ -47,6 +47,7 @@ use crate::rules::refurb::helpers::parenthesize_loop_iter_if_necessary; /// ## References /// - [Python documentation: `io.IOBase.writelines`](https://docs.python.org/3/library/io.html#io.IOBase.writelines) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct ForLoopWrites { name: String, } diff --git a/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs b/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs index def4bf832e..3622148fbf 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs @@ -67,6 +67,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// [iso-8601]: https://www.iso.org/obp/ui/#iso:std:iso:8601 /// [fromisoformat]: https://docs.python.org/3/library/datetime.html#datetime.date.fromisoformat #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct FromisoformatReplaceZ; impl AlwaysFixableViolation for FromisoformatReplaceZ { diff --git a/crates/ruff_linter/src/rules/refurb/rules/fstring_number_format.rs b/crates/ruff_linter/src/rules/refurb/rules/fstring_number_format.rs index c06fe722b0..68c95def5d 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/fstring_number_format.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/fstring_number_format.rs @@ -31,6 +31,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// are display-only, as they may change the runtime behaviour of the program /// or introduce syntax errors. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.13.0")] pub(crate) struct FStringNumberFormat { replacement: Option, base: Base, diff --git a/crates/ruff_linter/src/rules/refurb/rules/hardcoded_string_charset.rs b/crates/ruff_linter/src/rules/refurb/rules/hardcoded_string_charset.rs index 0bc22b3a33..151bdc3113 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/hardcoded_string_charset.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/hardcoded_string_charset.rs @@ -29,6 +29,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: String constants](https://docs.python.org/3/library/string.html#string-constants) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.7.0")] pub(crate) struct HardcodedStringCharset { name: &'static str, } diff --git a/crates/ruff_linter/src/rules/refurb/rules/hashlib_digest_hex.rs b/crates/ruff_linter/src/rules/refurb/rules/hashlib_digest_hex.rs index 837e5ed79f..deac83a9d0 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/hashlib_digest_hex.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/hashlib_digest_hex.rs @@ -31,6 +31,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `hashlib`](https://docs.python.org/3/library/hashlib.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct HashlibDigestHex; impl Violation for HashlibDigestHex { diff --git a/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs b/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs index 51047f15bb..fa660587ef 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs @@ -39,6 +39,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// (assuming `foo()` returns a truthy value), but only once in /// `foo() or bar()`. #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.3.6")] pub(crate) struct IfExpInsteadOfOrOperator; impl Violation for IfExpInsteadOfOrOperator { diff --git a/crates/ruff_linter/src/rules/refurb/rules/if_expr_min_max.rs b/crates/ruff_linter/src/rules/refurb/rules/if_expr_min_max.rs index a514b8704e..a3bfcdd0dd 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/if_expr_min_max.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/if_expr_min_max.rs @@ -31,6 +31,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `min`](https://docs.python.org/3.11/library/functions.html#min) /// - [Python documentation: `max`](https://docs.python.org/3.11/library/functions.html#max) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct IfExprMinMax { min_max: MinMax, expression: SourceCodeSnippet, diff --git a/crates/ruff_linter/src/rules/refurb/rules/implicit_cwd.rs b/crates/ruff_linter/src/rules/refurb/rules/implicit_cwd.rs index 8be3695d80..dffe4feee6 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/implicit_cwd.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/implicit_cwd.rs @@ -29,6 +29,7 @@ use crate::{checkers::ast::Checker, importer::ImportRequest}; /// ## References /// - [Python documentation: `Path.cwd`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.cwd) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct ImplicitCwd; impl Violation for ImplicitCwd { diff --git a/crates/ruff_linter/src/rules/refurb/rules/int_on_sliced_str.rs b/crates/ruff_linter/src/rules/refurb/rules/int_on_sliced_str.rs index 588569f397..f84fdac8ed 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/int_on_sliced_str.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/int_on_sliced_str.rs @@ -48,6 +48,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `int`](https://docs.python.org/3/library/functions.html#int) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct IntOnSlicedStr { base: u8, } diff --git a/crates/ruff_linter/src/rules/refurb/rules/isinstance_type_none.rs b/crates/ruff_linter/src/rules/refurb/rules/isinstance_type_none.rs index 4ff1f1c4af..27715d00f8 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/isinstance_type_none.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/isinstance_type_none.rs @@ -32,6 +32,7 @@ use crate::{FixAvailability, Violation}; /// - [Python documentation: `type`](https://docs.python.org/3/library/functions.html#type) /// - [Python documentation: Identity comparisons](https://docs.python.org/3/reference/expressions.html#is-not) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct IsinstanceTypeNone; impl Violation for IsinstanceTypeNone { diff --git a/crates/ruff_linter/src/rules/refurb/rules/list_reverse_copy.rs b/crates/ruff_linter/src/rules/refurb/rules/list_reverse_copy.rs index bf8e6c6207..b9f894177e 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/list_reverse_copy.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/list_reverse_copy.rs @@ -47,6 +47,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: More on Lists](https://docs.python.org/3/tutorial/datastructures.html#more-on-lists) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct ListReverseCopy { name: String, } diff --git a/crates/ruff_linter/src/rules/refurb/rules/math_constant.rs b/crates/ruff_linter/src/rules/refurb/rules/math_constant.rs index c2db8c2c7e..8672237644 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/math_constant.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/math_constant.rs @@ -28,6 +28,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `math` constants](https://docs.python.org/3/library/math.html#constants) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.6")] pub(crate) struct MathConstant { literal: String, constant: &'static str, diff --git a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs index cdc1646e85..7e5a432968 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs @@ -47,6 +47,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// - [Python documentation: `abc.ABC`](https://docs.python.org/3/library/abc.html#abc.ABC) /// - [Python documentation: `abc.ABCMeta`](https://docs.python.org/3/library/abc.html#abc.ABCMeta) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.2.0")] pub(crate) struct MetaClassABCMeta; impl AlwaysFixableViolation for MetaClassABCMeta { diff --git a/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs b/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs index 29a26f44ca..2b8ecfab45 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs @@ -38,6 +38,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `print`](https://docs.python.org/3/library/functions.html#print) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct PrintEmptyString { reason: Reason, } diff --git a/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs b/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs index 365f9bf112..b64f91829a 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs @@ -41,6 +41,7 @@ use crate::{FixAvailability, Violation}; /// - [Python documentation: `Path.read_bytes`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.read_bytes) /// - [Python documentation: `Path.read_text`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.read_text) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.2")] pub(crate) struct ReadWholeFile { filename: SourceCodeSnippet, suggestion: SourceCodeSnippet, diff --git a/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs b/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs index 6bdbf94184..a6ea1eb570 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs @@ -48,6 +48,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `io.IOBase.readlines`](https://docs.python.org/3/library/io.html#io.IOBase.readlines) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct ReadlinesInFor; impl AlwaysFixableViolation for ReadlinesInFor { diff --git a/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs b/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs index dc8081cc63..a54bd261d1 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs @@ -51,6 +51,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `math.log10`](https://docs.python.org/3/library/math.html#math.log10) /// - [Python documentation: `math.e`](https://docs.python.org/3/library/math.html#math.e) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct RedundantLogBase { base: Base, arg: String, diff --git a/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs b/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs index 3ea5561cd8..5d8b332475 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs @@ -32,6 +32,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct RegexFlagAlias { flag: RegexFlag, } diff --git a/crates/ruff_linter/src/rules/refurb/rules/reimplemented_operator.rs b/crates/ruff_linter/src/rules/refurb/rules/reimplemented_operator.rs index 9f2a227538..963a094498 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/reimplemented_operator.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/reimplemented_operator.rs @@ -69,6 +69,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [descriptors]: https://docs.python.org/3/howto/descriptor.html #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.9")] pub(crate) struct ReimplementedOperator { operator: Operator, target: FunctionLikeKind, diff --git a/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs b/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs index 592d7fb759..d9501a645b 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs @@ -46,6 +46,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// [PEP 709]: https://peps.python.org/pep-0709/ /// [#7771]: https://github.com/astral-sh/ruff/issues/7771 #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.291")] pub(crate) struct ReimplementedStarmap; impl Violation for ReimplementedStarmap { diff --git a/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs b/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs index aaf7e88831..8d797d9c54 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs @@ -45,6 +45,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: More on Lists](https://docs.python.org/3/tutorial/datastructures.html#more-on-lists) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.287")] pub(crate) struct RepeatedAppend { name: String, replacement: SourceCodeSnippet, diff --git a/crates/ruff_linter/src/rules/refurb/rules/repeated_global.rs b/crates/ruff_linter/src/rules/refurb/rules/repeated_global.rs index 685f7124cc..b7aaeb106a 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/repeated_global.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/repeated_global.rs @@ -37,6 +37,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// - [Python documentation: the `global` statement](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) /// - [Python documentation: the `nonlocal` statement](https://docs.python.org/3/reference/simple_stmts.html#the-nonlocal-statement) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.4.9")] pub(crate) struct RepeatedGlobal { global_kind: GlobalKind, } diff --git a/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs b/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs index de6bdae0b3..1df2fdde78 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs @@ -42,6 +42,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons) /// - [Python documentation: Membership test operations](https://docs.python.org/3/reference/expressions.html#membership-test-operations) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.1.0")] pub(crate) struct SingleItemMembershipTest { membership_test: MembershipTest, } diff --git a/crates/ruff_linter/src/rules/refurb/rules/slice_copy.rs b/crates/ruff_linter/src/rules/refurb/rules/slice_copy.rs index 91b7903889..33cdeb1bd6 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/slice_copy.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/slice_copy.rs @@ -37,6 +37,7 @@ use crate::rules::refurb::helpers::generate_method_call; /// ## References /// - [Python documentation: Mutable Sequence Types](https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.290")] pub(crate) struct SliceCopy; impl Violation for SliceCopy { diff --git a/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs b/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs index 0cfd94f3bc..10c0f8d8a3 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs @@ -38,6 +38,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// text = text.removeprefix("pre") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.9.0")] pub(crate) struct SliceToRemovePrefixOrSuffix { affix_kind: AffixKind, stmt_or_expression: StmtOrExpr, diff --git a/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs b/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs index 41965dc04f..a6187ccaee 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs @@ -47,6 +47,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `min`](https://docs.python.org/3/library/functions.html#min) /// - [Python documentation: `max`](https://docs.python.org/3/library/functions.html#max) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.4.2")] pub(crate) struct SortedMinMax { min_max: MinMax, } diff --git a/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs b/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs index 7d92379b36..20cb03109a 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs @@ -57,6 +57,7 @@ use crate::{checkers::ast::Checker, importer::ImportRequest}; /// /// - [Python documentation: `collections`](https://docs.python.org/3/library/collections.html) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.7.3")] pub(crate) struct SubclassBuiltin { subclass: String, replacement: String, diff --git a/crates/ruff_linter/src/rules/refurb/rules/type_none_comparison.rs b/crates/ruff_linter/src/rules/refurb/rules/type_none_comparison.rs index b498a5ee49..7057458aef 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/type_none_comparison.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/type_none_comparison.rs @@ -32,6 +32,7 @@ use crate::rules::refurb::helpers::replace_with_identity_check; /// - [Python documentation: `type`](https://docs.python.org/3/library/functions.html#type) /// - [Python documentation: Identity comparisons](https://docs.python.org/3/reference/expressions.html#is-not) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct TypeNoneComparison { replacement: IdentityCheck, } diff --git a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs index 4d3ab82689..420fe0f8c6 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs @@ -58,6 +58,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `range`](https://docs.python.org/3/library/stdtypes.html#range) /// - [Python documentation: `len`](https://docs.python.org/3/library/functions.html#len) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.0.291")] pub(crate) struct UnnecessaryEnumerate { subset: EnumerateSubset, } diff --git a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs index 0b6321d8cf..e34357bd55 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs @@ -60,6 +60,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `decimal`](https://docs.python.org/3/library/decimal.html) /// - [Python documentation: `fractions`](https://docs.python.org/3/library/fractions.html) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.3.5")] pub(crate) struct UnnecessaryFromFloat { method_name: MethodName, constructor: Constructor, diff --git a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs index b4c7e8a493..04d169e3f6 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs @@ -47,6 +47,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `decimal`](https://docs.python.org/3/library/decimal.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct VerboseDecimalConstructor { replacement: String, } diff --git a/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs b/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs index 310f4babf5..bbee6dcb5a 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs @@ -44,6 +44,7 @@ use crate::{FixAvailability, Violation}; /// - [Python documentation: `Path.write_bytes`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.write_bytes) /// - [Python documentation: `Path.write_text`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.write_text) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.3.6")] pub(crate) struct WriteWholeFile { filename: SourceCodeSnippet, suggestion: SourceCodeSnippet, diff --git a/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs b/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs index f314d38c1a..33435c648e 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs @@ -74,6 +74,7 @@ use ruff_text_size::Ranged; /// ## References /// - [Python Annotations Best Practices](https://docs.python.org/3.14/howto/annotations.html) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.12.1")] pub(crate) struct AccessAnnotationsFromClassDict { python_version: PythonVersion, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs b/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs index 9ee9352c64..ba1c696d27 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs @@ -46,6 +46,7 @@ use crate::rules::ruff::rules::confusables::confusable; /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.102")] pub(crate) struct AmbiguousUnicodeCharacterString { confusable: char, representant: char, @@ -99,6 +100,7 @@ impl Violation for AmbiguousUnicodeCharacterString { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.102")] pub(crate) struct AmbiguousUnicodeCharacterDocstring { confusable: char, representant: char, @@ -152,6 +154,7 @@ impl Violation for AmbiguousUnicodeCharacterDocstring { /// /// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.108")] pub(crate) struct AmbiguousUnicodeCharacterComment { confusable: char, representant: char, diff --git a/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs b/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs index 2d34e63a0c..186b09cb4a 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs @@ -38,6 +38,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Python documentation: `assert`](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct AssertWithPrintMessage; impl AlwaysFixableViolation for AssertWithPrintMessage { diff --git a/crates/ruff_linter/src/rules/ruff/rules/assignment_in_assert.rs b/crates/ruff_linter/src/rules/ruff/rules/assignment_in_assert.rs index 1de48b6454..5787eaf6bf 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/assignment_in_assert.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/assignment_in_assert.rs @@ -48,6 +48,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `-O`](https://docs.python.org/3/using/cmdline.html#cmdoption-O) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct AssignmentInAssert; impl Violation for AssignmentInAssert { diff --git a/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs b/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs index 17657e6f9c..e93433377f 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs @@ -53,6 +53,7 @@ use crate::checkers::ast::Checker; /// - [_The Heisenbug lurking in your async code_](https://textual.textualize.io/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/) /// - [The Python Standard Library](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.247")] pub(crate) struct AsyncioDanglingTask { expr: String, method: Method, diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs b/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs index 1f4a553714..4178217718 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs @@ -62,6 +62,7 @@ use ruff_python_ast::PythonVersion; /// [PEP 695]: https://peps.python.org/pep-0695/ /// [type parameter lists]: https://docs.python.org/3/reference/compound_stmts.html#type-params #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct ClassWithMixedTypeVars; impl Violation for ClassWithMixedTypeVars { diff --git a/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs b/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs index 8c38154151..cc79c64761 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs @@ -43,6 +43,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [PEP 448 – Additional Unpacking Generalizations](https://peps.python.org/pep-0448/) /// - [Python documentation: Sequence Types — `list`, `tuple`, `range`](https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.227")] pub(crate) struct CollectionLiteralConcatenation { expression: SourceCodeSnippet, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/dataclass_enum.rs b/crates/ruff_linter/src/rules/ruff/rules/dataclass_enum.rs index 9744cff0ae..04bcb06662 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/dataclass_enum.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/dataclass_enum.rs @@ -45,6 +45,7 @@ use crate::rules::ruff::helpers::{DataclassKind, dataclass_kind}; /// ## References /// - [Python documentation: Enum HOWTO § Dataclass support](https://docs.python.org/3/howto/enum.html#dataclass-support) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct DataclassEnum; impl Violation for DataclassEnum { diff --git a/crates/ruff_linter/src/rules/ruff/rules/decimal_from_float_literal.rs b/crates/ruff_linter/src/rules/ruff/rules/decimal_from_float_literal.rs index a3bbaa608e..7de91824ce 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/decimal_from_float_literal.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/decimal_from_float_literal.rs @@ -33,6 +33,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// of the `Decimal` instance that is constructed. This can lead to unexpected /// behavior if your program relies on the previous value (whether deliberately or not). #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.9.0")] pub(crate) struct DecimalFromFloatLiteral; impl AlwaysFixableViolation for DecimalFromFloatLiteral { diff --git a/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs b/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs index 61f4b3119f..ea792ce97c 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs @@ -51,6 +51,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// defaultdict(list) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct DefaultFactoryKwarg { default_factory: SourceCodeSnippet, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs b/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs index 48a43900df..8dcd347fe3 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs @@ -40,6 +40,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// f"{a!r}" /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.267")] pub(crate) struct ExplicitFStringTypeConversion; impl Violation for ExplicitFStringTypeConversion { diff --git a/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs b/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs index 8e55965694..a19a9c451a 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs @@ -40,6 +40,7 @@ use crate::{Applicability, Fix, FixAvailability, Violation}; /// /// [documentation]: https://docs.python.org/3.13/library/stdtypes.html#dict.get #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.8.5")] pub(crate) struct FalsyDictGetFallback; impl Violation for FalsyDictGetFallback { diff --git a/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs b/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs index 26c4c5d657..000dd2587a 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs @@ -61,6 +61,7 @@ use crate::rules::ruff::helpers::{ /// ## Options /// - `lint.flake8-bugbear.extend-immutable-calls` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.262")] pub(crate) struct FunctionCallInDataclassDefaultArgument { name: Option, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/if_key_in_dict_del.rs b/crates/ruff_linter/src/rules/ruff/rules/if_key_in_dict_del.rs index b167a78894..3b590f07ff 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/if_key_in_dict_del.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/if_key_in_dict_del.rs @@ -30,6 +30,7 @@ type Dict = ExprName; /// ## Fix safety /// This rule's fix is marked as safe, unless the if statement contains comments. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct IfKeyInDictDel; impl AlwaysFixableViolation for IfKeyInDictDel { diff --git a/crates/ruff_linter/src/rules/ruff/rules/implicit_classvar_in_dataclass.rs b/crates/ruff_linter/src/rules/ruff/rules/implicit_classvar_in_dataclass.rs index 47e3fc4d8e..46a29cd05e 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/implicit_classvar_in_dataclass.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/implicit_classvar_in_dataclass.rs @@ -52,6 +52,7 @@ use crate::rules::ruff::helpers::{DataclassKind, dataclass_kind}; /// ## Options /// - [`lint.dummy-variable-rgx`] #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.9.7")] pub(crate) struct ImplicitClassVarInDataclass; impl Violation for ImplicitClassVarInDataclass { diff --git a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs index 18fc913f24..4217c837f5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs @@ -84,6 +84,7 @@ use crate::rules::ruff::typing::type_hint_explicitly_allows_none; /// /// [PEP 484]: https://peps.python.org/pep-0484/#union-types #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.273")] pub(crate) struct ImplicitOptional { conversion_type: ConversionType, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs b/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs index ec3257d9ec..ba1ae5368d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs @@ -25,6 +25,7 @@ use crate::checkers::ast::Checker; /// print("got it!") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.11.9")] pub(crate) struct InEmptyCollection; impl Violation for InEmptyCollection { diff --git a/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs b/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs index e0c537f0b0..0c676e465c 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs @@ -38,6 +38,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## Options /// - `lint.ruff.parenthesize-tuple-in-subscript` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.5.7")] pub(crate) struct IncorrectlyParenthesizedTupleInSubscript { prefer_parentheses: bool, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/indented_form_feed.rs b/crates/ruff_linter/src/rules/ruff/rules/indented_form_feed.rs index b943c65536..d0e579367d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/indented_form_feed.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/indented_form_feed.rs @@ -31,6 +31,7 @@ use crate::{Violation, checkers::ast::LintContext}; /// /// [lexical-analysis-indentation]: https://docs.python.org/3/reference/lexical_analysis.html#indentation #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.9.6")] pub(crate) struct IndentedFormFeed; impl Violation for IndentedFormFeed { diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_assert_message_literal_argument.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_assert_message_literal_argument.rs index 3d2bbbaa83..1f6c25677e 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_assert_message_literal_argument.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_assert_message_literal_argument.rs @@ -26,6 +26,7 @@ use crate::checkers::ast::Checker; /// assert len(fruits) == 2 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct InvalidAssertMessageLiteralArgument; impl Violation for InvalidAssertMessageLiteralArgument { diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs index 81d8215225..f414c47899 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs @@ -55,6 +55,7 @@ use super::suppression_comment_visitor::{ /// This fix is always marked as unsafe because it deletes the invalid suppression comment, /// rather than trying to move it to a valid position, which the user more likely intended. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct InvalidFormatterSuppressionComment { reason: IgnoredReason, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs index 5c038455a0..dff668d028 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs @@ -26,6 +26,7 @@ use crate::checkers::ast::Checker; /// var = [1, 2, 3][0] /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.278")] pub(crate) struct InvalidIndexType { value_type: String, index_type: String, diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs index bc91f9cf54..4da7ce2996 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs @@ -31,6 +31,7 @@ use crate::{FixAvailability, Violation}; /// - [Specification of `[build-system]` in pyproject.toml](https://peps.python.org/pep-0518/) /// - [Draft but implemented license declaration extensions](https://peps.python.org/pep-0639) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct InvalidPyprojectToml { pub message: String, } 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 2f1344dce0..3c9cde312e 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 @@ -33,6 +33,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## Options /// - `lint.external` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.11.4")] pub(crate) struct InvalidRuleCode { pub(crate) rule_code: String, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs b/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs index 4e7081a6b4..b48a2520f2 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs @@ -44,6 +44,7 @@ use crate::{FixAvailability, Violation, checkers::ast::Checker}; /// - [`pytest` documentation: `pytest.warns`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-warns) /// - [`pytest` documentation: `pytest.deprecated_call`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-deprecated-call) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.12.0")] pub(crate) struct LegacyFormPytestRaises { context_type: PytestContextType, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs index 1567cea1d6..a1bfc622cc 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs @@ -60,6 +60,7 @@ use crate::rules::flake8_logging_format::rules::{LoggingCallType, find_logging_c /// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) /// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.13.2")] pub(crate) struct LoggingEagerConversion { pub(crate) format_conversion: FormatConversion, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/map_int_version_parsing.rs b/crates/ruff_linter/src/rules/ruff/rules/map_int_version_parsing.rs index 4b7fa7e69f..efc1b60f13 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/map_int_version_parsing.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/map_int_version_parsing.rs @@ -35,6 +35,7 @@ use crate::checkers::ast::Checker; /// /// [version-specifier]: https://packaging.python.org/en/latest/specifications/version-specifiers/#version-specifiers #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct MapIntVersionParsing; impl Violation for MapIntVersionParsing { 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 9194d758da..229abff0d7 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 @@ -62,6 +62,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// [gettext]: https://docs.python.org/3/library/gettext.html /// [FastAPI path]: https://fastapi.tiangolo.com/tutorial/path-params/ #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.2.1")] pub(crate) struct MissingFStringSyntax; impl AlwaysFixableViolation for MissingFStringSyntax { diff --git a/crates/ruff_linter/src/rules/ruff/rules/mutable_class_default.rs b/crates/ruff_linter/src/rules/ruff/rules/mutable_class_default.rs index 4114c553f1..6dd1e1df59 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mutable_class_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mutable_class_default.rs @@ -84,6 +84,7 @@ use crate::rules::ruff::helpers::{ /// /// [ClassVar]: https://docs.python.org/3/library/typing.html#typing.ClassVar #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.273")] pub(crate) struct MutableClassDefault; impl Violation for MutableClassDefault { diff --git a/crates/ruff_linter/src/rules/ruff/rules/mutable_dataclass_default.rs b/crates/ruff_linter/src/rules/ruff/rules/mutable_dataclass_default.rs index 736ab510ce..f5ad048a9d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mutable_dataclass_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mutable_dataclass_default.rs @@ -55,6 +55,7 @@ use crate::rules::ruff::helpers::{dataclass_kind, is_class_var_annotation}; /// mutable_default: ClassVar[list[int]] = [] /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.262")] pub(crate) struct MutableDataclassDefault; impl Violation for MutableDataclassDefault { diff --git a/crates/ruff_linter/src/rules/ruff/rules/mutable_fromkeys_value.rs b/crates/ruff_linter/src/rules/ruff/rules/mutable_fromkeys_value.rs index eea779a30e..9026431df7 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mutable_fromkeys_value.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mutable_fromkeys_value.rs @@ -49,6 +49,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `dict.fromkeys`](https://docs.python.org/3/library/stdtypes.html#dict.fromkeys) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.5.0")] pub(crate) struct MutableFromkeysValue; impl Violation for MutableFromkeysValue { diff --git a/crates/ruff_linter/src/rules/ruff/rules/needless_else.rs b/crates/ruff_linter/src/rules/ruff/rules/needless_else.rs index ca510970d6..a60b6bde2e 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/needless_else.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/needless_else.rs @@ -31,6 +31,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// bar() /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.9.3")] pub(crate) struct NeedlessElse; impl AlwaysFixableViolation for NeedlessElse { diff --git a/crates/ruff_linter/src/rules/ruff/rules/never_union.rs b/crates/ruff_linter/src/rules/ruff/rules/never_union.rs index c06c054502..a13fc69459 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/never_union.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/never_union.rs @@ -35,6 +35,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `typing.Never`](https://docs.python.org/3/library/typing.html#typing.Never) /// - [Python documentation: `typing.NoReturn`](https://docs.python.org/3/library/typing.html#typing.NoReturn) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct NeverUnion { never_like: NeverLike, union_like: UnionLike, diff --git a/crates/ruff_linter/src/rules/ruff/rules/non_octal_permissions.rs b/crates/ruff_linter/src/rules/ruff/rules/non_octal_permissions.rs index ed4c2aad65..461972614b 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/non_octal_permissions.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/non_octal_permissions.rs @@ -72,6 +72,7 @@ use crate::{FixAvailability, Violation}; /// /// A fix is only available if the integer literal matches a set of common modes. #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.12.1")] pub(crate) struct NonOctalPermissions; impl Violation for NonOctalPermissions { 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 d69c9cd567..b45bd3877a 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 @@ -30,6 +30,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `typing.Optional`](https://docs.python.org/3/library/typing.html#typing.Optional) /// - [Python documentation: `None`](https://docs.python.org/3/library/constants.html#None) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.7.4")] pub(crate) struct NoneNotAtEndOfUnion; impl Violation for NoneNotAtEndOfUnion { diff --git a/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs b/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs index e7ad588a8b..7433f63f2b 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs @@ -34,6 +34,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// y = (d and e) or f /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct ParenthesizeChainedOperators; impl AlwaysFixableViolation for ParenthesizeChainedOperators { diff --git a/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs b/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs index b2a2da56a9..36941a98c9 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs @@ -74,6 +74,7 @@ use crate::rules::ruff::helpers::{DataclassKind, dataclass_kind}; /// /// [documentation]: https://docs.python.org/3/library/dataclasses.html#init-only-variables #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.9.0")] pub(crate) struct PostInitDefault; impl Violation for PostInitDefault { diff --git a/crates/ruff_linter/src/rules/ruff/rules/pytest_raises_ambiguous_pattern.rs b/crates/ruff_linter/src/rules/ruff/rules/pytest_raises_ambiguous_pattern.rs index 3f0a849329..612a832bb8 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/pytest_raises_ambiguous_pattern.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/pytest_raises_ambiguous_pattern.rs @@ -64,6 +64,7 @@ use crate::rules::flake8_pytest_style::rules::is_pytest_raises; /// - [Python documentation: `re.escape`](https://docs.python.org/3/library/re.html#re.escape) /// - [`pytest` documentation: `pytest.raises`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.13.0")] pub(crate) struct PytestRaisesAmbiguousPattern; impl Violation for PytestRaisesAmbiguousPattern { diff --git a/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs b/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs index 4321e004c3..d4e062ad71 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs @@ -60,6 +60,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// /// [microbenchmarks]: https://github.com/astral-sh/ruff/issues/5073#issuecomment-1591836349 #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.285")] pub(crate) struct QuadraticListSummation; impl AlwaysFixableViolation for QuadraticListSummation { diff --git a/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs b/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs index e11a9a7ad1..010679cf23 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs @@ -25,6 +25,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// x = eval(command) # noqa: S307 /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.6.0")] pub(crate) struct RedirectedNOQA { original: String, target: String, diff --git a/crates/ruff_linter/src/rules/ruff/rules/redundant_bool_literal.rs b/crates/ruff_linter/src/rules/ruff/rules/redundant_bool_literal.rs index 3c0fafc623..4111f9522c 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/redundant_bool_literal.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/redundant_bool_literal.rs @@ -55,6 +55,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// [#14764]: https://github.com/python/mypy/issues/14764 /// [#5421]: https://github.com/microsoft/pyright/issues/5421 #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.8.0")] pub(crate) struct RedundantBoolLiteral { seen_others: bool, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 70fd5acd6c..f2dbf87474 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -89,6 +89,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// iteration order of the items in `__all__`, in which case this /// rule's fix could theoretically cause breakage. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct UnsortedDunderAll; impl Violation for UnsortedDunderAll { diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_slots.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_slots.rs index e626ac8535..4038a9508a 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_slots.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_slots.rs @@ -83,6 +83,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// `__slots__` definition occurs, in which case this rule's fix could /// theoretically cause breakage. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.8.0")] pub(crate) struct UnsortedDunderSlots { class_name: ast::name::Name, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs b/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs index a92f7afb7b..f814bf980b 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs @@ -41,6 +41,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// This rule will emit a diagnostic but not suggest a fix if `map` has been shadowed from its /// builtin binding. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct StarmapZip; impl Violation for StarmapZip { diff --git a/crates/ruff_linter/src/rules/ruff/rules/static_key_dict_comprehension.rs b/crates/ruff_linter/src/rules/ruff/rules/static_key_dict_comprehension.rs index 63fd8e3614..3cb7aa5633 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/static_key_dict_comprehension.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/static_key_dict_comprehension.rs @@ -28,6 +28,7 @@ use crate::Violation; /// /// [B035]: https://docs.astral.sh/ruff/rules/static-key-dict-comprehension/ #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "v0.2.0")] pub(crate) struct RuffStaticKeyDictComprehension; impl Violation for RuffStaticKeyDictComprehension { diff --git a/crates/ruff_linter/src/rules/ruff/rules/test_rules.rs b/crates/ruff_linter/src/rules/ruff/rules/test_rules.rs index c6fd18bd40..d261e05ef8 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/test_rules.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/test_rules.rs @@ -69,6 +69,7 @@ pub(crate) trait TestRule { /// bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.0.0")] pub(crate) struct StableTestRule; impl Violation for StableTestRule { @@ -102,6 +103,7 @@ impl TestRule for StableTestRule { /// bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.0.0")] pub(crate) struct StableTestRuleSafeFix; impl Violation for StableTestRuleSafeFix { @@ -140,6 +142,7 @@ impl TestRule for StableTestRuleSafeFix { /// bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.0.0")] pub(crate) struct StableTestRuleUnsafeFix; impl Violation for StableTestRuleUnsafeFix { @@ -181,6 +184,7 @@ impl TestRule for StableTestRuleUnsafeFix { /// bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.0.0")] pub(crate) struct StableTestRuleDisplayOnlyFix; impl Violation for StableTestRuleDisplayOnlyFix { @@ -225,6 +229,7 @@ impl TestRule for StableTestRuleDisplayOnlyFix { /// bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.0.0")] pub(crate) struct PreviewTestRule; impl Violation for PreviewTestRule { @@ -258,6 +263,7 @@ impl TestRule for PreviewTestRule { /// bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(deprecated_since = "0.0.0")] pub(crate) struct DeprecatedTestRule; impl Violation for DeprecatedTestRule { @@ -291,6 +297,7 @@ impl TestRule for DeprecatedTestRule { /// bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(deprecated_since = "0.0.0")] pub(crate) struct AnotherDeprecatedTestRule; impl Violation for AnotherDeprecatedTestRule { @@ -327,6 +334,7 @@ impl TestRule for AnotherDeprecatedTestRule { /// bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "0.0.0")] pub(crate) struct RemovedTestRule; impl Violation for RemovedTestRule { @@ -360,6 +368,7 @@ impl TestRule for RemovedTestRule { /// bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "0.0.0")] pub(crate) struct AnotherRemovedTestRule; impl Violation for AnotherRemovedTestRule { @@ -393,6 +402,7 @@ impl TestRule for AnotherRemovedTestRule { /// bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "0.0.0")] pub(crate) struct RedirectedFromTestRule; impl Violation for RedirectedFromTestRule { @@ -426,6 +436,7 @@ impl TestRule for RedirectedFromTestRule { /// bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.0.0")] pub(crate) struct RedirectedToTestRule; impl Violation for RedirectedToTestRule { @@ -459,6 +470,7 @@ impl TestRule for RedirectedToTestRule { /// bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "0.0.0")] pub(crate) struct RedirectedFromPrefixTestRule; impl Violation for RedirectedFromPrefixTestRule { @@ -495,6 +507,7 @@ impl TestRule for RedirectedFromPrefixTestRule { /// bar /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.0.0")] pub(crate) struct PanicyTestRule; impl Violation for PanicyTestRule { diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index 39c45b19e8..b3c34c29e0 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -45,6 +45,7 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// overriding the `__round__`, `__ceil__`, `__floor__`, or `__trunc__` dunder methods /// such that they don't return an integer. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct UnnecessaryCastToInt; impl AlwaysFixableViolation for UnnecessaryCastToInt { diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs index 803adda4df..06e72cbd98 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs @@ -54,6 +54,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## References /// - [Iterators and Iterables in Python: Run Efficient Iterations](https://realpython.com/python-iterators-iterables/#when-to-use-an-iterator-in-python) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.278")] pub(crate) struct UnnecessaryIterableAllocationForFirstElement { iterable: SourceCodeSnippet, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs index 72887978b3..7c13fb3d1c 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs @@ -29,6 +29,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// ... /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.2.0")] pub(crate) struct UnnecessaryKeyCheck; impl AlwaysFixableViolation for UnnecessaryKeyCheck { diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs index 0eb3640ad8..fd31765715 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs @@ -46,6 +46,7 @@ use crate::{Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `collections.deque`](https://docs.python.org/3/library/collections.html#collections.deque) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.9.0")] pub(crate) struct UnnecessaryEmptyIterableWithinDequeCall { has_maxlen: bool, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_nested_literal.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_nested_literal.rs index 91d384560b..d8d3dd7151 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_nested_literal.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_nested_literal.rs @@ -59,6 +59,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// /// [PEP 586](https://peps.python.org/pep-0586/) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.10.0")] pub(crate) struct UnnecessaryNestedLiteral; impl Violation for UnnecessaryNestedLiteral { 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 48f2efb2cc..5ccc516ee3 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 @@ -55,6 +55,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python Regular Expression HOWTO: Common Problems - Use String Methods](https://docs.python.org/3/howto/regex.html#use-string-methods) #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.8.1")] pub(crate) struct UnnecessaryRegularExpression { replacement: Option, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs index baf5ea38a5..c7fe4687e8 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs @@ -34,6 +34,7 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// The fix is marked unsafe if it is not possible to guarantee that the first argument of /// `round()` is of type `int`, or if the fix deletes comments. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.12.0")] pub(crate) struct UnnecessaryRound; impl AlwaysFixableViolation for UnnecessaryRound { diff --git a/crates/ruff_linter/src/rules/ruff/rules/unraw_re_pattern.rs b/crates/ruff_linter/src/rules/ruff/rules/unraw_re_pattern.rs index 8a10129d74..74c23da2a9 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unraw_re_pattern.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unraw_re_pattern.rs @@ -59,6 +59,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// re.compile(r"foo\bar") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.8.0")] pub(crate) struct UnrawRePattern { module: RegexModule, func: String, diff --git a/crates/ruff_linter/src/rules/ruff/rules/unsafe_markup_use.rs b/crates/ruff_linter/src/rules/ruff/rules/unsafe_markup_use.rs index 2cc04e602b..3adf4bc5d1 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unsafe_markup_use.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unsafe_markup_use.rs @@ -73,6 +73,7 @@ use crate::Violation; /// [markupsafe-markup]: https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup /// [flake8-markupsafe]: https://github.com/vmagamedov/flake8-markupsafe #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "0.10.0")] pub(crate) struct RuffUnsafeMarkupUse { name: String, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/unused_async.rs b/crates/ruff_linter/src/rules/ruff/rules/unused_async.rs index b7c55dc0b0..ca442de8ce 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unused_async.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unused_async.rs @@ -31,6 +31,7 @@ use crate::rules::fastapi::rules::is_fastapi_route; /// bar() /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.4.0")] pub(crate) struct UnusedAsync { name: 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 52e367500b..e4645a5541 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs @@ -43,6 +43,7 @@ pub(crate) struct UnusedCodes { /// ## References /// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct UnusedNOQA { pub codes: Option, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/unused_unpacked_variable.rs b/crates/ruff_linter/src/rules/ruff/rules/unused_unpacked_variable.rs index f17d3de60d..a14bd35cd5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unused_unpacked_variable.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unused_unpacked_variable.rs @@ -46,6 +46,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [F841]: https://docs.astral.sh/ruff/rules/unused-variable/ #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.13.0")] pub(crate) struct UnusedUnpackedVariable { pub name: String, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs b/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs index a87fc40273..a63e2f679f 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs @@ -68,6 +68,7 @@ use crate::{ /// /// [PEP 8]: https://peps.python.org/pep-0008/ #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.8.2")] pub(crate) struct UsedDummyVariable { name: String, shadowed_kind: Option, diff --git a/crates/ruff_linter/src/rules/ruff/rules/useless_if_else.rs b/crates/ruff_linter/src/rules/ruff/rules/useless_if_else.rs index 6d7502dd56..bbaa49f0fa 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/useless_if_else.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/useless_if_else.rs @@ -22,6 +22,7 @@ use crate::checkers::ast::Checker; /// foo = x /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.9.0")] pub(crate) struct UselessIfElse; impl Violation for UselessIfElse { diff --git a/crates/ruff_linter/src/rules/ruff/rules/zip_instead_of_pairwise.rs b/crates/ruff_linter/src/rules/ruff/rules/zip_instead_of_pairwise.rs index 2c5396f8b5..70cb828790 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/zip_instead_of_pairwise.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/zip_instead_of_pairwise.rs @@ -40,6 +40,7 @@ use crate::{checkers::ast::Checker, importer::ImportRequest}; /// ## References /// - [Python documentation: `itertools.pairwise`](https://docs.python.org/3/library/itertools.html#itertools.pairwise) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.257")] pub(crate) struct ZipInsteadOfPairwise; impl Violation for ZipInsteadOfPairwise { diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/error_instead_of_exception.rs b/crates/ruff_linter/src/rules/tryceratops/rules/error_instead_of_exception.rs index ebc2d40a75..289c318fd6 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/error_instead_of_exception.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/error_instead_of_exception.rs @@ -52,6 +52,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.236")] pub(crate) struct ErrorInsteadOfException; impl Violation for ErrorInsteadOfException { diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_args.rs b/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_args.rs index c359367ded..bea012e05c 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_args.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_args.rs @@ -44,6 +44,7 @@ use crate::checkers::ast::Checker; /// raise CantBeNegative(x) /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.236")] pub(crate) struct RaiseVanillaArgs; impl Violation for RaiseVanillaArgs { diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_class.rs b/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_class.rs index 6549f3caad..3de68c5f30 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_class.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_class.rs @@ -53,6 +53,7 @@ use crate::checkers::ast::Checker; /// logger.error("Oops") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.236")] pub(crate) struct RaiseVanillaClass; impl Violation for RaiseVanillaClass { diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/raise_within_try.rs b/crates/ruff_linter/src/rules/tryceratops/rules/raise_within_try.rs index ce14896377..cacab604c7 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/raise_within_try.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/raise_within_try.rs @@ -50,6 +50,7 @@ use crate::checkers::ast::Checker; /// raise /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.233")] pub(crate) struct RaiseWithinTry; impl Violation for RaiseWithinTry { diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/reraise_no_cause.rs b/crates/ruff_linter/src/rules/tryceratops/rules/reraise_no_cause.rs index a50d64136e..320869e13c 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/reraise_no_cause.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/reraise_no_cause.rs @@ -37,6 +37,7 @@ use crate::Violation; /// /// [B904]: https://docs.astral.sh/ruff/rules/raise-without-from-inside-except/ #[derive(ViolationMetadata)] +#[violation_metadata(removed_since = "v0.2.0")] pub(crate) struct ReraiseNoCause; /// TRY200 diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/try_consider_else.rs b/crates/ruff_linter/src/rules/tryceratops/rules/try_consider_else.rs index cbd24bf61e..317777e9fb 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/try_consider_else.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/try_consider_else.rs @@ -51,6 +51,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: Errors and Exceptions](https://docs.python.org/3/tutorial/errors.html) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.229")] pub(crate) struct TryConsiderElse; impl Violation for TryConsiderElse { diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/type_check_without_type_error.rs b/crates/ruff_linter/src/rules/tryceratops/rules/type_check_without_type_error.rs index 59eedace93..ee8b06cd90 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/type_check_without_type_error.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/type_check_without_type_error.rs @@ -36,6 +36,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `TypeError`](https://docs.python.org/3/library/exceptions.html#TypeError) #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.230")] pub(crate) struct TypeCheckWithoutTypeError; impl Violation for TypeCheckWithoutTypeError { diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/useless_try_except.rs b/crates/ruff_linter/src/rules/tryceratops/rules/useless_try_except.rs index f52a3e95c2..d9023378ec 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/useless_try_except.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/useless_try_except.rs @@ -29,6 +29,7 @@ use crate::checkers::ast::Checker; /// bar() /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "0.7.0")] pub(crate) struct UselessTryExcept; impl Violation for UselessTryExcept { diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/verbose_log_message.rs b/crates/ruff_linter/src/rules/tryceratops/rules/verbose_log_message.rs index 9b8c6619c8..1c107065f4 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/verbose_log_message.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/verbose_log_message.rs @@ -34,6 +34,7 @@ use crate::rules::tryceratops::helpers::LoggerCandidateVisitor; /// logger.exception("Found an error") /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.250")] pub(crate) struct VerboseLogMessage; impl Violation for VerboseLogMessage { diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/verbose_raise.rs b/crates/ruff_linter/src/rules/tryceratops/rules/verbose_raise.rs index c330632c97..033f9a93f9 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/verbose_raise.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/verbose_raise.rs @@ -36,6 +36,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// This rule's fix is marked as unsafe, as it doesn't properly handle bound /// exceptions that are shadowed between the `except` and `raise` statements. #[derive(ViolationMetadata)] +#[violation_metadata(stable_since = "v0.0.231")] pub(crate) struct VerboseRaise; impl AlwaysFixableViolation for VerboseRaise { diff --git a/crates/ruff_linter/src/violation.rs b/crates/ruff_linter/src/violation.rs index bcf671d489..dc77c99bdd 100644 --- a/crates/ruff_linter/src/violation.rs +++ b/crates/ruff_linter/src/violation.rs @@ -6,7 +6,10 @@ use ruff_db::diagnostic::Diagnostic; use ruff_source_file::SourceFile; use ruff_text_size::TextRange; -use crate::{codes::Rule, message::create_lint_diagnostic}; +use crate::{ + codes::{Rule, RuleGroup}, + message::create_lint_diagnostic, +}; #[derive(Debug, Copy, Clone, Serialize)] pub enum FixAvailability { @@ -32,6 +35,15 @@ pub trait ViolationMetadata { /// Returns an explanation of what this violation catches, /// why it's bad, and what users should do instead. fn explain() -> Option<&'static str>; + + /// Returns the rule group for this violation. + fn group() -> RuleGroup; + + /// Returns the file where the violation is declared. + fn file() -> &'static str; + + /// Returns the 1-based line where the violation is declared. + fn line() -> u32; } pub trait Violation: ViolationMetadata + Sized { diff --git a/crates/ruff_macros/src/lib.rs b/crates/ruff_macros/src/lib.rs index edc0c363df..f1c8edb047 100644 --- a/crates/ruff_macros/src/lib.rs +++ b/crates/ruff_macros/src/lib.rs @@ -82,7 +82,7 @@ pub fn cache_key(input: TokenStream) -> TokenStream { TokenStream::from(stream) } -#[proc_macro_derive(ViolationMetadata)] +#[proc_macro_derive(ViolationMetadata, attributes(violation_metadata))] pub fn derive_violation_metadata(item: TokenStream) -> TokenStream { let input: DeriveInput = parse_macro_input!(item); diff --git a/crates/ruff_macros/src/map_codes.rs b/crates/ruff_macros/src/map_codes.rs index cd20179cbe..08878fc315 100644 --- a/crates/ruff_macros/src/map_codes.rs +++ b/crates/ruff_macros/src/map_codes.rs @@ -20,8 +20,6 @@ struct Rule { linter: Ident, /// The code associated with the rule, e.g., `"E112"`. code: LitStr, - /// The rule group identifier, e.g., `RuleGroup::Preview`. - group: Path, /// The path to the struct implementing the rule, e.g. /// `rules::pycodestyle::rules::logical_lines::NoIndentedBlock` path: Path, @@ -104,7 +102,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { linter, rules .iter() - .map(|(code, Rule { group, attrs, .. })| (code.as_str(), group, attrs)), + .map(|(code, Rule { attrs, .. })| (code.as_str(), attrs)), )); output.extend(quote! { @@ -254,7 +252,6 @@ fn generate_rule_to_code(linter_to_rules: &BTreeMap NoqaCode(crate::registry::Linter::#linter.common_prefix(), #code), }); - - rule_group_match_arms.extend(quote! { - #(#attrs)* Rule::#rule_name => #group, - }); } let rule_to_code = quote! { @@ -307,28 +299,20 @@ See also https://github.com/astral-sh/ruff/issues/2186. } } - pub fn group(&self) -> RuleGroup { - use crate::registry::RuleNamespace; - - match self { - #rule_group_match_arms - } - } - pub fn is_preview(&self) -> bool { - matches!(self.group(), RuleGroup::Preview) + matches!(self.group(), RuleGroup::Preview { .. }) } pub(crate) fn is_stable(&self) -> bool { - matches!(self.group(), RuleGroup::Stable) + matches!(self.group(), RuleGroup::Stable { .. }) } pub fn is_deprecated(&self) -> bool { - matches!(self.group(), RuleGroup::Deprecated) + matches!(self.group(), RuleGroup::Deprecated { .. }) } pub fn is_removed(&self) -> bool { - matches!(self.group(), RuleGroup::Removed) + matches!(self.group(), RuleGroup::Removed { .. }) } } @@ -403,6 +387,9 @@ fn register_rules<'a>(input: impl Iterator) -> TokenStream { let mut rule_message_formats_match_arms = quote!(); let mut rule_fixable_match_arms = quote!(); let mut rule_explanation_match_arms = quote!(); + let mut rule_group_match_arms = quote!(); + let mut rule_file_match_arms = quote!(); + let mut rule_line_match_arms = quote!(); for Rule { name, attrs, path, .. @@ -420,6 +407,15 @@ fn register_rules<'a>(input: impl Iterator) -> TokenStream { quote! {#(#attrs)* Self::#name => <#path as crate::Violation>::FIX_AVAILABILITY,}, ); rule_explanation_match_arms.extend(quote! {#(#attrs)* Self::#name => #path::explain(),}); + rule_group_match_arms.extend( + quote! {#(#attrs)* Self::#name => <#path as crate::ViolationMetadata>::group(),}, + ); + rule_file_match_arms.extend( + quote! {#(#attrs)* Self::#name => <#path as crate::ViolationMetadata>::file(),}, + ); + rule_line_match_arms.extend( + quote! {#(#attrs)* Self::#name => <#path as crate::ViolationMetadata>::line(),}, + ); } quote! { @@ -455,12 +451,24 @@ fn register_rules<'a>(input: impl Iterator) -> TokenStream { pub const fn fixable(&self) -> crate::FixAvailability { match self { #rule_fixable_match_arms } } + + pub fn group(&self) -> crate::codes::RuleGroup { + match self { #rule_group_match_arms } + } + + pub fn file(&self) -> &'static str { + match self { #rule_file_match_arms } + } + + pub fn line(&self) -> u32 { + match self { #rule_line_match_arms } + } } } } impl Parse for Rule { - /// Parses a match arm such as `(Pycodestyle, "E112") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoIndentedBlock),` + /// Parses a match arm such as `(Pycodestyle, "E112") => rules::pycodestyle::rules::logical_lines::NoIndentedBlock,` fn parse(input: syn::parse::ParseStream) -> syn::Result { let attrs = Attribute::parse_outer(input)?; let pat_tuple; @@ -469,18 +477,13 @@ impl Parse for Rule { let _: Token!(,) = pat_tuple.parse()?; let code: LitStr = pat_tuple.parse()?; let _: Token!(=>) = input.parse()?; - let pat_tuple; - parenthesized!(pat_tuple in input); - let group: Path = pat_tuple.parse()?; - let _: Token!(,) = pat_tuple.parse()?; - let rule_path: Path = pat_tuple.parse()?; + let rule_path: Path = input.parse()?; let _: Token!(,) = input.parse()?; let rule_name = rule_path.segments.last().unwrap().ident.clone(); Ok(Rule { name: rule_name, linter, code, - group, path: rule_path, attrs, }) diff --git a/crates/ruff_macros/src/rule_code_prefix.rs b/crates/ruff_macros/src/rule_code_prefix.rs index 489718cd68..3b3c74fc10 100644 --- a/crates/ruff_macros/src/rule_code_prefix.rs +++ b/crates/ruff_macros/src/rule_code_prefix.rs @@ -2,11 +2,11 @@ use std::collections::{BTreeMap, BTreeSet}; use proc_macro2::Span; use quote::quote; -use syn::{Attribute, Ident, Path}; +use syn::{Attribute, Ident}; pub(crate) fn expand<'a>( prefix_ident: &Ident, - variants: impl Iterator)>, + variants: impl Iterator)>, ) -> proc_macro2::TokenStream { // Build up a map from prefix to matching RuleCodes. let mut prefix_to_codes: BTreeMap> = BTreeMap::default(); diff --git a/crates/ruff_macros/src/violation_metadata.rs b/crates/ruff_macros/src/violation_metadata.rs index 3bdceb4079..2fdabfabcc 100644 --- a/crates/ruff_macros/src/violation_metadata.rs +++ b/crates/ruff_macros/src/violation_metadata.rs @@ -5,6 +5,13 @@ use syn::{Attribute, DeriveInput, Error, Lit, LitStr, Meta}; pub(crate) fn violation_metadata(input: DeriveInput) -> syn::Result { let docs = get_docs(&input.attrs)?; + let Some(group) = get_rule_status(&input.attrs)? else { + return Err(Error::new_spanned( + input, + "Missing required rule group metadata", + )); + }; + let name = input.ident; let (impl_generics, ty_generics, where_clause) = &input.generics.split_for_impl(); @@ -20,6 +27,18 @@ pub(crate) fn violation_metadata(input: DeriveInput) -> syn::Result fn explain() -> Option<&'static str> { Some(#docs) } + + fn group() -> crate::codes::RuleGroup { + crate::codes::#group + } + + fn file() -> &'static str { + file!() + } + + fn line() -> u32 { + line!() + } } }) } @@ -43,6 +62,49 @@ fn get_docs(attrs: &[Attribute]) -> syn::Result { Ok(explanation) } +/// Extract the rule status attribute. +/// +/// These attributes look like: +/// +/// ```ignore +/// #[violation_metadata(stable_since = "1.2.3")] +/// struct MyRule; +/// ``` +/// +/// The result is returned as a `TokenStream` so that the version string literal can be combined +/// with the proper `RuleGroup` variant, e.g. `RuleGroup::Stable` for `stable_since` above. +fn get_rule_status(attrs: &[Attribute]) -> syn::Result> { + let mut group = None; + for attr in attrs { + if attr.path().is_ident("violation_metadata") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("stable_since") { + let lit: LitStr = meta.value()?.parse()?; + group = Some(quote!(RuleGroup::Stable { since: #lit })); + return Ok(()); + } else if meta.path.is_ident("preview_since") { + let lit: LitStr = meta.value()?.parse()?; + group = Some(quote!(RuleGroup::Preview { since: #lit })); + return Ok(()); + } else if meta.path.is_ident("deprecated_since") { + let lit: LitStr = meta.value()?.parse()?; + group = Some(quote!(RuleGroup::Deprecated { since: #lit })); + return Ok(()); + } else if meta.path.is_ident("removed_since") { + let lit: LitStr = meta.value()?.parse()?; + group = Some(quote!(RuleGroup::Removed { since: #lit })); + return Ok(()); + } + Err(Error::new_spanned( + attr, + "unimplemented violation metadata option", + )) + })?; + } + } + Ok(group) +} + fn parse_attr<'a, const LEN: usize>( path: [&'static str; LEN], attr: &'a Attribute, diff --git a/scripts/add_rule.py b/scripts/add_rule.py index 400c5ac6ec..48ecd34736 100755 --- a/scripts/add_rule.py +++ b/scripts/add_rule.py @@ -109,6 +109,7 @@ use crate::checkers::ast::Checker; /// ```python /// ``` #[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "TODO: current version + 1")] pub(crate) struct {name}; impl Violation for {name} {{ @@ -140,7 +141,7 @@ pub(crate) fn {rule_name_snake}(checker: &mut Checker) {{}} linter_name = linter.split(" ")[0].replace("-", "_") rule = f"""rules::{linter_name}::rules::{name}""" lines.append( - " " * 8 + f"""({variant}, "{code}") => (RuleGroup::Preview, {rule}),\n""", + " " * 8 + f"""({variant}, "{code}") => {rule},\n""", ) lines.sort() text += "".join(lines) From 83a3bc4ee94de552d5cec9a3146aff00dade6903 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:17:22 -0400 Subject: [PATCH 025/188] Bump 0.14.2 (#21051) --- CHANGELOG.md | 48 +++++++++++++++++++++++++++++++ Cargo.lock | 6 ++-- README.md | 6 ++-- crates/ruff/Cargo.toml | 2 +- crates/ruff_linter/Cargo.toml | 2 +- crates/ruff_wasm/Cargo.toml | 2 +- docs/integrations.md | 8 +++--- docs/tutorial.md | 2 +- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- 10 files changed, 64 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d95fa96277..4689757c34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## 0.14.2 + +Released on 2025-10-23. + +### Preview features + +- \[`flake8-gettext`\] Resolve qualified names and built-in bindings (`INT001`, `INT002`, `INT003`) ([#19045](https://github.com/astral-sh/ruff/pull/19045)) + +### Bug fixes + +- Avoid reusing nested, interpolated quotes before Python 3.12 ([#20930](https://github.com/astral-sh/ruff/pull/20930)) +- Catch syntax errors in nested interpolations before Python 3.12 ([#20949](https://github.com/astral-sh/ruff/pull/20949)) +- \[`fastapi`\] Handle ellipsis defaults in `FAST002` autofix ([#20810](https://github.com/astral-sh/ruff/pull/20810)) +- \[`flake8-simplify`\] Skip `SIM911` when unknown arguments are present ([#20697](https://github.com/astral-sh/ruff/pull/20697)) +- \[`pyupgrade`\] Always parenthesize assignment expressions in fix for `f-string` (`UP032`) ([#21003](https://github.com/astral-sh/ruff/pull/21003)) +- \[`pyupgrade`\] Fix `UP032` conversion for decimal ints with underscores ([#21022](https://github.com/astral-sh/ruff/pull/21022)) +- \[`fastapi`\] Skip autofix for keyword and `__debug__` path params (`FAST003`) ([#20960](https://github.com/astral-sh/ruff/pull/20960)) + +### Rule changes + +- \[`flake8-bugbear`\] Skip `B905` and `B912` for fewer than two iterables and no starred arguments ([#20998](https://github.com/astral-sh/ruff/pull/20998)) +- \[`ruff`\] Use `DiagnosticTag` for more `pyflakes` and `pandas` rules ([#20801](https://github.com/astral-sh/ruff/pull/20801)) + +### CLI + +- Improve JSON output from `ruff rule` ([#20168](https://github.com/astral-sh/ruff/pull/20168)) + +### Documentation + +- Add source to testimonial ([#20971](https://github.com/astral-sh/ruff/pull/20971)) +- Document when a rule was added ([#21035](https://github.com/astral-sh/ruff/pull/21035)) + +### Other changes + +- [syntax-errors] Name is parameter and global ([#20426](https://github.com/astral-sh/ruff/pull/20426)) +- [syntax-errors] Alternative `match` patterns bind different names ([#20682](https://github.com/astral-sh/ruff/pull/20682)) + +### Contributors + +- [@hengky-kurniawan-1](https://github.com/hengky-kurniawan-1) +- [@ShalokShalom](https://github.com/ShalokShalom) +- [@robsdedude](https://github.com/robsdedude) +- [@LoicRiegel](https://github.com/LoicRiegel) +- [@TaKO8Ki](https://github.com/TaKO8Ki) +- [@dylwil3](https://github.com/dylwil3) +- [@11happy](https://github.com/11happy) +- [@ntBre](https://github.com/ntBre) + ## 0.14.1 Released on 2025-10-16. diff --git a/Cargo.lock b/Cargo.lock index 80fdf8b809..8a6afb3be2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2835,7 +2835,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.14.1" +version = "0.14.2" dependencies = [ "anyhow", "argfile", @@ -3092,7 +3092,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.14.1" +version = "0.14.2" dependencies = [ "aho-corasick", "anyhow", @@ -3447,7 +3447,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.14.1" +version = "0.14.2" dependencies = [ "console_error_panic_hook", "console_log", diff --git a/README.md b/README.md index 3843efe5fa..92d707838a 100644 --- a/README.md +++ b/README.md @@ -147,8 +147,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.14.1/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.14.1/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.14.2/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.14.2/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -181,7 +181,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.14.1 + rev: v0.14.2 hooks: # Run the linter. - id: ruff-check diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 8dbd98d449..c1511d805b 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.14.1" +version = "0.14.2" publish = true authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index c655f357f5..bc25d4574f 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.14.1" +version = "0.14.2" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index e5b70f3b80..f399ef1007 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.14.1" +version = "0.14.2" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/integrations.md b/docs/integrations.md index 50cf0c1136..441845a474 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.14.1-alpine + name: ghcr.io/astral-sh/ruff:0.14.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.14.1 + rev: v0.14.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.14.1 + rev: v0.14.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.14.1 + rev: v0.14.2 hooks: # Run the linter. - id: ruff-check diff --git a/docs/tutorial.md b/docs/tutorial.md index 7e3d0bd0d1..f3f2a8b3dd 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.14.1 + rev: v0.14.2 hooks: # Run the linter. - id: ruff diff --git a/pyproject.toml b/pyproject.toml index f3727fd847..91b5430a8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.14.1" +version = "0.14.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 6bce2307a9..df9f38e5db 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.14.1" +version = "0.14.2" description = "" authors = ["Charles Marsh "] From 05cde8bd19056de1a2e27a9bdd8fd0fb859b30fc Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Oct 2025 05:12:52 +0800 Subject: [PATCH 026/188] [`airflow`] Extend `airflow.models..Param` check (`AIR311`) (#21043) ## Summary * Extend `airflow.models.Param` to include `airflow.models.param.Param` case and include both `airflow.models.param.ParamDict` and `airflow.models.param.DagParam` and their `airflow.models.` counter part ## Test Plan update the text fixture accordingly and reorganize them in the third commit --- .../test/fixtures/airflow/AIR311_names.py | 12 +- .../airflow/rules/suggested_to_update_3_0.rs | 9 +- ...irflow__tests__AIR311_AIR311_names.py.snap | 194 ++++++++++++++---- 3 files changed, 168 insertions(+), 47 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR311_names.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR311_names.py index 2b415aa407..cfacd90341 100644 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR311_names.py +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR311_names.py @@ -91,10 +91,20 @@ get_unique_task_id() task_decorator_factory() -from airflow.models import Param +from airflow.models import DagParam, Param, ParamsDict # airflow.models Param() +DagParam() +ParamsDict() + + +from airflow.models.param import DagParam, Param, ParamsDict + +# airflow.models.param +Param() +DagParam() +ParamsDict() from airflow.sensors.base import ( diff --git a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs index e939387bda..7f19161797 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs @@ -262,9 +262,14 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { name: (*rest).to_string(), } } - ["airflow", "models", "Param"] => Replacement::Rename { + [ + "airflow", + "models", + .., + rest @ ("Param" | "ParamsDict" | "DagParam"), + ] => Replacement::SourceModuleMoved { module: "airflow.sdk.definitions.param", - name: "Param", + name: (*rest).to_string(), }, // airflow.models.baseoperator diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_names.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_names.py.snap index a3bced5174..ef5a2957c1 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_names.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_names.py.snap @@ -737,79 +737,185 @@ AIR311 [*] `airflow.models.Param` is removed in Airflow 3.0; It still works in A 96 | # airflow.models 97 | Param() | ^^^^^ +98 | DagParam() +99 | ParamsDict() | help: Use `Param` from `airflow.sdk.definitions.param` instead. 91 | task_decorator_factory() 92 | 93 | - - from airflow.models import Param -94 + from airflow.sdk.definitions.param import Param -95 | + - from airflow.models import DagParam, Param, ParamsDict +94 + from airflow.models import DagParam, ParamsDict +95 + from airflow.sdk.definitions.param import Param +96 | +97 | # airflow.models +98 | Param() +note: This is an unsafe fix and may change runtime behavior + +AIR311 [*] `airflow.models.DagParam` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + --> AIR311_names.py:98:1 + | 96 | # airflow.models 97 | Param() +98 | DagParam() + | ^^^^^^^^ +99 | ParamsDict() + | +help: Use `DagParam` from `airflow.sdk.definitions.param` instead. +91 | task_decorator_factory() +92 | +93 | + - from airflow.models import DagParam, Param, ParamsDict +94 + from airflow.models import Param, ParamsDict +95 + from airflow.sdk.definitions.param import DagParam +96 | +97 | # airflow.models +98 | Param() +note: This is an unsafe fix and may change runtime behavior + +AIR311 [*] `airflow.models.ParamsDict` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + --> AIR311_names.py:99:1 + | +97 | Param() +98 | DagParam() +99 | ParamsDict() + | ^^^^^^^^^^ + | +help: Use `ParamsDict` from `airflow.sdk.definitions.param` instead. +91 | task_decorator_factory() +92 | +93 | + - from airflow.models import DagParam, Param, ParamsDict +94 + from airflow.models import DagParam, Param +95 + from airflow.sdk.definitions.param import ParamsDict +96 | +97 | # airflow.models +98 | Param() +note: This is an unsafe fix and may change runtime behavior + +AIR311 [*] `airflow.models.param.Param` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + --> AIR311_names.py:105:1 + | +104 | # airflow.models.param +105 | Param() + | ^^^^^ +106 | DagParam() +107 | ParamsDict() + | +help: Use `Param` from `airflow.sdk.definitions.param` instead. +99 | ParamsDict() +100 | +101 | + - from airflow.models.param import DagParam, Param, ParamsDict +102 + from airflow.models.param import DagParam, ParamsDict +103 + from airflow.sdk.definitions.param import Param +104 | +105 | # airflow.models.param +106 | Param() +note: This is an unsafe fix and may change runtime behavior + +AIR311 [*] `airflow.models.param.DagParam` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + --> AIR311_names.py:106:1 + | +104 | # airflow.models.param +105 | Param() +106 | DagParam() + | ^^^^^^^^ +107 | ParamsDict() + | +help: Use `DagParam` from `airflow.sdk.definitions.param` instead. +99 | ParamsDict() +100 | +101 | + - from airflow.models.param import DagParam, Param, ParamsDict +102 + from airflow.models.param import Param, ParamsDict +103 + from airflow.sdk.definitions.param import DagParam +104 | +105 | # airflow.models.param +106 | Param() +note: This is an unsafe fix and may change runtime behavior + +AIR311 [*] `airflow.models.param.ParamsDict` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + --> AIR311_names.py:107:1 + | +105 | Param() +106 | DagParam() +107 | ParamsDict() + | ^^^^^^^^^^ + | +help: Use `ParamsDict` from `airflow.sdk.definitions.param` instead. +99 | ParamsDict() +100 | +101 | + - from airflow.models.param import DagParam, Param, ParamsDict +102 + from airflow.models.param import DagParam, Param +103 + from airflow.sdk.definitions.param import ParamsDict +104 | +105 | # airflow.models.param +106 | Param() note: This is an unsafe fix and may change runtime behavior AIR311 [*] `airflow.sensors.base.BaseSensorOperator` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - --> AIR311_names.py:107:1 + --> AIR311_names.py:117:1 | -106 | # airflow.sensors.base -107 | BaseSensorOperator() +116 | # airflow.sensors.base +117 | BaseSensorOperator() | ^^^^^^^^^^^^^^^^^^ -108 | PokeReturnValue() -109 | poke_mode_only() +118 | PokeReturnValue() +119 | poke_mode_only() | help: Use `BaseSensorOperator` from `airflow.sdk` instead. -98 | -99 | -100 | from airflow.sensors.base import ( +108 | +109 | +110 | from airflow.sensors.base import ( - BaseSensorOperator, -101 | PokeReturnValue, -102 | poke_mode_only, -103 | ) -104 + from airflow.sdk import BaseSensorOperator -105 | -106 | # airflow.sensors.base -107 | BaseSensorOperator() +111 | PokeReturnValue, +112 | poke_mode_only, +113 | ) +114 + from airflow.sdk import BaseSensorOperator +115 | +116 | # airflow.sensors.base +117 | BaseSensorOperator() note: This is an unsafe fix and may change runtime behavior AIR311 [*] `airflow.sensors.base.PokeReturnValue` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - --> AIR311_names.py:108:1 + --> AIR311_names.py:118:1 | -106 | # airflow.sensors.base -107 | BaseSensorOperator() -108 | PokeReturnValue() +116 | # airflow.sensors.base +117 | BaseSensorOperator() +118 | PokeReturnValue() | ^^^^^^^^^^^^^^^ -109 | poke_mode_only() +119 | poke_mode_only() | help: Use `PokeReturnValue` from `airflow.sdk` instead. -99 | -100 | from airflow.sensors.base import ( -101 | BaseSensorOperator, +109 | +110 | from airflow.sensors.base import ( +111 | BaseSensorOperator, - PokeReturnValue, -102 | poke_mode_only, -103 | ) -104 + from airflow.sdk import PokeReturnValue -105 | -106 | # airflow.sensors.base -107 | BaseSensorOperator() +112 | poke_mode_only, +113 | ) +114 + from airflow.sdk import PokeReturnValue +115 | +116 | # airflow.sensors.base +117 | BaseSensorOperator() note: This is an unsafe fix and may change runtime behavior AIR311 [*] `airflow.sensors.base.poke_mode_only` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - --> AIR311_names.py:109:1 + --> AIR311_names.py:119:1 | -107 | BaseSensorOperator() -108 | PokeReturnValue() -109 | poke_mode_only() +117 | BaseSensorOperator() +118 | PokeReturnValue() +119 | poke_mode_only() | ^^^^^^^^^^^^^^ | help: Use `poke_mode_only` from `airflow.sdk` instead. -100 | from airflow.sensors.base import ( -101 | BaseSensorOperator, -102 | PokeReturnValue, +110 | from airflow.sensors.base import ( +111 | BaseSensorOperator, +112 | PokeReturnValue, - poke_mode_only, -103 | ) -104 + from airflow.sdk import poke_mode_only -105 | -106 | # airflow.sensors.base -107 | BaseSensorOperator() +113 | ) +114 + from airflow.sdk import poke_mode_only +115 | +116 | # airflow.sensors.base +117 | BaseSensorOperator() note: This is an unsafe fix and may change runtime behavior From 28aed61a22dcd13068530486c9af74f9727b2a43 Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Fri, 24 Oct 2025 06:02:41 +0800 Subject: [PATCH 027/188] [`pylint`] Implement `stop-iteration-return` (`PLR1708`) (#20733) ## Summary implement pylint rule stop-iteration-return / R1708 ## Test Plan --------- Co-authored-by: Brent Westbrook --- .../fixtures/pylint/stop_iteration_return.py | 131 ++++++++++++++++++ .../src/checkers/ast/analyze/statement.rs | 3 + crates/ruff_linter/src/codes.rs | 1 + crates/ruff_linter/src/rules/pylint/mod.rs | 1 + .../ruff_linter/src/rules/pylint/rules/mod.rs | 2 + .../pylint/rules/stop_iteration_return.rs | 114 +++++++++++++++ ...sts__PLR1708_stop_iteration_return.py.snap | 109 +++++++++++++++ ruff.schema.json | 1 + 8 files changed, 362 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/pylint/stop_iteration_return.py create mode 100644 crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs create mode 100644 crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1708_stop_iteration_return.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/stop_iteration_return.py b/crates/ruff_linter/resources/test/fixtures/pylint/stop_iteration_return.py new file mode 100644 index 0000000000..a35e31a21e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/stop_iteration_return.py @@ -0,0 +1,131 @@ +"""Test cases for PLR1708 stop-iteration-return.""" + + +# Valid cases - should not trigger the rule +def normal_function(): + raise StopIteration # Not a generator, should not trigger + + +def normal_function_with_value(): + raise StopIteration("value") # Not a generator, should not trigger + + +def generator_with_return(): + yield 1 + yield 2 + return "finished" # This is the correct way + + +def generator_with_yield_from(): + yield from [1, 2, 3] + + +def generator_without_stop_iteration(): + yield 1 + yield 2 + # No explicit termination + + +def generator_with_other_exception(): + yield 1 + raise ValueError("something else") # Different exception + + +# Invalid cases - should trigger the rule +def generator_with_stop_iteration(): + yield 1 + yield 2 + raise StopIteration # Should trigger + + +def generator_with_stop_iteration_value(): + yield 1 + yield 2 + raise StopIteration("finished") # Should trigger + + +def generator_with_stop_iteration_expr(): + yield 1 + yield 2 + raise StopIteration(1 + 2) # Should trigger + + +def async_generator_with_stop_iteration(): + yield 1 + yield 2 + raise StopIteration("async") # Should trigger + + +def nested_generator(): + def inner_gen(): + yield 1 + raise StopIteration("inner") # Should trigger + + yield from inner_gen() + + +def generator_in_class(): + class MyClass: + def generator_method(self): + yield 1 + raise StopIteration("method") # Should trigger + + return MyClass + + +# Complex cases +def complex_generator(): + try: + yield 1 + yield 2 + raise StopIteration("complex") # Should trigger + except ValueError: + yield 3 + finally: + pass + + +def generator_with_conditional_stop_iteration(condition): + yield 1 + if condition: + raise StopIteration("conditional") # Should trigger + yield 2 + + +# Edge cases +def generator_with_bare_stop_iteration(): + yield 1 + raise StopIteration # Should trigger (no arguments) + + +def generator_with_stop_iteration_in_loop(): + for i in range(5): + yield i + if i == 3: + raise StopIteration("loop") # Should trigger + + +# Should not trigger - different exceptions +def generator_with_runtime_error(): + yield 1 + raise RuntimeError("not StopIteration") # Should not trigger + + +def generator_with_custom_exception(): + yield 1 + raise CustomException("custom") # Should not trigger + + +class CustomException(Exception): + pass + + +# Generator comprehensions should not be affected +list_comp = [x for x in range(10)] # Should not trigger + + +# Lambda in generator context +def generator_with_lambda(): + yield 1 + func = lambda x: x # Just a regular lambda + yield 2 diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index cded9e44e6..7c0037e10d 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -951,6 +951,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.is_rule_enabled(Rule::MisplacedBareRaise) { pylint::rules::misplaced_bare_raise(checker, raise); } + if checker.is_rule_enabled(Rule::StopIterationReturn) { + pylint::rules::stop_iteration_return(checker, raise); + } } Stmt::AugAssign(aug_assign @ ast::StmtAugAssign { target, .. }) => { if checker.is_rule_enabled(Rule::GlobalStatement) { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index eab00b66d8..172841dc7c 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -286,6 +286,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "R1702") => rules::pylint::rules::TooManyNestedBlocks, (Pylint, "R1704") => rules::pylint::rules::RedefinedArgumentFromLocal, (Pylint, "R1706") => rules::pylint::rules::AndOrTernary, + (Pylint, "R1708") => rules::pylint::rules::StopIterationReturn, (Pylint, "R1711") => rules::pylint::rules::UselessReturn, (Pylint, "R1714") => rules::pylint::rules::RepeatedEqualityComparison, (Pylint, "R1722") => rules::pylint::rules::SysExitAlias, diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index 7813ffd1c6..a0ab9a908e 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -52,6 +52,7 @@ mod tests { #[test_case(Rule::ManualFromImport, Path::new("import_aliasing.py"))] #[test_case(Rule::IfStmtMinMax, Path::new("if_stmt_min_max.py"))] #[test_case(Rule::SingleStringSlots, Path::new("single_string_slots.py"))] + #[test_case(Rule::StopIterationReturn, Path::new("stop_iteration_return.py"))] #[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_0.py"))] #[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_1.py"))] #[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_2.py"))] diff --git a/crates/ruff_linter/src/rules/pylint/rules/mod.rs b/crates/ruff_linter/src/rules/pylint/rules/mod.rs index 891691f21b..2853bb84c9 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/mod.rs @@ -75,6 +75,7 @@ pub(crate) use shallow_copy_environ::*; pub(crate) use single_string_slots::*; pub(crate) use singledispatch_method::*; pub(crate) use singledispatchmethod_function::*; +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::*; @@ -185,6 +186,7 @@ mod shallow_copy_environ; mod single_string_slots; mod singledispatch_method; mod singledispatchmethod_function; +mod stop_iteration_return; mod subprocess_popen_preexec_fn; mod subprocess_run_without_check; mod super_without_brackets; diff --git a/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs b/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs new file mode 100644 index 0000000000..2abc339592 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs @@ -0,0 +1,114 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast as ast; +use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt}; +use ruff_text_size::Ranged; + +use crate::Violation; +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for explicit `raise StopIteration` in generator functions. +/// +/// ## Why is this bad? +/// Raising `StopIteration` in a generator function causes a `RuntimeError` +/// when the generator is iterated over. +/// +/// Instead of `raise StopIteration`, use `return` in generator functions. +/// +/// ## Example +/// ```python +/// def my_generator(): +/// yield 1 +/// yield 2 +/// raise StopIteration # This causes RuntimeError at runtime +/// ``` +/// +/// Use instead: +/// ```python +/// def my_generator(): +/// yield 1 +/// yield 2 +/// return # Use return instead +/// ``` +/// +/// ## References +/// - [PEP 479](https://peps.python.org/pep-0479/) +/// - [Python documentation](https://docs.python.org/3/library/exceptions.html#StopIteration) +#[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.14.3")] +pub(crate) struct StopIterationReturn; + +impl Violation for StopIterationReturn { + #[derive_message_formats] + fn message(&self) -> String { + "Explicit `raise StopIteration` in generator".to_string() + } + + fn fix_title(&self) -> Option { + Some("Use `return` instead".to_string()) + } +} + +/// PLR1708 +pub(crate) fn stop_iteration_return(checker: &Checker, raise_stmt: &ast::StmtRaise) { + // Fast-path: only continue if this is `raise StopIteration` (with or without args) + let Some(exc) = &raise_stmt.exc else { + return; + }; + + let is_stop_iteration = match exc.as_ref() { + ast::Expr::Call(ast::ExprCall { func, .. }) => { + checker.semantic().match_builtin_expr(func, "StopIteration") + } + expr => checker.semantic().match_builtin_expr(expr, "StopIteration"), + }; + + if !is_stop_iteration { + return; + } + + // Now check the (more expensive) generator context + if !in_generator_context(checker) { + return; + } + + checker.report_diagnostic(StopIterationReturn, raise_stmt.range()); +} + +/// Returns true if we're inside a function that contains any `yield`/`yield from`. +fn in_generator_context(checker: &Checker) -> bool { + for scope in checker.semantic().current_scopes() { + if let ruff_python_semantic::ScopeKind::Function(function_def) = scope.kind { + if contains_yield_statement(&function_def.body) { + return true; + } + } + } + false +} + +/// Check if a statement list contains any yield statements +fn contains_yield_statement(body: &[ast::Stmt]) -> bool { + struct YieldFinder { + found: bool, + } + + impl Visitor<'_> for YieldFinder { + fn visit_expr(&mut self, expr: &ast::Expr) { + if matches!(expr, ast::Expr::Yield(_) | ast::Expr::YieldFrom(_)) { + self.found = true; + } else { + walk_expr(self, expr); + } + } + } + + let mut finder = YieldFinder { found: false }; + for stmt in body { + walk_stmt(&mut finder, stmt); + if finder.found { + return true; + } + } + false +} diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1708_stop_iteration_return.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1708_stop_iteration_return.py.snap new file mode 100644 index 0000000000..5773ff81ea --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1708_stop_iteration_return.py.snap @@ -0,0 +1,109 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +PLR1708 Explicit `raise StopIteration` in generator + --> stop_iteration_return.py:38:5 + | +36 | yield 1 +37 | yield 2 +38 | raise StopIteration # Should trigger + | ^^^^^^^^^^^^^^^^^^^ + | +help: Use `return` instead + +PLR1708 Explicit `raise StopIteration` in generator + --> stop_iteration_return.py:44:5 + | +42 | yield 1 +43 | yield 2 +44 | raise StopIteration("finished") # Should trigger + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Use `return` instead + +PLR1708 Explicit `raise StopIteration` in generator + --> stop_iteration_return.py:50:5 + | +48 | yield 1 +49 | yield 2 +50 | raise StopIteration(1 + 2) # Should trigger + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Use `return` instead + +PLR1708 Explicit `raise StopIteration` in generator + --> stop_iteration_return.py:56:5 + | +54 | yield 1 +55 | yield 2 +56 | raise StopIteration("async") # Should trigger + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Use `return` instead + +PLR1708 Explicit `raise StopIteration` in generator + --> stop_iteration_return.py:62:9 + | +60 | def inner_gen(): +61 | yield 1 +62 | raise StopIteration("inner") # Should trigger + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +63 | +64 | yield from inner_gen() + | +help: Use `return` instead + +PLR1708 Explicit `raise StopIteration` in generator + --> stop_iteration_return.py:71:13 + | +69 | def generator_method(self): +70 | yield 1 +71 | raise StopIteration("method") # Should trigger + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +72 | +73 | return MyClass + | +help: Use `return` instead + +PLR1708 Explicit `raise StopIteration` in generator + --> stop_iteration_return.py:81:9 + | +79 | yield 1 +80 | yield 2 +81 | raise StopIteration("complex") # Should trigger + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +82 | except ValueError: +83 | yield 3 + | +help: Use `return` instead + +PLR1708 Explicit `raise StopIteration` in generator + --> stop_iteration_return.py:91:9 + | +89 | yield 1 +90 | if condition: +91 | raise StopIteration("conditional") # Should trigger + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +92 | yield 2 + | +help: Use `return` instead + +PLR1708 Explicit `raise StopIteration` in generator + --> stop_iteration_return.py:98:5 + | +96 | def generator_with_bare_stop_iteration(): +97 | yield 1 +98 | raise StopIteration # Should trigger (no arguments) + | ^^^^^^^^^^^^^^^^^^^ + | +help: Use `return` instead + +PLR1708 Explicit `raise StopIteration` in generator + --> stop_iteration_return.py:105:13 + | +103 | yield i +104 | if i == 3: +105 | raise StopIteration("loop") # Should trigger + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Use `return` instead diff --git a/ruff.schema.json b/ruff.schema.json index 1917af7f6d..04ef3fcc3d 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3715,6 +3715,7 @@ "PLR170", "PLR1702", "PLR1704", + "PLR1708", "PLR171", "PLR1711", "PLR1714", From be5a62f7e509d56b7d60da6bfc4d1a2f60a67833 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 24 Oct 2025 09:06:19 +0200 Subject: [PATCH 028/188] [ty] Timeout based workspace diagnostic progress reports (#21019) --- .../api/requests/workspace_diagnostic.rs | 100 ++++++++++-------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs index 8618771bfd..c990d4f4af 100644 --- a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs +++ b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs @@ -23,8 +23,8 @@ use ruff_db::files::File; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::time::Instant; +use std::sync::Mutex; +use std::time::{Duration, Instant}; use ty_project::{Db, ProgressReporter}; /// Handler for [Workspace diagnostics](workspace-diagnostics) @@ -199,77 +199,64 @@ impl RetriableRequestHandler for WorkspaceDiagnosticRequestHandler { /// /// Diagnostics are only streamed if the client sends a partial result token. struct WorkspaceDiagnosticsProgressReporter<'a> { - total_files: usize, - checked_files: AtomicUsize, work_done: LazyWorkDoneProgress, - response: std::sync::Mutex>, + state: Mutex>, } impl<'a> WorkspaceDiagnosticsProgressReporter<'a> { fn new(work_done: LazyWorkDoneProgress, response: ResponseWriter<'a>) -> Self { Self { - total_files: 0, - checked_files: AtomicUsize::new(0), + state: Mutex::new(ProgressReporterState { + total_files: 0, + checked_files: 0, + last_response_sent: Instant::now(), + response, + }), work_done, - response: std::sync::Mutex::new(response), } } fn into_final_report(self) -> WorkspaceDiagnosticReportResult { - let writer = self.response.into_inner().unwrap(); - writer.into_final_report() - } - - fn report_progress(&self) { - let checked = self.checked_files.load(Ordering::Relaxed); - let total = self.total_files; - - #[allow(clippy::cast_possible_truncation)] - let percentage = if total > 0 { - Some((checked * 100 / total) as u32) - } else { - None - }; - - self.work_done - .report_progress(format!("{checked}/{total} files"), percentage); - - if checked == total { - self.work_done - .set_finish_message(format!("Checked {total} files")); - } + let state = self.state.into_inner().unwrap(); + state.response.into_final_report() } } impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> { fn set_files(&mut self, files: usize) { - self.total_files += files; - self.report_progress(); + let state = self.state.get_mut().unwrap(); + state.total_files += files; + state.report_progress(&self.work_done); } fn report_checked_file(&self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]) { - let checked = self.checked_files.fetch_add(1, Ordering::Relaxed) + 1; - - if checked.is_multiple_of(100) || checked == self.total_files { - // Report progress every 100 files or when all files are checked - self.report_progress(); - } - // Another thread might have panicked at this point because of a salsa cancellation which // poisoned the result. If the response is poisoned, just don't report and wait for our thread // to unwind with a salsa cancellation next. - let Ok(mut response) = self.response.lock() else { + let Ok(mut state) = self.state.lock() else { return; }; + state.checked_files += 1; + + if state.checked_files == state.total_files { + state.report_progress(&self.work_done); + } else if state.last_response_sent.elapsed() >= Duration::from_millis(50) { + state.last_response_sent = Instant::now(); + + state.report_progress(&self.work_done); + } + // Don't report empty diagnostics. We clear previous diagnostics in `into_response` // which also handles the case where a file no longer has diagnostics because // it's no longer part of the project. if !diagnostics.is_empty() { - response.write_diagnostics_for_file(db, file, diagnostics); + state + .response + .write_diagnostics_for_file(db, file, diagnostics); } - response.maybe_flush(); + state.response.maybe_flush(); } fn report_diagnostics(&mut self, db: &dyn Db, diagnostics: Vec) { @@ -286,7 +273,7 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> { } } - let response = self.response.get_mut().unwrap(); + let response = &mut self.state.get_mut().unwrap().response; for (file, diagnostics) in by_file { response.write_diagnostics_for_file(db, file, &diagnostics); @@ -295,6 +282,33 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> { } } +struct ProgressReporterState<'a> { + total_files: usize, + checked_files: usize, + last_response_sent: Instant, + response: ResponseWriter<'a>, +} + +impl ProgressReporterState<'_> { + fn report_progress(&self, work_done: &LazyWorkDoneProgress) { + let checked = self.checked_files; + let total = self.total_files; + + #[allow(clippy::cast_possible_truncation)] + let percentage = if total > 0 { + Some((checked * 100 / total) as u32) + } else { + None + }; + + work_done.report_progress(format!("{checked}/{total} files"), percentage); + + if checked == total { + work_done.set_finish_message(format!("Checked {total} files")); + } + } +} + #[derive(Debug)] struct ResponseWriter<'a> { mode: ReportingMode, From 4522f35ea7671676bb69c8b663b0aba72dea42f9 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 24 Oct 2025 09:48:57 +0200 Subject: [PATCH 029/188] [ty] Add comment explaining why `HasTrackedScope` is implemented for `Identifier` and why it works (#21057) --- crates/ty_python_semantic/src/semantic_model.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 6a71f7adfb..34efa22a02 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -505,8 +505,10 @@ impl HasTrackedScope for ast::Expr {} impl HasTrackedScope for ast::ExprRef<'_> {} impl HasTrackedScope for &ast::ExprRef<'_> {} -// See https://github.com/astral-sh/ty/issues/572 why this implementation exists -// even when we never register identifiers during semantic index building. +// We never explicitly register the scope of an `Identifier`. +// However, `ExpressionsScopeMap` stores the text ranges of each scope. +// That allows us to look up the identifier's scope for as long as it's +// inside an expression (because the ranges overlap). impl HasTrackedScope for ast::Identifier {} #[cfg(test)] From e196c2ab37907be298a528ae03ae9ba2912129f9 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 24 Oct 2025 10:29:55 +0100 Subject: [PATCH 030/188] [ty] Consider `__len__` when determining the truthiness of an instance of a tuple class or a `@final` class (#21049) --- crates/ty_ide/src/goto_definition.rs | 229 ++++++++++++++++-- .../resources/mdtest/expression/boolean.md | 52 +++- .../resources/mdtest/narrow/boolean.md | 11 + crates/ty_python_semantic/src/types.rs | 70 +++++- crates/ty_python_semantic/src/types/class.rs | 25 +- .../src/types/ide_support.rs | 27 ++- 6 files changed, 349 insertions(+), 65 deletions(-) diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index 6cc6d0c23d..6dc9e203b6 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -1293,6 +1293,156 @@ class Test: .source( "main.py", " +class Test: + def __invert__(self) -> 'Test': ... + +a = Test() + +~a +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __invert__(self) -> 'Test': ... + | ^^^^^^^^^^ + 4 | + 5 | a = Test() + | + info: Source + --> main.py:7:1 + | + 5 | a = Test() + 6 | + 7 | ~a + | ^ + | + "); + } + + /// We jump to the `__invert__` definition here even though its signature is incorrect. + #[test] + fn goto_definition_unary_operator_with_bad_dunder_definition() { + let test = CursorTest::builder() + .source( + "main.py", + " +class Test: + def __invert__(self, extra_arg) -> 'Test': ... + +a = Test() + +~a +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __invert__(self, extra_arg) -> 'Test': ... + | ^^^^^^^^^^ + 4 | + 5 | a = Test() + | + info: Source + --> main.py:7:1 + | + 5 | a = Test() + 6 | + 7 | ~a + | ^ + | + "); + } + + #[test] + fn goto_definition_unary_after_operator() { + let test = CursorTest::builder() + .source( + "main.py", + " +class Test: + def __invert__(self) -> 'Test': ... + +a = Test() + +~ a +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __invert__(self) -> 'Test': ... + | ^^^^^^^^^^ + 4 | + 5 | a = Test() + | + info: Source + --> main.py:7:1 + | + 5 | a = Test() + 6 | + 7 | ~ a + | ^ + | + "); + } + + #[test] + fn goto_definition_unary_between_operator_and_operand() { + let test = CursorTest::builder() + .source( + "main.py", + " +class Test: + def __invert__(self) -> 'Test': ... + +a = Test() + +-a +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Definition + --> main.py:5:1 + | + 3 | def __invert__(self) -> 'Test': ... + 4 | + 5 | a = Test() + | ^ + 6 | + 7 | -a + | + info: Source + --> main.py:7:2 + | + 5 | a = Test() + 6 | + 7 | -a + | ^ + | + "); + } + + #[test] + fn goto_definition_unary_not_with_dunder_bool() { + let test = CursorTest::builder() + .source( + "main.py", + " class Test: def __bool__(self) -> bool: ... @@ -1325,17 +1475,17 @@ a = Test() } #[test] - fn goto_definition_unary_after_operator() { + fn goto_definition_unary_not_with_dunder_len() { let test = CursorTest::builder() .source( "main.py", " class Test: - def __bool__(self) -> bool: ... + def __len__(self) -> 42: ... a = Test() -not a +not a ", ) .build(); @@ -1345,8 +1495,8 @@ not a --> main.py:3:9 | 2 | class Test: - 3 | def __bool__(self) -> bool: ... - | ^^^^^^^^ + 3 | def __len__(self) -> 42: ... + | ^^^^^^^ 4 | 5 | a = Test() | @@ -1361,40 +1511,83 @@ not a "); } + /// If `__bool__` is defined incorrectly, `not` does not fallback to `__len__`. + /// Instead, we jump to the `__bool__` definition as usual. + /// The fallback only occurs if `__bool__` is not defined at all. #[test] - fn goto_definition_unary_between_operator_and_operand() { + fn goto_definition_unary_not_with_bad_dunder_bool_and_dunder_len() { let test = CursorTest::builder() .source( "main.py", " class Test: - def __bool__(self) -> bool: ... + def __bool__(self, extra_arg) -> bool: ... + def __len__(self) -> 42: ... a = Test() --a +not a ", ) .build(); assert_snapshot!(test.goto_definition(), @r" info[goto-definition]: Definition - --> main.py:5:1 + --> main.py:3:9 | - 3 | def __bool__(self) -> bool: ... - 4 | - 5 | a = Test() - | ^ - 6 | - 7 | -a + 2 | class Test: + 3 | def __bool__(self, extra_arg) -> bool: ... + | ^^^^^^^^ + 4 | def __len__(self) -> 42: ... | info: Source - --> main.py:7:2 + --> main.py:8:1 + | + 6 | a = Test() + 7 | + 8 | not a + | ^^^ + | + "); + } + + /// Same as for unary operators that only use a single dunder, + /// we still jump to `__len__` for `not` goto-definition even if + /// the `__len__` signature is incorrect (but only if there is no + /// `__bool__` definition). + #[test] + fn goto_definition_unary_not_with_no_dunder_bool_and_bad_dunder_len() { + let test = CursorTest::builder() + .source( + "main.py", + " +class Test: + def __len__(self, extra_arg) -> 42: ... + +a = Test() + +not a +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Definition + --> main.py:3:9 + | + 2 | class Test: + 3 | def __len__(self, extra_arg) -> 42: ... + | ^^^^^^^ + 4 | + 5 | a = Test() + | + info: Source + --> main.py:7:1 | 5 | a = Test() 6 | - 7 | -a - | ^ + 7 | not a + | ^^^ | "); } diff --git a/crates/ty_python_semantic/resources/mdtest/expression/boolean.md b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md index 9af250a0a5..ec78b20fb7 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/boolean.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md @@ -78,7 +78,7 @@ python-version = "3.11" ``` ```py -from typing import Literal +from typing import Literal, final reveal_type(bool(1)) # revealed: Literal[True] reveal_type(bool((0,))) # revealed: Literal[True] @@ -92,15 +92,11 @@ reveal_type(bool(foo)) # revealed: Literal[True] class SingleElementTupleSubclass(tuple[int]): ... reveal_type(bool(SingleElementTupleSubclass((0,)))) # revealed: Literal[True] -reveal_type(SingleElementTupleSubclass.__bool__) # revealed: (self: tuple[int], /) -> Literal[True] -reveal_type(SingleElementTupleSubclass((1,)).__bool__) # revealed: () -> Literal[True] # Unknown length, but we know the length is guaranteed to be >=2 class MixedTupleSubclass(tuple[int, *tuple[str, ...], bytes]): ... reveal_type(bool(MixedTupleSubclass((1, b"foo")))) # revealed: Literal[True] -reveal_type(MixedTupleSubclass.__bool__) # revealed: (self: tuple[int, *tuple[str, ...], bytes], /) -> Literal[True] -reveal_type(MixedTupleSubclass((1, b"foo")).__bool__) # revealed: () -> Literal[True] # Unknown length with an overridden `__bool__`: class VariadicTupleSubclassWithDunderBoolOverride(tuple[int, ...]): @@ -108,10 +104,6 @@ class VariadicTupleSubclassWithDunderBoolOverride(tuple[int, ...]): return True reveal_type(bool(VariadicTupleSubclassWithDunderBoolOverride((1,)))) # revealed: Literal[True] -reveal_type(VariadicTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def __bool__(self) -> Literal[True] - -# revealed: bound method VariadicTupleSubclassWithDunderBoolOverride.__bool__() -> Literal[True] -reveal_type(VariadicTupleSubclassWithDunderBoolOverride().__bool__) # Same again but for a subclass of a fixed-length tuple: class EmptyTupleSubclassWithDunderBoolOverride(tuple[()]): @@ -124,11 +116,28 @@ reveal_type(EmptyTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def # revealed: bound method EmptyTupleSubclassWithDunderBoolOverride.__bool__() -> Literal[True] reveal_type(EmptyTupleSubclassWithDunderBoolOverride().__bool__) + +@final +class FinalClassOverridingLenAndNotBool: + def __len__(self) -> Literal[42]: + return 42 + +reveal_type(bool(FinalClassOverridingLenAndNotBool())) # revealed: Literal[True] + +@final +class FinalClassWithNoLenOrBool: ... + +reveal_type(bool(FinalClassWithNoLenOrBool())) # revealed: Literal[True] + +def f(x: SingleElementTupleSubclass | FinalClassOverridingLenAndNotBool | FinalClassWithNoLenOrBool): + reveal_type(bool(x)) # revealed: Literal[True] ``` ## Falsy values ```py +from typing import final, Literal + reveal_type(bool(0)) # revealed: Literal[False] reveal_type(bool(())) # revealed: Literal[False] reveal_type(bool(None)) # revealed: Literal[False] @@ -139,13 +148,23 @@ reveal_type(bool()) # revealed: Literal[False] class EmptyTupleSubclass(tuple[()]): ... reveal_type(bool(EmptyTupleSubclass())) # revealed: Literal[False] -reveal_type(EmptyTupleSubclass.__bool__) # revealed: (self: tuple[()], /) -> Literal[False] -reveal_type(EmptyTupleSubclass().__bool__) # revealed: () -> Literal[False] + +@final +class FinalClassOverridingLenAndNotBool: + def __len__(self) -> Literal[0]: + return 0 + +reveal_type(bool(FinalClassOverridingLenAndNotBool())) # revealed: Literal[False] + +def f(x: EmptyTupleSubclass | FinalClassOverridingLenAndNotBool): + reveal_type(bool(x)) # revealed: Literal[False] ``` ## Ambiguous values ```py +from typing import Literal + reveal_type(bool([])) # revealed: bool reveal_type(bool({})) # revealed: bool reveal_type(bool(set())) # revealed: bool @@ -154,8 +173,15 @@ class VariadicTupleSubclass(tuple[int, ...]): ... def f(x: tuple[int, ...], y: VariadicTupleSubclass): reveal_type(bool(x)) # revealed: bool - reveal_type(x.__bool__) # revealed: () -> bool - reveal_type(y.__bool__) # revealed: () -> bool + +class NonFinalOverridingLenAndNotBool: + def __len__(self) -> Literal[42]: + return 42 + +# We cannot consider `__len__` for a non-`@final` type, +# because a subclass might override `__bool__`, +# and `__bool__` takes precedence over `__len__` +reveal_type(bool(NonFinalOverridingLenAndNotBool())) # revealed: bool ``` ## `__bool__` returning `NoReturn` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/boolean.md b/crates/ty_python_semantic/resources/mdtest/narrow/boolean.md index 17f4454ff2..dd86762ee5 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/boolean.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/boolean.md @@ -21,6 +21,8 @@ def _(flag: bool): ## Narrowing in `and` ```py +from typing import final + def _(flag: bool): class A: ... x: A | None = A() if flag else None @@ -28,6 +30,15 @@ def _(flag: bool): isinstance(x, A) and reveal_type(x) # revealed: A x is None and reveal_type(x) # revealed: None reveal_type(x) # revealed: A | None + +@final +class FinalClass: ... + +# We know that no subclass of `FinalClass` can exist, +# therefore no subtype of `FinalClass` can define `__bool__` +# or `__len__`, therefore `FinalClass` can safely be considered +# always-truthy, therefore this always resolves to `None` +reveal_type(FinalClass() and None) # revealed: None ``` ## Multiple `and` arms diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 6cc7f20739..0ccb90b0cb 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4463,18 +4463,15 @@ impl<'db> Type<'db> { visitor: &TryBoolVisitor<'db>, ) -> Result> { let type_to_truthiness = |ty| { - if let Type::BooleanLiteral(bool_val) = ty { - Truthiness::from(bool_val) - } else { - Truthiness::Ambiguous + match ty { + Type::BooleanLiteral(bool_val) => Truthiness::from(bool_val), + Type::IntLiteral(int_val) => Truthiness::from(int_val != 0), + // anything else is handled lower down + _ => Truthiness::Ambiguous, } }; - let try_dunder_bool = || { - // We only check the `__bool__` method for truth testing, even though at - // runtime there is a fallback to `__len__`, since `__bool__` takes precedence - // and a subclass could add a `__bool__` method. - + let try_dunders = || { match self.try_call_dunder( db, "__bool__", @@ -4509,18 +4506,67 @@ impl<'db> Type<'db> { Ok(Truthiness::Ambiguous) } - Err(CallDunderError::MethodNotAvailable) => 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.into_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, @@ -4635,9 +4681,9 @@ impl<'db> Type<'db> { .known_class(db) .and_then(KnownClass::bool) .map(Ok) - .unwrap_or_else(try_dunder_bool)?, + .unwrap_or_else(try_dunders)?, - Type::ProtocolInstance(_) => try_dunder_bool()?, + Type::ProtocolInstance(_) => try_dunders()?, Type::Union(union) => try_union(*union)?, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index ae577549e2..940679ed17 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -745,17 +745,6 @@ impl<'db> ClassType<'db> { }) }; - let synthesize_simple_tuple_method = |return_type| { - let parameters = - Parameters::new([Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(Type::instance(db, self))]); - - let synthesized_dunder_method = - CallableType::function_like(db, Signature::new(parameters, Some(return_type))); - - Member::definitely_declared(synthesized_dunder_method) - }; - match name { "__len__" if class_literal.is_tuple(db) => { let return_type = specialization @@ -765,16 +754,14 @@ impl<'db> ClassType<'db> { .map(Type::IntLiteral) .unwrap_or_else(|| KnownClass::Int.to_instance(db)); - synthesize_simple_tuple_method(return_type) - } + let parameters = + Parameters::new([Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(Type::instance(db, self))]); - "__bool__" if class_literal.is_tuple(db) => { - let return_type = specialization - .and_then(|spec| spec.tuple(db)) - .map(|tuple| tuple.truthiness().into_type(db)) - .unwrap_or_else(|| KnownClass::Bool.to_instance(db)); + let synthesized_dunder_method = + CallableType::function_like(db, Signature::new(parameters, Some(return_type))); - synthesize_simple_tuple_method(return_type) + Member::definitely_declared(synthesized_dunder_method) } "__getitem__" if class_literal.is_tuple(db) => { diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 331d89a412..9dad158369 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -10,6 +10,7 @@ use crate::semantic_index::scope::ScopeId; use crate::semantic_index::{ attribute_scopes, global_scope, place_table, semantic_index, use_def_map, }; +use crate::types::CallDunderError; use crate::types::call::{CallArguments, MatchedArgument}; use crate::types::signatures::Signature; use crate::types::{ @@ -973,13 +974,33 @@ pub fn definitions_for_unary_op<'db>( ast::UnaryOp::Not => "__bool__", }; - let Ok(bindings) = operand_ty.try_call_dunder( + let bindings = match operand_ty.try_call_dunder( db, unary_dunder_method, CallArguments::none(), TypeContext::default(), - ) else { - return None; + ) { + Ok(bindings) => bindings, + Err(CallDunderError::MethodNotAvailable) if unary_op.op == ast::UnaryOp::Not => { + // The runtime falls back to `__len__` for `not` if `__bool__` is not defined. + match operand_ty.try_call_dunder( + db, + "__len__", + CallArguments::none(), + TypeContext::default(), + ) { + Ok(bindings) => bindings, + Err(CallDunderError::MethodNotAvailable) => return None, + Err( + CallDunderError::PossiblyUnbound(bindings) + | CallDunderError::CallError(_, bindings), + ) => *bindings, + } + } + Err(CallDunderError::MethodNotAvailable) => return None, + Err( + CallDunderError::PossiblyUnbound(bindings) | CallDunderError::CallError(_, bindings), + ) => *bindings, }; let callable_type = promote_literals_for_self(db, bindings.callable_type()); From bf74c824ebc446223b44d0268acb140ccf4714d7 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 24 Oct 2025 14:34:16 +0100 Subject: [PATCH 031/188] [ty] Delegate truthiness inference of an enum `Literal` type to its enum-instance supertype (#21060) --- .../resources/mdtest/expression/boolean.md | 31 +++++++++++++++++-- .../mdtest/type_properties/truthiness.md | 10 +++--- crates/ty_python_semantic/src/types.rs | 8 ++--- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/expression/boolean.md b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md index ec78b20fb7..413acf8e39 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/boolean.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md @@ -78,6 +78,7 @@ python-version = "3.11" ``` ```py +import enum from typing import Literal, final reveal_type(bool(1)) # revealed: Literal[True] @@ -129,13 +130,20 @@ class FinalClassWithNoLenOrBool: ... reveal_type(bool(FinalClassWithNoLenOrBool())) # revealed: Literal[True] -def f(x: SingleElementTupleSubclass | FinalClassOverridingLenAndNotBool | FinalClassWithNoLenOrBool): +class EnumWithMembers(enum.Enum): + A = 1 + B = 2 + +reveal_type(bool(EnumWithMembers.A)) # revealed: Literal[True] + +def f(x: SingleElementTupleSubclass | FinalClassOverridingLenAndNotBool | FinalClassWithNoLenOrBool | Literal[EnumWithMembers.A]): reveal_type(bool(x)) # revealed: Literal[True] ``` ## Falsy values ```py +import enum from typing import final, Literal reveal_type(bool(0)) # revealed: Literal[False] @@ -156,13 +164,23 @@ class FinalClassOverridingLenAndNotBool: reveal_type(bool(FinalClassOverridingLenAndNotBool())) # revealed: Literal[False] -def f(x: EmptyTupleSubclass | FinalClassOverridingLenAndNotBool): +class EnumWithMembersOverridingBool(enum.Enum): + A = 1 + B = 2 + + def __bool__(self) -> Literal[False]: + return False + +reveal_type(bool(EnumWithMembersOverridingBool.A)) # revealed: Literal[False] + +def f(x: EmptyTupleSubclass | FinalClassOverridingLenAndNotBool | Literal[EnumWithMembersOverridingBool.A]): reveal_type(bool(x)) # revealed: Literal[False] ``` ## Ambiguous values ```py +import enum from typing import Literal reveal_type(bool([])) # revealed: bool @@ -182,6 +200,15 @@ class NonFinalOverridingLenAndNotBool: # because a subclass might override `__bool__`, # and `__bool__` takes precedence over `__len__` reveal_type(bool(NonFinalOverridingLenAndNotBool())) # revealed: bool + +class EnumWithMembersOverridingBool(enum.Enum): + A = 1 + B = 2 + + def __bool__(self) -> bool: + return False + +reveal_type(bool(EnumWithMembersOverridingBool.A)) # revealed: bool ``` ## `__bool__` returning `NoReturn` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md index 310b59602b..fc58c77482 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md @@ -183,13 +183,11 @@ class CustomLenEnum(Enum): def __len__(self): return 0 -# TODO: these could be `Literal[True]` -reveal_type(bool(NormalEnum.NO)) # revealed: bool -reveal_type(bool(NormalEnum.YES)) # revealed: bool +reveal_type(bool(NormalEnum.NO)) # revealed: Literal[True] +reveal_type(bool(NormalEnum.YES)) # revealed: Literal[True] -# TODO: these could be `Literal[False]` -reveal_type(bool(FalsyEnum.NO)) # revealed: bool -reveal_type(bool(FalsyEnum.YES)) # revealed: bool +reveal_type(bool(FalsyEnum.NO)) # revealed: Literal[False] +reveal_type(bool(FalsyEnum.YES)) # revealed: Literal[False] # All of the following must be `bool`: diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 0ccb90b0cb..d64473ddb6 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4692,10 +4692,10 @@ impl<'db> Type<'db> { Truthiness::Ambiguous } - Type::EnumLiteral(_) => { - // We currently make no attempt to infer the precise truthiness, but it's not impossible to do so. - // Note that custom `__bool__` or `__len__` methods on the class or superclasses affect the outcome. - Truthiness::Ambiguous + Type::EnumLiteral(enum_type) => { + enum_type + .enum_class_instance(db) + .try_bool_impl(db, allow_short_circuit, visitor)? } Type::IntLiteral(num) => Truthiness::from(*num != 0), From 75766692979bbe97d8a3fafa6f3fcd02c9c10bea Mon Sep 17 00:00:00 2001 From: Dan Parizher <105245560+danparizher@users.noreply.github.com> Date: Fri, 24 Oct 2025 09:40:26 -0400 Subject: [PATCH 032/188] [`flake8-pyi`] Fix PYI034 to not trigger on metaclasses (`PYI034`) (#20881) ## Summary Fixes #20781 --------- Co-authored-by: Alex Waygood Co-authored-by: Brent Westbrook Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .../test/fixtures/flake8_pyi/PYI034.py | 26 +++++ .../test/fixtures/flake8_pyi/PYI034.pyi | 25 +++++ .../flake8_pyi/rules/non_self_return_type.rs | 30 +++++- ...__flake8_pyi__tests__PYI034_PYI034.py.snap | 3 + ..._flake8_pyi__tests__PYI034_PYI034.pyi.snap | 5 + .../ruff_python_semantic/src/analyze/class.rs | 97 ++++++++++++++++++- 6 files changed, 184 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py index 52a612a79e..b35efc3c21 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py @@ -359,3 +359,29 @@ class Generic5(list[PotentialTypeVar]): def __new__(cls: type[Generic5]) -> Generic5: ... def __enter__(self: Generic5) -> Generic5: ... + +# Test cases based on issue #20781 - metaclasses that triggers IsMetaclass::Maybe +class MetaclassInWhichSelfCannotBeUsed5(type(Protocol)): + def __new__( + cls, name: str, bases: tuple[type[Any], ...], attrs: dict[str, Any], **kwargs: Any + ) -> MetaclassInWhichSelfCannotBeUsed5: + new_class = super().__new__(cls, name, bases, attrs, **kwargs) + return new_class + + +import django.db.models.base + + +class MetaclassInWhichSelfCannotBeUsed6(django.db.models.base.ModelBase): + def __new__(cls, name: str, bases: tuple[Any, ...], attrs: dict[str, Any], **kwargs: Any) -> MetaclassInWhichSelfCannotBeUsed6: + ... + + +class MetaclassInWhichSelfCannotBeUsed7(django.db.models.base.ModelBase): + def __new__(cls, /, name: str, bases: tuple[object, ...], attrs: dict[str, object], **kwds: object) -> MetaclassInWhichSelfCannotBeUsed7: + ... + + +class MetaclassInWhichSelfCannotBeUsed8(django.db.models.base.ModelBase): + def __new__(cls, name: builtins.str, bases: tuple, attributes: dict, /, **kw) -> MetaclassInWhichSelfCannotBeUsed8: + ... diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi index 9567b343ac..b22f76798b 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi @@ -252,3 +252,28 @@ from some_module import PotentialTypeVar class Generic5(list[PotentialTypeVar]): def __new__(cls: type[Generic5]) -> Generic5: ... def __enter__(self: Generic5) -> Generic5: ... + + +# Test case based on issue #20781 - metaclass that triggers IsMetaclass::Maybe +class MetaclassInWhichSelfCannotBeUsed5(type(Protocol)): + def __new__( + cls, name: str, bases: tuple[type[Any], ...], attrs: dict[str, Any], **kwargs: Any + ) -> MetaclassInWhichSelfCannotBeUsed5: ... + + +import django.db.models.base + + +class MetaclassInWhichSelfCannotBeUsed6(django.db.models.base.ModelBase): + def __new__(cls, name: str, bases: tuple[Any, ...], attrs: dict[str, Any], **kwargs: Any) -> MetaclassInWhichSelfCannotBeUsed6: + ... + + +class MetaclassInWhichSelfCannotBeUsed7(django.db.models.base.ModelBase): + def __new__(cls, /, name: str, bases: tuple[object, ...], attrs: dict[str, object], **kwds: object) -> MetaclassInWhichSelfCannotBeUsed7: + ... + + +class MetaclassInWhichSelfCannotBeUsed8(django.db.models.base.ModelBase): + def __new__(cls, name: builtins.str, bases: tuple, attributes: dict, /, **kw) -> MetaclassInWhichSelfCannotBeUsed8: + ... diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs index 59d01ff834..d3b2f75b2d 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -50,6 +50,29 @@ use ruff_text_size::Ranged; /// 1. `__aiter__` methods that return `AsyncIterator`, despite the class /// inheriting directly from `AsyncIterator`. /// +/// The rule attempts to avoid flagging methods on metaclasses, since +/// [PEP 673] specifies that `Self` is disallowed in metaclasses. Ruff can +/// detect a class as being a metaclass if it inherits from a stdlib +/// metaclass such as `builtins.type` or `abc.ABCMeta`, and additionally +/// infers that a class may be a metaclass if it has a `__new__` method +/// with a similar signature to `type.__new__`. The heuristic used to +/// identify a metaclass-like `__new__` method signature is that it: +/// +/// 1. Has exactly 5 parameters (including `cls`) +/// 1. Has a second parameter annotated with `str` +/// 1. Has a third parameter annotated with a `tuple` type +/// 1. Has a fourth parameter annotated with a `dict` type +/// 1. Has a fifth parameter is keyword-variadic (`**kwargs`) +/// +/// For example, the following class would be detected as a metaclass, disabling +/// the rule: +/// +/// ```python +/// class MyMetaclass(django.db.models.base.ModelBase): +/// def __new__(cls, name: str, bases: tuple[Any, ...], attrs: dict[str, Any], **kwargs: Any) -> MyMetaclass: +/// ... +/// ``` +/// /// ## Example /// /// ```pyi @@ -87,6 +110,8 @@ use ruff_text_size::Ranged; /// /// ## References /// - [Python documentation: `typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) +/// +/// [PEP 673]: https://peps.python.org/pep-0673/#valid-locations-for-self #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.271")] pub(crate) struct NonSelfReturnType { @@ -143,7 +168,10 @@ pub(crate) fn non_self_return_type( }; // PEP 673 forbids the use of `typing(_extensions).Self` in metaclasses. - if analyze::class::is_metaclass(class_def, semantic).is_yes() { + if !matches!( + analyze::class::is_metaclass(class_def, semantic), + analyze::class::IsMetaclass::No + ) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap index fd92d1055d..8893a346e1 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap @@ -451,6 +451,7 @@ help: Use `Self` as return type 359 + def __new__(cls) -> typing.Self: ... 360 | def __enter__(self: Generic5) -> Generic5: ... 361 | +362 | note: This is an unsafe fix and may change runtime behavior PYI034 [*] `__enter__` methods in classes like `Generic5` usually return `self` at runtime @@ -468,4 +469,6 @@ help: Use `Self` as return type - def __enter__(self: Generic5) -> Generic5: ... 360 + def __enter__(self) -> typing.Self: ... 361 | +362 | +363 | # Test cases based on issue #20781 - metaclasses that triggers IsMetaclass::Maybe note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.pyi.snap index 2018bb04f2..29c91b9a77 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.pyi.snap @@ -431,6 +431,8 @@ help: Use `Self` as return type - def __new__(cls: type[Generic5]) -> Generic5: ... 253 + def __new__(cls) -> typing.Self: ... 254 | def __enter__(self: Generic5) -> Generic5: ... +255 | +256 | note: This is a display-only fix and is likely to be incorrect PYI034 [*] `__enter__` methods in classes like `Generic5` usually return `self` at runtime @@ -447,4 +449,7 @@ help: Use `Self` as return type 253 | def __new__(cls: type[Generic5]) -> Generic5: ... - def __enter__(self: Generic5) -> Generic5: ... 254 + def __enter__(self) -> typing.Self: ... +255 | +256 | +257 | # Test case based on issue #20781 - metaclass that triggers IsMetaclass::Maybe note: This is a display-only fix and is likely to be incorrect diff --git a/crates/ruff_python_semantic/src/analyze/class.rs b/crates/ruff_python_semantic/src/analyze/class.rs index 0cb3a8a7f6..14f6b2c983 100644 --- a/crates/ruff_python_semantic/src/analyze/class.rs +++ b/crates/ruff_python_semantic/src/analyze/class.rs @@ -317,6 +317,91 @@ impl IsMetaclass { } } +/// Check if a class has a metaclass-like `__new__` method signature. +/// +/// A metaclass-like `__new__` method signature has: +/// 1. Exactly 5 parameters (including cls) +/// 2. Second parameter annotated with `str` +/// 3. Third parameter annotated with a `tuple` type +/// 4. Fourth parameter annotated with a `dict` type +/// 5. Fifth parameter is keyword-variadic (`**kwargs`) +/// +/// For example: +/// +/// ```python +/// class MyMetaclass(django.db.models.base.ModelBase): +/// def __new__(cls, name: str, bases: tuple[Any, ...], attrs: dict[str, Any], **kwargs: Any) -> MyMetaclass: +/// ... +/// ``` +fn has_metaclass_new_signature(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { + // Look for a __new__ method in the class body + for stmt in &class_def.body { + let ast::Stmt::FunctionDef(ast::StmtFunctionDef { + name, parameters, .. + }) = stmt + else { + continue; + }; + + if name != "__new__" { + continue; + } + + // Check if we have exactly 5 parameters (cls + 4 others) + if parameters.len() != 5 { + continue; + } + + // Check that there is no variadic parameter + if parameters.vararg.is_some() { + continue; + } + + // Check that the last parameter is keyword-variadic (**kwargs) + if parameters.kwarg.is_none() { + continue; + } + + // Check parameter annotations, skipping the first parameter (cls) + let mut param_iter = parameters.iter().skip(1); + + // Check second parameter (name: str) + let Some(second_param) = param_iter.next() else { + continue; + }; + if !second_param + .annotation() + .is_some_and(|annotation| semantic.match_builtin_expr(map_subscript(annotation), "str")) + { + continue; + } + + // Check third parameter (bases: tuple[...]) + let Some(third_param) = param_iter.next() else { + continue; + }; + if !third_param.annotation().is_some_and(|annotation| { + semantic.match_builtin_expr(map_subscript(annotation), "tuple") + }) { + continue; + } + + // Check fourth parameter (attrs: dict[...]) + let Some(fourth_param) = param_iter.next() else { + continue; + }; + if !fourth_param.annotation().is_some_and(|annotation| { + semantic.match_builtin_expr(map_subscript(annotation), "dict") + }) { + continue; + } + + return true; + } + + false +} + /// Returns `IsMetaclass::Yes` if the given class is definitely a metaclass, /// `IsMetaclass::No` if it's definitely *not* a metaclass, and /// `IsMetaclass::Maybe` otherwise. @@ -349,7 +434,17 @@ pub fn is_metaclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> match (is_base_class, maybe) { (true, true) => IsMetaclass::Maybe, (true, false) => IsMetaclass::Yes, - (false, _) => IsMetaclass::No, + (false, _) => { + // If it has >1 base class and a metaclass-like signature for `__new__`, + // then it might be a metaclass. + if class_def.bases().is_empty() { + IsMetaclass::No + } else if has_metaclass_new_signature(class_def, semantic) { + IsMetaclass::Maybe + } else { + IsMetaclass::No + } + } } } From 3e8685d2ec1f84f8aa017e10fbc9c5395400c71b Mon Sep 17 00:00:00 2001 From: Dan Parizher <105245560+danparizher@users.noreply.github.com> Date: Fri, 24 Oct 2025 10:07:19 -0400 Subject: [PATCH 033/188] [`pyflakes`] Fix false positive for `__class__` in lambda expressions within class definitions (`F821`) (#20564) ## Summary Fixes #20562 --------- Co-authored-by: Brent Westbrook --- .../test/fixtures/pyflakes/F821_33.py | 21 +++++++++++++ crates/ruff_linter/src/checkers/ast/mod.rs | 30 ++++++++++++++++++- crates/ruff_linter/src/rules/pyflakes/mod.rs | 1 + ...les__pyflakes__tests__F821_F821_33.py.snap | 12 ++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F821_33.py create mode 100644 crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_33.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_33.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_33.py new file mode 100644 index 0000000000..04a3726cf6 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_33.py @@ -0,0 +1,21 @@ +class C: + f = lambda self: __class__ + + +print(C().f().__name__) + +# Test: nested lambda +class D: + g = lambda self: (lambda: __class__) + + +print(D().g()().__name__) + +# Test: lambda outside class (should still fail) +h = lambda: __class__ + +# Test: lambda referencing module-level variable (should not be flagged as F821) +import uuid + +class E: + uuid = lambda: str(uuid.uuid4()) diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 7750d29f34..2321cfbb7c 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -2116,7 +2116,7 @@ impl<'a> Visitor<'a> for Checker<'a> { | Expr::DictComp(_) | Expr::SetComp(_) => { self.analyze.scopes.push(self.semantic.scope_id); - self.semantic.pop_scope(); + self.semantic.pop_scope(); // Lambda/Generator/Comprehension scope } _ => {} } @@ -3041,7 +3041,35 @@ impl<'a> Checker<'a> { if let Some(parameters) = parameters { self.visit_parameters(parameters); } + + // Here we add the implicit scope surrounding a lambda which allows code in the + // lambda to access `__class__` at runtime when the lambda is defined within a class. + // See the `ScopeKind::DunderClassCell` docs for more information. + let added_dunder_class_scope = if self + .semantic + .current_scopes() + .any(|scope| scope.kind.is_class()) + { + self.semantic.push_scope(ScopeKind::DunderClassCell); + let binding_id = self.semantic.push_binding( + TextRange::default(), + BindingKind::DunderClassCell, + BindingFlags::empty(), + ); + self.semantic + .current_scope_mut() + .add("__class__", binding_id); + true + } else { + false + }; + self.visit_expr(body); + + // Pop the DunderClassCell scope if it was added + if added_dunder_class_scope { + self.semantic.pop_scope(); + } } } self.semantic.restore(snapshot); diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 3dd7edde65..5f41a19f2a 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -166,6 +166,7 @@ mod tests { #[test_case(Rule::UndefinedName, Path::new("F821_30.py"))] #[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::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_33.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_33.py.snap new file mode 100644 index 0000000000..04e1cd30dd --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_33.py.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +--- +F821 Undefined name `__class__` + --> F821_33.py:15:13 + | +14 | # Test: lambda outside class (should still fail) +15 | h = lambda: __class__ + | ^^^^^^^^^ +16 | +17 | # Test: lambda referencing module-level variable (should not be flagged as F821) + | From 6f0982d2d6694ad9fbe553ec41135e51e14e3682 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 24 Oct 2025 10:58:23 -0400 Subject: [PATCH 034/188] chore: bump zizmor (#21064) --- .github/zizmor.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 2dc7f7dba3..237af95e7b 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,5 +1,5 @@ # Configuration for the zizmor static analysis tool, run via pre-commit in CI -# https://woodruffw.github.io/zizmor/configuration/ +# https://docs.zizmor.sh/configuration/ # # TODO: can we remove the ignores here so that our workflows are more secure? rules: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 130aaa554f..5913919d67 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -102,7 +102,7 @@ repos: # zizmor detects security vulnerabilities in GitHub Actions workflows. # Additional configuration for the tool is found in `.github/zizmor.yml` - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.15.2 + rev: v1.16.0 hooks: - id: zizmor From f36fa7d6c1ee2e18016f388263bc894e81dde3f5 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 24 Oct 2025 17:35:23 +0200 Subject: [PATCH 035/188] [ty] Fix missing newline before first diagnostic (#21058) --- crates/ty/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs index e1bb4e1f10..66a85870b2 100644 --- a/crates/ty/src/lib.rs +++ b/crates/ty/src/lib.rs @@ -280,7 +280,7 @@ impl MainLoop { match salsa::Cancelled::catch(|| { db.check_with_reporter(&mut reporter); - reporter.bar.finish(); + reporter.bar.finish_and_clear(); reporter.collector.into_sorted(&db) }) { Ok(result) => { From a2d0d398539d873cc9612d6cd8eb560cef7f55a6 Mon Sep 17 00:00:00 2001 From: Shahar Naveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Fri, 24 Oct 2025 18:44:48 +0300 Subject: [PATCH 036/188] Configurable "unparse mode" for `ruff_python_codegen::Generator` (#21041) Co-authored-by: Micha Reiser --- crates/ruff_python_codegen/src/generator.rs | 118 +++++++++++++------- crates/ruff_python_codegen/src/lib.rs | 2 +- 2 files changed, 77 insertions(+), 43 deletions(-) diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs index af5b1dc80e..710e295f62 100644 --- a/crates/ruff_python_codegen/src/generator.rs +++ b/crates/ruff_python_codegen/src/generator.rs @@ -17,6 +17,7 @@ use ruff_source_file::LineEnding; use super::stylist::{Indentation, Stylist}; mod precedence { + pub(crate) const MIN: u8 = 0; pub(crate) const NAMED_EXPR: u8 = 1; pub(crate) const ASSIGN: u8 = 3; pub(crate) const ANN_ASSIGN: u8 = 5; @@ -63,13 +64,36 @@ mod precedence { pub(crate) const MAX: u8 = 63; } +#[derive(Default)] +pub enum Mode { + /// Ruff's default unparsing behaviour. + #[default] + Default, + /// Emits same output as [`ast.unparse`](https://docs.python.org/3/library/ast.html#ast.unparse). + AstUnparse, +} + +impl Mode { + /// Quote style to use. + /// + /// - [`Default`](`Mode::Default`): Output of `[AnyStringFlags.quote_style`]. + /// - [`AstUnparse`](`Mode::AstUnparse`): Always return [`Quote::Single`]. + #[must_use] + fn quote_style(&self, flags: impl StringFlags) -> Quote { + match self { + Self::Default => flags.quote_style(), + Self::AstUnparse => Quote::Single, + } + } +} + pub struct Generator<'a> { /// The indentation style to use. indent: &'a Indentation, /// The line ending to use. line_ending: LineEnding, - /// Preferred quote style to use. For more info see [`Generator::with_preferred_quote`]. - preferred_quote: Option, + /// Unparsed code style. See [`Mode`] for more info. + mode: Mode, buffer: String, indent_depth: usize, num_newlines: usize, @@ -81,7 +105,7 @@ impl<'a> From<&'a Stylist<'a>> for Generator<'a> { Self { indent: stylist.indentation(), line_ending: stylist.line_ending(), - preferred_quote: None, + mode: Mode::default(), buffer: String::new(), indent_depth: 0, num_newlines: 0, @@ -96,7 +120,7 @@ impl<'a> Generator<'a> { // Style preferences. indent, line_ending, - preferred_quote: None, + mode: Mode::Default, // Internal state. buffer: String::new(), indent_depth: 0, @@ -105,13 +129,10 @@ impl<'a> Generator<'a> { } } - /// Set a preferred quote style for generated source code. - /// - /// - If [`None`], the generator will attempt to preserve the existing quote style whenever possible. - /// - If [`Some`], the generator will prefer the specified quote style, ignoring the one found in the source. + /// Sets the mode for code unparsing. #[must_use] - pub fn with_preferred_quote(mut self, quote: Option) -> Self { - self.preferred_quote = quote; + pub fn with_mode(mut self, mode: Mode) -> Self { + self.mode = mode; self } @@ -173,7 +194,7 @@ impl<'a> Generator<'a> { return; } } - let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style()); + let quote_style = self.mode.quote_style(flags); let escape = AsciiEscape::with_preferred_quote(s, quote_style); if let Some(len) = escape.layout().len { self.buffer.reserve(len); @@ -193,7 +214,7 @@ impl<'a> Generator<'a> { } self.p(flags.prefix().as_str()); - let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style()); + let quote_style = self.mode.quote_style(flags); let escape = UnicodeEscape::with_preferred_quote(s, quote_style); if let Some(len) = escape.layout().len { self.buffer.reserve(len); @@ -1303,7 +1324,11 @@ impl<'a> Generator<'a> { if tuple.is_empty() { self.p("()"); } else { - group_if!(precedence::TUPLE, { + let lvl = match self.mode { + Mode::Default => precedence::TUPLE, + Mode::AstUnparse => precedence::MIN, + }; + group_if!(lvl, { let mut first = true; for item in tuple { self.p_delim(&mut first, ", "); @@ -1525,7 +1550,7 @@ impl<'a> Generator<'a> { return; } - let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style()); + let quote_style = self.mode.quote_style(flags); let escape = UnicodeEscape::with_preferred_quote(&s, quote_style); if let Some(len) = escape.layout().len { self.buffer.reserve(len); @@ -1552,8 +1577,8 @@ impl<'a> Generator<'a> { ) { self.p(flags.prefix().as_str()); - let flags = - flags.with_quote_style(self.preferred_quote.unwrap_or_else(|| flags.quote_style())); + let quote_style = self.mode.quote_style(flags); + let flags = flags.with_quote_style(quote_style); self.p(flags.quote_str()); self.unparse_interpolated_string_body(values, flags); self.p(flags.quote_str()); @@ -1586,14 +1611,13 @@ impl<'a> Generator<'a> { #[cfg(test)] mod tests { - use ruff_python_ast::str::Quote; use ruff_python_ast::{Mod, ModModule}; use ruff_python_parser::{self, Mode, ParseOptions, parse_module}; use ruff_source_file::LineEnding; use crate::stylist::Indentation; - use super::Generator; + use super::{Generator, Mode as UnparseMode}; fn round_trip(contents: &str) -> String { let indentation = Indentation::default(); @@ -1605,16 +1629,15 @@ mod tests { } /// Like [`round_trip`] but configure the [`Generator`] with the requested - /// `indentation`, `line_ending` and `preferred_quote` settings. + /// `indentation`, `line_ending` and `unparse_mode` settings. fn round_trip_with( indentation: &Indentation, line_ending: LineEnding, - preferred_quote: Option, + unparse_mode: UnparseMode, contents: &str, ) -> String { let module = parse_module(contents).unwrap(); - let mut generator = - Generator::new(indentation, line_ending).with_preferred_quote(preferred_quote); + let mut generator = Generator::new(indentation, line_ending).with_mode(unparse_mode); generator.unparse_suite(module.suite()); generator.generate() } @@ -1814,6 +1837,7 @@ except* Exception as e: type Y = str" ); assert_eq!(round_trip(r"x = (1, 2, 3)"), r"x = 1, 2, 3"); + assert_eq!(round_trip(r"x = (1, (2, 3))"), r"x = 1, (2, 3)"); assert_eq!(round_trip(r"-(1) + ~(2) + +(3)"), r"-1 + ~2 + +3"); assert_round_trip!( r"def f(): @@ -2000,7 +2024,7 @@ if True: round_trip_with( &Indentation::new(" ".to_string()), LineEnding::default(), - None, + UnparseMode::Default, r" if True: pass @@ -2018,7 +2042,7 @@ if True: round_trip_with( &Indentation::new(" ".to_string()), LineEnding::default(), - None, + UnparseMode::Default, r" if True: pass @@ -2036,7 +2060,7 @@ if True: round_trip_with( &Indentation::new("\t".to_string()), LineEnding::default(), - None, + UnparseMode::Default, r" if True: pass @@ -2058,7 +2082,7 @@ if True: round_trip_with( &Indentation::default(), LineEnding::Lf, - None, + UnparseMode::Default, "if True:\n print(42)", ), "if True:\n print(42)", @@ -2068,7 +2092,7 @@ if True: round_trip_with( &Indentation::default(), LineEnding::CrLf, - None, + UnparseMode::Default, "if True:\n print(42)", ), "if True:\r\n print(42)", @@ -2078,30 +2102,40 @@ if True: round_trip_with( &Indentation::default(), LineEnding::Cr, - None, + UnparseMode::Default, "if True:\n print(42)", ), "if True:\r print(42)", ); } - #[test_case::test_case(r#""'hello'""#, r#""'hello'""#, Quote::Single ; "basic str ignored")] - #[test_case::test_case(r#"b"'hello'""#, r#"b"'hello'""#, Quote::Single ; "basic bytes ignored")] - #[test_case::test_case("'hello'", r#""hello""#, Quote::Double ; "basic str double")] - #[test_case::test_case(r#""hello""#, "'hello'", Quote::Single ; "basic str single")] - #[test_case::test_case("b'hello'", r#"b"hello""#, Quote::Double ; "basic bytes double")] - #[test_case::test_case(r#"b"hello""#, "b'hello'", Quote::Single ; "basic bytes single")] - #[test_case::test_case(r#""hello""#, r#""hello""#, Quote::Double ; "remain str double")] - #[test_case::test_case("'hello'", "'hello'", Quote::Single ; "remain str single")] - #[test_case::test_case("x: list['str']", r#"x: list["str"]"#, Quote::Double ; "type ann double")] - #[test_case::test_case(r#"x: list["str"]"#, "x: list['str']", Quote::Single ; "type ann single")] - #[test_case::test_case("f'hello'", r#"f"hello""#, Quote::Double ; "basic fstring double")] - #[test_case::test_case(r#"f"hello""#, "f'hello'", Quote::Single ; "basic fstring single")] - fn preferred_quote(inp: &str, out: &str, quote: Quote) { + #[test_case::test_case(r#""'hello'""#, r#""'hello'""# ; "basic str ignored")] + #[test_case::test_case(r#"b"'hello'""#, r#"b"'hello'""# ; "basic bytes ignored")] + #[test_case::test_case(r#""hello""#, "'hello'" ; "basic str single")] + #[test_case::test_case(r#"b"hello""#, "b'hello'" ; "basic bytes single")] + #[test_case::test_case("'hello'", "'hello'" ; "remain str single")] + #[test_case::test_case(r#"x: list["str"]"#, "x: list['str']" ; "type ann single")] + #[test_case::test_case(r#"f"hello""#, "f'hello'" ; "basic fstring single")] + fn ast_unparse_quote(inp: &str, out: &str) { let got = round_trip_with( &Indentation::default(), LineEnding::default(), - Some(quote), + UnparseMode::AstUnparse, + inp, + ); + assert_eq!(got, out); + } + + #[test_case::test_case("a,", "(a,)" ; "basic single")] + #[test_case::test_case("a, b", "(a, b)" ; "basic multi")] + #[test_case::test_case("x = a,", "x = (a,)" ; "basic assign single")] + #[test_case::test_case("x = a, b", "x = (a, b)" ; "basic assign multi")] + #[test_case::test_case("a, (b, c)", "(a, (b, c))" ; "nested")] + fn ast_tuple_parentheses(inp: &str, out: &str) { + let got = round_trip_with( + &Indentation::default(), + LineEnding::default(), + UnparseMode::AstUnparse, inp, ); assert_eq!(got, out); diff --git a/crates/ruff_python_codegen/src/lib.rs b/crates/ruff_python_codegen/src/lib.rs index aeeab4a747..c30b35d932 100644 --- a/crates/ruff_python_codegen/src/lib.rs +++ b/crates/ruff_python_codegen/src/lib.rs @@ -1,4 +1,4 @@ -pub use generator::Generator; +pub use generator::{Generator, Mode}; use ruff_python_parser::{ParseError, parse_module}; pub use stylist::{Indentation, Stylist}; From eb8c0ad87c4e10e2d66ada5e4bb7f436214ca9a8 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 24 Oct 2025 18:00:00 +0200 Subject: [PATCH 037/188] [ty] Add `--no-progress` option (#21063) --- crates/ty/docs/cli.md | 2 ++ crates/ty/src/args.rs | 20 ++++++++++++++++---- crates/ty/src/lib.rs | 2 +- crates/ty/src/printer.rs | 5 ++--- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/ty/docs/cli.md b/crates/ty/docs/cli.md index 4c44f523d7..97a9a1c2d9 100644 --- a/crates/ty/docs/cli.md +++ b/crates/ty/docs/cli.md @@ -58,6 +58,8 @@ over all configuration files.

This is an advanced option that should usually only be used for first-party or third-party modules that are not installed into your Python environment in a conventional way. Use --python to point ty to your Python environment if it is in an unusual location.

--help, -h

Print help (see a summary with '-h')

--ignore rule

Disables the rule. Can be specified multiple times.

+
--no-progress

Hide all progress outputs.

+

For example, spinners or progress bars.

--output-format output-format

The format to use for printing diagnostic messages

Possible values:

    diff --git a/crates/ty/src/args.rs b/crates/ty/src/args.rs index f1be98ea46..f6a52a3c8c 100644 --- a/crates/ty/src/args.rs +++ b/crates/ty/src/args.rs @@ -34,6 +34,7 @@ pub(crate) enum Command { } #[derive(Debug, Parser)] +#[expect(clippy::struct_excessive_bools)] pub(crate) struct CheckCommand { /// List of files or directories to check. #[clap( @@ -117,10 +118,6 @@ pub(crate) struct CheckCommand { #[arg(long)] pub(crate) output_format: Option, - /// Control when colored output is used. - #[arg(long, value_name = "WHEN")] - pub(crate) color: Option, - /// Use exit code 1 if there are any warning-level diagnostics. #[arg(long, conflicts_with = "exit_zero", default_missing_value = "true", num_args=0..1)] pub(crate) error_on_warning: Option, @@ -152,6 +149,21 @@ pub(crate) struct CheckCommand { /// Supports patterns like `tests/`, `*.tmp`, `**/__pycache__/**`. #[arg(long, help_heading = "File selection")] exclude: Option>, + + /// Control when colored output is used. + #[arg( + long, + value_name = "WHEN", + help_heading = "Global options", + display_order = 1000 + )] + pub(crate) color: Option, + + /// Hide all progress outputs. + /// + /// For example, spinners or progress bars. + #[arg(global = true, long, value_parser = clap::builder::BoolishValueParser::new(), help_heading = "Global options")] + pub no_progress: bool, } impl CheckCommand { diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs index 66a85870b2..2b28329f3f 100644 --- a/crates/ty/src/lib.rs +++ b/crates/ty/src/lib.rs @@ -71,7 +71,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result { let verbosity = args.verbosity.level(); let _guard = setup_tracing(verbosity, args.color.unwrap_or_default())?; - let printer = Printer::default().with_verbosity(verbosity); + let printer = Printer::new(verbosity, args.no_progress); tracing::debug!("Version: {}", version::version()); diff --git a/crates/ty/src/printer.rs b/crates/ty/src/printer.rs index 94c5286d35..1c86095c21 100644 --- a/crates/ty/src/printer.rs +++ b/crates/ty/src/printer.rs @@ -12,11 +12,10 @@ pub(crate) struct Printer { } impl Printer { - #[must_use] - pub(crate) fn with_verbosity(self, verbosity: VerbosityLevel) -> Self { + pub(crate) fn new(verbosity: VerbosityLevel, no_progress: bool) -> Self { Self { verbosity, - no_progress: self.no_progress, + no_progress, } } From adbf05802a391de7fa86bd29da77fe48a86814f3 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 24 Oct 2025 18:30:54 +0200 Subject: [PATCH 038/188] [ty] Fix rare panic with highly cyclic `TypeVar` definitions (#21059) --- Cargo.lock | 7 +- Cargo.toml | 2 +- .../1377_iteration_count_mismatch.md | 149 ++++++++++++++++++ crates/ty_test/Cargo.toml | 3 +- crates/ty_test/README.md | 14 ++ crates/ty_test/src/lib.rs | 98 +++++++++--- crates/ty_test/src/parser.rs | 114 +++++++++----- fuzz/Cargo.toml | 2 +- 8 files changed, 318 insertions(+), 71 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/regression/1377_iteration_count_mismatch.md diff --git a/Cargo.lock b/Cargo.lock index 8a6afb3be2..11897e189c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3563,7 +3563,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" version = "0.24.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161" +source = "git+https://github.com/salsa-rs/salsa.git?rev=25b3ef146cfa2615f4ec82760bd0c22b454d0a12#25b3ef146cfa2615f4ec82760bd0c22b454d0a12" dependencies = [ "boxcar", "compact_str", @@ -3587,12 +3587,12 @@ dependencies = [ [[package]] name = "salsa-macro-rules" version = "0.24.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161" +source = "git+https://github.com/salsa-rs/salsa.git?rev=25b3ef146cfa2615f4ec82760bd0c22b454d0a12#25b3ef146cfa2615f4ec82760bd0c22b454d0a12" [[package]] name = "salsa-macros" version = "0.24.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161" +source = "git+https://github.com/salsa-rs/salsa.git?rev=25b3ef146cfa2615f4ec82760bd0c22b454d0a12#25b3ef146cfa2615f4ec82760bd0c22b454d0a12" dependencies = [ "proc-macro2", "quote", @@ -4521,7 +4521,6 @@ name = "ty_test" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.9.4", "camino", "colored 3.0.0", "insta", diff --git a/Cargo.toml b/Cargo.toml index 22f4a5cbfd..301efbd180 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" } rustc-hash = { version = "2.0.0" } rustc-stable-hash = { version = "0.1.2" } # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d38145c29574758de7ffbe8a13cd4584c3b09161", default-features = false, features = [ +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "25b3ef146cfa2615f4ec82760bd0c22b454d0a12", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", diff --git a/crates/ty_python_semantic/resources/mdtest/regression/1377_iteration_count_mismatch.md b/crates/ty_python_semantic/resources/mdtest/regression/1377_iteration_count_mismatch.md new file mode 100644 index 0000000000..600e408331 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/regression/1377_iteration_count_mismatch.md @@ -0,0 +1,149 @@ +# Iteration count mismatch for highly cyclic type vars + +Regression test for . + +The code is an excerpt from that is minimal enough to +trigger the iteration count mismatch bug in Salsa. + + + +```toml +[environment] +extra-paths= ["/packages"] +``` + +`main.py`: + +```py +from __future__ import annotations + +from typing import TypeAlias + +from steam.message import Message + +TestAlias: TypeAlias = tuple[Message] +``` + +`/packages/steam/__init__.py`: + +```py + +``` + +`/packages/steam/abc.py`: + +```py +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Generic, Protocol + +from typing_extensions import TypeVar + +if TYPE_CHECKING: + from .clan import Clan + from .group import Group + +UserT = TypeVar("UserT", covariant=True) +MessageT = TypeVar("MessageT", bound="Message", default="Message", covariant=True) + +class Messageable(Protocol[MessageT]): ... + +ClanT = TypeVar("ClanT", bound="Clan | None", default="Clan | None", covariant=True) +GroupT = TypeVar("GroupT", bound="Group | None", default="Group | None", covariant=True) + +class Channel(Messageable[MessageT], Generic[MessageT, ClanT, GroupT]): ... + +ChannelT = TypeVar("ChannelT", bound=Channel, default=Channel, covariant=True) + +class Message(Generic[UserT, ChannelT]): ... +``` + +`/packages/steam/chat.py`: + +```py +from __future__ import annotations + +from typing import TYPE_CHECKING, Generic, TypeAlias + +from typing_extensions import Self, TypeVar + +from .abc import Channel, ClanT, GroupT, Message + +if TYPE_CHECKING: + from .clan import Clan + from .message import ClanMessage, GroupMessage + +ChatT = TypeVar("ChatT", bound="Chat", default="Chat", covariant=True) +MemberT = TypeVar("MemberT", covariant=True) + +AuthorT = TypeVar("AuthorT", covariant=True) + +class ChatMessage(Message[AuthorT, ChatT], Generic[AuthorT, MemberT, ChatT]): ... + +ChatMessageT = TypeVar("ChatMessageT", bound="GroupMessage | ClanMessage", default="GroupMessage | ClanMessage", covariant=True) + +class Chat(Channel[ChatMessageT, ClanT, GroupT]): ... + +ChatGroupTypeT = TypeVar("ChatGroupTypeT", covariant=True) + +class ChatGroup(Generic[MemberT, ChatT, ChatGroupTypeT]): ... +``` + +`/packages/steam/channel.py`: + +```py +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from .chat import Chat + +if TYPE_CHECKING: + from .clan import Clan + +class ClanChannel(Chat["Clan", None]): ... +``` + +`/packages/steam/clan.py`: + +```py +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +from typing_extensions import Self + +from .chat import ChatGroup + +class Clan(ChatGroup[str], str): ... +``` + +`/packages/steam/group.py`: + +```py +from __future__ import annotations + +from .chat import ChatGroup + +class Group(ChatGroup[str]): ... +``` + +`/packages/steam/message.py`: + +```py +from __future__ import annotations + +from typing import TYPE_CHECKING + +from typing_extensions import TypeVar + +from .abc import BaseUser, Message +from .chat import ChatMessage + +if TYPE_CHECKING: + from .channel import ClanChannel + +class GroupMessage(ChatMessage["str"]): ... +class ClanMessage(ChatMessage["ClanChannel"]): ... +``` diff --git a/crates/ty_test/Cargo.toml b/crates/ty_test/Cargo.toml index 670c46cc91..271ca8f084 100644 --- a/crates/ty_test/Cargo.toml +++ b/crates/ty_test/Cargo.toml @@ -23,12 +23,11 @@ ty_static = { workspace = true } ty_vendored = { workspace = true } anyhow = { workspace = true } -bitflags = { workspace = true } camino = { workspace = true } colored = { workspace = true } insta = { workspace = true, features = ["filters"] } memchr = { workspace = true } -path-slash ={ workspace = true } +path-slash = { workspace = true } regex = { workspace = true } rustc-hash = { workspace = true } rustc-stable-hash = { workspace = true } diff --git a/crates/ty_test/README.md b/crates/ty_test/README.md index 3db1832231..ecf4614d94 100644 --- a/crates/ty_test/README.md +++ b/crates/ty_test/README.md @@ -180,6 +180,20 @@ snapshotting. At present, there is no way to do inline snapshotting or to request more granular snapshotting of specific diagnostics. +## Expected panics + +It is possible to write tests that expect the type checker to panic during checking. Ideally, we'd fix those panics +but being able to add regression tests even before is useful. + +To mark a test as expecting a panic, add an HTML comment like this: + +```markdown + +``` + +The text after `expect-panic:` is a substring that must appear in the panic message. The message is optional; +but it is recommended to avoid false positives. + ## Multi-file tests Some tests require multiple files, with imports from one file into another. For this purpose, diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index f66eaaf26a..9460992312 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -8,7 +8,7 @@ use parser as test_parser; use ruff_db::Db as _; use ruff_db::diagnostic::{Diagnostic, DiagnosticId, DisplayDiagnosticConfig}; use ruff_db::files::{File, FileRootKind, system_path_to_file}; -use ruff_db::panic::catch_unwind; +use ruff_db::panic::{PanicError, catch_unwind}; use ruff_db::parsed::parsed_module; use ruff_db::system::{DbWithWritableSystem as _, SystemPath, SystemPathBuf}; use ruff_db::testing::{setup_logging, setup_logging_with_filter}; @@ -319,6 +319,7 @@ fn run_test( let mut snapshot_diagnostics = vec![]; let mut any_pull_types_failures = false; + let mut panic_info = None; let mut failures: Failures = test_files .iter() @@ -338,10 +339,17 @@ fn run_test( .map(|error| Diagnostic::invalid_syntax(test_file.file, error, error)), ); - let mdtest_result = attempt_test(db, check_types, test_file, "run mdtest", None); + let mdtest_result = attempt_test(db, check_types, test_file); let type_diagnostics = match mdtest_result { Ok(diagnostics) => diagnostics, - Err(failures) => return Some(failures), + Err(failures) => { + if test.should_expect_panic().is_ok() { + panic_info = Some(failures.info); + return None; + } + + return Some(failures.into_file_failures(db, "run mdtest", None)); + } }; diagnostics.extend(type_diagnostics); @@ -367,22 +375,20 @@ fn run_test( })); } - let pull_types_result = attempt_test( - db, - pull_types, - test_file, - "\"pull types\"", - Some( - "Note: either fix the panic or add the `` \ - directive to this test", - ), - ); + let pull_types_result = attempt_test(db, pull_types, test_file); match pull_types_result { Ok(()) => {} Err(failures) => { any_pull_types_failures = true; if !test.should_skip_pulling_types() { - return Some(failures); + return Some(failures.into_file_failures( + db, + "\"pull types\"", + Some( + "Note: either fix the panic or add the `` \ + directive to this test", + ), + )); } } } @@ -391,6 +397,39 @@ fn run_test( }) .collect(); + match panic_info { + Some(panic_info) => { + let expected_message = test + .should_expect_panic() + .expect("panic_info is only set when `should_expect_panic` is `Ok`"); + + let message = panic_info + .payload + .as_str() + .unwrap_or("Box") + .to_string(); + + if let Some(expected_message) = expected_message { + assert!( + message.contains(expected_message), + "Test `{}` is expected to panic with `{expected_message}`, but panicked with `{message}` instead.", + test.name() + ); + } + } + None => { + if let Ok(message) = test.should_expect_panic() { + if let Some(message) = message { + panic!( + "Test `{}` is expected to panic with `{message}`, but it didn't.", + test.name() + ); + } + panic!("Test `{}` is expected to panic but it didn't.", test.name()); + } + } + } + if test.should_skip_pulling_types() && !any_pull_types_failures { let mut by_line = matcher::FailuresByLine::default(); by_line.push( @@ -596,17 +635,32 @@ fn create_diagnostic_snapshot( /// /// If a panic occurs, a nicely formatted [`FileFailures`] is returned as an `Err()` variant. /// This will be formatted into a diagnostic message by `ty_test`. -fn attempt_test<'db, T, F>( +fn attempt_test<'db, 'a, T, F>( db: &'db Db, test_fn: F, - test_file: &TestFile, - action: &str, - clarification: Option<&str>, -) -> Result + test_file: &'a TestFile, +) -> Result> where F: FnOnce(&'db dyn ty_python_semantic::Db, File) -> T + std::panic::UnwindSafe, { - catch_unwind(|| test_fn(db, test_file.file)).map_err(|info| { + catch_unwind(|| test_fn(db, test_file.file)) + .map_err(|info| AttemptTestError { info, test_file }) +} + +struct AttemptTestError<'a> { + info: PanicError, + test_file: &'a TestFile, +} + +impl AttemptTestError<'_> { + fn into_file_failures( + self, + db: &Db, + action: &str, + clarification: Option<&str>, + ) -> FileFailures { + let info = self.info; + let mut by_line = matcher::FailuresByLine::default(); let mut messages = vec![]; match info.location { @@ -652,8 +706,8 @@ where by_line.push(OneIndexed::from_zero_indexed(0), messages); FileFailures { - backtick_offsets: test_file.backtick_offsets.clone(), + backtick_offsets: self.test_file.backtick_offsets.clone(), by_line, } - }) + } } diff --git a/crates/ty_test/src/parser.rs b/crates/ty_test/src/parser.rs index 12ad3b8039..bc039eece6 100644 --- a/crates/ty_test/src/parser.rs +++ b/crates/ty_test/src/parser.rs @@ -9,6 +9,7 @@ use anyhow::bail; use ruff_db::system::{SystemPath, SystemPathBuf}; use rustc_hash::FxHashMap; +use crate::config::MarkdownTestConfig; use ruff_index::{IndexVec, newtype_index}; use ruff_python_ast::PySourceType; use ruff_python_trivia::Cursor; @@ -16,8 +17,6 @@ use ruff_source_file::{LineIndex, LineRanges, OneIndexed}; use ruff_text_size::{TextLen, TextRange, TextSize}; use rustc_stable_hash::{FromStableHash, SipHasher128Hash, StableSipHasher128}; -use crate::config::MarkdownTestConfig; - /// Parse the Markdown `source` as a test suite with given `title`. pub(crate) fn parse<'s>(title: &'s str, source: &'s str) -> anyhow::Result> { let parser = Parser::new(title, source); @@ -145,13 +144,17 @@ impl<'m, 's> MarkdownTest<'m, 's> { pub(super) fn should_snapshot_diagnostics(&self) -> bool { self.section .directives - .contains(MdtestDirectives::SNAPSHOT_DIAGNOSTICS) + .has_directive_set(MdtestDirective::SnapshotDiagnostics) + } + + pub(super) fn should_expect_panic(&self) -> Result, ()> { + self.section.directives.get(MdtestDirective::ExpectPanic) } pub(super) fn should_skip_pulling_types(&self) -> bool { self.section .directives - .contains(MdtestDirectives::PULL_TYPES_SKIP) + .has_directive_set(MdtestDirective::PullTypesSkip) } } @@ -495,6 +498,7 @@ impl<'s> Parser<'s> { fn parse_impl(&mut self) -> anyhow::Result<()> { const SECTION_CONFIG_SNAPSHOT: &str = "snapshot-diagnostics"; const SECTION_CONFIG_PULLTYPES: &str = "pull-types:skip"; + const SECTION_CONFIG_EXPECT_PANIC: &str = "expect-panic"; const HTML_COMMENT_ALLOWLIST: &[&str] = &["blacken-docs:on", "blacken-docs:off"]; const CODE_BLOCK_END: &[u8] = b"```"; const HTML_COMMENT_END: &[u8] = b"-->"; @@ -506,16 +510,47 @@ impl<'s> Parser<'s> { memchr::memmem::find(self.cursor.as_bytes(), HTML_COMMENT_END) { let html_comment = self.cursor.as_str()[..position].trim(); - if html_comment == SECTION_CONFIG_SNAPSHOT { - self.process_mdtest_directive(MdtestDirective::SnapshotDiagnostics)?; - } else if html_comment == SECTION_CONFIG_PULLTYPES { - self.process_mdtest_directive(MdtestDirective::PullTypesSkip)?; - } else if !HTML_COMMENT_ALLOWLIST.contains(&html_comment) { - bail!( - "Unknown HTML comment `{html_comment}` -- possibly a typo? \ + let (directive, value) = match html_comment.split_once(':') { + Some((directive, value)) => { + (directive.trim(), Some(value.trim().to_string())) + } + None => (html_comment, None), + }; + + match directive { + SECTION_CONFIG_SNAPSHOT => { + anyhow::ensure!( + value.is_none(), + "The `{SECTION_CONFIG_SNAPSHOT}` directive does not take a value." + ); + self.process_mdtest_directive( + MdtestDirective::SnapshotDiagnostics, + None, + )?; + } + SECTION_CONFIG_PULLTYPES => { + anyhow::ensure!( + value.is_none(), + "The `{SECTION_CONFIG_PULLTYPES}` directive does not take a value." + ); + self.process_mdtest_directive( + MdtestDirective::PullTypesSkip, + None, + )?; + } + SECTION_CONFIG_EXPECT_PANIC => { + self.process_mdtest_directive(MdtestDirective::ExpectPanic, value)?; + } + _ => { + if !HTML_COMMENT_ALLOWLIST.contains(&html_comment) { + bail!( + "Unknown HTML comment `{html_comment}` -- possibly a typo? \ (Add to `HTML_COMMENT_ALLOWLIST` if this is a false positive)" - ); + ); + } + } } + self.cursor.skip_bytes(position + HTML_COMMENT_END.len()); } else { bail!("Unterminated HTML comment."); @@ -646,7 +681,7 @@ impl<'s> Parser<'s> { level: header_level.try_into()?, parent_id: Some(parent), config: self.sections[parent].config.clone(), - directives: self.sections[parent].directives, + directives: self.sections[parent].directives.clone(), }; if !self.current_section_files.is_empty() { @@ -793,7 +828,11 @@ impl<'s> Parser<'s> { Ok(()) } - fn process_mdtest_directive(&mut self, directive: MdtestDirective) -> anyhow::Result<()> { + fn process_mdtest_directive( + &mut self, + directive: MdtestDirective, + value: Option, + ) -> anyhow::Result<()> { if self.current_section_has_config { bail!( "Section config to enable {directive} must come before \ @@ -814,7 +853,7 @@ impl<'s> Parser<'s> { at most once.", ); } - current_section.directives.add_directive(directive); + current_section.directives.add_directive(directive, value); Ok(()) } @@ -833,12 +872,14 @@ impl<'s> Parser<'s> { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] enum MdtestDirective { /// A directive to enable snapshotting diagnostics. SnapshotDiagnostics, /// A directive to skip pull types. PullTypesSkip, + + ExpectPanic, } impl std::fmt::Display for MdtestDirective { @@ -846,40 +887,31 @@ impl std::fmt::Display for MdtestDirective { match self { MdtestDirective::SnapshotDiagnostics => f.write_str("snapshotting diagnostics"), MdtestDirective::PullTypesSkip => f.write_str("skipping the pull-types visitor"), + MdtestDirective::ExpectPanic => f.write_str("expect test to panic"), } } } -bitflags::bitflags! { - /// Directives that can be applied to a Markdown test section. - #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] - pub(crate) struct MdtestDirectives: u8 { - /// We should snapshot diagnostics for this section. - const SNAPSHOT_DIAGNOSTICS = 1 << 0; - /// We should skip pulling types for this section. - const PULL_TYPES_SKIP = 1 << 1; - } +/// The directives applied to a Markdown test section. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub(crate) struct MdtestDirectives { + directives: FxHashMap>, } impl MdtestDirectives { - const fn has_directive_set(self, directive: MdtestDirective) -> bool { - match directive { - MdtestDirective::SnapshotDiagnostics => { - self.contains(MdtestDirectives::SNAPSHOT_DIAGNOSTICS) - } - MdtestDirective::PullTypesSkip => self.contains(MdtestDirectives::PULL_TYPES_SKIP), - } + fn has_directive_set(&self, directive: MdtestDirective) -> bool { + self.directives.contains_key(&directive) } - fn add_directive(&mut self, directive: MdtestDirective) { - match directive { - MdtestDirective::SnapshotDiagnostics => { - self.insert(MdtestDirectives::SNAPSHOT_DIAGNOSTICS); - } - MdtestDirective::PullTypesSkip => { - self.insert(MdtestDirectives::PULL_TYPES_SKIP); - } - } + fn get(&self, directive: MdtestDirective) -> Result, ()> { + self.directives + .get(&directive) + .map(|s| s.as_deref()) + .ok_or(()) + } + + fn add_directive(&mut self, directive: MdtestDirective, value: Option) { + self.directives.insert(directive, value); } } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 24034b4854..82c9ae3be6 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -30,7 +30,7 @@ ty_python_semantic = { path = "../crates/ty_python_semantic" } ty_vendored = { path = "../crates/ty_vendored" } libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d38145c29574758de7ffbe8a13cd4584c3b09161", default-features = false, features = [ +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "25b3ef146cfa2615f4ec82760bd0c22b454d0a12", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", From f17ddd62ad191c2c164727ec3f30e485715d6cc6 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Fri, 24 Oct 2025 13:21:39 -0400 Subject: [PATCH 039/188] [ty] Avoid duplicate diagnostics during multi-inference of standalone expressions (#21056) ## Summary Resolves https://github.com/astral-sh/ty/issues/1428. --- .../resources/mdtest/call/union.md | 17 +++++++++++++++++ crates/ty_python_semantic/src/types/context.rs | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md index c950f7f482..1a4079204d 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/union.md +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md @@ -284,6 +284,8 @@ def _(flag: bool): Diagnostics unrelated to the type-context are only reported once: +`expression.py`: + ```py def f[T](x: T) -> list[T]: return [x] @@ -307,3 +309,18 @@ def _(x: int): # error: [possibly-unresolved-reference] "Name `z` used when possibly not defined" y(f(True), [z]) ``` + +`standalone_expression.py`: + +```py +def f(_: str): ... +def g(_: str): ... +def _(a: object, b: object, flag: bool): + if flag: + x = f + else: + x = g + + # error: [unsupported-operator] "Operator `>` is not supported for types `object` and `object`" + x(f"{'a' if a > b else 'b'}") +``` diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 2221ced32d..95e5ce8741 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -97,7 +97,9 @@ impl<'db, 'ast> InferContext<'db, 'ast> { } pub(crate) fn extend(&mut self, other: &TypeCheckDiagnostics) { - self.diagnostics.get_mut().extend(other); + if !self.is_in_multi_inference() { + self.diagnostics.get_mut().extend(other); + } } pub(super) fn is_lint_enabled(&self, lint: &'static LintMetadata) -> bool { From c3de8847d59ebac73f192a1415e283b278e30bce Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 24 Oct 2025 13:37:56 -0400 Subject: [PATCH 040/188] [ty] Consider domain of BDD when checking whether always satisfiable (#21050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That PR title might be a bit inscrutable. Consider the two constraints `T ≤ bool` and `T ≤ int`. Since `bool ≤ int`, by transitivity `T ≤ bool` implies `T ≤ int`. (Every type that is a subtype of `bool` is necessarily also a subtype of `int`.) That means that `T ≤ bool ∧ T ≰ int` is an impossible combination of constraints, and is therefore not a valid input to any BDD. We say that that assignment is not in the _domain_ of the BDD. The implication `T ≤ bool → T ≤ int` can be rewritten as `T ≰ bool ∨ T ≤ int`. (That's the definition of implication.) If we construct that constraint set in an mdtest, we should get a constraint set that is always satisfiable. Previously, that constraint set would correctly _display_ as `always`, but a `static_assert` on it would fail. The underlying cause is that our `is_always_satisfied` method would only test if the BDD was the `AlwaysTrue` terminal node. `T ≰ bool ∨ T ≤ int` does not simplify that far, because we purposefully keep around those constraints in the BDD structure so that it's easier to compare against other BDDs that reference those constraints. To fix this, we need a more nuanced definition of "always satisfied". Instead of evaluating to `true` for _every_ input, we only need it to evaluate to `true` for every _valid_ input — that is, every input in its domain. --- .../mdtest/type_properties/constraints.md | 35 +++++ crates/ty_python_semantic/src/types.rs | 35 ++--- .../ty_python_semantic/src/types/call/bind.rs | 10 +- crates/ty_python_semantic/src/types/class.rs | 2 +- .../src/types/constraints.rs | 131 +++++++++++++----- .../ty_python_semantic/src/types/generics.rs | 12 +- .../src/types/infer/builder.rs | 17 ++- .../ty_python_semantic/src/types/instance.rs | 4 +- .../src/types/signatures.rs | 4 +- crates/ty_python_semantic/src/types/tuple.rs | 18 ++- .../ty_extensions/ty_extensions.pyi | 2 + 11 files changed, 194 insertions(+), 76 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md index 607501a349..65b59a55b4 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md @@ -604,6 +604,8 @@ def _[T, U]() -> None: ## Other simplifications +### Displaying constraint sets + When displaying a constraint set, we transform the internal BDD representation into a DNF formula (i.e., the logical OR of several clauses, each of which is the logical AND of several constraints). This section contains several examples that show that we simplify the DNF formula as much as we can @@ -626,11 +628,44 @@ def f[T, U](): reveal_type((t1 | t2) & (u1 | u2)) ``` +We might simplify a BDD so much that we can no longer see the constraints that we used to construct +it! + +```py +from typing import Never +from ty_extensions import static_assert + +def f[T](): + t_int = range_constraint(Never, T, int) + t_bool = range_constraint(Never, T, bool) + + # `T ≤ bool` implies `T ≤ int`: if a type satisfies the former, it must always satisfy the + # latter. We can turn that into a constraint set, using the equivalence `p → q == ¬p ∨ q`: + implication = ~t_bool | t_int + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(implication) + static_assert(implication) + + # However, because of that implication, some inputs aren't valid: it's not possible for + # `T ≤ bool` to be true and `T ≤ int` to be false. This is reflected in the constraint set's + # "domain", which maps valid inputs to `true` and invalid inputs to `false`. This means that two + # constraint sets that are both always satisfied will not be identical if they have different + # domains! + always = range_constraint(Never, T, object) + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(always) + static_assert(always) + static_assert(implication != always) +``` + +### Normalized bounds + The lower and upper bounds of a constraint are normalized, so that we equate unions and intersections whose elements appear in different orders. ```py from typing import Never +from ty_extensions import range_constraint def f[T](): # revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)] diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index d64473ddb6..0d2d7ac866 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1234,7 +1234,7 @@ impl<'db> Type<'db> { self.filter_union(db, |elem| { !elem .when_disjoint_from(db, target, inferable) - .is_always_satisfied() + .is_always_satisfied(db) }) } @@ -1524,7 +1524,7 @@ 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) - .is_always_satisfied() + .is_always_satisfied(db) } fn when_subtype_of( @@ -1541,7 +1541,7 @@ impl<'db> Type<'db> { /// See [`TypeRelation::Assignability`] for more details. pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { self.when_assignable_to(db, target, InferableTypeVars::None) - .is_always_satisfied() + .is_always_satisfied(db) } fn when_assignable_to( @@ -1559,7 +1559,7 @@ impl<'db> Type<'db> { #[salsa::tracked(cycle_initial=is_redundant_with_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn is_redundant_with(self, db: &'db dyn Db, other: Type<'db>) -> bool { self.has_relation_to(db, other, InferableTypeVars::None, TypeRelation::Redundancy) - .is_always_satisfied() + .is_always_satisfied(db) } fn has_relation_to( @@ -1782,7 +1782,7 @@ impl<'db> Type<'db> { ) }) }) - .is_never_satisfied() => + .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, @@ -1925,7 +1925,7 @@ impl<'db> Type<'db> { relation_visitor, disjointness_visitor, ) - .is_never_satisfied() + .is_never_satisfied(db) }) => { // TODO: record the unification constraints @@ -2405,7 +2405,7 @@ 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() + .is_always_satisfied(db) } fn when_equivalent_to( @@ -2528,7 +2528,7 @@ impl<'db> Type<'db> { /// `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) - .is_always_satisfied() + .is_always_satisfied(db) } fn when_disjoint_from( @@ -4631,7 +4631,7 @@ impl<'db> Type<'db> { Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked_set)) => { let constraints = tracked_set.constraints(db); - Truthiness::from(constraints.is_always_satisfied()) + Truthiness::from(constraints.is_always_satisfied(db)) } Type::FunctionLiteral(_) @@ -7450,7 +7450,6 @@ impl<'db> TypeMapping<'_, 'db> { #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] #[derive(PartialOrd, Ord)] pub struct TrackedConstraintSet<'db> { - #[returns(ref)] constraints: ConstraintSet<'db>, } @@ -7646,17 +7645,11 @@ impl<'db> KnownInstanceType<'db> { } KnownInstanceType::ConstraintSet(tracked_set) => { let constraints = tracked_set.constraints(self.db); - if constraints.is_always_satisfied() { - f.write_str("ty_extensions.ConstraintSet[always]") - } else if constraints.is_never_satisfied() { - f.write_str("ty_extensions.ConstraintSet[never]") - } else { - write!( - f, - "ty_extensions.ConstraintSet[{}]", - constraints.display(self.db) - ) - } + write!( + f, + "ty_extensions.ConstraintSet[{}]", + constraints.display(self.db) + ) } } } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index ef1d8574cb..e6121da055 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1474,7 +1474,7 @@ impl<'db> CallableBinding<'db> { .unwrap_or(Type::unknown()); if argument_type .when_assignable_to(db, parameter_type, overload.inferable_typevars) - .is_always_satisfied() + .is_always_satisfied(db) { is_argument_assignable_to_any_overload = true; break 'overload; @@ -1707,7 +1707,7 @@ impl<'db> CallableBinding<'db> { current_parameter_type, overload.inferable_typevars, ) - .is_always_satisfied() + .is_always_satisfied(db) { participating_parameter_indexes.insert(parameter_index); } @@ -1830,7 +1830,7 @@ impl<'db> CallableBinding<'db> { first_overload_return_type, overload.inferable_typevars, ) - .is_always_satisfied() + .is_always_satisfied(db) }) } else { // No matching overload @@ -2705,7 +2705,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // building them in an earlier separate step. if argument_type .when_assignable_to(self.db, expected_ty, self.inferable_typevars) - .is_never_satisfied() + .is_never_satisfied(self.db) { let positional = matches!(argument, Argument::Positional | Argument::Synthetic) && !parameter.is_variadic(); @@ -2839,7 +2839,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { KnownClass::Str.to_instance(self.db), self.inferable_typevars, ) - .is_always_satisfied() + .is_always_satisfied(self.db) { self.errors.push(BindingError::InvalidKeyType { argument_index: adjusted_argument_index, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 940679ed17..61c531d17b 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -499,7 +499,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 { self.when_subclass_of(db, other, InferableTypeVars::None) - .is_always_satisfied() + .is_always_satisfied(db) } pub(super) fn when_subclass_of( diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 832aa71ab2..417399203c 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -98,7 +98,7 @@ pub(crate) trait IteratorConstraintsExtension { /// Returns the constraints under which any element of the iterator holds. /// /// This method short-circuits; if we encounter any element that - /// [`is_always_satisfied`][ConstraintSet::is_always_satisfied] true, then the overall result + /// [`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>( self, @@ -109,7 +109,7 @@ pub(crate) trait IteratorConstraintsExtension { /// 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] true, then the overall result + /// [`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>( self, @@ -129,7 +129,7 @@ where ) -> ConstraintSet<'db> { let mut result = ConstraintSet::never(); for child in self { - if result.union(db, f(child)).is_always_satisfied() { + if result.union(db, f(child)).is_always_satisfied(db) { return result; } } @@ -143,7 +143,7 @@ where ) -> ConstraintSet<'db> { let mut result = ConstraintSet::always(); for child in self { - if result.intersect(db, f(child)).is_never_satisfied() { + if result.intersect(db, f(child)).is_never_satisfied(db) { return result; } } @@ -176,13 +176,13 @@ impl<'db> ConstraintSet<'db> { } /// Returns whether this constraint set never holds - pub(crate) fn is_never_satisfied(self) -> bool { + pub(crate) fn is_never_satisfied(self, _db: &'db dyn Db) -> bool { self.node.is_never_satisfied() } /// Returns whether this constraint set always holds - pub(crate) fn is_always_satisfied(self) -> bool { - self.node.is_always_satisfied() + pub(crate) fn is_always_satisfied(self, db: &'db dyn Db) -> bool { + self.node.is_always_satisfied(db) } /// Updates this constraint set to hold the union of itself and another constraint set. @@ -208,7 +208,7 @@ impl<'db> ConstraintSet<'db> { /// provided as a thunk, to implement short-circuiting: the thunk is not forced if the /// constraint set is already saturated. pub(crate) fn and(mut self, db: &'db dyn Db, other: impl FnOnce() -> Self) -> Self { - if !self.is_never_satisfied() { + if !self.is_never_satisfied(db) { self.intersect(db, other()); } self @@ -218,7 +218,7 @@ impl<'db> ConstraintSet<'db> { /// as a thunk, to implement short-circuiting: the thunk is not forced if the constraint set is /// already saturated. pub(crate) fn or(mut self, db: &'db dyn Db, other: impl FnOnce() -> Self) -> Self { - if !self.is_always_satisfied() { + if !self.is_always_satisfied(db) { self.union(db, other()); } self @@ -247,7 +247,7 @@ impl<'db> ConstraintSet<'db> { } pub(crate) fn display(self, db: &'db dyn Db) -> impl Display { - self.node.display(db) + self.node.simplify(db).display(db) } } @@ -494,8 +494,16 @@ impl<'db> Node<'db> { } /// Returns whether this BDD represent the constant function `true`. - fn is_always_satisfied(self) -> bool { - matches!(self, Node::AlwaysTrue) + fn is_always_satisfied(self, db: &'db dyn Db) -> bool { + match self { + Node::AlwaysTrue => true, + Node::AlwaysFalse => false, + Node::Interior(_) => { + let domain = self.domain(db); + let restricted = self.and(db, domain); + restricted == domain + } + } } /// Returns whether this BDD represent the constant function `false`. @@ -538,6 +546,11 @@ impl<'db> Node<'db> { } } + fn implies(self, db: &'db dyn Db, other: Self) -> Self { + // p → q == ¬p ∨ q + self.negate(db).or(db, other) + } + /// Returns a new BDD that evaluates to `true` when both input BDDs evaluate to the same /// result. fn iff(self, db: &'db dyn Db, other: Self) -> Self { @@ -738,7 +751,21 @@ impl<'db> Node<'db> { fn simplify(self, db: &'db dyn Db) -> Self { match self { Node::AlwaysTrue | Node::AlwaysFalse => self, - Node::Interior(interior) => interior.simplify(db), + Node::Interior(interior) => { + let (simplified, _) = interior.simplify(db); + simplified + } + } + } + + /// Returns the domain (the set of allowed inputs) for a BDD. + fn domain(self, db: &'db dyn Db) -> Self { + match self { + Node::AlwaysTrue | Node::AlwaysFalse => Node::AlwaysTrue, + Node::Interior(interior) => { + let (_, domain) = interior.simplify(db); + domain + } } } @@ -801,10 +828,7 @@ impl<'db> Node<'db> { } } - DisplayNode { - node: self.simplify(db), - db, - } + DisplayNode { node: self, db } } /// Displays the full graph structure of this BDD. `prefix` will be output before each line @@ -1009,8 +1033,14 @@ impl<'db> InteriorNode<'db> { } } + /// Returns a simplified version of a BDD, along with the BDD's domain. + /// + /// Both are 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 is excluded from the BDD's domain. At the same time, 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(self, db: &'db dyn Db) -> (Node<'db>, Node<'db>) { // 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. @@ -1037,6 +1067,7 @@ impl<'db> InteriorNode<'db> { // Repeatedly pop constraint pairs off of the visit queue, checking whether each pair can // be simplified. let mut simplified = Node::Interior(self); + let mut domain = Node::AlwaysTrue; while let Some((left_constraint, right_constraint)) = to_visit.pop() { // If the constraints refer to different typevars, they trivially cannot be compared. // TODO: We might need to consider when one constraint's upper or lower bound refers to @@ -1056,12 +1087,24 @@ impl<'db> InteriorNode<'db> { None }; if let Some((larger_constraint, smaller_constraint)) = larger_smaller { + let positive_larger_node = + Node::new_satisfied_constraint(db, larger_constraint.when_true()); + let negative_larger_node = + Node::new_satisfied_constraint(db, larger_constraint.when_false()); + + let positive_smaller_node = + Node::new_satisfied_constraint(db, smaller_constraint.when_true()); + + // smaller → larger + let implication = positive_smaller_node.implies(db, positive_larger_node); + domain = domain.and(db, implication); + // larger ∨ smaller = larger simplified = simplified.substitute_union( db, larger_constraint.when_true(), smaller_constraint.when_true(), - Node::new_satisfied_constraint(db, larger_constraint.when_true()), + positive_larger_node, ); // ¬larger ∧ ¬smaller = ¬larger @@ -1069,7 +1112,7 @@ impl<'db> InteriorNode<'db> { db, larger_constraint.when_false(), smaller_constraint.when_false(), - Node::new_satisfied_constraint(db, larger_constraint.when_false()), + negative_larger_node, ); // smaller ∧ ¬larger = false @@ -1111,6 +1154,21 @@ impl<'db> InteriorNode<'db> { let negative_intersection_node = Node::new_satisfied_constraint(db, intersection_constraint.when_false()); + let positive_left_node = + Node::new_satisfied_constraint(db, left_constraint.when_true()); + let negative_left_node = + Node::new_satisfied_constraint(db, left_constraint.when_false()); + + let positive_right_node = + Node::new_satisfied_constraint(db, right_constraint.when_true()); + let negative_right_node = + Node::new_satisfied_constraint(db, right_constraint.when_false()); + + // (left ∧ right) → intersection + let implication = (positive_left_node.and(db, positive_right_node)) + .implies(db, positive_intersection_node); + domain = domain.and(db, implication); + // left ∧ right = intersection simplified = simplified.substitute_intersection( db, @@ -1134,8 +1192,7 @@ impl<'db> InteriorNode<'db> { db, left_constraint.when_true(), right_constraint.when_false(), - Node::new_satisfied_constraint(db, left_constraint.when_true()) - .and(db, negative_intersection_node), + positive_left_node.and(db, negative_intersection_node), ); // ¬left ∧ right = ¬intersection ∧ right @@ -1144,8 +1201,7 @@ impl<'db> InteriorNode<'db> { db, left_constraint.when_false(), right_constraint.when_true(), - Node::new_satisfied_constraint(db, right_constraint.when_true()) - .and(db, negative_intersection_node), + positive_right_node.and(db, negative_intersection_node), ); // left ∨ ¬right = intersection ∨ ¬right @@ -1155,8 +1211,7 @@ impl<'db> InteriorNode<'db> { db, left_constraint.when_true(), right_constraint.when_false(), - Node::new_satisfied_constraint(db, right_constraint.when_false()) - .or(db, positive_intersection_node), + negative_right_node.or(db, positive_intersection_node), ); // ¬left ∨ right = ¬left ∨ intersection @@ -1165,8 +1220,7 @@ impl<'db> InteriorNode<'db> { db, left_constraint.when_false(), right_constraint.when_true(), - Node::new_satisfied_constraint(db, left_constraint.when_false()) - .or(db, positive_intersection_node), + negative_left_node.or(db, positive_intersection_node), ); } @@ -1174,6 +1228,16 @@ impl<'db> InteriorNode<'db> { // All of the below hold because we just proved that the intersection of left // and right is empty. + let positive_left_node = + Node::new_satisfied_constraint(db, left_constraint.when_true()); + let positive_right_node = + Node::new_satisfied_constraint(db, right_constraint.when_true()); + + // (left ∧ right) → false + let implication = (positive_left_node.and(db, positive_right_node)) + .implies(db, Node::AlwaysFalse); + domain = domain.and(db, implication); + // left ∧ right = false simplified = simplified.substitute_intersection( db, @@ -1196,7 +1260,7 @@ impl<'db> InteriorNode<'db> { db, left_constraint.when_true(), right_constraint.when_false(), - Node::new_constraint(db, left_constraint), + positive_left_node, ); // ¬left ∧ right = right @@ -1205,13 +1269,13 @@ impl<'db> InteriorNode<'db> { db, left_constraint.when_false(), right_constraint.when_true(), - Node::new_constraint(db, right_constraint), + positive_right_node, ); } } } - simplified + (simplified, domain) } } @@ -1459,6 +1523,11 @@ impl<'db> SatisfiedClauses<'db> { while self.simplify_one_round() { // Keep going } + + // We can remove any clauses that have been simplified to the point where they are empty. + // (Clauses are intersections, so an empty clause is `false`, which does not contribute + // anything to the outer union.) + self.clauses.retain(|clause| !clause.constraints.is_empty()); } fn simplify_one_round(&mut self) -> bool { diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index e8c6305d06..931ec28756 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1179,7 +1179,7 @@ impl<'db> Specialization<'db> { ), TypeVarVariance::Bivariant => ConstraintSet::from(true), }; - if result.intersect(db, compatible).is_never_satisfied() { + if result.intersect(db, compatible).is_never_satisfied(db) { return result; } } @@ -1221,7 +1221,7 @@ impl<'db> Specialization<'db> { } TypeVarVariance::Bivariant => ConstraintSet::from(true), }; - if result.intersect(db, compatible).is_never_satisfied() { + if result.intersect(db, compatible).is_never_satisfied(db) { return result; } } @@ -1232,7 +1232,7 @@ impl<'db> Specialization<'db> { (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() { + if result.intersect(db, compatible).is_never_satisfied(db) { return result; } } @@ -1386,7 +1386,7 @@ impl<'db> SpecializationBuilder<'db> { && !actual.is_never() && actual .when_subtype_of(self.db, formal, self.inferable) - .is_always_satisfied() + .is_always_satisfied(self.db) { return Ok(()); } @@ -1472,7 +1472,7 @@ impl<'db> SpecializationBuilder<'db> { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { if !ty .when_assignable_to(self.db, bound, self.inferable) - .is_always_satisfied() + .is_always_satisfied(self.db) { return Err(SpecializationError::MismatchedBound { bound_typevar, @@ -1485,7 +1485,7 @@ impl<'db> SpecializationBuilder<'db> { for constraint in constraints.elements(self.db) { if ty .when_assignable_to(self.db, *constraint, self.inferable) - .is_always_satisfied() + .is_always_satisfied(self.db) { self.add_type_mapping(bound_typevar, *constraint); 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 9eae531590..126f1ad557 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8189,7 +8189,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) => { let left = left.constraints(self.db()); let right = right.constraints(self.db()); - let result = left.and(self.db(), || *right); + let result = left.and(self.db(), || right); Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( TrackedConstraintSet::new(self.db(), result), ))) @@ -8202,7 +8202,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) => { let left = left.constraints(self.db()); let right = right.constraints(self.db()); - let result = left.or(self.db(), || *right); + let result = left.or(self.db(), || right); Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( TrackedConstraintSet::new(self.db(), result), ))) @@ -8930,6 +8930,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { })) } + ( + Type::KnownInstance(KnownInstanceType::ConstraintSet(left)), + Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), + ) => match op { + ast::CmpOp::Eq => Some(Ok(Type::BooleanLiteral( + left.constraints(self.db()) == right.constraints(self.db()) + ))), + ast::CmpOp::NotEq => Some(Ok(Type::BooleanLiteral( + left.constraints(self.db()) != right.constraints(self.db()) + ))), + _ => None, + } + ( Type::NominalInstance(nominal1), Type::NominalInstance(nominal2), diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index c8aa42d726..fb1fb5c0a7 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -429,7 +429,7 @@ impl<'db> NominalInstanceType<'db> { disjointness_visitor, relation_visitor, ); - if result.union(db, compatible).is_always_satisfied() { + if result.union(db, compatible).is_always_satisfied(db) { return result; } } @@ -659,7 +659,7 @@ impl<'db> ProtocolInstanceType<'db> { &HasRelationToVisitor::default(), &IsDisjointVisitor::default(), ) - .is_always_satisfied() + .is_always_satisfied(db) } fn initial<'db>(_db: &'db dyn Db, _value: ProtocolInstanceType<'db>, _: ()) -> bool { diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 26c3aca6a1..b0ff205e48 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -621,7 +621,7 @@ impl<'db> Signature<'db> { db, self_type.is_equivalent_to_impl(db, other_type, inferable, visitor), ) - .is_never_satisfied() + .is_never_satisfied(db) }; if self.parameters.is_gradual() != other.parameters.is_gradual() { @@ -787,7 +787,7 @@ impl<'db> Signature<'db> { disjointness_visitor, ), ) - .is_never_satisfied() + .is_never_satisfied(db) }; // Return types are covariant. diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index fb08672de1..cfb2febce4 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -477,7 +477,7 @@ impl<'db> FixedLengthTuple> { ); if result .intersect(db, element_constraints) - .is_never_satisfied() + .is_never_satisfied(db) { return result; } @@ -496,7 +496,7 @@ impl<'db> FixedLengthTuple> { ); if result .intersect(db, element_constraints) - .is_never_satisfied() + .is_never_satisfied(db) { return result; } @@ -834,7 +834,7 @@ impl<'db> VariableLengthTuple> { ); if result .intersect(db, element_constraints) - .is_never_satisfied() + .is_never_satisfied(db) { return result; } @@ -854,7 +854,7 @@ impl<'db> VariableLengthTuple> { ); if result .intersect(db, element_constraints) - .is_never_satisfied() + .is_never_satisfied(db) { return result; } @@ -907,7 +907,10 @@ impl<'db> VariableLengthTuple> { return ConstraintSet::from(false); } }; - if result.intersect(db, pair_constraints).is_never_satisfied() { + if result + .intersect(db, pair_constraints) + .is_never_satisfied(db) + { return result; } } @@ -943,7 +946,10 @@ impl<'db> VariableLengthTuple> { return ConstraintSet::from(false); } }; - if result.intersect(db, pair_constraints).is_never_satisfied() { + if result + .intersect(db, pair_constraints) + .is_never_satisfied(db) + { return result; } } diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi index 262ded8867..6c87eb8160 100644 --- a/crates/ty_vendored/ty_extensions/ty_extensions.pyi +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi @@ -44,6 +44,8 @@ type JustComplex = TypeOf[1.0j] # Constraints class ConstraintSet: def __bool__(self) -> bool: ... + def __eq__(self, other: ConstraintSet) -> bool: ... + def __ne__(self, other: ConstraintSet) -> bool: ... def __and__(self, other: ConstraintSet) -> ConstraintSet: ... def __or__(self, other: ConstraintSet) -> ConstraintSet: ... def __invert__(self) -> ConstraintSet: ... From 304ac22e74c3b7207ae6a6eea071973da6bd5f72 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Fri, 24 Oct 2025 16:14:18 -0400 Subject: [PATCH 041/188] [ty] Use constructor parameter types as type context (#21054) ## Summary Resolves https://github.com/astral-sh/ty/issues/1408. --- .../resources/mdtest/bidirectional.md | 81 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 78 +++++++++++++----- .../ty_python_semantic/src/types/call/bind.rs | 8 ++ .../src/types/infer/builder.rs | 18 ++++- 4 files changed, 161 insertions(+), 24 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index 3485304b6b..627492855f 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -145,3 +145,84 @@ def h[T](x: T, cond: bool) -> T | list[T]: def i[T](x: T, cond: bool) -> T | list[T]: return x if cond else [x] ``` + +## Type context sources + +Type context is sourced from various places, including annotated assignments: + +```py +from typing import Literal + +a: list[Literal[1]] = [1] +``` + +Function parameter annotations: + +```py +def b(x: list[Literal[1]]): ... + +b([1]) +``` + +Bound method parameter annotations: + +```py +class C: + def __init__(self, x: list[Literal[1]]): ... + def foo(self, x: list[Literal[1]]): ... + +C([1]).foo([1]) +``` + +Declared variable types: + +```py +d: list[Literal[1]] +d = [1] +``` + +Declared attribute types: + +```py +class E: + e: list[Literal[1]] + +def _(e: E): + # TODO: Implement attribute type context. + # error: [invalid-assignment] "Object of type `list[Unknown | int]` is not assignable to attribute `e` of type `list[Literal[1]]`" + e.e = [1] +``` + +Function return types: + +```py +def f() -> list[Literal[1]]: + return [1] +``` + +## Class constructor parameters + +```toml +[environment] +python-version = "3.12" +``` + +The parameters of both `__init__` and `__new__` are used as type context sources for constructor +calls: + +```py +def f[T](x: T) -> list[T]: + return [x] + +class A: + def __new__(cls, value: list[int | str]): + return super().__new__(cls, value) + + def __init__(self, value: list[int | None]): ... + +A(f(1)) + +# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `list[int | str]`, found `list[list[Unknown]]`" +# error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `list[int | None]`, found `list[list[Unknown]]`" +A(f([])) +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 0d2d7ac866..8946861894 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6007,6 +6007,9 @@ impl<'db> Type<'db> { /// Given a class literal or non-dynamic `SubclassOf` type, try calling it (creating an instance) /// and return the resulting instance type. /// + /// The `infer_argument_types` closure should be invoked with the signatures of `__new__` and + /// `__init__`, such that the argument types can be inferred with the correct type context. + /// /// Models `type.__call__` behavior. /// TODO: model metaclass `__call__`. /// @@ -6017,10 +6020,10 @@ impl<'db> Type<'db> { /// /// Foo() /// ``` - fn try_call_constructor( + fn try_call_constructor<'ast>( self, db: &'db dyn Db, - argument_types: CallArguments<'_, 'db>, + infer_argument_types: impl FnOnce(Option>) -> CallArguments<'ast, 'db>, tcx: TypeContext<'db>, ) -> Result, ConstructorCallError<'db>> { debug_assert!(matches!( @@ -6076,11 +6079,63 @@ impl<'db> Type<'db> { // easy to check if that's the one we found? // Note that `__new__` is a static method, so we must inject the `cls` argument. let new_method = self_type.lookup_dunder_new(db, ()); + + // Construct an instance type that we can use to look up the `__init__` instance method. + // This performs the same logic as `Type::to_instance`, except for generic class literals. + // TODO: we should use the actual return type of `__new__` to determine the instance type + let init_ty = self_type + .to_instance(db) + .expect("type should be convertible to instance type"); + + // Lookup the `__init__` instance method in the MRO. + let init_method = init_ty.member_lookup_with_policy( + db, + "__init__".into(), + MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + ); + + // Infer the call argument types, using both `__new__` and `__init__` for type-context. + let bindings = match ( + new_method.as_ref().map(|method| &method.place), + &init_method.place, + ) { + (Some(Place::Defined(new_method, ..)), Place::Undefined) => Some( + new_method + .bindings(db) + .map(|binding| binding.with_bound_type(self_type)), + ), + + (Some(Place::Undefined) | None, Place::Defined(init_method, ..)) => { + Some(init_method.bindings(db)) + } + + (Some(Place::Defined(new_method, ..)), Place::Defined(init_method, ..)) => { + let callable = UnionBuilder::new(db) + .add(*new_method) + .add(*init_method) + .build(); + + let new_method_bindings = new_method + .bindings(db) + .map(|binding| binding.with_bound_type(self_type)); + + Some(Bindings::from_union( + callable, + [new_method_bindings, init_method.bindings(db)], + )) + } + + _ => None, + }; + + let argument_types = infer_argument_types(bindings); + let new_call_outcome = new_method.and_then(|new_method| { match new_method.place.try_call_dunder_get(db, self_type) { Place::Defined(new_method, _, boundness) => { let result = new_method.try_call(db, argument_types.with_self(Some(self_type)).as_ref()); + if boundness == Definedness::PossiblyUndefined { Some(Err(DunderNewCallError::PossiblyUnbound(result.err()))) } else { @@ -6091,24 +6146,7 @@ impl<'db> Type<'db> { } }); - // Construct an instance type that we can use to look up the `__init__` instance method. - // This performs the same logic as `Type::to_instance`, except for generic class literals. - // TODO: we should use the actual return type of `__new__` to determine the instance type - let init_ty = self_type - .to_instance(db) - .expect("type should be convertible to instance type"); - - let init_call_outcome = if new_call_outcome.is_none() - || !init_ty - .member_lookup_with_policy( - db, - "__init__".into(), - MemberLookupPolicy::NO_INSTANCE_FALLBACK - | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, - ) - .place - .is_undefined() - { + let init_call_outcome = if new_call_outcome.is_none() || !init_method.is_undefined() { Some(init_ty.try_call_dunder(db, "__init__", argument_types, tcx)) } else { None diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index e6121da055..e72d798dd6 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -100,6 +100,14 @@ impl<'db> Bindings<'db> { self.elements.iter() } + pub(crate) fn map(self, f: impl Fn(CallableBinding<'db>) -> CallableBinding<'db>) -> Self { + Self { + callable_type: self.callable_type, + argument_forms: self.argument_forms, + elements: self.elements.into_iter().map(f).collect(), + } + } + /// Match the arguments of a call site against the parameters of a collection of possibly /// unioned, possibly overloaded signatures. /// diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 126f1ad557..9a5091f837 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -6798,9 +6798,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .to_class_type(self.db()) .is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class)) { - let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; - self.infer_argument_types(arguments, &mut call_arguments, &argument_forms); - if matches!( class.known(self.db()), Some(KnownClass::TypeVar | KnownClass::ExtensionsTypeVar) @@ -6819,8 +6816,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + let db = self.db(); + let infer_call_arguments = |bindings: Option>| { + if let Some(bindings) = bindings { + let bindings = bindings.match_parameters(self.db(), &call_arguments); + self.infer_all_argument_types(arguments, &mut call_arguments, &bindings); + } else { + let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; + self.infer_argument_types(arguments, &mut call_arguments, &argument_forms); + } + + call_arguments + }; + return callable_type - .try_call_constructor(self.db(), call_arguments, tcx) + .try_call_constructor(db, infer_call_arguments, tcx) .unwrap_or_else(|err| { err.report_diagnostic(&self.context, callable_type, call_expression.into()); err.return_type() From 1ade9a59435deac27345ea4c8fbecef56cdf67e4 Mon Sep 17 00:00:00 2001 From: Dan Parizher <105245560+danparizher@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:54:09 -0400 Subject: [PATCH 042/188] [`pydoclint`] Fix false positive on explicit exception re-raising (`DOC501`, `DOC502`) (#21011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #20973 (`docstring-extraneous-exception`) false positive when exceptions mentioned in docstrings are caught and explicitly re-raised using `raise e` or `raise e from None`. ## Problem Analysis The DOC502 rule was incorrectly flagging exceptions mentioned in docstrings as "not explicitly raised" when they were actually being explicitly re-raised through exception variables bound in `except` clauses. **Root Cause**: The `BodyVisitor` in `check_docstring.rs` only checked for direct exception references (like `raise OSError()`) but didn't recognize when a variable bound to an exception in an `except` clause was being re-raised. **Example of the bug**: ```python def f(): """Do nothing. Raises ------ OSError If the OS errors. """ try: pass except OSError as e: raise e # This was incorrectly flagged as not explicitly raising OSError ``` The issue occurred because `resolve_qualified_name(e)` couldn't resolve the variable `e` to a qualified exception name, since `e` is just a variable binding, not a direct reference to an exception class. ## Approach Modified the `BodyVisitor` in `crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs` to: 1. **Track exception variable bindings**: Added `exception_variables` field to map exception variable names to their exception types within `except` clauses 2. **Enhanced raise statement detection**: Updated `visit_stmt` to check if a `raise` statement uses a variable name that's bound to an exception in the current `except` clause 3. **Proper scope management**: Clear exception variable mappings when leaving `except` handlers to prevent cross-contamination **Key changes**: - Added `exception_variables: FxHashMap<&'a str, QualifiedName<'a>>` to track variable-to-exception mappings - Enhanced `visit_except_handler` to store exception variable bindings when entering `except` clauses - Modified `visit_stmt` to check for variable-based re-raising: `raise e` → lookup `e` in `exception_variables` - Clear mappings when exiting `except` handlers to maintain proper scope --------- Co-authored-by: Brent Westbrook --- .../test/fixtures/pydoclint/DOC502_google.py | 52 ++++++++++++++++ .../rules/pydoclint/rules/check_docstring.rs | 62 ++++++++++++++----- ...ng-missing-exception_DOC501_google.py.snap | 60 ++++++++++++++++++ ...tion_DOC501_google.py_ignore_one_line.snap | 60 ++++++++++++++++++ 4 files changed, 218 insertions(+), 16 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py index f9e7f7a89f..9709d9ff53 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py @@ -81,3 +81,55 @@ def calculate_speed(distance: float, time: float) -> float: except TypeError: print("Not a number? Shame on you!") raise + + +# This should NOT trigger DOC502 because OSError is explicitly re-raised +def f(): + """Do nothing. + + Raises: + OSError: If the OS errors. + """ + try: + pass + except OSError as e: + raise e + + +# This should NOT trigger DOC502 because OSError is explicitly re-raised with from None +def g(): + """Do nothing. + + Raises: + OSError: If the OS errors. + """ + try: + pass + except OSError as e: + raise e from None + + +# This should NOT trigger DOC502 because ValueError is explicitly re-raised from tuple exception +def h(): + """Do nothing. + + Raises: + ValueError: If something goes wrong. + """ + try: + pass + except (ValueError, TypeError) as e: + raise e + + +# This should NOT trigger DOC502 because TypeError is explicitly re-raised from tuple exception +def i(): + """Do nothing. + + Raises: + TypeError: If something goes wrong. + """ + try: + pass + except (ValueError, TypeError) as e: + raise e diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index adcdcc5dec..dd88250952 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -9,6 +9,7 @@ use ruff_python_semantic::{Definition, SemanticModel}; use ruff_python_stdlib::identifiers::is_identifier; use ruff_source_file::{LineRanges, NewlineWithTrailingNewline}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; +use rustc_hash::FxHashMap; use crate::Violation; use crate::checkers::ast::Checker; @@ -823,6 +824,8 @@ struct BodyVisitor<'a> { currently_suspended_exceptions: Option<&'a ast::Expr>, raised_exceptions: Vec>, semantic: &'a SemanticModel<'a>, + /// Maps exception variable names to their exception expressions in the current except clause + exception_variables: FxHashMap<&'a str, &'a ast::Expr>, } impl<'a> BodyVisitor<'a> { @@ -833,6 +836,7 @@ impl<'a> BodyVisitor<'a> { currently_suspended_exceptions: None, raised_exceptions: Vec::new(), semantic, + exception_variables: FxHashMap::default(), } } @@ -864,20 +868,47 @@ impl<'a> BodyVisitor<'a> { raised_exceptions, } } + + /// Store `exception` if its qualified name does not correspond to one of the exempt types. + fn maybe_store_exception(&mut self, exception: &'a Expr, range: TextRange) { + let Some(qualified_name) = self.semantic.resolve_qualified_name(exception) else { + return; + }; + if is_exception_or_base_exception(&qualified_name) { + return; + } + self.raised_exceptions.push(ExceptionEntry { + qualified_name, + range, + }); + } } impl<'a> Visitor<'a> for BodyVisitor<'a> { fn visit_except_handler(&mut self, handler: &'a ast::ExceptHandler) { let ast::ExceptHandler::ExceptHandler(handler_inner) = handler; self.currently_suspended_exceptions = handler_inner.type_.as_deref(); + + // Track exception variable bindings + if let Some(name) = handler_inner.name.as_ref() { + if let Some(exceptions) = self.currently_suspended_exceptions { + // Store the exception expression(s) for later resolution + self.exception_variables + .insert(name.id.as_str(), exceptions); + } + } + visitor::walk_except_handler(self, handler); self.currently_suspended_exceptions = None; + // Clear exception variables when leaving the except handler + self.exception_variables.clear(); } fn visit_stmt(&mut self, stmt: &'a Stmt) { match stmt { Stmt::Raise(ast::StmtRaise { exc, .. }) => { if let Some(exc) = exc.as_ref() { + // First try to resolve the exception directly if let Some(qualified_name) = self.semantic.resolve_qualified_name(map_callable(exc)) { @@ -885,28 +916,27 @@ impl<'a> Visitor<'a> for BodyVisitor<'a> { qualified_name, range: exc.range(), }); + } else if let ast::Expr::Name(name) = exc.as_ref() { + // If it's a variable name, check if it's bound to an exception in the + // current except clause + if let Some(exception_expr) = self.exception_variables.get(name.id.as_str()) + { + if let ast::Expr::Tuple(tuple) = exception_expr { + for exception in tuple { + self.maybe_store_exception(exception, stmt.range()); + } + } else { + self.maybe_store_exception(exception_expr, stmt.range()); + } + } } } else if let Some(exceptions) = self.currently_suspended_exceptions { - let mut maybe_store_exception = |exception| { - let Some(qualified_name) = self.semantic.resolve_qualified_name(exception) - else { - return; - }; - if is_exception_or_base_exception(&qualified_name) { - return; - } - self.raised_exceptions.push(ExceptionEntry { - qualified_name, - range: stmt.range(), - }); - }; - if let ast::Expr::Tuple(tuple) = exceptions { for exception in tuple { - maybe_store_exception(exception); + self.maybe_store_exception(exception, stmt.range()); } } else { - maybe_store_exception(exceptions); + self.maybe_store_exception(exceptions, stmt.range()); } } } diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py.snap index ab13770174..c0b82991ae 100644 --- a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py.snap +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py.snap @@ -61,6 +61,66 @@ DOC501 Raised exception `FasterThanLightError` missing from docstring | help: Add `FasterThanLightError` to the docstring +DOC501 Raised exception `ZeroDivisionError` missing from docstring + --> DOC501_google.py:70:5 + | +68 | # DOC501 +69 | def calculate_speed(distance: float, time: float) -> float: +70 | / """Calculate speed as distance divided by time. +71 | | +72 | | Args: +73 | | distance: Distance traveled. +74 | | time: Time spent traveling. +75 | | +76 | | Returns: +77 | | Speed as distance divided by time. +78 | | """ + | |_______^ +79 | try: +80 | return distance / time + | +help: Add `ZeroDivisionError` to the docstring + +DOC501 Raised exception `ValueError` missing from docstring + --> DOC501_google.py:88:5 + | +86 | # DOC501 +87 | def calculate_speed(distance: float, time: float) -> float: +88 | / """Calculate speed as distance divided by time. +89 | | +90 | | Args: +91 | | distance: Distance traveled. +92 | | time: Time spent traveling. +93 | | +94 | | Returns: +95 | | Speed as distance divided by time. +96 | | """ + | |_______^ +97 | try: +98 | return distance / time + | +help: Add `ValueError` to the docstring + +DOC501 Raised exception `ZeroDivisionError` missing from docstring + --> DOC501_google.py:88:5 + | +86 | # DOC501 +87 | def calculate_speed(distance: float, time: float) -> float: +88 | / """Calculate speed as distance divided by time. +89 | | +90 | | Args: +91 | | distance: Distance traveled. +92 | | time: Time spent traveling. +93 | | +94 | | Returns: +95 | | Speed as distance divided by time. +96 | | """ + | |_______^ +97 | try: +98 | return distance / time + | +help: Add `ZeroDivisionError` to the docstring + DOC501 Raised exception `AnotherError` missing from docstring --> DOC501_google.py:106:5 | diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py_ignore_one_line.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py_ignore_one_line.snap index ab13770174..c0b82991ae 100644 --- a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py_ignore_one_line.snap +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py_ignore_one_line.snap @@ -61,6 +61,66 @@ DOC501 Raised exception `FasterThanLightError` missing from docstring | help: Add `FasterThanLightError` to the docstring +DOC501 Raised exception `ZeroDivisionError` missing from docstring + --> DOC501_google.py:70:5 + | +68 | # DOC501 +69 | def calculate_speed(distance: float, time: float) -> float: +70 | / """Calculate speed as distance divided by time. +71 | | +72 | | Args: +73 | | distance: Distance traveled. +74 | | time: Time spent traveling. +75 | | +76 | | Returns: +77 | | Speed as distance divided by time. +78 | | """ + | |_______^ +79 | try: +80 | return distance / time + | +help: Add `ZeroDivisionError` to the docstring + +DOC501 Raised exception `ValueError` missing from docstring + --> DOC501_google.py:88:5 + | +86 | # DOC501 +87 | def calculate_speed(distance: float, time: float) -> float: +88 | / """Calculate speed as distance divided by time. +89 | | +90 | | Args: +91 | | distance: Distance traveled. +92 | | time: Time spent traveling. +93 | | +94 | | Returns: +95 | | Speed as distance divided by time. +96 | | """ + | |_______^ +97 | try: +98 | return distance / time + | +help: Add `ValueError` to the docstring + +DOC501 Raised exception `ZeroDivisionError` missing from docstring + --> DOC501_google.py:88:5 + | +86 | # DOC501 +87 | def calculate_speed(distance: float, time: float) -> float: +88 | / """Calculate speed as distance divided by time. +89 | | +90 | | Args: +91 | | distance: Distance traveled. +92 | | time: Time spent traveling. +93 | | +94 | | Returns: +95 | | Speed as distance divided by time. +96 | | """ + | |_______^ +97 | try: +98 | return distance / time + | +help: Add `ZeroDivisionError` to the docstring + DOC501 Raised exception `AnotherError` missing from docstring --> DOC501_google.py:106:5 | From 64ab79e5721ec6fdd2182fbf9d39a26534ccca43 Mon Sep 17 00:00:00 2001 From: Auguste Lalande Date: Fri, 24 Oct 2025 17:19:30 -0400 Subject: [PATCH 043/188] Add missing docstring sections to the numpy list (#20931) ## Summary Add docstring sections which were missing from the numpy list as pointed out here #20923. For now these are only the official sections as documented [here](https://numpydoc.readthedocs.io/en/latest/format.html#sections). ## Test Plan Added a test case for DOC102 --- .../test/fixtures/pydoclint/DOC102_numpy.py | 19 +++++++++++++++++++ crates/ruff_linter/src/docstrings/google.rs | 4 ++-- crates/ruff_linter/src/docstrings/numpy.rs | 3 +++ crates/ruff_linter/src/docstrings/sections.rs | 3 +++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC102_numpy.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC102_numpy.py index fb7f86b25f..ea93e16cc1 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC102_numpy.py +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC102_numpy.py @@ -370,3 +370,22 @@ class Foo: The flag converter instance with all flags parsed. """ return + +# OK +def baz(x: int) -> int: + """ + Show a `Warnings` DOC102 false positive. + + Parameters + ---------- + x : int + + Warnings + -------- + This function demonstrates a DOC102 false positive + + Returns + ------- + int + """ + return x diff --git a/crates/ruff_linter/src/docstrings/google.rs b/crates/ruff_linter/src/docstrings/google.rs index 32d97ff10d..a6aba363cc 100644 --- a/crates/ruff_linter/src/docstrings/google.rs +++ b/crates/ruff_linter/src/docstrings/google.rs @@ -11,6 +11,8 @@ pub(crate) static GOOGLE_SECTIONS: &[SectionKind] = &[ SectionKind::References, SectionKind::Returns, SectionKind::SeeAlso, + SectionKind::Warnings, + SectionKind::Warns, SectionKind::Yields, // Google-only SectionKind::Args, @@ -32,7 +34,5 @@ pub(crate) static GOOGLE_SECTIONS: &[SectionKind] = &[ SectionKind::Tip, SectionKind::Todo, SectionKind::Warning, - SectionKind::Warnings, - SectionKind::Warns, SectionKind::Yield, ]; diff --git a/crates/ruff_linter/src/docstrings/numpy.rs b/crates/ruff_linter/src/docstrings/numpy.rs index da3fbef1ac..2c91acf197 100644 --- a/crates/ruff_linter/src/docstrings/numpy.rs +++ b/crates/ruff_linter/src/docstrings/numpy.rs @@ -11,11 +11,14 @@ pub(crate) static NUMPY_SECTIONS: &[SectionKind] = &[ SectionKind::References, SectionKind::Returns, SectionKind::SeeAlso, + SectionKind::Warnings, + SectionKind::Warns, SectionKind::Yields, // NumPy-only SectionKind::ExtendedSummary, SectionKind::OtherParams, SectionKind::OtherParameters, SectionKind::Parameters, + SectionKind::Receives, SectionKind::ShortSummary, ]; diff --git a/crates/ruff_linter/src/docstrings/sections.rs b/crates/ruff_linter/src/docstrings/sections.rs index 364836be77..a4151c67aa 100644 --- a/crates/ruff_linter/src/docstrings/sections.rs +++ b/crates/ruff_linter/src/docstrings/sections.rs @@ -36,6 +36,7 @@ pub(crate) enum SectionKind { OtherParameters, Parameters, Raises, + Receives, References, Return, Returns, @@ -76,6 +77,7 @@ impl SectionKind { "other parameters" => Some(Self::OtherParameters), "parameters" => Some(Self::Parameters), "raises" => Some(Self::Raises), + "receives" => Some(Self::Receives), "references" => Some(Self::References), "return" => Some(Self::Return), "returns" => Some(Self::Returns), @@ -117,6 +119,7 @@ impl SectionKind { Self::OtherParameters => "Other Parameters", Self::Parameters => "Parameters", Self::Raises => "Raises", + Self::Receives => "Receives", Self::References => "References", Self::Return => "Return", Self::Returns => "Returns", From 8e51db3ecd80cc115221b6568483514d2e30a6a2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:43:39 +0100 Subject: [PATCH 044/188] Update Rust crate bstr to v1.12.1 (#21089) 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 11897e189c..3823ed76df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,9 +295,9 @@ checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", From e692b7f1ee0ac66223fc7586afe33dae1e5d47a2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:44:02 +0100 Subject: [PATCH 045/188] Update Rust crate get-size2 to v0.7.1 (#21091) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3823ed76df..645c3cee23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1224,9 +1224,9 @@ dependencies = [ [[package]] name = "get-size-derive2" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3814abc7da8ab18d2fd820f5b540b5e39b6af0a32de1bdd7c47576693074843" +checksum = "46b134aa084df7c3a513a1035c52f623e4b3065dfaf3d905a4f28a2e79b5bb3f" dependencies = [ "attribute-derive", "quote", @@ -1235,9 +1235,9 @@ dependencies = [ [[package]] name = "get-size2" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfe2cec5b5ce8fb94dcdb16a1708baa4d0609cc3ce305ca5d3f6f2ffb59baed" +checksum = "c0d51c9f2e956a517619ad9e7eaebc7a573f9c49b38152e12eade750f89156f9" dependencies = [ "compact_str", "get-size-derive2", From c83c4d52a4c24dbdf9d66ab2494a0e5c07abe42e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:46:38 +0100 Subject: [PATCH 046/188] Update Rust crate clap to v4.5.50 (#21090) 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 645c3cee23..1a25626a00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -433,9 +433,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.49" +version = "4.5.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" +checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" dependencies = [ "clap_builder", "clap_derive", @@ -443,9 +443,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.49" +version = "4.5.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" +checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" dependencies = [ "anstream", "anstyle", @@ -633,7 +633,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -642,7 +642,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1093,7 +1093,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]] @@ -1690,7 +1690,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1754,7 +1754,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3545,7 +3545,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3941,7 +3941,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5021,7 +5021,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 bca5d33385bdc5b42d49947d502445198c5959ec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:47:12 +0100 Subject: [PATCH 047/188] Update cargo-bins/cargo-binstall action to v1.15.9 (#21086) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .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 c57773078f..7541b715a4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -438,7 +438,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo-binstall" - uses: cargo-bins/cargo-binstall@a66119fbb1c952daba62640c2609111fe0803621 # v1.15.7 + uses: cargo-bins/cargo-binstall@afcf9780305558bcc9e4bc94b7589ab2bb8b6106 # v1.15.9 - 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 @@ -699,7 +699,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - - uses: cargo-bins/cargo-binstall@a66119fbb1c952daba62640c2609111fe0803621 # v1.15.7 + - uses: cargo-bins/cargo-binstall@afcf9780305558bcc9e4bc94b7589ab2bb8b6106 # v1.15.9 - run: cargo binstall --no-confirm cargo-shear - run: cargo shear From d846a0319a4e40152d34ec75289967ac784e6524 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:47:46 +0100 Subject: [PATCH 048/188] Update dependency mdformat-mkdocs to v4.4.2 (#21088) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/requirements-insiders.txt | 2 +- docs/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/requirements-insiders.txt b/docs/requirements-insiders.txt index a225850aff..127d4bfaa2 100644 --- a/docs/requirements-insiders.txt +++ b/docs/requirements-insiders.txt @@ -4,5 +4,5 @@ mkdocs==1.6.1 mkdocs-material @ git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@39da7a5e761410349e9a1b8abf593b0cdd5453ff mkdocs-redirects==1.2.2 mdformat==0.7.22 -mdformat-mkdocs==4.4.1 +mdformat-mkdocs==4.4.2 mkdocs-github-admonitions-plugin @ git+https://github.com/PGijsbers/admonitions.git#7343d2f4a92e4d1491094530ef3d0d02d93afbb7 diff --git a/docs/requirements.txt b/docs/requirements.txt index 1062e321e7..9742b48785 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,5 +4,5 @@ mkdocs==1.6.1 mkdocs-material==9.5.38 mkdocs-redirects==1.2.2 mdformat==0.7.22 -mdformat-mkdocs==4.4.1 +mdformat-mkdocs==4.4.2 mkdocs-github-admonitions-plugin @ git+https://github.com/PGijsbers/admonitions.git#7343d2f4a92e4d1491094530ef3d0d02d93afbb7 From fdb8ea487cb37f32116d94ae66b947de54990d3b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 08:29:32 +0100 Subject: [PATCH 049/188] Update upload and download artifacts github actions (#21083) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Micha Reiser --- .github/workflows/release.yml | 18 +++++++++--------- dist-workspace.toml | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6261aed8ab..e2a385715f 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.30.0/cargo-dist-installer.sh | sh" - name: Cache dist - uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 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@6027e3dd177782cd8ab9af838c04fd81a07f1d47 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: name: artifacts-plan-dist-manifest path: plan-dist-manifest.json @@ -128,14 +128,14 @@ jobs: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 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@634f93cb2916e3fdff6788551b99b062d0335ce0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 with: pattern: artifacts-* path: target/distrib/ @@ -153,7 +153,7 @@ jobs: cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" - uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: name: artifacts-build-global path: | @@ -179,14 +179,14 @@ jobs: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 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@634f93cb2916e3fdff6788551b99b062d0335ce0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 with: pattern: artifacts-* path: target/distrib/ @@ -200,7 +200,7 @@ jobs: cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: # Overwrite the previous copy name: artifacts-dist-manifest @@ -256,7 +256,7 @@ jobs: submodules: recursive # Create a GitHub Release while uploading all files to it - name: "Download GitHub Artifacts" - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 with: pattern: artifacts-* path: artifacts diff --git a/dist-workspace.toml b/dist-workspace.toml index 5d1b64992e..20f123b05a 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -67,6 +67,6 @@ global = "depot-ubuntu-latest-4" [dist.github-action-commits] "actions/checkout" = "ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493" # v5.0.0 -"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2 -"actions/download-artifact" = "634f93cb2916e3fdff6788551b99b062d0335ce0" # v5.0.0 +"actions/upload-artifact" = "330a01c490aca151604b8cf639adc76d48f6c5d4" # v5.0.0 +"actions/download-artifact" = "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53" # v6.0.0 "actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3 From fa12fd0184cabcf92f76f62d3fccce9de0c193ff Mon Sep 17 00:00:00 2001 From: Shahar Naveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:42:48 +0200 Subject: [PATCH 050/188] Clearer error message when `line-length` goes beyond threshold (#21072) Co-authored-by: Micha Reiser --- crates/ruff_linter/src/line_width.rs | 17 ++++++++++++++++- crates/ruff_workspace/src/pyproject.rs | 14 +++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/line_width.rs b/crates/ruff_linter/src/line_width.rs index 7525a68cdd..c8cf857621 100644 --- a/crates/ruff_linter/src/line_width.rs +++ b/crates/ruff_linter/src/line_width.rs @@ -14,7 +14,7 @@ use ruff_text_size::TextSize; /// The length of a line of text that is considered too long. /// /// The allowed range of values is 1..=320 -#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct LineLength( #[cfg_attr(feature = "schemars", schemars(range(min = 1, max = 320)))] NonZeroU16, @@ -46,6 +46,21 @@ impl fmt::Display for LineLength { } } +impl<'de> serde::Deserialize<'de> for LineLength { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = u16::deserialize(deserializer)?; + Self::try_from(value).map_err(|_| { + serde::de::Error::custom(format!( + "line-length must be between 1 and {} (got {value})", + Self::MAX, + )) + }) + } +} + impl CacheKey for LineLength { fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_u16(self.0.get()); diff --git a/crates/ruff_workspace/src/pyproject.rs b/crates/ruff_workspace/src/pyproject.rs index 0f2251ee11..9cfa01a35d 100644 --- a/crates/ruff_workspace/src/pyproject.rs +++ b/crates/ruff_workspace/src/pyproject.rs @@ -266,7 +266,6 @@ mod tests { use crate::pyproject::{Pyproject, Tools, find_settings_toml, parse_pyproject_toml}; #[test] - fn deserialize() -> Result<()> { let pyproject: Pyproject = toml::from_str(r"")?; assert_eq!(pyproject.tool, None); @@ -456,6 +455,19 @@ other-attribute = 1 .is_err() ); + let invalid_line_length = toml::from_str::( + r" +[tool.ruff] +line-length = 500 +", + ) + .expect_err("Deserialization should have failed for a too large line-length"); + + assert_eq!( + invalid_line_length.message(), + "line-length must be between 1 and 320 (got 500)" + ); + Ok(()) } From 8a73519b252f1523130e3953c8fe866f90824f4b Mon Sep 17 00:00:00 2001 From: Dan Parizher <105245560+danparizher@users.noreply.github.com> Date: Mon, 27 Oct 2025 04:19:15 -0400 Subject: [PATCH 051/188] [`flake8-django`] Apply `DJ001` to annotated fields (#20907) --- .../test/fixtures/flake8_django/DJ001.py | 6 ++++ .../rules/nullable_model_string_field.rs | 9 ++++-- ..._flake8_django__tests__DJ001_DJ001.py.snap | 29 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ001.py b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ001.py index 267c2b69d9..4be84baf30 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ001.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_django/DJ001.py @@ -46,3 +46,9 @@ class CorrectModel(models.Model): max_length=255, null=True, blank=True, unique=True ) urlfieldu = models.URLField(max_length=255, null=True, blank=True, unique=True) + + +class IncorrectModelWithSimpleAnnotations(models.Model): + charfield: models.CharField = models.CharField(max_length=255, null=True) + textfield: models.TextField = models.TextField(max_length=255, null=True) + slugfield: models.SlugField = models.SlugField(max_length=255, null=True) diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs b/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs index 47e463d5df..9ff80f0d52 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs @@ -61,9 +61,14 @@ pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) { } for statement in body { - let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else { - continue; + let value = match statement { + Stmt::Assign(ast::StmtAssign { value, .. }) => value, + Stmt::AnnAssign(ast::StmtAnnAssign { + value: Some(value), .. + }) => value, + _ => continue, }; + if let Some(field_name) = is_nullable_field(value, checker.semantic()) { checker.report_diagnostic( DjangoNullableModelStringField { diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ001_DJ001.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ001_DJ001.py.snap index dccc159fa5..56a8c683fb 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ001_DJ001.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ001_DJ001.py.snap @@ -186,3 +186,32 @@ DJ001 Avoid using `null=True` on string-based fields such as `URLField` 30 | urlfield = models.URLField(max_length=255, null=True) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | + +DJ001 Avoid using `null=True` on string-based fields such as `CharField` + --> DJ001.py:52:35 + | +51 | class IncorrectModelWithSimpleAnnotations(models.Model): +52 | charfield: models.CharField = models.CharField(max_length=255, null=True) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +53 | textfield: models.TextField = models.TextField(max_length=255, null=True) +54 | slugfield: models.SlugField = models.SlugField(max_length=255, null=True) + | + +DJ001 Avoid using `null=True` on string-based fields such as `TextField` + --> DJ001.py:53:35 + | +51 | class IncorrectModelWithSimpleAnnotations(models.Model): +52 | charfield: models.CharField = models.CharField(max_length=255, null=True) +53 | textfield: models.TextField = models.TextField(max_length=255, null=True) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +54 | slugfield: models.SlugField = models.SlugField(max_length=255, null=True) + | + +DJ001 Avoid using `null=True` on string-based fields such as `SlugField` + --> DJ001.py:54:35 + | +52 | charfield: models.CharField = models.CharField(max_length=255, null=True) +53 | textfield: models.TextField = models.TextField(max_length=255, null=True) +54 | slugfield: models.SlugField = models.SlugField(max_length=255, null=True) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | From 3c7f56f5825d5d6c1971bf76d453752a722c5e58 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 27 Oct 2025 11:34:29 +0100 Subject: [PATCH 052/188] Restore `indent.py` (#21094) --- .../test/fixtures/ruff/fmt_on_off/indent.py | 55 +++++ .../format@fmt_on_off__indent.py.snap | 226 ++++++++++++++++++ 2 files changed, 281 insertions(+) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/indent.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/indent.py index e69de29bb2..c0cb6c1849 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/indent.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/indent.py @@ -0,0 +1,55 @@ +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: + pass + more + # fmt: on + + formatted + + def test(): + a_small_indent + # fmt: off +# fix under-indented comments + (or_the_inner_expression + +expressions + ) + + if True: + pass + # fmt: on + + +# fmt: off +def test(): + pass + + # It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment + # of the `test` function if the "proper" indentation is larger than 2 spaces. + # fmt: on + +disabled + formatting; + +# fmt: on + +formatted; + +def test(): + pass + # fmt: off + """A multiline strings + that should not get formatted""" + + "A single quoted multiline \ + string" + + disabled + formatting; + +# fmt: on + +formatted; diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap index 3a68d9ceae..b1c58a7f63 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap @@ -4,6 +4,61 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off --- ## Input ```python +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: + pass + more + # fmt: on + + formatted + + def test(): + a_small_indent + # fmt: off +# fix under-indented comments + (or_the_inner_expression + +expressions + ) + + if True: + pass + # fmt: on + + +# fmt: off +def test(): + pass + + # It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment + # of the `test` function if the "proper" indentation is larger than 2 spaces. + # fmt: on + +disabled + formatting; + +# fmt: on + +formatted; + +def test(): + pass + # fmt: off + """A multiline strings + that should not get formatted""" + + "A single quoted multiline \ + string" + + disabled + formatting; + +# fmt: on + +formatted; ``` ## Outputs @@ -23,6 +78,63 @@ source_type = Python ``` ```python +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: + pass + more + # fmt: on + + formatted + + def test(): + a_small_indent + # fmt: off + # fix under-indented comments + (or_the_inner_expression + +expressions + ) + + if True: + pass + # fmt: on + + +# fmt: off +def test(): + pass + + # It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment + # of the `test` function if the "proper" indentation is larger than 2 spaces. + # fmt: on + +disabled + formatting; + +# fmt: on + +formatted + + +def test(): + pass + # fmt: off + """A multiline strings + that should not get formatted""" + + "A single quoted multiline \ + string" + + disabled + formatting; + + +# fmt: on + +formatted ``` @@ -42,6 +154,63 @@ source_type = Python ``` ```python +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: + pass + more + # fmt: on + + formatted + + def test(): + a_small_indent + # fmt: off + # fix under-indented comments + (or_the_inner_expression + +expressions + ) + + if True: + pass + # fmt: on + + +# fmt: off +def test(): + pass + + # It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment + # of the `test` function if the "proper" indentation is larger than 2 spaces. + # fmt: on + +disabled + formatting; + +# fmt: on + +formatted + + +def test(): + pass + # fmt: off + """A multiline strings + that should not get formatted""" + + "A single quoted multiline \ + string" + + disabled + formatting; + + +# fmt: on + +formatted ``` @@ -61,4 +230,61 @@ source_type = Python ``` ```python +def test(): + # fmt: off + a_very_small_indent + ( +not_fixed + ) + + if True: + pass + more + # fmt: on + + formatted + + def test(): + a_small_indent + # fmt: off + # fix under-indented comments + (or_the_inner_expression + +expressions + ) + + if True: + pass + # fmt: on + + +# fmt: off +def test(): + pass + + # It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment + # of the `test` function if the "proper" indentation is larger than 2 spaces. + # fmt: on + +disabled + formatting; + +# fmt: on + +formatted + + +def test(): + pass + # fmt: off + """A multiline strings + that should not get formatted""" + + "A single quoted multiline \ + string" + + disabled + formatting; + + +# fmt: on + +formatted ``` From db0e921db1466f8d23e28c27eaba19983a4367f0 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 27 Oct 2025 11:19:12 +0000 Subject: [PATCH 053/188] [ty] Fix bug where ty would think all types had an `__mro__` attribute (#20995) --- .../completion-evaluation-tasks.csv | 2 +- crates/ty_ide/src/completion.rs | 8 +- .../resources/mdtest/annotations/annotated.md | 8 +- .../resources/mdtest/annotations/any.md | 3 +- .../annotations/stdlib_typing_aliases.md | 37 +- .../annotations/unsupported_special_forms.md | 3 +- .../resources/mdtest/attributes.md | 9 +- .../resources/mdtest/class/super.md | 9 +- .../resources/mdtest/classes.md | 4 +- .../mdtest/dataclasses/dataclasses.md | 4 +- .../mdtest/generics/pep695/classes.md | 30 +- .../resources/mdtest/import/errors.md | 9 +- .../resources/mdtest/loops/for.md | 6 +- .../resources/mdtest/mro.md | 220 ++++++---- .../resources/mdtest/named_tuple.md | 11 +- .../resources/mdtest/protocols.md | 43 +- ...__bases__`_includes…_(d2532518c44112c8).snap | 44 +- ...__bases__`_lists_wi…_(ea7ebc83ec359b54).snap | 414 +++++++++--------- ...licit_Super_Objec…_(b753048091f275c0).snap | 301 ++++++------- .../resources/mdtest/stubs/class.md | 4 +- .../resources/mdtest/subscript/tuple.md | 24 +- .../resources/mdtest/ty_extensions.md | 6 +- .../resources/mdtest/type_of/basic.md | 4 +- .../mdtest/type_of/typing_dot_Type.md | 3 +- .../mdtest/type_qualifiers/classvar.md | 5 +- .../resources/mdtest/type_qualifiers/final.md | 5 +- crates/ty_python_semantic/src/types.rs | 12 +- crates/ty_python_semantic/src/types/class.rs | 5 - .../src/types/class_base.rs | 21 + .../ty_python_semantic/src/types/function.rs | 83 ++++ crates/ty_test/src/matcher.rs | 28 +- .../ty_extensions/ty_extensions.pyi | 14 +- 32 files changed, 780 insertions(+), 599 deletions(-) diff --git a/crates/ty_completion_eval/completion-evaluation-tasks.csv b/crates/ty_completion_eval/completion-evaluation-tasks.csv index cf73a817e1..4c5d3e35b9 100644 --- a/crates/ty_completion_eval/completion-evaluation-tasks.csv +++ b/crates/ty_completion_eval/completion-evaluation-tasks.csv @@ -20,6 +20,6 @@ scope-existing-over-new-import,main.py,0,474 scope-prioritize-closer,main.py,0,2 scope-simple-long-identifier,main.py,0,1 tstring-completions,main.py,0,1 -ty-extensions-lower-stdlib,main.py,0,7 +ty-extensions-lower-stdlib,main.py,0,8 type-var-typing-over-ast,main.py,0,3 type-var-typing-over-ast,main.py,1,270 diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index dc3da14149..e08181e64a 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -1864,7 +1864,7 @@ C. __instancecheck__ :: bound method .__instancecheck__(instance: Any, /) -> bool __itemsize__ :: int __module__ :: str - __mro__ :: tuple[, ] + __mro__ :: tuple[type, ...] __name__ :: str __ne__ :: def __ne__(self, value: object, /) -> bool __new__ :: def __new__(cls) -> Self@__new__ @@ -1933,7 +1933,7 @@ Meta. __instancecheck__ :: def __instancecheck__(self, instance: Any, /) -> bool __itemsize__ :: int __module__ :: str - __mro__ :: tuple[, , ] + __mro__ :: tuple[type, ...] __name__ :: str __ne__ :: def __ne__(self, value: object, /) -> bool __or__ :: def __or__[Self](self: Self@__or__, value: Any, /) -> UnionType | Self@__or__ @@ -2061,7 +2061,7 @@ Quux. __instancecheck__ :: bound method .__instancecheck__(instance: Any, /) -> bool __itemsize__ :: int __module__ :: str - __mro__ :: tuple[, ] + __mro__ :: tuple[type, ...] __name__ :: str __ne__ :: def __ne__(self, value: object, /) -> bool __new__ :: def __new__(cls) -> Self@__new__ @@ -2138,7 +2138,7 @@ Answer. __len__ :: bound method .__len__() -> int __members__ :: MappingProxyType[str, Unknown] __module__ :: str - __mro__ :: tuple[, , ] + __mro__ :: tuple[type, ...] __name__ :: str __ne__ :: def __ne__(self, value: object, /) -> bool __new__ :: def __new__(cls, value: object) -> Self@__new__ diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md b/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md index 893fb3c637..5089804119 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md @@ -72,21 +72,23 @@ Inheriting from `Annotated[T, ...]` is equivalent to inheriting from `T` itself. ```py from typing_extensions import Annotated +from ty_extensions import reveal_mro class C(Annotated[int, "foo"]): ... -# TODO: Should be `tuple[Literal[C], Literal[int], Literal[object]]` -reveal_type(C.__mro__) # revealed: tuple[, @Todo(Inference of subscript on special form), ] +# TODO: Should be `(, , )` +reveal_mro(C) # revealed: (, @Todo(Inference of subscript on special form), ) ``` ### Not parameterized ```py from typing_extensions import Annotated +from ty_extensions import reveal_mro # At runtime, this is an error. # error: [invalid-base] class C(Annotated): ... -reveal_type(C.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(C) # revealed: (, Unknown, ) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/any.md b/crates/ty_python_semantic/resources/mdtest/annotations/any.md index e5244051f0..d6baf8cbf9 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/any.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/any.md @@ -56,10 +56,11 @@ allowed, even when the unknown superclass is `int`. The assignment to `y` should ```py from typing import Any +from ty_extensions import reveal_mro class SubclassOfAny(Any): ... -reveal_type(SubclassOfAny.__mro__) # revealed: tuple[, Any, ] +reveal_mro(SubclassOfAny) # revealed: (, Any, ) x: SubclassOfAny = 1 # error: [invalid-assignment] y: int = SubclassOfAny() diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md b/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md index 990fbe33fd..f96be3951b 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md @@ -114,6 +114,7 @@ The aliases can be inherited from. Some of these are still partially or wholly T ```py import typing +from ty_extensions import reveal_mro #################### ### Built-ins @@ -121,23 +122,23 @@ import typing class ListSubclass(typing.List): ... -# revealed: tuple[, , , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(ListSubclass.__mro__) +# revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(ListSubclass) class DictSubclass(typing.Dict): ... -# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(DictSubclass.__mro__) +# revealed: (, , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(DictSubclass) class SetSubclass(typing.Set): ... -# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(SetSubclass.__mro__) +# revealed: (, , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(SetSubclass) class FrozenSetSubclass(typing.FrozenSet): ... -# revealed: tuple[, , , , , , typing.Protocol, typing.Generic, ] -reveal_type(FrozenSetSubclass.__mro__) +# revealed: (, , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(FrozenSetSubclass) #################### ### `collections` @@ -145,26 +146,26 @@ reveal_type(FrozenSetSubclass.__mro__) class ChainMapSubclass(typing.ChainMap): ... -# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(ChainMapSubclass.__mro__) +# revealed: (, , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(ChainMapSubclass) class CounterSubclass(typing.Counter): ... -# revealed: tuple[, , , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(CounterSubclass.__mro__) +# revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(CounterSubclass) class DefaultDictSubclass(typing.DefaultDict): ... -# revealed: tuple[, , , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(DefaultDictSubclass.__mro__) +# revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(DefaultDictSubclass) class DequeSubclass(typing.Deque): ... -# revealed: tuple[, , , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(DequeSubclass.__mro__) +# revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(DequeSubclass) class OrderedDictSubclass(typing.OrderedDict): ... -# revealed: tuple[, , , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(OrderedDictSubclass.__mro__) +# revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(OrderedDictSubclass) ``` 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 8a6f499655..c61a94a8d6 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 @@ -78,6 +78,7 @@ You can't inherit from most of these. `typing.Callable` is an exception. ```py from typing import Callable from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate, Generic +from ty_extensions import reveal_mro class A(Self): ... # error: [invalid-base] class B(Unpack): ... # error: [invalid-base] @@ -87,7 +88,7 @@ class E(Concatenate): ... # error: [invalid-base] class F(Callable): ... class G(Generic): ... # error: [invalid-base] "Cannot inherit from plain `Generic`" -reveal_type(F.__mro__) # revealed: tuple[, @Todo(Support for Callable as a base class), ] +reveal_mro(F) # revealed: (, @Todo(Support for Callable as a base class), ) ``` ## Subscriptability diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 82d7f0c57b..22d7327500 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -1468,6 +1468,8 @@ C.X = "bar" ### Multiple inheritance ```py +from ty_extensions import reveal_mro + class O: ... class F(O): @@ -1481,8 +1483,8 @@ class C(D, F): ... class B(E, D): ... class A(B, C): ... -# revealed: tuple[, , , , , , , ] -reveal_type(A.__mro__) +# revealed: (, , , , , , , ) +reveal_mro(A) # `E` is earlier in the MRO than `F`, so we should use the type of `E.X` reveal_type(A.X) # revealed: Unknown | Literal[42] @@ -1682,6 +1684,7 @@ Similar principles apply if `Any` appears in the middle of an inheritance hierar ```py from typing import ClassVar, Literal +from ty_extensions import reveal_mro class A: x: ClassVar[Literal[1]] = 1 @@ -1689,7 +1692,7 @@ class A: class B(Any): ... class C(B, A): ... -reveal_type(C.__mro__) # revealed: tuple[, , Any, , ] +reveal_mro(C) # revealed: (, , Any, , ) reveal_type(C.x) # revealed: Literal[1] & Any ``` diff --git a/crates/ty_python_semantic/resources/mdtest/class/super.md b/crates/ty_python_semantic/resources/mdtest/class/super.md index d08c5777c1..5d4a4249b7 100644 --- a/crates/ty_python_semantic/resources/mdtest/class/super.md +++ b/crates/ty_python_semantic/resources/mdtest/class/super.md @@ -26,6 +26,7 @@ python-version = "3.12" ```py from __future__ import annotations +from ty_extensions import reveal_mro class A: def a(self): ... @@ -39,7 +40,7 @@ class C(B): def c(self): ... cc: int = 3 -reveal_type(C.__mro__) # revealed: tuple[, , , ] +reveal_mro(C) # revealed: (, , , ) super(C, C()).a super(C, C()).b @@ -420,6 +421,8 @@ When the owner is a union type, `super()` is built separately for each branch, a super objects are combined into a union. ```py +from ty_extensions import reveal_mro + class A: ... class B: @@ -429,8 +432,8 @@ class C(A, B): ... class D(B, A): ... def f(x: C | D): - reveal_type(C.__mro__) # revealed: tuple[, , , ] - reveal_type(D.__mro__) # revealed: tuple[, , , ] + reveal_mro(C) # revealed: (, , , ) + reveal_mro(D) # revealed: (, , , ) s = super(A, x) reveal_type(s) # revealed: , C> | , D> diff --git a/crates/ty_python_semantic/resources/mdtest/classes.md b/crates/ty_python_semantic/resources/mdtest/classes.md index fde58831c2..4d92cb9cdf 100644 --- a/crates/ty_python_semantic/resources/mdtest/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/classes.md @@ -13,6 +13,8 @@ python-version = "3.12" ``` ```py +from ty_extensions import reveal_mro + A = int class G[T]: ... @@ -21,5 +23,5 @@ class C(A, G["B"]): ... A = str B = bytes -reveal_type(C.__mro__) # revealed: tuple[, , , typing.Generic, ] +reveal_mro(C) # revealed: (, , , typing.Generic, ) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 34899b10fc..e7171b6dd4 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -1173,6 +1173,7 @@ and attributes like the MRO are unchanged: ```py from dataclasses import dataclass +from ty_extensions import reveal_mro @dataclass class Person: @@ -1180,7 +1181,8 @@ class Person: age: int | None = None reveal_type(type(Person)) # revealed: -reveal_type(Person.__mro__) # revealed: tuple[, ] +reveal_type(Person.__mro__) # revealed: tuple[type, ...] +reveal_mro(Person) # revealed: (, ) ``` The generated methods have the following signatures: 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 1f52d16d9a..30a9ee88ae 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -11,7 +11,7 @@ At its simplest, to define a generic class using PEP 695 syntax, you add a list `ParamSpec`s or `TypeVarTuple`s after the class name. ```py -from ty_extensions import generic_context +from ty_extensions import generic_context, reveal_mro class SingleTypevar[T]: ... class MultipleTypevars[T, S]: ... @@ -77,13 +77,13 @@ T = TypeVar("T") # error: [invalid-generic-class] "Cannot both inherit from `typing.Generic` and use PEP 695 type variables" class BothGenericSyntaxes[U](Generic[T]): ... -reveal_type(BothGenericSyntaxes.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(BothGenericSyntaxes) # revealed: (, Unknown, ) # error: [invalid-generic-class] "Cannot both inherit from `typing.Generic` and use PEP 695 type variables" # error: [invalid-base] "Cannot inherit from plain `Generic`" class DoublyInvalid[T](Generic): ... -reveal_type(DoublyInvalid.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(DoublyInvalid) # revealed: (, Unknown, ) ``` Generic classes implicitly inherit from `Generic`: @@ -91,26 +91,26 @@ Generic classes implicitly inherit from `Generic`: ```py class Foo[T]: ... -# revealed: tuple[, typing.Generic, ] -reveal_type(Foo.__mro__) -# revealed: tuple[, typing.Generic, ] -reveal_type(Foo[int].__mro__) +# revealed: (, typing.Generic, ) +reveal_mro(Foo) +# revealed: (, typing.Generic, ) +reveal_mro(Foo[int]) class A: ... class Bar[T](A): ... -# revealed: tuple[, , typing.Generic, ] -reveal_type(Bar.__mro__) -# revealed: tuple[, , typing.Generic, ] -reveal_type(Bar[int].__mro__) +# revealed: (, , typing.Generic, ) +reveal_mro(Bar) +# revealed: (, , typing.Generic, ) +reveal_mro(Bar[int]) class B: ... class Baz[T](A, B): ... -# revealed: tuple[, , , typing.Generic, ] -reveal_type(Baz.__mro__) -# revealed: tuple[, , , typing.Generic, ] -reveal_type(Baz[int].__mro__) +# revealed: (, , , typing.Generic, ) +reveal_mro(Baz) +# revealed: (, , , typing.Generic, ) +reveal_mro(Baz[int]) ``` ## Specializing generic classes explicitly diff --git a/crates/ty_python_semantic/resources/mdtest/import/errors.md b/crates/ty_python_semantic/resources/mdtest/import/errors.md index 14e785613b..41b311370a 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/errors.md +++ b/crates/ty_python_semantic/resources/mdtest/import/errors.md @@ -67,22 +67,25 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]" `a.py`: ```py +from ty_extensions import reveal_mro + class A: ... -reveal_type(A.__mro__) # revealed: tuple[, ] +reveal_mro(A) # revealed: (, ) import b class C(b.B): ... -reveal_type(C.__mro__) # revealed: tuple[, , , ] +reveal_mro(C) # revealed: (, , , ) ``` `b.py`: ```py +from ty_extensions import reveal_mro from a import A class B(A): ... -reveal_type(B.__mro__) # revealed: tuple[, , ] +reveal_mro(B) # revealed: (, , ) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md index cbec5378fc..ed51e51c56 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -798,11 +798,11 @@ A class literal can be iterated over if it has `Any` or `Unknown` in its MRO, si ```py from unresolved_module import SomethingUnknown # error: [unresolved-import] from typing import Any, Iterable -from ty_extensions import static_assert, is_assignable_to, TypeOf, Unknown +from ty_extensions import static_assert, is_assignable_to, TypeOf, Unknown, reveal_mro class Foo(SomethingUnknown): ... -reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Foo) # revealed: (, Unknown, ) # TODO: these should pass static_assert(is_assignable_to(TypeOf[Foo], Iterable[Unknown])) # error: [static-assert-error] @@ -815,7 +815,7 @@ for x in Foo: class Bar(Any): ... -reveal_type(Bar.__mro__) # revealed: tuple[, Any, ] +reveal_mro(Bar) # revealed: (, Any, ) # TODO: these should pass static_assert(is_assignable_to(TypeOf[Bar], Iterable[Any])) # error: [static-assert-error] diff --git a/crates/ty_python_semantic/resources/mdtest/mro.md b/crates/ty_python_semantic/resources/mdtest/mro.md index e27bc761dd..da9a40b4a7 100644 --- a/crates/ty_python_semantic/resources/mdtest/mro.md +++ b/crates/ty_python_semantic/resources/mdtest/mro.md @@ -1,55 +1,74 @@ # Method Resolution Order tests -Tests that assert that we can infer the correct type for a class's `__mro__` attribute. +Tests that assert that we can infer the correct MRO for a class. -This attribute is rarely accessed directly at runtime. However, it's extremely important for *us* to -know the precise possible values of a class's Method Resolution Order, or we won't be able to infer -the correct type of attributes accessed from instances. +It's extremely important for us to know the precise possible values of a class's Method Resolution +Order, or we won't be able to infer the correct type of attributes accessed from instances. For documentation on method resolution orders, see: - - +At runtime, the MRO for a class can be inspected using the `__mro__` attribute. However, rather than +special-casing inference of that attribute, we allow our inferred MRO of a class to be introspected +using the `ty_extensions.reveal_mro` function. This is because the MRO ty infers for a class will +often be different than a class's "real MRO" at runtime. This is often deliberate and desirable, but +would be confusing to users. For example, typeshed pretends that builtin sequences such as `tuple` +and `list` inherit from `collections.abc.Sequence`, resulting in a much longer inferred MRO for +these classes than what they actually have at runtime. Other differences to "real MROs" at runtime +include the facts that ty's inferred MRO will often include non-class elements, such as generic +aliases, `Any` and `Unknown`. + ## No bases ```py +from ty_extensions import reveal_mro + class C: ... -reveal_type(C.__mro__) # revealed: tuple[, ] +reveal_mro(C) # revealed: (, ) ``` ## The special case: `object` itself ```py -reveal_type(object.__mro__) # revealed: tuple[] +from ty_extensions import reveal_mro + +reveal_mro(object) # revealed: (,) ``` ## Explicit inheritance from `object` ```py +from ty_extensions import reveal_mro + class C(object): ... -reveal_type(C.__mro__) # revealed: tuple[, ] +reveal_mro(C) # revealed: (, ) ``` ## Explicit inheritance from non-`object` single base ```py +from ty_extensions import reveal_mro + class A: ... class B(A): ... -reveal_type(B.__mro__) # revealed: tuple[, , ] +reveal_mro(B) # revealed: (, , ) ``` ## Linearization of multiple bases ```py +from ty_extensions import reveal_mro + class A: ... class B: ... class C(A, B): ... -reveal_type(C.__mro__) # revealed: tuple[, , , ] +reveal_mro(C) # revealed: (, , , ) ``` ## Complex diamond inheritance (1) @@ -57,14 +76,16 @@ reveal_type(C.__mro__) # revealed: tuple[, , , This is "ex_2" from ```py +from ty_extensions import reveal_mro + class O: ... class X(O): ... class Y(O): ... class A(X, Y): ... class B(Y, X): ... -reveal_type(A.__mro__) # revealed: tuple[, , , , ] -reveal_type(B.__mro__) # revealed: tuple[, , , , ] +reveal_mro(A) # revealed: (, , , , ) +reveal_mro(B) # revealed: (, , , , ) ``` ## Complex diamond inheritance (2) @@ -72,6 +93,8 @@ reveal_type(B.__mro__) # revealed: tuple[, , , This is "ex_5" from ```py +from ty_extensions import reveal_mro + class O: ... class F(O): ... class E(O): ... @@ -80,12 +103,12 @@ class C(D, F): ... class B(D, E): ... class A(B, C): ... -# revealed: tuple[, , , , ] -reveal_type(C.__mro__) -# revealed: tuple[, , , , ] -reveal_type(B.__mro__) -# revealed: tuple[, , , , , , , ] -reveal_type(A.__mro__) +# revealed: (, , , , ) +reveal_mro(C) +# revealed: (, , , , ) +reveal_mro(B) +# revealed: (, , , , , , , ) +reveal_mro(A) ``` ## Complex diamond inheritance (3) @@ -93,6 +116,8 @@ reveal_type(A.__mro__) This is "ex_6" from ```py +from ty_extensions import reveal_mro + class O: ... class F(O): ... class E(O): ... @@ -101,12 +126,12 @@ class C(D, F): ... class B(E, D): ... class A(B, C): ... -# revealed: tuple[, , , , ] -reveal_type(C.__mro__) -# revealed: tuple[, , , , ] -reveal_type(B.__mro__) -# revealed: tuple[, , , , , , , ] -reveal_type(A.__mro__) +# revealed: (, , , , ) +reveal_mro(C) +# revealed: (, , , , ) +reveal_mro(B) +# revealed: (, , , , , , , ) +reveal_mro(A) ``` ## Complex diamond inheritance (4) @@ -114,6 +139,8 @@ reveal_type(A.__mro__) This is "ex_9" from ```py +from ty_extensions import reveal_mro + class O: ... class A(O): ... class B(O): ... @@ -125,19 +152,20 @@ class K2(D, B, E): ... class K3(D, A): ... class Z(K1, K2, K3): ... -# revealed: tuple[, , , , , ] -reveal_type(K1.__mro__) -# revealed: tuple[, , , , , ] -reveal_type(K2.__mro__) -# revealed: tuple[, , , , ] -reveal_type(K3.__mro__) -# revealed: tuple[, , , , , , , , , , ] -reveal_type(Z.__mro__) +# revealed: (, , , , , ) +reveal_mro(K1) +# revealed: (, , , , , ) +reveal_mro(K2) +# revealed: (, , , , ) +reveal_mro(K3) +# revealed: (, , , , , , , , , , ) +reveal_mro(Z) ``` ## Inheritance from `Unknown` ```py +from ty_extensions import reveal_mro from does_not_exist import DoesNotExist # error: [unresolved-import] class A(DoesNotExist): ... @@ -147,11 +175,11 @@ class D(A, B, C): ... class E(B, C): ... class F(E, A): ... -reveal_type(A.__mro__) # revealed: tuple[, Unknown, ] -reveal_type(D.__mro__) # revealed: tuple[, , Unknown, , , ] -reveal_type(E.__mro__) # revealed: tuple[, , , ] -# revealed: tuple[, , , , , Unknown, ] -reveal_type(F.__mro__) +reveal_mro(A) # revealed: (, Unknown, ) +reveal_mro(D) # revealed: (, , Unknown, , , ) +reveal_mro(E) # revealed: (, , , ) +# revealed: (, , , , , Unknown, ) +reveal_mro(F) ``` ## Inheritance with intersections that include `Unknown` @@ -160,23 +188,22 @@ An intersection that includes `Unknown` or `Any` is permitted as long as the int disjoint from `type`. ```py +from ty_extensions import reveal_mro from does_not_exist import DoesNotExist # error: [unresolved-import] reveal_type(DoesNotExist) # revealed: Unknown if hasattr(DoesNotExist, "__mro__"): - # TODO: this should be `Unknown & ` or similar - # (The second part of the intersection is incorrectly simplified to `object` due to https://github.com/astral-sh/ty/issues/986) - reveal_type(DoesNotExist) # revealed: Unknown + reveal_type(DoesNotExist) # revealed: Unknown & class Foo(DoesNotExist): ... # no error! - reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + reveal_mro(Foo) # revealed: (, Unknown, ) if not isinstance(DoesNotExist, type): reveal_type(DoesNotExist) # revealed: Unknown & ~type class Foo(DoesNotExist): ... # error: [unsupported-base] - reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + reveal_mro(Foo) # revealed: (, Unknown, ) ``` ## Inheritance from `type[Any]` and `type[Unknown]` @@ -186,14 +213,14 @@ guarantee: ```py from typing import Any -from ty_extensions import Unknown, Intersection +from ty_extensions import Unknown, Intersection, reveal_mro def f(x: type[Any], y: Intersection[Unknown, type[Any]]): class Foo(x): ... - reveal_type(Foo.__mro__) # revealed: tuple[, Any, ] + reveal_mro(Foo) # revealed: (, Any, ) class Bar(y): ... - reveal_type(Bar.__mro__) # revealed: tuple[, Unknown, ] + reveal_mro(Bar) # revealed: (, Unknown, ) ``` ## `__bases__` lists that cause errors at runtime @@ -202,14 +229,16 @@ If the class's `__bases__` cause an exception to be raised at runtime and theref creation to fail, we infer the class's `__mro__` as being `[, Unknown, object]`: ```py +from ty_extensions import reveal_mro + # error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[, ]`" class Foo(object, int): ... -reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Foo) # revealed: (, Unknown, ) class Bar(Foo): ... -reveal_type(Bar.__mro__) # revealed: tuple[, , Unknown, ] +reveal_mro(Bar) # revealed: (, , Unknown, ) # This is the `TypeError` at the bottom of "ex_2" # in the examples at @@ -219,17 +248,17 @@ class Y(O): ... class A(X, Y): ... class B(Y, X): ... -reveal_type(A.__mro__) # revealed: tuple[, , , , ] -reveal_type(B.__mro__) # revealed: tuple[, , , , ] +reveal_mro(A) # revealed: (, , , , ) +reveal_mro(B) # revealed: (, , , , ) # error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Z` with bases list `[, ]`" class Z(A, B): ... -reveal_type(Z.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Z) # revealed: (, Unknown, ) class AA(Z): ... -reveal_type(AA.__mro__) # revealed: tuple[, , Unknown, ] +reveal_mro(AA) # revealed: (, , Unknown, ) ``` ## `__bases__` includes a `Union` @@ -241,6 +270,8 @@ find a union type in a class's bases, we infer the class's `__mro__` as being `[, Unknown, object]`, the same as for MROs that cause errors at runtime. ```py +from ty_extensions import reveal_mro + def returns_bool() -> bool: return True @@ -257,7 +288,7 @@ reveal_type(x) # revealed: | # error: 11 [unsupported-base] "Unsupported class base with type ` | `" class Foo(x): ... -reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Foo) # revealed: (, Unknown, ) ``` ## `__bases__` is a union of a dynamic type and valid bases @@ -268,6 +299,7 @@ diagnostic, and we use the dynamic type as a base to prevent further downstream ```py from typing import Any +from ty_extensions import reveal_mro def _(flag: bool, any: Any): if flag: @@ -276,12 +308,14 @@ def _(flag: bool, any: Any): class Base: ... class Foo(Base): ... - reveal_type(Foo.__mro__) # revealed: tuple[, Any, ] + reveal_mro(Foo) # revealed: (, Any, ) ``` ## `__bases__` includes multiple `Union`s ```py +from ty_extensions import reveal_mro + def returns_bool() -> bool: return True @@ -307,12 +341,14 @@ reveal_type(y) # revealed: | # error: 14 [unsupported-base] "Unsupported class base with type ` | `" class Foo(x, y): ... -reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Foo) # revealed: (, Unknown, ) ``` ## `__bases__` lists that cause errors... now with `Union`s ```py +from ty_extensions import reveal_mro + def returns_bool() -> bool: return True @@ -328,11 +364,11 @@ else: # error: 21 [unsupported-base] "Unsupported class base with type ` | `" class PossibleError(foo, X): ... -reveal_type(PossibleError.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(PossibleError) # revealed: (, Unknown, ) class A(X, Y): ... -reveal_type(A.__mro__) # revealed: tuple[, , , , ] +reveal_mro(A) # revealed: (, , , , ) if returns_bool(): class B(X, Y): ... @@ -340,13 +376,13 @@ if returns_bool(): else: class B(Y, X): ... -# revealed: tuple[, , , , ] | tuple[, , , , ] -reveal_type(B.__mro__) +# revealed: (, , , , ) | (, , , , ) +reveal_mro(B) # error: 12 [unsupported-base] "Unsupported class base with type ` | `" class Z(A, B): ... -reveal_type(Z.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Z) # revealed: (, Unknown, ) ``` ## `__bases__` lists that include objects that are not instances of `type` @@ -389,9 +425,11 @@ class BadSub2(Bad2()): ... # error: [invalid-base] ```py +from ty_extensions import reveal_mro + class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`" -reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Foo) # revealed: (, Unknown, ) class Spam: ... class Eggs: ... @@ -413,12 +451,12 @@ class Ham( # fmt: on -reveal_type(Ham.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Ham) # revealed: (, Unknown, ) class Mushrooms: ... class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] -reveal_type(Omelette.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Omelette) # revealed: (, Unknown, ) # fmt: off @@ -494,6 +532,7 @@ however, for gradual types this would break the the dynamic base can usually be materialised to a type that would lead to a resolvable MRO. ```py +from ty_extensions import reveal_mro from unresolvable_module import UnknownBase1, UnknownBase2 # error: [unresolved-import] reveal_type(UnknownBase1) # revealed: Unknown @@ -502,7 +541,7 @@ reveal_type(UnknownBase2) # revealed: Unknown # no error here -- we respect the gradual guarantee: class Foo(UnknownBase1, UnknownBase2): ... -reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Foo) # revealed: (, Unknown, ) ``` However, if there are duplicate class elements, we do emit an error, even if there are also multiple @@ -513,7 +552,7 @@ bases materialize to: # error: [duplicate-base] "Duplicate base class `Foo`" class Bar(UnknownBase1, Foo, UnknownBase2, Foo): ... -reveal_type(Bar.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Bar) # revealed: (, Unknown, ) ``` ## Unrelated objects inferred as `Any`/`Unknown` do not have special `__mro__` attributes @@ -529,25 +568,26 @@ reveal_type(unknown_object.__mro__) # revealed: Unknown ```py from typing import Generic, TypeVar, Iterator +from ty_extensions import reveal_mro T = TypeVar("T") class peekable(Generic[T], Iterator[T]): ... -# revealed: tuple[, , , typing.Protocol, typing.Generic, ] -reveal_type(peekable.__mro__) +# revealed: (, , , typing.Protocol, typing.Generic, ) +reveal_mro(peekable) class peekable2(Iterator[T], Generic[T]): ... -# revealed: tuple[, , , typing.Protocol, typing.Generic, ] -reveal_type(peekable2.__mro__) +# revealed: (, , , typing.Protocol, typing.Generic, ) +reveal_mro(peekable2) class Base: ... class Intermediate(Base, Generic[T]): ... class Sub(Intermediate[T], Base): ... -# revealed: tuple[, , , typing.Generic, ] -reveal_type(Sub.__mro__) +# revealed: (, , , typing.Generic, ) +reveal_mro(Sub) ``` ## Unresolvable MROs involving generics have the original bases reported in the error message, not the resolved bases @@ -569,17 +609,19 @@ class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro] These are invalid, but we need to be able to handle them gracefully without panicking. ```pyi +from ty_extensions import reveal_mro + class Foo(Foo): ... # error: [cyclic-class-definition] reveal_type(Foo) # revealed: -reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Foo) # revealed: (, Unknown, ) class Bar: ... class Baz: ... class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-definition] reveal_type(Boz) # revealed: -reveal_type(Boz.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Boz) # revealed: (, Unknown, ) ``` ## Classes with indirect cycles in their MROs @@ -587,31 +629,37 @@ reveal_type(Boz.__mro__) # revealed: tuple[, Unknown, , Unknown, ] -reveal_type(Bar.__mro__) # revealed: tuple[, Unknown, ] -reveal_type(Baz.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Foo) # revealed: (, Unknown, ) +reveal_mro(Bar) # revealed: (, Unknown, ) +reveal_mro(Baz) # revealed: (, Unknown, ) ``` ## Classes with cycles in their MROs, and multiple inheritance ```pyi +from ty_extensions import reveal_mro + class Spam: ... class Foo(Bar): ... # error: [cyclic-class-definition] class Bar(Baz): ... # error: [cyclic-class-definition] class Baz(Foo, Spam): ... # error: [cyclic-class-definition] -reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] -reveal_type(Bar.__mro__) # revealed: tuple[, Unknown, ] -reveal_type(Baz.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Foo) # revealed: (, Unknown, ) +reveal_mro(Bar) # revealed: (, Unknown, ) +reveal_mro(Baz) # revealed: (, Unknown, ) ``` ## Classes with cycles in their MRO, and a sub-graph ```pyi +from ty_extensions import reveal_mro + class FooCycle(BarCycle): ... # error: [cyclic-class-definition] class Foo: ... class BarCycle(FooCycle): ... # error: [cyclic-class-definition] @@ -622,10 +670,10 @@ class Bar(Foo): ... class Baz(Bar, BarCycle): ... class Spam(Baz): ... -reveal_type(FooCycle.__mro__) # revealed: tuple[, Unknown, ] -reveal_type(BarCycle.__mro__) # revealed: tuple[, Unknown, ] -reveal_type(Baz.__mro__) # revealed: tuple[, Unknown, ] -reveal_type(Spam.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(FooCycle) # revealed: (, Unknown, ) +reveal_mro(BarCycle) # revealed: (, Unknown, ) +reveal_mro(Baz) # revealed: (, Unknown, ) +reveal_mro(Spam) # revealed: (, Unknown, ) ``` ## Other classes with possible cycles @@ -636,20 +684,22 @@ python-version = "3.13" ``` ```pyi +from ty_extensions import reveal_mro + class C(C.a): ... reveal_type(C.__class__) # revealed: -reveal_type(C.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(C) # revealed: (, Unknown, ) class D(D.a): a: D reveal_type(D.__class__) # revealed: -reveal_type(D.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(D) # revealed: (, Unknown, ) class E[T](E.a): ... reveal_type(E.__class__) # revealed: -reveal_type(E.__mro__) # revealed: tuple[, Unknown, typing.Generic, ] +reveal_mro(E) # revealed: (, Unknown, typing.Generic, ) class F[T](F(), F): ... # error: [cyclic-class-definition] reveal_type(F.__class__) # revealed: type[Unknown] -reveal_type(F.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(F) # revealed: (, Unknown, ) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index f8c8a330f2..c49cf1708c 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -9,7 +9,7 @@ name, and not just by its numeric position within the tuple: ```py from typing import NamedTuple -from ty_extensions import static_assert, is_subtype_of, is_assignable_to +from ty_extensions import static_assert, is_subtype_of, is_assignable_to, reveal_mro class Person(NamedTuple): id: int @@ -25,8 +25,8 @@ reveal_type(alice.id) # revealed: int reveal_type(alice.name) # revealed: str reveal_type(alice.age) # revealed: int | None -# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(Person.__mro__) +# revealed: (, , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(Person) static_assert(is_subtype_of(Person, tuple[int, str, int | None])) static_assert(is_subtype_of(Person, tuple[object, ...])) @@ -329,9 +329,8 @@ reveal_type(typing.NamedTuple.__name__) # revealed: str reveal_type(typing.NamedTuple.__qualname__) # revealed: str reveal_type(typing.NamedTuple.__kwdefaults__) # revealed: dict[str, Any] | None -# TODO: this should cause us to emit a diagnostic and reveal `Unknown` (function objects don't have an `__mro__` attribute), -# but the fact that we don't isn't actually a `NamedTuple` bug (https://github.com/astral-sh/ty/issues/986) -reveal_type(typing.NamedTuple.__mro__) # revealed: tuple[, ] +# error: [unresolved-attribute] +reveal_type(typing.NamedTuple.__mro__) # revealed: Unknown ``` By the normal rules, `NamedTuple` and `type[NamedTuple]` should not be valid in type expressions -- diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 2818de99e4..f410da25df 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -25,10 +25,11 @@ A protocol is defined by inheriting from the `Protocol` class, which is annotate ```py from typing import Protocol +from ty_extensions import reveal_mro class MyProtocol(Protocol): ... -reveal_type(MyProtocol.__mro__) # revealed: tuple[, typing.Protocol, typing.Generic, ] +reveal_mro(MyProtocol) # revealed: (, typing.Protocol, typing.Generic, ) ``` Just like for any other class base, it is an error for `Protocol` to appear multiple times in a @@ -37,7 +38,7 @@ class's bases: ```py class Foo(Protocol, Protocol): ... # error: [duplicate-base] -reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +reveal_mro(Foo) # revealed: (, Unknown, ) ``` Protocols can also be generic, either by including `Generic[]` in the bases list, subscripting @@ -64,7 +65,7 @@ class Bar3[T](Protocol[T]): # Note that this class definition *will* actually succeed at runtime, # unlike classes that combine PEP-695 type parameters with inheritance from `Generic[]` -reveal_type(Bar3.__mro__) # revealed: tuple[, typing.Protocol, typing.Generic, ] +reveal_mro(Bar3) # revealed: (, typing.Protocol, typing.Generic, ) ``` It's an error to include both bare `Protocol` and subscripted `Protocol[]` in the bases list @@ -74,8 +75,8 @@ simultaneously: class DuplicateBases(Protocol, Protocol[T]): # error: [duplicate-base] x: T -# revealed: tuple[, Unknown, ] -reveal_type(DuplicateBases.__mro__) +# revealed: (, Unknown, ) +reveal_mro(DuplicateBases) ``` The introspection helper `typing(_extensions).is_protocol` can be used to verify whether a class is @@ -124,8 +125,8 @@ it is not sufficient for it to have `Protocol` in its MRO. ```py class SubclassOfMyProtocol(MyProtocol): ... -# revealed: tuple[, , typing.Protocol, typing.Generic, ] -reveal_type(SubclassOfMyProtocol.__mro__) +# revealed: (, , typing.Protocol, typing.Generic, ) +reveal_mro(SubclassOfMyProtocol) reveal_type(is_protocol(SubclassOfMyProtocol)) # revealed: Literal[False] ``` @@ -143,8 +144,8 @@ class OtherProtocol(Protocol): class ComplexInheritance(SubProtocol, OtherProtocol, Protocol): ... -# revealed: tuple[, , , , typing.Protocol, typing.Generic, ] -reveal_type(ComplexInheritance.__mro__) +# revealed: (, , , , typing.Protocol, typing.Generic, ) +reveal_mro(ComplexInheritance) reveal_type(is_protocol(ComplexInheritance)) # revealed: Literal[True] ``` @@ -156,22 +157,22 @@ or `TypeError` is raised at runtime when the class is created. # error: [invalid-protocol] "Protocol class `Invalid` cannot inherit from non-protocol class `NotAProtocol`" class Invalid(NotAProtocol, Protocol): ... -# revealed: tuple[, , typing.Protocol, typing.Generic, ] -reveal_type(Invalid.__mro__) +# revealed: (, , typing.Protocol, typing.Generic, ) +reveal_mro(Invalid) # error: [invalid-protocol] "Protocol class `AlsoInvalid` cannot inherit from non-protocol class `NotAProtocol`" class AlsoInvalid(MyProtocol, OtherProtocol, NotAProtocol, Protocol): ... -# revealed: tuple[, , , , typing.Protocol, typing.Generic, ] -reveal_type(AlsoInvalid.__mro__) +# revealed: (, , , , typing.Protocol, typing.Generic, ) +reveal_mro(AlsoInvalid) class NotAGenericProtocol[T]: ... # error: [invalid-protocol] "Protocol class `StillInvalid` cannot inherit from non-protocol class `NotAGenericProtocol`" class StillInvalid(NotAGenericProtocol[int], Protocol): ... -# revealed: tuple[, , typing.Protocol, typing.Generic, ] -reveal_type(StillInvalid.__mro__) +# revealed: (, , typing.Protocol, typing.Generic, ) +reveal_mro(StillInvalid) ``` But two exceptions to this rule are `object` and `Generic`: @@ -188,7 +189,7 @@ T = TypeVar("T") # type checkers. class Fine(Protocol, object): ... -reveal_type(Fine.__mro__) # revealed: tuple[, typing.Protocol, typing.Generic, ] +reveal_mro(Fine) # revealed: (, typing.Protocol, typing.Generic, ) class StillFine(Protocol, Generic[T], object): ... class EvenThis[T](Protocol, object): ... @@ -202,8 +203,8 @@ And multiple inheritance from a mix of protocol and non-protocol classes is fine ```py class FineAndDandy(MyProtocol, OtherProtocol, NotAProtocol): ... -# revealed: tuple[, , , typing.Protocol, typing.Generic, , ] -reveal_type(FineAndDandy.__mro__) +# revealed: (, , , typing.Protocol, typing.Generic, , ) +reveal_mro(FineAndDandy) ``` But if `Protocol` is not present in the bases list, the resulting class doesn't count as a protocol @@ -270,11 +271,11 @@ second argument to `issubclass()` at runtime: ```py import abc import typing -from ty_extensions import TypeOf +from ty_extensions import TypeOf, reveal_mro reveal_type(type(Protocol)) # revealed: -# revealed: tuple[, , , ] -reveal_type(type(Protocol).__mro__) +# revealed: (, , , ) +reveal_mro(type(Protocol)) static_assert(is_subtype_of(TypeOf[Protocol], type)) static_assert(is_subtype_of(TypeOf[Protocol], abc.ABCMeta)) static_assert(is_subtype_of(TypeOf[Protocol], typing._ProtocolMeta)) diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_includes…_(d2532518c44112c8).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_includes…_(d2532518c44112c8).snap index ecdcac656c..1990038775 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_includes…_(d2532518c44112c8).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_includes…_(d2532518c44112c8).snap @@ -12,36 +12,38 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md ## mdtest_snippet.py ``` - 1 | def returns_bool() -> bool: - 2 | return True - 3 | - 4 | class A: ... - 5 | class B: ... - 6 | - 7 | if returns_bool(): - 8 | x = A - 9 | else: -10 | x = B -11 | -12 | reveal_type(x) # revealed: | + 1 | from ty_extensions import reveal_mro + 2 | + 3 | def returns_bool() -> bool: + 4 | return True + 5 | + 6 | class A: ... + 7 | class B: ... + 8 | + 9 | if returns_bool(): +10 | x = A +11 | else: +12 | x = B 13 | -14 | # error: 11 [unsupported-base] "Unsupported class base with type ` | `" -15 | class Foo(x): ... -16 | -17 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +14 | reveal_type(x) # revealed: | +15 | +16 | # error: 11 [unsupported-base] "Unsupported class base with type ` | `" +17 | class Foo(x): ... +18 | +19 | reveal_mro(Foo) # revealed: (, Unknown, ) ``` # Diagnostics ``` warning[unsupported-base]: Unsupported class base with type ` | ` - --> src/mdtest_snippet.py:15:11 + --> src/mdtest_snippet.py:17:11 | -14 | # error: 11 [unsupported-base] "Unsupported class base with type ` | `" -15 | class Foo(x): ... +16 | # error: 11 [unsupported-base] "Unsupported class base with type ` | `" +17 | class Foo(x): ... | ^ -16 | -17 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +18 | +19 | reveal_mro(Foo) # revealed: (, Unknown, ) | info: ty cannot resolve a consistent MRO for class `Foo` due to this base info: Only class objects or `Any` are supported as class bases diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_lists_wi…_(ea7ebc83ec359b54).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_lists_wi…_(ea7ebc83ec359b54).snap index 4f216ab4a8..06d3a7ea05 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_lists_wi…_(ea7ebc83ec359b54).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or…_-_`__bases__`_lists_wi…_(ea7ebc83ec359b54).snap @@ -12,109 +12,115 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md ## mdtest_snippet.py ``` - 1 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`" + 1 | from ty_extensions import reveal_mro 2 | - 3 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + 3 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`" 4 | - 5 | class Spam: ... - 6 | class Eggs: ... - 7 | class Bar: ... - 8 | class Baz: ... - 9 | -10 | # fmt: off + 5 | reveal_mro(Foo) # revealed: (, Unknown, ) + 6 | + 7 | class Spam: ... + 8 | class Eggs: ... + 9 | class Bar: ... +10 | class Baz: ... 11 | -12 | # error: [duplicate-base] "Duplicate base class `Spam`" -13 | # error: [duplicate-base] "Duplicate base class `Eggs`" -14 | class Ham( -15 | Spam, -16 | Eggs, -17 | Bar, -18 | Baz, -19 | Spam, -20 | Eggs, -21 | ): ... -22 | -23 | # fmt: on +12 | # fmt: off +13 | +14 | # error: [duplicate-base] "Duplicate base class `Spam`" +15 | # error: [duplicate-base] "Duplicate base class `Eggs`" +16 | class Ham( +17 | Spam, +18 | Eggs, +19 | Bar, +20 | Baz, +21 | Spam, +22 | Eggs, +23 | ): ... 24 | -25 | reveal_type(Ham.__mro__) # revealed: tuple[, Unknown, ] +25 | # fmt: on 26 | -27 | class Mushrooms: ... -28 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] -29 | -30 | reveal_type(Omelette.__mro__) # revealed: tuple[, Unknown, ] +27 | reveal_mro(Ham) # revealed: (, Unknown, ) +28 | +29 | class Mushrooms: ... +30 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] 31 | -32 | # fmt: off +32 | reveal_mro(Omelette) # revealed: (, Unknown, ) 33 | -34 | # error: [duplicate-base] "Duplicate base class `Eggs`" -35 | class VeryEggyOmelette( -36 | Eggs, -37 | Ham, -38 | Spam, -39 | Eggs, -40 | Mushrooms, -41 | Bar, -42 | Eggs, -43 | Baz, +34 | # fmt: off +35 | +36 | # error: [duplicate-base] "Duplicate base class `Eggs`" +37 | class VeryEggyOmelette( +38 | Eggs, +39 | Ham, +40 | Spam, +41 | Eggs, +42 | Mushrooms, +43 | Bar, 44 | Eggs, -45 | ): ... -46 | -47 | # fmt: off -48 | # fmt: off -49 | -50 | class A: ... +45 | Baz, +46 | Eggs, +47 | ): ... +48 | +49 | # fmt: off +50 | # fmt: off 51 | -52 | class B( # type: ignore[duplicate-base] -53 | A, -54 | A, -55 | ): ... -56 | -57 | class C( -58 | A, -59 | A -60 | ): # type: ignore[duplicate-base] -61 | x: int -62 | -63 | # fmt: on -64 | # fmt: off -65 | -66 | # error: [duplicate-base] -67 | class D( -68 | A, -69 | # error: [unused-ignore-comment] -70 | A, # type: ignore[duplicate-base] -71 | ): ... -72 | -73 | # error: [duplicate-base] -74 | class E( -75 | A, -76 | A -77 | ): -78 | # error: [unused-ignore-comment] -79 | x: int # type: ignore[duplicate-base] -80 | -81 | # fmt: on +52 | class A: ... +53 | +54 | class B( # type: ignore[duplicate-base] +55 | A, +56 | A, +57 | ): ... +58 | +59 | class C( +60 | A, +61 | A +62 | ): # type: ignore[duplicate-base] +63 | x: int +64 | +65 | # fmt: on +66 | # fmt: off +67 | +68 | # error: [duplicate-base] +69 | class D( +70 | A, +71 | # error: [unused-ignore-comment] +72 | A, # type: ignore[duplicate-base] +73 | ): ... +74 | +75 | # error: [duplicate-base] +76 | class E( +77 | A, +78 | A +79 | ): +80 | # error: [unused-ignore-comment] +81 | x: int # type: ignore[duplicate-base] +82 | +83 | # fmt: on ``` # Diagnostics ``` error[duplicate-base]: Duplicate base class `str` - --> src/mdtest_snippet.py:1:7 + --> src/mdtest_snippet.py:3:7 | -1 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`" - | ^^^^^^^^^^^^^ +1 | from ty_extensions import reveal_mro 2 | -3 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +3 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`" + | ^^^^^^^^^^^^^ +4 | +5 | reveal_mro(Foo) # revealed: (, Unknown, ) | info: The definition of class `Foo` will raise `TypeError` at runtime - --> src/mdtest_snippet.py:1:11 + --> src/mdtest_snippet.py:3:11 | -1 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`" +1 | from ty_extensions import reveal_mro +2 | +3 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`" | --- ^^^ Class `str` later repeated here | | | Class `str` first included in bases list here -2 | -3 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +4 | +5 | reveal_mro(Foo) # revealed: (, Unknown, ) | info: rule `duplicate-base` is enabled by default @@ -122,37 +128,37 @@ info: rule `duplicate-base` is enabled by default ``` error[duplicate-base]: Duplicate base class `Spam` - --> src/mdtest_snippet.py:14:7 + --> src/mdtest_snippet.py:16:7 | -12 | # error: [duplicate-base] "Duplicate base class `Spam`" -13 | # error: [duplicate-base] "Duplicate base class `Eggs`" -14 | class Ham( +14 | # error: [duplicate-base] "Duplicate base class `Spam`" +15 | # error: [duplicate-base] "Duplicate base class `Eggs`" +16 | class Ham( | _______^ -15 | | Spam, -16 | | Eggs, -17 | | Bar, -18 | | Baz, -19 | | Spam, -20 | | Eggs, -21 | | ): ... +17 | | Spam, +18 | | Eggs, +19 | | Bar, +20 | | Baz, +21 | | Spam, +22 | | Eggs, +23 | | ): ... | |_^ -22 | -23 | # fmt: on +24 | +25 | # fmt: on | info: The definition of class `Ham` will raise `TypeError` at runtime - --> src/mdtest_snippet.py:15:5 + --> src/mdtest_snippet.py:17:5 | -13 | # error: [duplicate-base] "Duplicate base class `Eggs`" -14 | class Ham( -15 | Spam, +15 | # error: [duplicate-base] "Duplicate base class `Eggs`" +16 | class Ham( +17 | Spam, | ---- Class `Spam` first included in bases list here -16 | Eggs, -17 | Bar, -18 | Baz, -19 | Spam, +18 | Eggs, +19 | Bar, +20 | Baz, +21 | Spam, | ^^^^ Class `Spam` later repeated here -20 | Eggs, -21 | ): ... +22 | Eggs, +23 | ): ... | info: rule `duplicate-base` is enabled by default @@ -160,36 +166,36 @@ info: rule `duplicate-base` is enabled by default ``` error[duplicate-base]: Duplicate base class `Eggs` - --> src/mdtest_snippet.py:14:7 + --> src/mdtest_snippet.py:16:7 | -12 | # error: [duplicate-base] "Duplicate base class `Spam`" -13 | # error: [duplicate-base] "Duplicate base class `Eggs`" -14 | class Ham( +14 | # error: [duplicate-base] "Duplicate base class `Spam`" +15 | # error: [duplicate-base] "Duplicate base class `Eggs`" +16 | class Ham( | _______^ -15 | | Spam, -16 | | Eggs, -17 | | Bar, -18 | | Baz, -19 | | Spam, -20 | | Eggs, -21 | | ): ... +17 | | Spam, +18 | | Eggs, +19 | | Bar, +20 | | Baz, +21 | | Spam, +22 | | Eggs, +23 | | ): ... | |_^ -22 | -23 | # fmt: on +24 | +25 | # fmt: on | info: The definition of class `Ham` will raise `TypeError` at runtime - --> src/mdtest_snippet.py:16:5 + --> src/mdtest_snippet.py:18:5 | -14 | class Ham( -15 | Spam, -16 | Eggs, +16 | class Ham( +17 | Spam, +18 | Eggs, | ---- Class `Eggs` first included in bases list here -17 | Bar, -18 | Baz, -19 | Spam, -20 | Eggs, +19 | Bar, +20 | Baz, +21 | Spam, +22 | Eggs, | ^^^^ Class `Eggs` later repeated here -21 | ): ... +23 | ): ... | info: rule `duplicate-base` is enabled by default @@ -197,24 +203,24 @@ info: rule `duplicate-base` is enabled by default ``` error[duplicate-base]: Duplicate base class `Mushrooms` - --> src/mdtest_snippet.py:28:7 + --> src/mdtest_snippet.py:30:7 | -27 | class Mushrooms: ... -28 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] +29 | class Mushrooms: ... +30 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -29 | -30 | reveal_type(Omelette.__mro__) # revealed: tuple[, Unknown, ] +31 | +32 | reveal_mro(Omelette) # revealed: (, Unknown, ) | info: The definition of class `Omelette` will raise `TypeError` at runtime - --> src/mdtest_snippet.py:28:28 + --> src/mdtest_snippet.py:30:28 | -27 | class Mushrooms: ... -28 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] +29 | class Mushrooms: ... +30 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] | --------- ^^^^^^^^^ Class `Mushrooms` later repeated here | | | Class `Mushrooms` first included in bases list here -29 | -30 | reveal_type(Omelette.__mro__) # revealed: tuple[, Unknown, ] +31 | +32 | reveal_mro(Omelette) # revealed: (, Unknown, ) | info: rule `duplicate-base` is enabled by default @@ -222,44 +228,44 @@ info: rule `duplicate-base` is enabled by default ``` error[duplicate-base]: Duplicate base class `Eggs` - --> src/mdtest_snippet.py:35:7 + --> src/mdtest_snippet.py:37:7 | -34 | # error: [duplicate-base] "Duplicate base class `Eggs`" -35 | class VeryEggyOmelette( +36 | # error: [duplicate-base] "Duplicate base class `Eggs`" +37 | class VeryEggyOmelette( | _______^ -36 | | Eggs, -37 | | Ham, -38 | | Spam, -39 | | Eggs, -40 | | Mushrooms, -41 | | Bar, -42 | | Eggs, -43 | | Baz, +38 | | Eggs, +39 | | Ham, +40 | | Spam, +41 | | Eggs, +42 | | Mushrooms, +43 | | Bar, 44 | | Eggs, -45 | | ): ... +45 | | Baz, +46 | | Eggs, +47 | | ): ... | |_^ -46 | -47 | # fmt: off +48 | +49 | # fmt: off | info: The definition of class `VeryEggyOmelette` will raise `TypeError` at runtime - --> src/mdtest_snippet.py:36:5 + --> src/mdtest_snippet.py:38:5 | -34 | # error: [duplicate-base] "Duplicate base class `Eggs`" -35 | class VeryEggyOmelette( -36 | Eggs, +36 | # error: [duplicate-base] "Duplicate base class `Eggs`" +37 | class VeryEggyOmelette( +38 | Eggs, | ---- Class `Eggs` first included in bases list here -37 | Ham, -38 | Spam, -39 | Eggs, +39 | Ham, +40 | Spam, +41 | Eggs, | ^^^^ Class `Eggs` later repeated here -40 | Mushrooms, -41 | Bar, -42 | Eggs, - | ^^^^ Class `Eggs` later repeated here -43 | Baz, +42 | Mushrooms, +43 | Bar, 44 | Eggs, | ^^^^ Class `Eggs` later repeated here -45 | ): ... +45 | Baz, +46 | Eggs, + | ^^^^ Class `Eggs` later repeated here +47 | ): ... | info: rule `duplicate-base` is enabled by default @@ -267,30 +273,30 @@ info: rule `duplicate-base` is enabled by default ``` error[duplicate-base]: Duplicate base class `A` - --> src/mdtest_snippet.py:67:7 + --> src/mdtest_snippet.py:69:7 | -66 | # error: [duplicate-base] -67 | class D( +68 | # error: [duplicate-base] +69 | class D( | _______^ -68 | | A, -69 | | # error: [unused-ignore-comment] -70 | | A, # type: ignore[duplicate-base] -71 | | ): ... +70 | | A, +71 | | # error: [unused-ignore-comment] +72 | | A, # type: ignore[duplicate-base] +73 | | ): ... | |_^ -72 | -73 | # error: [duplicate-base] +74 | +75 | # error: [duplicate-base] | info: The definition of class `D` will raise `TypeError` at runtime - --> src/mdtest_snippet.py:68:5 + --> src/mdtest_snippet.py:70:5 | -66 | # error: [duplicate-base] -67 | class D( -68 | A, +68 | # error: [duplicate-base] +69 | class D( +70 | A, | - Class `A` first included in bases list here -69 | # error: [unused-ignore-comment] -70 | A, # type: ignore[duplicate-base] +71 | # error: [unused-ignore-comment] +72 | A, # type: ignore[duplicate-base] | ^ Class `A` later repeated here -71 | ): ... +73 | ): ... | info: rule `duplicate-base` is enabled by default @@ -298,42 +304,42 @@ info: rule `duplicate-base` is enabled by default ``` info[unused-ignore-comment] - --> src/mdtest_snippet.py:70:9 + --> src/mdtest_snippet.py:72:9 | -68 | A, -69 | # error: [unused-ignore-comment] -70 | A, # type: ignore[duplicate-base] +70 | A, +71 | # error: [unused-ignore-comment] +72 | A, # type: ignore[duplicate-base] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unused blanket `type: ignore` directive -71 | ): ... +73 | ): ... | ``` ``` error[duplicate-base]: Duplicate base class `A` - --> src/mdtest_snippet.py:74:7 + --> src/mdtest_snippet.py:76:7 | -73 | # error: [duplicate-base] -74 | class E( +75 | # error: [duplicate-base] +76 | class E( | _______^ -75 | | A, -76 | | A -77 | | ): +77 | | A, +78 | | A +79 | | ): | |_^ -78 | # error: [unused-ignore-comment] -79 | x: int # type: ignore[duplicate-base] +80 | # error: [unused-ignore-comment] +81 | x: int # type: ignore[duplicate-base] | info: The definition of class `E` will raise `TypeError` at runtime - --> src/mdtest_snippet.py:75:5 + --> src/mdtest_snippet.py:77:5 | -73 | # error: [duplicate-base] -74 | class E( -75 | A, +75 | # error: [duplicate-base] +76 | class E( +77 | A, | - Class `A` first included in bases list here -76 | A +78 | A | ^ Class `A` later repeated here -77 | ): -78 | # error: [unused-ignore-comment] +79 | ): +80 | # error: [unused-ignore-comment] | info: rule `duplicate-base` is enabled by default @@ -341,14 +347,14 @@ info: rule `duplicate-base` is enabled by default ``` info[unused-ignore-comment] - --> src/mdtest_snippet.py:79:13 + --> src/mdtest_snippet.py:81:13 | -77 | ): -78 | # error: [unused-ignore-comment] -79 | x: int # type: ignore[duplicate-base] +79 | ): +80 | # error: [unused-ignore-comment] +81 | x: int # type: ignore[duplicate-base] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unused blanket `type: ignore` directive -80 | -81 | # fmt: on +82 | +83 | # fmt: on | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec…_(b753048091f275c0).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec…_(b753048091f275c0).snap index 873e98e2dc..245c95d394 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec…_(b753048091f275c0).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec…_(b753048091f275c0).snap @@ -13,120 +13,121 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md ``` 1 | from __future__ import annotations - 2 | - 3 | class A: - 4 | def a(self): ... - 5 | aa: int = 1 - 6 | - 7 | class B(A): - 8 | def b(self): ... - 9 | bb: int = 2 - 10 | - 11 | class C(B): - 12 | def c(self): ... - 13 | cc: int = 3 - 14 | - 15 | reveal_type(C.__mro__) # revealed: tuple[, , , ] - 16 | - 17 | super(C, C()).a - 18 | super(C, C()).b - 19 | super(C, C()).c # error: [unresolved-attribute] - 20 | - 21 | super(B, C()).a - 22 | super(B, C()).b # error: [unresolved-attribute] - 23 | super(B, C()).c # error: [unresolved-attribute] - 24 | - 25 | super(A, C()).a # error: [unresolved-attribute] - 26 | super(A, C()).b # error: [unresolved-attribute] - 27 | super(A, C()).c # error: [unresolved-attribute] - 28 | - 29 | reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown - 30 | reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown - 31 | reveal_type(super(C, C()).aa) # revealed: int - 32 | reveal_type(super(C, C()).bb) # revealed: int - 33 | import types - 34 | from typing_extensions import Callable, TypeIs, Literal, TypedDict - 35 | - 36 | def f(): ... - 37 | - 38 | class Foo[T]: - 39 | def method(self): ... - 40 | @property - 41 | def some_property(self): ... - 42 | - 43 | type Alias = int - 44 | - 45 | class SomeTypedDict(TypedDict): - 46 | x: int - 47 | y: bytes - 48 | - 49 | # revealed: , FunctionType> - 50 | reveal_type(super(object, f)) - 51 | # revealed: , WrapperDescriptorType> - 52 | reveal_type(super(object, types.FunctionType.__get__)) - 53 | # revealed: , GenericAlias> - 54 | reveal_type(super(object, Foo[int])) - 55 | # revealed: , _SpecialForm> - 56 | reveal_type(super(object, Literal)) - 57 | # revealed: , TypeAliasType> - 58 | reveal_type(super(object, Alias)) - 59 | # revealed: , MethodType> - 60 | reveal_type(super(object, Foo().method)) - 61 | # revealed: , property> - 62 | reveal_type(super(object, Foo.some_property)) - 63 | - 64 | def g(x: object) -> TypeIs[list[object]]: - 65 | return isinstance(x, list) - 66 | - 67 | def _(x: object, y: SomeTypedDict, z: Callable[[int, str], bool]): - 68 | if hasattr(x, "bar"): - 69 | # revealed: - 70 | reveal_type(x) - 71 | # error: [invalid-super-argument] - 72 | # revealed: Unknown - 73 | reveal_type(super(object, x)) - 74 | - 75 | # error: [invalid-super-argument] - 76 | # revealed: Unknown - 77 | reveal_type(super(object, z)) - 78 | - 79 | is_list = g(x) - 80 | # revealed: TypeIs[list[object] @ x] - 81 | reveal_type(is_list) - 82 | # revealed: , bool> - 83 | reveal_type(super(object, is_list)) - 84 | - 85 | # revealed: , dict[Literal["x", "y"], int | bytes]> - 86 | reveal_type(super(object, y)) - 87 | - 88 | # The first argument to `super()` must be an actual class object; - 89 | # instances of `GenericAlias` are not accepted at runtime: - 90 | # - 91 | # error: [invalid-super-argument] - 92 | # revealed: Unknown - 93 | reveal_type(super(list[int], [])) - 94 | class Super: - 95 | def method(self) -> int: - 96 | return 42 - 97 | - 98 | class Sub(Super): - 99 | def method(self: Sub) -> int: -100 | # revealed: , Sub> -101 | return reveal_type(super(self.__class__, self)).method() + 2 | from ty_extensions import reveal_mro + 3 | + 4 | class A: + 5 | def a(self): ... + 6 | aa: int = 1 + 7 | + 8 | class B(A): + 9 | def b(self): ... + 10 | bb: int = 2 + 11 | + 12 | class C(B): + 13 | def c(self): ... + 14 | cc: int = 3 + 15 | + 16 | reveal_mro(C) # revealed: (, , , ) + 17 | + 18 | super(C, C()).a + 19 | super(C, C()).b + 20 | super(C, C()).c # error: [unresolved-attribute] + 21 | + 22 | super(B, C()).a + 23 | super(B, C()).b # error: [unresolved-attribute] + 24 | super(B, C()).c # error: [unresolved-attribute] + 25 | + 26 | super(A, C()).a # error: [unresolved-attribute] + 27 | super(A, C()).b # error: [unresolved-attribute] + 28 | super(A, C()).c # error: [unresolved-attribute] + 29 | + 30 | reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown + 31 | reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown + 32 | reveal_type(super(C, C()).aa) # revealed: int + 33 | reveal_type(super(C, C()).bb) # revealed: int + 34 | import types + 35 | from typing_extensions import Callable, TypeIs, Literal, TypedDict + 36 | + 37 | def f(): ... + 38 | + 39 | class Foo[T]: + 40 | def method(self): ... + 41 | @property + 42 | def some_property(self): ... + 43 | + 44 | type Alias = int + 45 | + 46 | class SomeTypedDict(TypedDict): + 47 | x: int + 48 | y: bytes + 49 | + 50 | # revealed: , FunctionType> + 51 | reveal_type(super(object, f)) + 52 | # revealed: , WrapperDescriptorType> + 53 | reveal_type(super(object, types.FunctionType.__get__)) + 54 | # revealed: , GenericAlias> + 55 | reveal_type(super(object, Foo[int])) + 56 | # revealed: , _SpecialForm> + 57 | reveal_type(super(object, Literal)) + 58 | # revealed: , TypeAliasType> + 59 | reveal_type(super(object, Alias)) + 60 | # revealed: , MethodType> + 61 | reveal_type(super(object, Foo().method)) + 62 | # revealed: , property> + 63 | reveal_type(super(object, Foo.some_property)) + 64 | + 65 | def g(x: object) -> TypeIs[list[object]]: + 66 | return isinstance(x, list) + 67 | + 68 | def _(x: object, y: SomeTypedDict, z: Callable[[int, str], bool]): + 69 | if hasattr(x, "bar"): + 70 | # revealed: + 71 | reveal_type(x) + 72 | # error: [invalid-super-argument] + 73 | # revealed: Unknown + 74 | reveal_type(super(object, x)) + 75 | + 76 | # error: [invalid-super-argument] + 77 | # revealed: Unknown + 78 | reveal_type(super(object, z)) + 79 | + 80 | is_list = g(x) + 81 | # revealed: TypeIs[list[object] @ x] + 82 | reveal_type(is_list) + 83 | # revealed: , bool> + 84 | reveal_type(super(object, is_list)) + 85 | + 86 | # revealed: , dict[Literal["x", "y"], int | bytes]> + 87 | reveal_type(super(object, y)) + 88 | + 89 | # The first argument to `super()` must be an actual class object; + 90 | # instances of `GenericAlias` are not accepted at runtime: + 91 | # + 92 | # error: [invalid-super-argument] + 93 | # revealed: Unknown + 94 | reveal_type(super(list[int], [])) + 95 | class Super: + 96 | def method(self) -> int: + 97 | return 42 + 98 | + 99 | class Sub(Super): +100 | def method(self: Sub) -> int: +101 | # revealed: , Sub> +102 | return reveal_type(super(self.__class__, self)).method() ``` # Diagnostics ``` error[unresolved-attribute]: Object of type `, C>` has no attribute `c` - --> src/mdtest_snippet.py:19:1 + --> src/mdtest_snippet.py:20:1 | -17 | super(C, C()).a -18 | super(C, C()).b -19 | super(C, C()).c # error: [unresolved-attribute] +18 | super(C, C()).a +19 | super(C, C()).b +20 | super(C, C()).c # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ -20 | -21 | super(B, C()).a +21 | +22 | super(B, C()).a | info: rule `unresolved-attribute` is enabled by default @@ -134,12 +135,12 @@ info: rule `unresolved-attribute` is enabled by default ``` error[unresolved-attribute]: Object of type `, C>` has no attribute `b` - --> src/mdtest_snippet.py:22:1 + --> src/mdtest_snippet.py:23:1 | -21 | super(B, C()).a -22 | super(B, C()).b # error: [unresolved-attribute] +22 | super(B, C()).a +23 | super(B, C()).b # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ -23 | super(B, C()).c # error: [unresolved-attribute] +24 | super(B, C()).c # error: [unresolved-attribute] | info: rule `unresolved-attribute` is enabled by default @@ -147,14 +148,14 @@ info: rule `unresolved-attribute` is enabled by default ``` error[unresolved-attribute]: Object of type `, C>` has no attribute `c` - --> src/mdtest_snippet.py:23:1 + --> src/mdtest_snippet.py:24:1 | -21 | super(B, C()).a -22 | super(B, C()).b # error: [unresolved-attribute] -23 | super(B, C()).c # error: [unresolved-attribute] +22 | super(B, C()).a +23 | super(B, C()).b # error: [unresolved-attribute] +24 | super(B, C()).c # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ -24 | -25 | super(A, C()).a # error: [unresolved-attribute] +25 | +26 | super(A, C()).a # error: [unresolved-attribute] | info: rule `unresolved-attribute` is enabled by default @@ -162,14 +163,14 @@ info: rule `unresolved-attribute` is enabled by default ``` error[unresolved-attribute]: Object of type `, C>` has no attribute `a` - --> src/mdtest_snippet.py:25:1 + --> src/mdtest_snippet.py:26:1 | -23 | super(B, C()).c # error: [unresolved-attribute] -24 | -25 | super(A, C()).a # error: [unresolved-attribute] +24 | super(B, C()).c # error: [unresolved-attribute] +25 | +26 | super(A, C()).a # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ -26 | super(A, C()).b # error: [unresolved-attribute] -27 | super(A, C()).c # error: [unresolved-attribute] +27 | super(A, C()).b # error: [unresolved-attribute] +28 | super(A, C()).c # error: [unresolved-attribute] | info: rule `unresolved-attribute` is enabled by default @@ -177,12 +178,12 @@ info: rule `unresolved-attribute` is enabled by default ``` error[unresolved-attribute]: Object of type `, C>` has no attribute `b` - --> src/mdtest_snippet.py:26:1 + --> src/mdtest_snippet.py:27:1 | -25 | super(A, C()).a # error: [unresolved-attribute] -26 | super(A, C()).b # error: [unresolved-attribute] +26 | super(A, C()).a # error: [unresolved-attribute] +27 | super(A, C()).b # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ -27 | super(A, C()).c # error: [unresolved-attribute] +28 | super(A, C()).c # error: [unresolved-attribute] | info: rule `unresolved-attribute` is enabled by default @@ -190,14 +191,14 @@ info: rule `unresolved-attribute` is enabled by default ``` error[unresolved-attribute]: Object of type `, C>` has no attribute `c` - --> src/mdtest_snippet.py:27:1 + --> src/mdtest_snippet.py:28:1 | -25 | super(A, C()).a # error: [unresolved-attribute] -26 | super(A, C()).b # error: [unresolved-attribute] -27 | super(A, C()).c # error: [unresolved-attribute] +26 | super(A, C()).a # error: [unresolved-attribute] +27 | super(A, C()).b # error: [unresolved-attribute] +28 | super(A, C()).c # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ -28 | -29 | reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown +29 | +30 | reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown | info: rule `unresolved-attribute` is enabled by default @@ -205,14 +206,14 @@ info: rule `unresolved-attribute` is enabled by default ``` error[invalid-super-argument]: `` is an abstract/structural type in `super(, )` call - --> src/mdtest_snippet.py:73:21 + --> src/mdtest_snippet.py:74:21 | -71 | # error: [invalid-super-argument] -72 | # revealed: Unknown -73 | reveal_type(super(object, x)) +72 | # error: [invalid-super-argument] +73 | # revealed: Unknown +74 | reveal_type(super(object, x)) | ^^^^^^^^^^^^^^^^ -74 | -75 | # error: [invalid-super-argument] +75 | +76 | # error: [invalid-super-argument] | info: rule `invalid-super-argument` is enabled by default @@ -220,14 +221,14 @@ info: rule `invalid-super-argument` is enabled by default ``` error[invalid-super-argument]: `(int, str, /) -> bool` is an abstract/structural type in `super(, (int, str, /) -> bool)` call - --> src/mdtest_snippet.py:77:17 + --> src/mdtest_snippet.py:78:17 | -75 | # error: [invalid-super-argument] -76 | # revealed: Unknown -77 | reveal_type(super(object, z)) +76 | # error: [invalid-super-argument] +77 | # revealed: Unknown +78 | reveal_type(super(object, z)) | ^^^^^^^^^^^^^^^^ -78 | -79 | is_list = g(x) +79 | +80 | is_list = g(x) | info: rule `invalid-super-argument` is enabled by default @@ -235,14 +236,14 @@ info: rule `invalid-super-argument` is enabled by default ``` error[invalid-super-argument]: `types.GenericAlias` instance `list[int]` is not a valid class - --> src/mdtest_snippet.py:93:13 + --> src/mdtest_snippet.py:94:13 | -91 | # error: [invalid-super-argument] -92 | # revealed: Unknown -93 | reveal_type(super(list[int], [])) +92 | # error: [invalid-super-argument] +93 | # revealed: Unknown +94 | reveal_type(super(list[int], [])) | ^^^^^^^^^^^^^^^^^^^^ -94 | class Super: -95 | def method(self) -> int: +95 | class Super: +96 | def method(self) -> int: | info: rule `invalid-super-argument` is enabled by default diff --git a/crates/ty_python_semantic/resources/mdtest/stubs/class.md b/crates/ty_python_semantic/resources/mdtest/stubs/class.md index 58355ec62b..e76316860a 100644 --- a/crates/ty_python_semantic/resources/mdtest/stubs/class.md +++ b/crates/ty_python_semantic/resources/mdtest/stubs/class.md @@ -11,12 +11,14 @@ In type stubs, classes can reference themselves in their base class definitions. `typeshed`, we have `class str(Sequence[str]): ...`. ```pyi +from ty_extensions import reveal_mro + class Foo[T]: ... class Bar(Foo[Bar]): ... reveal_type(Bar) # revealed: -reveal_type(Bar.__mro__) # revealed: tuple[, , typing.Generic, ] +reveal_mro(Bar) # revealed: (, , typing.Generic, ) ``` ## Access to attributes declared in stubs diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md index 97dfe6439e..8de82f11d6 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md @@ -125,13 +125,14 @@ The stdlib API `os.stat` is a commonly used API that returns an instance of a tu ```py import os import stat +from ty_extensions import reveal_mro reveal_type(os.stat("my_file.txt")) # revealed: stat_result reveal_type(os.stat("my_file.txt")[stat.ST_MODE]) # revealed: int reveal_type(os.stat("my_file.txt")[stat.ST_ATIME]) # revealed: int | float -# revealed: tuple[, , , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(os.stat_result.__mro__) +# revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(os.stat_result) # There are no specific overloads for the `float` elements in `os.stat_result`, # because the fallback `(self, index: SupportsIndex, /) -> int | float` overload @@ -336,15 +337,17 @@ python-version = "3.9" ``` ```py +from ty_extensions import reveal_mro + class A(tuple[int, str]): ... -# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(A.__mro__) +# revealed: (, , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(A) class C(tuple): ... -# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(C.__mro__) +# revealed: (, , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(C) ``` ## `typing.Tuple` @@ -376,16 +379,17 @@ python-version = "3.9" ```py from typing import Tuple +from ty_extensions import reveal_mro class A(Tuple[int, str]): ... -# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(A.__mro__) +# revealed: (, , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(A) class C(Tuple): ... -# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] -reveal_type(C.__mro__) +# revealed: (, , , , , , , typing.Protocol, typing.Generic, ) +reveal_mro(C) ``` ### Union subscript access diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md index 37a76f24e2..ba88851015 100644 --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md @@ -91,7 +91,7 @@ The `Unknown` type is a special type that we use to represent actually unknown t annotation), as opposed to `Any` which represents an explicitly unknown type. ```py -from ty_extensions import Unknown, static_assert, is_assignable_to +from ty_extensions import Unknown, static_assert, is_assignable_to, reveal_mro static_assert(is_assignable_to(Unknown, int)) static_assert(is_assignable_to(int, Unknown)) @@ -107,8 +107,8 @@ def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None ```py class C(Unknown): ... -# revealed: tuple[, Unknown, ] -reveal_type(C.__mro__) +# revealed: (, Unknown, ) +reveal_mro(C) # error: "Special form `ty_extensions.Unknown` expected no type parameter" u: Unknown[str] 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 159395ce23..80223c37a4 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md @@ -145,10 +145,12 @@ _: type[A, B] ## As a base class ```py +from ty_extensions import reveal_mro + class Foo(type[int]): ... # TODO: should be `tuple[, , ] -reveal_type(Foo.__mro__) # revealed: tuple[, @Todo(GenericAlias instance), ] +reveal_mro(Foo) # revealed: (, @Todo(GenericAlias instance), ) ``` ## Display of generic `type[]` types diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.md b/crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.md index bda56c9384..33ea650090 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.md @@ -23,10 +23,11 @@ not a class. ```py from typing import Type +from ty_extensions import reveal_mro class C(Type): ... # Runtime value: `(C, type, typing.Generic, object)` # TODO: Add `Generic` to the MRO -reveal_type(C.__mro__) # revealed: tuple[, , ] +reveal_mro(C) # revealed: (, , ) ``` 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 95aeb24a11..f697543c1b 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md @@ -123,6 +123,7 @@ python-version = "3.12" ```py from typing import ClassVar +from ty_extensions import reveal_mro # error: [invalid-type-form] "`ClassVar` annotations are only allowed in class-body scopes" x: ClassVar[int] = 1 @@ -155,8 +156,8 @@ def f[T](x: T) -> ClassVar[T]: class Foo(ClassVar[tuple[int]]): ... # TODO: Show `Unknown` instead of `@Todo` type in the MRO; or ignore `ClassVar` and show the MRO as if `ClassVar` was not there -# revealed: tuple[, @Todo(Inference of subscript on special form), ] -reveal_type(Foo.__mro__) +# revealed: (, @Todo(Inference of subscript on special form), ) +reveal_mro(Foo) ``` [`typing.classvar`]: https://docs.python.org/3/library/typing.html#typing.ClassVar 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 ce2879e248..29e3d72ec3 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md @@ -265,6 +265,7 @@ python-version = "3.12" ```py from typing import Final, ClassVar, Annotated +from ty_extensions import reveal_mro LEGAL_A: Final[int] = 1 LEGAL_B: Final = 1 @@ -304,8 +305,8 @@ def f[T](x: T) -> Final[T]: class Foo(Final[tuple[int]]): ... # TODO: Show `Unknown` instead of `@Todo` type in the MRO; or ignore `Final` and show the MRO as if `Final` was not there -# revealed: tuple[, @Todo(Inference of subscript on special form), ] -reveal_type(Foo.__mro__) +# revealed: (, @Todo(Inference of subscript on special form), ) +reveal_mro(Foo) ``` ### Attribute assignment outside `__init__` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 8946861894..f2330fabc3 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4364,14 +4364,6 @@ impl<'db> Type<'db> { } Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { - let class_attr_plain = self.find_name_in_mro_with_policy(db, name_str,policy).expect( - "Calling `find_name_in_mro` on class literals and subclass-of types should always return `Some`", - ); - - if name == "__mro__" { - return class_attr_plain; - } - if let Some(enum_class) = match self { Type::ClassLiteral(literal) => Some(literal), Type::SubclassOf(subclass_of) => subclass_of @@ -4392,6 +4384,10 @@ impl<'db> Type<'db> { } } + let class_attr_plain = self.find_name_in_mro_with_policy(db, name_str,policy).expect( + "Calling `find_name_in_mro` on class literals and subclass-of types should always return `Some`", + ); + let class_attr_fallback = Self::try_call_dunder_get_on_attribute( db, class_attr_plain, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 61c531d17b..51cb3bdc3a 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1981,11 +1981,6 @@ impl<'db> ClassLiteral<'db> { name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - if name == "__mro__" { - let tuple_elements = self.iter_mro(db, specialization); - return Place::bound(Type::heterogeneous_tuple(db, tuple_elements)).into(); - } - self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization)) } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index d22dbd5542..4d43b58d06 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -348,6 +348,27 @@ impl<'db> ClassBase<'db> { } } } + + pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display { + struct ClassBaseDisplay<'db> { + db: &'db dyn Db, + base: ClassBase<'db>, + } + + impl std::fmt::Display for ClassBaseDisplay<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.base { + ClassBase::Dynamic(dynamic) => dynamic.fmt(f), + ClassBase::Class(class) => Type::from(class).display(self.db).fmt(f), + ClassBase::Protocol => f.write_str("typing.Protocol"), + ClassBase::Generic => f.write_str("typing.Generic"), + ClassBase::TypedDict => f.write_str("typing.TypedDict"), + } + } + } + + ClassBaseDisplay { db, base: self } + } } impl<'db> From> for ClassBase<'db> { diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 2b096c0990..87870e61b8 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1319,6 +1319,8 @@ pub enum KnownFunction { HasMember, /// `ty_extensions.reveal_protocol_interface` RevealProtocolInterface, + /// `ty_extensions.reveal_mro` + RevealMro, /// `ty_extensions.range_constraint` RangeConstraint, /// `ty_extensions.negated_range_constraint` @@ -1397,6 +1399,7 @@ impl KnownFunction { | Self::RevealProtocolInterface | Self::RangeConstraint | Self::NegatedRangeConstraint + | Self::RevealMro | Self::AllMembers => module.is_ty_extensions(), Self::ImportModule => module.is_importlib(), @@ -1619,6 +1622,85 @@ impl KnownFunction { } } + KnownFunction::RevealMro => { + let [Some(param_type)] = parameter_types else { + return; + }; + let mut good_argument = true; + let classes = match param_type { + Type::ClassLiteral(class) => vec![ClassType::NonGeneric(*class)], + Type::GenericAlias(generic_alias) => vec![ClassType::Generic(*generic_alias)], + Type::Union(union) => { + let elements = union.elements(db); + let mut classes = Vec::with_capacity(elements.len()); + for element in elements { + match element { + Type::ClassLiteral(class) => { + classes.push(ClassType::NonGeneric(*class)); + } + Type::GenericAlias(generic_alias) => { + classes.push(ClassType::Generic(*generic_alias)); + } + _ => { + good_argument = false; + break; + } + } + } + classes + } + _ => { + good_argument = false; + vec![] + } + }; + if !good_argument { + let Some(builder) = + context.report_lint(&INVALID_ARGUMENT_TYPE, call_expression) + else { + return; + }; + let mut diagnostic = + builder.into_diagnostic("Invalid argument to `reveal_mro`"); + diagnostic.set_primary_message(format_args!( + "Can only pass a class object, generic alias or a union thereof" + )); + return; + } + if let Some(builder) = + context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info) + { + let mut diag = builder.into_diagnostic("Revealed MRO"); + let span = context.span(&call_expression.arguments.args[0]); + let mut message = String::new(); + for (i, class) in classes.iter().enumerate() { + message.push('('); + for class in class.iter_mro(db) { + message.push_str(&class.display(db).to_string()); + // Omit the comma for the last element (which is always `object`) + if class + .into_class() + .is_none_or(|base| !base.is_object(context.db())) + { + message.push_str(", "); + } + } + // If the last element was also the first element + // (i.e., it's a length-1 tuple -- which can only happen if we're revealing + // the MRO for `object` itself), add a trailing comma so that it's still a + // valid tuple display. + if class.is_object(db) { + message.push(','); + } + message.push(')'); + if i < classes.len() - 1 { + message.push_str(" | "); + } + } + diag.annotate(Annotation::primary(span).message(message)); + } + } + KnownFunction::IsInstance | KnownFunction::IsSubclass => { let [Some(first_arg), Some(second_argument)] = parameter_types else { return; @@ -1822,6 +1904,7 @@ pub(crate) mod tests { | KnownFunction::RevealProtocolInterface | KnownFunction::RangeConstraint | KnownFunction::NegatedRangeConstraint + | KnownFunction::RevealMro | KnownFunction::AllMembers => KnownModule::TyExtensions, KnownFunction::ImportModule => KnownModule::ImportLib, diff --git a/crates/ty_test/src/matcher.rs b/crates/ty_test/src/matcher.rs index 8c1baeff52..ce8214e72c 100644 --- a/crates/ty_test/src/matcher.rs +++ b/crates/ty_test/src/matcher.rs @@ -345,32 +345,24 @@ impl Matcher { return false; }; - // reveal_type - if primary_message == "Revealed type" - && primary_annotation == expected_reveal_type_message + // reveal_type, reveal_protocol_interface + if matches!( + primary_message, + "Revealed type" | "Revealed protocol interface" + ) && primary_annotation == expected_reveal_type_message { return true; } - // reveal_protocol_interface - if primary_message == "Revealed protocol interface" - && primary_annotation == expected_reveal_type_message + // reveal_when_assignable_to, reveal_when_subtype_of, reveal_mro + if matches!( + primary_message, + "Assignability holds" | "Subtyping holds" | "Revealed MRO" + ) && primary_annotation == expected_type { return true; } - // reveal_when_assignable_to - if primary_message == "Assignability holds" - && primary_annotation == expected_type - { - return true; - } - - // reveal_when_subtype_of - if primary_message == "Subtyping holds" && primary_annotation == expected_type { - return true; - } - false }; diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi index 6c87eb8160..0fe23b3535 100644 --- a/crates/ty_vendored/ty_extensions/ty_extensions.pyi +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi @@ -1,5 +1,6 @@ # ruff: noqa: PYI021 import sys +import types from collections.abc import Iterable from enum import Enum from typing import ( @@ -115,11 +116,16 @@ def all_members(obj: Any) -> tuple[str, ...]: ... # Returns `True` if the given object has a member with the given name. def has_member(obj: Any, name: str) -> bool: ... +def reveal_protocol_interface(protocol: type) -> None: + """ + Passing a protocol type to this function will cause ty to emit an info-level + diagnostic describing the protocol's interface. -# Passing a protocol type to this function will cause ty to emit an info-level -# diagnostic describing the protocol's interface. Passing a non-protocol type -# will cause ty to emit an error diagnostic. -def reveal_protocol_interface(protocol: type) -> None: ... + Passing a non-protocol type will cause ty to emit an error diagnostic. + """ + +def reveal_mro(cls: type | types.GenericAlias) -> None: + """Reveal the MRO that ty infers for the given class or generic alias.""" # A protocol describing an interface that should be satisfied by all named tuples # created using `typing.NamedTuple` or `collections.namedtuple`. From 116611bd39c83ea91fdb3cdf5243b80a734e67b4 Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 27 Oct 2025 09:52:17 -0500 Subject: [PATCH 054/188] Fix finding keyword range for clause header after statement ending with semicolon (#21067) When formatting clause headers for clauses that are not their own node, like an `else` clause or `finally` clause, we begin searching for the keyword at the end of the previous statement. However, if the previous statement ended in a semicolon this caused a panic because we only expected trivia between the end of the last statement and the keyword. This PR adjusts the starting point of our search for the keyword to begin after the optional semicolon in these cases. Closes #21065 --- .../ruff/range_formatting/clause_header.py | 8 + .../src/statement/clause.rs | 152 ++++++++++++++---- ...at@range_formatting__clause_header.py.snap | 16 +- 3 files changed, 145 insertions(+), 31 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/clause_header.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/clause_header.py index d30c32f11d..7217b91ea7 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/clause_header.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/clause_header.py @@ -42,3 +42,11 @@ def test4( a): if b + c : # trailing clause header comment print("Not formatted" ) + +def test5(): + x = 1 + try: + a; + finally: + b + diff --git a/crates/ruff_python_formatter/src/statement/clause.rs b/crates/ruff_python_formatter/src/statement/clause.rs index 7cc82ca923..a5c172f4f8 100644 --- a/crates/ruff_python_formatter/src/statement/clause.rs +++ b/crates/ruff_python_formatter/src/statement/clause.rs @@ -216,7 +216,11 @@ impl ClauseHeader<'_> { .decorator_list .last() .map_or_else(|| header.start(), Ranged::end); - find_keyword(start_position, SimpleTokenKind::Class, source) + find_keyword( + StartPosition::ClauseStart(start_position), + SimpleTokenKind::Class, + source, + ) } ClauseHeader::Function(header) => { let start_position = header @@ -228,21 +232,39 @@ impl ClauseHeader<'_> { } else { SimpleTokenKind::Def }; - find_keyword(start_position, keyword, source) + find_keyword(StartPosition::ClauseStart(start_position), keyword, source) } - ClauseHeader::If(header) => find_keyword(header.start(), SimpleTokenKind::If, source), + ClauseHeader::If(header) => find_keyword( + StartPosition::clause_start(header), + SimpleTokenKind::If, + source, + ), ClauseHeader::ElifElse(ElifElseClause { test: None, range, .. - }) => find_keyword(range.start(), SimpleTokenKind::Else, source), + }) => find_keyword( + StartPosition::clause_start(range), + SimpleTokenKind::Else, + source, + ), ClauseHeader::ElifElse(ElifElseClause { test: Some(_), range, .. - }) => find_keyword(range.start(), SimpleTokenKind::Elif, source), - ClauseHeader::Try(header) => find_keyword(header.start(), SimpleTokenKind::Try, source), - ClauseHeader::ExceptHandler(header) => { - find_keyword(header.start(), SimpleTokenKind::Except, source) - } + }) => find_keyword( + StartPosition::clause_start(range), + SimpleTokenKind::Elif, + source, + ), + ClauseHeader::Try(header) => find_keyword( + StartPosition::clause_start(header), + SimpleTokenKind::Try, + source, + ), + ClauseHeader::ExceptHandler(header) => find_keyword( + StartPosition::clause_start(header), + SimpleTokenKind::Except, + source, + ), ClauseHeader::TryFinally(header) => { let last_statement = header .orelse @@ -252,25 +274,35 @@ impl ClauseHeader<'_> { .or_else(|| header.body.last().map(AnyNodeRef::from)) .unwrap(); - find_keyword(last_statement.end(), SimpleTokenKind::Finally, source) - } - ClauseHeader::Match(header) => { - find_keyword(header.start(), SimpleTokenKind::Match, source) - } - ClauseHeader::MatchCase(header) => { - find_keyword(header.start(), SimpleTokenKind::Case, source) + find_keyword( + StartPosition::LastStatement(last_statement.end()), + SimpleTokenKind::Finally, + source, + ) } + ClauseHeader::Match(header) => find_keyword( + StartPosition::clause_start(header), + SimpleTokenKind::Match, + source, + ), + ClauseHeader::MatchCase(header) => find_keyword( + StartPosition::clause_start(header), + SimpleTokenKind::Case, + source, + ), ClauseHeader::For(header) => { let keyword = if header.is_async { SimpleTokenKind::Async } else { SimpleTokenKind::For }; - find_keyword(header.start(), keyword, source) - } - ClauseHeader::While(header) => { - find_keyword(header.start(), SimpleTokenKind::While, source) + find_keyword(StartPosition::clause_start(header), keyword, source) } + ClauseHeader::While(header) => find_keyword( + StartPosition::clause_start(header), + SimpleTokenKind::While, + source, + ), ClauseHeader::With(header) => { let keyword = if header.is_async { SimpleTokenKind::Async @@ -278,7 +310,7 @@ impl ClauseHeader<'_> { SimpleTokenKind::With }; - find_keyword(header.start(), keyword, source) + find_keyword(StartPosition::clause_start(header), keyword, source) } ClauseHeader::OrElse(header) => match header { ElseClause::Try(try_stmt) => { @@ -289,12 +321,18 @@ impl ClauseHeader<'_> { .or_else(|| try_stmt.body.last().map(AnyNodeRef::from)) .unwrap(); - find_keyword(last_statement.end(), SimpleTokenKind::Else, source) + find_keyword( + StartPosition::LastStatement(last_statement.end()), + SimpleTokenKind::Else, + source, + ) } ElseClause::For(StmtFor { body, .. }) - | ElseClause::While(StmtWhile { body, .. }) => { - find_keyword(body.last().unwrap().end(), SimpleTokenKind::Else, source) - } + | ElseClause::While(StmtWhile { body, .. }) => find_keyword( + StartPosition::LastStatement(body.last().unwrap().end()), + SimpleTokenKind::Else, + source, + ), }, } } @@ -434,16 +472,41 @@ impl Format> for FormatClauseBody<'_> { } } -/// Finds the range of `keyword` starting the search at `start_position`. Expects only comments and `(` between -/// the `start_position` and the `keyword` token. +/// Finds the range of `keyword` starting the search at `start_position`. +/// +/// If the start position is at the end of the previous statement, the +/// search will skip the optional semi-colon at the end of that statement. +/// Other than this, we expect only trivia between the `start_position` +/// and the keyword. fn find_keyword( - start_position: TextSize, + start_position: StartPosition, keyword: SimpleTokenKind, source: &str, ) -> FormatResult { - let mut tokenizer = SimpleTokenizer::starts_at(start_position, source).skip_trivia(); + let next_token = match start_position { + StartPosition::ClauseStart(text_size) => SimpleTokenizer::starts_at(text_size, source) + .skip_trivia() + .next(), + StartPosition::LastStatement(text_size) => { + let mut tokenizer = SimpleTokenizer::starts_at(text_size, source).skip_trivia(); - match tokenizer.next() { + let mut token = tokenizer.next(); + + // If the last statement ends with a semi-colon, skip it. + if matches!( + token, + Some(SimpleToken { + kind: SimpleTokenKind::Semi, + .. + }) + ) { + token = tokenizer.next(); + } + token + } + }; + + match next_token { Some(token) if token.kind() == keyword => Ok(token.range()), Some(other) => { debug_assert!( @@ -466,6 +529,35 @@ fn find_keyword( } } +/// Offset directly before clause header. +/// +/// Can either be the beginning of the clause header +/// or the end of the last statement preceding the clause. +#[derive(Clone, Copy)] +enum StartPosition { + /// The beginning of a clause header + ClauseStart(TextSize), + /// The end of the last statement in the suite preceding a clause. + /// + /// For example: + /// ```python + /// if cond: + /// a + /// b + /// c; + /// # ...^here + /// else: + /// d + /// ``` + LastStatement(TextSize), +} + +impl StartPosition { + fn clause_start(ranged: impl Ranged) -> Self { + Self::ClauseStart(ranged.start()) + } +} + /// Returns the range of the `:` ending the clause header or `Err` if the colon can't be found. fn colon_range(after_keyword_or_condition: TextSize, source: &str) -> FormatResult { let mut tokenizer = SimpleTokenizer::starts_at(after_keyword_or_condition, source) diff --git a/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__clause_header.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__clause_header.py.snap index 8f16a68d08..07e89d0649 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__clause_header.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__clause_header.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/clause_header.py -snapshot_kind: text --- ## Input ```python @@ -49,6 +48,14 @@ def test4( a): if b + c : # trailing clause header comment print("Not formatted" ) + +def test5(): + x = 1 + try: + a; + finally: + b + ``` ## Output @@ -96,4 +103,11 @@ if a + b: # trailing clause header comment if b + c: # trailing clause header comment print("Not formatted" ) + +def test5(): + x = 1 + try: + a + finally: + b ``` From fffbe5a879af0c1b36e44b65219257bcd7e54483 Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 27 Oct 2025 10:23:36 -0500 Subject: [PATCH 055/188] [`pyflakes`] Revert to stable behavior if imports for module lie in alternate branches for `F401` (#20878) Closes #20839 --- crates/ruff_linter/src/rules/pyflakes/mod.rs | 32 +++++++++ .../src/rules/pyflakes/rules/unused_import.rs | 32 +++++++++ ...yflakes__tests__f401_different_branch.snap | 4 ++ ...es__pyflakes__tests__f401_same_branch.snap | 18 +++++ ...__pyflakes__tests__f401_type_checking.snap | 66 +++++++++++++++++++ crates/ruff_python_semantic/src/model.rs | 42 ++++++++++++ 6 files changed, 194 insertions(+) create mode 100644 crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_different_branch.snap create mode 100644 crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_same_branch.snap create mode 100644 crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_type_checking.snap diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 5f41a19f2a..d290ed38c5 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -528,6 +528,38 @@ mod tests { import a", "f401_use_in_between_imports" )] + #[test_case( + r" + if cond: + import a + import a.b + a.foo() + ", + "f401_same_branch" + )] + #[test_case( + r" + try: + import a.b.c + except ImportError: + import argparse + import a + a.b = argparse.Namespace() + ", + "f401_different_branch" + )] + #[test_case( + r" + import mlflow.pyfunc.loaders.chat_agent + import mlflow.pyfunc.loaders.chat_model + import mlflow.pyfunc.loaders.code_model + from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER + + if IS_PYDANTIC_V2_OR_NEWER: + import mlflow.pyfunc.loaders.responses_agent + ", + "f401_type_checking" + )] fn f401_preview_refined_submodule_handling(contents: &str, snapshot: &str) { let diagnostics = test_contents( &SourceKind::Python(dedent(contents).to_string()), diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs index 37530f1a60..88936e22ab 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs @@ -898,6 +898,10 @@ fn best_match<'a, 'b>( #[inline] fn has_simple_shadowed_bindings(scope: &Scope, id: BindingId, semantic: &SemanticModel) -> bool { + let Some(binding_node) = semantic.binding(id).source else { + return false; + }; + scope.shadowed_bindings(id).enumerate().all(|(i, shadow)| { let shadowed_binding = semantic.binding(shadow); // Bail if one of the shadowed bindings is @@ -912,6 +916,34 @@ fn has_simple_shadowed_bindings(scope: &Scope, id: BindingId, semantic: &Semanti if i > 0 && shadowed_binding.is_used() { return false; } + // We want to allow a situation like this: + // + // ```python + // import a.b + // if TYPE_CHECKING: + // import a.b.c + // ``` + // but bail in a situation like this: + // + // ```python + // try: + // import a.b + // except ImportError: + // import argparse + // import a + // a.b = argparse.Namespace() + // ``` + // + // So we require that all the shadowed bindings dominate the + // last live binding for the import. That is: if the last live + // binding is executed it should imply that all the shadowed + // bindings were executed as well. + if shadowed_binding + .source + .is_none_or(|node_id| !semantic.dominates(node_id, binding_node)) + { + return false; + } matches!( shadowed_binding.kind, BindingKind::Import(_) | BindingKind::SubmoduleImport(_) diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_different_branch.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_different_branch.snap new file mode 100644 index 0000000000..d0b409f39e --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_different_branch.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_same_branch.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_same_branch.snap new file mode 100644 index 0000000000..48c6e3bdad --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_same_branch.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +--- +F401 [*] `a.b` imported but unused + --> f401_preview_submodule.py:4:12 + | +2 | if cond: +3 | import a +4 | import a.b + | ^^^ +5 | a.foo() + | +help: Remove unused import: `a.b` +1 | +2 | if cond: +3 | import a + - import a.b +4 | a.foo() diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_type_checking.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_type_checking.snap new file mode 100644 index 0000000000..61c59e4c8c --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_type_checking.snap @@ -0,0 +1,66 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +--- +F401 [*] `mlflow.pyfunc.loaders.chat_agent` imported but unused + --> f401_preview_submodule.py:2:8 + | +2 | import mlflow.pyfunc.loaders.chat_agent + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +3 | import mlflow.pyfunc.loaders.chat_model +4 | import mlflow.pyfunc.loaders.code_model + | +help: Remove unused import: `mlflow.pyfunc.loaders.chat_agent` +1 | + - import mlflow.pyfunc.loaders.chat_agent +2 | import mlflow.pyfunc.loaders.chat_model +3 | import mlflow.pyfunc.loaders.code_model +4 | from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER + +F401 [*] `mlflow.pyfunc.loaders.chat_model` imported but unused + --> f401_preview_submodule.py:3:8 + | +2 | import mlflow.pyfunc.loaders.chat_agent +3 | import mlflow.pyfunc.loaders.chat_model + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4 | import mlflow.pyfunc.loaders.code_model +5 | from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER + | +help: Remove unused import: `mlflow.pyfunc.loaders.chat_model` +1 | +2 | import mlflow.pyfunc.loaders.chat_agent + - import mlflow.pyfunc.loaders.chat_model +3 | import mlflow.pyfunc.loaders.code_model +4 | from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER +5 | + +F401 [*] `mlflow.pyfunc.loaders.code_model` imported but unused + --> f401_preview_submodule.py:4:8 + | +2 | import mlflow.pyfunc.loaders.chat_agent +3 | import mlflow.pyfunc.loaders.chat_model +4 | import mlflow.pyfunc.loaders.code_model + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +5 | from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER + | +help: Remove unused import: `mlflow.pyfunc.loaders.code_model` +1 | +2 | import mlflow.pyfunc.loaders.chat_agent +3 | import mlflow.pyfunc.loaders.chat_model + - import mlflow.pyfunc.loaders.code_model +4 | from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER +5 | +6 | if IS_PYDANTIC_V2_OR_NEWER: + +F401 [*] `mlflow.pyfunc.loaders.responses_agent` imported but unused + --> f401_preview_submodule.py:8:12 + | +7 | if IS_PYDANTIC_V2_OR_NEWER: +8 | import mlflow.pyfunc.loaders.responses_agent + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Remove unused import: `mlflow.pyfunc.loaders.responses_agent` +5 | from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER +6 | +7 | if IS_PYDANTIC_V2_OR_NEWER: + - import mlflow.pyfunc.loaders.responses_agent +8 + pass diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 4f2dc47162..40ddadc4f8 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -1684,6 +1684,48 @@ impl<'a> SemanticModel<'a> { left == right } + /// Returns `true` if any execution path to `node` passes through `dominator`. + /// + /// More precisely, it returns true if the path of branches leading + /// to `dominator` is a prefix of the path of branches leading to `node`. + /// + /// In this code snippet: + /// + /// ```python + /// if cond: + /// dominator + /// if other_cond: + /// node + /// else: + /// other_node + /// ``` + /// + /// we have that `node` is dominated by `dominator` but that + /// `other_node` is not dominated by `dominator`. + /// + /// This implementation assumes that the statements are in the same scope. + pub fn dominates(&self, dominator: NodeId, node: NodeId) -> bool { + // Collect the branch path for the left statement. + let dominator = self + .nodes + .branch_id(dominator) + .iter() + .flat_map(|branch_id| self.branches.ancestor_ids(*branch_id)) + .collect::>(); + + // Collect the branch path for the right statement. + let node = self + .nodes + .branch_id(node) + .iter() + .flat_map(|branch_id| self.branches.ancestor_ids(*branch_id)) + .collect::>(); + + // Note that the paths are in "reverse" order - + // from most nested to least nested. + node.ends_with(&dominator) + } + /// Returns `true` if the given expression is an unused variable, or consists solely of /// references to other unused variables. This method is conservative in that it considers a /// variable to be "used" if it's shadowed by another variable with usages. From 96b60c11d9efbfbf97faa5394fc3f7cb0a5c40c5 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:04:55 -0400 Subject: [PATCH 056/188] Respect `--output-format` with `--watch` (#21097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary -- Fixes #19550 This PR copies our non-watch diagnostic rendering code into `Printer::write_continuously` in preview mode, allowing it to use whatever output format is passed in. I initially marked this as also fixing #19552, but I guess that's not true currently but will be true once this is stabilized and we can remove the warning. Test Plan -- Existing tests, but I don't think we have any `watch` tests, so some manual testing as well. The default with just `ruff check --watch` is still `concise`, adding just `--preview` still gives the `full` output, and then specifying any other output format works, with JSON as one example: Screenshot 2025-10-27 at 9 21 41 AM --- crates/ruff/src/printer.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/crates/ruff/src/printer.rs b/crates/ruff/src/printer.rs index 5c1b1d0e6a..1de440ac6e 100644 --- a/crates/ruff/src/printer.rs +++ b/crates/ruff/src/printer.rs @@ -9,9 +9,7 @@ use itertools::{Itertools, iterate}; use ruff_linter::linter::FixTable; use serde::Serialize; -use ruff_db::diagnostic::{ - Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics, SecondaryCode, -}; +use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, SecondaryCode}; use ruff_linter::fs::relativize_path; use ruff_linter::logging::LogLevel; use ruff_linter::message::{EmitterContext, render_diagnostics}; @@ -390,21 +388,18 @@ impl Printer { let context = EmitterContext::new(&diagnostics.notebook_indexes); let format = if preview { - DiagnosticFormat::Full + self.format } else { - DiagnosticFormat::Concise + OutputFormat::Concise }; let config = DisplayDiagnosticConfig::default() + .preview(preview) .hide_severity(true) .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()) .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) - .format(format) - .with_fix_applicability(self.unsafe_fixes.required_applicability()); - write!( - writer, - "{}", - DisplayDiagnostics::new(&context, &config, &diagnostics.inner) - )?; + .with_fix_applicability(self.unsafe_fixes.required_applicability()) + .show_fix_diff(preview); + render_diagnostics(writer, format, config, &context, &diagnostics.inner)?; } writer.flush()?; From 7fee62b2dedd658860dbcc32f0e504d99c3f97a1 Mon Sep 17 00:00:00 2001 From: Bhuminjay Soni Date: Tue, 28 Oct 2025 02:48:11 +0530 Subject: [PATCH 057/188] [semantic error tests]: refactor semantic error tests to separate files (#20926) ## Summary This PR refactors semantic error tests in each seperate file ## Test Plan ## CC - @ntBre --------- Signed-off-by: 11happy Co-authored-by: Brent Westbrook --- ...nc_comprehension_outside_async_function.py | 12 + .../duplicate_match_class_attribute.py | 3 + .../semantic_errors/duplicate_match_key.py | 3 + .../duplicate_type_parameter.py | 1 + .../semantic_errors/global_parameter.py | 29 ++ .../semantic_errors/invalid_expression.py | 8 + .../invalid_star_expression.py | 8 + .../irrefutable_case_pattern.py | 11 + .../multiple_case_assignment.py | 5 + .../semantic_errors/rebound_comprehension.py | 1 + .../single_starred_assignment.py | 1 + .../semantic_errors/write_to_debug.py | 7 + crates/ruff_linter/src/linter.rs | 280 +++--------------- ...nction_async_in_sync_okay_on_310_3.10.snap | 4 - ...nction_async_in_sync_okay_on_311_3.11.snap | 4 - ...cFunction_deferred_function_body_3.10.snap | 4 - ..._duplicate_match_class_attribute_3.10.snap | 11 - ...GlobalParameter_global_parameter_3.10.snap | 56 ---- ...sion_walrus_in_return_annotation_3.12.snap | 9 - ...ression_yield_from_in_base_class_3.12.snap | 10 - ...d_expression_yield_in_type_alias_3.12.snap | 9 - ...d_expression_yield_in_type_param_3.12.snap | 9 - ...pression_invalid_star_expression_3.10.snap | 10 - ...sion_invalid_star_expression_for_3.10.snap | 10 - ...on_invalid_star_expression_yield_3.10.snap | 10 - ...irrefutable_case_pattern_capture_3.10.snap | 12 - ...rrefutable_case_pattern_wildcard_3.10.snap | 12 - ...ignment_multiple_case_assignment_3.10.snap | 12 - ...rror_WriteToDebug_write_to_debug_3.10.snap | 9 - ..._write_to_debug_class_type_param_3.12.snap | 10 - ...write_to_debug_in_function_param_3.10.snap | 10 - ...nsion_outside_async_function.py_3.10.snap} | 4 +- ...nsion_outside_async_function.py_3.11.snap} | 0 ...plicate_match_class_attribute.py_3.10.snap | 11 + ...ax_error_duplicate_match_key.py_3.10.snap} | 8 +- ...ror_duplicate_type_parameter.py_3.12.snap} | 2 +- ...syntax_error_global_parameter.py_3.10.snap | 56 ++++ ...ntax_error_invalid_expression.py_3.12.snap | 43 +++ ...error_invalid_star_expression.py_3.10.snap | 30 ++ ...rror_irrefutable_case_pattern.py_3.10.snap | 22 ++ ...rror_multiple_case_assignment.py_3.10.snap | 12 + ..._error_rebound_comprehension.py_3.10.snap} | 2 +- ...or_single_starred_assignment.py_3.10.snap} | 2 +- ...c_syntax_error_write_to_debug.py_3.10.snap | 41 +++ ...c_syntax_error_write_to_debug.py_3.12.snap | 31 ++ 45 files changed, 378 insertions(+), 466 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/async_comprehension_outside_async_function.py create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_match_class_attribute.py create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_match_key.py create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_type_parameter.py create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/global_parameter.py create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_expression.py create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_star_expression.py create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/irrefutable_case_pattern.py create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/multiple_case_assignment.py create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/rebound_comprehension.py create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/single_starred_assignment.py create mode 100644 crates/ruff_linter/resources/test/fixtures/semantic_errors/write_to_debug.py delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_310_3.10.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_311_3.11.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_deferred_function_body_3.10.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchClassAttribute_duplicate_match_class_attribute_3.10.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_GlobalParameter_global_parameter_3.10.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_walrus_in_return_annotation_3.12.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_from_in_base_class_3.12.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_alias_3.12.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_param_3.12.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_3.10.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_for_3.10.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_yield_3.10.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_capture_3.10.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_wildcard_3.10.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_MultipleCaseAssignment_multiple_case_assignment_3.10.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_3.10.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_class_type_param_3.12.snap delete mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_in_function_param_3.10.snap rename crates/ruff_linter/src/snapshots/{ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_error_on_310_3.10.snap => ruff_linter__linter__tests__semantic_syntax_error_async_comprehension_outside_async_function.py_3.10.snap} (63%) rename crates/ruff_linter/src/snapshots/{ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_false_positive_3.10.snap => ruff_linter__linter__tests__semantic_syntax_error_async_comprehension_outside_async_function.py_3.11.snap} (100%) create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_match_class_attribute.py_3.10.snap rename crates/ruff_linter/src/snapshots/{ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchKey_duplicate_match_key_3.10.snap => ruff_linter__linter__tests__semantic_syntax_error_duplicate_match_key.py_3.10.snap} (51%) rename crates/ruff_linter/src/snapshots/{ruff_linter__linter__tests__semantic_syntax_error_DuplicateTypeParameter_duplicate_type_param_3.12.snap => ruff_linter__linter__tests__semantic_syntax_error_duplicate_type_parameter.py_3.12.snap} (64%) create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_global_parameter.py_3.10.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_expression.py_3.12.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_star_expression.py_3.10.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_irrefutable_case_pattern.py_3.10.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_multiple_case_assignment.py_3.10.snap rename crates/ruff_linter/src/snapshots/{ruff_linter__linter__tests__semantic_syntax_error_ReboundComprehensionVariable_rebound_comprehension_3.10.snap => ruff_linter__linter__tests__semantic_syntax_error_rebound_comprehension.py_3.10.snap} (69%) rename crates/ruff_linter/src/snapshots/{ruff_linter__linter__tests__semantic_syntax_error_SingleStarredAssignment_single_starred_assignment_3.10.snap => ruff_linter__linter__tests__semantic_syntax_error_single_starred_assignment.py_3.10.snap} (66%) create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_write_to_debug.py_3.10.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_write_to_debug.py_3.12.snap diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/async_comprehension_outside_async_function.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/async_comprehension_outside_async_function.py new file mode 100644 index 0000000000..f91d8f0396 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/async_comprehension_outside_async_function.py @@ -0,0 +1,12 @@ +async def f(): return [[x async for x in foo(n)] for n in range(3)] + +async def test(): return [[x async for x in elements(n)] async for n in range(3)] + +async def f(): [x for x in foo()] and [x async for x in foo()] + +async def f(): + def g(): ... + [x async for x in foo()] + +[x async for x in y] + diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_match_class_attribute.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_match_class_attribute.py new file mode 100644 index 0000000000..fa0fd34504 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_match_class_attribute.py @@ -0,0 +1,3 @@ +match x: + case Point(x=1, x=2): + pass \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_match_key.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_match_key.py new file mode 100644 index 0000000000..e0fd8ea854 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_match_key.py @@ -0,0 +1,3 @@ +match x: + case {'key': 1, 'key': 2}: + pass \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_type_parameter.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_type_parameter.py new file mode 100644 index 0000000000..4f881e96e2 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_type_parameter.py @@ -0,0 +1 @@ +class C[T, T]: pass \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/global_parameter.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/global_parameter.py new file mode 100644 index 0000000000..65394d9cb4 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/global_parameter.py @@ -0,0 +1,29 @@ +def f(a): + global a + +def g(a): + if True: + global a + +def h(a): + def inner(): + global a + +def i(a): + try: + global a + except Exception: + pass + +def f(a): + a = 1 + global a + +def f(a): + a = 1 + a = 2 + global a + +def f(a): + class Inner: + global a # ok \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_expression.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_expression.py new file mode 100644 index 0000000000..d8fd5608b3 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_expression.py @@ -0,0 +1,8 @@ +type X[T: (yield 1)] = int + +type Y = (yield 1) + +def f[T](x: int) -> (y := 3): return x + +class C[T]((yield from [object])): + pass \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_star_expression.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_star_expression.py new file mode 100644 index 0000000000..03c083ce91 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_star_expression.py @@ -0,0 +1,8 @@ +def func(): + return *x + +for *x in range(10): + pass + +def func(): + yield *x \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/irrefutable_case_pattern.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/irrefutable_case_pattern.py new file mode 100644 index 0000000000..dd0dde5f89 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/irrefutable_case_pattern.py @@ -0,0 +1,11 @@ +match value: + case _: + pass + case 1: + pass + +match value: + case irrefutable: + pass + case 1: + pass \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/multiple_case_assignment.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/multiple_case_assignment.py new file mode 100644 index 0000000000..04d00ba676 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/multiple_case_assignment.py @@ -0,0 +1,5 @@ +match x: + case [a, a]: + pass + case _: + pass \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/rebound_comprehension.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/rebound_comprehension.py new file mode 100644 index 0000000000..3e316dc360 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/rebound_comprehension.py @@ -0,0 +1 @@ +[x:= 2 for x in range(2)] \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/single_starred_assignment.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/single_starred_assignment.py new file mode 100644 index 0000000000..32b0ea8738 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/single_starred_assignment.py @@ -0,0 +1 @@ +*a = [1, 2, 3, 4] \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/semantic_errors/write_to_debug.py b/crates/ruff_linter/resources/test/fixtures/semantic_errors/write_to_debug.py new file mode 100644 index 0000000000..ee998417c9 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/semantic_errors/write_to_debug.py @@ -0,0 +1,7 @@ +__debug__ = False + +def process(__debug__): + pass + +class Generic[__debug__]: + pass \ No newline at end of file diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 3503a69c25..2e4f284bee 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -919,17 +919,6 @@ mod tests { Ok(()) } - /// Wrapper around `test_contents_syntax_errors` for testing a snippet of code instead of a - /// file. - fn test_snippet_syntax_errors(contents: &str, settings: &LinterSettings) -> Vec { - let contents = dedent(contents); - test_contents_syntax_errors( - &SourceKind::Python(contents.to_string()), - Path::new(""), - settings, - ) - } - /// A custom test runner that prints syntax errors in addition to other diagnostics. Adapted /// from `flakes` in pyflakes/mod.rs. fn test_contents_syntax_errors( @@ -972,245 +961,38 @@ mod tests { } #[test_case( - "async_in_sync_error_on_310", - "async def f(): return [[x async for x in foo(n)] for n in range(3)]", - PythonVersion::PY310, - "AsyncComprehensionOutsideAsyncFunction" + Path::new("async_comprehension_outside_async_function.py"), + PythonVersion::PY311 )] #[test_case( - "async_in_sync_okay_on_311", - "async def f(): return [[x async for x in foo(n)] for n in range(3)]", - PythonVersion::PY311, - "AsyncComprehensionOutsideAsyncFunction" + Path::new("async_comprehension_outside_async_function.py"), + PythonVersion::PY310 )] - #[test_case( - "async_in_sync_okay_on_310", - "async def test(): return [[x async for x in elements(n)] async for n in range(3)]", - PythonVersion::PY310, - "AsyncComprehensionOutsideAsyncFunction" - )] - #[test_case( - "deferred_function_body", - " - async def f(): [x for x in foo()] and [x async for x in foo()] - async def f(): - def g(): ... - [x async for x in foo()] - ", - PythonVersion::PY310, - "AsyncComprehensionOutsideAsyncFunction" - )] - #[test_case( - "async_in_sync_false_positive", - "[x async for x in y]", - PythonVersion::PY310, - "AsyncComprehensionOutsideAsyncFunction" - )] - #[test_case( - "rebound_comprehension", - "[x:= 2 for x in range(2)]", - PythonVersion::PY310, - "ReboundComprehensionVariable" - )] - #[test_case( - "duplicate_type_param", - "class C[T, T]: pass", - PythonVersion::PY312, - "DuplicateTypeParameter" - )] - #[test_case( - "multiple_case_assignment", - " - match x: - case [a, a]: - pass - case _: - pass - ", - PythonVersion::PY310, - "MultipleCaseAssignment" - )] - #[test_case( - "duplicate_match_key", - " - match x: - case {'key': 1, 'key': 2}: - pass - ", - PythonVersion::PY310, - "DuplicateMatchKey" - )] - #[test_case( - "global_parameter", - " - def f(a): - global a + #[test_case(Path::new("rebound_comprehension.py"), PythonVersion::PY310)] + #[test_case(Path::new("duplicate_type_parameter.py"), PythonVersion::PY312)] + #[test_case(Path::new("multiple_case_assignment.py"), PythonVersion::PY310)] + #[test_case(Path::new("duplicate_match_key.py"), PythonVersion::PY310)] + #[test_case(Path::new("duplicate_match_class_attribute.py"), PythonVersion::PY310)] + #[test_case(Path::new("invalid_star_expression.py"), PythonVersion::PY310)] + #[test_case(Path::new("irrefutable_case_pattern.py"), PythonVersion::PY310)] + #[test_case(Path::new("single_starred_assignment.py"), PythonVersion::PY310)] + #[test_case(Path::new("write_to_debug.py"), PythonVersion::PY312)] + #[test_case(Path::new("write_to_debug.py"), PythonVersion::PY310)] + #[test_case(Path::new("invalid_expression.py"), PythonVersion::PY312)] + #[test_case(Path::new("global_parameter.py"), PythonVersion::PY310)] + fn test_semantic_errors(path: &Path, python_version: PythonVersion) -> Result<()> { + let snapshot = format!( + "semantic_syntax_error_{}_{}", + path.to_string_lossy(), + python_version + ); + let path = Path::new("resources/test/fixtures/semantic_errors").join(path); + let contents = std::fs::read_to_string(&path)?; + let source_kind = SourceKind::Python(contents); - def g(a): - if True: - global a - - def h(a): - def inner(): - global a - - def i(a): - try: - global a - except Exception: - pass - - def f(a): - a = 1 - global a - - def f(a): - a = 1 - a = 2 - global a - - def f(a): - class Inner: - global a # ok - ", - PythonVersion::PY310, - "GlobalParameter" - )] - #[test_case( - "duplicate_match_class_attribute", - " - match x: - case Point(x=1, x=2): - pass - ", - PythonVersion::PY310, - "DuplicateMatchClassAttribute" - )] - #[test_case( - "invalid_star_expression", - " - def func(): - return *x - ", - PythonVersion::PY310, - "InvalidStarExpression" - )] - #[test_case( - "invalid_star_expression_for", - " - for *x in range(10): - pass - ", - PythonVersion::PY310, - "InvalidStarExpression" - )] - #[test_case( - "invalid_star_expression_yield", - " - def func(): - yield *x - ", - PythonVersion::PY310, - "InvalidStarExpression" - )] - #[test_case( - "irrefutable_case_pattern_wildcard", - " - match value: - case _: - pass - case 1: - pass - ", - PythonVersion::PY310, - "IrrefutableCasePattern" - )] - #[test_case( - "irrefutable_case_pattern_capture", - " - match value: - case irrefutable: - pass - case 1: - pass - ", - PythonVersion::PY310, - "IrrefutableCasePattern" - )] - #[test_case( - "single_starred_assignment", - "*a = [1, 2, 3, 4]", - PythonVersion::PY310, - "SingleStarredAssignment" - )] - #[test_case( - "write_to_debug", - " - __debug__ = False - ", - PythonVersion::PY310, - "WriteToDebug" - )] - #[test_case( - "write_to_debug_in_function_param", - " - def process(__debug__): - pass - ", - PythonVersion::PY310, - "WriteToDebug" - )] - #[test_case( - "write_to_debug_class_type_param", - " - class Generic[__debug__]: - pass - ", - PythonVersion::PY312, - "WriteToDebug" - )] - #[test_case( - "invalid_expression_yield_in_type_param", - " - type X[T: (yield 1)] = int - ", - PythonVersion::PY312, - "InvalidExpression" - )] - #[test_case( - "invalid_expression_yield_in_type_alias", - " - type Y = (yield 1) - ", - PythonVersion::PY312, - "InvalidExpression" - )] - #[test_case( - "invalid_expression_walrus_in_return_annotation", - " - def f[T](x: int) -> (y := 3): return x - ", - PythonVersion::PY312, - "InvalidExpression" - )] - #[test_case( - "invalid_expression_yield_from_in_base_class", - " - class C[T]((yield from [object])): - pass - ", - PythonVersion::PY312, - "InvalidExpression" - )] - fn test_semantic_errors( - name: &str, - contents: &str, - python_version: PythonVersion, - error_type: &str, - ) { - let snapshot = format!("semantic_syntax_error_{error_type}_{name}_{python_version}"); - let diagnostics = test_snippet_syntax_errors( - contents, + let diagnostics = test_contents_syntax_errors( + &source_kind, + &path, &LinterSettings { rules: settings::rule_table::RuleTable::empty(), unresolved_target_version: python_version.into(), @@ -1218,7 +1000,11 @@ mod tests { ..Default::default() }, ); - assert_diagnostics!(snapshot, diagnostics); + insta::with_settings!({filters => vec![(r"\\", "/")]}, { + assert_diagnostics!(format!("{snapshot}"), diagnostics); + }); + + Ok(()) } #[test_case(PythonVersion::PY310)] diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_310_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_310_3.10.snap deleted file mode 100644 index 4ba33c756c..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_310_3.10.snap +++ /dev/null @@ -1,4 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- - diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_311_3.11.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_311_3.11.snap deleted file mode 100644 index 4ba33c756c..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_311_3.11.snap +++ /dev/null @@ -1,4 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- - diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_deferred_function_body_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_deferred_function_body_3.10.snap deleted file mode 100644 index 4ba33c756c..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_deferred_function_body_3.10.snap +++ /dev/null @@ -1,4 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- - diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchClassAttribute_duplicate_match_class_attribute_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchClassAttribute_duplicate_match_class_attribute_3.10.snap deleted file mode 100644 index 5c8dcb3dea..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchClassAttribute_duplicate_match_class_attribute_3.10.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: attribute name `x` repeated in class pattern - --> :3:21 - | -2 | match x: -3 | case Point(x=1, x=2): - | ^ -4 | pass - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_GlobalParameter_global_parameter_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_GlobalParameter_global_parameter_3.10.snap deleted file mode 100644 index c1c7fbd378..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_GlobalParameter_global_parameter_3.10.snap +++ /dev/null @@ -1,56 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: name `a` is parameter and global - --> :3:12 - | -2 | def f(a): -3 | global a - | ^ -4 | -5 | def g(a): - | - -invalid-syntax: name `a` is parameter and global - --> :7:16 - | -5 | def g(a): -6 | if True: -7 | global a - | ^ -8 | -9 | def h(a): - | - -invalid-syntax: name `a` is parameter and global - --> :15:16 - | -13 | def i(a): -14 | try: -15 | global a - | ^ -16 | except Exception: -17 | pass - | - -invalid-syntax: name `a` is parameter and global - --> :21:12 - | -19 | def f(a): -20 | a = 1 -21 | global a - | ^ -22 | -23 | def f(a): - | - -invalid-syntax: name `a` is parameter and global - --> :26:12 - | -24 | a = 1 -25 | a = 2 -26 | global a - | ^ -27 | -28 | def f(a): - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_walrus_in_return_annotation_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_walrus_in_return_annotation_3.12.snap deleted file mode 100644 index 8993d2acbe..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_walrus_in_return_annotation_3.12.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: named expression cannot be used within a generic definition - --> :2:22 - | -2 | def f[T](x: int) -> (y := 3): return x - | ^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_from_in_base_class_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_from_in_base_class_3.12.snap deleted file mode 100644 index aa0b77de19..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_from_in_base_class_3.12.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: yield expression cannot be used within a generic definition - --> :2:13 - | -2 | class C[T]((yield from [object])): - | ^^^^^^^^^^^^^^^^^^^ -3 | pass - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_alias_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_alias_3.12.snap deleted file mode 100644 index 5c1135bdb3..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_alias_3.12.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: yield expression cannot be used within a type alias - --> :2:11 - | -2 | type Y = (yield 1) - | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_param_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_param_3.12.snap deleted file mode 100644 index 97171b88c3..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_param_3.12.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: yield expression cannot be used within a TypeVar bound - --> :2:12 - | -2 | type X[T: (yield 1)] = int - | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_3.10.snap deleted file mode 100644 index 0e454a7167..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_3.10.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: Starred expression cannot be used here - --> :3:12 - | -2 | def func(): -3 | return *x - | ^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_for_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_for_3.10.snap deleted file mode 100644 index d6ea25739b..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_for_3.10.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: Starred expression cannot be used here - --> :2:5 - | -2 | for *x in range(10): - | ^^ -3 | pass - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_yield_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_yield_3.10.snap deleted file mode 100644 index a471ef94fd..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_yield_3.10.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: Starred expression cannot be used here - --> :3:11 - | -2 | def func(): -3 | yield *x - | ^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_capture_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_capture_3.10.snap deleted file mode 100644 index 44ce08366d..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_capture_3.10.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: name capture `irrefutable` makes remaining patterns unreachable - --> :3:10 - | -2 | match value: -3 | case irrefutable: - | ^^^^^^^^^^^ -4 | pass -5 | case 1: - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_wildcard_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_wildcard_3.10.snap deleted file mode 100644 index 74006baae1..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_wildcard_3.10.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: wildcard makes remaining patterns unreachable - --> :3:10 - | -2 | match value: -3 | case _: - | ^ -4 | pass -5 | case 1: - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_MultipleCaseAssignment_multiple_case_assignment_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_MultipleCaseAssignment_multiple_case_assignment_3.10.snap deleted file mode 100644 index 64e8b8e863..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_MultipleCaseAssignment_multiple_case_assignment_3.10.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: multiple assignments to name `a` in pattern - --> :3:14 - | -2 | match x: -3 | case [a, a]: - | ^ -4 | pass -5 | case _: - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_3.10.snap deleted file mode 100644 index 52841dbfab..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_3.10.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: cannot assign to `__debug__` - --> :2:1 - | -2 | __debug__ = False - | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_class_type_param_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_class_type_param_3.12.snap deleted file mode 100644 index 80b922ffa3..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_class_type_param_3.12.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: cannot assign to `__debug__` - --> :2:15 - | -2 | class Generic[__debug__]: - | ^^^^^^^^^ -3 | pass - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_in_function_param_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_in_function_param_3.10.snap deleted file mode 100644 index 0c0b86534c..0000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_in_function_param_3.10.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -invalid-syntax: cannot assign to `__debug__` - --> :2:13 - | -2 | def process(__debug__): - | ^^^^^^^^^ -3 | pass - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_error_on_310_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_async_comprehension_outside_async_function.py_3.10.snap similarity index 63% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_error_on_310_3.10.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_async_comprehension_outside_async_function.py_3.10.snap index 24eeb50a9a..08822c21f2 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_error_on_310_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_async_comprehension_outside_async_function.py_3.10.snap @@ -2,8 +2,10 @@ source: crates/ruff_linter/src/linter.rs --- invalid-syntax: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) - --> :1:27 + --> resources/test/fixtures/semantic_errors/async_comprehension_outside_async_function.py:1:27 | 1 | async def f(): return [[x async for x in foo(n)] for n in range(3)] | ^^^^^^^^^^^^^^^^^^^^^ +2 | +3 | async def test(): return [[x async for x in elements(n)] async for n in range(3)] | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_false_positive_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_async_comprehension_outside_async_function.py_3.11.snap similarity index 100% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_false_positive_3.10.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_async_comprehension_outside_async_function.py_3.11.snap diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_match_class_attribute.py_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_match_class_attribute.py_3.10.snap new file mode 100644 index 0000000000..f384495839 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_match_class_attribute.py_3.10.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +invalid-syntax: attribute name `x` repeated in class pattern + --> resources/test/fixtures/semantic_errors/duplicate_match_class_attribute.py:2:21 + | +1 | match x: +2 | case Point(x=1, x=2): + | ^ +3 | pass + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchKey_duplicate_match_key_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_match_key.py_3.10.snap similarity index 51% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchKey_duplicate_match_key_3.10.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_match_key.py_3.10.snap index 5f4a0040a8..9ab24be2ea 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchKey_duplicate_match_key_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_match_key.py_3.10.snap @@ -2,10 +2,10 @@ source: crates/ruff_linter/src/linter.rs --- invalid-syntax: mapping pattern checks duplicate key `'key'` - --> :3:21 + --> resources/test/fixtures/semantic_errors/duplicate_match_key.py:2:21 | -2 | match x: -3 | case {'key': 1, 'key': 2}: +1 | match x: +2 | case {'key': 1, 'key': 2}: | ^^^^^ -4 | pass +3 | pass | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateTypeParameter_duplicate_type_param_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_type_parameter.py_3.12.snap similarity index 64% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateTypeParameter_duplicate_type_param_3.12.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_type_parameter.py_3.12.snap index bfd85698f6..9b3d052471 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateTypeParameter_duplicate_type_param_3.12.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_type_parameter.py_3.12.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/linter.rs --- invalid-syntax: duplicate type parameter - --> :1:12 + --> resources/test/fixtures/semantic_errors/duplicate_type_parameter.py:1:12 | 1 | class C[T, T]: pass | ^ diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_global_parameter.py_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_global_parameter.py_3.10.snap new file mode 100644 index 0000000000..e183fb0e88 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_global_parameter.py_3.10.snap @@ -0,0 +1,56 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +invalid-syntax: name `a` is parameter and global + --> resources/test/fixtures/semantic_errors/global_parameter.py:2:12 + | +1 | def f(a): +2 | global a + | ^ +3 | +4 | def g(a): + | + +invalid-syntax: name `a` is parameter and global + --> resources/test/fixtures/semantic_errors/global_parameter.py:6:16 + | +4 | def g(a): +5 | if True: +6 | global a + | ^ +7 | +8 | def h(a): + | + +invalid-syntax: name `a` is parameter and global + --> resources/test/fixtures/semantic_errors/global_parameter.py:14:16 + | +12 | def i(a): +13 | try: +14 | global a + | ^ +15 | except Exception: +16 | pass + | + +invalid-syntax: name `a` is parameter and global + --> resources/test/fixtures/semantic_errors/global_parameter.py:20:12 + | +18 | def f(a): +19 | a = 1 +20 | global a + | ^ +21 | +22 | def f(a): + | + +invalid-syntax: name `a` is parameter and global + --> resources/test/fixtures/semantic_errors/global_parameter.py:25:12 + | +23 | a = 1 +24 | a = 2 +25 | global a + | ^ +26 | +27 | def f(a): + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_expression.py_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_expression.py_3.12.snap new file mode 100644 index 0000000000..a33a0c9ce9 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_expression.py_3.12.snap @@ -0,0 +1,43 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +invalid-syntax: yield expression cannot be used within a TypeVar bound + --> resources/test/fixtures/semantic_errors/invalid_expression.py:1:12 + | +1 | type X[T: (yield 1)] = int + | ^^^^^^^ +2 | +3 | type Y = (yield 1) + | + +invalid-syntax: yield expression cannot be used within a type alias + --> resources/test/fixtures/semantic_errors/invalid_expression.py:3:11 + | +1 | type X[T: (yield 1)] = int +2 | +3 | type Y = (yield 1) + | ^^^^^^^ +4 | +5 | def f[T](x: int) -> (y := 3): return x + | + +invalid-syntax: named expression cannot be used within a generic definition + --> resources/test/fixtures/semantic_errors/invalid_expression.py:5:22 + | +3 | type Y = (yield 1) +4 | +5 | def f[T](x: int) -> (y := 3): return x + | ^^^^^^ +6 | +7 | class C[T]((yield from [object])): + | + +invalid-syntax: yield expression cannot be used within a generic definition + --> resources/test/fixtures/semantic_errors/invalid_expression.py:7:13 + | +5 | def f[T](x: int) -> (y := 3): return x +6 | +7 | class C[T]((yield from [object])): + | ^^^^^^^^^^^^^^^^^^^ +8 | pass + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_star_expression.py_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_star_expression.py_3.10.snap new file mode 100644 index 0000000000..f26a2d73a7 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_star_expression.py_3.10.snap @@ -0,0 +1,30 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +invalid-syntax: Starred expression cannot be used here + --> resources/test/fixtures/semantic_errors/invalid_star_expression.py:2:12 + | +1 | def func(): +2 | return *x + | ^^ +3 | +4 | for *x in range(10): + | + +invalid-syntax: Starred expression cannot be used here + --> resources/test/fixtures/semantic_errors/invalid_star_expression.py:4:5 + | +2 | return *x +3 | +4 | for *x in range(10): + | ^^ +5 | pass + | + +invalid-syntax: Starred expression cannot be used here + --> resources/test/fixtures/semantic_errors/invalid_star_expression.py:8:11 + | +7 | def func(): +8 | yield *x + | ^^ + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_irrefutable_case_pattern.py_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_irrefutable_case_pattern.py_3.10.snap new file mode 100644 index 0000000000..afeb965ebb --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_irrefutable_case_pattern.py_3.10.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +invalid-syntax: wildcard makes remaining patterns unreachable + --> resources/test/fixtures/semantic_errors/irrefutable_case_pattern.py:2:10 + | +1 | match value: +2 | case _: + | ^ +3 | pass +4 | case 1: + | + +invalid-syntax: name capture `irrefutable` makes remaining patterns unreachable + --> resources/test/fixtures/semantic_errors/irrefutable_case_pattern.py:8:10 + | + 7 | match value: + 8 | case irrefutable: + | ^^^^^^^^^^^ + 9 | pass +10 | case 1: + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_multiple_case_assignment.py_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_multiple_case_assignment.py_3.10.snap new file mode 100644 index 0000000000..2d88f93cad --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_multiple_case_assignment.py_3.10.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +invalid-syntax: multiple assignments to name `a` in pattern + --> resources/test/fixtures/semantic_errors/multiple_case_assignment.py:2:14 + | +1 | match x: +2 | case [a, a]: + | ^ +3 | pass +4 | case _: + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_ReboundComprehensionVariable_rebound_comprehension_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_rebound_comprehension.py_3.10.snap similarity index 69% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_ReboundComprehensionVariable_rebound_comprehension_3.10.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_rebound_comprehension.py_3.10.snap index 5f7b81b610..e08f2e9e17 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_ReboundComprehensionVariable_rebound_comprehension_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_rebound_comprehension.py_3.10.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/linter.rs --- invalid-syntax: assignment expression cannot rebind comprehension variable - --> :1:2 + --> resources/test/fixtures/semantic_errors/rebound_comprehension.py:1:2 | 1 | [x:= 2 for x in range(2)] | ^ diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_SingleStarredAssignment_single_starred_assignment_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_single_starred_assignment.py_3.10.snap similarity index 66% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_SingleStarredAssignment_single_starred_assignment_3.10.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_single_starred_assignment.py_3.10.snap index e4955a4574..7c3cc3916b 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_SingleStarredAssignment_single_starred_assignment_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_single_starred_assignment.py_3.10.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/linter.rs --- invalid-syntax: starred assignment target must be in a list or tuple - --> :1:1 + --> resources/test/fixtures/semantic_errors/single_starred_assignment.py:1:1 | 1 | *a = [1, 2, 3, 4] | ^^ diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_write_to_debug.py_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_write_to_debug.py_3.10.snap new file mode 100644 index 0000000000..798c6b21ae --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_write_to_debug.py_3.10.snap @@ -0,0 +1,41 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +invalid-syntax: cannot assign to `__debug__` + --> resources/test/fixtures/semantic_errors/write_to_debug.py:1:1 + | +1 | __debug__ = False + | ^^^^^^^^^ +2 | +3 | def process(__debug__): + | + +invalid-syntax: cannot assign to `__debug__` + --> resources/test/fixtures/semantic_errors/write_to_debug.py:3:13 + | +1 | __debug__ = False +2 | +3 | def process(__debug__): + | ^^^^^^^^^ +4 | pass + | + +invalid-syntax: Cannot use type parameter lists on Python 3.10 (syntax was added in Python 3.12) + --> resources/test/fixtures/semantic_errors/write_to_debug.py:6:14 + | +4 | pass +5 | +6 | class Generic[__debug__]: + | ^^^^^^^^^^^ +7 | pass + | + +invalid-syntax: cannot assign to `__debug__` + --> resources/test/fixtures/semantic_errors/write_to_debug.py:6:15 + | +4 | pass +5 | +6 | class Generic[__debug__]: + | ^^^^^^^^^ +7 | pass + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_write_to_debug.py_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_write_to_debug.py_3.12.snap new file mode 100644 index 0000000000..5e5e7f789a --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_write_to_debug.py_3.12.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +invalid-syntax: cannot assign to `__debug__` + --> resources/test/fixtures/semantic_errors/write_to_debug.py:1:1 + | +1 | __debug__ = False + | ^^^^^^^^^ +2 | +3 | def process(__debug__): + | + +invalid-syntax: cannot assign to `__debug__` + --> resources/test/fixtures/semantic_errors/write_to_debug.py:3:13 + | +1 | __debug__ = False +2 | +3 | def process(__debug__): + | ^^^^^^^^^ +4 | pass + | + +invalid-syntax: cannot assign to `__debug__` + --> resources/test/fixtures/semantic_errors/write_to_debug.py:6:15 + | +4 | pass +5 | +6 | class Generic[__debug__]: + | ^^^^^^^^^ +7 | pass + | From 29462ea1d486dd8976251a2d3b6ebb3890c55ad3 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 27 Oct 2025 22:01:08 -0400 Subject: [PATCH 058/188] [ty] Add new "constraint implication" typing relation (#21010) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds the new **_constraint implication_** relationship between types, aka `is_subtype_of_given`, which tests whether one type is a subtype of another _assuming that the constraints in a particular constraint set hold_. For concrete types, constraint implication is exactly the same as subtyping. (A concrete type is any fully static type that is not a typevar. It can _contain_ a typevar, though — `list[T]` is considered concrete.) The interesting case is typevars. The other typing relationships (TODO: will) all "punt" on the question when considering a typevar, by translating the desired relationship into a constraint set. At some point, though, we need to resolve a constraint set; at that point, we can no longer punt on the question. Unlike with concrete types, the answer will depend on the constraint set that we are considering. --- .../mdtest/type_properties/constraints.md | 56 ++++ .../type_properties/is_subtype_of_given.md | 198 ++++++++++++ crates/ty_python_semantic/src/types.rs | 35 ++- .../ty_python_semantic/src/types/call/bind.rs | 28 ++ .../src/types/constraints.rs | 288 +++++++++++++++--- .../ty_python_semantic/src/types/function.rs | 9 +- .../src/types/type_ordering.rs | 6 +- .../ty_extensions/ty_extensions.pyi | 17 +- 8 files changed, 578 insertions(+), 59 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of_given.md diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md index 65b59a55b4..5d1f4e7142 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md @@ -602,6 +602,62 @@ def _[T, U]() -> None: reveal_type(~union | union) ``` +## Typevar ordering + +Constraints can relate two typevars — i.e., `S ≤ T`. We could encode that in one of two ways: +`Never ≤ S ≤ T` or `S ≤ T ≤ object`. In other words, we can decide whether `S` or `T` is the typevar +being constrained. The other is then the lower or upper bound of the constraint. + +To handle this, we enforce an arbitrary ordering on typevars, and always place the constraint on the +"earlier" typevar. For the example above, that does not change how the constraint is displayed, +since we always hide `Never` lower bounds and `object` upper bounds. + +```py +from typing import Never +from ty_extensions import range_constraint + +def f[S, T](): + # revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)] + reveal_type(range_constraint(Never, S, T)) + # revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)] + reveal_type(range_constraint(S, T, object)) + +def f[T, S](): + # revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)] + reveal_type(range_constraint(Never, S, T)) + # revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)] + reveal_type(range_constraint(S, T, object)) +``` + +Equivalence constraints are similar; internally we arbitrarily choose the "earlier" typevar to be +the constraint, and the other the bound. But we display the result the same way no matter what. + +```py +def f[S, T](): + # revealed: ty_extensions.ConstraintSet[(S@f = T@f)] + reveal_type(range_constraint(T, S, T)) + # revealed: ty_extensions.ConstraintSet[(S@f = T@f)] + reveal_type(range_constraint(S, T, S)) + +def f[T, S](): + # revealed: ty_extensions.ConstraintSet[(S@f = T@f)] + reveal_type(range_constraint(T, S, T)) + # revealed: ty_extensions.ConstraintSet[(S@f = T@f)] + reveal_type(range_constraint(S, T, S)) +``` + +But in the case of `S ≤ T ≤ U`, we end up with an ambiguity. Depending on the typevar ordering, that +might display as `S ≤ T ≤ U`, or as `(S ≤ T) ∧ (T ≤ U)`. + +```py +def f[S, T, U](): + # Could be either of: + # ty_extensions.ConstraintSet[(S@f ≤ T@f ≤ U@f)] + # ty_extensions.ConstraintSet[(S@f ≤ T@f) ∧ (T@f ≤ U@f)] + # reveal_type(range_constraint(S, T, U)) + ... +``` + ## Other simplifications ### Displaying constraint sets diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of_given.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of_given.md new file mode 100644 index 0000000000..c1a0577fa3 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of_given.md @@ -0,0 +1,198 @@ +# Constraint implication + +```toml +[environment] +python-version = "3.12" +``` + +This file tests the _constraint implication_ relationship between types, aka `is_subtype_of_given`, +which tests whether one type is a [subtype][subtyping] of another _assuming that the constraints in +a particular constraint set hold_. + +## Concrete types + +For concrete types, constraint implication is exactly the same as subtyping. (A concrete type is any +fully static type that is not a typevar. It can _contain_ a typevar, though — `list[T]` is +considered concrete.) + +```py +from ty_extensions import is_subtype_of, is_subtype_of_given, static_assert + +def equivalent_to_other_relationships[T](): + static_assert(is_subtype_of(bool, int)) + static_assert(is_subtype_of_given(True, bool, int)) + + static_assert(not is_subtype_of(bool, str)) + static_assert(not is_subtype_of_given(True, bool, str)) +``` + +Moreover, for concrete types, the answer does not depend on which constraint set we are considering. +`bool` is a subtype of `int` no matter what types any typevars are specialized to — and even if +there isn't a valid specialization for the typevars we are considering. + +```py +from typing import Never +from ty_extensions import range_constraint + +def even_given_constraints[T](): + constraints = range_constraint(Never, T, int) + static_assert(is_subtype_of_given(constraints, bool, int)) + static_assert(not is_subtype_of_given(constraints, bool, str)) + +def even_given_unsatisfiable_constraints(): + static_assert(is_subtype_of_given(False, bool, int)) + static_assert(not is_subtype_of_given(False, bool, str)) +``` + +## Type variables + +The interesting case is typevars. The other typing relationships (TODO: will) all "punt" on the +question when considering a typevar, by translating the desired relationship into a constraint set. + +```py +from typing import Any +from ty_extensions import is_assignable_to, is_subtype_of + +def assignability[T](): + # TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ bool] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, bool)) + # TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ int] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, int)) + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, object)) + +def subtyping[T](): + # TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ bool] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, bool)) + # TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ int] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, int)) + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(T, object)) +``` + +When checking assignability with a dynamic type, we use the bottom and top materializations of the +lower and upper bounds, respectively. For subtyping, we use the top and bottom materializations. +(That is, assignability turns into a "permissive" constraint, and subtyping turns into a +"conservative" constraint.) + +```py +class Covariant[T]: + def get(self) -> T: + raise ValueError + +class Contravariant[T]: + def set(self, value: T): + pass + +def assignability[T](): + # aka [T@assignability ≤ object], which is always satisfiable + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Any)) + + # aka [Never ≤ T@assignability], which is always satisfiable + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Any, T)) + + # TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ Covariant[object]] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, Covariant[Any])) + # TODO: revealed: ty_extensions.ConstraintSet[Covariant[Never] ≤ T@assignability] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(Covariant[Any], T)) + + # TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ Contravariant[Never]] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, Contravariant[Any])) + # TODO: revealed: ty_extensions.ConstraintSet[Contravariant[object] ≤ T@assignability] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(Contravariant[Any], T)) + +def subtyping[T](): + # aka [T@assignability ≤ object], which is always satisfiable + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Any)) + + # aka [Never ≤ T@assignability], which is always satisfiable + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Any, T)) + + # TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ Covariant[Never]] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Covariant[Any])) + # TODO: revealed: ty_extensions.ConstraintSet[Covariant[object] ≤ T@subtyping] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Covariant[Any], T)) + + # TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ Contravariant[object]] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Contravariant[Any])) + # TODO: revealed: ty_extensions.ConstraintSet[Contravariant[Never] ≤ T@subtyping] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Contravariant[Any], T)) +``` + +At some point, though, we need to resolve a constraint set; at that point, we can no longer punt on +the question. Unlike with concrete types, the answer will depend on the constraint set that we are +considering. + +```py +from typing import Never +from ty_extensions import is_subtype_of_given, range_constraint, static_assert + +def given_constraints[T](): + static_assert(not is_subtype_of_given(True, T, int)) + static_assert(not is_subtype_of_given(True, T, bool)) + static_assert(not is_subtype_of_given(True, T, str)) + + # These are vacuously true; false implies anything + static_assert(is_subtype_of_given(False, T, int)) + static_assert(is_subtype_of_given(False, T, bool)) + static_assert(is_subtype_of_given(False, T, str)) + + given_int = range_constraint(Never, T, int) + static_assert(is_subtype_of_given(given_int, T, int)) + static_assert(not is_subtype_of_given(given_int, T, bool)) + static_assert(not is_subtype_of_given(given_int, T, str)) + + given_bool = range_constraint(Never, T, bool) + static_assert(is_subtype_of_given(given_bool, T, int)) + static_assert(is_subtype_of_given(given_bool, T, bool)) + static_assert(not is_subtype_of_given(given_bool, T, str)) + + given_both = given_bool & given_int + static_assert(is_subtype_of_given(given_both, T, int)) + static_assert(is_subtype_of_given(given_both, T, bool)) + static_assert(not is_subtype_of_given(given_both, T, str)) + + given_str = range_constraint(Never, T, str) + static_assert(not is_subtype_of_given(given_str, T, int)) + static_assert(not is_subtype_of_given(given_str, T, bool)) + static_assert(is_subtype_of_given(given_str, T, str)) +``` + +This might require propagating constraints from other typevars. + +```py +def mutually_constrained[T, U](): + # If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well. + given_int = range_constraint(U, T, U) & range_constraint(Never, U, int) + # TODO: no static-assert-error + # error: [static-assert-error] + static_assert(is_subtype_of_given(given_int, T, int)) + static_assert(not is_subtype_of_given(given_int, T, bool)) + static_assert(not is_subtype_of_given(given_int, T, str)) + + # If [T ≤ U ∧ U ≤ int], then [T ≤ int] must be true as well. + given_int = range_constraint(Never, T, U) & range_constraint(Never, U, int) + # TODO: no static-assert-error + # error: [static-assert-error] + static_assert(is_subtype_of_given(given_int, T, int)) + static_assert(not is_subtype_of_given(given_int, T, bool)) + static_assert(not is_subtype_of_given(given_int, T, str)) +``` + +[subtyping]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index f2330fabc3..8f87593986 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1724,7 +1724,7 @@ impl<'db> Type<'db> { // since subtyping between a TypeVar and an arbitrary other type cannot be guaranteed to be reflexive. (Type::TypeVar(lhs_bound_typevar), Type::TypeVar(rhs_bound_typevar)) if !lhs_bound_typevar.is_inferable(db, inferable) - && lhs_bound_typevar.identity(db) == rhs_bound_typevar.identity(db) => + && lhs_bound_typevar.is_same_typevar_as(db, rhs_bound_typevar) => { ConstraintSet::from(true) } @@ -2621,7 +2621,7 @@ impl<'db> Type<'db> { // constraints, which are handled below. (Type::TypeVar(self_bound_typevar), Type::TypeVar(other_bound_typevar)) if !self_bound_typevar.is_inferable(db, inferable) - && self_bound_typevar.identity(db) == other_bound_typevar.identity(db) => + && self_bound_typevar.is_same_typevar_as(db, other_bound_typevar) => { ConstraintSet::from(false) } @@ -4833,6 +4833,30 @@ impl<'db> Type<'db> { ) .into(), + Some(KnownFunction::IsSubtypeOfGiven) => Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("constraints"))) + .with_annotated_type(UnionType::from_elements( + db, + [ + KnownClass::Bool.to_instance(db), + KnownClass::ConstraintSet.to_instance(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()), + ]), + Some(KnownClass::ConstraintSet.to_instance(db)), + ), + ) + .into(), + Some(KnownFunction::RangeConstraint | KnownFunction::NegatedRangeConstraint) => { Binding::single( self, @@ -8534,7 +8558,6 @@ pub struct BoundTypeVarIdentity<'db> { /// 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)] -#[derive(PartialOrd, Ord)] pub struct BoundTypeVarInstance<'db> { pub typevar: TypeVarInstance<'db>, binding_context: BindingContext<'db>, @@ -8555,6 +8578,12 @@ 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 { + 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( diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index e72d798dd6..d1aa621b82 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -17,6 +17,7 @@ use crate::db::Db; use crate::dunder_all::dunder_all_names; use crate::place::{Definedness, Place}; use crate::types::call::arguments::{Expansion, is_expandable_type}; +use crate::types::constraints::ConstraintSet; use crate::types::diagnostic::{ CALL_NON_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, POSITIONAL_ONLY_PARAMETER_AS_KWARG, @@ -704,6 +705,33 @@ impl<'db> Bindings<'db> { } } + Some(KnownFunction::IsSubtypeOfGiven) => { + let [Some(constraints), Some(ty_a), Some(ty_b)] = + overload.parameter_types() + else { + continue; + }; + + let constraints = match constraints { + Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked)) => { + tracked.constraints(db) + } + Type::BooleanLiteral(b) => ConstraintSet::from(*b), + _ => continue, + }; + + let result = constraints.when_subtype_of_given( + db, + *ty_a, + *ty_b, + InferableTypeVars::None, + ); + let tracked = TrackedConstraintSet::new(db, result); + overload.set_return_type(Type::KnownInstance( + KnownInstanceType::ConstraintSet(tracked), + )); + } + Some(KnownFunction::IsAssignableTo) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { let constraints = diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 417399203c..b5e3cc3e7d 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -64,7 +64,8 @@ use rustc_hash::FxHashSet; use salsa::plumbing::AsId; use crate::Db; -use crate::types::{BoundTypeVarIdentity, IntersectionType, Type, UnionType}; +use crate::types::generics::InferableTypeVars; +use crate::types::{BoundTypeVarInstance, IntersectionType, Type, TypeRelation, UnionType}; /// An extension trait for building constraint sets from [`Option`] values. pub(crate) trait OptionConstraintsExtension { @@ -175,6 +176,31 @@ impl<'db> ConstraintSet<'db> { } } + /// Returns a constraint set that constraints a typevar to a particular range of types. + pub(crate) fn constrain_typevar( + db: &'db dyn Db, + typevar: BoundTypeVarInstance<'db>, + lower: Type<'db>, + upper: Type<'db>, + relation: TypeRelation, + ) -> Self { + let (lower, upper) = match relation { + // TODO: Is this the correct constraint for redundancy? + TypeRelation::Subtyping | TypeRelation::Redundancy => ( + lower.top_materialization(db), + upper.bottom_materialization(db), + ), + TypeRelation::Assignability => ( + lower.bottom_materialization(db), + upper.top_materialization(db), + ), + }; + + Self { + node: ConstrainedTypeVar::new_node(db, typevar, lower, upper), + } + } + /// Returns whether this constraint set never holds pub(crate) fn is_never_satisfied(self, _db: &'db dyn Db) -> bool { self.node.is_never_satisfied() @@ -185,6 +211,51 @@ impl<'db> ConstraintSet<'db> { self.node.is_always_satisfied(db) } + /// Returns the constraints under which `lhs` is a subtype of `rhs`, assuming that the + /// constraints in this constraint set hold. + /// + /// For concrete types (types that are not typevars), this returns the same result as + /// [`when_subtype_of`][Type::when_subtype_of]. (Constraint sets place restrictions on + /// typevars, so if you are not comparing typevars, the constraint set can have no effect on + /// whether subtyping holds.) + /// + /// If you're comparing a typevar, we have to consider what restrictions the constraint set + /// places on that typevar to determine if subtyping holds. For instance, if you want to check + /// whether `T ≤ int`, then answer will depend on what constraint set you are considering: + /// + /// ```text + /// when_subtype_of_given(T ≤ bool, T, int) ⇒ true + /// when_subtype_of_given(T ≤ int, T, int) ⇒ true + /// when_subtype_of_given(T ≤ str, T, int) ⇒ false + /// ``` + /// + /// In the first two cases, the constraint set ensures that `T` will always specialize to a + /// type that is a subtype of `int`. In the final case, the constraint set requires `T` to + /// specialize to a subtype of `str`, and there is no such type that is also a subtype of + /// `int`. + /// + /// There are two constraint sets that deserve special consideration. + /// + /// - The "always true" constraint set does not place any restrictions on any typevar. In this + /// case, `when_subtype_of_given` will return the same result as `when_subtype_of`, even if + /// you're comparing against a typevar. + /// + /// - The "always false" constraint set represents an impossible situation. In this case, every + /// subtype check will be vacuously true, even if you're comparing two concrete types that + /// are not actually subtypes of each other. (That is, + /// `when_subtype_of_given(false, int, str)` will return true!) + pub(crate) fn when_subtype_of_given( + self, + db: &'db dyn Db, + lhs: Type<'db>, + rhs: Type<'db>, + inferable: InferableTypeVars<'_, 'db>, + ) -> Self { + Self { + node: self.node.when_subtype_of_given(db, lhs, rhs, inferable), + } + } + /// Updates this constraint set to hold the union of itself and another constraint set. pub(crate) fn union(&mut self, db: &'db dyn Db, other: Self) -> Self { self.node = self.node.or(db, other.node); @@ -227,20 +298,16 @@ impl<'db> ConstraintSet<'db> { pub(crate) fn range( db: &'db dyn Db, lower: Type<'db>, - typevar: BoundTypeVarIdentity<'db>, + typevar: BoundTypeVarInstance<'db>, upper: Type<'db>, ) -> Self { - let lower = lower.bottom_materialization(db); - let upper = upper.top_materialization(db); - Self { - node: ConstrainedTypeVar::new_node(db, lower, typevar, upper), - } + Self::constrain_typevar(db, typevar, lower, upper, TypeRelation::Assignability) } pub(crate) fn negated_range( db: &'db dyn Db, lower: Type<'db>, - typevar: BoundTypeVarIdentity<'db>, + typevar: BoundTypeVarInstance<'db>, upper: Type<'db>, ) -> Self { Self::range(db, lower, typevar, upper).negate(db) @@ -257,11 +324,26 @@ impl From for ConstraintSet<'_> { } } +impl<'db> BoundTypeVarInstance<'db> { + /// Returns whether this typevar can be the lower or upper bound of another typevar in a + /// constraint set. + /// + /// We enforce an (arbitrary) ordering on typevars, and ensure that the bounds of a constraint + /// are "later" according to that order than the typevar being constrained. Having an order + /// ensures that we can build up transitive relationships between constraints without incurring + /// 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) + } +} + /// 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> { - typevar: BoundTypeVarIdentity<'db>, + typevar: BoundTypeVarInstance<'db>, lower: Type<'db>, upper: Type<'db>, } @@ -276,8 +358,8 @@ impl<'db> ConstrainedTypeVar<'db> { /// Panics if `lower` and `upper` are not both fully static. fn new_node( db: &'db dyn Db, + typevar: BoundTypeVarInstance<'db>, lower: Type<'db>, - typevar: BoundTypeVarIdentity<'db>, upper: Type<'db>, ) -> Node<'db> { debug_assert_eq!(lower, lower.bottom_materialization(db)); @@ -297,7 +379,69 @@ impl<'db> ConstrainedTypeVar<'db> { let lower = lower.normalized(db); let upper = upper.normalized(db); - Node::new_constraint(db, ConstrainedTypeVar::new(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. + // + // In the comments below, we use brackets to indicate which typevar is "earlier", and + // therefore the typevar that the constraint applies to. + 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) { + (lower, typevar) + } else { + (typevar, lower) + }; + Node::new_constraint( + db, + ConstrainedTypeVar::new( + db, + typevar, + Type::TypeVar(bound), + Type::TypeVar(bound), + ), + ) + } + + // 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) => + { + let lower = Node::new_constraint( + db, + ConstrainedTypeVar::new(db, lower, Type::Never, Type::TypeVar(typevar)), + ); + let upper = Node::new_constraint( + db, + ConstrainedTypeVar::new(db, upper, Type::TypeVar(typevar), Type::object()), + ); + lower.and(db, upper) + } + + // L ≤ T ≤ U == ([L] ≤ T) && ([T] ≤ U) + (Type::TypeVar(lower), _) if typevar.can_be_bound_for(db, lower) => { + let lower = Node::new_constraint( + db, + ConstrainedTypeVar::new(db, lower, Type::Never, Type::TypeVar(typevar)), + ); + let upper = Self::new_node(db, typevar, Type::Never, upper); + lower.and(db, upper) + } + + // L ≤ T ≤ U == (L ≤ [T]) && (T ≤ [U]) + (_, Type::TypeVar(upper)) if typevar.can_be_bound_for(db, upper) => { + let lower = Self::new_node(db, typevar, lower, Type::object()); + let upper = Node::new_constraint( + db, + ConstrainedTypeVar::new(db, upper, Type::TypeVar(typevar), Type::object()), + ); + lower.and(db, upper) + } + + _ => Node::new_constraint(db, ConstrainedTypeVar::new(db, typevar, lower, upper)), + } } fn when_true(self) -> ConstraintAssignment<'db> { @@ -308,14 +452,6 @@ impl<'db> ConstrainedTypeVar<'db> { ConstraintAssignment::Negative(self) } - fn contains(self, db: &'db dyn Db, other: Self) -> bool { - if self.typevar(db) != other.typevar(db) { - return false; - } - self.lower(db).is_subtype_of(db, other.lower(db)) - && other.upper(db).is_subtype_of(db, self.upper(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 @@ -329,16 +465,20 @@ impl<'db> ConstrainedTypeVar<'db> { /// simplifications that we perform that operate on constraints with the same typevar, and this /// ensures that we can find all candidate simplifications more easily. fn ordering(self, db: &'db dyn Db) -> impl Ord { - (self.typevar(db), self.as_id()) + (self.typevar(db).identity(db), self.as_id()) } /// Returns whether this constraint implies another — i.e., whether every type that /// satisfies this constraint also satisfies `other`. /// - /// This is used (among other places) to simplify how we display constraint sets, by removing - /// redundant constraints from a clause. + /// 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 { - other.contains(db, self) + if !self.typevar(db).is_same_typevar_as(db, other.typevar(db)) { + return false; + } + other.lower(db).is_subtype_of(db, self.lower(db)) + && self.upper(db).is_subtype_of(db, other.upper(db)) } /// Returns the intersection of two range constraints, or `None` if the intersection is empty. @@ -376,11 +516,32 @@ impl<'db> ConstrainedTypeVar<'db> { 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); 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 + // using the in BDD. + if let Type::TypeVar(bound) = lower { + let bound = bound.identity(self.db).display(self.db).to_string(); + let typevar = typevar.identity(self.db).display(self.db).to_string(); + let (smaller, larger) = if bound < typevar { + (bound, typevar) + } else { + (typevar, bound) + }; + return write!( + f, + "({} {} {})", + smaller, + if self.negated { "≠" } else { "=" }, + larger, + ); + } + return write!( f, "({} {} {})", - self.constraint.typevar(self.db).display(self.db), + typevar.identity(self.db).display(self.db), if self.negated { "≠" } else { "=" }, lower.display(self.db) ); @@ -393,7 +554,7 @@ impl<'db> ConstrainedTypeVar<'db> { if !lower.is_never() { write!(f, "{} ≤ ", lower.display(self.db))?; } - self.constraint.typevar(self.db).display(self.db).fmt(f)?; + typevar.identity(self.db).display(self.db).fmt(f)?; if !upper.is_object() { write!(f, " ≤ {}", upper.display(self.db))?; } @@ -588,6 +749,39 @@ impl<'db> Node<'db> { .or(db, self.negate(db).and(db, else_node)) } + fn when_subtype_of_given( + self, + db: &'db dyn Db, + lhs: Type<'db>, + rhs: Type<'db>, + inferable: InferableTypeVars<'_, 'db>, + ) -> Self { + match (lhs, rhs) { + // When checking subtyping involving a typevar, we project the BDD so that it only + // contains that typevar, and any other typevars that could be its upper/lower bound. + // (That is, other typevars that are "later" in our arbitrary ordering of typevars.) + // + // Having done that, 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. + (Type::TypeVar(bound_typevar), _) => { + let constraint = ConstrainedTypeVar::new_node(db, bound_typevar, Type::Never, rhs); + let (simplified, domain) = self.implies(db, constraint).simplify_and_domain(db); + simplified.and(db, domain) + } + + (_, Type::TypeVar(bound_typevar)) => { + let constraint = + ConstrainedTypeVar::new_node(db, bound_typevar, lhs, Type::object()); + let (simplified, domain) = self.implies(db, constraint).simplify_and_domain(db); + simplified.and(db, domain) + } + + // If neither type is a typevar, then we fall back on a normal subtyping check. + _ => lhs.when_subtype_of(db, rhs, inferable).node, + } + } + /// 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.) @@ -747,26 +941,24 @@ impl<'db> Node<'db> { interior.if_false(db).for_each_constraint(db, f); } + /// Returns a simplified version of a BDD, along with the BDD's domain. + fn simplify_and_domain(self, db: &'db dyn Db) -> (Self, Self) { + match self { + Node::AlwaysTrue | Node::AlwaysFalse => (self, Node::AlwaysTrue), + Node::Interior(interior) => interior.simplify(db), + } + } + /// Simplifies a BDD, replacing constraints with simpler or smaller constraints where possible. fn simplify(self, db: &'db dyn Db) -> Self { - match self { - Node::AlwaysTrue | Node::AlwaysFalse => self, - Node::Interior(interior) => { - let (simplified, _) = interior.simplify(db); - simplified - } - } + let (simplified, _) = self.simplify_and_domain(db); + simplified } /// Returns the domain (the set of allowed inputs) for a BDD. fn domain(self, db: &'db dyn Db) -> Self { - match self { - Node::AlwaysTrue | Node::AlwaysFalse => Node::AlwaysTrue, - Node::Interior(interior) => { - let (_, domain) = interior.simplify(db); - domain - } - } + let (_, domain) = self.simplify_and_domain(db); + domain } /// Returns clauses describing all of the variable assignments that cause this BDD to evaluate @@ -1079,10 +1271,10 @@ impl<'db> InteriorNode<'db> { // 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.contains(db, right_constraint) { - Some((left_constraint, right_constraint)) - } else if right_constraint.contains(db, left_constraint) { + let larger_smaller = if left_constraint.implies(db, right_constraint) { Some((right_constraint, left_constraint)) + } else if right_constraint.implies(db, left_constraint) { + Some((left_constraint, right_constraint)) } else { None }; @@ -1313,8 +1505,8 @@ impl<'db> ConstraintAssignment<'db> { /// Returns whether this constraint implies another — i.e., whether every type that /// satisfies this constraint also satisfies `other`. /// - /// This is used (among other places) to simplify how we display constraint sets, by removing - /// redundant constraints from a clause. + /// 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 { match (self, other) { // For two positive constraints, one range has to fully contain the other; the smaller @@ -1641,10 +1833,10 @@ mod tests { let u = BoundTypeVarInstance::synthetic(&db, "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.identity(&db), str_type); - let t_bool = ConstraintSet::range(&db, bool_type, t.identity(&db), bool_type); - let u_str = ConstraintSet::range(&db, str_type, u.identity(&db), str_type); - let u_bool = ConstraintSet::range(&db, bool_type, u.identity(&db), bool_type); + 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 = (t_str.or(&db, || t_bool)).and(&db, || u_str.or(&db, || u_bool)); let actual = constraints.node.display_graph(&db, &"").to_string(); assert_eq!(actual, expected); diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 87870e61b8..9aae4aef02 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1299,6 +1299,8 @@ pub enum KnownFunction { IsEquivalentTo, /// `ty_extensions.is_subtype_of` IsSubtypeOf, + /// `ty_extensions.is_subtype_of_given` + IsSubtypeOfGiven, /// `ty_extensions.is_assignable_to` IsAssignableTo, /// `ty_extensions.is_disjoint_from` @@ -1391,6 +1393,7 @@ impl KnownFunction { | Self::IsSingleValued | Self::IsSingleton | Self::IsSubtypeOf + | Self::IsSubtypeOfGiven | Self::GenericContext | Self::DunderAllNames | Self::EnumMembers @@ -1783,7 +1786,7 @@ impl KnownFunction { return; }; - let constraints = ConstraintSet::range(db, *lower, typevar.identity(db), *upper); + let constraints = ConstraintSet::range(db, *lower, *typevar, *upper); let tracked = TrackedConstraintSet::new(db, constraints); overload.set_return_type(Type::KnownInstance(KnownInstanceType::ConstraintSet( tracked, @@ -1796,8 +1799,7 @@ impl KnownFunction { return; }; - let constraints = - ConstraintSet::negated_range(db, *lower, typevar.identity(db), *upper); + let constraints = ConstraintSet::negated_range(db, *lower, *typevar, *upper); let tracked = TrackedConstraintSet::new(db, constraints); overload.set_return_type(Type::KnownInstance(KnownInstanceType::ConstraintSet( tracked, @@ -1892,6 +1894,7 @@ pub(crate) mod tests { KnownFunction::IsSingleton | KnownFunction::IsSubtypeOf + | KnownFunction::IsSubtypeOfGiven | KnownFunction::GenericContext | KnownFunction::DunderAllNames | KnownFunction::EnumMembers diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index 7b52a45e8d..e45e0c9ba5 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -1,5 +1,7 @@ use std::cmp::Ordering; +use salsa::plumbing::AsId; + use crate::{db::Db, types::bound_super::SuperOwnerKind}; use super::{ @@ -137,7 +139,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::ProtocolInstance(_), _) => Ordering::Less, (_, Type::ProtocolInstance(_)) => Ordering::Greater, - (Type::TypeVar(left), Type::TypeVar(right)) => left.cmp(right), + // 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, diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi index 0fe23b3535..be9b4ebff4 100644 --- a/crates/ty_vendored/ty_extensions/ty_extensions.pyi +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi @@ -63,26 +63,35 @@ def negated_range_constraint( # Ideally, these would be annotated using `TypeForm`, but that has not been # standardized yet (https://peps.python.org/pep-0747). def is_equivalent_to(type_a: Any, type_b: Any) -> ConstraintSet: - """Returns a constraint that is satisfied when `type_a` and `type_b` are + """Returns a constraint set that is satisfied when `type_a` and `type_b` are `equivalent`_ types. .. _equivalent: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent """ def is_subtype_of(ty: Any, of: Any) -> ConstraintSet: - """Returns a constraint that is satisfied when `ty` is a `subtype`_ of `of`. + """Returns a constraint set that is satisfied when `ty` is a `subtype`_ of `of`. + + .. _subtype: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence + """ + +def is_subtype_of_given( + constraints: bool | ConstraintSet, ty: Any, of: Any +) -> ConstraintSet: + """Returns a constraint set that is satisfied when `ty` is a `subtype`_ of `of`, + assuming that all of the constraints in `constraints` hold. .. _subtype: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence """ def is_assignable_to(ty: Any, to: Any) -> ConstraintSet: - """Returns a constraint that is satisfied when `ty` is `assignable`_ to `to`. + """Returns a constraint set that is satisfied when `ty` is `assignable`_ to `to`. .. _assignable: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation """ def is_disjoint_from(type_a: Any, type_b: Any) -> ConstraintSet: - """Returns a constraint that is satisfied when `type_a` and `type_b` are disjoint types. + """Returns a constraint set that is satisfied when `type_a` and `type_b` are disjoint types. Two types are disjoint if they have no inhabitants in common. """ From ae0343f848987fbb1ca84c5364602f3fb537d898 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 28 Oct 2025 12:26:05 +0100 Subject: [PATCH 059/188] [ty] Rename `inner` query for better debugging experience (#21106) --- crates/ty_python_semantic/src/types/instance.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index fb1fb5c0a7..8a4912d242 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -649,7 +649,11 @@ impl<'db> ProtocolInstanceType<'db> { /// normalised to `object`. pub(super) fn is_equivalent_to_object(self, db: &'db dyn Db) -> bool { #[salsa::tracked(cycle_initial=initial, heap_size=ruff_memory_usage::heap_size)] - fn inner<'db>(db: &'db dyn Db, protocol: ProtocolInstanceType<'db>, _: ()) -> bool { + fn is_equivalent_to_object_inner<'db>( + db: &'db dyn Db, + protocol: ProtocolInstanceType<'db>, + _: (), + ) -> bool { Type::object() .satisfies_protocol( db, @@ -666,7 +670,7 @@ impl<'db> ProtocolInstanceType<'db> { true } - inner(db, self, ()) + is_equivalent_to_object_inner(db, self, ()) } /// Return a "normalized" version of this `Protocol` type. From 4c4ddc8c29e649dad381b21c90593226687825e1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:49:26 +0000 Subject: [PATCH 060/188] Update Rust crate ignore to v0.4.24 (#20979) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Micha Reiser --- Cargo.lock | 22 +++++++++---------- crates/ruff_db/src/system/os.rs | 12 ++++++++-- .../src/session/index/ruff_settings.rs | 4 ++++ crates/ruff_workspace/src/resolver.rs | 5 +++++ 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a25626a00..216e3f115a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,7 +633,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]] @@ -642,7 +642,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1007,7 +1007,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1093,7 +1093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1523,9 +1523,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +checksum = "81776e6f9464432afcc28d03e52eb101c93b6f0566f52aef2427663e700f0403" dependencies = [ "crossbeam-deque", "globset", @@ -1690,7 +1690,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1754,7 +1754,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3545,7 +3545,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3941,7 +3941,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5021,7 +5021,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.52.0", ] [[package]] diff --git a/crates/ruff_db/src/system/os.rs b/crates/ruff_db/src/system/os.rs index b9f7e6e3db..92513b72d8 100644 --- a/crates/ruff_db/src/system/os.rs +++ b/crates/ruff_db/src/system/os.rs @@ -200,7 +200,12 @@ impl System for OsSystem { /// The walker ignores files according to [`ignore::WalkBuilder::standard_filters`] /// when setting [`WalkDirectoryBuilder::standard_filters`] to true. fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder { - WalkDirectoryBuilder::new(path, OsDirectoryWalker {}) + WalkDirectoryBuilder::new( + path, + OsDirectoryWalker { + cwd: self.current_directory().to_path_buf(), + }, + ) } fn glob( @@ -454,7 +459,9 @@ struct ListedDirectory { } #[derive(Debug)] -struct OsDirectoryWalker; +struct OsDirectoryWalker { + cwd: SystemPathBuf, +} impl DirectoryWalker for OsDirectoryWalker { fn walk( @@ -473,6 +480,7 @@ impl DirectoryWalker for OsDirectoryWalker { }; let mut builder = ignore::WalkBuilder::new(first.as_std_path()); + builder.current_dir(self.cwd.as_std_path()); builder.standard_filters(standard_filters); builder.hidden(hidden); diff --git a/crates/ruff_server/src/session/index/ruff_settings.rs b/crates/ruff_server/src/session/index/ruff_settings.rs index 93dc739d87..7fc1a99ad9 100644 --- a/crates/ruff_server/src/session/index/ruff_settings.rs +++ b/crates/ruff_server/src/session/index/ruff_settings.rs @@ -255,6 +255,10 @@ impl RuffSettingsIndex { // Add any settings within the workspace itself let mut builder = WalkBuilder::new(root); + if let Ok(cwd) = std::env::current_dir() { + builder.current_dir(cwd); + } + builder.standard_filters( respect_gitignore.unwrap_or_else(|| fallback.file_resolver.respect_gitignore), ); diff --git a/crates/ruff_workspace/src/resolver.rs b/crates/ruff_workspace/src/resolver.rs index 1452583fcd..1740cc184a 100644 --- a/crates/ruff_workspace/src/resolver.rs +++ b/crates/ruff_workspace/src/resolver.rs @@ -480,6 +480,11 @@ pub fn python_files_in_path<'a>( .ok_or_else(|| anyhow!("Expected at least one path to search for Python files"))?; // Create the `WalkBuilder`. let mut builder = WalkBuilder::new(first_path); + + if let Ok(cwd) = std::env::current_dir() { + builder.current_dir(cwd); + } + for path in rest_paths { builder.add(path); } From 7b959ef44bf3c1cb4e2f65e01dda2028b3d6fb46 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Wed, 29 Oct 2025 03:24:35 +0900 Subject: [PATCH 061/188] Avoid sending an unnecessary "clear diagnostics" message for clients supporting pull diagnostics (#21105) --- .../ruff_server/src/server/api/diagnostics.rs | 23 +++++++++++++++++-- .../server/api/notifications/did_change.rs | 6 +---- .../api/notifications/did_change_notebook.rs | 5 +--- .../notifications/did_change_watched_files.rs | 10 ++------ .../src/server/api/notifications/did_close.rs | 2 +- .../src/server/api/notifications/did_open.rs | 12 +--------- .../api/notifications/did_open_notebook.rs | 5 +--- 7 files changed, 28 insertions(+), 35 deletions(-) diff --git a/crates/ruff_server/src/server/api/diagnostics.rs b/crates/ruff_server/src/server/api/diagnostics.rs index 6f8efe47e8..2c8faab6db 100644 --- a/crates/ruff_server/src/server/api/diagnostics.rs +++ b/crates/ruff_server/src/server/api/diagnostics.rs @@ -1,4 +1,7 @@ +use lsp_types::Url; + use crate::{ + Session, lint::DiagnosticsMap, session::{Client, DocumentQuery, DocumentSnapshot}, }; @@ -19,10 +22,21 @@ pub(super) fn generate_diagnostics(snapshot: &DocumentSnapshot) -> DiagnosticsMa } pub(super) fn publish_diagnostics_for_document( - snapshot: &DocumentSnapshot, + session: &Session, + url: &Url, client: &Client, ) -> crate::server::Result<()> { - for (uri, diagnostics) in generate_diagnostics(snapshot) { + // Publish diagnostics if the client doesn't support pull diagnostics + if session.resolved_client_capabilities().pull_diagnostics { + return Ok(()); + } + + let snapshot = session + .take_snapshot(url.clone()) + .ok_or_else(|| anyhow::anyhow!("Unable to take snapshot for document with URL {url}")) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + + for (uri, diagnostics) in generate_diagnostics(&snapshot) { client .send_notification::( lsp_types::PublishDiagnosticsParams { @@ -38,9 +52,14 @@ pub(super) fn publish_diagnostics_for_document( } pub(super) fn clear_diagnostics_for_document( + session: &Session, query: &DocumentQuery, client: &Client, ) -> crate::server::Result<()> { + if session.resolved_client_capabilities().pull_diagnostics { + return Ok(()); + } + client .send_notification::( lsp_types::PublishDiagnosticsParams { diff --git a/crates/ruff_server/src/server/api/notifications/did_change.rs b/crates/ruff_server/src/server/api/notifications/did_change.rs index 8e77cb593f..5ac7a1f606 100644 --- a/crates/ruff_server/src/server/api/notifications/did_change.rs +++ b/crates/ruff_server/src/server/api/notifications/did_change.rs @@ -31,11 +31,7 @@ impl super::SyncNotificationHandler for DidChange { .update_text_document(&key, content_changes, new_version) .with_failure_code(ErrorCode::InternalError)?; - // Publish diagnostics if the client doesn't support pull diagnostics - if !session.resolved_client_capabilities().pull_diagnostics { - let snapshot = session.take_snapshot(key.into_url()).unwrap(); - publish_diagnostics_for_document(&snapshot, client)?; - } + publish_diagnostics_for_document(session, &key.into_url(), client)?; Ok(()) } diff --git a/crates/ruff_server/src/server/api/notifications/did_change_notebook.rs b/crates/ruff_server/src/server/api/notifications/did_change_notebook.rs index d092ccacb8..da11755d71 100644 --- a/crates/ruff_server/src/server/api/notifications/did_change_notebook.rs +++ b/crates/ruff_server/src/server/api/notifications/did_change_notebook.rs @@ -27,10 +27,7 @@ impl super::SyncNotificationHandler for DidChangeNotebook { .with_failure_code(ErrorCode::InternalError)?; // publish new diagnostics - let snapshot = session - .take_snapshot(key.into_url()) - .expect("snapshot should be available"); - publish_diagnostics_for_document(&snapshot, client)?; + publish_diagnostics_for_document(session, &key.into_url(), client)?; Ok(()) } diff --git a/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs b/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs index bc97231411..cb157d81f9 100644 --- a/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs +++ b/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs @@ -31,19 +31,13 @@ impl super::SyncNotificationHandler for DidChangeWatchedFiles { } else { // publish diagnostics for text documents for url in session.text_document_urls() { - let snapshot = session - .take_snapshot(url.clone()) - .expect("snapshot should be available"); - publish_diagnostics_for_document(&snapshot, client)?; + publish_diagnostics_for_document(session, url, client)?; } } // always publish diagnostics for notebook files (since they don't use pull diagnostics) for url in session.notebook_document_urls() { - let snapshot = session - .take_snapshot(url.clone()) - .expect("snapshot should be available"); - publish_diagnostics_for_document(&snapshot, client)?; + publish_diagnostics_for_document(session, url, client)?; } } diff --git a/crates/ruff_server/src/server/api/notifications/did_close.rs b/crates/ruff_server/src/server/api/notifications/did_close.rs index a3075a4846..5a482c4fcc 100644 --- a/crates/ruff_server/src/server/api/notifications/did_close.rs +++ b/crates/ruff_server/src/server/api/notifications/did_close.rs @@ -27,7 +27,7 @@ impl super::SyncNotificationHandler for DidClose { ); return Ok(()); }; - clear_diagnostics_for_document(snapshot.query(), client)?; + clear_diagnostics_for_document(session, snapshot.query(), client)?; session .close_document(&key) diff --git a/crates/ruff_server/src/server/api/notifications/did_open.rs b/crates/ruff_server/src/server/api/notifications/did_open.rs index 41a6fb6cf8..fa5f6b92df 100644 --- a/crates/ruff_server/src/server/api/notifications/did_open.rs +++ b/crates/ruff_server/src/server/api/notifications/did_open.rs @@ -1,6 +1,5 @@ use crate::TextDocument; use crate::server::Result; -use crate::server::api::LSPResult; use crate::server::api::diagnostics::publish_diagnostics_for_document; use crate::session::{Client, Session}; use lsp_types as types; @@ -30,16 +29,7 @@ impl super::SyncNotificationHandler for DidOpen { session.open_text_document(uri.clone(), document); - // Publish diagnostics if the client doesn't support pull diagnostics - if !session.resolved_client_capabilities().pull_diagnostics { - let snapshot = session - .take_snapshot(uri.clone()) - .ok_or_else(|| { - anyhow::anyhow!("Unable to take snapshot for document with URL {uri}") - }) - .with_failure_code(lsp_server::ErrorCode::InternalError)?; - publish_diagnostics_for_document(&snapshot, client)?; - } + publish_diagnostics_for_document(session, &uri, client)?; Ok(()) } diff --git a/crates/ruff_server/src/server/api/notifications/did_open_notebook.rs b/crates/ruff_server/src/server/api/notifications/did_open_notebook.rs index a75e88ecc5..3ce27168e4 100644 --- a/crates/ruff_server/src/server/api/notifications/did_open_notebook.rs +++ b/crates/ruff_server/src/server/api/notifications/did_open_notebook.rs @@ -40,10 +40,7 @@ impl super::SyncNotificationHandler for DidOpenNotebook { session.open_notebook_document(uri.clone(), notebook); // publish diagnostics - let snapshot = session - .take_snapshot(uri) - .expect("snapshot should be available"); - publish_diagnostics_for_document(&snapshot, client)?; + publish_diagnostics_for_document(session, &uri, client)?; Ok(()) } From 4d2ee41e2425c5684a0add65bc35c140f3cdcf42 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 28 Oct 2025 14:32:41 -0400 Subject: [PATCH 062/188] [ty] Move constraint set mdtest functions into `ConstraintSet` class (#21108) We have several functions in `ty_extensions` for testing our constraint set implementation. This PR refactors those functions so that they are all methods of the `ConstraintSet` class, rather than being standalone top-level functions. :tophat: to @sharkdp for pointing out that `KnownBoundMethod` gives us what we need to implement that! --- .../mdtest/type_properties/constraints.md | 210 ++++++++--------- ...type_of_given.md => implies_subtype_of.md} | 80 +++---- crates/ty_python_semantic/src/types.rs | 223 +++++++++++++----- .../ty_python_semantic/src/types/call/bind.rs | 81 ++++--- .../src/types/constraints.rs | 15 +- .../ty_python_semantic/src/types/display.rs | 12 + .../ty_python_semantic/src/types/function.rs | 44 +--- .../ty_extensions/ty_extensions.pyi | 39 +-- 8 files changed, 407 insertions(+), 297 deletions(-) rename crates/ty_python_semantic/resources/mdtest/type_properties/{is_subtype_of_given.md => implies_subtype_of.md} (70%) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md index 5d1f4e7142..00a3e2837f 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md @@ -34,7 +34,7 @@ upper bound. ```py from typing import Any, final, Never, Sequence -from ty_extensions import range_constraint +from ty_extensions import ConstraintSet class Super: ... class Base(Super): ... @@ -45,7 +45,7 @@ class Unrelated: ... def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super)] - reveal_type(range_constraint(Sub, T, Super)) + reveal_type(ConstraintSet.range(Sub, T, Super)) ``` Every type is a supertype of `Never`, so a lower bound of `Never` is the same as having no lower @@ -54,7 +54,7 @@ bound. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(T@_ ≤ Base)] - reveal_type(range_constraint(Never, T, Base)) + reveal_type(ConstraintSet.range(Never, T, Base)) ``` Similarly, every type is a subtype of `object`, so an upper bound of `object` is the same as having @@ -63,7 +63,7 @@ no upper bound. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(Base ≤ T@_)] - reveal_type(range_constraint(Base, T, object)) + reveal_type(ConstraintSet.range(Base, T, object)) ``` And a range constraint with _both_ a lower bound of `Never` and an upper bound of `object` does not @@ -72,7 +72,7 @@ constrain the typevar at all. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[always] - reveal_type(range_constraint(Never, T, object)) + reveal_type(ConstraintSet.range(Never, T, object)) ``` If the lower bound and upper bounds are "inverted" (the upper bound is a subtype of the lower bound) @@ -81,9 +81,9 @@ or incomparable, then there is no type that can satisfy the constraint. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[never] - reveal_type(range_constraint(Super, T, Sub)) + reveal_type(ConstraintSet.range(Super, T, Sub)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(range_constraint(Base, T, Unrelated)) + reveal_type(ConstraintSet.range(Base, T, Unrelated)) ``` The lower and upper bound can be the same type, in which case the typevar can only be specialized to @@ -92,7 +92,7 @@ that specific type. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(T@_ = Base)] - reveal_type(range_constraint(Base, T, Base)) + reveal_type(ConstraintSet.range(Base, T, Base)) ``` Constraints can only refer to fully static types, so the lower and upper bounds are transformed into @@ -101,14 +101,14 @@ their bottom and top materializations, respectively. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(Base ≤ T@_)] - reveal_type(range_constraint(Base, T, Any)) + reveal_type(ConstraintSet.range(Base, T, Any)) # revealed: ty_extensions.ConstraintSet[(Sequence[Base] ≤ T@_ ≤ Sequence[object])] - reveal_type(range_constraint(Sequence[Base], T, Sequence[Any])) + reveal_type(ConstraintSet.range(Sequence[Base], T, Sequence[Any])) # revealed: ty_extensions.ConstraintSet[(T@_ ≤ Base)] - reveal_type(range_constraint(Any, T, Base)) + reveal_type(ConstraintSet.range(Any, T, Base)) # revealed: ty_extensions.ConstraintSet[(Sequence[Never] ≤ T@_ ≤ Sequence[Base])] - reveal_type(range_constraint(Sequence[Any], T, Sequence[Base])) + reveal_type(ConstraintSet.range(Sequence[Any], T, Sequence[Base])) ``` ### Negated range @@ -119,7 +119,7 @@ strict subtype of the lower bound, a strict supertype of the upper bound, or inc ```py from typing import Any, final, Never, Sequence -from ty_extensions import negated_range_constraint +from ty_extensions import ConstraintSet class Super: ... class Base(Super): ... @@ -130,7 +130,7 @@ class Unrelated: ... def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Super)] - reveal_type(negated_range_constraint(Sub, T, Super)) + reveal_type(~ConstraintSet.range(Sub, T, Super)) ``` Every type is a supertype of `Never`, so a lower bound of `Never` is the same as having no lower @@ -139,7 +139,7 @@ bound. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base)] - reveal_type(negated_range_constraint(Never, T, Base)) + reveal_type(~ConstraintSet.range(Never, T, Base)) ``` Similarly, every type is a subtype of `object`, so an upper bound of `object` is the same as having @@ -148,7 +148,7 @@ no upper bound. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_)] - reveal_type(negated_range_constraint(Base, T, object)) + reveal_type(~ConstraintSet.range(Base, T, object)) ``` And a negated range constraint with _both_ a lower bound of `Never` and an upper bound of `object` @@ -157,7 +157,7 @@ cannot be satisfied at all. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[never] - reveal_type(negated_range_constraint(Never, T, object)) + reveal_type(~ConstraintSet.range(Never, T, object)) ``` If the lower bound and upper bounds are "inverted" (the upper bound is a subtype of the lower bound) @@ -166,9 +166,9 @@ or incomparable, then the negated range constraint can always be satisfied. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[always] - reveal_type(negated_range_constraint(Super, T, Sub)) + reveal_type(~ConstraintSet.range(Super, T, Sub)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(negated_range_constraint(Base, T, Unrelated)) + reveal_type(~ConstraintSet.range(Base, T, Unrelated)) ``` The lower and upper bound can be the same type, in which case the typevar can be specialized to any @@ -177,7 +177,7 @@ type other than that specific type. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(T@_ ≠ Base)] - reveal_type(negated_range_constraint(Base, T, Base)) + reveal_type(~ConstraintSet.range(Base, T, Base)) ``` Constraints can only refer to fully static types, so the lower and upper bounds are transformed into @@ -186,14 +186,14 @@ their bottom and top materializations, respectively. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_)] - reveal_type(negated_range_constraint(Base, T, Any)) + reveal_type(~ConstraintSet.range(Base, T, Any)) # revealed: ty_extensions.ConstraintSet[¬(Sequence[Base] ≤ T@_ ≤ Sequence[object])] - reveal_type(negated_range_constraint(Sequence[Base], T, Sequence[Any])) + reveal_type(~ConstraintSet.range(Sequence[Base], T, Sequence[Any])) # revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base)] - reveal_type(negated_range_constraint(Any, T, Base)) + reveal_type(~ConstraintSet.range(Any, T, Base)) # revealed: ty_extensions.ConstraintSet[¬(Sequence[Never] ≤ T@_ ≤ Sequence[Base])] - reveal_type(negated_range_constraint(Sequence[Any], T, Sequence[Base])) + reveal_type(~ConstraintSet.range(Sequence[Any], T, Sequence[Base])) ``` ## Intersection @@ -204,7 +204,7 @@ cases, we can simplify the result of an intersection. ### Different typevars ```py -from ty_extensions import range_constraint, negated_range_constraint +from ty_extensions import ConstraintSet class Super: ... class Base(Super): ... @@ -216,9 +216,9 @@ We cannot simplify the intersection of constraints that refer to different typev ```py def _[T, U]() -> None: # revealed: ty_extensions.ConstraintSet[((Sub ≤ T@_ ≤ Base) ∧ (Sub ≤ U@_ ≤ Base))] - reveal_type(range_constraint(Sub, T, Base) & range_constraint(Sub, U, Base)) + reveal_type(ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Sub, U, Base)) # revealed: ty_extensions.ConstraintSet[(¬(Sub ≤ T@_ ≤ Base) ∧ ¬(Sub ≤ U@_ ≤ Base))] - reveal_type(negated_range_constraint(Sub, T, Base) & negated_range_constraint(Sub, U, Base)) + reveal_type(~ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Sub, U, Base)) ``` ### Intersection of two ranges @@ -227,7 +227,7 @@ The intersection of two ranges is where the ranges "overlap". ```py from typing import final -from ty_extensions import range_constraint +from ty_extensions import ConstraintSet class Super: ... class Base(Super): ... @@ -239,13 +239,13 @@ class Unrelated: ... def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)] - reveal_type(range_constraint(SubSub, T, Base) & range_constraint(Sub, T, Super)) + reveal_type(ConstraintSet.range(SubSub, T, Base) & ConstraintSet.range(Sub, T, Super)) # revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)] - reveal_type(range_constraint(SubSub, T, Super) & range_constraint(Sub, T, Base)) + reveal_type(ConstraintSet.range(SubSub, T, Super) & ConstraintSet.range(Sub, T, Base)) # revealed: ty_extensions.ConstraintSet[(T@_ = Base)] - reveal_type(range_constraint(Sub, T, Base) & range_constraint(Base, T, Super)) + reveal_type(ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Base, T, Super)) # revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super)] - reveal_type(range_constraint(Sub, T, Super) & range_constraint(Sub, T, Super)) + reveal_type(ConstraintSet.range(Sub, T, Super) & ConstraintSet.range(Sub, T, Super)) ``` If they don't overlap, the intersection is empty. @@ -253,9 +253,9 @@ If they don't overlap, the intersection is empty. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[never] - reveal_type(range_constraint(SubSub, T, Sub) & range_constraint(Base, T, Super)) + reveal_type(ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Base, T, Super)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(range_constraint(SubSub, T, Sub) & range_constraint(Unrelated, T, object)) + reveal_type(ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Unrelated, T, object)) ``` ### Intersection of a range and a negated range @@ -266,7 +266,7 @@ the intersection as removing the hole from the range constraint. ```py from typing import final, Never -from ty_extensions import range_constraint, negated_range_constraint +from ty_extensions import ConstraintSet class Super: ... class Base(Super): ... @@ -282,9 +282,9 @@ If the negative range completely contains the positive range, then the intersect ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[never] - reveal_type(range_constraint(Sub, T, Base) & negated_range_constraint(SubSub, T, Super)) + reveal_type(ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(SubSub, T, Super)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(range_constraint(Sub, T, Base) & negated_range_constraint(Sub, T, Base)) + reveal_type(ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Sub, T, Base)) ``` If the negative range is disjoint from the positive range, the negative range doesn't remove @@ -293,11 +293,11 @@ anything; the intersection is the positive range. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)] - reveal_type(range_constraint(Sub, T, Base) & negated_range_constraint(Never, T, Unrelated)) + reveal_type(ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Never, T, Unrelated)) # revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Sub)] - reveal_type(range_constraint(SubSub, T, Sub) & negated_range_constraint(Base, T, Super)) + reveal_type(ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super)) # revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super)] - reveal_type(range_constraint(Base, T, Super) & negated_range_constraint(SubSub, T, Sub)) + reveal_type(ConstraintSet.range(Base, T, Super) & ~ConstraintSet.range(SubSub, T, Sub)) ``` Otherwise we clip the negative constraint to the mininum range that overlaps with the positive @@ -306,9 +306,9 @@ range. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[((SubSub ≤ T@_ ≤ Base) ∧ ¬(Sub ≤ T@_ ≤ Base))] - reveal_type(range_constraint(SubSub, T, Base) & negated_range_constraint(Sub, T, Super)) + reveal_type(ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Super)) # revealed: ty_extensions.ConstraintSet[((SubSub ≤ T@_ ≤ Super) ∧ ¬(Sub ≤ T@_ ≤ Base))] - reveal_type(range_constraint(SubSub, T, Super) & negated_range_constraint(Sub, T, Base)) + reveal_type(ConstraintSet.range(SubSub, T, Super) & ~ConstraintSet.range(Sub, T, Base)) ``` ### Intersection of two negated ranges @@ -318,7 +318,7 @@ smaller constraint. For negated ranges, the smaller constraint is the one with t ```py from typing import final -from ty_extensions import negated_range_constraint +from ty_extensions import ConstraintSet class Super: ... class Base(Super): ... @@ -330,9 +330,9 @@ class Unrelated: ... def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[¬(SubSub ≤ T@_ ≤ Super)] - reveal_type(negated_range_constraint(SubSub, T, Super) & negated_range_constraint(Sub, T, Base)) + reveal_type(~ConstraintSet.range(SubSub, T, Super) & ~ConstraintSet.range(Sub, T, Base)) # revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Super)] - reveal_type(negated_range_constraint(Sub, T, Super) & negated_range_constraint(Sub, T, Super)) + reveal_type(~ConstraintSet.range(Sub, T, Super) & ~ConstraintSet.range(Sub, T, Super)) ``` Otherwise, the union cannot be simplified. @@ -340,11 +340,11 @@ Otherwise, the union cannot be simplified. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(¬(Base ≤ T@_ ≤ Super) ∧ ¬(Sub ≤ T@_ ≤ Base))] - reveal_type(negated_range_constraint(Sub, T, Base) & negated_range_constraint(Base, T, Super)) + reveal_type(~ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Base, T, Super)) # revealed: ty_extensions.ConstraintSet[(¬(Base ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Sub))] - reveal_type(negated_range_constraint(SubSub, T, Sub) & negated_range_constraint(Base, T, Super)) + reveal_type(~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super)) # revealed: ty_extensions.ConstraintSet[(¬(SubSub ≤ T@_ ≤ Sub) ∧ ¬(Unrelated ≤ T@_))] - reveal_type(negated_range_constraint(SubSub, T, Sub) & negated_range_constraint(Unrelated, T, object)) + reveal_type(~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Unrelated, T, object)) ``` In particular, the following does not simplify, even though it seems like it could simplify to @@ -361,7 +361,7 @@ that type _is_ in `SubSub ≤ T ≤ Super`, it is not correct to simplify the un ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(¬(Sub ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Base))] - reveal_type(negated_range_constraint(SubSub, T, Base) & negated_range_constraint(Sub, T, Super)) + reveal_type(~ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Super)) ``` ## Union @@ -372,7 +372,7 @@ can simplify the result of an union. ### Different typevars ```py -from ty_extensions import range_constraint, negated_range_constraint +from ty_extensions import ConstraintSet class Super: ... class Base(Super): ... @@ -384,9 +384,9 @@ We cannot simplify the union of constraints that refer to different typevars. ```py def _[T, U]() -> None: # revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) ∨ (Sub ≤ U@_ ≤ Base)] - reveal_type(range_constraint(Sub, T, Base) | range_constraint(Sub, U, Base)) + reveal_type(ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Sub, U, Base)) # revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base) ∨ ¬(Sub ≤ U@_ ≤ Base)] - reveal_type(negated_range_constraint(Sub, T, Base) | negated_range_constraint(Sub, U, Base)) + reveal_type(~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Sub, U, Base)) ``` ### Union of two ranges @@ -396,7 +396,7 @@ bounds. ```py from typing import final -from ty_extensions import range_constraint +from ty_extensions import ConstraintSet class Super: ... class Base(Super): ... @@ -408,9 +408,9 @@ class Unrelated: ... def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Super)] - reveal_type(range_constraint(SubSub, T, Super) | range_constraint(Sub, T, Base)) + reveal_type(ConstraintSet.range(SubSub, T, Super) | ConstraintSet.range(Sub, T, Base)) # revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super)] - reveal_type(range_constraint(Sub, T, Super) | range_constraint(Sub, T, Super)) + reveal_type(ConstraintSet.range(Sub, T, Super) | ConstraintSet.range(Sub, T, Super)) ``` Otherwise, the union cannot be simplified. @@ -418,11 +418,11 @@ Otherwise, the union cannot be simplified. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super) ∨ (Sub ≤ T@_ ≤ Base)] - reveal_type(range_constraint(Sub, T, Base) | range_constraint(Base, T, Super)) + reveal_type(ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Base, T, Super)) # revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super) ∨ (SubSub ≤ T@_ ≤ Sub)] - reveal_type(range_constraint(SubSub, T, Sub) | range_constraint(Base, T, Super)) + reveal_type(ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super)) # revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Sub) ∨ (Unrelated ≤ T@_)] - reveal_type(range_constraint(SubSub, T, Sub) | range_constraint(Unrelated, T, object)) + reveal_type(ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Unrelated, T, object)) ``` In particular, the following does not simplify, even though it seems like it could simplify to @@ -438,7 +438,7 @@ not include `Sub`. That means it should not be in the union. Since that type _is ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super) ∨ (SubSub ≤ T@_ ≤ Base)] - reveal_type(range_constraint(SubSub, T, Base) | range_constraint(Sub, T, Super)) + reveal_type(ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super)) ``` ### Union of a range and a negated range @@ -449,7 +449,7 @@ the union as filling part of the hole with the types from the range constraint. ```py from typing import final, Never -from ty_extensions import range_constraint, negated_range_constraint +from ty_extensions import ConstraintSet class Super: ... class Base(Super): ... @@ -465,9 +465,9 @@ If the positive range completely contains the negative range, then the union is ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[always] - reveal_type(negated_range_constraint(Sub, T, Base) | range_constraint(SubSub, T, Super)) + reveal_type(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(SubSub, T, Super)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(negated_range_constraint(Sub, T, Base) | range_constraint(Sub, T, Base)) + reveal_type(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Sub, T, Base)) ``` If the negative range is disjoint from the positive range, the positive range doesn't add anything; @@ -476,11 +476,11 @@ the union is the negative range. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)] - reveal_type(negated_range_constraint(Sub, T, Base) | range_constraint(Never, T, Unrelated)) + reveal_type(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Never, T, Unrelated)) # revealed: ty_extensions.ConstraintSet[¬(SubSub ≤ T@_ ≤ Sub)] - reveal_type(negated_range_constraint(SubSub, T, Sub) | range_constraint(Base, T, Super)) + reveal_type(~ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super)) # revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_ ≤ Super)] - reveal_type(negated_range_constraint(Base, T, Super) | range_constraint(SubSub, T, Sub)) + reveal_type(~ConstraintSet.range(Base, T, Super) | ConstraintSet.range(SubSub, T, Sub)) ``` Otherwise we clip the positive constraint to the mininum range that overlaps with the negative @@ -489,9 +489,9 @@ range. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) ∨ ¬(SubSub ≤ T@_ ≤ Base)] - reveal_type(negated_range_constraint(SubSub, T, Base) | range_constraint(Sub, T, Super)) + reveal_type(~ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super)) # revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) ∨ ¬(SubSub ≤ T@_ ≤ Super)] - reveal_type(negated_range_constraint(SubSub, T, Super) | range_constraint(Sub, T, Base)) + reveal_type(~ConstraintSet.range(SubSub, T, Super) | ConstraintSet.range(Sub, T, Base)) ``` ### Union of two negated ranges @@ -500,7 +500,7 @@ The union of two negated ranges has a hole where the ranges "overlap". ```py from typing import final -from ty_extensions import negated_range_constraint +from ty_extensions import ConstraintSet class Super: ... class Base(Super): ... @@ -512,13 +512,13 @@ class Unrelated: ... def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)] - reveal_type(negated_range_constraint(SubSub, T, Base) | negated_range_constraint(Sub, T, Super)) + reveal_type(~ConstraintSet.range(SubSub, T, Base) | ~ConstraintSet.range(Sub, T, Super)) # revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)] - reveal_type(negated_range_constraint(SubSub, T, Super) | negated_range_constraint(Sub, T, Base)) + reveal_type(~ConstraintSet.range(SubSub, T, Super) | ~ConstraintSet.range(Sub, T, Base)) # revealed: ty_extensions.ConstraintSet[(T@_ ≠ Base)] - reveal_type(negated_range_constraint(Sub, T, Base) | negated_range_constraint(Base, T, Super)) + reveal_type(~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Base, T, Super)) # revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Super)] - reveal_type(negated_range_constraint(Sub, T, Super) | negated_range_constraint(Sub, T, Super)) + reveal_type(~ConstraintSet.range(Sub, T, Super) | ~ConstraintSet.range(Sub, T, Super)) ``` If the holes don't overlap, the union is always satisfied. @@ -526,9 +526,9 @@ If the holes don't overlap, the union is always satisfied. ```py def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[always] - reveal_type(negated_range_constraint(SubSub, T, Sub) | negated_range_constraint(Base, T, Super)) + reveal_type(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Base, T, Super)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(negated_range_constraint(SubSub, T, Sub) | negated_range_constraint(Unrelated, T, object)) + reveal_type(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Unrelated, T, object)) ``` ## Negation @@ -537,7 +537,7 @@ def _[T]() -> None: ```py from typing import Never -from ty_extensions import range_constraint +from ty_extensions import ConstraintSet class Super: ... class Base(Super): ... @@ -545,20 +545,20 @@ class Sub(Base): ... def _[T]() -> None: # revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)] - reveal_type(~range_constraint(Sub, T, Base)) + reveal_type(~ConstraintSet.range(Sub, T, Base)) # revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base)] - reveal_type(~range_constraint(Never, T, Base)) + reveal_type(~ConstraintSet.range(Never, T, Base)) # revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_)] - reveal_type(~range_constraint(Sub, T, object)) + reveal_type(~ConstraintSet.range(Sub, T, object)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(~range_constraint(Never, T, object)) + reveal_type(~ConstraintSet.range(Never, T, object)) ``` The union of a range constraint and its negation should always be satisfiable. ```py def _[T]() -> None: - constraint = range_constraint(Sub, T, Base) + constraint = ConstraintSet.range(Sub, T, Base) # revealed: ty_extensions.ConstraintSet[always] reveal_type(constraint | ~constraint) ``` @@ -567,7 +567,7 @@ def _[T]() -> None: ```py from typing import final, Never -from ty_extensions import range_constraint +from ty_extensions import ConstraintSet class Base: ... @@ -576,20 +576,20 @@ class Unrelated: ... def _[T, U]() -> None: # revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base) ∨ ¬(U@_ ≤ Base)] - reveal_type(~(range_constraint(Never, T, Base) & range_constraint(Never, U, Base))) + reveal_type(~(ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base))) ``` The union of a constraint and its negation should always be satisfiable. ```py def _[T, U]() -> None: - c1 = range_constraint(Never, T, Base) & range_constraint(Never, U, Base) + c1 = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base) # revealed: ty_extensions.ConstraintSet[always] reveal_type(c1 | ~c1) # revealed: ty_extensions.ConstraintSet[always] reveal_type(~c1 | c1) - c2 = range_constraint(Unrelated, T, object) & range_constraint(Unrelated, U, object) + c2 = ConstraintSet.range(Unrelated, T, object) & ConstraintSet.range(Unrelated, U, object) # revealed: ty_extensions.ConstraintSet[always] reveal_type(c2 | ~c2) # revealed: ty_extensions.ConstraintSet[always] @@ -614,19 +614,19 @@ since we always hide `Never` lower bounds and `object` upper bounds. ```py from typing import Never -from ty_extensions import range_constraint +from ty_extensions import ConstraintSet def f[S, T](): # revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)] - reveal_type(range_constraint(Never, S, T)) + reveal_type(ConstraintSet.range(Never, S, T)) # revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)] - reveal_type(range_constraint(S, T, object)) + reveal_type(ConstraintSet.range(S, T, object)) def f[T, S](): # revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)] - reveal_type(range_constraint(Never, S, T)) + reveal_type(ConstraintSet.range(Never, S, T)) # revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)] - reveal_type(range_constraint(S, T, object)) + reveal_type(ConstraintSet.range(S, T, object)) ``` Equivalence constraints are similar; internally we arbitrarily choose the "earlier" typevar to be @@ -635,15 +635,15 @@ the constraint, and the other the bound. But we display the result the same way ```py def f[S, T](): # revealed: ty_extensions.ConstraintSet[(S@f = T@f)] - reveal_type(range_constraint(T, S, T)) + reveal_type(ConstraintSet.range(T, S, T)) # revealed: ty_extensions.ConstraintSet[(S@f = T@f)] - reveal_type(range_constraint(S, T, S)) + reveal_type(ConstraintSet.range(S, T, S)) def f[T, S](): # revealed: ty_extensions.ConstraintSet[(S@f = T@f)] - reveal_type(range_constraint(T, S, T)) + reveal_type(ConstraintSet.range(T, S, T)) # revealed: ty_extensions.ConstraintSet[(S@f = T@f)] - reveal_type(range_constraint(S, T, S)) + reveal_type(ConstraintSet.range(S, T, S)) ``` But in the case of `S ≤ T ≤ U`, we end up with an ambiguity. Depending on the typevar ordering, that @@ -654,7 +654,7 @@ def f[S, T, U](): # Could be either of: # ty_extensions.ConstraintSet[(S@f ≤ T@f ≤ U@f)] # ty_extensions.ConstraintSet[(S@f ≤ T@f) ∧ (T@f ≤ U@f)] - # reveal_type(range_constraint(S, T, U)) + # reveal_type(ConstraintSet.range(S, T, U)) ... ``` @@ -668,13 +668,13 @@ This section contains several examples that show that we simplify the DNF formul before displaying it. ```py -from ty_extensions import range_constraint +from ty_extensions import ConstraintSet def f[T, U](): - t1 = range_constraint(str, T, str) - t2 = range_constraint(bool, T, bool) - u1 = range_constraint(str, U, str) - u2 = range_constraint(bool, U, bool) + t1 = ConstraintSet.range(str, T, str) + t2 = ConstraintSet.range(bool, T, bool) + u1 = ConstraintSet.range(str, U, str) + u2 = ConstraintSet.range(bool, U, bool) # revealed: ty_extensions.ConstraintSet[(T@f = bool) ∨ (T@f = str)] reveal_type(t1 | t2) @@ -692,8 +692,8 @@ from typing import Never from ty_extensions import static_assert def f[T](): - t_int = range_constraint(Never, T, int) - t_bool = range_constraint(Never, T, bool) + t_int = ConstraintSet.range(Never, T, int) + t_bool = ConstraintSet.range(Never, T, bool) # `T ≤ bool` implies `T ≤ int`: if a type satisfies the former, it must always satisfy the # latter. We can turn that into a constraint set, using the equivalence `p → q == ¬p ∨ q`: @@ -707,7 +707,7 @@ def f[T](): # "domain", which maps valid inputs to `true` and invalid inputs to `false`. This means that two # constraint sets that are both always satisfied will not be identical if they have different # domains! - always = range_constraint(Never, T, object) + always = ConstraintSet.range(Never, T, object) # revealed: ty_extensions.ConstraintSet[always] reveal_type(always) static_assert(always) @@ -721,11 +721,11 @@ intersections whose elements appear in different orders. ```py from typing import Never -from ty_extensions import range_constraint +from ty_extensions import ConstraintSet def f[T](): # revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)] - reveal_type(range_constraint(Never, T, str | int)) + reveal_type(ConstraintSet.range(Never, T, str | int)) # revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)] - reveal_type(range_constraint(Never, T, int | str)) + reveal_type(ConstraintSet.range(Never, T, int | str)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of_given.md b/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md similarity index 70% rename from crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of_given.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md index c1a0577fa3..7a276b6d2c 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of_given.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md @@ -5,7 +5,7 @@ python-version = "3.12" ``` -This file tests the _constraint implication_ relationship between types, aka `is_subtype_of_given`, +This file tests the _constraint implication_ relationship between types, aka `implies_subtype_of`, which tests whether one type is a [subtype][subtyping] of another _assuming that the constraints in a particular constraint set hold_. @@ -16,14 +16,14 @@ fully static type that is not a typevar. It can _contain_ a typevar, though — considered concrete.) ```py -from ty_extensions import is_subtype_of, is_subtype_of_given, static_assert +from ty_extensions import ConstraintSet, is_subtype_of, static_assert def equivalent_to_other_relationships[T](): static_assert(is_subtype_of(bool, int)) - static_assert(is_subtype_of_given(True, bool, int)) + static_assert(ConstraintSet.always().implies_subtype_of(bool, int)) static_assert(not is_subtype_of(bool, str)) - static_assert(not is_subtype_of_given(True, bool, str)) + static_assert(not ConstraintSet.always().implies_subtype_of(bool, str)) ``` Moreover, for concrete types, the answer does not depend on which constraint set we are considering. @@ -32,16 +32,16 @@ there isn't a valid specialization for the typevars we are considering. ```py from typing import Never -from ty_extensions import range_constraint +from ty_extensions import ConstraintSet def even_given_constraints[T](): - constraints = range_constraint(Never, T, int) - static_assert(is_subtype_of_given(constraints, bool, int)) - static_assert(not is_subtype_of_given(constraints, bool, str)) + constraints = ConstraintSet.range(Never, T, int) + static_assert(constraints.implies_subtype_of(bool, int)) + static_assert(not constraints.implies_subtype_of(bool, str)) def even_given_unsatisfiable_constraints(): - static_assert(is_subtype_of_given(False, bool, int)) - static_assert(not is_subtype_of_given(False, bool, str)) + static_assert(ConstraintSet.never().implies_subtype_of(bool, int)) + static_assert(not ConstraintSet.never().implies_subtype_of(bool, str)) ``` ## Type variables @@ -141,37 +141,37 @@ considering. ```py from typing import Never -from ty_extensions import is_subtype_of_given, range_constraint, static_assert +from ty_extensions import ConstraintSet, static_assert def given_constraints[T](): - static_assert(not is_subtype_of_given(True, T, int)) - static_assert(not is_subtype_of_given(True, T, bool)) - static_assert(not is_subtype_of_given(True, T, str)) + static_assert(not ConstraintSet.always().implies_subtype_of(T, int)) + static_assert(not ConstraintSet.always().implies_subtype_of(T, bool)) + static_assert(not ConstraintSet.always().implies_subtype_of(T, str)) # These are vacuously true; false implies anything - static_assert(is_subtype_of_given(False, T, int)) - static_assert(is_subtype_of_given(False, T, bool)) - static_assert(is_subtype_of_given(False, T, str)) + static_assert(ConstraintSet.never().implies_subtype_of(T, int)) + static_assert(ConstraintSet.never().implies_subtype_of(T, bool)) + static_assert(ConstraintSet.never().implies_subtype_of(T, str)) - given_int = range_constraint(Never, T, int) - static_assert(is_subtype_of_given(given_int, T, int)) - static_assert(not is_subtype_of_given(given_int, T, bool)) - static_assert(not is_subtype_of_given(given_int, T, str)) + given_int = ConstraintSet.range(Never, T, int) + static_assert(given_int.implies_subtype_of(T, int)) + static_assert(not given_int.implies_subtype_of(T, bool)) + static_assert(not given_int.implies_subtype_of(T, str)) - given_bool = range_constraint(Never, T, bool) - static_assert(is_subtype_of_given(given_bool, T, int)) - static_assert(is_subtype_of_given(given_bool, T, bool)) - static_assert(not is_subtype_of_given(given_bool, T, str)) + given_bool = ConstraintSet.range(Never, T, bool) + static_assert(given_bool.implies_subtype_of(T, int)) + static_assert(given_bool.implies_subtype_of(T, bool)) + static_assert(not given_bool.implies_subtype_of(T, str)) given_both = given_bool & given_int - static_assert(is_subtype_of_given(given_both, T, int)) - static_assert(is_subtype_of_given(given_both, T, bool)) - static_assert(not is_subtype_of_given(given_both, T, str)) + static_assert(given_both.implies_subtype_of(T, int)) + static_assert(given_both.implies_subtype_of(T, bool)) + static_assert(not given_both.implies_subtype_of(T, str)) - given_str = range_constraint(Never, T, str) - static_assert(not is_subtype_of_given(given_str, T, int)) - static_assert(not is_subtype_of_given(given_str, T, bool)) - static_assert(is_subtype_of_given(given_str, T, str)) + given_str = ConstraintSet.range(Never, T, str) + static_assert(not given_str.implies_subtype_of(T, int)) + static_assert(not given_str.implies_subtype_of(T, bool)) + static_assert(given_str.implies_subtype_of(T, str)) ``` This might require propagating constraints from other typevars. @@ -179,20 +179,20 @@ This might require propagating constraints from other typevars. ```py def mutually_constrained[T, U](): # If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well. - given_int = range_constraint(U, T, U) & range_constraint(Never, U, int) + given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) # TODO: no static-assert-error # error: [static-assert-error] - static_assert(is_subtype_of_given(given_int, T, int)) - static_assert(not is_subtype_of_given(given_int, T, bool)) - static_assert(not is_subtype_of_given(given_int, T, str)) + static_assert(given_int.implies_subtype_of(T, int)) + static_assert(not given_int.implies_subtype_of(T, bool)) + static_assert(not given_int.implies_subtype_of(T, str)) # If [T ≤ U ∧ U ≤ int], then [T ≤ int] must be true as well. - given_int = range_constraint(Never, T, U) & range_constraint(Never, U, int) + given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) # TODO: no static-assert-error # error: [static-assert-error] - static_assert(is_subtype_of_given(given_int, T, int)) - static_assert(not is_subtype_of_given(given_int, T, bool)) - static_assert(not is_subtype_of_given(given_int, T, str)) + static_assert(given_int.implies_subtype_of(T, int)) + static_assert(not given_int.implies_subtype_of(T, bool)) + static_assert(not given_int.implies_subtype_of(T, str)) ``` [subtyping]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 8f87593986..5efb65d289 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4119,6 +4119,39 @@ impl<'db> Type<'db> { Place::bound(Type::KnownBoundMethod(KnownBoundMethodType::PathOpen)).into() } + Type::ClassLiteral(class) + if name == "range" && class.is_known(db, KnownClass::ConstraintSet) => + { + Place::bound(Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetRange, + )) + .into() + } + Type::ClassLiteral(class) + if name == "always" && class.is_known(db, KnownClass::ConstraintSet) => + { + Place::bound(Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetAlways, + )) + .into() + } + Type::ClassLiteral(class) + if name == "never" && class.is_known(db, KnownClass::ConstraintSet) => + { + Place::bound(Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetNever, + )) + .into() + } + Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked)) + if name == "implies_subtype_of" => + { + Place::bound(Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(tracked), + )) + .into() + } + Type::ClassLiteral(class) if name == "__get__" && class.is_known(db, KnownClass::FunctionType) => { @@ -4833,51 +4866,6 @@ impl<'db> Type<'db> { ) .into(), - Some(KnownFunction::IsSubtypeOfGiven) => Binding::single( - self, - Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("constraints"))) - .with_annotated_type(UnionType::from_elements( - db, - [ - KnownClass::Bool.to_instance(db), - KnownClass::ConstraintSet.to_instance(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()), - ]), - Some(KnownClass::ConstraintSet.to_instance(db)), - ), - ) - .into(), - - Some(KnownFunction::RangeConstraint | KnownFunction::NegatedRangeConstraint) => { - Binding::single( - self, - Signature::new( - Parameters::new([ - 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()), - ]), - Some(KnownClass::ConstraintSet.to_instance(db)), - ), - ) - .into() - } - Some(KnownFunction::IsSingleton | KnownFunction::IsSingleValued) => { Binding::single( self, @@ -6918,7 +6906,14 @@ impl<'db> Type<'db> { | Type::AlwaysTruthy | Type::AlwaysFalsy | Type::WrapperDescriptor(_) - | Type::KnownBoundMethod(KnownBoundMethodType::StrStartswith(_) | KnownBoundMethodType::PathOpen) + | Type::KnownBoundMethod( + KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::PathOpen + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + ) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_) // A non-generic class never needs to be specialized. A generic class is specialized @@ -7064,7 +7059,12 @@ impl<'db> Type<'db> { | Type::AlwaysFalsy | Type::WrapperDescriptor(_) | Type::KnownBoundMethod( - KnownBoundMethodType::StrStartswith(_) | KnownBoundMethodType::PathOpen, + KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::PathOpen + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), ) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_) @@ -10318,6 +10318,12 @@ pub enum KnownBoundMethodType<'db> { StrStartswith(StringLiteralType<'db>), /// Method wrapper for `Path.open`, PathOpen, + + // ConstraintSet methods + ConstraintSetRange, + ConstraintSetAlways, + ConstraintSetNever, + ConstraintSetImpliesSubtypeOf(TrackedConstraintSet<'db>), } pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -10341,7 +10347,11 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size KnownBoundMethodType::StrStartswith(string_literal) => { visitor.visit_type(db, Type::StringLiteral(string_literal)); } - KnownBoundMethodType::PathOpen => {} + KnownBoundMethodType::PathOpen + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => {} } } @@ -10393,9 +10403,23 @@ impl<'db> KnownBoundMethodType<'db> { ConstraintSet::from(self == other) } - (KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen) => { - ConstraintSet::from(true) - } + (KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen) + | ( + KnownBoundMethodType::ConstraintSetRange, + KnownBoundMethodType::ConstraintSetRange, + ) + | ( + KnownBoundMethodType::ConstraintSetAlways, + KnownBoundMethodType::ConstraintSetAlways, + ) + | ( + KnownBoundMethodType::ConstraintSetNever, + KnownBoundMethodType::ConstraintSetNever, + ) + | ( + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), + ) => ConstraintSet::from(true), ( KnownBoundMethodType::FunctionTypeDunderGet(_) @@ -10403,13 +10427,21 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::PropertyDunderGet(_) | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::PathOpen, + | KnownBoundMethodType::PathOpen + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), KnownBoundMethodType::FunctionTypeDunderGet(_) | KnownBoundMethodType::FunctionTypeDunderCall(_) | KnownBoundMethodType::PropertyDunderGet(_) | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::PathOpen, + | KnownBoundMethodType::PathOpen + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), ) => ConstraintSet::from(false), } } @@ -10445,9 +10477,26 @@ impl<'db> KnownBoundMethodType<'db> { ConstraintSet::from(self == other) } - (KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen) => { - ConstraintSet::from(true) - } + (KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen) + | ( + KnownBoundMethodType::ConstraintSetRange, + KnownBoundMethodType::ConstraintSetRange, + ) + | ( + KnownBoundMethodType::ConstraintSetAlways, + KnownBoundMethodType::ConstraintSetAlways, + ) + | ( + KnownBoundMethodType::ConstraintSetNever, + KnownBoundMethodType::ConstraintSetNever, + ) => ConstraintSet::from(true), + + ( + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(left_constraints), + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(right_constraints), + ) => left_constraints + .constraints(db) + .iff(db, right_constraints.constraints(db)), ( KnownBoundMethodType::FunctionTypeDunderGet(_) @@ -10455,13 +10504,21 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::PropertyDunderGet(_) | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::PathOpen, + | KnownBoundMethodType::PathOpen + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), KnownBoundMethodType::FunctionTypeDunderGet(_) | KnownBoundMethodType::FunctionTypeDunderCall(_) | KnownBoundMethodType::PropertyDunderGet(_) | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::PathOpen, + | KnownBoundMethodType::PathOpen + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), ) => ConstraintSet::from(false), } } @@ -10480,7 +10537,12 @@ impl<'db> KnownBoundMethodType<'db> { KnownBoundMethodType::PropertyDunderSet(property) => { KnownBoundMethodType::PropertyDunderSet(property.normalized_impl(db, visitor)) } - KnownBoundMethodType::StrStartswith(_) | KnownBoundMethodType::PathOpen => self, + KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::PathOpen + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => self, } } @@ -10493,6 +10555,10 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::PropertyDunderSet(_) => KnownClass::MethodWrapperType, KnownBoundMethodType::StrStartswith(_) => KnownClass::BuiltinFunctionType, KnownBoundMethodType::PathOpen => KnownClass::MethodType, + KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => KnownClass::ConstraintSet, } } @@ -10592,6 +10658,45 @@ impl<'db> KnownBoundMethodType<'db> { KnownBoundMethodType::PathOpen => { Either::Right(std::iter::once(Signature::todo("`Path.open` return type"))) } + + KnownBoundMethodType::ConstraintSetRange => { + Either::Right(std::iter::once(Signature::new( + Parameters::new([ + 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()), + ]), + Some(KnownClass::ConstraintSet.to_instance(db)), + ))) + } + + KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever => { + Either::Right(std::iter::once(Signature::new( + Parameters::empty(), + Some(KnownClass::ConstraintSet.to_instance(db)), + ))) + } + + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => { + Either::Right(std::iter::once(Signature::new( + Parameters::new([ + 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()), + ]), + Some(KnownClass::ConstraintSet.to_instance(db)), + ))) + } } } } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index d1aa621b82..81ed3fed50 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -705,33 +705,6 @@ impl<'db> Bindings<'db> { } } - Some(KnownFunction::IsSubtypeOfGiven) => { - let [Some(constraints), Some(ty_a), Some(ty_b)] = - overload.parameter_types() - else { - continue; - }; - - let constraints = match constraints { - Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked)) => { - tracked.constraints(db) - } - Type::BooleanLiteral(b) => ConstraintSet::from(*b), - _ => continue, - }; - - let result = constraints.when_subtype_of_given( - db, - *ty_a, - *ty_b, - InferableTypeVars::None, - ); - let tracked = TrackedConstraintSet::new(db, result); - overload.set_return_type(Type::KnownInstance( - KnownInstanceType::ConstraintSet(tracked), - )); - } - Some(KnownFunction::IsAssignableTo) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { let constraints = @@ -1149,6 +1122,60 @@ impl<'db> Bindings<'db> { } }, + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetRange) => { + let [Some(lower), Some(Type::TypeVar(typevar)), Some(upper)] = + overload.parameter_types() + else { + return; + }; + let constraints = ConstraintSet::range(db, *lower, *typevar, *upper); + let tracked = TrackedConstraintSet::new(db, constraints); + overload.set_return_type(Type::KnownInstance( + KnownInstanceType::ConstraintSet(tracked), + )); + } + + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetAlways) => { + if !overload.parameter_types().is_empty() { + return; + } + let constraints = ConstraintSet::from(true); + let tracked = TrackedConstraintSet::new(db, constraints); + overload.set_return_type(Type::KnownInstance( + KnownInstanceType::ConstraintSet(tracked), + )); + } + + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetNever) => { + if !overload.parameter_types().is_empty() { + return; + } + let constraints = ConstraintSet::from(false); + let tracked = TrackedConstraintSet::new(db, constraints); + overload.set_return_type(Type::KnownInstance( + KnownInstanceType::ConstraintSet(tracked), + )); + } + + Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(tracked), + ) => { + let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else { + continue; + }; + + let result = tracked.constraints(db).when_subtype_of_given( + db, + *ty_a, + *ty_b, + InferableTypeVars::None, + ); + let tracked = TrackedConstraintSet::new(db, result); + overload.set_return_type(Type::KnownInstance( + KnownInstanceType::ConstraintSet(tracked), + )); + } + 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 b5e3cc3e7d..d7aee0eb6f 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -295,6 +295,12 @@ impl<'db> ConstraintSet<'db> { self } + pub(crate) fn iff(self, db: &'db dyn Db, other: Self) -> Self { + ConstraintSet { + node: self.node.iff(db, other.node), + } + } + pub(crate) fn range( db: &'db dyn Db, lower: Type<'db>, @@ -304,15 +310,6 @@ impl<'db> ConstraintSet<'db> { Self::constrain_typevar(db, typevar, lower, upper, TypeRelation::Assignability) } - pub(crate) fn negated_range( - db: &'db dyn Db, - lower: Type<'db>, - typevar: BoundTypeVarInstance<'db>, - upper: Type<'db>, - ) -> Self { - Self::range(db, lower, typevar, upper).negate(db) - } - pub(crate) fn display(self, db: &'db dyn Db) -> impl Display { self.node.simplify(db).display(db) } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 560943aba1..7748dd3ab5 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -523,6 +523,18 @@ impl Display for DisplayRepresentation<'_> { Type::KnownBoundMethod(KnownBoundMethodType::PathOpen) => { f.write_str("bound method `Path.open`") } + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetRange) => { + f.write_str("bound method `ConstraintSet.range`") + } + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetAlways) => { + f.write_str("bound method `ConstraintSet.always`") + } + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetNever) => { + f.write_str("bound method `ConstraintSet.never`") + } + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)) => { + f.write_str("bound method `ConstraintSet.implies_subtype_of`") + } Type::WrapperDescriptor(kind) => { let (method, object) = match kind { WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"), diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 9aae4aef02..459edb6c25 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -81,9 +81,9 @@ use crate::types::visitor::any_over_type; use crate::types::{ ApplyTypeMappingVisitor, BoundMethodType, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, - HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, - NormalizedVisitor, SpecialFormType, TrackedConstraintSet, Truthiness, Type, TypeContext, - TypeMapping, TypeRelation, UnionBuilder, binding_type, todo_type, walk_signature, + HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, NormalizedVisitor, + SpecialFormType, Truthiness, Type, TypeContext, TypeMapping, TypeRelation, UnionBuilder, + binding_type, todo_type, walk_signature, }; use crate::{Db, FxOrderSet, ModuleName, resolve_module}; @@ -1299,8 +1299,6 @@ pub enum KnownFunction { IsEquivalentTo, /// `ty_extensions.is_subtype_of` IsSubtypeOf, - /// `ty_extensions.is_subtype_of_given` - IsSubtypeOfGiven, /// `ty_extensions.is_assignable_to` IsAssignableTo, /// `ty_extensions.is_disjoint_from` @@ -1323,10 +1321,6 @@ pub enum KnownFunction { RevealProtocolInterface, /// `ty_extensions.reveal_mro` RevealMro, - /// `ty_extensions.range_constraint` - RangeConstraint, - /// `ty_extensions.negated_range_constraint` - NegatedRangeConstraint, } impl KnownFunction { @@ -1393,15 +1387,12 @@ impl KnownFunction { | Self::IsSingleValued | Self::IsSingleton | Self::IsSubtypeOf - | Self::IsSubtypeOfGiven | Self::GenericContext | Self::DunderAllNames | Self::EnumMembers | Self::StaticAssert | Self::HasMember | Self::RevealProtocolInterface - | Self::RangeConstraint - | Self::NegatedRangeConstraint | Self::RevealMro | Self::AllMembers => module.is_ty_extensions(), Self::ImportModule => module.is_importlib(), @@ -1780,32 +1771,6 @@ impl KnownFunction { overload.set_return_type(Type::module_literal(db, file, module)); } - KnownFunction::RangeConstraint => { - let [Some(lower), Some(Type::TypeVar(typevar)), Some(upper)] = parameter_types - else { - return; - }; - - let constraints = ConstraintSet::range(db, *lower, *typevar, *upper); - let tracked = TrackedConstraintSet::new(db, constraints); - overload.set_return_type(Type::KnownInstance(KnownInstanceType::ConstraintSet( - tracked, - ))); - } - - KnownFunction::NegatedRangeConstraint => { - let [Some(lower), Some(Type::TypeVar(typevar)), Some(upper)] = parameter_types - else { - return; - }; - - let constraints = ConstraintSet::negated_range(db, *lower, *typevar, *upper); - let tracked = TrackedConstraintSet::new(db, constraints); - overload.set_return_type(Type::KnownInstance(KnownInstanceType::ConstraintSet( - tracked, - ))); - } - KnownFunction::Open => { // TODO: Temporary special-casing for `builtins.open` to avoid an excessive number of // false positives in lieu of proper support for PEP-613 type aliases. @@ -1894,7 +1859,6 @@ pub(crate) mod tests { KnownFunction::IsSingleton | KnownFunction::IsSubtypeOf - | KnownFunction::IsSubtypeOfGiven | KnownFunction::GenericContext | KnownFunction::DunderAllNames | KnownFunction::EnumMembers @@ -1905,8 +1869,6 @@ pub(crate) mod tests { | KnownFunction::IsEquivalentTo | KnownFunction::HasMember | KnownFunction::RevealProtocolInterface - | KnownFunction::RangeConstraint - | KnownFunction::NegatedRangeConstraint | KnownFunction::RevealMro | KnownFunction::AllMembers => KnownModule::TyExtensions, diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi index be9b4ebff4..79cda64bef 100644 --- a/crates/ty_vendored/ty_extensions/ty_extensions.pyi +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi @@ -44,6 +44,29 @@ type JustComplex = TypeOf[1.0j] # Constraints class ConstraintSet: + @staticmethod + def range(lower_bound: Any, typevar: Any, upper_bound: Any) -> Self: + """ + Returns a constraint set that requires `typevar` to specialize to a type + that is a supertype of `lower_bound` and a subtype of `upper_bound`. + """ + + @staticmethod + def always() -> Self: + """Returns a constraint set that is always satisfied""" + + @staticmethod + def never() -> Self: + """Returns a constraint set that is never satisfied""" + + def implies_subtype_of(self, ty: Any, of: Any) -> Self: + """ + Returns a constraint set that is satisfied when `ty` is a `subtype`_ of + `of`, assuming that all of the constraints in `self` hold. + + .. _subtype: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence + """ + def __bool__(self) -> bool: ... def __eq__(self, other: ConstraintSet) -> bool: ... def __ne__(self, other: ConstraintSet) -> bool: ... @@ -51,13 +74,6 @@ class ConstraintSet: def __or__(self, other: ConstraintSet) -> ConstraintSet: ... def __invert__(self) -> ConstraintSet: ... -def range_constraint( - lower_bound: Any, typevar: Any, upper_bound: Any -) -> ConstraintSet: ... -def negated_range_constraint( - lower_bound: Any, typevar: Any, upper_bound: Any -) -> ConstraintSet: ... - # Predicates on types # # Ideally, these would be annotated using `TypeForm`, but that has not been @@ -75,15 +91,6 @@ def is_subtype_of(ty: Any, of: Any) -> ConstraintSet: .. _subtype: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence """ -def is_subtype_of_given( - constraints: bool | ConstraintSet, ty: Any, of: Any -) -> ConstraintSet: - """Returns a constraint set that is satisfied when `ty` is a `subtype`_ of `of`, - assuming that all of the constraints in `constraints` hold. - - .. _subtype: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence - """ - def is_assignable_to(ty: Any, to: Any) -> ConstraintSet: """Returns a constraint set that is satisfied when `ty` is `assignable`_ to `to`. From 17850eee4b6fa3ca6a793d2b48098cf608c91195 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 28 Oct 2025 14:59:49 -0400 Subject: [PATCH 063/188] [ty] Reformat constraint set mdtests (#21111) This PR updates the mdtests that test how our generics solver interacts with our new constraint set implementation. Because the rendering of a constraint set can get long, this standardizes on putting the `revealed` assertion on a separate line. We also add a `static_assert` test for each constraint set to verify that they are all coerced into simple `bool`s correctly. This is a pure reformatting (not even a refactoring!) that changes no behavior. I've pulled it out of #20093 to reduce the amount of effort that will be required to review that PR. --- .../mdtest/generics/pep695/variables.md | 672 ++++++++++++++---- 1 file changed, 543 insertions(+), 129 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md index 68cce5ad3e..a1262e8913 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md @@ -129,7 +129,7 @@ specialization. Thus, the typevar is a subtype of itself and of `object`, but no (including other typevars). ```py -from ty_extensions import is_assignable_to, is_subtype_of +from ty_extensions import is_assignable_to, is_subtype_of, static_assert class Super: ... class Base(Super): ... @@ -137,23 +137,85 @@ class Sub(Base): ... class Unrelated: ... def unbounded_unconstrained[T, U](t: T, u: U) -> None: - reveal_type(is_assignable_to(T, T)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(T, object)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(T, Super)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(U, U)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(U, object)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(U, Super)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(T, U)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(U, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, T)) + static_assert(is_assignable_to(T, T)) - reveal_type(is_subtype_of(T, T)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_subtype_of(T, object)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_subtype_of(T, Super)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(U, U)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_subtype_of(U, object)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_subtype_of(U, Super)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(T, U)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(U, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, object)) + static_assert(is_assignable_to(T, object)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, Super)) + static_assert(not is_assignable_to(T, Super)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Any)) + static_assert(is_assignable_to(T, Any)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Any, T)) + static_assert(is_assignable_to(Any, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(U, U)) + static_assert(is_assignable_to(U, U)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(U, object)) + static_assert(is_assignable_to(U, object)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(U, Super)) + static_assert(not is_assignable_to(U, Super)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, U)) + static_assert(not is_assignable_to(T, U)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(U, T)) + static_assert(not is_assignable_to(U, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(T, T)) + static_assert(is_subtype_of(T, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(T, object)) + static_assert(is_subtype_of(T, object)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Super)) + static_assert(not is_subtype_of(T, Super)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Any)) + static_assert(not is_subtype_of(T, Any)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Any, T)) + static_assert(not is_subtype_of(Any, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(U, U)) + static_assert(is_subtype_of(U, U)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(U, object)) + static_assert(is_subtype_of(U, object)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(U, Super)) + static_assert(not is_subtype_of(U, Super)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, U)) + static_assert(not is_subtype_of(T, U)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(U, T)) + static_assert(not is_subtype_of(U, T)) ``` A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of @@ -167,40 +229,138 @@ from typing import Any from typing_extensions import final def bounded[T: Super](t: T) -> None: - reveal_type(is_assignable_to(T, Super)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(T, Sub)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(Super, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(Sub, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Any)) + static_assert(is_assignable_to(T, Any)) - reveal_type(is_subtype_of(T, Super)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_subtype_of(T, Sub)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Super, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Sub, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Any, T)) + static_assert(is_assignable_to(Any, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Super)) + static_assert(is_assignable_to(T, Super)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, Sub)) + static_assert(not is_assignable_to(T, Sub)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(Super, T)) + static_assert(not is_assignable_to(Super, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(Sub, T)) + static_assert(not is_assignable_to(Sub, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Any)) + static_assert(not is_subtype_of(T, Any)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Any, T)) + static_assert(not is_subtype_of(Any, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(T, Super)) + static_assert(is_subtype_of(T, Super)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Sub)) + static_assert(not is_subtype_of(T, Sub)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Super, T)) + static_assert(not is_subtype_of(Super, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Sub, T)) + static_assert(not is_subtype_of(Sub, T)) def bounded_by_gradual[T: Any](t: T) -> None: - reveal_type(is_assignable_to(T, Any)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(Any, T)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(T, Super)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(Super, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(T, Sub)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(Sub, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Any)) + static_assert(is_assignable_to(T, Any)) - reveal_type(is_subtype_of(T, Any)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Any, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(T, Super)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Super, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(T, Sub)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Sub, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Any, T)) + static_assert(is_assignable_to(Any, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Super)) + static_assert(is_assignable_to(T, Super)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(Super, T)) + static_assert(not is_assignable_to(Super, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Sub)) + static_assert(is_assignable_to(T, Sub)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(Sub, T)) + static_assert(not is_assignable_to(Sub, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Any)) + static_assert(not is_subtype_of(T, Any)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Any, T)) + static_assert(not is_subtype_of(Any, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Super)) + static_assert(not is_subtype_of(T, Super)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Super, T)) + static_assert(not is_subtype_of(Super, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Sub)) + static_assert(not is_subtype_of(T, Sub)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Sub, T)) + static_assert(not is_subtype_of(Sub, T)) @final class FinalClass: ... def bounded_final[T: FinalClass](t: T) -> None: - reveal_type(is_assignable_to(T, FinalClass)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(FinalClass, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Any)) + static_assert(is_assignable_to(T, Any)) - reveal_type(is_subtype_of(T, FinalClass)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_subtype_of(FinalClass, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Any, T)) + static_assert(is_assignable_to(Any, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, FinalClass)) + static_assert(is_assignable_to(T, FinalClass)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(FinalClass, T)) + static_assert(not is_assignable_to(FinalClass, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Any)) + static_assert(not is_subtype_of(T, Any)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Any, T)) + static_assert(not is_subtype_of(Any, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(T, FinalClass)) + static_assert(is_subtype_of(T, FinalClass)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(FinalClass, T)) + static_assert(not is_subtype_of(FinalClass, T)) ``` Two distinct fully static typevars are not subtypes of each other, even if they have the same @@ -210,18 +370,38 @@ typevars to `Never` in addition to that final class. ```py def two_bounded[T: Super, U: Super](t: T, u: U) -> None: - reveal_type(is_assignable_to(T, U)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(U, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, U)) + static_assert(not is_assignable_to(T, U)) - reveal_type(is_subtype_of(T, U)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(U, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(U, T)) + static_assert(not is_assignable_to(U, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, U)) + static_assert(not is_subtype_of(T, U)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(U, T)) + static_assert(not is_subtype_of(U, T)) def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None: - reveal_type(is_assignable_to(T, U)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(U, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, U)) + static_assert(not is_assignable_to(T, U)) - reveal_type(is_subtype_of(T, U)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(U, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(U, T)) + static_assert(not is_assignable_to(U, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, U)) + static_assert(not is_subtype_of(T, U)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(U, T)) + static_assert(not is_subtype_of(U, T)) ``` A constrained fully static typevar is assignable to the union of its constraints, but not to any of @@ -232,64 +412,238 @@ intersection of all of its constraints is a subtype of the typevar. from ty_extensions import Intersection def constrained[T: (Base, Unrelated)](t: T) -> None: - reveal_type(is_assignable_to(T, Super)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(T, Base)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(T, Sub)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(T, Unrelated)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(T, Super | Unrelated)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(T, Base | Unrelated)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(T, Sub | Unrelated)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(Super, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(Unrelated, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(Super | Unrelated, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(Intersection[Base, Unrelated], T)) # revealed: ty_extensions.ConstraintSet[always] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, Super)) + static_assert(not is_assignable_to(T, Super)) - reveal_type(is_subtype_of(T, Super)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(T, Base)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(T, Sub)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(T, Unrelated)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(T, Super | Unrelated)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_subtype_of(T, Base | Unrelated)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_subtype_of(T, Sub | Unrelated)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Super, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Unrelated, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Super | Unrelated, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Intersection[Base, Unrelated], T)) # revealed: ty_extensions.ConstraintSet[always] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, Base)) + static_assert(not is_assignable_to(T, Base)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, Sub)) + static_assert(not is_assignable_to(T, Sub)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, Unrelated)) + static_assert(not is_assignable_to(T, Unrelated)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Any)) + static_assert(is_assignable_to(T, Any)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Super | Unrelated)) + static_assert(is_assignable_to(T, Super | Unrelated)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Base | Unrelated)) + static_assert(is_assignable_to(T, Base | Unrelated)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, Sub | Unrelated)) + static_assert(not is_assignable_to(T, Sub | Unrelated)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Any, T)) + static_assert(is_assignable_to(Any, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(Super, T)) + static_assert(not is_assignable_to(Super, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(Unrelated, T)) + static_assert(not is_assignable_to(Unrelated, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(Super | Unrelated, T)) + static_assert(not is_assignable_to(Super | Unrelated, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Intersection[Base, Unrelated], T)) + static_assert(is_assignable_to(Intersection[Base, Unrelated], T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Super)) + static_assert(not is_subtype_of(T, Super)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Base)) + static_assert(not is_subtype_of(T, Base)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Sub)) + static_assert(not is_subtype_of(T, Sub)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Unrelated)) + static_assert(not is_subtype_of(T, Unrelated)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Any)) + static_assert(not is_subtype_of(T, Any)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(T, Super | Unrelated)) + static_assert(is_subtype_of(T, Super | Unrelated)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(T, Base | Unrelated)) + static_assert(is_subtype_of(T, Base | Unrelated)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Sub | Unrelated)) + static_assert(not is_subtype_of(T, Sub | Unrelated)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Any, T)) + static_assert(not is_subtype_of(Any, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Super, T)) + static_assert(not is_subtype_of(Super, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Unrelated, T)) + static_assert(not is_subtype_of(Unrelated, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Super | Unrelated, T)) + static_assert(not is_subtype_of(Super | Unrelated, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(Intersection[Base, Unrelated], T)) + static_assert(is_subtype_of(Intersection[Base, Unrelated], T)) def constrained_by_gradual[T: (Base, Any)](t: T) -> None: - reveal_type(is_assignable_to(T, Super)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(T, Base)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(T, Sub)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(T, Unrelated)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(T, Any)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(T, Super | Any)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(T, Super | Unrelated)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(Super, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(Base, T)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(Unrelated, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(Any, T)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(Super | Any, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(Base | Any, T)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(Super | Unrelated, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(Intersection[Base, Unrelated], T)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(Intersection[Base, Any], T)) # revealed: ty_extensions.ConstraintSet[always] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Super)) + static_assert(is_assignable_to(T, Super)) - reveal_type(is_subtype_of(T, Super)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(T, Base)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(T, Sub)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(T, Unrelated)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(T, Any)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(T, Super | Any)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(T, Super | Unrelated)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Super, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Base, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Unrelated, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Any, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Super | Any, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Base | Any, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Super | Unrelated, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Intersection[Base, Unrelated], T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(Intersection[Base, Any], T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Base)) + static_assert(is_assignable_to(T, Base)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, Sub)) + static_assert(not is_assignable_to(T, Sub)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, Unrelated)) + static_assert(not is_assignable_to(T, Unrelated)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Any)) + static_assert(is_assignable_to(T, Any)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Super | Any)) + static_assert(is_assignable_to(T, Super | Any)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, Super | Unrelated)) + static_assert(is_assignable_to(T, Super | Unrelated)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(Super, T)) + static_assert(not is_assignable_to(Super, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Base, T)) + static_assert(is_assignable_to(Base, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(Unrelated, T)) + static_assert(not is_assignable_to(Unrelated, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Any, T)) + static_assert(is_assignable_to(Any, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(Super | Any, T)) + static_assert(not is_assignable_to(Super | Any, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Base | Any, T)) + static_assert(is_assignable_to(Base | Any, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(Super | Unrelated, T)) + static_assert(not is_assignable_to(Super | Unrelated, T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Intersection[Base, Unrelated], T)) + static_assert(is_assignable_to(Intersection[Base, Unrelated], T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Intersection[Base, Any], T)) + static_assert(is_assignable_to(Intersection[Base, Any], T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Super)) + static_assert(not is_subtype_of(T, Super)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Base)) + static_assert(not is_subtype_of(T, Base)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Sub)) + static_assert(not is_subtype_of(T, Sub)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Unrelated)) + static_assert(not is_subtype_of(T, Unrelated)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Any)) + static_assert(not is_subtype_of(T, Any)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Super | Any)) + static_assert(not is_subtype_of(T, Super | Any)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, Super | Unrelated)) + static_assert(not is_subtype_of(T, Super | Unrelated)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Super, T)) + static_assert(not is_subtype_of(Super, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Base, T)) + static_assert(not is_subtype_of(Base, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Unrelated, T)) + static_assert(not is_subtype_of(Unrelated, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Any, T)) + static_assert(not is_subtype_of(Any, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Super | Any, T)) + static_assert(not is_subtype_of(Super | Any, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Base | Any, T)) + static_assert(not is_subtype_of(Base | Any, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Super | Unrelated, T)) + static_assert(not is_subtype_of(Super | Unrelated, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Intersection[Base, Unrelated], T)) + static_assert(not is_subtype_of(Intersection[Base, Unrelated], T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(Intersection[Base, Any], T)) + static_assert(not is_subtype_of(Intersection[Base, Any], T)) ``` Two distinct fully static typevars are not subtypes of each other, even if they have the same @@ -299,58 +653,108 @@ the same type. ```py def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None: - reveal_type(is_assignable_to(T, U)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(U, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, U)) + static_assert(not is_assignable_to(T, U)) - reveal_type(is_subtype_of(T, U)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(U, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(U, T)) + static_assert(not is_assignable_to(U, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, U)) + static_assert(not is_subtype_of(T, U)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(U, T)) + static_assert(not is_subtype_of(U, T)) @final class AnotherFinalClass: ... def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None: - reveal_type(is_assignable_to(T, U)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_assignable_to(U, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(T, U)) + static_assert(not is_assignable_to(T, U)) - reveal_type(is_subtype_of(T, U)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(U, T)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_assignable_to(U, T)) + static_assert(not is_assignable_to(U, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T, U)) + static_assert(not is_subtype_of(T, U)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(U, T)) + static_assert(not is_subtype_of(U, T)) ``` A bound or constrained typevar is a subtype of itself in a union: ```py def union[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None: - reveal_type(is_assignable_to(T, T | None)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(U, U | None)) # revealed: ty_extensions.ConstraintSet[always] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T, T | None)) + static_assert(is_assignable_to(T, T | None)) - reveal_type(is_subtype_of(T, T | None)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_subtype_of(U, U | None)) # revealed: ty_extensions.ConstraintSet[always] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(U, U | None)) + static_assert(is_assignable_to(U, U | None)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(T, T | None)) + static_assert(is_subtype_of(T, T | None)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(U, U | None)) + static_assert(is_subtype_of(U, U | None)) ``` A bound or constrained typevar in a union with a dynamic type is assignable to the typevar: ```py def union_with_dynamic[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None: - reveal_type(is_assignable_to(T | Any, T)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(U | Any, U)) # revealed: ty_extensions.ConstraintSet[always] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(T | Any, T)) + static_assert(is_assignable_to(T | Any, T)) - reveal_type(is_subtype_of(T | Any, T)) # revealed: ty_extensions.ConstraintSet[never] - reveal_type(is_subtype_of(U | Any, U)) # revealed: ty_extensions.ConstraintSet[never] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(U | Any, U)) + static_assert(is_assignable_to(U | Any, U)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(T | Any, T)) + static_assert(not is_subtype_of(T | Any, T)) + + # revealed: ty_extensions.ConstraintSet[never] + reveal_type(is_subtype_of(U | Any, U)) + static_assert(not is_subtype_of(U | Any, U)) ``` And an intersection of a typevar with another type is always a subtype of the TypeVar: ```py -from ty_extensions import Intersection, Not, is_disjoint_from, static_assert +from ty_extensions import Intersection, Not, is_disjoint_from class A: ... def inter[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None: - reveal_type(is_assignable_to(Intersection[T, Unrelated], T)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_subtype_of(Intersection[T, Unrelated], T)) # revealed: ty_extensions.ConstraintSet[always] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Intersection[T, Unrelated], T)) + static_assert(is_assignable_to(Intersection[T, Unrelated], T)) - reveal_type(is_assignable_to(Intersection[U, A], U)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_subtype_of(Intersection[U, A], U)) # revealed: ty_extensions.ConstraintSet[always] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(Intersection[T, Unrelated], T)) + static_assert(is_subtype_of(Intersection[T, Unrelated], T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Intersection[U, A], U)) + static_assert(is_assignable_to(Intersection[U, A], U)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(Intersection[U, A], U)) + static_assert(is_subtype_of(Intersection[U, A], U)) static_assert(is_disjoint_from(Not[T], T)) static_assert(is_disjoint_from(T, Not[T])) @@ -647,14 +1051,24 @@ The intersection of a typevar with any other type is assignable to (and if fully of) itself. ```py -from ty_extensions import is_assignable_to, is_subtype_of, Not +from ty_extensions import is_assignable_to, is_subtype_of, Not, static_assert def intersection_is_assignable[T](t: T) -> None: - reveal_type(is_assignable_to(Intersection[T, None], T)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_assignable_to(Intersection[T, Not[None]], T)) # revealed: ty_extensions.ConstraintSet[always] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Intersection[T, None], T)) + static_assert(is_assignable_to(Intersection[T, None], T)) - reveal_type(is_subtype_of(Intersection[T, None], T)) # revealed: ty_extensions.ConstraintSet[always] - reveal_type(is_subtype_of(Intersection[T, Not[None]], T)) # revealed: ty_extensions.ConstraintSet[always] + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_assignable_to(Intersection[T, Not[None]], T)) + static_assert(is_assignable_to(Intersection[T, Not[None]], T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(Intersection[T, None], T)) + static_assert(is_subtype_of(Intersection[T, None], T)) + + # revealed: ty_extensions.ConstraintSet[always] + reveal_type(is_subtype_of(Intersection[T, Not[None]], T)) + static_assert(is_subtype_of(Intersection[T, Not[None]], T)) ``` ## Narrowing From d0aebaa25367b480c598b56fbd5a2b47bd97aa6f Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Wed, 29 Oct 2025 04:14:58 +0900 Subject: [PATCH 064/188] [`ISC001`] fix panic when string literals are unclosed (#21034) Co-authored-by: Micha Reiser --- .../ISC_syntax_error_2.py | 7 + .../rules/flake8_implicit_str_concat/mod.rs | 8 ++ .../rules/implicit.rs | 67 +++++---- ...__tests__ISC001_ISC_syntax_error_2.py.snap | 134 ++++++++++++++++++ ...__tests__ISC002_ISC_syntax_error_2.py.snap | 53 +++++++ 5 files changed, 244 insertions(+), 25 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC_syntax_error_2.py create mode 100644 crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC_syntax_error_2.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC002_ISC_syntax_error_2.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC_syntax_error_2.py b/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC_syntax_error_2.py new file mode 100644 index 0000000000..0f1264571d --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC_syntax_error_2.py @@ -0,0 +1,7 @@ +# Regression test for https://github.com/astral-sh/ruff/issues/21023 +'' ' +"" "" +'' '' ' +"" "" " +f"" f" +f"" f"" f" diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs index 2b906f3027..f02a049c5a 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs @@ -23,6 +23,14 @@ mod tests { Rule::MultiLineImplicitStringConcatenation, Path::new("ISC_syntax_error.py") )] + #[test_case( + Rule::SingleLineImplicitStringConcatenation, + Path::new("ISC_syntax_error_2.py") + )] + #[test_case( + Rule::MultiLineImplicitStringConcatenation, + Path::new("ISC_syntax_error_2.py") + )] #[test_case(Rule::ExplicitStringConcatenation, Path::new("ISC.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/implicit.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/implicit.rs index c9cc873667..c33dcccabc 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/implicit.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/implicit.rs @@ -1,13 +1,12 @@ use std::borrow::Cow; use itertools::Itertools; - use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::str::{leading_quote, trailing_quote}; +use ruff_python_ast::StringFlags; use ruff_python_index::Indexer; -use ruff_python_parser::{TokenKind, Tokens}; +use ruff_python_parser::{Token, TokenKind, Tokens}; use ruff_source_file::LineRanges; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::{Ranged, TextLen, TextRange}; use crate::Locator; use crate::checkers::ast::LintContext; @@ -169,7 +168,8 @@ pub(crate) fn implicit( SingleLineImplicitStringConcatenation, TextRange::new(a_range.start(), b_range.end()), ) { - if let Some(fix) = concatenate_strings(a_range, b_range, locator) { + if let Some(fix) = concatenate_strings(a_token, b_token, a_range, b_range, locator) + { diagnostic.set_fix(fix); } } @@ -177,38 +177,55 @@ pub(crate) fn implicit( } } -fn concatenate_strings(a_range: TextRange, b_range: TextRange, locator: &Locator) -> Option { - let a_text = locator.slice(a_range); - let b_text = locator.slice(b_range); - - let a_leading_quote = leading_quote(a_text)?; - let b_leading_quote = leading_quote(b_text)?; - - // Require, for now, that the leading quotes are the same. - if a_leading_quote != b_leading_quote { +/// Concatenates two strings +/// +/// The `a_string_range` and `b_string_range` are the range of the entire string, +/// not just of the string token itself (important for interpolated strings where +/// the start token doesn't span the entire token). +fn concatenate_strings( + a_token: &Token, + b_token: &Token, + a_string_range: TextRange, + b_string_range: TextRange, + locator: &Locator, +) -> Option { + if a_token.string_flags()?.is_unclosed() || b_token.string_flags()?.is_unclosed() { return None; } - let a_trailing_quote = trailing_quote(a_text)?; - let b_trailing_quote = trailing_quote(b_text)?; + let a_string_flags = a_token.string_flags()?; + let b_string_flags = b_token.string_flags()?; - // Require, for now, that the trailing quotes are the same. - if a_trailing_quote != b_trailing_quote { + let a_prefix = a_string_flags.prefix(); + let b_prefix = b_string_flags.prefix(); + + // Require, for now, that the strings have the same prefix, + // quote style, and number of quotes + if a_prefix != b_prefix + || a_string_flags.quote_style() != b_string_flags.quote_style() + || a_string_flags.is_triple_quoted() != b_string_flags.is_triple_quoted() + { return None; } + let a_text = locator.slice(a_string_range); + let b_text = locator.slice(b_string_range); + + let quotes = a_string_flags.quote_str(); + + let opener_len = a_string_flags.opener_len(); + let closer_len = a_string_flags.closer_len(); + let mut a_body = - Cow::Borrowed(&a_text[a_leading_quote.len()..a_text.len() - a_trailing_quote.len()]); - let b_body = &b_text[b_leading_quote.len()..b_text.len() - b_trailing_quote.len()]; + Cow::Borrowed(&a_text[TextRange::new(opener_len, a_text.text_len() - closer_len)]); + let b_body = &b_text[TextRange::new(opener_len, b_text.text_len() - closer_len)]; - if a_leading_quote.find(['r', 'R']).is_none() - && matches!(b_body.bytes().next(), Some(b'0'..=b'7')) - { + if !a_string_flags.is_raw_string() && matches!(b_body.bytes().next(), Some(b'0'..=b'7')) { normalize_ending_octal(&mut a_body); } - let concatenation = format!("{a_leading_quote}{a_body}{b_body}{a_trailing_quote}"); - let range = TextRange::new(a_range.start(), b_range.end()); + let concatenation = format!("{a_prefix}{quotes}{a_body}{b_body}{quotes}"); + let range = TextRange::new(a_string_range.start(), b_string_range.end()); Some(Fix::safe_edit(Edit::range_replacement( concatenation, diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC_syntax_error_2.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC_syntax_error_2.py.snap new file mode 100644 index 0000000000..6cd8232919 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC_syntax_error_2.py.snap @@ -0,0 +1,134 @@ +--- +source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs +--- +ISC001 Implicitly concatenated string literals on one line + --> ISC_syntax_error_2.py:2:1 + | +1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023 +2 | '' ' + | ^^^^ +3 | "" "" +4 | '' '' ' + | +help: Combine string literals + +invalid-syntax: missing closing quote in string literal + --> ISC_syntax_error_2.py:2:4 + | +1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023 +2 | '' ' + | ^ +3 | "" "" +4 | '' '' ' + | + +ISC001 Implicitly concatenated string literals on one line + --> ISC_syntax_error_2.py:3:1 + | +1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023 +2 | '' ' +3 | "" "" + | ^^^^^ +4 | '' '' ' +5 | "" "" " + | +help: Combine string literals + +ISC001 Implicitly concatenated string literals on one line + --> ISC_syntax_error_2.py:4:1 + | +2 | '' ' +3 | "" "" +4 | '' '' ' + | ^^^^^ +5 | "" "" " +6 | f"" f" + | +help: Combine string literals + +ISC001 Implicitly concatenated string literals on one line + --> ISC_syntax_error_2.py:4:4 + | +2 | '' ' +3 | "" "" +4 | '' '' ' + | ^^^^ +5 | "" "" " +6 | f"" f" + | +help: Combine string literals + +invalid-syntax: missing closing quote in string literal + --> ISC_syntax_error_2.py:4:7 + | +2 | '' ' +3 | "" "" +4 | '' '' ' + | ^ +5 | "" "" " +6 | f"" f" + | + +ISC001 Implicitly concatenated string literals on one line + --> ISC_syntax_error_2.py:5:1 + | +3 | "" "" +4 | '' '' ' +5 | "" "" " + | ^^^^^ +6 | f"" f" +7 | f"" f"" f" + | +help: Combine string literals + +ISC001 Implicitly concatenated string literals on one line + --> ISC_syntax_error_2.py:5:4 + | +3 | "" "" +4 | '' '' ' +5 | "" "" " + | ^^^^ +6 | f"" f" +7 | f"" f"" f" + | +help: Combine string literals + +invalid-syntax: missing closing quote in string literal + --> ISC_syntax_error_2.py:5:7 + | +3 | "" "" +4 | '' '' ' +5 | "" "" " + | ^ +6 | f"" f" +7 | f"" f"" f" + | + +invalid-syntax: f-string: unterminated string + --> ISC_syntax_error_2.py:6:7 + | +4 | '' '' ' +5 | "" "" " +6 | f"" f" + | ^ +7 | f"" f"" f" + | + +ISC001 Implicitly concatenated string literals on one line + --> ISC_syntax_error_2.py:7:1 + | +5 | "" "" " +6 | f"" f" +7 | f"" f"" f" + | ^^^^^^^ + | +help: Combine string literals + +invalid-syntax: f-string: unterminated string + --> ISC_syntax_error_2.py:7:11 + | +5 | "" "" " +6 | f"" f" +7 | f"" f"" f" + | ^ + | diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC002_ISC_syntax_error_2.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC002_ISC_syntax_error_2.py.snap new file mode 100644 index 0000000000..3f47730b50 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC002_ISC_syntax_error_2.py.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs +--- +invalid-syntax: missing closing quote in string literal + --> ISC_syntax_error_2.py:2:4 + | +1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023 +2 | '' ' + | ^ +3 | "" "" +4 | '' '' ' + | + +invalid-syntax: missing closing quote in string literal + --> ISC_syntax_error_2.py:4:7 + | +2 | '' ' +3 | "" "" +4 | '' '' ' + | ^ +5 | "" "" " +6 | f"" f" + | + +invalid-syntax: missing closing quote in string literal + --> ISC_syntax_error_2.py:5:7 + | +3 | "" "" +4 | '' '' ' +5 | "" "" " + | ^ +6 | f"" f" +7 | f"" f"" f" + | + +invalid-syntax: f-string: unterminated string + --> ISC_syntax_error_2.py:6:7 + | +4 | '' '' ' +5 | "" "" " +6 | f"" f" + | ^ +7 | f"" f"" f" + | + +invalid-syntax: f-string: unterminated string + --> ISC_syntax_error_2.py:7:11 + | +5 | "" "" " +6 | f"" f" +7 | f"" f"" f" + | ^ + | From 349061117c18683db607bc66140d5c870210b40a Mon Sep 17 00:00:00 2001 From: Dan Parizher <105245560+danparizher@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:47:52 -0400 Subject: [PATCH 065/188] [`refurb`] Preserve digit separators in `Decimal` constructor (`FURB157`) (#20588) ## Summary Fixes #20572 --------- Co-authored-by: Brent Westbrook --- .../resources/test/fixtures/refurb/FURB157.py | 16 ++ .../rules/verbose_decimal_constructor.rs | 47 ++++- ...es__refurb__tests__FURB157_FURB157.py.snap | 183 +++++++++++++++++- 3 files changed, 232 insertions(+), 14 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py index 236fe1b046..d795fd1941 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py @@ -69,3 +69,19 @@ Decimal(float("\N{space}\N{hyPHen-MINus}nan")) Decimal(float("\x20\N{character tabulation}\N{hyphen-minus}nan")) Decimal(float(" -" "nan")) Decimal(float("-nAn")) + +# Test cases for digit separators (safe fixes) +# https://github.com/astral-sh/ruff/issues/20572 +Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000) +Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567) +Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000) +Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999) + +# Test cases for non-thousands separators +Decimal("12_34_56_78") # Safe fix: preserves non-thousands separators +Decimal("1234_5678") # Safe fix: preserves non-thousands separators + +# Separators _and_ leading zeros +Decimal("0001_2345") +Decimal("000_1_2345") +Decimal("000_000") diff --git a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs index 04d169e3f6..50c98026d5 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs @@ -1,5 +1,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; +use itertools::Itertools; use ruff_python_ast::{self as ast, Expr}; use ruff_python_trivia::PythonWhitespace; use ruff_text_size::Ranged; @@ -91,18 +92,20 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal // using this regex: // https://github.com/python/cpython/blob/ac556a2ad1213b8bb81372fe6fb762f5fcb076de/Lib/_pydecimal.py#L6060-L6077 // _after_ trimming whitespace from the string and removing all occurrences of "_". - let mut trimmed = Cow::from(str_literal.to_str().trim_whitespace()); - if memchr::memchr(b'_', trimmed.as_bytes()).is_some() { - trimmed = Cow::from(trimmed.replace('_', "")); - } + let original_str = str_literal.to_str().trim_whitespace(); // Extract the unary sign, if any. - let (unary, rest) = if let Some(trimmed) = trimmed.strip_prefix('+') { - ("+", Cow::from(trimmed)) - } else if let Some(trimmed) = trimmed.strip_prefix('-') { - ("-", Cow::from(trimmed)) + let (unary, original_str) = if let Some(trimmed) = original_str.strip_prefix('+') { + ("+", trimmed) + } else if let Some(trimmed) = original_str.strip_prefix('-') { + ("-", trimmed) } else { - ("", trimmed) + ("", original_str) }; + let mut rest = Cow::from(original_str); + let has_digit_separators = memchr::memchr(b'_', rest.as_bytes()).is_some(); + if has_digit_separators { + rest = Cow::from(rest.replace('_', "")); + } // Early return if we now have an empty string // or a very long string: @@ -118,6 +121,13 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal return; } + // If the original string had digit separators, normalize them + let rest = if has_digit_separators { + Cow::from(normalize_digit_separators(original_str)) + } else { + Cow::from(rest) + }; + // If all the characters are zeros, then the value is zero. let rest = match (unary, rest.is_empty()) { // `Decimal("-0")` is not the same as `Decimal("0")` @@ -126,10 +136,11 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal return; } (_, true) => "0", - _ => rest, + _ => &rest, }; let replacement = format!("{unary}{rest}"); + let mut diagnostic = checker.report_diagnostic( VerboseDecimalConstructor { replacement: replacement.clone(), @@ -186,6 +197,22 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal } } +/// Normalizes digit separators in a numeric string by: +/// - Stripping leading and trailing underscores +/// - Collapsing medial underscore sequences to single underscores +fn normalize_digit_separators(original_str: &str) -> String { + // Strip leading and trailing underscores + let trimmed = original_str + .trim_start_matches(['_', '0']) + .trim_end_matches('_'); + + // Collapse medial underscore sequences to single underscores + trimmed + .chars() + .dedup_by(|a, b| *a == '_' && a == b) + .collect() +} + // ```console // $ python // >>> import sys diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap index 3bb10588ab..92e8057055 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap @@ -167,12 +167,12 @@ FURB157 [*] Verbose expression in `Decimal` constructor | ^^^^^^^ 24 | Decimal("__1____000") | -help: Replace with `1000` +help: Replace with `1_000` 20 | # See https://github.com/astral-sh/ruff/issues/13807 21 | 22 | # Errors - Decimal("1_000") -23 + Decimal(1000) +23 + Decimal(1_000) 24 | Decimal("__1____000") 25 | 26 | # Ok @@ -187,12 +187,12 @@ FURB157 [*] Verbose expression in `Decimal` constructor 25 | 26 | # Ok | -help: Replace with `1000` +help: Replace with `1_000` 21 | 22 | # Errors 23 | Decimal("1_000") - Decimal("__1____000") -24 + Decimal(1000) +24 + Decimal(1_000) 25 | 26 | # Ok 27 | Decimal("2e-4") @@ -494,6 +494,7 @@ help: Replace with `"nan"` 69 + Decimal("nan") 70 | Decimal(float(" -" "nan")) 71 | Decimal(float("-nAn")) +72 | FURB157 [*] Verbose expression in `Decimal` constructor --> FURB157.py:70:9 @@ -511,6 +512,8 @@ help: Replace with `"nan"` - Decimal(float(" -" "nan")) 70 + Decimal("nan") 71 | Decimal(float("-nAn")) +72 | +73 | # Test cases for digit separators (safe fixes) FURB157 [*] Verbose expression in `Decimal` constructor --> FURB157.py:71:9 @@ -519,6 +522,8 @@ FURB157 [*] Verbose expression in `Decimal` constructor 70 | Decimal(float(" -" "nan")) 71 | Decimal(float("-nAn")) | ^^^^^^^^^^^^^ +72 | +73 | # Test cases for digit separators (safe fixes) | help: Replace with `"nan"` 68 | Decimal(float("\N{space}\N{hyPHen-MINus}nan")) @@ -526,3 +531,173 @@ help: Replace with `"nan"` 70 | Decimal(float(" -" "nan")) - Decimal(float("-nAn")) 71 + Decimal("nan") +72 | +73 | # Test cases for digit separators (safe fixes) +74 | # https://github.com/astral-sh/ruff/issues/20572 + +FURB157 [*] Verbose expression in `Decimal` constructor + --> FURB157.py:75:9 + | +73 | # Test cases for digit separators (safe fixes) +74 | # https://github.com/astral-sh/ruff/issues/20572 +75 | Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000) + | ^^^^^^^^^^^^ +76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567) +77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000) + | +help: Replace with `15_000_000` +72 | +73 | # Test cases for digit separators (safe fixes) +74 | # https://github.com/astral-sh/ruff/issues/20572 + - Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000) +75 + Decimal(15_000_000) # Safe fix: normalizes separators, becomes Decimal(15_000_000) +76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567) +77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000) +78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999) + +FURB157 [*] Verbose expression in `Decimal` constructor + --> FURB157.py:76:9 + | +74 | # https://github.com/astral-sh/ruff/issues/20572 +75 | Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000) +76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567) + | ^^^^^^^^^^^ +77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000) +78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999) + | +help: Replace with `1_234_567` +73 | # Test cases for digit separators (safe fixes) +74 | # https://github.com/astral-sh/ruff/issues/20572 +75 | Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000) + - Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567) +76 + Decimal(1_234_567) # Safe fix: normalizes separators, becomes Decimal(1_234_567) +77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000) +78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999) +79 | + +FURB157 [*] Verbose expression in `Decimal` constructor + --> FURB157.py:77:9 + | +75 | Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000) +76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567) +77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000) + | ^^^^^^^^ +78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999) + | +help: Replace with `-5_000` +74 | # https://github.com/astral-sh/ruff/issues/20572 +75 | Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000) +76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567) + - Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000) +77 + Decimal(-5_000) # Safe fix: normalizes separators, becomes Decimal(-5_000) +78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999) +79 | +80 | # Test cases for non-thousands separators + +FURB157 [*] Verbose expression in `Decimal` constructor + --> FURB157.py:78:9 + | +76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567) +77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000) +78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999) + | ^^^^^^^^ +79 | +80 | # Test cases for non-thousands separators + | +help: Replace with `+9_999` +75 | Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000) +76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567) +77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000) + - Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999) +78 + Decimal(+9_999) # Safe fix: normalizes separators, becomes Decimal(+9_999) +79 | +80 | # Test cases for non-thousands separators +81 | Decimal("12_34_56_78") # Safe fix: preserves non-thousands separators + +FURB157 [*] Verbose expression in `Decimal` constructor + --> FURB157.py:81:9 + | +80 | # Test cases for non-thousands separators +81 | Decimal("12_34_56_78") # Safe fix: preserves non-thousands separators + | ^^^^^^^^^^^^^ +82 | Decimal("1234_5678") # Safe fix: preserves non-thousands separators + | +help: Replace with `12_34_56_78` +78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999) +79 | +80 | # Test cases for non-thousands separators + - Decimal("12_34_56_78") # Safe fix: preserves non-thousands separators +81 + Decimal(12_34_56_78) # Safe fix: preserves non-thousands separators +82 | Decimal("1234_5678") # Safe fix: preserves non-thousands separators +83 | +84 | # Separators _and_ leading zeros + +FURB157 [*] Verbose expression in `Decimal` constructor + --> FURB157.py:82:9 + | +80 | # Test cases for non-thousands separators +81 | Decimal("12_34_56_78") # Safe fix: preserves non-thousands separators +82 | Decimal("1234_5678") # Safe fix: preserves non-thousands separators + | ^^^^^^^^^^^ +83 | +84 | # Separators _and_ leading zeros + | +help: Replace with `1234_5678` +79 | +80 | # Test cases for non-thousands separators +81 | Decimal("12_34_56_78") # Safe fix: preserves non-thousands separators + - Decimal("1234_5678") # Safe fix: preserves non-thousands separators +82 + Decimal(1234_5678) # Safe fix: preserves non-thousands separators +83 | +84 | # Separators _and_ leading zeros +85 | Decimal("0001_2345") + +FURB157 [*] Verbose expression in `Decimal` constructor + --> FURB157.py:85:9 + | +84 | # Separators _and_ leading zeros +85 | Decimal("0001_2345") + | ^^^^^^^^^^^ +86 | Decimal("000_1_2345") +87 | Decimal("000_000") + | +help: Replace with `1_2345` +82 | Decimal("1234_5678") # Safe fix: preserves non-thousands separators +83 | +84 | # Separators _and_ leading zeros + - Decimal("0001_2345") +85 + Decimal(1_2345) +86 | Decimal("000_1_2345") +87 | Decimal("000_000") + +FURB157 [*] Verbose expression in `Decimal` constructor + --> FURB157.py:86:9 + | +84 | # Separators _and_ leading zeros +85 | Decimal("0001_2345") +86 | Decimal("000_1_2345") + | ^^^^^^^^^^^^ +87 | Decimal("000_000") + | +help: Replace with `1_2345` +83 | +84 | # Separators _and_ leading zeros +85 | Decimal("0001_2345") + - Decimal("000_1_2345") +86 + Decimal(1_2345) +87 | Decimal("000_000") + +FURB157 [*] Verbose expression in `Decimal` constructor + --> FURB157.py:87:9 + | +85 | Decimal("0001_2345") +86 | Decimal("000_1_2345") +87 | Decimal("000_000") + | ^^^^^^^^^ + | +help: Replace with `0` +84 | # Separators _and_ leading zeros +85 | Decimal("0001_2345") +86 | Decimal("000_1_2345") + - Decimal("000_000") +87 + Decimal(0) From 196a68e4c8f03142573dceeeba2c8485f59e132a Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Tue, 28 Oct 2025 11:23:15 -0400 Subject: [PATCH 066/188] [ty] Render `import <...>` in completions when "label details" isn't supported This fixes a bug where the `import module` part of a completion for unimported candidates would be missing. This makes it especially confusing because the user can't tell where the symbol is coming from, and there is no hint that an `import` statement will be inserted. Previously, we were using [`CompletionItemLabelDetails`] to render the `import module` part of the suggestion. But this is only supported in clients that support version 3.17 (or newer) of the LSP specification. It turns out that this support isn't widespread yet. In particular, Heliex doesn't seem to support "label details." To fix this, we take a [cue from rust-analyzer][rust-analyzer-details]. We detect if the client supports "label details," and if so, use it. Otherwise, we push the `import module` text into the completion label itself. Fixes https://github.com/astral-sh/ruff/pull/20439#issuecomment-3313689568 [`CompletionItemLabelDetails`]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemLabelDetails [rust-analyzer-details]: https://github.com/rust-lang/rust-analyzer/blob/5d905576d49233ed843bb40df4ed57e5534558f5/crates/rust-analyzer/src/lsp/to_proto.rs#L391-L404 --- crates/ty_server/src/capabilities.rs | 15 +++++++++++ .../src/server/api/requests/completion.rs | 27 ++++++++++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/crates/ty_server/src/capabilities.rs b/crates/ty_server/src/capabilities.rs index fed7ac811e..bda8546b3d 100644 --- a/crates/ty_server/src/capabilities.rs +++ b/crates/ty_server/src/capabilities.rs @@ -36,6 +36,7 @@ bitflags::bitflags! { const DIAGNOSTIC_DYNAMIC_REGISTRATION = 1 << 14; const WORKSPACE_CONFIGURATION = 1 << 15; const RENAME_DYNAMIC_REGISTRATION = 1 << 16; + const COMPLETION_ITEM_LABEL_DETAILS_SUPPORT = 1 << 17; } } @@ -158,6 +159,11 @@ impl ResolvedClientCapabilities { self.contains(Self::RENAME_DYNAMIC_REGISTRATION) } + /// Returns `true` if the client supports "label details" in completion items. + pub(crate) const fn supports_completion_item_label_details(self) -> bool { + self.contains(Self::COMPLETION_ITEM_LABEL_DETAILS_SUPPORT) + } + pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self { let mut flags = Self::empty(); @@ -314,6 +320,15 @@ impl ResolvedClientCapabilities { flags |= Self::WORK_DONE_PROGRESS; } + if text_document + .and_then(|text_document| text_document.completion.as_ref()) + .and_then(|completion| completion.completion_item.as_ref()) + .and_then(|completion_item| completion_item.label_details_support) + .unwrap_or_default() + { + flags |= Self::COMPLETION_ITEM_LABEL_DETAILS_SUPPORT; + } + flags } } diff --git a/crates/ty_server/src/server/api/requests/completion.rs b/crates/ty_server/src/server/api/requests/completion.rs index 35e1c3fd25..a3e7d91f94 100644 --- a/crates/ty_server/src/server/api/requests/completion.rs +++ b/crates/ty_server/src/server/api/requests/completion.rs @@ -81,15 +81,30 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler { new_text: edit.content().map(ToString::to_string).unwrap_or_default(), } }); + + let name = comp.name.to_string(); + let import_suffix = comp.module_name.map(|name| format!(" (import {name})")); + let (label, label_details) = if snapshot + .resolved_client_capabilities() + .supports_completion_item_label_details() + { + let label_details = CompletionItemLabelDetails { + detail: import_suffix, + description: type_display.clone(), + }; + (name, Some(label_details)) + } else { + let label = import_suffix + .map(|suffix| format!("{name}{suffix}")) + .unwrap_or_else(|| name); + (label, None) + }; CompletionItem { - label: comp.name.into(), + label, kind, sort_text: Some(format!("{i:-max_index_len$}")), - detail: type_display.clone(), - label_details: Some(CompletionItemLabelDetails { - detail: comp.module_name.map(|name| format!(" (import {name})")), - description: type_display, - }), + detail: type_display, + label_details, insert_text: comp.insert.map(String::from), additional_text_edits: import_edit.map(|edit| vec![edit]), documentation: comp From 9ce3fa3fe36b82ecfc4ace7a1e8c12a36e190751 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 27 Oct 2025 14:00:03 -0400 Subject: [PATCH 067/188] [ty] Refactor `ty_ide` completion tests The status quo grew organically and didn't do well when one wanted to mix and match different settings to generate a snapshot. This does a small refactor to use more of a builder to generate snapshots. --- crates/ty_ide/src/completion.rs | 1195 +++++++++++++++++-------------- 1 file changed, 642 insertions(+), 553 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index e08181e64a..1b384493fe 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -868,7 +868,7 @@ mod tests { use ruff_python_parser::{Mode, ParseOptions, TokenKind, Tokens}; use crate::completion::{Completion, completion}; - use crate::tests::{CursorTest, cursor_test}; + use crate::tests::{CursorTest, CursorTestBuilder}; use super::{CompletionKind, CompletionSettings, token_suffix_by_kinds}; @@ -938,73 +938,62 @@ mod tests { ); } - // At time of writing (2025-05-22), the tests below show some of the - // naivete of our completions. That is, we don't even take what has been - // typed into account. We just kind return all possible completions - // regardless of what has been typed and rely on the client to do filtering - // based on prefixes and what not. - // - // In the future, we might consider using "text edits,"[1] which will let - // us have more control over which completions are shown to the end user. - // But that will require us to at least do some kind of filtering based on - // what has been typed. - // - // [1]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion - #[test] fn empty() { - let test = cursor_test( + let test = completion_test_builder( "\ ", ); assert_snapshot!( - test.completions_without_builtins(), + test.skip_builtins().build().snapshot(), @"", ); } #[test] fn builtins() { - let test = cursor_test( + let builder = completion_test_builder( "\ ", ); - test.assert_completions_include("filter"); + let test = builder.build(); + + test.contains("filter"); // Sunder items should be filtered out - test.assert_completions_do_not_include("_T"); + test.not_contains("_T"); // Dunder attributes should not be stripped - test.assert_completions_include("__annotations__"); + test.contains("__annotations__"); // See `private_symbols_in_stub` for more comprehensive testing private of symbol filtering. } #[test] fn builtins_not_included_object_attr() { - let test = cursor_test( + let builder = completion_test_builder( "\ import re re. ", ); - test.assert_completions_do_not_include("filter"); + builder.build().not_contains("filter"); } #[test] fn builtins_not_included_import() { - let test = cursor_test( + let builder = completion_test_builder( "\ from re import ", ); - test.assert_completions_do_not_include("filter"); + builder.build().not_contains("filter"); } #[test] fn imports1() { - let test = cursor_test( + let builder = completion_test_builder( "\ import re @@ -1012,12 +1001,12 @@ import re ", ); - assert_snapshot!(test.completions_without_builtins(), @"re"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"re"); } #[test] fn imports2() { - let test = cursor_test( + let builder = completion_test_builder( "\ from os import path @@ -1025,26 +1014,26 @@ from os import path ", ); - assert_snapshot!(test.completions_without_builtins(), @"path"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"path"); } // N.B. We don't currently explore module APIs. This // is still just emitting symbols from the detected scope. #[test] fn module_api() { - let test = cursor_test( + let builder = completion_test_builder( "\ import re re. ", ); - test.assert_completions_include("findall"); + builder.build().contains("findall"); } #[test] fn private_symbols_in_stub() { - let test = CursorTest::builder() + let builder = CursorTest::builder() .source( "package/__init__.pyi", r#"\ @@ -1079,31 +1068,33 @@ class _PrivateProtocol(Protocol): "#, ) .source("main.py", "import package; package.") - .build(); - test.assert_completions_include("public_name"); - test.assert_completions_include("_private_name"); - test.assert_completions_include("__mangled_name"); - test.assert_completions_include("__dunder_name__"); - test.assert_completions_include("public_type_var"); - test.assert_completions_do_not_include("_private_type_var"); - test.assert_completions_do_not_include("__mangled_type_var"); - test.assert_completions_include("public_param_spec"); - test.assert_completions_do_not_include("_private_param_spec"); - test.assert_completions_include("public_type_var_tuple"); - test.assert_completions_do_not_include("_private_type_var_tuple"); - test.assert_completions_include("public_explicit_type_alias"); - test.assert_completions_do_not_include("_private_explicit_type_alias"); - test.assert_completions_include("public_implicit_union_alias"); - test.assert_completions_do_not_include("_private_implicit_union_alias"); - test.assert_completions_include("PublicProtocol"); - test.assert_completions_do_not_include("_PrivateProtocol"); + .completion_test_builder(); + + let test = builder.build(); + test.contains("public_name"); + test.contains("_private_name"); + test.contains("__mangled_name"); + test.contains("__dunder_name__"); + test.contains("public_type_var"); + test.not_contains("_private_type_var"); + test.not_contains("__mangled_type_var"); + test.contains("public_param_spec"); + test.not_contains("_private_param_spec"); + test.contains("public_type_var_tuple"); + test.not_contains("_private_type_var_tuple"); + test.contains("public_explicit_type_alias"); + test.not_contains("_private_explicit_type_alias"); + test.contains("public_implicit_union_alias"); + test.not_contains("_private_implicit_union_alias"); + test.contains("PublicProtocol"); + test.not_contains("_PrivateProtocol"); } /// Unlike [`private_symbols_in_stub`], this test doesn't use a `.pyi` file so all of the names /// are visible. #[test] fn private_symbols_in_module() { - let test = CursorTest::builder() + let builder = CursorTest::builder() .source( "package/__init__.py", r#"\ @@ -1135,27 +1126,29 @@ class _PrivateProtocol(Protocol): "#, ) .source("main.py", "import package; package.") - .build(); - test.assert_completions_include("public_name"); - test.assert_completions_include("_private_name"); - test.assert_completions_include("__mangled_name"); - test.assert_completions_include("__dunder_name__"); - test.assert_completions_include("public_type_var"); - test.assert_completions_include("_private_type_var"); - test.assert_completions_include("__mangled_type_var"); - test.assert_completions_include("public_param_spec"); - test.assert_completions_include("_private_param_spec"); - test.assert_completions_include("public_type_var_tuple"); - test.assert_completions_include("_private_type_var_tuple"); - test.assert_completions_include("public_explicit_type_alias"); - test.assert_completions_include("_private_explicit_type_alias"); - test.assert_completions_include("PublicProtocol"); - test.assert_completions_include("_PrivateProtocol"); + .completion_test_builder(); + + let test = builder.build(); + test.contains("public_name"); + test.contains("_private_name"); + test.contains("__mangled_name"); + test.contains("__dunder_name__"); + test.contains("public_type_var"); + test.contains("_private_type_var"); + test.contains("__mangled_type_var"); + test.contains("public_param_spec"); + test.contains("_private_param_spec"); + test.contains("public_type_var_tuple"); + test.contains("_private_type_var_tuple"); + test.contains("public_explicit_type_alias"); + test.contains("_private_explicit_type_alias"); + test.contains("PublicProtocol"); + test.contains("_PrivateProtocol"); } #[test] fn one_function_prefix() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): ... @@ -1163,12 +1156,12 @@ f ", ); - assert_snapshot!(test.completions_without_builtins(), @"foo"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"foo"); } #[test] fn one_function_not_prefix() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): ... @@ -1176,12 +1169,15 @@ g ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!( + builder.skip_builtins().build().snapshot(), + @"", + ); } #[test] fn one_function_blank() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): ... @@ -1189,14 +1185,14 @@ def foo(): ... ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo "); } #[test] fn nested_function_prefix() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): def foofoo(): ... @@ -1205,12 +1201,12 @@ f ", ); - assert_snapshot!(test.completions_without_builtins(), @"foo"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"foo"); } #[test] fn nested_function_blank() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): def foofoo(): ... @@ -1219,14 +1215,14 @@ def foo(): ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo "); } #[test] fn nested_function_not_in_global_scope_prefix() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): def foofoo(): ... @@ -1234,7 +1230,7 @@ def foo(): ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo foofoo "); @@ -1242,7 +1238,7 @@ def foo(): #[test] fn nested_function_not_in_global_scope_blank() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): def foofoo(): ... @@ -1268,14 +1264,14 @@ def foo(): // matches the current cursor's indentation. This seems fraught // however. It's not clear to me that we can always assume a // correspondence between scopes and indentation level. - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo "); } #[test] fn double_nested_function_not_in_global_scope_prefix1() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): def foofoo(): @@ -1284,7 +1280,7 @@ def foo(): ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo foofoo "); @@ -1292,7 +1288,7 @@ def foo(): #[test] fn double_nested_function_not_in_global_scope_prefix2() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): def foofoo(): @@ -1300,7 +1296,7 @@ def foo(): f", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo foofoo "); @@ -1308,7 +1304,7 @@ def foo(): #[test] fn double_nested_function_not_in_global_scope_prefix3() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): def foofoo(): @@ -1318,7 +1314,7 @@ def frob(): ... ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo foofoo frob @@ -1327,7 +1323,7 @@ def frob(): ... #[test] fn double_nested_function_not_in_global_scope_prefix4() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): def foofoo(): @@ -1337,7 +1333,7 @@ def frob(): ... ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo frob "); @@ -1345,7 +1341,7 @@ def frob(): ... #[test] fn double_nested_function_not_in_global_scope_prefix5() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): def foofoo(): @@ -1355,7 +1351,7 @@ def frob(): ... ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo foofoo foofoofoo @@ -1365,7 +1361,7 @@ def frob(): ... #[test] fn double_nested_function_not_in_global_scope_blank1() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): def foofoo(): @@ -1383,14 +1379,14 @@ def foo(): // account for the indented whitespace, or some other technique // needs to be used to get the scope containing `foofoo` but not // `foofoofoo`. - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo "); } #[test] fn double_nested_function_not_in_global_scope_blank2() { - let test = cursor_test( + let builder = completion_test_builder( " \ def foo(): def foofoo(): @@ -1399,14 +1395,14 @@ def foo(): ); // FIXME: Should include `foofoo` (but not `foofoofoo`). - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo "); } #[test] fn double_nested_function_not_in_global_scope_blank3() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): def foofoo(): @@ -1417,7 +1413,7 @@ def frob(): ... ); // FIXME: Should include `foofoo` (but not `foofoofoo`). - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo frob "); @@ -1425,7 +1421,7 @@ def frob(): ... #[test] fn double_nested_function_not_in_global_scope_blank4() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): def foofoo(): @@ -1437,7 +1433,7 @@ def frob(): ... ); // FIXME: Should include `foofoo` (but not `foofoofoo`). - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo frob "); @@ -1445,7 +1441,7 @@ def frob(): ... #[test] fn double_nested_function_not_in_global_scope_blank5() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): def foofoo(): @@ -1458,7 +1454,7 @@ def frob(): ... ); // FIXME: Should include `foofoo` (but not `foofoofoo`). - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo frob "); @@ -1470,18 +1466,20 @@ def frob(): ... /// start of a zero-length token. #[test] fn completion_at_eof() { - let test = cursor_test("def f(msg: str):\n msg."); - test.assert_completions_include("upper"); - test.assert_completions_include("capitalize"); + completion_test_builder("def f(msg: str):\n msg.") + .build() + .contains("upper") + .contains("capitalize"); - let test = cursor_test("def f(msg: str):\n msg.u"); - test.assert_completions_include("upper"); - test.assert_completions_do_not_include("capitalize"); + completion_test_builder("def f(msg: str):\n msg.u") + .build() + .contains("upper") + .not_contains("capitalize"); } #[test] fn list_comprehension1() { - let test = cursor_test( + let builder = completion_test_builder( "\ [ for bar in [1, 2, 3]] ", @@ -1491,80 +1489,80 @@ def frob(): ... // the list comprehension is not yet valid and so we do not // detect this as a definition of `bar`. assert_snapshot!( - test.completions_without_builtins(), + builder.skip_builtins().build().snapshot(), @"", ); } #[test] fn list_comprehension2() { - let test = cursor_test( + let builder = completion_test_builder( "\ [f for foo in [1, 2, 3]] ", ); - assert_snapshot!(test.completions_without_builtins(), @"foo"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"foo"); } #[test] fn lambda_prefix1() { - let test = cursor_test( + let builder = completion_test_builder( "\ (lambda foo: (1 + f + 2))(2) ", ); - assert_snapshot!(test.completions_without_builtins(), @"foo"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"foo"); } #[test] fn lambda_prefix2() { - let test = cursor_test( + let builder = completion_test_builder( "\ (lambda foo: f + 1)(2) ", ); - assert_snapshot!(test.completions_without_builtins(), @"foo"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"foo"); } #[test] fn lambda_prefix3() { - let test = cursor_test( + let builder = completion_test_builder( "\ (lambda foo: (f + 1))(2) ", ); - assert_snapshot!(test.completions_without_builtins(), @"foo"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"foo"); } #[test] fn lambda_prefix4() { - let test = cursor_test( + let builder = completion_test_builder( "\ (lambda foo: 1 + f)(2) ", ); - assert_snapshot!(test.completions_without_builtins(), @"foo"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"foo"); } #[test] fn lambda_blank1() { - let test = cursor_test( + let builder = completion_test_builder( "\ (lambda foo: 1 + + 2)(2) ", ); - assert_snapshot!(test.completions_without_builtins(), @"foo"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"foo"); } #[test] fn lambda_blank2() { - let test = cursor_test( + let builder = completion_test_builder( "\ (lambda foo: + 1)(2) ", @@ -1582,14 +1580,14 @@ def frob(): ... // The `lambda_blank1` test works because there are expressions // on either side of . assert_snapshot!( - test.completions_without_builtins(), + builder.skip_builtins().build().snapshot(), @"", ); } #[test] fn lambda_blank3() { - let test = cursor_test( + let builder = completion_test_builder( "\ (lambda foo: ( + 1))(2) ", @@ -1597,14 +1595,14 @@ def frob(): ... // FIXME: Should include `foo`. assert_snapshot!( - test.completions_without_builtins(), + builder.skip_builtins().build().snapshot(), @"", ); } #[test] fn lambda_blank4() { - let test = cursor_test( + let builder = completion_test_builder( "\ (lambda foo: 1 + )(2) ", @@ -1612,14 +1610,14 @@ def frob(): ... // FIXME: Should include `foo`. assert_snapshot!( - test.completions_without_builtins(), + builder.skip_builtins().build().snapshot(), @"", ); } #[test] fn class_prefix1() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Foo: bar = 1 @@ -1628,7 +1626,7 @@ class Foo: ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" bar frob "); @@ -1636,7 +1634,7 @@ class Foo: #[test] fn class_prefix2() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Foo: bar = 1 @@ -1644,12 +1642,12 @@ class Foo: ", ); - assert_snapshot!(test.completions_without_builtins(), @"bar"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"bar"); } #[test] fn class_blank1() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Foo: bar = 1 @@ -1664,14 +1662,14 @@ class Foo: // // These don't work for similar reasons as other // tests above with the inside of whitespace. - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" Foo "); } #[test] fn class_blank2() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Foo: bar = 1 @@ -1683,14 +1681,14 @@ class Foo: // FIXME: Should include `bar`, `quux` and `frob`. // (Unclear if `Foo` should be included, but a false // positive isn't the end of the world.) - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" Foo "); } #[test] fn class_super1() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Bar: ... @@ -1699,7 +1697,7 @@ class Foo(): ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" Bar Foo "); @@ -1707,7 +1705,7 @@ class Foo(): #[test] fn class_super2() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Foo(): bar = 1 @@ -1716,7 +1714,7 @@ class Bar: ... ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" Bar Foo "); @@ -1724,7 +1722,7 @@ class Bar: ... #[test] fn class_super3() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Foo( bar = 1 @@ -1733,7 +1731,7 @@ class Bar: ... ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" Bar Foo "); @@ -1741,14 +1739,14 @@ class Bar: ... #[test] fn class_super4() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Bar: ... class Foo(", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" Bar Foo "); @@ -1756,7 +1754,7 @@ class Foo(", #[test] fn class_init1() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Quux: def __init__(self): @@ -1769,7 +1767,7 @@ quux. ", ); - assert_snapshot!(test.completions_without_builtins_with_types(), @r" + assert_snapshot!(builder.skip_builtins().type_signatures().build().snapshot(), @r" bar :: Unknown | Literal[2] baz :: Unknown | Literal[3] foo :: Unknown | Literal[1] @@ -1801,7 +1799,7 @@ quux. #[test] fn class_init2() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Quux: def __init__(self): @@ -1814,7 +1812,7 @@ quux.b ", ); - assert_snapshot!(test.completions_without_builtins_with_types(), @r" + assert_snapshot!(builder.skip_builtins().type_signatures().build().snapshot(), @r" bar :: Unknown | Literal[2] baz :: Unknown | Literal[3] __getattribute__ :: bound method Quux.__getattribute__(name: str, /) -> Any @@ -1825,7 +1823,7 @@ quux.b #[test] fn metaclass1() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Meta(type): @property @@ -1838,7 +1836,7 @@ C. ", ); - assert_snapshot!(test.completions_without_builtins_with_types(), @r" + assert_snapshot!(builder.skip_builtins().type_signatures().build().snapshot(), @r" meta_attr :: int mro :: bound method .mro() -> list[type] __annotate__ :: @Todo | None @@ -1889,7 +1887,7 @@ C. #[test] fn metaclass2() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Meta(type): @property @@ -1909,7 +1907,7 @@ Meta. // just redact them. ---AG filters => [(r"(?m)\s*__(annotations|new|annotate)__.+$", "")]}, { - assert_snapshot!(test.completions_without_builtins_with_types(), @r" + assert_snapshot!(builder.skip_builtins().type_signatures().build().snapshot(), @r" meta_attr :: property mro :: def mro(self) -> list[type] __base__ :: type | None @@ -1959,7 +1957,7 @@ Meta. #[test] fn class_init3() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Quux: def __init__(self): @@ -1970,7 +1968,7 @@ class Quux: ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" bar baz foo @@ -2002,7 +2000,7 @@ class Quux: #[test] fn class_attributes1() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Quux: some_attribute: int = 1 @@ -2031,7 +2029,7 @@ Quux. ", ); - assert_snapshot!(test.completions_without_builtins_with_types(), @r" + assert_snapshot!(builder.skip_builtins().type_signatures().build().snapshot(), @r" mro :: bound method .mro() -> list[type] some_attribute :: int some_class_method :: bound method .some_class_method() -> int @@ -2086,7 +2084,7 @@ Quux. #[test] fn enum_attributes() { - let test = cursor_test( + let builder = completion_test_builder( "\ from enum import Enum @@ -2103,7 +2101,7 @@ Answer. // rendered differently in release mode. filters => [(r"(?m)\s*__(call|reduce_ex|annotate|signature)__.+$", "")]}, { - assert_snapshot!(test.completions_without_builtins_with_types(), @r" + assert_snapshot!(builder.skip_builtins().type_signatures().build().snapshot(), @r" NO :: Literal[Answer.NO] YES :: Literal[Answer.YES] mro :: bound method .mro() -> list[type] @@ -2178,7 +2176,7 @@ Answer. // We don't yet take function parameters into account. #[test] fn call_prefix1() { - let test = cursor_test( + let builder = completion_test_builder( "\ def bar(okay=None): ... @@ -2188,12 +2186,12 @@ bar(o ", ); - assert_snapshot!(test.completions_without_builtins(), @"foo"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"foo"); } #[test] fn call_blank1() { - let test = cursor_test( + let builder = completion_test_builder( "\ def bar(okay=None): ... @@ -2203,7 +2201,7 @@ bar( ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" bar foo "); @@ -2211,7 +2209,7 @@ bar( #[test] fn duplicate1() { - let test = cursor_test( + let builder = completion_test_builder( "\ def foo(): ... @@ -2222,7 +2220,7 @@ class C: ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo self "); @@ -2230,7 +2228,7 @@ class C: #[test] fn instance_methods_are_not_regular_functions1() { - let test = cursor_test( + let builder = completion_test_builder( "\ class C: def foo(self): ... @@ -2239,12 +2237,12 @@ class C: ", ); - assert_snapshot!(test.completions_without_builtins(), @"C"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"C"); } #[test] fn instance_methods_are_not_regular_functions2() { - let test = cursor_test( + let builder = completion_test_builder( "\ class C: def foo(self): ... @@ -2256,7 +2254,7 @@ class C: // FIXME: Should NOT include `foo` here, since // that is only a method that can be called on // `self`. - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" foo self "); @@ -2264,7 +2262,7 @@ class C: #[test] fn identifier_keyword_clash1() { - let test = cursor_test( + let builder = completion_test_builder( "\ classy_variable_name = 1 @@ -2272,12 +2270,12 @@ class ", ); - assert_snapshot!(test.completions_without_builtins(), @"classy_variable_name"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"classy_variable_name"); } #[test] fn identifier_keyword_clash2() { - let test = cursor_test( + let builder = completion_test_builder( "\ some_symbol = 1 @@ -2285,12 +2283,12 @@ print(f\"{some ", ); - assert_snapshot!(test.completions_without_builtins(), @"some_symbol"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"some_symbol"); } #[test] fn statically_unreachable_symbols() { - let test = cursor_test( + let builder = completion_test_builder( "\ if 1 + 2 != 3: hidden_symbol = 1 @@ -2299,15 +2297,12 @@ hidden_ ", ); - assert_snapshot!( - test.completions_without_builtins(), - @"", - ); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @""); } #[test] fn completions_inside_unreachable_sections() { - let test = cursor_test( + let builder = completion_test_builder( "\ import sys @@ -2322,14 +2317,14 @@ if sys.platform == \"not-my-current-platform\": // currently make no effort to provide a good IDE experience within sections that // are unreachable assert_snapshot!( - test.completions_without_builtins(), + builder.skip_builtins().build().snapshot(), @"", ); } #[test] fn star_import() { - let test = cursor_test( + let builder = completion_test_builder( "\ from typing import * @@ -2337,49 +2332,48 @@ Re ", ); - test.assert_completions_include("Reversible"); // `ReadableBuffer` is a symbol in `typing`, but it is not re-exported - test.assert_completions_do_not_include("ReadableBuffer"); + builder + .build() + .contains("Reversible") + .not_contains("ReadableBuffer"); } #[test] fn attribute_access_empty_list() { - let test = cursor_test( + let builder = completion_test_builder( "\ []. ", ); - - test.assert_completions_include("append"); + builder.build().contains("append"); } #[test] fn attribute_access_empty_dict() { - let test = cursor_test( + let builder = completion_test_builder( "\ {}. ", ); - test.assert_completions_include("values"); - test.assert_completions_do_not_include("add"); + builder.build().contains("values").not_contains("add"); } #[test] fn attribute_access_set() { - let test = cursor_test( + let builder = completion_test_builder( "\ {1}. ", ); - test.assert_completions_include("add"); - test.assert_completions_do_not_include("values"); + builder.build().contains("add").not_contains("values"); } #[test] fn attribute_parens() { - let test = cursor_test( + let builder = completion_test_builder( "\ class A: x: str @@ -2389,12 +2383,12 @@ a = A() ", ); - test.assert_completions_include("x"); + builder.build().contains("x"); } #[test] fn attribute_double_parens() { - let test = cursor_test( + let builder = completion_test_builder( "\ class A: x: str @@ -2404,12 +2398,12 @@ a = A() ", ); - test.assert_completions_include("x"); + builder.build().contains("x"); } #[test] fn attribute_on_constructor_directly() { - let test = cursor_test( + let builder = completion_test_builder( "\ class A: x: str @@ -2418,45 +2412,45 @@ A(). ", ); - test.assert_completions_include("x"); + builder.build().contains("x"); } #[test] fn attribute_not_on_integer() { - let test = cursor_test( + let builder = completion_test_builder( "\ 3. ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @""); } #[test] fn attribute_on_integer() { - let test = cursor_test( + let builder = completion_test_builder( "\ (3). ", ); - test.assert_completions_include("bit_length"); + builder.build().contains("bit_length"); } #[test] fn attribute_on_float() { - let test = cursor_test( + let builder = completion_test_builder( "\ 3.14. ", ); - test.assert_completions_include("conjugate"); + builder.build().contains("conjugate"); } #[test] fn nested_attribute_access1() { - let test = cursor_test( + let builder = completion_test_builder( "\ class A: x: str @@ -2469,13 +2463,12 @@ b.a. ", ); - test.assert_completions_do_not_include("a"); - test.assert_completions_include("x"); + builder.build().not_contains("a").contains("x"); } #[test] fn nested_attribute_access2() { - let test = cursor_test( + let builder = completion_test_builder( "\ class B: c: int @@ -2488,28 +2481,32 @@ a = A() ", ); - test.assert_completions_include("c"); - test.assert_completions_do_not_include("b"); - test.assert_completions_do_not_include("pop"); + builder + .build() + .contains("c") + .not_contains("b") + .not_contains("pop"); } #[test] fn nested_attribute_access3() { - let test = cursor_test( + let builder = completion_test_builder( "\ a = A() ([1] + [\"abc\".] + [3]).pop() ", ); - test.assert_completions_include("capitalize"); - test.assert_completions_do_not_include("append"); - test.assert_completions_do_not_include("pop"); + builder + .build() + .contains("capitalize") + .not_contains("append") + .not_contains("pop"); } #[test] fn nested_attribute_access4() { - let test = cursor_test( + let builder = completion_test_builder( "\ class B: c: int @@ -2524,13 +2521,12 @@ foo(). ", ); - test.assert_completions_include("b"); - test.assert_completions_do_not_include("c"); + builder.build().contains("b").not_contains("c"); } #[test] fn nested_attribute_access5() { - let test = cursor_test( + let builder = completion_test_builder( "\ class B: c: int @@ -2545,13 +2541,12 @@ foo().b. ", ); - test.assert_completions_include("c"); - test.assert_completions_do_not_include("b"); + builder.build().contains("c").not_contains("b"); } #[test] fn betwixt_attribute_access1() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Foo: xyz: str @@ -2567,14 +2562,16 @@ quux..foo.xyz ", ); - test.assert_completions_include("bar"); - test.assert_completions_do_not_include("xyz"); - test.assert_completions_do_not_include("foo"); + builder + .build() + .contains("bar") + .not_contains("xyz") + .not_contains("foo"); } #[test] fn betwixt_attribute_access2() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Foo: xyz: str @@ -2590,14 +2587,16 @@ quux.b.foo.xyz ", ); - test.assert_completions_include("bar"); - test.assert_completions_do_not_include("xyz"); - test.assert_completions_do_not_include("foo"); + builder + .build() + .contains("bar") + .not_contains("xyz") + .not_contains("foo"); } #[test] fn betwixt_attribute_access3() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Foo: xyz: str @@ -2613,12 +2612,12 @@ quux = Quux() ", ); - test.assert_completions_include("quux"); + builder.build().contains("quux"); } #[test] fn betwixt_attribute_access4() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Foo: xyz: str @@ -2634,29 +2633,29 @@ q.foo.xyz ", ); - test.assert_completions_include("quux"); + builder.build().contains("quux"); } #[test] fn ellipsis1() { - let test = cursor_test( + let builder = completion_test_builder( "\ ... ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @""); } #[test] fn ellipsis2() { - let test = cursor_test( + let builder = completion_test_builder( "\ .... ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" __annotations__ __class__ __delattr__ @@ -2685,18 +2684,18 @@ q.foo.xyz #[test] fn ellipsis3() { - let test = cursor_test( + let builder = completion_test_builder( "\ class Foo: ... ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @""); } #[test] fn ordering() { - let test = cursor_test( + let builder = completion_test_builder( "\ class A: foo: str @@ -2713,7 +2712,7 @@ A. ); assert_snapshot!( - test.completions_if(|c| c.name.contains("FOO") || c.name.contains("foo")), + builder.filter(|c| c.name.contains("FOO") || c.name.contains("foo")).build().snapshot(), @r" FOO foo @@ -2730,38 +2729,38 @@ A. // Ref: https://github.com/astral-sh/ty/issues/572 #[test] fn scope_id_missing_function_identifier1() { - let test = cursor_test( + let builder = completion_test_builder( "\ def m ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @""); } // Ref: https://github.com/astral-sh/ty/issues/572 #[test] fn scope_id_missing_function_identifier2() { - let test = cursor_test( + let builder = completion_test_builder( "\ def m(): pass ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @""); } // Ref: https://github.com/astral-sh/ty/issues/572 #[test] fn fscope_id_missing_function_identifier3() { - let test = cursor_test( + let builder = completion_test_builder( "\ def m(): pass ", ); - assert_snapshot!(test.completions_without_builtins(), @r" + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r" m "); } @@ -2769,31 +2768,31 @@ def m(): pass // Ref: https://github.com/astral-sh/ty/issues/572 #[test] fn scope_id_missing_class_identifier1() { - let test = cursor_test( + let builder = completion_test_builder( "\ class M ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @""); } // Ref: https://github.com/astral-sh/ty/issues/572 #[test] fn scope_id_missing_type_alias1() { - let test = cursor_test( + let builder = completion_test_builder( "\ Fo = float ", ); - assert_snapshot!(test.completions_without_builtins(), @"Fo"); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @"Fo"); } // Ref: https://github.com/astral-sh/ty/issues/572 #[test] fn scope_id_missing_import1() { - let test = cursor_test( + let builder = completion_test_builder( "\ import fo ", @@ -2803,13 +2802,13 @@ import fo // which is kind of annoying. So just assert that it // runs without panicking and produces some non-empty // output. - assert!(!test.completions_without_builtins().is_empty()); + assert!(!builder.skip_builtins().build().completions().is_empty()); } // Ref: https://github.com/astral-sh/ty/issues/572 #[test] fn scope_id_missing_import2() { - let test = cursor_test( + let builder = completion_test_builder( "\ import foo as ba ", @@ -2819,13 +2818,13 @@ import foo as ba // which is kind of annoying. So just assert that it // runs without panicking and produces some non-empty // output. - assert!(!test.completions_without_builtins().is_empty()); + assert!(!builder.skip_builtins().build().completions().is_empty()); } // Ref: https://github.com/astral-sh/ty/issues/572 #[test] fn scope_id_missing_from_import1() { - let test = cursor_test( + let builder = completion_test_builder( "\ from fo import wat ", @@ -2835,37 +2834,37 @@ from fo import wat // which is kind of annoying. So just assert that it // runs without panicking and produces some non-empty // output. - assert!(!test.completions_without_builtins().is_empty()); + assert!(!builder.skip_builtins().build().completions().is_empty()); } // Ref: https://github.com/astral-sh/ty/issues/572 #[test] fn scope_id_missing_from_import2() { - let test = cursor_test( + let builder = completion_test_builder( "\ from foo import wa ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @""); } // Ref: https://github.com/astral-sh/ty/issues/572 #[test] fn scope_id_missing_from_import3() { - let test = cursor_test( + let builder = completion_test_builder( "\ from foo import wat as ba ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @""); } // Ref: https://github.com/astral-sh/ty/issues/572 #[test] fn scope_id_missing_try_except1() { - let test = cursor_test( + let builder = completion_test_builder( "\ try: pass @@ -2875,7 +2874,7 @@ except Type: ); assert_snapshot!( - test.completions_without_builtins(), + builder.skip_builtins().build().snapshot(), @"", ); } @@ -2883,19 +2882,19 @@ except Type: // Ref: https://github.com/astral-sh/ty/issues/572 #[test] fn scope_id_missing_global1() { - let test = cursor_test( + let builder = completion_test_builder( "\ def _(): global fo ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @""); } #[test] fn string_dot_attr1() { - let test = cursor_test( + let builder = completion_test_builder( r#" foo = 1 bar = 2 @@ -2910,12 +2909,12 @@ f = Foo() "#, ); - assert_snapshot!(test.completions_without_builtins(), @r""); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r""); } #[test] fn string_dot_attr2() { - let test = cursor_test( + let builder = completion_test_builder( r#" foo = 1 bar = 2 @@ -2930,12 +2929,12 @@ f"{f. "#, ); - test.assert_completions_include("method"); + builder.build().contains("method"); } #[test] fn string_dot_attr3() { - let test = cursor_test( + let builder = completion_test_builder( r#" foo = 1 bar = 2 @@ -2950,12 +2949,12 @@ t"{f. "#, ); - test.assert_completions_include("method"); + builder.build().contains("method"); } #[test] fn no_panic_for_attribute_table_that_contains_subscript() { - let test = cursor_test( + let builder = completion_test_builder( r#" class Point: def orthogonal_direction(self): @@ -2965,107 +2964,107 @@ def test_point(p2: Point): p2. "#, ); - test.assert_completions_include("orthogonal_direction"); + builder.build().contains("orthogonal_direction"); } #[test] fn from_import1() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn from_import2() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import abiflags, ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn from_import3() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import , abiflags ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn from_import4() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import abiflags, \ ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn from_import5() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import abiflags as foo, ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn from_import6() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import abiflags as foo, g ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn from_import7() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import abiflags as foo, \ ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn from_import8() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import abiflags as foo, \ g ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn from_import9() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import ( abiflags, ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn from_import10() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import ( abiflags, @@ -3073,65 +3072,65 @@ from sys import ( ) ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn from_import11() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import ( ) ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn from_import_unknown_in_module() { - let test = cursor_test( + let builder = completion_test_builder( "\ foo = 1 from ? import ", ); - assert_snapshot!(test.completions_without_builtins(), @r""); + assert_snapshot!(builder.skip_builtins().build().snapshot(), @r""); } #[test] fn from_import_unknown_in_import_names1() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import ?, ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn from_import_unknown_in_import_names2() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import ??, ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn from_import_unknown_in_import_names3() { - let test = cursor_test( + let builder = completion_test_builder( "\ from sys import ??, , ?? ", ); - test.assert_completions_include("getsizeof"); + builder.build().contains("getsizeof"); } #[test] fn relative_from_import1() { - let test = CursorTest::builder() + CursorTest::builder() .source("package/__init__.py", "") .source( "package/foo.py", @@ -3142,13 +3141,14 @@ Cougar = 3 ", ) .source("package/sub1/sub2/bar.py", "from ...foo import ") - .build(); - test.assert_completions_include("Cheetah"); + .completion_test_builder() + .build() + .contains("Cheetah"); } #[test] fn relative_from_import2() { - let test = CursorTest::builder() + CursorTest::builder() .source("package/__init__.py", "") .source( "package/sub1/foo.py", @@ -3159,13 +3159,14 @@ Cougar = 3 ", ) .source("package/sub1/sub2/bar.py", "from ..foo import ") - .build(); - test.assert_completions_include("Cheetah"); + .completion_test_builder() + .build() + .contains("Cheetah"); } #[test] fn relative_from_import3() { - let test = CursorTest::builder() + CursorTest::builder() .source("package/__init__.py", "") .source( "package/sub1/sub2/foo.py", @@ -3176,13 +3177,14 @@ Cougar = 3 ", ) .source("package/sub1/sub2/bar.py", "from .foo import ") - .build(); - test.assert_completions_include("Cheetah"); + .completion_test_builder() + .build() + .contains("Cheetah"); } #[test] fn from_import_with_submodule1() { - let test = CursorTest::builder() + CursorTest::builder() .source("main.py", "from package import ") .source("package/__init__.py", "") .source("package/foo.py", "") @@ -3191,229 +3193,216 @@ Cougar = 3 .source("package/data.txt", "") .source("package/sub/__init__.py", "") .source("package/not-a-submodule/__init__.py", "") - .build(); - - test.assert_completions_include("foo"); - test.assert_completions_include("bar"); - test.assert_completions_include("sub"); - test.assert_completions_do_not_include("foo-bar"); - test.assert_completions_do_not_include("data"); - test.assert_completions_do_not_include("not-a-submodule"); + .completion_test_builder() + .build() + .contains("foo") + .contains("bar") + .contains("sub") + .not_contains("foo-bar") + .not_contains("data") + .not_contains("not-a-submodule"); } #[test] fn from_import_with_vendored_submodule1() { - let test = cursor_test( + let builder = completion_test_builder( "\ from http import ", ); - test.assert_completions_include("client"); + builder.build().contains("client"); } #[test] fn from_import_with_vendored_submodule2() { - let test = cursor_test( + let builder = completion_test_builder( "\ from email import ", ); - test.assert_completions_include("mime"); - test.assert_completions_do_not_include("base"); + builder.build().contains("mime").not_contains("base"); } #[test] fn import_submodule_not_attribute1() { - let test = cursor_test( + let builder = completion_test_builder( "\ import importlib importlib. ", ); - test.assert_completions_do_not_include("resources"); + builder.build().not_contains("resources"); } #[test] fn import_submodule_not_attribute2() { - let test = cursor_test( + let builder = completion_test_builder( "\ import importlib.resources importlib. ", ); - test.assert_completions_include("resources"); + builder.build().contains("resources"); } #[test] fn import_submodule_not_attribute3() { - let test = cursor_test( + let builder = completion_test_builder( "\ import importlib import importlib.resources importlib. ", ); - test.assert_completions_include("resources"); + builder.build().contains("resources"); } #[test] fn import_with_leading_character() { - let test = cursor_test( + let builder = completion_test_builder( "\ import c ", ); - test.assert_completions_include("collections"); + builder.build().contains("collections"); } #[test] fn import_without_leading_character() { - let test = cursor_test( + let builder = completion_test_builder( "\ import ", ); - test.assert_completions_include("collections"); + builder.build().contains("collections"); } #[test] fn import_multiple() { - let test = cursor_test( + let builder = completion_test_builder( "\ import re, c, sys ", ); - test.assert_completions_include("collections"); + builder.build().contains("collections"); } #[test] fn import_with_aliases() { - let test = cursor_test( + let builder = completion_test_builder( "\ import re as regexp, c, sys as system ", ); - test.assert_completions_include("collections"); + builder.build().contains("collections"); } #[test] fn import_over_multiple_lines() { - let test = cursor_test( + let builder = completion_test_builder( "\ import re as regexp, \\ c, \\ sys as system ", ); - test.assert_completions_include("collections"); + builder.build().contains("collections"); } #[test] fn import_unknown_in_module() { - let test = cursor_test( + let builder = completion_test_builder( "\ import ?, ", ); - test.assert_completions_include("collections"); + builder.build().contains("collections"); } #[test] fn import_via_from_with_leading_character() { - let test = cursor_test( + let builder = completion_test_builder( "\ from c ", ); - test.assert_completions_include("collections"); + builder.build().contains("collections"); } #[test] fn import_via_from_without_leading_character() { - let test = cursor_test( + let builder = completion_test_builder( "\ from ", ); - test.assert_completions_include("collections"); + builder.build().contains("collections"); } #[test] fn import_statement_with_submodule_with_leading_character() { - let test = cursor_test( + let builder = completion_test_builder( "\ import os.p ", ); - test.assert_completions_include("path"); - test.assert_completions_do_not_include("abspath"); + builder.build().contains("path").not_contains("abspath"); } #[test] fn import_statement_with_submodule_multiple() { - let test = cursor_test( + let builder = completion_test_builder( "\ import re, os.p, zlib ", ); - test.assert_completions_include("path"); - test.assert_completions_do_not_include("abspath"); + builder.build().contains("path").not_contains("abspath"); } #[test] fn import_statement_with_submodule_without_leading_character() { - let test = cursor_test( + let builder = completion_test_builder( "\ import os. ", ); - test.assert_completions_include("path"); - test.assert_completions_do_not_include("abspath"); + builder.build().contains("path").not_contains("abspath"); } #[test] fn import_via_from_with_submodule_with_leading_character() { - let test = cursor_test( + let builder = completion_test_builder( "\ from os.p ", ); - test.assert_completions_include("path"); - test.assert_completions_do_not_include("abspath"); + builder.build().contains("path").not_contains("abspath"); } #[test] fn import_via_from_with_submodule_without_leading_character() { - let test = cursor_test( + let builder = completion_test_builder( "\ from os. ", ); - test.assert_completions_include("path"); - test.assert_completions_do_not_include("abspath"); + builder.build().contains("path").not_contains("abspath"); } #[test] fn auto_import_with_submodule() { - let test = CursorTest::builder() + CursorTest::builder() .source("main.py", "Abra") .source("package/__init__.py", "AbraKadabra = 1") - .build(); - - let settings = CompletionSettings { auto_import: true }; - let expected = "AbraKadabra"; - let completions = completion(&test.db, &settings, test.cursor.file, test.cursor.offset); - assert!( - completions - .iter() - .any(|completion| completion.name == expected), - "Expected completions to include `{expected}`" - ); + .completion_test_builder() + .auto_import() + .build() + .contains("AbraKadabra"); } #[test] fn import_type_check_only_lowers_ranking() { - let test = CursorTest::builder() + let builder = CursorTest::builder() .source( "main.py", r#" @@ -3434,10 +3423,10 @@ from os. class Azorubine: pass "#, ) - .build(); + .completion_test_builder(); - let settings = CompletionSettings::default(); - let completions = completion(&test.db, &settings, test.cursor.file, test.cursor.offset); + let test = builder.build(); + let completions = test.completions(); let [apple_pos, banana_pos, cat_pos, azo_pos, ann_pos] = ["Apple", "Banana", "Cat", "Azorubine", "__annotations__"].map(|name| { @@ -3456,12 +3445,11 @@ from os. fn type_check_only_is_type_check_only() { // `@typing.type_check_only` is a function that's unavailable at runtime // and so should be the last "non-underscore" completion in `typing` - let test = cursor_test("from typing import t"); - - let settings = CompletionSettings::default(); - let completions = completion(&test.db, &settings, test.cursor.file, test.cursor.offset); - let last_nonunderscore = completions - .into_iter() + let builder = completion_test_builder("from typing import t"); + let test = builder.build(); + let last_nonunderscore = test + .completions() + .iter() .filter(|c| !c.name.starts_with('_')) .next_back() .unwrap(); @@ -3474,7 +3462,7 @@ from os. fn regression_test_issue_642() { // Regression test for https://github.com/astral-sh/ty/issues/642 - let test = cursor_test( + let test = completion_test_builder( r#" match 0: case 1 i: @@ -3483,540 +3471,535 @@ from os. ); assert_snapshot!( - test.completions_without_builtins(), + test.skip_builtins().build().snapshot(), @"", ); } #[test] fn completion_kind_recursive_type_alias() { - let test = cursor_test( + let builder = completion_test_builder( r#" type T = T | None def f(rec: T): re "#, ); + let test = builder.build(); - let completions = completion( - &test.db, - &CompletionSettings::default(), - test.cursor.file, - test.cursor.offset, - ); - let completion = completions.iter().find(|c| c.name == "rec").unwrap(); - - assert_eq!(completion.kind(&test.db), Some(CompletionKind::Struct)); + let completion = test.completions().iter().find(|c| c.name == "rec").unwrap(); + assert_eq!(completion.kind(builder.db()), Some(CompletionKind::Struct)); } #[test] fn no_completions_in_comment() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 # zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_string_double_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(\"zqzq\") ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(\"Foo.zqzq\") ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_string_incomplete_double_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(\"zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(\"Foo.zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_string_single_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print('zqzq') ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print('Foo.zqzq') ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_string_incomplete_single_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print('zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print('Foo.zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_string_double_triple_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(\"\"\"zqzq\"\"\") ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(\"\"\"Foo.zqzq\"\"\") ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_string_incomplete_double_triple_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(\"\"\"zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(\"\"\"Foo.zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_string_single_triple_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print('''zqzq''') ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print('''Foo.zqzq''') ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_string_incomplete_single_triple_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print('''zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print('''Foo.zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_fstring_double_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(f\"zqzq\") ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(f\"{Foo} and Foo.zqzq\") ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_fstring_incomplete_double_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(f\"zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(f\"{Foo} and Foo.zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_fstring_single_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(f'zqzq') ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(f'{Foo} and Foo.zqzq') ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_fstring_incomplete_single_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(f'zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(f'{Foo} and Foo.zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_fstring_double_triple_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(f\"\"\"zqzq\"\"\") ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(f\"\"\"{Foo} and Foo.zqzq\"\"\") ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_fstring_incomplete_double_triple_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(f\"\"\"zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(f\"\"\"{Foo} and Foo.zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_fstring_single_triple_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(f'''zqzq''') ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(f'''{Foo} and Foo.zqzq''') ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_fstring_incomplete_single_triple_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(f'''zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(f'''{Foo} and Foo.zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_tstring_double_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(t\"zqzq\") ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(t\"{Foo} and Foo.zqzq\") ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_tstring_incomplete_double_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(t\"zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(t\"{Foo} and Foo.zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_tstring_single_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(t'zqzq') ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(t'{Foo} and Foo.zqzq') ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_tstring_incomplete_single_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(t'zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(t'{Foo} and Foo.zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_tstring_double_triple_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(t\"\"\"zqzq\"\"\") ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(t\"\"\"{Foo} and Foo.zqzq\"\"\") ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_tstring_incomplete_double_triple_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(t\"\"\"zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(t\"\"\"{Foo} and Foo.zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_tstring_single_triple_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(t'''zqzq''') ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(t'''{Foo} and Foo.zqzq''') ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn no_completions_in_tstring_incomplete_single_triple_quote() { - let test = cursor_test( + let test = completion_test_builder( "\ zqzqzq = 1 print(t'''zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); - let test = cursor_test( + let test = completion_test_builder( "\ class Foo: zqzqzq = 1 print(t'''{Foo} and Foo.zqzq ", ); - assert_snapshot!(test.completions_without_builtins(), @""); + assert_snapshot!(test.skip_builtins().build().snapshot(), @""); } #[test] fn typevar_with_upper_bound() { - let test = cursor_test( + let builder = completion_test_builder( "\ def f[T: str](msg: T): msg. ", ); - test.assert_completions_include("upper"); - test.assert_completions_include("capitalize"); + let test = builder.build(); + test.contains("upper"); + test.contains("capitalize"); } #[test] fn typevar_with_constraints() { // Test TypeVar with constraints - let test = cursor_test( + let builder = completion_test_builder( "\ from typing import TypeVar @@ -4034,66 +4017,144 @@ def f(x: T): x. ", ); - test.assert_completions_include("on_a_and_b"); - test.assert_completions_do_not_include("only_on_a"); - test.assert_completions_do_not_include("only_on_b"); + let test = builder.build(); + + test.contains("on_a_and_b"); + test.not_contains("only_on_a"); + test.not_contains("only_on_b"); } #[test] fn typevar_without_bounds_or_constraints() { - let test = cursor_test( + let test = completion_test_builder( "\ def f[T](x: T): x. ", ); - test.assert_completions_include("__repr__"); + test.build().contains("__repr__"); } - // NOTE: The methods below are getting somewhat ridiculous. - // We should refactor this by converting to using a builder - // to set different modes. ---AG + /// A way to create a simple single-file (named `main.py`) completion test + /// builder. + /// + /// Use cases that require multiple files with a `` marker + /// in a file other than `main.py` can use `CursorTest::builder()` + /// and then `CursorTestBuilder::completion_test_builder()`. + fn completion_test_builder(source: &str) -> CompletionTestBuilder { + CursorTest::builder() + .source("main.py", source) + .completion_test_builder() + } - impl CursorTest { - /// Returns all completions except for builtins. - fn completions_without_builtins(&self) -> String { - self.completions_if(|c| !c.builtin) - } + /// A builder for executing a completion test. + /// + /// This mostly owns the responsibility for generating snapshots + /// of completions from a cursor position in source code. Most of + /// the options involve some kind of filtering or adjustment to + /// apply to the snapshots, depending on what one wants to test. + struct CompletionTestBuilder { + cursor_test: CursorTest, + settings: CompletionSettings, + skip_builtins: bool, + type_signatures: bool, + predicate: Option bool>>, + } - fn completions_without_builtins_with_types(&self) -> String { - self.completions_if_snapshot( - |c| !c.builtin, - |c| { - format!( - "{} :: {}", - c.name, - c.ty.map(|ty| ty.display(&self.db).to_string()) - .unwrap_or_else(|| "Unavailable".to_string()) - ) - }, - ) - } - - fn completions_if(&self, predicate: impl Fn(&Completion) -> bool) -> String { - self.completions_if_snapshot(predicate, |c| c.name.as_str().to_string()) - } - - fn completions_if_snapshot( - &self, - predicate: impl Fn(&Completion) -> bool, - snapshot: impl Fn(&Completion) -> String, - ) -> String { - let settings = CompletionSettings::default(); - let completions = completion(&self.db, &settings, self.cursor.file, self.cursor.offset); - if completions.is_empty() { - return "".to_string(); - } - let included = completions + impl CompletionTestBuilder { + /// Returns completions based on this configuration. + fn build(&self) -> CompletionTest<'_> { + let original = completion( + &self.cursor_test.db, + &self.settings, + self.cursor_test.cursor.file, + self.cursor_test.cursor.offset, + ); + let filtered = original .iter() - .filter(|label| predicate(label)) - .map(snapshot) - .collect::>(); - if included.is_empty() { + .filter(|c| !self.skip_builtins || !c.builtin) + .filter(|c| { + self.predicate + .as_ref() + .map(|predicate| predicate(c)) + .unwrap_or(true) + }) + .cloned() + .collect(); + CompletionTest { + db: self.db(), + original, + filtered, + type_signatures: self.type_signatures, + } + } + + /// Returns the underlying test DB. + fn db(&self) -> &ty_project::TestDb { + &self.cursor_test.db + } + + /// When enabled, symbols that aren't in scope but available + /// in the environment will be included. + /// + /// Not enabled by default. + fn auto_import(mut self) -> CompletionTestBuilder { + self.settings.auto_import = true; + self + } + + /// When set, builtins from completions are skipped. This is + /// useful in tests to reduce noise for scope based completions. + /// + /// Not enabled by default. + fn skip_builtins(mut self) -> CompletionTestBuilder { + self.skip_builtins = true; + self + } + + /// When set, type signatures of each completion item are + /// included in the snapshot. This is useful when one wants + /// to specifically test types, but it usually best to leave + /// off as it can add lots of noise. + /// + /// Not enabled by default. + fn type_signatures(mut self) -> CompletionTestBuilder { + self.type_signatures = true; + self + } + + /// Apply arbitrary filtering to completions. + fn filter( + mut self, + predicate: impl Fn(&Completion) -> bool + 'static, + ) -> CompletionTestBuilder { + self.predicate = Some(Box::new(predicate)); + self + } + } + + struct CompletionTest<'db> { + db: &'db ty_project::TestDb, + /// The original completions returned before any additional + /// test-specific filtering. We keep this around in order to + /// slightly modify the test snapshot generated. This + /// lets us differentiate between "absolutely no completions + /// were returned" and "completions were returned, but you + /// filtered them out." + original: Vec>, + /// The completions that the test should act upon. These are + /// filtered by things like `skip_builtins`. + filtered: Vec>, + /// Whether type signatures should be included in the snapshot + /// generated by `CompletionTest::snapshot`. + type_signatures: bool, + } + + impl<'db> CompletionTest<'db> { + fn snapshot(&self) -> String { + if self.original.is_empty() { + return "".to_string(); + } else if self.filtered.is_empty() { // It'd be nice to include the actual number of // completions filtered out, but in practice, the // number is environment dependent. For example, on @@ -4102,33 +4163,61 @@ def f[T](x: T): // ---AG return "".to_string(); } - included.join("\n") + self.filtered + .iter() + .map(|c| { + let name = c.name.as_str().to_string(); + if !self.type_signatures { + name + } else { + let ty = + c.ty.map(|ty| ty.display(self.db).to_string()) + .unwrap_or_else(|| "Unavailable".to_string()); + format!("{name} :: {ty}") + } + }) + .collect::>() + .join("\n") } #[track_caller] - fn assert_completions_include(&self, expected: &str) { - let settings = CompletionSettings::default(); - let completions = completion(&self.db, &settings, self.cursor.file, self.cursor.offset); - + fn contains(&self, expected: &str) -> &CompletionTest<'db> { assert!( - completions + self.filtered .iter() .any(|completion| completion.name == expected), "Expected completions to include `{expected}`" ); + self } #[track_caller] - fn assert_completions_do_not_include(&self, unexpected: &str) { - let settings = CompletionSettings::default(); - let completions = completion(&self.db, &settings, self.cursor.file, self.cursor.offset); - + fn not_contains(&self, unexpected: &str) -> &CompletionTest<'db> { assert!( - completions + self.filtered .iter() .all(|completion| completion.name != unexpected), "Expected completions to not include `{unexpected}`", ); + self + } + + /// Returns the underlying completions if the convenience assertions + /// aren't sufficiently expressive. + fn completions(&self) -> &[Completion<'db>] { + &self.filtered + } + } + + impl CursorTestBuilder { + fn completion_test_builder(&self) -> CompletionTestBuilder { + CompletionTestBuilder { + cursor_test: self.build(), + settings: CompletionSettings::default(), + skip_builtins: false, + type_signatures: false, + predicate: None, + } } } From 2d4e0edee4eb2317b2179fc931397a41a5a98231 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 27 Oct 2025 14:30:24 -0400 Subject: [PATCH 068/188] [ty] Add evaluation test for auto-import including symbols in current module This shouldn't happen. And indeed, currently, this results in a sub-optimal ranking. --- .../completion-evaluation-tasks.csv | 1 + .../completion.toml | 2 ++ .../auto-import-skips-current-module/main.py | 19 +++++++++++++++++++ .../pyproject.toml | 5 +++++ .../subdir/__init__.py | 1 + .../auto-import-skips-current-module/uv.lock | 8 ++++++++ 6 files changed, 36 insertions(+) create mode 100644 crates/ty_completion_eval/truth/auto-import-skips-current-module/completion.toml create mode 100644 crates/ty_completion_eval/truth/auto-import-skips-current-module/main.py create mode 100644 crates/ty_completion_eval/truth/auto-import-skips-current-module/pyproject.toml create mode 100644 crates/ty_completion_eval/truth/auto-import-skips-current-module/subdir/__init__.py create mode 100644 crates/ty_completion_eval/truth/auto-import-skips-current-module/uv.lock diff --git a/crates/ty_completion_eval/completion-evaluation-tasks.csv b/crates/ty_completion_eval/completion-evaluation-tasks.csv index 4c5d3e35b9..01b8ca4373 100644 --- a/crates/ty_completion_eval/completion-evaluation-tasks.csv +++ b/crates/ty_completion_eval/completion-evaluation-tasks.csv @@ -1,4 +1,5 @@ name,file,index,rank +auto-import-skips-current-module,main.py,0,4 fstring-completions,main.py,0,1 higher-level-symbols-preferred,main.py,0, higher-level-symbols-preferred,main.py,1,1 diff --git a/crates/ty_completion_eval/truth/auto-import-skips-current-module/completion.toml b/crates/ty_completion_eval/truth/auto-import-skips-current-module/completion.toml new file mode 100644 index 0000000000..cbd5805f07 --- /dev/null +++ b/crates/ty_completion_eval/truth/auto-import-skips-current-module/completion.toml @@ -0,0 +1,2 @@ +[settings] +auto-import = true diff --git a/crates/ty_completion_eval/truth/auto-import-skips-current-module/main.py b/crates/ty_completion_eval/truth/auto-import-skips-current-module/main.py new file mode 100644 index 0000000000..4043a87617 --- /dev/null +++ b/crates/ty_completion_eval/truth/auto-import-skips-current-module/main.py @@ -0,0 +1,19 @@ +Kadabra = 1 + +# This is meant to reflect that auto-import +# does *not* include completions for `Kadabra`. +# That is, before a bug was fixed, completions +# would offer two variants for `Kadabra`: one +# for the current module (correct) and another +# from auto-import that would insert +# `from main import Kadabra` into this module +# (incorrect). +# +# Since the incorrect one wasn't ranked above +# the correct one, this task unfortunately +# doesn't change the evaluation results. But +# I've added it anyway in case it does in the +# future (or if we change our evaluation metric +# to something that incorporates suggestions +# after the correct one). +Kada diff --git a/crates/ty_completion_eval/truth/auto-import-skips-current-module/pyproject.toml b/crates/ty_completion_eval/truth/auto-import-skips-current-module/pyproject.toml new file mode 100644 index 0000000000..cd277d8097 --- /dev/null +++ b/crates/ty_completion_eval/truth/auto-import-skips-current-module/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "test" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [] diff --git a/crates/ty_completion_eval/truth/auto-import-skips-current-module/subdir/__init__.py b/crates/ty_completion_eval/truth/auto-import-skips-current-module/subdir/__init__.py new file mode 100644 index 0000000000..a205414e7b --- /dev/null +++ b/crates/ty_completion_eval/truth/auto-import-skips-current-module/subdir/__init__.py @@ -0,0 +1 @@ +AbraKadabra = 1 diff --git a/crates/ty_completion_eval/truth/auto-import-skips-current-module/uv.lock b/crates/ty_completion_eval/truth/auto-import-skips-current-module/uv.lock new file mode 100644 index 0000000000..a4937d10d3 --- /dev/null +++ b/crates/ty_completion_eval/truth/auto-import-skips-current-module/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "test" +version = "0.1.0" +source = { virtual = "." } From 765257bdcee023c7ebb6ecbc02245b4c4990b06d Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 27 Oct 2025 14:17:07 -0400 Subject: [PATCH 069/188] [ty] Filter out "unimported" from the current module Note that this doesn't change the evaluation results unfortunately. In particular, prior to this fix, the correct result was ranked above the redundant result. Our MRR-based evaluation doesn't care about anything below the rank of the correct answer, and so this change isn't reflected in our evaluation. Fixes astral-sh/ty#1445 --- crates/ty_ide/src/completion.rs | 54 ++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 1b384493fe..18af1da727 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -330,6 +330,10 @@ fn add_unimported_completions<'db>( let members = importer.members_in_scope_at(scoped.node, scoped.node.start()); for symbol in all_symbols(db, typed) { + if symbol.module.file(db) == Some(file) { + continue; + } + let request = ImportRequest::import_from(symbol.module.name(db).as_str(), &symbol.symbol.name); // FIXME: `all_symbols` doesn't account for wildcard imports. @@ -866,6 +870,7 @@ fn compare_suggestions(c1: &Completion, c2: &Completion) -> Ordering { mod tests { use insta::assert_snapshot; use ruff_python_parser::{Mode, ParseOptions, TokenKind, Tokens}; + use ty_python_semantic::ModuleName; use crate::completion::{Completion, completion}; use crate::tests::{CursorTest, CursorTestBuilder}; @@ -3400,6 +3405,24 @@ from os. .contains("AbraKadabra"); } + #[test] + fn auto_import_should_not_include_symbols_in_current_module() { + let snapshot = CursorTest::builder() + .source("main.py", "Kadabra = 1\nKad") + .source("package/__init__.py", "AbraKadabra = 1") + .completion_test_builder() + .auto_import() + .type_signatures() + .module_names() + .filter(|c| c.name.contains("Kadabra")) + .build() + .snapshot(); + assert_snapshot!(snapshot, @r" + AbraKadabra :: Unavailable :: package + Kadabra :: Literal[1] :: Current module + "); + } + #[test] fn import_type_check_only_lowers_ranking() { let builder = CursorTest::builder() @@ -4058,6 +4081,9 @@ def f[T](x: T): settings: CompletionSettings, skip_builtins: bool, type_signatures: bool, + module_names: bool, + // This doesn't seem like a "very complex" type to me... ---AG + #[allow(clippy::type_complexity)] predicate: Option bool>>, } @@ -4086,6 +4112,7 @@ def f[T](x: T): original, filtered, type_signatures: self.type_signatures, + module_names: self.module_names, } } @@ -4123,6 +4150,13 @@ def f[T](x: T): self } + /// When set, the module name for each symbol is included + /// in the snapshot (if available). + fn module_names(mut self) -> CompletionTestBuilder { + self.module_names = true; + self + } + /// Apply arbitrary filtering to completions. fn filter( mut self, @@ -4148,6 +4182,9 @@ def f[T](x: T): /// Whether type signatures should be included in the snapshot /// generated by `CompletionTest::snapshot`. type_signatures: bool, + /// Whether module names should be included in the snapshot + /// generated by `CompletionTest::snapshot`. + module_names: bool, } impl<'db> CompletionTest<'db> { @@ -4166,15 +4203,21 @@ def f[T](x: T): self.filtered .iter() .map(|c| { - let name = c.name.as_str().to_string(); - if !self.type_signatures { - name - } else { + let mut snapshot = c.name.as_str().to_string(); + if self.type_signatures { let ty = c.ty.map(|ty| ty.display(self.db).to_string()) .unwrap_or_else(|| "Unavailable".to_string()); - format!("{name} :: {ty}") + snapshot = format!("{snapshot} :: {ty}"); } + if self.module_names { + let module_name = c + .module_name + .map(ModuleName::as_str) + .unwrap_or("Current module"); + snapshot = format!("{snapshot} :: {module_name}"); + } + snapshot }) .collect::>() .join("\n") @@ -4216,6 +4259,7 @@ def f[T](x: T): settings: CompletionSettings::default(), skip_builtins: false, type_signatures: false, + module_names: false, predicate: None, } } From 8b22fd1a5f645487651dfa6f8a934f7157a10d1f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 29 Oct 2025 10:18:33 -0400 Subject: [PATCH 070/188] [ty] Rename `Type::into_nominal_instance` (#21124) --- crates/ty_python_semantic/src/types.rs | 28 +++++++++---------- .../src/types/bound_super.rs | 2 +- .../ty_python_semantic/src/types/builder.rs | 4 +-- crates/ty_python_semantic/src/types/class.rs | 2 +- .../ty_python_semantic/src/types/instance.rs | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 5efb65d289..14cfb64ed4 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -812,17 +812,17 @@ impl<'db> Type<'db> { } fn is_none(&self, db: &'db dyn Db) -> bool { - self.into_nominal_instance() + self.as_nominal_instance() .is_some_and(|instance| instance.has_known_class(db, KnownClass::NoneType)) } fn is_bool(&self, db: &'db dyn Db) -> bool { - self.into_nominal_instance() + self.as_nominal_instance() .is_some_and(|instance| instance.has_known_class(db, KnownClass::Bool)) } fn is_enum(&self, db: &'db dyn Db) -> bool { - self.into_nominal_instance() + self.as_nominal_instance() .and_then(|instance| crate::types::enums::enum_metadata(db, instance.class_literal(db))) .is_some() } @@ -852,7 +852,7 @@ impl<'db> Type<'db> { } pub(crate) fn is_notimplemented(&self, db: &'db dyn Db) -> bool { - self.into_nominal_instance() + self.as_nominal_instance() .is_some_and(|instance| instance.has_known_class(db, KnownClass::NotImplementedType)) } @@ -899,7 +899,7 @@ impl<'db> Type<'db> { ) -> Option> { let class_type = match self { Type::NominalInstance(instance) => instance, - Type::TypeAlias(alias) => alias.value_type(db).into_nominal_instance()?, + Type::TypeAlias(alias) => alias.value_type(db).as_nominal_instance()?, _ => return None, } .class(db); @@ -939,7 +939,7 @@ impl<'db> Type<'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. fn tuple_instance_spec(&self, db: &'db dyn Db) -> Option>> { - self.into_nominal_instance() + self.as_nominal_instance() .and_then(|instance| instance.tuple_spec(db)) } @@ -954,7 +954,7 @@ impl<'db> Type<'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`. fn exact_tuple_instance_spec(&self, db: &'db dyn Db) -> Option>> { - self.into_nominal_instance() + self.as_nominal_instance() .and_then(|instance| instance.own_tuple_spec(db)) } @@ -1044,7 +1044,7 @@ impl<'db> Type<'db> { } #[track_caller] - pub(crate) fn expect_class_literal(self) -> ClassLiteral<'db> { + pub(crate) const fn expect_class_literal(self) -> ClassLiteral<'db> { self.as_class_literal() .expect("Expected a Type::ClassLiteral variant") } @@ -1058,7 +1058,7 @@ impl<'db> Type<'db> { matches!(self, Type::ClassLiteral(..)) } - pub(crate) fn as_enum_literal(self) -> Option> { + pub(crate) const fn as_enum_literal(self) -> Option> { match self { Type::EnumLiteral(enum_literal) => Some(enum_literal), _ => None, @@ -1067,7 +1067,7 @@ impl<'db> Type<'db> { #[cfg(test)] #[track_caller] - pub(crate) fn expect_enum_literal(self) -> EnumLiteralType<'db> { + pub(crate) const fn expect_enum_literal(self) -> EnumLiteralType<'db> { self.as_enum_literal() .expect("Expected a Type::EnumLiteral variant") } @@ -1076,7 +1076,7 @@ impl<'db> Type<'db> { matches!(self, Type::TypedDict(..)) } - pub(crate) fn as_typed_dict(self) -> Option> { + pub(crate) const fn as_typed_dict(self) -> Option> { match self { Type::TypedDict(typed_dict) => Some(typed_dict), _ => None, @@ -1126,7 +1126,7 @@ impl<'db> Type<'db> { #[cfg(test)] #[track_caller] - pub(crate) fn expect_union(self) -> UnionType<'db> { + pub(crate) const fn expect_union(self) -> UnionType<'db> { self.as_union().expect("Expected a Type::Union variant") } @@ -4332,7 +4332,7 @@ impl<'db> Type<'db> { // It will need a special handling, so it remember the origin type to properly // resolve the attribute. if matches!( - self.into_nominal_instance() + self.as_nominal_instance() .and_then(|instance| instance.known_class(db)), Some(KnownClass::ModuleType | KnownClass::GenericAlias) ) { @@ -4544,7 +4544,7 @@ impl<'db> Type<'db> { // 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.into_nominal_instance() { + 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) { diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 72782a4eac..011318db51 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -186,7 +186,7 @@ impl<'db> SuperOwnerKind<'db> { } SuperOwnerKind::Instance(instance) => instance .normalized_impl(db, visitor) - .into_nominal_instance() + .as_nominal_instance() .map(Self::Instance) .unwrap_or(Self::Dynamic(DynamicType::Any)), } diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index de413c00a9..7f46a80239 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -854,7 +854,7 @@ impl<'db> InnerIntersectionBuilder<'db> { _ => { let known_instance = new_positive - .into_nominal_instance() + .as_nominal_instance() .and_then(|instance| instance.known_class(db)); if known_instance == Some(KnownClass::Object) { @@ -966,7 +966,7 @@ impl<'db> InnerIntersectionBuilder<'db> { let contains_bool = || { self.positive .iter() - .filter_map(|ty| ty.into_nominal_instance()) + .filter_map(|ty| ty.as_nominal_instance()) .filter_map(|instance| instance.known_class(db)) .any(KnownClass::is_bool) }; diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 51cb3bdc3a..a2a3da8dfd 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1295,7 +1295,7 @@ impl<'db> Field<'db> { /// pub(crate) fn is_kw_only_sentinel(&self, db: &'db dyn Db) -> bool { self.declared_ty - .into_nominal_instance() + .as_nominal_instance() .is_some_and(|instance| instance.has_known_class(db, KnownClass::KwOnly)) } } diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 8a4912d242..9c69023c63 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -88,7 +88,7 @@ impl<'db> Type<'db> { Type::NominalInstance(NominalInstanceType(NominalInstanceInner::ExactTuple(tuple))) } - pub(crate) const fn into_nominal_instance(self) -> Option> { + pub(crate) const fn as_nominal_instance(self) -> Option> { match self { Type::NominalInstance(instance_type) => Some(instance_type), _ => None, From 83a00c0ac8b043d44a2cb1ecd7e7946aca8415b8 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Wed, 29 Oct 2025 23:56:12 +0900 Subject: [PATCH 071/188] [ty] follow the breaking API changes made in salsa-rs/salsa#1015 (#21117) --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- crates/ty_python_semantic/src/dunder_all.rs | 6 +++++- crates/ty_python_semantic/src/place.rs | 6 +++++- .../src/semantic_index/re_exports.rs | 2 +- crates/ty_python_semantic/src/types.rs | 15 ++++++++++++++- crates/ty_python_semantic/src/types/class.rs | 19 +++++++++++++++++-- crates/ty_python_semantic/src/types/enums.rs | 1 + .../ty_python_semantic/src/types/function.rs | 2 ++ .../ty_python_semantic/src/types/generics.rs | 1 + crates/ty_python_semantic/src/types/infer.rs | 17 +++++++++++++++-- .../ty_python_semantic/src/types/instance.rs | 7 ++++++- crates/ty_python_semantic/src/types/narrow.rs | 2 ++ .../src/types/protocol_class.rs | 1 + crates/ty_python_semantic/src/types/tuple.rs | 6 +++++- fuzz/Cargo.toml | 2 +- 16 files changed, 80 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 216e3f115a..070f471e4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3563,7 +3563,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" version = "0.24.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=25b3ef146cfa2615f4ec82760bd0c22b454d0a12#25b3ef146cfa2615f4ec82760bd0c22b454d0a12" +source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198" dependencies = [ "boxcar", "compact_str", @@ -3587,12 +3587,12 @@ dependencies = [ [[package]] name = "salsa-macro-rules" version = "0.24.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=25b3ef146cfa2615f4ec82760bd0c22b454d0a12#25b3ef146cfa2615f4ec82760bd0c22b454d0a12" +source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198" [[package]] name = "salsa-macros" version = "0.24.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=25b3ef146cfa2615f4ec82760bd0c22b454d0a12#25b3ef146cfa2615f4ec82760bd0c22b454d0a12" +source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 301efbd180..1cce423668 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" } rustc-hash = { version = "2.0.0" } rustc-stable-hash = { version = "0.1.2" } # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "25b3ef146cfa2615f4ec82760bd0c22b454d0a12", default-features = false, features = [ +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "cdd0b85516a52c18b8a6d17a2279a96ed6c3e198", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", diff --git a/crates/ty_python_semantic/src/dunder_all.rs b/crates/ty_python_semantic/src/dunder_all.rs index 7273bc975b..17f1706b7e 100644 --- a/crates/ty_python_semantic/src/dunder_all.rs +++ b/crates/ty_python_semantic/src/dunder_all.rs @@ -10,7 +10,11 @@ use crate::semantic_index::{SemanticIndex, semantic_index}; use crate::types::{Truthiness, Type, TypeContext, infer_expression_types}; use crate::{Db, ModuleName, resolve_module}; -fn dunder_all_names_cycle_initial(_db: &dyn Db, _file: File) -> Option> { +fn dunder_all_names_cycle_initial( + _db: &dyn Db, + _id: salsa::Id, + _file: File, +) -> Option> { None } diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 99804ae5c8..3989942b04 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -697,6 +697,7 @@ impl<'db> From> for PlaceAndQualifiers<'db> { fn place_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _scope: ScopeId<'db>, _place_id: ScopedPlaceId, _requires_explicit_reexport: RequiresExplicitReExport, @@ -1528,7 +1529,10 @@ mod implicit_globals { .collect() } - fn module_type_symbols_initial(_db: &dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> { + fn module_type_symbols_initial( + _db: &dyn Db, + _id: salsa::Id, + ) -> smallvec::SmallVec<[ast::name::Name; 8]> { smallvec::SmallVec::default() } 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 52f26ef4a0..f1d741bd12 100644 --- a/crates/ty_python_semantic/src/semantic_index/re_exports.rs +++ b/crates/ty_python_semantic/src/semantic_index/re_exports.rs @@ -30,7 +30,7 @@ use rustc_hash::FxHashMap; use crate::{Db, module_name::ModuleName, resolve_module}; -fn exports_cycle_initial(_db: &dyn Db, _file: File) -> Box<[Name]> { +fn exports_cycle_initial(_db: &dyn Db, _id: salsa::Id, _file: File) -> Box<[Name]> { Box::default() } diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 14cfb64ed4..78844ef0c3 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -371,6 +371,7 @@ impl Default for MemberLookupPolicy { fn member_lookup_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _self: Type<'db>, _name: Name, _policy: MemberLookupPolicy, @@ -380,6 +381,7 @@ fn member_lookup_cycle_initial<'db>( fn class_lookup_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _self: Type<'db>, _name: Name, _policy: MemberLookupPolicy, @@ -389,6 +391,7 @@ fn class_lookup_cycle_initial<'db>( fn variance_cycle_initial<'db, T>( _db: &'db dyn Db, + _id: salsa::Id, _self: T, _typevar: BoundTypeVarInstance<'db>, ) -> TypeVarVariance { @@ -7420,6 +7423,7 @@ impl<'db> VarianceInferable<'db> for Type<'db> { #[allow(clippy::trivially_copy_pass_by_ref)] fn is_redundant_with_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _subtype: Type<'db>, _supertype: Type<'db>, ) -> bool { @@ -7428,6 +7432,7 @@ fn is_redundant_with_cycle_initial<'db>( fn apply_specialization_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _self: Type<'db>, _specialization: Specialization<'db>, ) -> Type<'db> { @@ -8498,6 +8503,7 @@ impl<'db> TypeVarInstance<'db> { fn lazy_bound_or_constraints_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _self: TypeVarInstance<'db>, ) -> Option> { None @@ -8505,6 +8511,7 @@ fn lazy_bound_or_constraints_cycle_initial<'db>( fn lazy_default_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _self: TypeVarInstance<'db>, ) -> Option> { None @@ -10032,6 +10039,7 @@ fn walk_bound_method_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( fn into_callable_type_cycle_initial<'db>( db: &'db dyn Db, + _id: salsa::Id, _self: BoundMethodType<'db>, ) -> CallableType<'db> { CallableType::bottom(db) @@ -11007,12 +11015,17 @@ impl<'db> PEP695TypeAliasType<'db> { fn generic_context_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _self: PEP695TypeAliasType<'db>, ) -> Option> { None } -fn value_type_cycle_initial<'db>(_db: &'db dyn Db, _self: PEP695TypeAliasType<'db>) -> Type<'db> { +fn value_type_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self: PEP695TypeAliasType<'db>, +) -> Type<'db> { Type::Never } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index a2a3da8dfd..d7f50ce716 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -71,6 +71,7 @@ use rustc_hash::FxHashSet; fn explicit_bases_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _self: ClassLiteral<'db>, ) -> Box<[Type<'db>]> { Box::default() @@ -78,6 +79,7 @@ fn explicit_bases_cycle_initial<'db>( fn inheritance_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _self: ClassLiteral<'db>, ) -> Option { None @@ -85,6 +87,7 @@ fn inheritance_cycle_initial<'db>( fn implicit_attribute_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _class_body_scope: ScopeId<'db>, _name: String, _target_method_decorator: MethodDecorator, @@ -94,6 +97,7 @@ fn implicit_attribute_initial<'db>( fn try_mro_cycle_initial<'db>( db: &'db dyn Db, + _id: salsa::Id, self_: ClassLiteral<'db>, specialization: Option>, ) -> Result, MroError<'db>> { @@ -104,13 +108,18 @@ fn try_mro_cycle_initial<'db>( } #[allow(clippy::unnecessary_wraps)] -fn is_typed_dict_cycle_initial<'db>(_db: &'db dyn Db, _self: ClassLiteral<'db>) -> bool { +fn is_typed_dict_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self: ClassLiteral<'db>, +) -> bool { false } #[allow(clippy::unnecessary_wraps)] fn try_metaclass_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _self_: ClassLiteral<'db>, ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { Err(MetaclassError { @@ -169,6 +178,7 @@ impl<'db> CodeGeneratorKind<'db> { fn code_generator_of_class_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _class: ClassLiteral<'db>, _specialization: Option>, ) -> Option> { @@ -1181,7 +1191,11 @@ impl<'db> ClassType<'db> { } } -fn into_callable_cycle_initial<'db>(_db: &'db dyn Db, _self: ClassType<'db>) -> Type<'db> { +fn into_callable_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self: ClassType<'db>, +) -> Type<'db> { Type::Never } @@ -1334,6 +1348,7 @@ impl get_size2::GetSize for ClassLiteral<'_> {} fn generic_context_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _self: ClassLiteral<'db>, ) -> Option> { None diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index daffbaebbb..671b919929 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -39,6 +39,7 @@ impl EnumMetadata<'_> { #[allow(clippy::unnecessary_wraps)] fn enum_metadata_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _class: ClassLiteral<'db>, ) -> Option> { Some(EnumMetadata::empty()) diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 459edb6c25..36b826435a 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1195,6 +1195,7 @@ fn is_mode_with_nontrivial_return_type<'db>(db: &'db dyn Db, mode: Type<'db>) -> fn signature_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _function: FunctionType<'db>, ) -> CallableSignature<'db> { CallableSignature::single(Signature::bottom()) @@ -1202,6 +1203,7 @@ fn signature_cycle_initial<'db>( fn last_definition_signature_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _function: FunctionType<'db>, ) -> Signature<'db> { Signature::bottom() diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 931ec28756..59216ca607 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -627,6 +627,7 @@ impl<'db> GenericContext<'db> { fn inferable_typevars_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _self: GenericContext<'db>, ) -> FxHashSet> { FxHashSet::default() diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index d244597e16..78e91f5883 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -81,7 +81,11 @@ pub(crate) fn infer_scope_types<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Sc TypeInferenceBuilder::new(db, InferenceRegion::Scope(scope), index, &module).finish_scope() } -fn scope_cycle_initial<'db>(_db: &'db dyn Db, scope: ScopeId<'db>) -> ScopeInference<'db> { +fn scope_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + scope: ScopeId<'db>, +) -> ScopeInference<'db> { ScopeInference::cycle_initial(scope) } @@ -126,6 +130,7 @@ fn definition_cycle_recover<'db>( fn definition_cycle_initial<'db>( db: &'db dyn Db, + _id: salsa::Id, definition: Definition<'db>, ) -> DefinitionInference<'db> { DefinitionInference::cycle_initial(definition.scope(db)) @@ -158,6 +163,7 @@ pub(crate) fn infer_deferred_types<'db>( fn deferred_cycle_initial<'db>( db: &'db dyn Db, + _id: salsa::Id, definition: Definition<'db>, ) -> DefinitionInference<'db> { DefinitionInference::cycle_initial(definition.scope(db)) @@ -240,6 +246,7 @@ fn expression_cycle_recover<'db>( fn expression_cycle_initial<'db>( db: &'db dyn Db, + _id: salsa::Id, input: InferExpression<'db>, ) -> ExpressionInference<'db> { ExpressionInference::cycle_initial(input.expression(db).scope(db)) @@ -287,6 +294,7 @@ fn infer_expression_type_impl<'db>(db: &'db dyn Db, input: InferExpression<'db>) fn single_expression_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _input: InferExpression<'db>, ) -> Type<'db> { Type::Never @@ -399,6 +407,7 @@ pub(crate) fn static_expression_truthiness<'db>( fn static_expression_truthiness_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _expression: Expression<'db>, ) -> Truthiness { Truthiness::Ambiguous @@ -422,7 +431,11 @@ pub(super) fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> U unpacker.finish() } -fn unpack_cycle_initial<'db>(_db: &'db dyn Db, _unpack: Unpack<'db>) -> UnpackResult<'db> { +fn unpack_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _unpack: Unpack<'db>, +) -> UnpackResult<'db> { UnpackResult::cycle_initial(Type::Never) } diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 9c69023c63..f6c7b8406d 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -666,7 +666,12 @@ impl<'db> ProtocolInstanceType<'db> { .is_always_satisfied(db) } - fn initial<'db>(_db: &'db dyn Db, _value: ProtocolInstanceType<'db>, _: ()) -> bool { + fn initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _value: ProtocolInstanceType<'db>, + _: (), + ) -> bool { true } diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index b797eb4764..736272cb4a 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -120,6 +120,7 @@ fn all_negative_narrowing_constraints_for_pattern<'db>( fn constraints_for_expression_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _expression: Expression<'db>, ) -> Option> { None @@ -127,6 +128,7 @@ fn constraints_for_expression_cycle_initial<'db>( fn negative_constraints_for_expression_cycle_initial<'db>( _db: &'db dyn Db, + _id: salsa::Id, _expression: Expression<'db>, ) -> Option> { None diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 46a1142d4c..822bb548f7 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -866,6 +866,7 @@ fn cached_protocol_interface<'db>( #[allow(clippy::trivially_copy_pass_by_ref)] fn proto_interface_cycle_initial<'db>( db: &'db dyn Db, + _id: salsa::Id, _class: ClassType<'db>, ) -> ProtocolInterface<'db> { ProtocolInterface::empty(db) diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index cfb2febce4..8f989ce04f 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -290,7 +290,11 @@ impl<'db> TupleType<'db> { } } -fn to_class_type_cycle_initial<'db>(db: &'db dyn Db, self_: TupleType<'db>) -> ClassType<'db> { +fn to_class_type_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + self_: TupleType<'db>, +) -> ClassType<'db> { let tuple_class = KnownClass::Tuple .try_to_class_literal(db) .expect("Typeshed should always have a `tuple` class in `builtins.pyi`"); diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 82c9ae3be6..359e59d1e1 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -30,7 +30,7 @@ ty_python_semantic = { path = "../crates/ty_python_semantic" } ty_vendored = { path = "../crates/ty_vendored" } libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "25b3ef146cfa2615f4ec82760bd0c22b454d0a12", default-features = false, features = [ +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "cdd0b85516a52c18b8a6d17a2279a96ed6c3e198", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", From d38a5292d2a9c8509da6868046da0caa49ed5e46 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Wed, 29 Oct 2025 22:57:37 +0800 Subject: [PATCH 072/188] [`airflow`] warning `airflow....DAG.create_dagrun` has been removed (`AIR301`) (#21093) --- .../airflow/AIR301_class_attribute.py | 18 + .../src/rules/airflow/rules/removal_in_3.rs | 6 + .../airflow/rules/suggested_to_update_3_0.rs | 10 +- ...sts__AIR301_AIR301_class_attribute.py.snap | 940 +++++++++--------- ...airflow__tests__AIR311_AIR311_args.py.snap | 19 + 5 files changed, 537 insertions(+), 456 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_class_attribute.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_class_attribute.py index 605ad5b07e..a290ffcebc 100644 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_class_attribute.py +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_class_attribute.py @@ -10,6 +10,7 @@ from airflow.datasets import ( ) from airflow.datasets.manager import DatasetManager from airflow.lineage.hook import DatasetLineageInfo, HookLineageCollector +from airflow.models.dag import DAG from airflow.providers.amazon.aws.auth_manager.aws_auth_manager import AwsAuthManager from airflow.providers.apache.beam.hooks import BeamHook, NotAir302HookError from airflow.providers.google.cloud.secrets.secret_manager import ( @@ -20,6 +21,7 @@ from airflow.providers_manager import ProvidersManager from airflow.secrets.base_secrets import BaseSecretsBackend from airflow.secrets.local_filesystem import LocalFilesystemBackend + # airflow.Dataset dataset_from_root = DatasetFromRoot() dataset_from_root.iter_datasets() @@ -56,6 +58,10 @@ hlc.add_input_dataset() hlc.add_output_dataset() hlc.collected_datasets() +# airflow.models.dag.DAG +test_dag = DAG(dag_id="test_dag") +test_dag.create_dagrun() + # airflow.providers.amazon.auth_manager.aws_auth_manager aam = AwsAuthManager() aam.is_authorized_dataset() @@ -96,3 +102,15 @@ base_secret_backend.get_connections() # airflow.secrets.local_filesystem lfb = LocalFilesystemBackend() lfb.get_connections() + +from airflow.models import DAG + +# airflow.DAG +test_dag = DAG(dag_id="test_dag") +test_dag.create_dagrun() + +from airflow import DAG + +# airflow.DAG +test_dag = DAG(dag_id="test_dag") +test_dag.create_dagrun() 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 df09b37e81..78c89da0b4 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 @@ -492,6 +492,12 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) { "collected_datasets" => Replacement::AttrName("collected_assets"), _ => return, }, + ["airflow", "models", "dag", "DAG"] | ["airflow", "models", "DAG"] | ["airflow", "DAG"] => { + match attr.as_str() { + "create_dagrun" => Replacement::None, + _ => return, + } + } ["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() { "initialize_providers_dataset_uri_resources" => { Replacement::AttrName("initialize_providers_asset_uri_resources") diff --git a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs index 7f19161797..327576cf9d 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs @@ -288,10 +288,12 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { }, // airflow.model..DAG - ["airflow", "models", .., "DAG"] => Replacement::SourceModuleMoved { - module: "airflow.sdk", - name: "DAG".to_string(), - }, + ["airflow", "models", "dag", "DAG"] | ["airflow", "models", "DAG"] | ["airflow", "DAG"] => { + Replacement::SourceModuleMoved { + module: "airflow.sdk", + name: "DAG".to_string(), + } + } // airflow.sensors.base [ diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_class_attribute.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_class_attribute.py.snap index 1c235414df..f49b67de40 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_class_attribute.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_class_attribute.py.snap @@ -2,292 +2,292 @@ source: crates/ruff_linter/src/rules/airflow/mod.rs --- AIR301 [*] `iter_datasets` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:25:19 + --> AIR301_class_attribute.py:27:19 | -23 | # airflow.Dataset -24 | dataset_from_root = DatasetFromRoot() -25 | dataset_from_root.iter_datasets() +25 | # airflow.Dataset +26 | dataset_from_root = DatasetFromRoot() +27 | dataset_from_root.iter_datasets() | ^^^^^^^^^^^^^ -26 | dataset_from_root.iter_dataset_aliases() +28 | dataset_from_root.iter_dataset_aliases() | help: Use `iter_assets` instead -22 | -23 | # airflow.Dataset -24 | dataset_from_root = DatasetFromRoot() +24 | +25 | # airflow.Dataset +26 | dataset_from_root = DatasetFromRoot() - dataset_from_root.iter_datasets() -25 + dataset_from_root.iter_assets() -26 | dataset_from_root.iter_dataset_aliases() -27 | -28 | # airflow.datasets +27 + dataset_from_root.iter_assets() +28 | dataset_from_root.iter_dataset_aliases() +29 | +30 | # airflow.datasets AIR301 [*] `iter_dataset_aliases` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:26:19 + --> AIR301_class_attribute.py:28:19 | -24 | dataset_from_root = DatasetFromRoot() -25 | dataset_from_root.iter_datasets() -26 | dataset_from_root.iter_dataset_aliases() +26 | dataset_from_root = DatasetFromRoot() +27 | dataset_from_root.iter_datasets() +28 | dataset_from_root.iter_dataset_aliases() | ^^^^^^^^^^^^^^^^^^^^ -27 | -28 | # airflow.datasets +29 | +30 | # airflow.datasets | help: Use `iter_asset_aliases` instead -23 | # airflow.Dataset -24 | dataset_from_root = DatasetFromRoot() -25 | dataset_from_root.iter_datasets() +25 | # airflow.Dataset +26 | dataset_from_root = DatasetFromRoot() +27 | dataset_from_root.iter_datasets() - dataset_from_root.iter_dataset_aliases() -26 + dataset_from_root.iter_asset_aliases() -27 | -28 | # airflow.datasets -29 | dataset_to_test_method_call = Dataset() +28 + dataset_from_root.iter_asset_aliases() +29 | +30 | # airflow.datasets +31 | dataset_to_test_method_call = Dataset() AIR301 [*] `iter_datasets` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:30:29 + --> AIR301_class_attribute.py:32:29 | -28 | # airflow.datasets -29 | dataset_to_test_method_call = Dataset() -30 | dataset_to_test_method_call.iter_datasets() +30 | # airflow.datasets +31 | dataset_to_test_method_call = Dataset() +32 | dataset_to_test_method_call.iter_datasets() | ^^^^^^^^^^^^^ -31 | dataset_to_test_method_call.iter_dataset_aliases() +33 | dataset_to_test_method_call.iter_dataset_aliases() | help: Use `iter_assets` instead -27 | -28 | # airflow.datasets -29 | dataset_to_test_method_call = Dataset() +29 | +30 | # airflow.datasets +31 | dataset_to_test_method_call = Dataset() - dataset_to_test_method_call.iter_datasets() -30 + dataset_to_test_method_call.iter_assets() -31 | dataset_to_test_method_call.iter_dataset_aliases() -32 | -33 | alias_to_test_method_call = DatasetAlias() +32 + dataset_to_test_method_call.iter_assets() +33 | dataset_to_test_method_call.iter_dataset_aliases() +34 | +35 | alias_to_test_method_call = DatasetAlias() AIR301 [*] `iter_dataset_aliases` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:31:29 + --> AIR301_class_attribute.py:33:29 | -29 | dataset_to_test_method_call = Dataset() -30 | dataset_to_test_method_call.iter_datasets() -31 | dataset_to_test_method_call.iter_dataset_aliases() +31 | dataset_to_test_method_call = Dataset() +32 | dataset_to_test_method_call.iter_datasets() +33 | dataset_to_test_method_call.iter_dataset_aliases() | ^^^^^^^^^^^^^^^^^^^^ -32 | -33 | alias_to_test_method_call = DatasetAlias() +34 | +35 | alias_to_test_method_call = DatasetAlias() | help: Use `iter_asset_aliases` instead -28 | # airflow.datasets -29 | dataset_to_test_method_call = Dataset() -30 | dataset_to_test_method_call.iter_datasets() +30 | # airflow.datasets +31 | dataset_to_test_method_call = Dataset() +32 | dataset_to_test_method_call.iter_datasets() - dataset_to_test_method_call.iter_dataset_aliases() -31 + dataset_to_test_method_call.iter_asset_aliases() -32 | -33 | alias_to_test_method_call = DatasetAlias() -34 | alias_to_test_method_call.iter_datasets() +33 + dataset_to_test_method_call.iter_asset_aliases() +34 | +35 | alias_to_test_method_call = DatasetAlias() +36 | alias_to_test_method_call.iter_datasets() AIR301 [*] `iter_datasets` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:34:27 + --> AIR301_class_attribute.py:36:27 | -33 | alias_to_test_method_call = DatasetAlias() -34 | alias_to_test_method_call.iter_datasets() +35 | alias_to_test_method_call = DatasetAlias() +36 | alias_to_test_method_call.iter_datasets() | ^^^^^^^^^^^^^ -35 | alias_to_test_method_call.iter_dataset_aliases() +37 | alias_to_test_method_call.iter_dataset_aliases() | help: Use `iter_assets` instead -31 | dataset_to_test_method_call.iter_dataset_aliases() -32 | -33 | alias_to_test_method_call = DatasetAlias() +33 | dataset_to_test_method_call.iter_dataset_aliases() +34 | +35 | alias_to_test_method_call = DatasetAlias() - alias_to_test_method_call.iter_datasets() -34 + alias_to_test_method_call.iter_assets() -35 | alias_to_test_method_call.iter_dataset_aliases() -36 | -37 | any_to_test_method_call = DatasetAny() +36 + alias_to_test_method_call.iter_assets() +37 | alias_to_test_method_call.iter_dataset_aliases() +38 | +39 | any_to_test_method_call = DatasetAny() AIR301 [*] `iter_dataset_aliases` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:35:27 + --> AIR301_class_attribute.py:37:27 | -33 | alias_to_test_method_call = DatasetAlias() -34 | alias_to_test_method_call.iter_datasets() -35 | alias_to_test_method_call.iter_dataset_aliases() +35 | alias_to_test_method_call = DatasetAlias() +36 | alias_to_test_method_call.iter_datasets() +37 | alias_to_test_method_call.iter_dataset_aliases() | ^^^^^^^^^^^^^^^^^^^^ -36 | -37 | any_to_test_method_call = DatasetAny() +38 | +39 | any_to_test_method_call = DatasetAny() | help: Use `iter_asset_aliases` instead -32 | -33 | alias_to_test_method_call = DatasetAlias() -34 | alias_to_test_method_call.iter_datasets() +34 | +35 | alias_to_test_method_call = DatasetAlias() +36 | alias_to_test_method_call.iter_datasets() - alias_to_test_method_call.iter_dataset_aliases() -35 + alias_to_test_method_call.iter_asset_aliases() -36 | -37 | any_to_test_method_call = DatasetAny() -38 | any_to_test_method_call.iter_datasets() +37 + alias_to_test_method_call.iter_asset_aliases() +38 | +39 | any_to_test_method_call = DatasetAny() +40 | any_to_test_method_call.iter_datasets() AIR301 [*] `iter_datasets` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:38:25 + --> AIR301_class_attribute.py:40:25 | -37 | any_to_test_method_call = DatasetAny() -38 | any_to_test_method_call.iter_datasets() +39 | any_to_test_method_call = DatasetAny() +40 | any_to_test_method_call.iter_datasets() | ^^^^^^^^^^^^^ -39 | any_to_test_method_call.iter_dataset_aliases() +41 | any_to_test_method_call.iter_dataset_aliases() | help: Use `iter_assets` instead -35 | alias_to_test_method_call.iter_dataset_aliases() -36 | -37 | any_to_test_method_call = DatasetAny() +37 | alias_to_test_method_call.iter_dataset_aliases() +38 | +39 | any_to_test_method_call = DatasetAny() - any_to_test_method_call.iter_datasets() -38 + any_to_test_method_call.iter_assets() -39 | any_to_test_method_call.iter_dataset_aliases() -40 | -41 | # airflow.datasets.manager +40 + any_to_test_method_call.iter_assets() +41 | any_to_test_method_call.iter_dataset_aliases() +42 | +43 | # airflow.datasets.manager AIR301 [*] `iter_dataset_aliases` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:39:25 + --> AIR301_class_attribute.py:41:25 | -37 | any_to_test_method_call = DatasetAny() -38 | any_to_test_method_call.iter_datasets() -39 | any_to_test_method_call.iter_dataset_aliases() +39 | any_to_test_method_call = DatasetAny() +40 | any_to_test_method_call.iter_datasets() +41 | any_to_test_method_call.iter_dataset_aliases() | ^^^^^^^^^^^^^^^^^^^^ -40 | -41 | # airflow.datasets.manager +42 | +43 | # airflow.datasets.manager | help: Use `iter_asset_aliases` instead -36 | -37 | any_to_test_method_call = DatasetAny() -38 | any_to_test_method_call.iter_datasets() +38 | +39 | any_to_test_method_call = DatasetAny() +40 | any_to_test_method_call.iter_datasets() - any_to_test_method_call.iter_dataset_aliases() -39 + any_to_test_method_call.iter_asset_aliases() -40 | -41 | # airflow.datasets.manager -42 | dm = DatasetManager() +41 + any_to_test_method_call.iter_asset_aliases() +42 | +43 | # airflow.datasets.manager +44 | dm = DatasetManager() AIR301 [*] `airflow.datasets.manager.DatasetManager` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:42:6 + --> AIR301_class_attribute.py:44:6 | -41 | # airflow.datasets.manager -42 | dm = DatasetManager() +43 | # airflow.datasets.manager +44 | dm = DatasetManager() | ^^^^^^^^^^^^^^ -43 | dm.register_dataset_change() -44 | dm.create_datasets() +45 | dm.register_dataset_change() +46 | dm.create_datasets() | help: Use `AssetManager` from `airflow.assets.manager` instead. -19 | from airflow.providers_manager import ProvidersManager -20 | from airflow.secrets.base_secrets import BaseSecretsBackend -21 | from airflow.secrets.local_filesystem import LocalFilesystemBackend -22 + from airflow.assets.manager import AssetManager -23 | -24 | # airflow.Dataset -25 | dataset_from_root = DatasetFromRoot() +20 | from airflow.providers_manager import ProvidersManager +21 | from airflow.secrets.base_secrets import BaseSecretsBackend +22 | from airflow.secrets.local_filesystem import LocalFilesystemBackend +23 + from airflow.assets.manager import AssetManager +24 | +25 | +26 | # airflow.Dataset -------------------------------------------------------------------------------- -40 | any_to_test_method_call.iter_dataset_aliases() -41 | -42 | # airflow.datasets.manager +42 | any_to_test_method_call.iter_dataset_aliases() +43 | +44 | # airflow.datasets.manager - dm = DatasetManager() -43 + dm = AssetManager() -44 | dm.register_dataset_change() -45 | dm.create_datasets() -46 | dm.notify_dataset_created() +45 + dm = AssetManager() +46 | dm.register_dataset_change() +47 | dm.create_datasets() +48 | dm.notify_dataset_created() AIR301 [*] `register_dataset_change` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:43:4 - | -41 | # airflow.datasets.manager -42 | dm = DatasetManager() -43 | dm.register_dataset_change() - | ^^^^^^^^^^^^^^^^^^^^^^^ -44 | dm.create_datasets() -45 | dm.notify_dataset_created() - | -help: Use `register_asset_change` instead -40 | -41 | # airflow.datasets.manager -42 | dm = DatasetManager() - - dm.register_dataset_change() -43 + dm.register_asset_change() -44 | dm.create_datasets() -45 | dm.notify_dataset_created() -46 | dm.notify_dataset_changed() - -AIR301 [*] `create_datasets` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:44:4 - | -42 | dm = DatasetManager() -43 | dm.register_dataset_change() -44 | dm.create_datasets() - | ^^^^^^^^^^^^^^^ -45 | dm.notify_dataset_created() -46 | dm.notify_dataset_changed() - | -help: Use `create_assets` instead -41 | # airflow.datasets.manager -42 | dm = DatasetManager() -43 | dm.register_dataset_change() - - dm.create_datasets() -44 + dm.create_assets() -45 | dm.notify_dataset_created() -46 | dm.notify_dataset_changed() -47 | dm.notify_dataset_alias_created() - -AIR301 [*] `notify_dataset_created` is removed in Airflow 3.0 --> AIR301_class_attribute.py:45:4 | -43 | dm.register_dataset_change() -44 | dm.create_datasets() -45 | dm.notify_dataset_created() - | ^^^^^^^^^^^^^^^^^^^^^^ -46 | dm.notify_dataset_changed() -47 | dm.notify_dataset_alias_created() +43 | # airflow.datasets.manager +44 | dm = DatasetManager() +45 | dm.register_dataset_change() + | ^^^^^^^^^^^^^^^^^^^^^^^ +46 | dm.create_datasets() +47 | dm.notify_dataset_created() | -help: Use `notify_asset_created` instead -42 | dm = DatasetManager() -43 | dm.register_dataset_change() -44 | dm.create_datasets() - - dm.notify_dataset_created() -45 + dm.notify_asset_created() -46 | dm.notify_dataset_changed() -47 | dm.notify_dataset_alias_created() -48 | +help: Use `register_asset_change` instead +42 | +43 | # airflow.datasets.manager +44 | dm = DatasetManager() + - dm.register_dataset_change() +45 + dm.register_asset_change() +46 | dm.create_datasets() +47 | dm.notify_dataset_created() +48 | dm.notify_dataset_changed() -AIR301 [*] `notify_dataset_changed` is removed in Airflow 3.0 +AIR301 [*] `create_datasets` is removed in Airflow 3.0 --> AIR301_class_attribute.py:46:4 | -44 | dm.create_datasets() -45 | dm.notify_dataset_created() -46 | dm.notify_dataset_changed() - | ^^^^^^^^^^^^^^^^^^^^^^ -47 | dm.notify_dataset_alias_created() +44 | dm = DatasetManager() +45 | dm.register_dataset_change() +46 | dm.create_datasets() + | ^^^^^^^^^^^^^^^ +47 | dm.notify_dataset_created() +48 | dm.notify_dataset_changed() | -help: Use `notify_asset_changed` instead -43 | dm.register_dataset_change() -44 | dm.create_datasets() -45 | dm.notify_dataset_created() - - dm.notify_dataset_changed() -46 + dm.notify_asset_changed() -47 | dm.notify_dataset_alias_created() -48 | -49 | # airflow.lineage.hook +help: Use `create_assets` instead +43 | # airflow.datasets.manager +44 | dm = DatasetManager() +45 | dm.register_dataset_change() + - dm.create_datasets() +46 + dm.create_assets() +47 | dm.notify_dataset_created() +48 | dm.notify_dataset_changed() +49 | dm.notify_dataset_alias_created() -AIR301 [*] `notify_dataset_alias_created` is removed in Airflow 3.0 +AIR301 [*] `notify_dataset_created` is removed in Airflow 3.0 --> AIR301_class_attribute.py:47:4 | -45 | dm.notify_dataset_created() -46 | dm.notify_dataset_changed() -47 | dm.notify_dataset_alias_created() +45 | dm.register_dataset_change() +46 | dm.create_datasets() +47 | dm.notify_dataset_created() + | ^^^^^^^^^^^^^^^^^^^^^^ +48 | dm.notify_dataset_changed() +49 | dm.notify_dataset_alias_created() + | +help: Use `notify_asset_created` instead +44 | dm = DatasetManager() +45 | dm.register_dataset_change() +46 | dm.create_datasets() + - dm.notify_dataset_created() +47 + dm.notify_asset_created() +48 | dm.notify_dataset_changed() +49 | dm.notify_dataset_alias_created() +50 | + +AIR301 [*] `notify_dataset_changed` is removed in Airflow 3.0 + --> AIR301_class_attribute.py:48:4 + | +46 | dm.create_datasets() +47 | dm.notify_dataset_created() +48 | dm.notify_dataset_changed() + | ^^^^^^^^^^^^^^^^^^^^^^ +49 | dm.notify_dataset_alias_created() + | +help: Use `notify_asset_changed` instead +45 | dm.register_dataset_change() +46 | dm.create_datasets() +47 | dm.notify_dataset_created() + - dm.notify_dataset_changed() +48 + dm.notify_asset_changed() +49 | dm.notify_dataset_alias_created() +50 | +51 | # airflow.lineage.hook + +AIR301 [*] `notify_dataset_alias_created` is removed in Airflow 3.0 + --> AIR301_class_attribute.py:49:4 + | +47 | dm.notify_dataset_created() +48 | dm.notify_dataset_changed() +49 | dm.notify_dataset_alias_created() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -48 | -49 | # airflow.lineage.hook +50 | +51 | # airflow.lineage.hook | help: Use `notify_asset_alias_created` instead -44 | dm.create_datasets() -45 | dm.notify_dataset_created() -46 | dm.notify_dataset_changed() +46 | dm.create_datasets() +47 | dm.notify_dataset_created() +48 | dm.notify_dataset_changed() - dm.notify_dataset_alias_created() -47 + dm.notify_asset_alias_created() -48 | -49 | # airflow.lineage.hook -50 | dl_info = DatasetLineageInfo() +49 + dm.notify_asset_alias_created() +50 | +51 | # airflow.lineage.hook +52 | dl_info = DatasetLineageInfo() AIR301 [*] `airflow.lineage.hook.DatasetLineageInfo` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:50:11 + --> AIR301_class_attribute.py:52:11 | -49 | # airflow.lineage.hook -50 | dl_info = DatasetLineageInfo() +51 | # airflow.lineage.hook +52 | dl_info = DatasetLineageInfo() | ^^^^^^^^^^^^^^^^^^ -51 | dl_info.dataset +53 | dl_info.dataset | help: Use `AssetLineageInfo` from `airflow.lineage.hook` instead. 9 | DatasetAny, @@ -295,344 +295,380 @@ help: Use `AssetLineageInfo` from `airflow.lineage.hook` instead. 11 | from airflow.datasets.manager import DatasetManager - from airflow.lineage.hook import DatasetLineageInfo, HookLineageCollector 12 + from airflow.lineage.hook import DatasetLineageInfo, HookLineageCollector, AssetLineageInfo -13 | from airflow.providers.amazon.aws.auth_manager.aws_auth_manager import AwsAuthManager -14 | from airflow.providers.apache.beam.hooks import BeamHook, NotAir302HookError -15 | from airflow.providers.google.cloud.secrets.secret_manager import ( +13 | from airflow.models.dag import DAG +14 | from airflow.providers.amazon.aws.auth_manager.aws_auth_manager import AwsAuthManager +15 | from airflow.providers.apache.beam.hooks import BeamHook, NotAir302HookError -------------------------------------------------------------------------------- -47 | dm.notify_dataset_alias_created() -48 | -49 | # airflow.lineage.hook +49 | dm.notify_dataset_alias_created() +50 | +51 | # airflow.lineage.hook - dl_info = DatasetLineageInfo() -50 + dl_info = AssetLineageInfo() -51 | dl_info.dataset -52 | -53 | hlc = HookLineageCollector() +52 + dl_info = AssetLineageInfo() +53 | dl_info.dataset +54 | +55 | hlc = HookLineageCollector() AIR301 [*] `dataset` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:51:9 + --> AIR301_class_attribute.py:53:9 | -49 | # airflow.lineage.hook -50 | dl_info = DatasetLineageInfo() -51 | dl_info.dataset +51 | # airflow.lineage.hook +52 | dl_info = DatasetLineageInfo() +53 | dl_info.dataset | ^^^^^^^ -52 | -53 | hlc = HookLineageCollector() +54 | +55 | hlc = HookLineageCollector() | help: Use `asset` instead -48 | -49 | # airflow.lineage.hook -50 | dl_info = DatasetLineageInfo() +50 | +51 | # airflow.lineage.hook +52 | dl_info = DatasetLineageInfo() - dl_info.dataset -51 + dl_info.asset -52 | -53 | hlc = HookLineageCollector() -54 | hlc.create_dataset() +53 + dl_info.asset +54 | +55 | hlc = HookLineageCollector() +56 | hlc.create_dataset() AIR301 [*] `create_dataset` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:54:5 - | -53 | hlc = HookLineageCollector() -54 | hlc.create_dataset() - | ^^^^^^^^^^^^^^ -55 | hlc.add_input_dataset() -56 | hlc.add_output_dataset() - | -help: Use `create_asset` instead -51 | dl_info.dataset -52 | -53 | hlc = HookLineageCollector() - - hlc.create_dataset() -54 + hlc.create_asset() -55 | hlc.add_input_dataset() -56 | hlc.add_output_dataset() -57 | hlc.collected_datasets() - -AIR301 [*] `add_input_dataset` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:55:5 - | -53 | hlc = HookLineageCollector() -54 | hlc.create_dataset() -55 | hlc.add_input_dataset() - | ^^^^^^^^^^^^^^^^^ -56 | hlc.add_output_dataset() -57 | hlc.collected_datasets() - | -help: Use `add_input_asset` instead -52 | -53 | hlc = HookLineageCollector() -54 | hlc.create_dataset() - - hlc.add_input_dataset() -55 + hlc.add_input_asset() -56 | hlc.add_output_dataset() -57 | hlc.collected_datasets() -58 | - -AIR301 [*] `add_output_dataset` is removed in Airflow 3.0 --> AIR301_class_attribute.py:56:5 | -54 | hlc.create_dataset() -55 | hlc.add_input_dataset() -56 | hlc.add_output_dataset() - | ^^^^^^^^^^^^^^^^^^ -57 | hlc.collected_datasets() +55 | hlc = HookLineageCollector() +56 | hlc.create_dataset() + | ^^^^^^^^^^^^^^ +57 | hlc.add_input_dataset() +58 | hlc.add_output_dataset() | -help: Use `add_output_asset` instead -53 | hlc = HookLineageCollector() -54 | hlc.create_dataset() -55 | hlc.add_input_dataset() - - hlc.add_output_dataset() -56 + hlc.add_output_asset() -57 | hlc.collected_datasets() -58 | -59 | # airflow.providers.amazon.auth_manager.aws_auth_manager +help: Use `create_asset` instead +53 | dl_info.dataset +54 | +55 | hlc = HookLineageCollector() + - hlc.create_dataset() +56 + hlc.create_asset() +57 | hlc.add_input_dataset() +58 | hlc.add_output_dataset() +59 | hlc.collected_datasets() -AIR301 [*] `collected_datasets` is removed in Airflow 3.0 +AIR301 [*] `add_input_dataset` is removed in Airflow 3.0 --> AIR301_class_attribute.py:57:5 | -55 | hlc.add_input_dataset() -56 | hlc.add_output_dataset() -57 | hlc.collected_datasets() +55 | hlc = HookLineageCollector() +56 | hlc.create_dataset() +57 | hlc.add_input_dataset() + | ^^^^^^^^^^^^^^^^^ +58 | hlc.add_output_dataset() +59 | hlc.collected_datasets() + | +help: Use `add_input_asset` instead +54 | +55 | hlc = HookLineageCollector() +56 | hlc.create_dataset() + - hlc.add_input_dataset() +57 + hlc.add_input_asset() +58 | hlc.add_output_dataset() +59 | hlc.collected_datasets() +60 | + +AIR301 [*] `add_output_dataset` is removed in Airflow 3.0 + --> AIR301_class_attribute.py:58:5 + | +56 | hlc.create_dataset() +57 | hlc.add_input_dataset() +58 | hlc.add_output_dataset() | ^^^^^^^^^^^^^^^^^^ -58 | -59 | # airflow.providers.amazon.auth_manager.aws_auth_manager +59 | hlc.collected_datasets() + | +help: Use `add_output_asset` instead +55 | hlc = HookLineageCollector() +56 | hlc.create_dataset() +57 | hlc.add_input_dataset() + - hlc.add_output_dataset() +58 + hlc.add_output_asset() +59 | hlc.collected_datasets() +60 | +61 | # airflow.models.dag.DAG + +AIR301 [*] `collected_datasets` is removed in Airflow 3.0 + --> AIR301_class_attribute.py:59:5 + | +57 | hlc.add_input_dataset() +58 | hlc.add_output_dataset() +59 | hlc.collected_datasets() + | ^^^^^^^^^^^^^^^^^^ +60 | +61 | # airflow.models.dag.DAG | help: Use `collected_assets` instead -54 | hlc.create_dataset() -55 | hlc.add_input_dataset() -56 | hlc.add_output_dataset() +56 | hlc.create_dataset() +57 | hlc.add_input_dataset() +58 | hlc.add_output_dataset() - hlc.collected_datasets() -57 + hlc.collected_assets() -58 | -59 | # airflow.providers.amazon.auth_manager.aws_auth_manager -60 | aam = AwsAuthManager() +59 + hlc.collected_assets() +60 | +61 | # airflow.models.dag.DAG +62 | test_dag = DAG(dag_id="test_dag") + +AIR301 `create_dagrun` is removed in Airflow 3.0 + --> AIR301_class_attribute.py:63:10 + | +61 | # airflow.models.dag.DAG +62 | test_dag = DAG(dag_id="test_dag") +63 | test_dag.create_dagrun() + | ^^^^^^^^^^^^^ +64 | +65 | # airflow.providers.amazon.auth_manager.aws_auth_manager + | AIR301 [*] `is_authorized_dataset` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:61:5 + --> AIR301_class_attribute.py:67:5 | -59 | # airflow.providers.amazon.auth_manager.aws_auth_manager -60 | aam = AwsAuthManager() -61 | aam.is_authorized_dataset() +65 | # airflow.providers.amazon.auth_manager.aws_auth_manager +66 | aam = AwsAuthManager() +67 | aam.is_authorized_dataset() | ^^^^^^^^^^^^^^^^^^^^^ -62 | -63 | # airflow.providers.apache.beam.hooks +68 | +69 | # airflow.providers.apache.beam.hooks | help: Use `is_authorized_asset` instead -58 | -59 | # airflow.providers.amazon.auth_manager.aws_auth_manager -60 | aam = AwsAuthManager() +64 | +65 | # airflow.providers.amazon.auth_manager.aws_auth_manager +66 | aam = AwsAuthManager() - aam.is_authorized_dataset() -61 + aam.is_authorized_asset() -62 | -63 | # airflow.providers.apache.beam.hooks -64 | # check get_conn_uri is caught if the class inherits from an airflow hook +67 + aam.is_authorized_asset() +68 | +69 | # airflow.providers.apache.beam.hooks +70 | # check get_conn_uri is caught if the class inherits from an airflow hook AIR301 [*] `get_conn_uri` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:73:13 + --> AIR301_class_attribute.py:79:13 | -71 | # airflow.providers.google.cloud.secrets.secret_manager -72 | csm_backend = CloudSecretManagerBackend() -73 | csm_backend.get_conn_uri() +77 | # airflow.providers.google.cloud.secrets.secret_manager +78 | csm_backend = CloudSecretManagerBackend() +79 | csm_backend.get_conn_uri() | ^^^^^^^^^^^^ -74 | csm_backend.get_connections() +80 | csm_backend.get_connections() | help: Use `get_conn_value` instead -70 | -71 | # airflow.providers.google.cloud.secrets.secret_manager -72 | csm_backend = CloudSecretManagerBackend() +76 | +77 | # airflow.providers.google.cloud.secrets.secret_manager +78 | csm_backend = CloudSecretManagerBackend() - csm_backend.get_conn_uri() -73 + csm_backend.get_conn_value() -74 | csm_backend.get_connections() -75 | -76 | # airflow.providers.hashicorp.secrets.vault +79 + csm_backend.get_conn_value() +80 | csm_backend.get_connections() +81 | +82 | # airflow.providers.hashicorp.secrets.vault AIR301 [*] `get_connections` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:74:13 + --> AIR301_class_attribute.py:80:13 | -72 | csm_backend = CloudSecretManagerBackend() -73 | csm_backend.get_conn_uri() -74 | csm_backend.get_connections() +78 | csm_backend = CloudSecretManagerBackend() +79 | csm_backend.get_conn_uri() +80 | csm_backend.get_connections() | ^^^^^^^^^^^^^^^ -75 | -76 | # airflow.providers.hashicorp.secrets.vault +81 | +82 | # airflow.providers.hashicorp.secrets.vault | help: Use `get_connection` instead -71 | # airflow.providers.google.cloud.secrets.secret_manager -72 | csm_backend = CloudSecretManagerBackend() -73 | csm_backend.get_conn_uri() +77 | # airflow.providers.google.cloud.secrets.secret_manager +78 | csm_backend = CloudSecretManagerBackend() +79 | csm_backend.get_conn_uri() - csm_backend.get_connections() -74 + csm_backend.get_connection() -75 | -76 | # airflow.providers.hashicorp.secrets.vault -77 | vault_backend = VaultBackend() +80 + csm_backend.get_connection() +81 | +82 | # airflow.providers.hashicorp.secrets.vault +83 | vault_backend = VaultBackend() AIR301 [*] `get_conn_uri` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:78:15 + --> AIR301_class_attribute.py:84:15 | -76 | # airflow.providers.hashicorp.secrets.vault -77 | vault_backend = VaultBackend() -78 | vault_backend.get_conn_uri() +82 | # airflow.providers.hashicorp.secrets.vault +83 | vault_backend = VaultBackend() +84 | vault_backend.get_conn_uri() | ^^^^^^^^^^^^ -79 | vault_backend.get_connections() +85 | vault_backend.get_connections() | help: Use `get_conn_value` instead -75 | -76 | # airflow.providers.hashicorp.secrets.vault -77 | vault_backend = VaultBackend() +81 | +82 | # airflow.providers.hashicorp.secrets.vault +83 | vault_backend = VaultBackend() - vault_backend.get_conn_uri() -78 + vault_backend.get_conn_value() -79 | vault_backend.get_connections() -80 | -81 | not_an_error = NotAir302SecretError() +84 + vault_backend.get_conn_value() +85 | vault_backend.get_connections() +86 | +87 | not_an_error = NotAir302SecretError() AIR301 [*] `get_connections` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:79:15 + --> AIR301_class_attribute.py:85:15 | -77 | vault_backend = VaultBackend() -78 | vault_backend.get_conn_uri() -79 | vault_backend.get_connections() +83 | vault_backend = VaultBackend() +84 | vault_backend.get_conn_uri() +85 | vault_backend.get_connections() | ^^^^^^^^^^^^^^^ -80 | -81 | not_an_error = NotAir302SecretError() +86 | +87 | not_an_error = NotAir302SecretError() | help: Use `get_connection` instead -76 | # airflow.providers.hashicorp.secrets.vault -77 | vault_backend = VaultBackend() -78 | vault_backend.get_conn_uri() +82 | # airflow.providers.hashicorp.secrets.vault +83 | vault_backend = VaultBackend() +84 | vault_backend.get_conn_uri() - vault_backend.get_connections() -79 + vault_backend.get_connection() -80 | -81 | not_an_error = NotAir302SecretError() -82 | not_an_error.get_conn_uri() +85 + vault_backend.get_connection() +86 | +87 | not_an_error = NotAir302SecretError() +88 | not_an_error.get_conn_uri() AIR301 [*] `initialize_providers_dataset_uri_resources` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:86:4 + --> AIR301_class_attribute.py:92:4 | -84 | # airflow.providers_manager -85 | pm = ProvidersManager() -86 | pm.initialize_providers_dataset_uri_resources() +90 | # airflow.providers_manager +91 | pm = ProvidersManager() +92 | pm.initialize_providers_dataset_uri_resources() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -87 | pm.dataset_factories -88 | pm.dataset_uri_handlers +93 | pm.dataset_factories +94 | pm.dataset_uri_handlers | help: Use `initialize_providers_asset_uri_resources` instead -83 | -84 | # airflow.providers_manager -85 | pm = ProvidersManager() +89 | +90 | # airflow.providers_manager +91 | pm = ProvidersManager() - pm.initialize_providers_dataset_uri_resources() -86 + pm.initialize_providers_asset_uri_resources() -87 | pm.dataset_factories -88 | pm.dataset_uri_handlers -89 | pm.dataset_to_openlineage_converters +92 + pm.initialize_providers_asset_uri_resources() +93 | pm.dataset_factories +94 | pm.dataset_uri_handlers +95 | pm.dataset_to_openlineage_converters AIR301 [*] `dataset_factories` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:87:4 + --> AIR301_class_attribute.py:93:4 | -85 | pm = ProvidersManager() -86 | pm.initialize_providers_dataset_uri_resources() -87 | pm.dataset_factories +91 | pm = ProvidersManager() +92 | pm.initialize_providers_dataset_uri_resources() +93 | pm.dataset_factories | ^^^^^^^^^^^^^^^^^ -88 | pm.dataset_uri_handlers -89 | pm.dataset_to_openlineage_converters +94 | pm.dataset_uri_handlers +95 | pm.dataset_to_openlineage_converters | help: Use `asset_factories` instead -84 | # airflow.providers_manager -85 | pm = ProvidersManager() -86 | pm.initialize_providers_dataset_uri_resources() +90 | # airflow.providers_manager +91 | pm = ProvidersManager() +92 | pm.initialize_providers_dataset_uri_resources() - pm.dataset_factories -87 + pm.asset_factories -88 | pm.dataset_uri_handlers -89 | pm.dataset_to_openlineage_converters -90 | +93 + pm.asset_factories +94 | pm.dataset_uri_handlers +95 | pm.dataset_to_openlineage_converters +96 | AIR301 [*] `dataset_uri_handlers` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:88:4 + --> AIR301_class_attribute.py:94:4 | -86 | pm.initialize_providers_dataset_uri_resources() -87 | pm.dataset_factories -88 | pm.dataset_uri_handlers +92 | pm.initialize_providers_dataset_uri_resources() +93 | pm.dataset_factories +94 | pm.dataset_uri_handlers | ^^^^^^^^^^^^^^^^^^^^ -89 | pm.dataset_to_openlineage_converters +95 | pm.dataset_to_openlineage_converters | help: Use `asset_uri_handlers` instead -85 | pm = ProvidersManager() -86 | pm.initialize_providers_dataset_uri_resources() -87 | pm.dataset_factories +91 | pm = ProvidersManager() +92 | pm.initialize_providers_dataset_uri_resources() +93 | pm.dataset_factories - pm.dataset_uri_handlers -88 + pm.asset_uri_handlers -89 | pm.dataset_to_openlineage_converters -90 | -91 | # airflow.secrets.base_secrets +94 + pm.asset_uri_handlers +95 | pm.dataset_to_openlineage_converters +96 | +97 | # airflow.secrets.base_secrets AIR301 [*] `dataset_to_openlineage_converters` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:89:4 + --> AIR301_class_attribute.py:95:4 | -87 | pm.dataset_factories -88 | pm.dataset_uri_handlers -89 | pm.dataset_to_openlineage_converters +93 | pm.dataset_factories +94 | pm.dataset_uri_handlers +95 | pm.dataset_to_openlineage_converters | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -90 | -91 | # airflow.secrets.base_secrets +96 | +97 | # airflow.secrets.base_secrets | help: Use `asset_to_openlineage_converters` instead -86 | pm.initialize_providers_dataset_uri_resources() -87 | pm.dataset_factories -88 | pm.dataset_uri_handlers +92 | pm.initialize_providers_dataset_uri_resources() +93 | pm.dataset_factories +94 | pm.dataset_uri_handlers - pm.dataset_to_openlineage_converters -89 + pm.asset_to_openlineage_converters -90 | -91 | # airflow.secrets.base_secrets -92 | base_secret_backend = BaseSecretsBackend() +95 + pm.asset_to_openlineage_converters +96 | +97 | # airflow.secrets.base_secrets +98 | base_secret_backend = BaseSecretsBackend() AIR301 [*] `get_conn_uri` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:93:21 - | -91 | # airflow.secrets.base_secrets -92 | base_secret_backend = BaseSecretsBackend() -93 | base_secret_backend.get_conn_uri() - | ^^^^^^^^^^^^ -94 | base_secret_backend.get_connections() - | + --> AIR301_class_attribute.py:99:21 + | + 97 | # airflow.secrets.base_secrets + 98 | base_secret_backend = BaseSecretsBackend() + 99 | base_secret_backend.get_conn_uri() + | ^^^^^^^^^^^^ +100 | base_secret_backend.get_connections() + | help: Use `get_conn_value` instead -90 | -91 | # airflow.secrets.base_secrets -92 | base_secret_backend = BaseSecretsBackend() - - base_secret_backend.get_conn_uri() -93 + base_secret_backend.get_conn_value() -94 | base_secret_backend.get_connections() -95 | -96 | # airflow.secrets.local_filesystem +96 | +97 | # airflow.secrets.base_secrets +98 | base_secret_backend = BaseSecretsBackend() + - base_secret_backend.get_conn_uri() +99 + base_secret_backend.get_conn_value() +100 | base_secret_backend.get_connections() +101 | +102 | # airflow.secrets.local_filesystem AIR301 [*] `get_connections` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:94:21 - | -92 | base_secret_backend = BaseSecretsBackend() -93 | base_secret_backend.get_conn_uri() -94 | base_secret_backend.get_connections() - | ^^^^^^^^^^^^^^^ -95 | -96 | # airflow.secrets.local_filesystem - | + --> AIR301_class_attribute.py:100:21 + | + 98 | base_secret_backend = BaseSecretsBackend() + 99 | base_secret_backend.get_conn_uri() +100 | base_secret_backend.get_connections() + | ^^^^^^^^^^^^^^^ +101 | +102 | # airflow.secrets.local_filesystem + | help: Use `get_connection` instead -91 | # airflow.secrets.base_secrets -92 | base_secret_backend = BaseSecretsBackend() -93 | base_secret_backend.get_conn_uri() - - base_secret_backend.get_connections() -94 + base_secret_backend.get_connection() -95 | -96 | # airflow.secrets.local_filesystem -97 | lfb = LocalFilesystemBackend() +97 | # airflow.secrets.base_secrets +98 | base_secret_backend = BaseSecretsBackend() +99 | base_secret_backend.get_conn_uri() + - base_secret_backend.get_connections() +100 + base_secret_backend.get_connection() +101 | +102 | # airflow.secrets.local_filesystem +103 | lfb = LocalFilesystemBackend() AIR301 [*] `get_connections` is removed in Airflow 3.0 - --> AIR301_class_attribute.py:98:5 - | -96 | # airflow.secrets.local_filesystem -97 | lfb = LocalFilesystemBackend() -98 | lfb.get_connections() - | ^^^^^^^^^^^^^^^ - | + --> AIR301_class_attribute.py:104:5 + | +102 | # airflow.secrets.local_filesystem +103 | lfb = LocalFilesystemBackend() +104 | lfb.get_connections() + | ^^^^^^^^^^^^^^^ +105 | +106 | from airflow.models import DAG + | help: Use `get_connection` instead -95 | -96 | # airflow.secrets.local_filesystem -97 | lfb = LocalFilesystemBackend() - - lfb.get_connections() -98 + lfb.get_connection() +101 | +102 | # airflow.secrets.local_filesystem +103 | lfb = LocalFilesystemBackend() + - lfb.get_connections() +104 + lfb.get_connection() +105 | +106 | from airflow.models import DAG +107 | + +AIR301 `create_dagrun` is removed in Airflow 3.0 + --> AIR301_class_attribute.py:110:10 + | +108 | # airflow.DAG +109 | test_dag = DAG(dag_id="test_dag") +110 | test_dag.create_dagrun() + | ^^^^^^^^^^^^^ +111 | +112 | from airflow import DAG + | + +AIR301 `create_dagrun` is removed in Airflow 3.0 + --> AIR301_class_attribute.py:116:10 + | +114 | # airflow.DAG +115 | test_dag = DAG(dag_id="test_dag") +116 | test_dag.create_dagrun() + | ^^^^^^^^^^^^^ + | diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_args.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_args.py.snap index 871c3389d7..8212d10d90 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_args.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_args.py.snap @@ -1,6 +1,25 @@ --- source: crates/ruff_linter/src/rules/airflow/mod.rs --- +AIR311 [*] `airflow.DAG` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + --> AIR311_args.py:13:1 + | +13 | DAG(dag_id="class_sla_callback", sla_miss_callback=sla_callback) + | ^^^ + | +help: Use `DAG` from `airflow.sdk` instead. +2 | +3 | from datetime import timedelta +4 | + - from airflow import DAG, dag +5 + from airflow import dag +6 | from airflow.operators.datetime import BranchDateTimeOperator +7 + from airflow.sdk import DAG +8 | +9 | +10 | def sla_callback(*arg, **kwargs): +note: This is an unsafe fix and may change runtime behavior + AIR311 `sla_miss_callback` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. --> AIR311_args.py:13:34 | From 7045898ffacb34743c036ca94c7f791f62087335 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Wed, 29 Oct 2025 18:39:36 +0000 Subject: [PATCH 073/188] [ty] Dont provide goto definition for definitions which are not reexported in builtins (#21127) --- crates/ty_ide/src/goto_definition.rs | 17 +++++++++++++++++ .../ty_python_semantic/src/types/ide_support.rs | 1 + 2 files changed, 18 insertions(+) diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index 6dc9e203b6..fc0fc28fb9 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -1592,6 +1592,23 @@ a = Test() "); } + /// Regression test for . + /// We must ensure we respect re-import convention for stub files for + /// imports in builtins.pyi. + #[test] + fn goto_definition_unimported_symbol_imported_in_builtins() { + let test = CursorTest::builder() + .source( + "main.py", + " +TracebackType +", + ) + .build(); + + assert_snapshot!(test.goto_definition(), @"No goto target found"); + } + impl CursorTest { fn goto_definition(&self) -> String { let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset) diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 9dad158369..02a9330299 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -615,6 +615,7 @@ pub fn definitions_for_name<'db>( }; find_symbol_in_scope(db, builtins_scope, name_str) .into_iter() + .filter(|def| def.is_reexported(db)) .flat_map(|def| { resolve_definition( db, From aca8ba76a489f2ec43958868254de2ce14c8e9d2 Mon Sep 17 00:00:00 2001 From: Jonas Vacek Date: Wed, 29 Oct 2025 20:03:56 +0100 Subject: [PATCH 074/188] [`flake8-bandit`] Fix correct example for `S308` (#21128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixed the incorrect import example in the "correct exmaple" ## Test Plan 🤷 --- .../src/rules/flake8_bandit/rules/suspicious_function_call.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs index 0cc1ebe4ed..423b7287bb 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs @@ -375,7 +375,7 @@ impl Violation for SuspiciousEvalUsage { /// /// /// def render_username(username): -/// return django.utils.html.format_html("{}", username) # username is escaped. +/// return format_html("{}", username) # username is escaped. /// ``` /// /// ## References From 5139f76d1f94ae3a3fc239dbbcc0791d0bec920c Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 29 Oct 2025 22:22:38 +0100 Subject: [PATCH 075/188] [ty] Infer type of `self` for decorated methods and properties (#21123) ## Summary Infer a type of unannotated `self` parameters in decorated methods / properties. closes https://github.com/astral-sh/ty/issues/1448 ## Test Plan Existing tests, some new tests. --- crates/ruff_benchmark/benches/ty_walltime.rs | 2 +- crates/ty_ide/src/semantic_tokens.rs | 2 +- .../resources/mdtest/annotations/self.md | 36 +++++++++++++-- .../resources/mdtest/overloads.md | 1 + ...…_-_`@classmethod`_(aaa04d4cfa3adaba).snap | 45 ++++++++++++------- .../ty_python_semantic/src/types/function.rs | 18 +++++--- .../src/types/infer/builder.rs | 26 +++++++---- 7 files changed, 97 insertions(+), 33 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index be6195d96a..47bff641d7 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -226,7 +226,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new( max_dep_date: "2025-08-09", python_version: PythonVersion::PY311, }, - 750, + 800, ); #[track_caller] diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index 4e66881f48..12e5e6581b 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -1413,7 +1413,7 @@ u = List.__name__ # __name__ should be variable "property" @ 168..176: Decorator "prop" @ 185..189: Method [definition] "self" @ 190..194: SelfParameter - "self" @ 212..216: Variable + "self" @ 212..216: TypeParameter "CONSTANT" @ 217..225: Variable [readonly] "obj" @ 227..230: Variable "MyClass" @ 233..240: Class diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 621db497c4..b635104a75 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -116,7 +116,7 @@ A.implicit_self(1) Passing `self` implicitly also verifies the type: ```py -from typing import Never +from typing import Never, Callable class Strange: def can_not_be_called(self: Never) -> None: ... @@ -139,6 +139,9 @@ The first parameter of instance methods always has type `Self`, if it is not exp The name `self` is not special in any way. ```py +def some_decorator(f: Callable) -> Callable: + return f + class B: def name_does_not_matter(this) -> Self: reveal_type(this) # revealed: Self@name_does_not_matter @@ -153,18 +156,45 @@ class B: reveal_type(self) # revealed: Self@keyword_only return self + @some_decorator + def decorated_method(self) -> Self: + reveal_type(self) # revealed: Self@decorated_method + return self + @property def a_property(self) -> Self: - # TODO: Should reveal Self@a_property - reveal_type(self) # revealed: Unknown + reveal_type(self) # revealed: Self@a_property return self + async def async_method(self) -> Self: + reveal_type(self) # revealed: Self@async_method + return self + + @staticmethod + def static_method(self): + # The parameter can be called `self`, but it is not treated as `Self` + reveal_type(self) # revealed: Unknown + + @staticmethod + @some_decorator + def decorated_static_method(self): + reveal_type(self) # revealed: Unknown + # TODO: On Python <3.10, this should ideally be rejected, because `staticmethod` objects were not callable. + @some_decorator + @staticmethod + def decorated_static_method_2(self): + reveal_type(self) # revealed: Unknown + reveal_type(B().name_does_not_matter()) # revealed: B reveal_type(B().positional_only(1)) # revealed: B reveal_type(B().keyword_only(x=1)) # revealed: B +reveal_type(B().decorated_method()) # revealed: Unknown # TODO: this should be B reveal_type(B().a_property) # revealed: Unknown + +async def _(): + reveal_type(await B().async_method()) # revealed: B ``` This also works for generic classes: diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md index 794abac2fc..f74cafb80b 100644 --- a/crates/ty_python_semantic/resources/mdtest/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/overloads.md @@ -598,6 +598,7 @@ class CheckClassMethod: # error: [invalid-overload] def try_from3(cls, x: int | str) -> CheckClassMethod | None: if isinstance(x, int): + # error: [call-non-callable] return cls(x) return None diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat…_-_`@classmethod`_(aaa04d4cfa3adaba).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat…_-_`@classmethod`_(aaa04d4cfa3adaba).snap index a2e0271576..9fb873f913 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat…_-_`@classmethod`_(aaa04d4cfa3adaba).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat…_-_`@classmethod`_(aaa04d4cfa3adaba).snap @@ -53,20 +53,21 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md 39 | # error: [invalid-overload] 40 | def try_from3(cls, x: int | str) -> CheckClassMethod | None: 41 | if isinstance(x, int): -42 | return cls(x) -43 | return None -44 | -45 | @overload -46 | @classmethod -47 | def try_from4(cls, x: int) -> CheckClassMethod: ... -48 | @overload -49 | @classmethod -50 | def try_from4(cls, x: str) -> None: ... -51 | @classmethod -52 | def try_from4(cls, x: int | str) -> CheckClassMethod | None: -53 | if isinstance(x, int): -54 | return cls(x) -55 | return None +42 | # error: [call-non-callable] +43 | return cls(x) +44 | return None +45 | +46 | @overload +47 | @classmethod +48 | def try_from4(cls, x: int) -> CheckClassMethod: ... +49 | @overload +50 | @classmethod +51 | def try_from4(cls, x: str) -> None: ... +52 | @classmethod +53 | def try_from4(cls, x: int | str) -> CheckClassMethod | None: +54 | if isinstance(x, int): +55 | return cls(x) +56 | return None ``` # Diagnostics @@ -124,8 +125,22 @@ error[invalid-overload]: Overloaded function `try_from3` does not use the `@clas | | | Missing here 41 | if isinstance(x, int): -42 | return cls(x) +42 | # error: [call-non-callable] | info: rule `invalid-overload` is enabled by default ``` + +``` +error[call-non-callable]: Object of type `CheckClassMethod` is not callable + --> src/mdtest_snippet.py:43:20 + | +41 | if isinstance(x, int): +42 | # error: [call-non-callable] +43 | return cls(x) + | ^^^^^^ +44 | return None + | +info: rule `call-non-callable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 36b826435a..0f5797ae7a 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -185,6 +185,16 @@ 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 { + 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 { + matches!(function_name, "__init_subclass__" | "__class_getitem__") +} + /// Representation of a function definition in the AST: either a non-generic function, or a generic /// function that has not been specialized. /// @@ -257,17 +267,15 @@ impl<'db> OverloadLiteral<'db> { /// 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 { - self.has_known_decorator(db, FunctionDecorators::STATICMETHOD) || self.name(db) == "__new__" + 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 { self.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) - || matches!( - self.name(db).as_str(), - "__init_subclass__" | "__class_getitem__" - ) + || is_implicit_classmethod(self.name(db)) } fn node<'ast>( diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 9a5091f837..edf8581bcd 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -78,6 +78,7 @@ use crate::types::diagnostic::{ }; use crate::types::function::{ FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral, + is_implicit_classmethod, is_implicit_staticmethod, }; use crate::types::generics::{ GenericContext, InferableTypeVars, LegacyGenericBase, SpecializationBuilder, bind_typevar, @@ -2580,18 +2581,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return None; } - let method = infer_definition_types(db, method_definition) - .declaration_type(method_definition) - .inner_type() - .as_function_literal()?; + let function_node = function_definition.node(self.module()); + let function_name = &function_node.name; - if method.is_classmethod(db) { - // TODO: set the type for `cls` argument - return None; - } else if method.is_staticmethod(db) { + // TODO: handle implicit type of `cls` for classmethods + if is_implicit_classmethod(function_name) || is_implicit_staticmethod(function_name) { return None; } + let inference = infer_definition_types(db, method_definition); + for decorator in &function_node.decorator_list { + let decorator_ty = inference.expression_type(&decorator.expression); + if decorator_ty.as_class_literal().is_some_and(|class| { + matches!( + class.known(db), + Some(KnownClass::Classmethod | KnownClass::Staticmethod) + ) + }) { + return None; + } + } + let class_definition = self.index.expect_single_definition(class); let class_literal = infer_definition_types(db, class_definition) .declaration_type(class_definition) From 980b4c55b28e02346dd47f6e02ede490534c8e10 Mon Sep 17 00:00:00 2001 From: Dan Parizher <105245560+danparizher@users.noreply.github.com> Date: Wed, 29 Oct 2025 17:37:39 -0400 Subject: [PATCH 076/188] [`ruff-ecosystem`] Fix CLI crash on Python 3.14 (#21092) --- .github/workflows/ci.yaml | 3 +-- python/ruff-ecosystem/ruff_ecosystem/cli.py | 3 ++- scripts/check_ecosystem.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7541b715a4..806949d81e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -531,8 +531,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 with: - # TODO: figure out why `ruff-ecosystem` crashes on Python 3.14 - python-version: "3.13" + python-version: ${{ env.PYTHON_VERSION }} activate-environment: true - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 diff --git a/python/ruff-ecosystem/ruff_ecosystem/cli.py b/python/ruff-ecosystem/ruff_ecosystem/cli.py index 6dda945a4b..b996504c22 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/cli.py +++ b/python/ruff-ecosystem/ruff_ecosystem/cli.py @@ -87,7 +87,8 @@ def entrypoint(): ) with cache_context as cache: - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) main_task = asyncio.ensure_future( main( command=RuffCommand(args.ruff_command), diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index 824cdcc217..00b691dc51 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -528,7 +528,8 @@ if __name__ == "__main__": else: logging.basicConfig(level=logging.INFO) - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) if args.checkouts: args.checkouts.mkdir(exist_ok=True, parents=True) main_task = asyncio.ensure_future( From 1ebedf6df59469b2adb3bce8d6e6718f7ee93beb Mon Sep 17 00:00:00 2001 From: Dan Parizher <105245560+danparizher@users.noreply.github.com> Date: Wed, 29 Oct 2025 17:45:08 -0400 Subject: [PATCH 077/188] [`ruff`] Add support for additional eager conversion patterns (`RUF065`) (#20657) ## Summary Fixes #20583 --- .../resources/test/fixtures/ruff/RUF065.py | 26 +++ .../ruff/rules/logging_eager_conversion.rs | 112 +++++++++-- ..._rules__ruff__tests__RUF065_RUF065.py.snap | 177 ++++++++++++++++++ 3 files changed, 301 insertions(+), 14 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF065.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF065.py index d7de9df7d2..879fb186bf 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF065.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF065.py @@ -43,3 +43,29 @@ logging.warning("Value: %r", repr(42)) logging.error("Error: %r", repr([1, 2, 3])) logging.info("Debug info: %s", repr("test\nstring")) logging.warning("Value: %s", repr(42)) + +# %s + ascii() +logging.info("ASCII: %s", ascii("Hello\nWorld")) +logging.warning("ASCII: %s", ascii("test")) + +# %s + oct() +logging.info("Octal: %s", oct(42)) +logging.warning("Octal: %s", oct(255)) + +# %s + hex() +logging.info("Hex: %s", hex(42)) +logging.warning("Hex: %s", hex(255)) + + +# Test with imported functions +from logging import info, log + +info("ASCII: %s", ascii("Hello\nWorld")) +log(logging.INFO, "ASCII: %s", ascii("test")) + +info("Octal: %s", oct(42)) +log(logging.INFO, "Octal: %s", oct(255)) + +info("Hex: %s", hex(42)) +log(logging.INFO, "Hex: %s", hex(255)) + diff --git a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs index a1bfc622cc..2b09c8a1e0 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs @@ -63,19 +63,44 @@ use crate::rules::flake8_logging_format::rules::{LoggingCallType, find_logging_c #[violation_metadata(preview_since = "0.13.2")] pub(crate) struct LoggingEagerConversion { pub(crate) format_conversion: FormatConversion, + pub(crate) function_name: Option<&'static str>, } impl Violation for LoggingEagerConversion { #[derive_message_formats] fn message(&self) -> String { - let LoggingEagerConversion { format_conversion } = self; - let (format_str, call_arg) = match format_conversion { - FormatConversion::Str => ("%s", "str()"), - FormatConversion::Repr => ("%r", "repr()"), - FormatConversion::Ascii => ("%a", "ascii()"), - FormatConversion::Bytes => ("%b", "bytes()"), - }; - format!("Unnecessary `{call_arg}` conversion when formatting with `{format_str}`") + let LoggingEagerConversion { + format_conversion, + function_name, + } = self; + match (format_conversion, function_name.as_deref()) { + (FormatConversion::Str, Some("oct")) => { + "Unnecessary `oct()` conversion when formatting with `%s`. \ + Use `%#o` instead of `%s`" + .to_string() + } + (FormatConversion::Str, Some("hex")) => { + "Unnecessary `hex()` conversion when formatting with `%s`. \ + Use `%#x` instead of `%s`" + .to_string() + } + (FormatConversion::Str, _) => { + "Unnecessary `str()` conversion when formatting with `%s`".to_string() + } + (FormatConversion::Repr, _) => { + "Unnecessary `repr()` conversion when formatting with `%s`. \ + Use `%r` instead of `%s`" + .to_string() + } + (FormatConversion::Ascii, _) => { + "Unnecessary `ascii()` conversion when formatting with `%s`. \ + Use `%a` instead of `%s`" + .to_string() + } + (FormatConversion::Bytes, _) => { + "Unnecessary `bytes()` conversion when formatting with `%b`".to_string() + } + } } } @@ -118,12 +143,71 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall) continue; }; - // Check for use of %s with str() - if checker.semantic().match_builtin_expr(func.as_ref(), "str") - && matches!(format_conversion, FormatConversion::Str) - { - checker - .report_diagnostic(LoggingEagerConversion { format_conversion }, arg.range()); + // Check for various eager conversion patterns + match format_conversion { + // %s with str() - remove str() call + FormatConversion::Str + if checker.semantic().match_builtin_expr(func.as_ref(), "str") => + { + checker.report_diagnostic( + LoggingEagerConversion { + format_conversion, + function_name: None, + }, + arg.range(), + ); + } + // %s with repr() - suggest using %r instead + FormatConversion::Str + if checker.semantic().match_builtin_expr(func.as_ref(), "repr") => + { + checker.report_diagnostic( + LoggingEagerConversion { + format_conversion: FormatConversion::Repr, + function_name: None, + }, + arg.range(), + ); + } + // %s with ascii() - suggest using %a instead + FormatConversion::Str + if checker + .semantic() + .match_builtin_expr(func.as_ref(), "ascii") => + { + checker.report_diagnostic( + LoggingEagerConversion { + format_conversion: FormatConversion::Ascii, + function_name: None, + }, + arg.range(), + ); + } + // %s with oct() - suggest using %#o instead + FormatConversion::Str + if checker.semantic().match_builtin_expr(func.as_ref(), "oct") => + { + checker.report_diagnostic( + LoggingEagerConversion { + format_conversion: FormatConversion::Str, + function_name: Some("oct"), + }, + arg.range(), + ); + } + // %s with hex() - suggest using %#x instead + FormatConversion::Str + if checker.semantic().match_builtin_expr(func.as_ref(), "hex") => + { + checker.report_diagnostic( + LoggingEagerConversion { + format_conversion: FormatConversion::Str, + function_name: Some("hex"), + }, + arg.range(), + ); + } + _ => {} } } } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065.py.snap index 9589c4555d..9f96c36307 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065.py.snap @@ -21,6 +21,26 @@ RUF065 Unnecessary `str()` conversion when formatting with `%s` 7 | # %s + repr() | +RUF065 Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s` + --> RUF065.py:8:26 + | +7 | # %s + repr() +8 | logging.info("Hello %s", repr("World!")) + | ^^^^^^^^^^^^^^ +9 | logging.log(logging.INFO, "Hello %s", repr("World!")) + | + +RUF065 Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s` + --> RUF065.py:9:39 + | + 7 | # %s + repr() + 8 | logging.info("Hello %s", repr("World!")) + 9 | logging.log(logging.INFO, "Hello %s", repr("World!")) + | ^^^^^^^^^^^^^^ +10 | +11 | # %r + str() + | + RUF065 Unnecessary `str()` conversion when formatting with `%s` --> RUF065.py:22:18 | @@ -40,3 +60,160 @@ RUF065 Unnecessary `str()` conversion when formatting with `%s` 24 | 25 | # %s + repr() | + +RUF065 Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s` + --> RUF065.py:26:18 + | +25 | # %s + repr() +26 | info("Hello %s", repr("World!")) + | ^^^^^^^^^^^^^^ +27 | log(logging.INFO, "Hello %s", repr("World!")) + | + +RUF065 Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s` + --> RUF065.py:27:31 + | +25 | # %s + repr() +26 | info("Hello %s", repr("World!")) +27 | log(logging.INFO, "Hello %s", repr("World!")) + | ^^^^^^^^^^^^^^ +28 | +29 | # %r + str() + | + +RUF065 Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s` + --> RUF065.py:44:32 + | +42 | logging.warning("Value: %r", repr(42)) +43 | logging.error("Error: %r", repr([1, 2, 3])) +44 | logging.info("Debug info: %s", repr("test\nstring")) + | ^^^^^^^^^^^^^^^^^^^^ +45 | logging.warning("Value: %s", repr(42)) + | + +RUF065 Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s` + --> RUF065.py:45:30 + | +43 | logging.error("Error: %r", repr([1, 2, 3])) +44 | logging.info("Debug info: %s", repr("test\nstring")) +45 | logging.warning("Value: %s", repr(42)) + | ^^^^^^^^ +46 | +47 | # %s + ascii() + | + +RUF065 Unnecessary `ascii()` conversion when formatting with `%s`. Use `%a` instead of `%s` + --> RUF065.py:48:27 + | +47 | # %s + ascii() +48 | logging.info("ASCII: %s", ascii("Hello\nWorld")) + | ^^^^^^^^^^^^^^^^^^^^^ +49 | logging.warning("ASCII: %s", ascii("test")) + | + +RUF065 Unnecessary `ascii()` conversion when formatting with `%s`. Use `%a` instead of `%s` + --> RUF065.py:49:30 + | +47 | # %s + ascii() +48 | logging.info("ASCII: %s", ascii("Hello\nWorld")) +49 | logging.warning("ASCII: %s", ascii("test")) + | ^^^^^^^^^^^^^ +50 | +51 | # %s + oct() + | + +RUF065 Unnecessary `oct()` conversion when formatting with `%s`. Use `%#o` instead of `%s` + --> RUF065.py:52:27 + | +51 | # %s + oct() +52 | logging.info("Octal: %s", oct(42)) + | ^^^^^^^ +53 | logging.warning("Octal: %s", oct(255)) + | + +RUF065 Unnecessary `oct()` conversion when formatting with `%s`. Use `%#o` instead of `%s` + --> RUF065.py:53:30 + | +51 | # %s + oct() +52 | logging.info("Octal: %s", oct(42)) +53 | logging.warning("Octal: %s", oct(255)) + | ^^^^^^^^ +54 | +55 | # %s + hex() + | + +RUF065 Unnecessary `hex()` conversion when formatting with `%s`. Use `%#x` instead of `%s` + --> RUF065.py:56:25 + | +55 | # %s + hex() +56 | logging.info("Hex: %s", hex(42)) + | ^^^^^^^ +57 | logging.warning("Hex: %s", hex(255)) + | + +RUF065 Unnecessary `hex()` conversion when formatting with `%s`. Use `%#x` instead of `%s` + --> RUF065.py:57:28 + | +55 | # %s + hex() +56 | logging.info("Hex: %s", hex(42)) +57 | logging.warning("Hex: %s", hex(255)) + | ^^^^^^^^ + | + +RUF065 Unnecessary `ascii()` conversion when formatting with `%s`. Use `%a` instead of `%s` + --> RUF065.py:63:19 + | +61 | from logging import info, log +62 | +63 | info("ASCII: %s", ascii("Hello\nWorld")) + | ^^^^^^^^^^^^^^^^^^^^^ +64 | log(logging.INFO, "ASCII: %s", ascii("test")) + | + +RUF065 Unnecessary `ascii()` conversion when formatting with `%s`. Use `%a` instead of `%s` + --> RUF065.py:64:32 + | +63 | info("ASCII: %s", ascii("Hello\nWorld")) +64 | log(logging.INFO, "ASCII: %s", ascii("test")) + | ^^^^^^^^^^^^^ +65 | +66 | info("Octal: %s", oct(42)) + | + +RUF065 Unnecessary `oct()` conversion when formatting with `%s`. Use `%#o` instead of `%s` + --> RUF065.py:66:19 + | +64 | log(logging.INFO, "ASCII: %s", ascii("test")) +65 | +66 | info("Octal: %s", oct(42)) + | ^^^^^^^ +67 | log(logging.INFO, "Octal: %s", oct(255)) + | + +RUF065 Unnecessary `oct()` conversion when formatting with `%s`. Use `%#o` instead of `%s` + --> RUF065.py:67:32 + | +66 | info("Octal: %s", oct(42)) +67 | log(logging.INFO, "Octal: %s", oct(255)) + | ^^^^^^^^ +68 | +69 | info("Hex: %s", hex(42)) + | + +RUF065 Unnecessary `hex()` conversion when formatting with `%s`. Use `%#x` instead of `%s` + --> RUF065.py:69:17 + | +67 | log(logging.INFO, "Octal: %s", oct(255)) +68 | +69 | info("Hex: %s", hex(42)) + | ^^^^^^^ +70 | log(logging.INFO, "Hex: %s", hex(255)) + | + +RUF065 Unnecessary `hex()` conversion when formatting with `%s`. Use `%#x` instead of `%s` + --> RUF065.py:70:30 + | +69 | info("Hex: %s", hex(42)) +70 | log(logging.INFO, "Hex: %s", hex(255)) + | ^^^^^^^^ + | From 1b0ee4677e216562033f8a2f9b006738734cb2b9 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 30 Oct 2025 15:21:55 +0100 Subject: [PATCH 078/188] [ty] Use `range` instead of custom `IntIterable` (#21138) ## Summary We previously didn't understand `range` and wrote these custom `IntIterable`/`IntIterator` classes for tests. We can now remove them and make the tests shorter in some places. --- .../resources/mdtest/attributes.md | 36 ++----- .../resources/mdtest/comprehensions/basic.md | 102 +++++------------- .../mdtest/comprehensions/invalid_syntax.md | 12 +-- 3 files changed, 35 insertions(+), 115 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 22d7327500..10b6d42318 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -300,14 +300,6 @@ reveal_type(c_instance.b) # revealed: Unknown | list[Literal[2, 3]] #### Attributes defined in for-loop (unpacking) ```py -class IntIterator: - def __next__(self) -> int: - return 1 - -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - class TupleIterator: def __next__(self) -> tuple[int, str]: return (1, "a") @@ -320,7 +312,7 @@ class NonIterable: ... class C: def __init__(self): - for self.x in IntIterable(): + for self.x in range(3): pass for _, self.y in TupleIterable(): @@ -378,14 +370,6 @@ reveal_type(c_instance.y) # revealed: Unknown | int #### Attributes defined in comprehensions ```py -class IntIterator: - def __next__(self) -> int: - return 1 - -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - class TupleIterator: def __next__(self) -> tuple[int, str]: return (1, "a") @@ -398,7 +382,7 @@ class C: def __init__(self) -> None: # TODO: Should not emit this diagnostic # error: [unresolved-attribute] - [... for self.a in IntIterable()] + [... for self.a in range(3)] # TODO: Should not emit this diagnostic # error: [unresolved-attribute] # error: [unresolved-attribute] @@ -406,11 +390,11 @@ class C: # TODO: Should not emit this diagnostic # error: [unresolved-attribute] # error: [unresolved-attribute] - [... for self.d in IntIterable() for self.e in IntIterable()] + [... for self.d in range(3) for self.e in range(3)] # TODO: Should not emit this diagnostic # error: [unresolved-attribute] - [[... for self.f in IntIterable()] for _ in IntIterable()] - [[... for self.g in IntIterable()] for self in [D()]] + [[... for self.f in range(3)] for _ in range(3)] + [[... for self.g in range(3)] for self in [D()]] class D: g: int @@ -2058,16 +2042,8 @@ mod.global_symbol = 1 # TODO: this should be an error, but we do not understand list unpackings yet. [_, mod.global_symbol] = [1, 2] -class IntIterator: - def __next__(self) -> int: - return 42 - -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - # error: [invalid-assignment] "Object of type `int` is not assignable to attribute `global_symbol` of type `str`" -for mod.global_symbol in IntIterable(): +for mod.global_symbol in range(3): pass ``` diff --git a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md index ea7ed41cbf..bdd9ec435c 100644 --- a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md @@ -3,71 +3,47 @@ ## Basic comprehensions ```py -class IntIterator: - def __next__(self) -> int: - return 42 +# revealed: int +[reveal_type(x) for x in range(3)] -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() +class Row: + def __next__(self) -> range: + return range(3) + +class Table: + def __iter__(self) -> Row: + return Row() + +# revealed: tuple[int, range] +[reveal_type((cell, row)) for row in Table() for cell in row] # revealed: int -[reveal_type(x) for x in IntIterable()] - -class IteratorOfIterables: - def __next__(self) -> IntIterable: - return IntIterable() - -class IterableOfIterables: - def __iter__(self) -> IteratorOfIterables: - return IteratorOfIterables() - -# revealed: tuple[int, IntIterable] -[reveal_type((x, y)) for y in IterableOfIterables() for x in y] +{reveal_type(x): 0 for x in range(3)} # revealed: int -{reveal_type(x): 0 for x in IntIterable()} - -# revealed: int -{0: reveal_type(x) for x in IntIterable()} +{0: reveal_type(x) for x in range(3)} ``` ## Nested comprehension ```py -class IntIterator: - def __next__(self) -> int: - return 42 - -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - # revealed: tuple[int, int] -[[reveal_type((x, y)) for x in IntIterable()] for y in IntIterable()] +[[reveal_type((x, y)) for x in range(3)] for y in range(3)] ``` ## Comprehension referencing outer comprehension ```py -class IntIterator: - def __next__(self) -> int: - return 42 +class Row: + def __next__(self) -> range: + return range(3) -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() +class Table: + def __iter__(self) -> Row: + return Row() -class IteratorOfIterables: - def __next__(self) -> IntIterable: - return IntIterable() - -class IterableOfIterables: - def __iter__(self) -> IteratorOfIterables: - return IteratorOfIterables() - -# revealed: tuple[int, IntIterable] -[[reveal_type((x, y)) for x in y] for y in IterableOfIterables()] +# revealed: tuple[int, range] +[[reveal_type((cell, row)) for cell in row] for row in Table()] ``` ## Comprehension with unbound iterable @@ -79,17 +55,9 @@ Iterating over an unbound iterable yields `Unknown`: # revealed: Unknown [reveal_type(z) for z in x] -class IntIterator: - def __next__(self) -> int: - return 42 - -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - # error: [not-iterable] "Object of type `int` is not iterable" # revealed: tuple[int, Unknown] -[reveal_type((x, z)) for x in IntIterable() for z in x] +[reveal_type((x, z)) for x in range(3) for z in x] ``` ## Starred expressions @@ -99,16 +67,8 @@ Starred expressions must be iterable ```py class NotIterable: ... -class Iterator: - def __next__(self) -> int: - return 42 - -class Iterable: - def __iter__(self) -> Iterator: - return Iterator() - # This is fine: -x = [*Iterable()] +x = [*range(3)] # error: [not-iterable] "Object of type `NotIterable` is not iterable" y = [*NotIterable()] @@ -138,16 +98,8 @@ This tests that we understand that `async` comprehensions do *not* work accordin iteration protocol ```py -class Iterator: - def __next__(self) -> int: - return 42 - -class Iterable: - def __iter__(self) -> Iterator: - return Iterator() - async def _(): - # error: [not-iterable] "Object of type `Iterable` is not async-iterable" + # error: [not-iterable] "Object of type `range` is not async-iterable" # revealed: Unknown - [reveal_type(x) async for x in Iterable()] + [reveal_type(x) async for x in range(3)] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md index 20c71be171..f16ec0505d 100644 --- a/crates/ty_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md +++ b/crates/ty_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md @@ -1,14 +1,6 @@ # Comprehensions with invalid syntax ```py -class IntIterator: - def __next__(self) -> int: - return 42 - -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - # Missing 'in' keyword. # It's reasonably clear here what they *meant* to write, @@ -16,7 +8,7 @@ class IntIterable: # error: [invalid-syntax] "Expected 'in', found name" # revealed: int -[reveal_type(a) for a IntIterable()] +[reveal_type(a) for a range(3)] # Missing iteration variable @@ -25,7 +17,7 @@ class IntIterable: # error: [invalid-syntax] "Expected 'in', found name" # error: [unresolved-reference] # revealed: Unknown -[reveal_type(b) for in IntIterable()] +[reveal_type(b) for in range(3)] # Missing iterable From e55bc943e5f8708aeaced34488ca5d790c388cee Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 30 Oct 2025 15:38:57 +0100 Subject: [PATCH 079/188] [ty] Reachability and narrowing for enum methods (#21130) ## Summary Adds proper type narrowing and reachability analysis for matching on non-inferable type variables bound to enums. For example: ```py from enum import Enum class Answer(Enum): NO = 0 YES = 1 def is_yes(self) -> bool: # no error here! match self: case Answer.YES: return True case Answer.NO: return False ``` closes https://github.com/astral-sh/ty/issues/1404 ## Test Plan Added regression tests --- .../mdtest/exhaustiveness_checking.md | 19 ++++++++ .../resources/mdtest/narrow/match.md | 48 +++++++++++++++++++ .../reachability_constraints.rs | 21 +++++++- .../ty_python_semantic/src/types/builder.rs | 38 +++++++++++++-- 4 files changed, 121 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md index 800ae7c7bb..7218359750 100644 --- a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -379,3 +379,22 @@ def as_pattern_non_exhaustive(subject: int | str): # this diagnostic is correct: the inferred type of `subject` is `str` assert_never(subject) # error: [type-assertion-failure] ``` + +## Exhaustiveness checking for methods of enums + +```py +from enum import Enum + +class Answer(Enum): + YES = "yes" + NO = "no" + + def is_yes(self) -> bool: + reveal_type(self) # revealed: Self@is_yes + + match self: + case Answer.YES: + return True + case Answer.NO: + return False +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index 55772eab24..ee51d50af2 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -252,3 +252,51 @@ match x: reveal_type(x) # revealed: object ``` + +## Narrowing on `Self` in `match` statements + +When performing narrowing on `self` inside methods on enums, we take into account that `Self` might +refer to a subtype of the enum class, like `Literal[Answer.YES]`. This is why we do not simplify +`Self & ~Literal[Answer.YES]` to `Literal[Answer.NO, Answer.MAYBE]`. Otherwise, we wouldn't be able +to return `self` in the `assert_yes` method below: + +```py +from enum import Enum +from typing_extensions import Self, assert_never + +class Answer(Enum): + NO = 0 + YES = 1 + MAYBE = 2 + + def is_yes(self) -> bool: + reveal_type(self) # revealed: Self@is_yes + + match self: + case Answer.YES: + reveal_type(self) # revealed: Self@is_yes + return True + case Answer.NO | Answer.MAYBE: + reveal_type(self) # revealed: Self@is_yes & ~Literal[Answer.YES] + return False + case _: + assert_never(self) # no error + + def assert_yes(self) -> Self: + reveal_type(self) # revealed: Self@assert_yes + + match self: + case Answer.YES: + reveal_type(self) # revealed: Self@assert_yes + return self + case _: + reveal_type(self) # revealed: Self@assert_yes & ~Literal[Answer.YES] + raise ValueError("Answer is not YES") + +Answer.YES.is_yes() + +try: + reveal_type(Answer.MAYBE.assert_yes()) # revealed: Literal[Answer.MAYBE] +except ValueError: + pass +``` 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 3d09733324..af3ef642e3 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -802,10 +802,27 @@ impl ReachabilityConstraints { fn analyze_single_pattern_predicate(db: &dyn Db, predicate: PatternPredicate) -> Truthiness { let subject_ty = infer_expression_type(db, predicate.subject(db), TypeContext::default()); - let narrowed_subject_ty = IntersectionBuilder::new(db) + let narrowed_subject = IntersectionBuilder::new(db) .add_positive(subject_ty) - .add_negative(type_excluded_by_previous_patterns(db, predicate)) + .add_negative(type_excluded_by_previous_patterns(db, predicate)); + + let narrowed_subject_ty = narrowed_subject.clone().build(); + + // Consider a case where we match on a subject type of `Self` with an upper bound of `Answer`, + // where `Answer` is a {YES, NO} enum. After a previous pattern matching on `NO`, the narrowed + // subject type is `Self & ~Literal[NO]`. This type is *not* equivalent to `Literal[YES]`, + // because `Self` could also specialize to `Literal[NO]` or `Never`, making the intersection + // empty. However, if the current pattern matches on `YES`, the *next* narrowed subject type + // will be `Self & ~Literal[NO] & ~Literal[YES]`, which *is* always equivalent to `Never`. This + // means that subsequent patterns can never match. And we know that if we reach this point, + // the current pattern will have to match. We return `AlwaysTrue` here, since the call to + // `analyze_single_pattern_predicate_kind` below would return `Ambiguous` in this case. + let next_narrowed_subject_ty = narrowed_subject + .add_negative(pattern_kind_to_type(db, predicate.kind(db))) .build(); + if !narrowed_subject_ty.is_never() && next_narrowed_subject_ty.is_never() { + return Truthiness::AlwaysTrue; + } let truthiness = Self::analyze_single_pattern_predicate_kind( db, diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index 7f46a80239..6b555b6fdb 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -44,6 +44,7 @@ use crate::types::{ TypeVarBoundOrConstraints, UnionType, }; use crate::{Db, FxOrderSet}; +use rustc_hash::FxHashSet; use smallvec::SmallVec; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -422,9 +423,9 @@ impl<'db> UnionBuilder<'db> { .iter() .filter_map(UnionElement::to_type_element) .filter_map(Type::as_enum_literal) - .map(|literal| literal.name(self.db).clone()) - .chain(std::iter::once(enum_member_to_add.name(self.db).clone())) - .collect::>(); + .map(|literal| literal.name(self.db)) + .chain(std::iter::once(enum_member_to_add.name(self.db))) + .collect::>(); let all_members_are_in_union = metadata .members @@ -780,6 +781,37 @@ impl<'db> IntersectionBuilder<'db> { seen_aliases, ) } + Type::EnumLiteral(enum_literal) => { + let enum_class = enum_literal.enum_class(self.db); + let metadata = + enum_metadata(self.db, enum_class).expect("Class of enum literal is an enum"); + + let enum_members_in_negative_part = self + .intersections + .iter() + .flat_map(|intersection| &intersection.negative) + .filter_map(|ty| ty.as_enum_literal()) + .filter(|lit| lit.enum_class(self.db) == enum_class) + .map(|lit| lit.name(self.db)) + .chain(std::iter::once(enum_literal.name(self.db))) + .collect::>(); + + let all_members_are_in_negative_part = metadata + .members + .keys() + .all(|name| enum_members_in_negative_part.contains(name)); + + if all_members_are_in_negative_part { + for inner in &mut self.intersections { + inner.add_negative(self.db, enum_literal.enum_class_instance(self.db)); + } + } else { + for inner in &mut self.intersections { + inner.add_negative(self.db, ty); + } + } + self + } _ => { for inner in &mut self.intersections { inner.add_negative(self.db, ty); From 10bda3df00679c6808d8593470fe693ea8c9ae8f Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Thu, 30 Oct 2025 22:29:07 +0530 Subject: [PATCH 080/188] [`pyupgrade`] Fix false positive for `TypeVar` with default on Python <3.13 (`UP046`,`UP047`) (#21045) ## Summary Type default for Type parameter was added in Python 3.13 (PEP 696). `typing_extensions.TypeVar` backports the default argument to earlier versions. `UP046` & `UP047` were getting triggered when `typing_extensions.TypeVar` with `default` argument was used on python version < 3.13 It shouldn't be triggered for python version < 3.13 This commit fixes the bug by adding a python version check before triggering them. Fixes #20929. ## Test Plan ### Manual testing 1 As the issue author pointed out in https://github.com/astral-sh/ruff/issues/20929#issuecomment-3413194511, ran the following on `main` branch: > % cargo run -p ruff -- check ../efax/ --target-version py312 --no-cache
    Output ```zsh Compiling ruff_linter v0.14.1 (/Users/prakhar/ruff/crates/ruff_linter) Compiling ruff v0.14.1 (/Users/prakhar/ruff/crates/ruff) Compiling ruff_graph v0.1.0 (/Users/prakhar/ruff/crates/ruff_graph) Compiling ruff_workspace v0.0.0 (/Users/prakhar/ruff/crates/ruff_workspace) Compiling ruff_server v0.2.2 (/Users/prakhar/ruff/crates/ruff_server) Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.72s Running `target/debug/ruff check ../efax/ --target-version py312 --no-cache` UP046 Generic class `ExpectationParametrization` uses `Generic` subclass instead of type parameters --> /Users/prakhar/efax/efax/_src/expectation_parametrization.py:17:48 | 17 | class ExpectationParametrization(Distribution, Generic[NP]): | ^^^^^^^^^^^ 18 | """The expectation parametrization of an exponential family distribution. | help: Use type parameters UP046 Generic class `ExpToNat` uses `Generic` subclass instead of type parameters --> /Users/prakhar/efax/efax/_src/mixins/exp_to_nat/exp_to_nat.py:27:68 | 26 | @dataclass 27 | class ExpToNat(ExpectationParametrization[NP], SimpleDistribution, Generic[NP]): | ^^^^^^^^^^^ 28 | """This mixin implements the conversion from expectation to natural parameters. | help: Use type parameters UP046 Generic class `HasEntropyEP` uses `Generic` subclass instead of type parameters --> /Users/prakhar/efax/efax/_src/mixins/has_entropy.py:25:20 | 23 | HasEntropy, 24 | JaxAbstractClass, 25 | Generic[NP]): | ^^^^^^^^^^^ 26 | @abstract_jit 27 | @abstractmethod | help: Use type parameters UP046 Generic class `HasEntropyNP` uses `Generic` subclass instead of type parameters --> /Users/prakhar/efax/efax/_src/mixins/has_entropy.py:64:20 | 62 | class HasEntropyNP(NaturalParametrization[EP], 63 | HasEntropy, 64 | Generic[EP]): | ^^^^^^^^^^^ 65 | @jit 66 | @final | help: Use type parameters UP046 Generic class `NaturalParametrization` uses `Generic` subclass instead of type parameters --> /Users/prakhar/efax/efax/_src/natural_parametrization.py:43:30 | 41 | class NaturalParametrization(Distribution, 42 | JaxAbstractClass, 43 | Generic[EP, Domain]): | ^^^^^^^^^^^^^^^^^^^ 44 | """The natural parametrization of an exponential family distribution. | help: Use type parameters UP046 Generic class `Structure` uses `Generic` subclass instead of type parameters --> /Users/prakhar/efax/efax/_src/structure/structure.py:31:17 | 30 | @dataclass 31 | class Structure(Generic[P]): | ^^^^^^^^^^ 32 | """This class generalizes the notion of type for Distribution objects. | help: Use type parameters UP046 Generic class `DistributionInfo` uses `Generic` subclass instead of type parameters --> /Users/prakhar/efax/tests/distribution_info.py:20:24 | 20 | class DistributionInfo(Generic[NP, EP, Domain]): | ^^^^^^^^^^^^^^^^^^^^^^^ 21 | def __init__(self, dimensions: int = 1, safety: float = 0.0) -> None: 22 | super().__init__() | help: Use type parameters Found 7 errors. No fixes available (7 hidden fixes can be enabled with the `--unsafe-fixes` option). ```
    Running it after the changes: ```zsh ruff % cargo run -p ruff -- check ../efax/ --target-version py312 --no-cache Compiling ruff_linter v0.14.1 (/Users/prakhar/ruff/crates/ruff_linter) Compiling ruff v0.14.1 (/Users/prakhar/ruff/crates/ruff) Compiling ruff_graph v0.1.0 (/Users/prakhar/ruff/crates/ruff_graph) Compiling ruff_workspace v0.0.0 (/Users/prakhar/ruff/crates/ruff_workspace) Compiling ruff_server v0.2.2 (/Users/prakhar/ruff/crates/ruff_server) Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.86s Running `target/debug/ruff check ../efax/ --target-version py312 --no-cache` All checks passed! ``` --- ### Manual testing 2 Ran the check on the following script (mainly to verify `UP047`): ```py from __future__ import annotations from typing import Generic from typing_extensions import TypeVar T = TypeVar("T", default=int) def generic_function(var: T) -> T: return var Q = TypeVar("Q", default=str) class GenericClass(Generic[Q]): var: Q ``` On `main` branch: > ruff % cargo run -p ruff -- check ~/up046.py --target-version py312 --preview --no-cache
    Output ```zsh Compiling ruff_linter v0.14.1 (/Users/prakhar/ruff/crates/ruff_linter) Compiling ruff v0.14.1 (/Users/prakhar/ruff/crates/ruff) Compiling ruff_graph v0.1.0 (/Users/prakhar/ruff/crates/ruff_graph) Compiling ruff_workspace v0.0.0 (/Users/prakhar/ruff/crates/ruff_workspace) Compiling ruff_server v0.2.2 (/Users/prakhar/ruff/crates/ruff_server) Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.43s Running `target/debug/ruff check /Users/prakhar/up046.py --target-version py312 --preview --no-cache` UP047 Generic function `generic_function` should use type parameters --> /Users/prakhar/up046.py:10:5 | 10 | def generic_function(var: T) -> T: | ^^^^^^^^^^^^^^^^^^^^^^^^ 11 | return var | help: Use type parameters UP046 Generic class `GenericClass` uses `Generic` subclass instead of type parameters --> /Users/prakhar/up046.py:17:20 | 17 | class GenericClass(Generic[Q]): | ^^^^^^^^^^ 18 | var: Q | help: Use type parameters Found 2 errors. No fixes available (2 hidden fixes can be enabled with the `--unsafe-fixes` option). ```
    After the fix (this branch): ```zsh ruff % cargo run -p ruff -- check ~/up046.py --target-version py312 --preview --no-cache Compiling ruff_linter v0.14.1 (/Users/prakhar/ruff/crates/ruff_linter) Compiling ruff v0.14.1 (/Users/prakhar/ruff/crates/ruff) Compiling ruff_graph v0.1.0 (/Users/prakhar/ruff/crates/ruff_graph) Compiling ruff_workspace v0.0.0 (/Users/prakhar/ruff/crates/ruff_workspace) Compiling ruff_server v0.2.2 (/Users/prakhar/ruff/crates/ruff_server) Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.40s Running `target/debug/ruff check /Users/prakhar/up046.py --target-version py312 --preview --no-cache` All checks passed! ``` Signed-off-by: Prakhar Pratyush --- .../test/fixtures/pyupgrade/UP046_2.py | 13 ++++++++++++ .../pyupgrade/{UP047.py => UP047_0.py} | 0 .../test/fixtures/pyupgrade/UP047_1.py | 12 +++++++++++ crates/ruff_linter/src/rules/pyupgrade/mod.rs | 20 +++++++++++++++++-- .../src/rules/pyupgrade/rules/pep695/mod.rs | 14 ++++++++----- ...__rules__pyupgrade__tests__UP046_2.py.snap | 4 ++++ ..._rules__pyupgrade__tests__UP047_0.py.snap} | 12 +++++------ ...ade__tests__UP047_0.py__preview_diff.snap} | 2 +- ...__rules__pyupgrade__tests__UP047_1.py.snap | 4 ++++ 9 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_2.py rename crates/ruff_linter/resources/test/fixtures/pyupgrade/{UP047.py => UP047_0.py} (100%) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_1.py create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_2.py.snap rename crates/ruff_linter/src/rules/pyupgrade/snapshots/{ruff_linter__rules__pyupgrade__tests__UP047.py.snap => ruff_linter__rules__pyupgrade__tests__UP047_0.py.snap} (95%) rename crates/ruff_linter/src/rules/pyupgrade/snapshots/{ruff_linter__rules__pyupgrade__tests__UP047.py__preview_diff.snap => ruff_linter__rules__pyupgrade__tests__UP047_0.py__preview_diff.snap} (96%) create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_1.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_2.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_2.py new file mode 100644 index 0000000000..aab7dce4d3 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_2.py @@ -0,0 +1,13 @@ +"""This is placed in a separate fixture as `TypeVar` needs to be imported +from `typing_extensions` to support default arguments in Python version < 3.13. +We verify that UP046 doesn't apply in this case. +""" + +from typing import Generic +from typing_extensions import TypeVar + +T = TypeVar("T", default=str) + + +class DefaultTypeVar(Generic[T]): + var: T diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_0.py similarity index 100% rename from crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py rename to crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_0.py diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_1.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_1.py new file mode 100644 index 0000000000..186c8305ef --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_1.py @@ -0,0 +1,12 @@ +"""This is placed in a separate fixture as `TypeVar` needs to be imported +from `typing_extensions` to support default arguments in Python version < 3.13. +We verify that UP047 doesn't apply in this case. +""" + +from typing_extensions import TypeVar + +T = TypeVar("T", default=int) + + +def default_var(var: T) -> T: + return var diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 044c90b3a2..c8919fe1b0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -111,7 +111,7 @@ mod tests { #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))] #[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_0.py"))] #[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_1.py"))] - #[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))] + #[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047_0.py"))] #[test_case(Rule::PrivateTypeParameter, Path::new("UP049_0.py"))] #[test_case(Rule::PrivateTypeParameter, Path::new("UP049_1.py"))] #[test_case(Rule::UselessClassMetaclassType, Path::new("UP050.py"))] @@ -125,6 +125,22 @@ mod tests { Ok(()) } + #[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_2.py"))] + #[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047_1.py"))] + fn rules_not_applied_default_typevar_backported(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = path.to_string_lossy().to_string(); + let diagnostics = test_path( + Path::new("pyupgrade").join(path).as_path(), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + unresolved_target_version: PythonVersion::PY312.into(), + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))] #[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))] fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> { @@ -144,7 +160,7 @@ mod tests { #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))] #[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_0.py"))] #[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_1.py"))] - #[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))] + #[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047_0.py"))] fn type_var_default_preview(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}__preview_diff", path.to_string_lossy()); assert_diagnostics_diff!( diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index c633c5b4ee..10f95ca180 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -6,8 +6,8 @@ use std::fmt::Display; use itertools::Itertools; use ruff_python_ast::{ - self as ast, Arguments, Expr, ExprCall, ExprName, ExprSubscript, Identifier, Stmt, StmtAssign, - TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, + self as ast, Arguments, Expr, ExprCall, ExprName, ExprSubscript, Identifier, PythonVersion, + Stmt, StmtAssign, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, name::Name, visitor::{self, Visitor}, }; @@ -369,15 +369,19 @@ fn in_nested_context(checker: &Checker) -> bool { } /// Deduplicate `vars`, returning `None` if `vars` is empty or any duplicates are found. -/// Also returns `None` if any `TypeVar` has a default value and preview mode is not enabled. +/// Also returns `None` if any `TypeVar` has a default value and the target Python version +/// is below 3.13 or preview mode is not enabled. Note that `typing_extensions` backports +/// the default argument, but the rule should be skipped in that case. fn check_type_vars<'a>(vars: Vec>, checker: &Checker) -> Option>> { if vars.is_empty() { return None; } - // If any type variables have defaults and preview mode is not enabled, skip the rule + // If any type variables have defaults, skip the rule unless + // running with preview mode enabled and targeting Python 3.13+. if vars.iter().any(|tv| tv.default.is_some()) - && !is_type_var_default_enabled(checker.settings()) + && (checker.target_version() < PythonVersion::PY313 + || !is_type_var_default_enabled(checker.settings())) { return None; } diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_2.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_2.py.snap new file mode 100644 index 0000000000..2bacb5d540 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_2.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_0.py.snap similarity index 95% rename from crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap rename to crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_0.py.snap index 9cbe736a7a..07a3869ecd 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_0.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs --- UP047 [*] Generic function `f` should use type parameters - --> UP047.py:12:5 + --> UP047_0.py:12:5 | 12 | def f(t: T) -> T: | ^^^^^^^ @@ -20,7 +20,7 @@ help: Use type parameters note: This is an unsafe fix and may change runtime behavior UP047 [*] Generic function `g` should use type parameters - --> UP047.py:16:5 + --> UP047_0.py:16:5 | 16 | def g(ts: tuple[*Ts]) -> tuple[*Ts]: | ^^^^^^^^^^^^^^^^^ @@ -38,7 +38,7 @@ help: Use type parameters note: This is an unsafe fix and may change runtime behavior UP047 [*] Generic function `h` should use type parameters - --> UP047.py:20:5 + --> UP047_0.py:20:5 | 20 | def h( | _____^ @@ -62,7 +62,7 @@ help: Use type parameters note: This is an unsafe fix and may change runtime behavior UP047 [*] Generic function `i` should use type parameters - --> UP047.py:29:5 + --> UP047_0.py:29:5 | 29 | def i(s: S) -> S: | ^^^^^^^ @@ -80,7 +80,7 @@ help: Use type parameters note: This is an unsafe fix and may change runtime behavior UP047 [*] Generic function `broken_fix` should use type parameters - --> UP047.py:39:5 + --> UP047_0.py:39:5 | 37 | # TypeVars with the new-style generic syntax and will be rejected by type 38 | # checkers @@ -100,7 +100,7 @@ help: Use type parameters note: This is an unsafe fix and may change runtime behavior UP047 [*] Generic function `any_str_param` should use type parameters - --> UP047.py:43:5 + --> UP047_0.py:43:5 | 43 | def any_str_param(s: AnyStr) -> AnyStr: | ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py__preview_diff.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_0.py__preview_diff.snap similarity index 96% rename from crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py__preview_diff.snap rename to crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_0.py__preview_diff.snap index 2a11cd2e59..2d1ff52344 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047.py__preview_diff.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_0.py__preview_diff.snap @@ -11,7 +11,7 @@ Added: 1 --- Added --- UP047 [*] Generic function `default_var` should use type parameters - --> UP047.py:51:5 + --> UP047_0.py:51:5 | 51 | def default_var(v: V) -> V: | ^^^^^^^^^^^^^^^^^ diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_1.py.snap new file mode 100644 index 0000000000..2bacb5d540 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_1.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- + From f0fe6d62fb6711d963c64f60b5dc8a558ffb4551 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:40:03 -0400 Subject: [PATCH 081/188] Fix syntax error false positive on nested alternative patterns (#21104) ## Summary Fixes #21101 by storing the child visitor's names in the parent visitor. This makes sure that `visitor.names` on line 1818 isn't empty after we visit a nested OR pattern. ## Test Plan New inline test cases derived from the issue, [playground](https://play.ruff.rs/7b6439ac-ee8f-4593-9a3e-c2aa34a595d0) --- .../inline/ok/nested_alternative_patterns.py | 7 + .../ruff_python_parser/src/semantic_errors.rs | 10 + ...syntax@nested_alternative_patterns.py.snap | 495 ++++++++++++++++++ 3 files changed, 512 insertions(+) create mode 100644 crates/ruff_python_parser/resources/inline/ok/nested_alternative_patterns.py create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@nested_alternative_patterns.py.snap diff --git a/crates/ruff_python_parser/resources/inline/ok/nested_alternative_patterns.py b/crates/ruff_python_parser/resources/inline/ok/nested_alternative_patterns.py new file mode 100644 index 0000000000..d322fa3899 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/nested_alternative_patterns.py @@ -0,0 +1,7 @@ +match ruff: + case {"lint": {"select": x} | {"extend-select": x}} | {"select": x}: + ... +match 42: + case [[x] | [x]] | x: ... +match 42: + case [[x | x] | [x]] | x: ... diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index aba6e5ed7c..f35029d4b9 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -1841,6 +1841,15 @@ impl<'a, Ctx: SemanticSyntaxContext> MatchPatternVisitor<'a, Ctx> { // case (x, (y | y)): ... // case [a, _] | [a, _]: ... // case [a] | [C(a)]: ... + + // test_ok nested_alternative_patterns + // match ruff: + // case {"lint": {"select": x} | {"extend-select": x}} | {"select": x}: + // ... + // match 42: + // case [[x] | [x]] | x: ... + // match 42: + // case [[x | x] | [x]] | x: ... SemanticSyntaxChecker::add_error( self.ctx, SemanticSyntaxErrorKind::DifferentMatchPatternBindings, @@ -1848,6 +1857,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MatchPatternVisitor<'a, Ctx> { ); break; } + self.names = visitor.names; } } } diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nested_alternative_patterns.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nested_alternative_patterns.py.snap new file mode 100644 index 0000000000..8d241b04ef --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nested_alternative_patterns.py.snap @@ -0,0 +1,495 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/nested_alternative_patterns.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..181, + body: [ + Match( + StmtMatch { + node_index: NodeIndex(None), + range: 0..96, + subject: Name( + ExprName { + node_index: NodeIndex(None), + range: 6..10, + id: Name("ruff"), + ctx: Load, + }, + ), + cases: [ + MatchCase { + range: 16..96, + node_index: NodeIndex(None), + pattern: MatchOr( + PatternMatchOr { + node_index: NodeIndex(None), + range: 21..83, + patterns: [ + MatchMapping( + PatternMatchMapping { + node_index: NodeIndex(None), + range: 21..67, + keys: [ + StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 22..28, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 22..28, + node_index: NodeIndex(None), + value: "lint", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + ], + patterns: [ + MatchOr( + PatternMatchOr { + node_index: NodeIndex(None), + range: 30..66, + patterns: [ + MatchMapping( + PatternMatchMapping { + node_index: NodeIndex(None), + range: 30..43, + keys: [ + StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 31..39, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 31..39, + node_index: NodeIndex(None), + value: "select", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + ], + patterns: [ + MatchAs( + PatternMatchAs { + node_index: NodeIndex(None), + range: 41..42, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 41..42, + node_index: NodeIndex(None), + }, + ), + }, + ), + ], + rest: None, + }, + ), + MatchMapping( + PatternMatchMapping { + node_index: NodeIndex(None), + range: 46..66, + keys: [ + StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 47..62, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 47..62, + node_index: NodeIndex(None), + value: "extend-select", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + ], + patterns: [ + MatchAs( + PatternMatchAs { + node_index: NodeIndex(None), + range: 64..65, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 64..65, + node_index: NodeIndex(None), + }, + ), + }, + ), + ], + rest: None, + }, + ), + ], + }, + ), + ], + rest: None, + }, + ), + MatchMapping( + PatternMatchMapping { + node_index: NodeIndex(None), + range: 70..83, + keys: [ + StringLiteral( + ExprStringLiteral { + node_index: NodeIndex(None), + range: 71..79, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 71..79, + node_index: NodeIndex(None), + value: "select", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + unclosed: false, + }, + }, + ), + }, + }, + ), + ], + patterns: [ + MatchAs( + PatternMatchAs { + node_index: NodeIndex(None), + range: 81..82, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 81..82, + node_index: NodeIndex(None), + }, + ), + }, + ), + ], + rest: None, + }, + ), + ], + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 93..96, + value: EllipsisLiteral( + ExprEllipsisLiteral { + node_index: NodeIndex(None), + range: 93..96, + }, + ), + }, + ), + ], + }, + ], + }, + ), + Match( + StmtMatch { + node_index: NodeIndex(None), + range: 97..136, + subject: NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 103..105, + value: Int( + 42, + ), + }, + ), + cases: [ + MatchCase { + range: 111..136, + node_index: NodeIndex(None), + pattern: MatchOr( + PatternMatchOr { + node_index: NodeIndex(None), + range: 116..131, + patterns: [ + MatchSequence( + PatternMatchSequence { + node_index: NodeIndex(None), + range: 116..127, + patterns: [ + MatchOr( + PatternMatchOr { + node_index: NodeIndex(None), + range: 117..126, + patterns: [ + MatchSequence( + PatternMatchSequence { + node_index: NodeIndex(None), + range: 117..120, + patterns: [ + MatchAs( + PatternMatchAs { + node_index: NodeIndex(None), + range: 118..119, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 118..119, + node_index: NodeIndex(None), + }, + ), + }, + ), + ], + }, + ), + MatchSequence( + PatternMatchSequence { + node_index: NodeIndex(None), + range: 123..126, + patterns: [ + MatchAs( + PatternMatchAs { + node_index: NodeIndex(None), + range: 124..125, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 124..125, + node_index: NodeIndex(None), + }, + ), + }, + ), + ], + }, + ), + ], + }, + ), + ], + }, + ), + MatchAs( + PatternMatchAs { + node_index: NodeIndex(None), + range: 130..131, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 130..131, + node_index: NodeIndex(None), + }, + ), + }, + ), + ], + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 133..136, + value: EllipsisLiteral( + ExprEllipsisLiteral { + node_index: NodeIndex(None), + range: 133..136, + }, + ), + }, + ), + ], + }, + ], + }, + ), + Match( + StmtMatch { + node_index: NodeIndex(None), + range: 137..180, + subject: NumberLiteral( + ExprNumberLiteral { + node_index: NodeIndex(None), + range: 143..145, + value: Int( + 42, + ), + }, + ), + cases: [ + MatchCase { + range: 151..180, + node_index: NodeIndex(None), + pattern: MatchOr( + PatternMatchOr { + node_index: NodeIndex(None), + range: 156..175, + patterns: [ + MatchSequence( + PatternMatchSequence { + node_index: NodeIndex(None), + range: 156..171, + patterns: [ + MatchOr( + PatternMatchOr { + node_index: NodeIndex(None), + range: 157..170, + patterns: [ + MatchSequence( + PatternMatchSequence { + node_index: NodeIndex(None), + range: 157..164, + patterns: [ + MatchOr( + PatternMatchOr { + node_index: NodeIndex(None), + range: 158..163, + patterns: [ + MatchAs( + PatternMatchAs { + node_index: NodeIndex(None), + range: 158..159, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 158..159, + node_index: NodeIndex(None), + }, + ), + }, + ), + MatchAs( + PatternMatchAs { + node_index: NodeIndex(None), + range: 162..163, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 162..163, + node_index: NodeIndex(None), + }, + ), + }, + ), + ], + }, + ), + ], + }, + ), + MatchSequence( + PatternMatchSequence { + node_index: NodeIndex(None), + range: 167..170, + patterns: [ + MatchAs( + PatternMatchAs { + node_index: NodeIndex(None), + range: 168..169, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 168..169, + node_index: NodeIndex(None), + }, + ), + }, + ), + ], + }, + ), + ], + }, + ), + ], + }, + ), + MatchAs( + PatternMatchAs { + node_index: NodeIndex(None), + range: 174..175, + pattern: None, + name: Some( + Identifier { + id: Name("x"), + range: 174..175, + node_index: NodeIndex(None), + }, + ), + }, + ), + ], + }, + ), + guard: None, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 177..180, + value: EllipsisLiteral( + ExprEllipsisLiteral { + node_index: NodeIndex(None), + range: 177..180, + }, + ), + }, + ), + ], + }, + ], + }, + ), + ], + }, +) +``` From 9bacd19c5a44594f44be1b0dcebd7ccf1777e633 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 30 Oct 2025 13:42:46 -0400 Subject: [PATCH 082/188] [ty] Fix lookup of `__new__` on instances (#21147) ## Summary We weren't correctly modeling it as a `staticmethod` in all cases, leading us to incorrectly infer that the `cls` argument would be bound if it was accessed on an instance (rather than the class object). ## Test Plan Added mdtests that fail on `main`. The primer output also looks good! --- crates/ty_ide/src/completion.rs | 2 +- .../resources/mdtest/call/methods.md | 22 +++++++++++++++ crates/ty_python_semantic/src/types.rs | 27 +++++++++++-------- .../ty_python_semantic/src/types/call/bind.rs | 8 +++--- crates/ty_python_semantic/src/types/class.rs | 10 ++----- 5 files changed, 44 insertions(+), 25 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 18af1da727..99debdf4d9 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -1791,7 +1791,7 @@ quux. __init_subclass__ :: bound method type[Quux].__init_subclass__() -> None __module__ :: str __ne__ :: bound method Quux.__ne__(value: object, /) -> bool - __new__ :: bound method Quux.__new__() -> Quux + __new__ :: def __new__(cls) -> Self@__new__ __reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...] __reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...] __repr__ :: bound method Quux.__repr__() -> str diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index 8179e3272d..f101aa6e64 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -588,6 +588,28 @@ reveal_type(C.f2(1)) # revealed: str reveal_type(C().f2(1)) # revealed: str ``` +### `__new__` + +`__new__` is an implicit `@staticmethod`; accessing it on an instance does not bind the `cls` +argument: + +```py +from typing_extensions import Self + +reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__ +reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__ +# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__] +reveal_type(int.__new__) +# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__] +reveal_type((42).__new__) + +class X: + def __init__(self, val: int): ... + def make_another(self) -> Self: + reveal_type(self.__new__) # revealed: def __new__(cls) -> Self@__new__ + return self.__new__(X) +``` + ## Builtin functions and methods Some builtin functions and methods are heavily special-cased by ty. This mdtest checks that various diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 78844ef0c3..6b48499e9b 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -3584,16 +3584,21 @@ impl<'db> Type<'db> { } } - #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] - #[allow(unused_variables)] - // If we choose name `_unit`, the macro will generate code that uses `_unit`, causing clippy to fail. - fn lookup_dunder_new(self, db: &'db dyn Db, unit: ()) -> Option> { - self.find_name_in_mro_with_policy( - db, - "__new__", - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK - | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, - ) + fn lookup_dunder_new(self, db: &'db dyn Db) -> Option> { + #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] + fn lookup_dunder_new_inner<'db>( + db: &'db dyn Db, + ty: Type<'db>, + _: (), + ) -> Option> { + let mut flags = MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK; + if !ty.is_subtype_of(db, KnownClass::Type.to_instance(db)) { + flags |= MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK; + } + ty.find_name_in_mro_with_policy(db, "__new__", flags) + } + + lookup_dunder_new_inner(db, self, ()) } /// Look up an attribute in the MRO of the meta-type of `self`. This returns class-level attributes @@ -6089,7 +6094,7 @@ impl<'db> Type<'db> { // An alternative might be to not skip `object.__new__` but instead mark it such that it's // easy to check if that's the one we found? // Note that `__new__` is a static method, so we must inject the `cls` argument. - let new_method = self_type.lookup_dunder_new(db, ()); + let new_method = self_type.lookup_dunder_new(db); // Construct an instance type that we can use to look up the `__init__` instance method. // This performs the same logic as `Type::to_instance`, except for generic class literals. diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 81ed3fed50..1b4629b301 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -25,8 +25,8 @@ use crate::types::diagnostic::{ }; use crate::types::enums::is_enum_class; use crate::types::function::{ - DataclassTransformerFlags, DataclassTransformerParams, FunctionDecorators, FunctionType, - KnownFunction, OverloadLiteral, + DataclassTransformerFlags, DataclassTransformerParams, FunctionType, KnownFunction, + OverloadLiteral, }; use crate::types::generics::{ InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError, @@ -357,9 +357,7 @@ impl<'db> Bindings<'db> { _ => {} } - } else if function - .has_known_decorator(db, FunctionDecorators::STATICMETHOD) - { + } else if function.is_staticmethod(db) { overload.set_return_type(*function_ty); } else { match overload.parameter_types() { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index d7f50ce716..75190a3c3a 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1051,16 +1051,10 @@ impl<'db> ClassType<'db> { return Type::Callable(metaclass_dunder_call_function.into_callable_type(db)); } - let dunder_new_function_symbol = self_ty - .member_lookup_with_policy( - db, - "__new__".into(), - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, - ) - .place; + let dunder_new_function_symbol = self_ty.lookup_dunder_new(db); let dunder_new_signature = dunder_new_function_symbol - .ignore_possibly_undefined() + .and_then(|place_and_quals| place_and_quals.ignore_possibly_undefined()) .and_then(|ty| match ty { Type::FunctionLiteral(function) => Some(function.signature(db)), Type::Callable(callable) => Some(callable.signatures(db)), From 1c7ea690a820deaa0e17ecf72593ebc4781f3752 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:14:29 -0400 Subject: [PATCH 083/188] [`flake8-type-checking`] Fix `TC003` false positive with `future-annotations` (#21125) Summary -- Fixes #21121 by upgrading `RuntimeEvaluated` annotations like `dataclasses.KW_ONLY` to `RuntimeRequired`. We already had special handling for `TypingOnly` annotations in this context but not `RuntimeEvaluated`. Combining that with the `future-annotations` setting, which allowed ignoring the `RuntimeEvaluated` flag, led to the reported bug where we would try to move `KW_ONLY` into a `TYPE_CHECKING` block. Test Plan -- A new test based on the issue --- .../fixtures/flake8_type_checking/TC003.py | 11 ++++++ .../test/fixtures/pyupgrade/UP037_3.py | 17 +++++++++ crates/ruff_linter/src/checkers/ast/mod.rs | 8 +++++ .../src/rules/flake8_type_checking/mod.rs | 20 +++++++++++ ...future_import_kw_only__TC003_TC003.py.snap | 28 +++++++++++++++ crates/ruff_linter/src/rules/pyupgrade/mod.rs | 15 ++++++++ ...__rules__pyupgrade__tests__UP037_3.py.snap | 36 +++++++++++++++++++ ...grade__tests__rules_py313__UP037_3.py.snap | 4 +++ 8 files changed, 139 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_3.py create mode 100644 crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__add_future_import_kw_only__TC003_TC003.py.snap create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_3.py.snap create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__rules_py313__UP037_3.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC003.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC003.py index d9dd926e42..9ea93ac378 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC003.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC003.py @@ -14,3 +14,14 @@ def f(): import os print(os) + + +# regression test for https://github.com/astral-sh/ruff/issues/21121 +from dataclasses import KW_ONLY, dataclass + + +@dataclass +class DataClass: + a: int + _: KW_ONLY # should be an exception to TC003, even with future-annotations + b: int diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_3.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_3.py new file mode 100644 index 0000000000..929486d6da --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_3.py @@ -0,0 +1,17 @@ +""" +Regression test for an ecosystem hit on +https://github.com/astral-sh/ruff/pull/21125. + +We should mark all of the components of special dataclass annotations as +runtime-required, not just the first layer. +""" + +from dataclasses import dataclass +from typing import ClassVar, Optional + + +@dataclass(frozen=True) +class EmptyCell: + _singleton: ClassVar[Optional["EmptyCell"]] = None + # the behavior of _singleton above should match a non-ClassVar + _doubleton: "EmptyCell" diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 2321cfbb7c..2280dc33cb 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -1400,6 +1400,14 @@ impl<'a> Visitor<'a> for Checker<'a> { AnnotationContext::RuntimeRequired => { self.visit_runtime_required_annotation(annotation); } + AnnotationContext::RuntimeEvaluated + if flake8_type_checking::helpers::is_dataclass_meta_annotation( + annotation, + self.semantic(), + ) => + { + self.visit_runtime_required_annotation(annotation); + } AnnotationContext::RuntimeEvaluated => { self.visit_runtime_evaluated_annotation(annotation); } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs index fd8f08626d..337c420621 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs @@ -98,6 +98,26 @@ mod tests { Ok(()) } + #[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TC003.py"))] + fn add_future_import_dataclass_kw_only_py313(rule: Rule, path: &Path) -> Result<()> { + let snapshot = format!( + "add_future_import_kw_only__{}_{}", + rule.noqa_code(), + path.to_string_lossy() + ); + let diagnostics = test_path( + Path::new("flake8_type_checking").join(path).as_path(), + &settings::LinterSettings { + future_annotations: true, + // The issue in #21121 also didn't trigger on Python 3.14 + unresolved_target_version: PythonVersion::PY313.into(), + ..settings::LinterSettings::for_rule(rule) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + // we test these rules as a pair, since they're opposites of one another // so we want to make sure their fixes are not going around in circles. #[test_case(Rule::UnquotedTypeAlias, Path::new("TC007.py"))] diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__add_future_import_kw_only__TC003_TC003.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__add_future_import_kw_only__TC003_TC003.py.snap new file mode 100644 index 0000000000..c2f1077e90 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__add_future_import_kw_only__TC003_TC003.py.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +--- +TC003 [*] Move standard library import `os` into a type-checking block + --> TC003.py:8:12 + | + 7 | def f(): + 8 | import os + | ^^ + 9 | +10 | x: os + | +help: Move into type-checking block +2 | +3 | For typing-only import detection tests, see `TC002.py`. +4 | """ +5 + from typing import TYPE_CHECKING +6 + +7 + if TYPE_CHECKING: +8 + import os +9 | +10 | +11 | def f(): + - import os +12 | +13 | x: os +14 | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index c8919fe1b0..c933de5ee8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -64,6 +64,7 @@ mod tests { #[test_case(Rule::QuotedAnnotation, Path::new("UP037_0.py"))] #[test_case(Rule::QuotedAnnotation, Path::new("UP037_1.py"))] #[test_case(Rule::QuotedAnnotation, Path::new("UP037_2.pyi"))] + #[test_case(Rule::QuotedAnnotation, Path::new("UP037_3.py"))] #[test_case(Rule::RedundantOpenModes, Path::new("UP015.py"))] #[test_case(Rule::RedundantOpenModes, Path::new("UP015_1.py"))] #[test_case(Rule::ReplaceStdoutStderr, Path::new("UP022.py"))] @@ -156,6 +157,20 @@ mod tests { Ok(()) } + #[test_case(Rule::QuotedAnnotation, Path::new("UP037_3.py"))] + fn rules_py313(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("rules_py313__{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("pyupgrade").join(path).as_path(), + &settings::LinterSettings { + unresolved_target_version: PythonVersion::PY313.into(), + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.py"))] #[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))] #[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_0.py"))] diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_3.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_3.py.snap new file mode 100644 index 0000000000..19cdea494d --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_3.py.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP037 [*] Remove quotes from type annotation + --> UP037_3.py:15:35 + | +13 | @dataclass(frozen=True) +14 | class EmptyCell: +15 | _singleton: ClassVar[Optional["EmptyCell"]] = None + | ^^^^^^^^^^^ +16 | # the behavior of _singleton above should match a non-ClassVar +17 | _doubleton: "EmptyCell" + | +help: Remove quotes +12 | +13 | @dataclass(frozen=True) +14 | class EmptyCell: + - _singleton: ClassVar[Optional["EmptyCell"]] = None +15 + _singleton: ClassVar[Optional[EmptyCell]] = None +16 | # the behavior of _singleton above should match a non-ClassVar +17 | _doubleton: "EmptyCell" + +UP037 [*] Remove quotes from type annotation + --> UP037_3.py:17:17 + | +15 | _singleton: ClassVar[Optional["EmptyCell"]] = None +16 | # the behavior of _singleton above should match a non-ClassVar +17 | _doubleton: "EmptyCell" + | ^^^^^^^^^^^ + | +help: Remove quotes +14 | class EmptyCell: +15 | _singleton: ClassVar[Optional["EmptyCell"]] = None +16 | # the behavior of _singleton above should match a non-ClassVar + - _doubleton: "EmptyCell" +17 + _doubleton: EmptyCell diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__rules_py313__UP037_3.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__rules_py313__UP037_3.py.snap new file mode 100644 index 0000000000..2bacb5d540 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__rules_py313__UP037_3.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- + From c0b04d4b7cfd725a9ca1b92b8bc4d93a63c56c59 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 30 Oct 2025 16:11:04 -0400 Subject: [PATCH 084/188] [ty] Update "constraint implication" relation to work on constraints between two typevars (#21068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's possible for a constraint to mention two typevars. For instance, in the body of ```py def f[S: int, T: S](): ... ``` the baseline constraint set would be `(T ≤ S) ∧ (S ≤ int)`. That is, `S` must specialize to some subtype of `int`, and `T` must specialize to a subtype of the type that `S` specializes to. This PR updates the new "constraint implication" relationship from #21010 to work on these kinds of constraint sets. For instance, in the example above, we should be able to see that `T ≤ int` must always hold: ```py def f[S, T](): constraints = ConstraintSet.range(Never, S, int) & ConstraintSet.range(Never, T, S) static_assert(constraints.implies_subtype_of(T, int)) # now succeeds! ``` This did not require major changes to the implementation of `implies_subtype_of`. That method already relies on how our `simplify` and `domain` methods expand a constraint set to include the transitive closure of the constraints that it mentions, and to mark certain combinations of constraints as impossible. Previously, that transitive closure logic only looked at pairs of constraints that constrain the same typevar. (For instance, to notice that `(T ≤ bool) ∧ ¬(T ≤ int)` is impossible.) Now we also look at pairs of constraints that constraint different typevars, if one of the constraints is bound by the other — that is, pairs of the form `T ≤ S` and `S ≤ something`, or `S ≤ T` and `something ≤ S`. In those cases, transitivity lets us add a new derived constraint that `T ≤ something` or `something ≤ T`, respectively. Having done that, our existing `implies_subtype_of` logic finds and takes into account that derived constraint. --- .../type_properties/implies_subtype_of.md | 4 - .../src/types/constraints.rs | 113 ++++++++++++++---- 2 files changed, 89 insertions(+), 28 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md index 7a276b6d2c..ecb1adcada 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md @@ -180,16 +180,12 @@ This might require propagating constraints from other typevars. def mutually_constrained[T, U](): # If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well. given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) - # TODO: no static-assert-error - # error: [static-assert-error] static_assert(given_int.implies_subtype_of(T, int)) static_assert(not given_int.implies_subtype_of(T, bool)) static_assert(not given_int.implies_subtype_of(T, str)) # If [T ≤ U ∧ U ≤ int], then [T ≤ int] must be true as well. given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) - # TODO: no static-assert-error - # error: [static-assert-error] static_assert(given_int.implies_subtype_of(T, int)) static_assert(not given_int.implies_subtype_of(T, bool)) static_assert(not given_int.implies_subtype_of(T, str)) diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index d7aee0eb6f..ef7632ff2e 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -753,30 +753,24 @@ impl<'db> Node<'db> { rhs: Type<'db>, inferable: InferableTypeVars<'_, 'db>, ) -> Self { - match (lhs, rhs) { - // When checking subtyping involving a typevar, we project the BDD so that it only - // contains that typevar, and any other typevars that could be its upper/lower bound. - // (That is, other typevars that are "later" in our arbitrary ordering of typevars.) - // - // Having done that, 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. + // 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. + let constraint = match (lhs, rhs) { (Type::TypeVar(bound_typevar), _) => { - let constraint = ConstrainedTypeVar::new_node(db, bound_typevar, Type::Never, rhs); - let (simplified, domain) = self.implies(db, constraint).simplify_and_domain(db); - simplified.and(db, domain) + ConstrainedTypeVar::new_node(db, bound_typevar, Type::Never, rhs) } - (_, Type::TypeVar(bound_typevar)) => { - let constraint = - ConstrainedTypeVar::new_node(db, bound_typevar, lhs, Type::object()); - let (simplified, domain) = self.implies(db, constraint).simplify_and_domain(db); - simplified.and(db, domain) + ConstrainedTypeVar::new_node(db, bound_typevar, lhs, Type::object()) } - // If neither type is a typevar, then we fall back on a normal subtyping check. - _ => lhs.when_subtype_of(db, rhs, inferable).node, - } + _ => return lhs.when_subtype_of(db, rhs, inferable).node, + }; + + let simplified_self = self.simplify(db); + let implication = simplified_self.implies(db, constraint); + let (simplified, domain) = implication.simplify_and_domain(db); + simplified.and(db, domain) } /// Returns a new BDD that returns the same results as `self`, but with some inputs fixed to @@ -1258,14 +1252,85 @@ impl<'db> InteriorNode<'db> { let mut simplified = Node::Interior(self); let mut domain = Node::AlwaysTrue; while let Some((left_constraint, right_constraint)) = to_visit.pop() { - // If the constraints refer to different typevars, they trivially cannot be compared. - // TODO: We might need to consider when one constraint's upper or lower bound refers to - // the other constraint's typevar. - let typevar = left_constraint.typevar(db); - if typevar != right_constraint.typevar(db) { + // 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); + 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, + ) + } else { + ( + right_typevar, + right_constraint, + left_typevar, + 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 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), + ) { + // (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)) + } + + // (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)) + } + + // (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) + } + + _ => continue, + }; + + let new_node = Node::new_constraint( + db, + ConstrainedTypeVar::new(db, constrained_typevar, new_lower, new_upper), + ); + let positive_left_node = + Node::new_satisfied_constraint(db, left_constraint.when_true()); + let positive_right_node = + Node::new_satisfied_constraint(db, right_constraint.when_true()); + let lhs = positive_left_node.and(db, positive_right_node); + let implication = lhs.implies(db, new_node); + domain = domain.and(db, implication); + + let intersection = new_node.ite(db, lhs, Node::AlwaysFalse); + simplified = simplified.and(db, intersection); continue; } + // From here on out we know that both constraints constrain the same typevar. + // 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) { From 13375d0e42bcf7e4dac346d8fad991b97222371d Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 30 Oct 2025 16:44:51 -0400 Subject: [PATCH 085/188] [ty] Use the top materialization of classes for narrowing in class-patterns for `match` statements (#21150) --- .../mdtest/exhaustiveness_checking.md | 19 +++++ .../resources/mdtest/narrow/match.md | 75 +++++++++++++++++++ .../reachability_constraints.rs | 5 +- crates/ty_python_semantic/src/types/narrow.rs | 22 ++++-- 4 files changed, 113 insertions(+), 8 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md index 7218359750..29b267024b 100644 --- a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -182,6 +182,11 @@ def match_non_exhaustive(x: Color): ## `isinstance` checks +```toml +[environment] +python-version = "3.12" +``` + ```py from typing import assert_never @@ -189,6 +194,9 @@ class A: ... class B: ... class C: ... +class GenericClass[T]: + x: T + def if_else_exhaustive(x: A | B | C): if isinstance(x, A): pass @@ -253,6 +261,17 @@ def match_non_exhaustive(x: A | B | C): # this diagnostic is correct: the inferred type of `x` is `B & ~A & ~C` assert_never(x) # error: [type-assertion-failure] + +# Note: no invalid-return-type diagnostic; the `match` is exhaustive +def match_exhaustive_generic[T](obj: GenericClass[T]) -> GenericClass[T]: + match obj: + case GenericClass(x=42): + reveal_type(obj) # revealed: GenericClass[T@match_exhaustive_generic] + return obj + case GenericClass(x=x): + reveal_type(x) # revealed: @Todo(`match` pattern definition types) + reveal_type(obj) # revealed: GenericClass[T@match_exhaustive_generic] + return obj ``` ## `isinstance` checks with generics diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index ee51d50af2..f0c107851b 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -69,6 +69,81 @@ match x: reveal_type(x) # revealed: object ``` +## Class patterns with generic classes + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import assert_never + +class Covariant[T]: + def get(self) -> T: + raise NotImplementedError + +def f(x: Covariant[int]): + match x: + case Covariant(): + reveal_type(x) # revealed: Covariant[int] + case _: + reveal_type(x) # revealed: Never + assert_never(x) +``` + +## Class patterns with generic `@final` classes + +These work the same as non-`@final` classes. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import assert_never, final + +@final +class Covariant[T]: + def get(self) -> T: + raise NotImplementedError + +def f(x: Covariant[int]): + match x: + case Covariant(): + reveal_type(x) # revealed: Covariant[int] + case _: + reveal_type(x) # revealed: Never + assert_never(x) +``` + +## Class patterns where the class pattern does not resolve to a class + +In general this does not allow for narrowing, but we make an exception for `Any`. This is to support +[real ecosystem code](https://github.com/jax-ml/jax/blob/d2ce04b6c3d03ae18b145965b8b8b92e09e8009c/jax/_src/pallas/mosaic_gpu/lowering.py#L3372-L3387) +found in `jax`. + +```py +from typing import Any + +X = Any + +def f(obj: object): + match obj: + case int(): + reveal_type(obj) # revealed: int + case X(): + reveal_type(obj) # revealed: Any & ~int + +def g(obj: object, Y: Any): + match obj: + case int(): + reveal_type(obj) # revealed: int + case Y(): + reveal_type(obj) # revealed: Any & ~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/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index af3ef642e3..1224190209 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -771,8 +771,9 @@ impl ReachabilityConstraints { truthiness } PatternPredicateKind::Class(class_expr, kind) => { - let class_ty = - infer_expression_type(db, *class_expr, TypeContext::default()).to_instance(db); + let class_ty = infer_expression_type(db, *class_expr, TypeContext::default()) + .as_class_literal() + .map(|class| Type::instance(db, class.top_materialization(db))); class_ty.map_or(Truthiness::Ambiguous, |class_ty| { if subject_ty.is_subtype_of(db, class_ty) { diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 736272cb4a..5b709551f5 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -11,8 +11,9 @@ use crate::types::enums::{enum_member_literals, enum_metadata}; use crate::types::function::KnownFunction; use crate::types::infer::infer_same_file_expression_type; use crate::types::{ - ClassLiteral, ClassType, IntersectionBuilder, KnownClass, SubclassOfInner, SubclassOfType, - Truthiness, Type, TypeContext, TypeVarBoundOrConstraints, UnionBuilder, infer_expression_types, + ClassLiteral, ClassType, IntersectionBuilder, KnownClass, SpecialFormType, SubclassOfInner, + SubclassOfType, Truthiness, Type, TypeContext, TypeVarBoundOrConstraints, UnionBuilder, + infer_expression_types, }; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; @@ -962,11 +963,20 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { let subject = place_expr(subject.node_ref(self.db, self.module))?; let place = self.expect_place(&subject); - let ty = infer_same_file_expression_type(self.db, cls, TypeContext::default(), self.module) - .to_instance(self.db)?; + let class_type = + infer_same_file_expression_type(self.db, cls, TypeContext::default(), self.module); - let ty = ty.negate_if(self.db, !is_positive); - Some(NarrowingConstraints::from_iter([(place, ty)])) + let narrowed_type = match class_type { + Type::ClassLiteral(class) => { + Type::instance(self.db, class.top_materialization(self.db)) + .negate_if(self.db, !is_positive) + } + dynamic @ Type::Dynamic(_) => dynamic, + Type::SpecialForm(SpecialFormType::Any) => Type::any(), + _ => return None, + }; + + Some(NarrowingConstraints::from_iter([(place, narrowed_type)])) } fn evaluate_match_pattern_value( From 3be3a10a2fc6ddaafc13d1f5bca566282285be5f Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Thu, 30 Oct 2025 23:19:59 +0000 Subject: [PATCH 086/188] [ty] Don't provide completions when in class or function definition (#21146) --- crates/ty_ide/src/completion.rs | 111 ++++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 5 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 99debdf4d9..6118b4c85b 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -212,7 +212,10 @@ pub fn completion<'db>( offset: TextSize, ) -> Vec> { let parsed = parsed_module(db, file).load(db); - if is_in_comment(&parsed, offset) || is_in_string(&parsed, offset) { + + let tokens = tokens_start_before(parsed.tokens(), offset); + + if is_in_comment(tokens) || is_in_string(tokens) || is_in_definition_place(db, tokens, file) { return vec![]; } @@ -829,8 +832,7 @@ fn find_typed_text( /// Whether the given offset within the parsed module is within /// a comment or not. -fn is_in_comment(parsed: &ParsedModuleRef, offset: TextSize) -> bool { - let tokens = tokens_start_before(parsed.tokens(), offset); +fn is_in_comment(tokens: &[Token]) -> bool { tokens.last().is_some_and(|t| t.kind().is_comment()) } @@ -839,8 +841,7 @@ fn is_in_comment(parsed: &ParsedModuleRef, offset: TextSize) -> bool { /// /// Note that this will return `false` when positioned within an /// interpolation block in an f-string or a t-string. -fn is_in_string(parsed: &ParsedModuleRef, offset: TextSize) -> bool { - let tokens = tokens_start_before(parsed.tokens(), offset); +fn is_in_string(tokens: &[Token]) -> bool { tokens.last().is_some_and(|t| { matches!( t.kind(), @@ -849,6 +850,29 @@ fn is_in_string(parsed: &ParsedModuleRef, offset: TextSize) -> bool { }) } +/// If the tokens end with `class f` or `def f` we return true. +/// If the tokens end with `class` or `def`, we return false. +/// This is fine because we don't provide completions anyway. +fn is_in_definition_place(db: &dyn Db, tokens: &[Token], file: File) -> bool { + tokens + .len() + .checked_sub(2) + .and_then(|i| tokens.get(i)) + .is_some_and(|t| { + if matches!( + t.kind(), + TokenKind::Def | TokenKind::Class | TokenKind::Type + ) { + true + } else if t.kind() == TokenKind::Name { + let source = source_text(db, file); + &source[t.range()] == "type" + } else { + false + } + }) +} + /// Order completions according to the following rules: /// /// 1) Names with no underscore prefix @@ -4058,6 +4082,83 @@ def f[T](x: T): test.build().contains("__repr__"); } + #[test] + fn no_completions_in_function_def_name() { + let builder = completion_test_builder( + "\ +def f + ", + ); + + builder.auto_import().build().not_contains("fabs"); + } + + #[test] + fn no_completions_in_function_def_empty_name() { + let builder = completion_test_builder( + "\ +def + ", + ); + + builder.auto_import().build().not_contains("fabs"); + } + + #[test] + fn no_completions_in_class_def_name() { + let builder = completion_test_builder( + "\ +class f + ", + ); + + builder.auto_import().build().not_contains("fabs"); + } + + #[test] + fn no_completions_in_class_def_empty_name() { + let builder = completion_test_builder( + "\ +class + ", + ); + + builder.auto_import().build().not_contains("fabs"); + } + + #[test] + fn no_completions_in_type_def_name() { + let builder = completion_test_builder( + "\ +type f = int + ", + ); + + builder.auto_import().build().not_contains("fabs"); + } + + #[test] + fn no_completions_in_maybe_type_def_name() { + let builder = completion_test_builder( + "\ +type f + ", + ); + + builder.auto_import().build().not_contains("fabs"); + } + + #[test] + fn no_completions_in_type_def_empty_name() { + let builder = completion_test_builder( + "\ +type + ", + ); + + builder.auto_import().build().not_contains("fabs"); + } + /// A way to create a simple single-file (named `main.py`) completion test /// builder. /// From 8737a2d5f5138d855ef4b3ff6982bd7684324eab Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Thu, 30 Oct 2025 17:06:29 -0700 Subject: [PATCH 087/188] Bump v0.14.3 (#21152) - **Upgrade to rooster==0.1.1** - **Changelog for v0.14.3** - **Bump v0.14.3** --- CHANGELOG.md | 55 +++++++++++++++++++++++++++++++ Cargo.lock | 6 ++-- README.md | 6 ++-- crates/ruff/Cargo.toml | 2 +- crates/ruff_linter/Cargo.toml | 2 +- crates/ruff_wasm/Cargo.toml | 2 +- docs/integrations.md | 8 ++--- docs/tutorial.md | 2 +- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- scripts/release.sh | 2 +- 11 files changed, 72 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4689757c34..e7d5ed2e2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,60 @@ # Changelog +## 0.14.3 + +Released on 2025-10-30. + +### Preview features + +- Respect `--output-format` with `--watch` ([#21097](https://github.com/astral-sh/ruff/pull/21097)) +- \[`pydoclint`\] Fix false positive on explicit exception re-raising (`DOC501`, `DOC502`) ([#21011](https://github.com/astral-sh/ruff/pull/21011)) +- \[`pyflakes`\] Revert to stable behavior if imports for module lie in alternate branches for `F401` ([#20878](https://github.com/astral-sh/ruff/pull/20878)) +- \[`pylint`\] Implement `stop-iteration-return` (`PLR1708`) ([#20733](https://github.com/astral-sh/ruff/pull/20733)) +- \[`ruff`\] Add support for additional eager conversion patterns (`RUF065`) ([#20657](https://github.com/astral-sh/ruff/pull/20657)) + +### Bug fixes + +- Fix finding keyword range for clause header after statement ending with semicolon ([#21067](https://github.com/astral-sh/ruff/pull/21067)) +- Fix syntax error false positive on nested alternative patterns ([#21104](https://github.com/astral-sh/ruff/pull/21104)) +- \[`ISC001`\] Fix panic when string literals are unclosed ([#21034](https://github.com/astral-sh/ruff/pull/21034)) +- \[`flake8-django`\] Apply `DJ001` to annotated fields ([#20907](https://github.com/astral-sh/ruff/pull/20907)) +- \[`flake8-pyi`\] Fix `PYI034` to not trigger on metaclasses (`PYI034`) ([#20881](https://github.com/astral-sh/ruff/pull/20881)) +- \[`flake8-type-checking`\] Fix `TC003` false positive with `future-annotations` ([#21125](https://github.com/astral-sh/ruff/pull/21125)) +- \[`pyflakes`\] Fix false positive for `__class__` in lambda expressions within class definitions (`F821`) ([#20564](https://github.com/astral-sh/ruff/pull/20564)) +- \[`pyupgrade`\] Fix false positive for `TypeVar` with default on Python \<3.13 (`UP046`,`UP047`) ([#21045](https://github.com/astral-sh/ruff/pull/21045)) + +### Rule changes + +- Add missing docstring sections to the numpy list ([#20931](https://github.com/astral-sh/ruff/pull/20931)) +- \[`airflow`\] Extend `airflow.models..Param` check (`AIR311`) ([#21043](https://github.com/astral-sh/ruff/pull/21043)) +- \[`airflow`\] Warn that `airflow....DAG.create_dagrun` has been removed (`AIR301`) ([#21093](https://github.com/astral-sh/ruff/pull/21093)) +- \[`refurb`\] Preserve digit separators in `Decimal` constructor (`FURB157`) ([#20588](https://github.com/astral-sh/ruff/pull/20588)) + +### Server + +- Avoid sending an unnecessary "clear diagnostics" message for clients supporting pull diagnostics ([#21105](https://github.com/astral-sh/ruff/pull/21105)) + +### Documentation + +- \[`flake8-bandit`\] Fix correct example for `S308` ([#21128](https://github.com/astral-sh/ruff/pull/21128)) + +### Other changes + +- Clearer error message when `line-length` goes beyond threshold ([#21072](https://github.com/astral-sh/ruff/pull/21072)) + +### Contributors + +- [@danparizher](https://github.com/danparizher) +- [@jvacek](https://github.com/jvacek) +- [@ntBre](https://github.com/ntBre) +- [@augustelalande](https://github.com/augustelalande) +- [@prakhar1144](https://github.com/prakhar1144) +- [@TaKO8Ki](https://github.com/TaKO8Ki) +- [@dylwil3](https://github.com/dylwil3) +- [@fatelei](https://github.com/fatelei) +- [@ShaharNaveh](https://github.com/ShaharNaveh) +- [@Lee-W](https://github.com/Lee-W) + ## 0.14.2 Released on 2025-10-23. diff --git a/Cargo.lock b/Cargo.lock index 070f471e4e..38eff20a3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2835,7 +2835,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.14.2" +version = "0.14.3" dependencies = [ "anyhow", "argfile", @@ -3092,7 +3092,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.14.2" +version = "0.14.3" dependencies = [ "aho-corasick", "anyhow", @@ -3447,7 +3447,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.14.2" +version = "0.14.3" dependencies = [ "console_error_panic_hook", "console_log", diff --git a/README.md b/README.md index 92d707838a..dcb399dd83 100644 --- a/README.md +++ b/README.md @@ -147,8 +147,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.14.2/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.14.2/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.14.3/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.14.3/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -181,7 +181,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.14.2 + rev: v0.14.3 hooks: # Run the linter. - id: ruff-check diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index c1511d805b..e977d5223e 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.14.2" +version = "0.14.3" publish = true authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index bc25d4574f..0826f28fbc 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.14.2" +version = "0.14.3" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index f399ef1007..2dc77f3b8e 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.14.2" +version = "0.14.3" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/integrations.md b/docs/integrations.md index 441845a474..f37ce29852 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.14.2-alpine + name: ghcr.io/astral-sh/ruff:0.14.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.14.2 + rev: v0.14.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.14.2 + rev: v0.14.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.14.2 + rev: v0.14.3 hooks: # Run the linter. - id: ruff-check diff --git a/docs/tutorial.md b/docs/tutorial.md index f3f2a8b3dd..4b0c43ac06 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.14.2 + rev: v0.14.3 hooks: # Run the linter. - id: ruff diff --git a/pyproject.toml b/pyproject.toml index 91b5430a8e..28c9c93b39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.14.2" +version = "0.14.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 df9f38e5db..92a8da6ea7 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.14.2" +version = "0.14.3" description = "" authors = ["Charles Marsh "] diff --git a/scripts/release.sh b/scripts/release.sh index ad97c20c8a..ae8d22f525 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -12,7 +12,7 @@ project_root="$(dirname "$script_root")" echo "Updating metadata with rooster..." cd "$project_root" uvx --python 3.12 --isolated -- \ - rooster@0.1.0 release "$@" + rooster@0.1.1 release "$@" echo "Updating lockfile..." cargo update -p ruff From 4b758b374689f0ce8d96a25629786ae0fb9a8a03 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Fri, 31 Oct 2025 00:43:50 +0000 Subject: [PATCH 088/188] [ty] Fix tests for definition completions (#21153) --- crates/ty_ide/src/completion.rs | 51 ++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 6118b4c85b..273a148ef3 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -854,23 +854,25 @@ fn is_in_string(tokens: &[Token]) -> bool { /// If the tokens end with `class` or `def`, we return false. /// This is fine because we don't provide completions anyway. fn is_in_definition_place(db: &dyn Db, tokens: &[Token], file: File) -> bool { + let is_definition_keyword = |token: &Token| { + if matches!( + token.kind(), + TokenKind::Def | TokenKind::Class | TokenKind::Type + ) { + true + } else if token.kind() == TokenKind::Name { + let source = source_text(db, file); + &source[token.range()] == "type" + } else { + false + } + }; + tokens .len() .checked_sub(2) .and_then(|i| tokens.get(i)) - .is_some_and(|t| { - if matches!( - t.kind(), - TokenKind::Def | TokenKind::Class | TokenKind::Type - ) { - true - } else if t.kind() == TokenKind::Name { - let source = source_text(db, file); - &source[t.range()] == "type" - } else { - false - } - }) + .is_some_and(is_definition_keyword) } /// Order completions according to the following rules: @@ -4090,18 +4092,19 @@ def f ", ); - builder.auto_import().build().not_contains("fabs"); + assert!(builder.auto_import().build().completions().is_empty()); } #[test] - fn no_completions_in_function_def_empty_name() { + fn completions_in_function_def_empty_name() { let builder = completion_test_builder( "\ def ", ); - builder.auto_import().build().not_contains("fabs"); + // This is okay because the ide will not request completions when the cursor is in this position. + assert!(!builder.auto_import().build().completions().is_empty()); } #[test] @@ -4112,18 +4115,19 @@ class f ", ); - builder.auto_import().build().not_contains("fabs"); + assert!(builder.auto_import().build().completions().is_empty()); } #[test] - fn no_completions_in_class_def_empty_name() { + fn completions_in_class_def_empty_name() { let builder = completion_test_builder( "\ class ", ); - builder.auto_import().build().not_contains("fabs"); + // This is okay because the ide will not request completions when the cursor is in this position. + assert!(!builder.auto_import().build().completions().is_empty()); } #[test] @@ -4134,7 +4138,7 @@ type f = int ", ); - builder.auto_import().build().not_contains("fabs"); + assert!(builder.auto_import().build().completions().is_empty()); } #[test] @@ -4145,18 +4149,19 @@ type f ", ); - builder.auto_import().build().not_contains("fabs"); + assert!(builder.auto_import().build().completions().is_empty()); } #[test] - fn no_completions_in_type_def_empty_name() { + fn completions_in_type_def_empty_name() { let builder = completion_test_builder( "\ type ", ); - builder.auto_import().build().not_contains("fabs"); + // This is okay because the ide will not request completions when the cursor is in this position. + assert!(!builder.auto_import().build().completions().is_empty()); } /// A way to create a simple single-file (named `main.py`) completion test From 4b026c2a553caff2a25641c14f9cdc8153ee3a63 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 31 Oct 2025 02:16:43 +0100 Subject: [PATCH 089/188] Fix missing diagnostics for notebooks (#21156) --- .../ruff_server/src/server/api/diagnostics.rs | 23 ++----------------- .../server/api/notifications/did_change.rs | 6 ++++- .../api/notifications/did_change_notebook.rs | 5 +++- .../notifications/did_change_watched_files.rs | 10 ++++++-- .../src/server/api/notifications/did_close.rs | 2 +- .../src/server/api/notifications/did_open.rs | 12 +++++++++- .../api/notifications/did_open_notebook.rs | 5 +++- 7 files changed, 35 insertions(+), 28 deletions(-) diff --git a/crates/ruff_server/src/server/api/diagnostics.rs b/crates/ruff_server/src/server/api/diagnostics.rs index 2c8faab6db..6f8efe47e8 100644 --- a/crates/ruff_server/src/server/api/diagnostics.rs +++ b/crates/ruff_server/src/server/api/diagnostics.rs @@ -1,7 +1,4 @@ -use lsp_types::Url; - use crate::{ - Session, lint::DiagnosticsMap, session::{Client, DocumentQuery, DocumentSnapshot}, }; @@ -22,21 +19,10 @@ pub(super) fn generate_diagnostics(snapshot: &DocumentSnapshot) -> DiagnosticsMa } pub(super) fn publish_diagnostics_for_document( - session: &Session, - url: &Url, + snapshot: &DocumentSnapshot, client: &Client, ) -> crate::server::Result<()> { - // Publish diagnostics if the client doesn't support pull diagnostics - if session.resolved_client_capabilities().pull_diagnostics { - return Ok(()); - } - - let snapshot = session - .take_snapshot(url.clone()) - .ok_or_else(|| anyhow::anyhow!("Unable to take snapshot for document with URL {url}")) - .with_failure_code(lsp_server::ErrorCode::InternalError)?; - - for (uri, diagnostics) in generate_diagnostics(&snapshot) { + for (uri, diagnostics) in generate_diagnostics(snapshot) { client .send_notification::( lsp_types::PublishDiagnosticsParams { @@ -52,14 +38,9 @@ pub(super) fn publish_diagnostics_for_document( } pub(super) fn clear_diagnostics_for_document( - session: &Session, query: &DocumentQuery, client: &Client, ) -> crate::server::Result<()> { - if session.resolved_client_capabilities().pull_diagnostics { - return Ok(()); - } - client .send_notification::( lsp_types::PublishDiagnosticsParams { diff --git a/crates/ruff_server/src/server/api/notifications/did_change.rs b/crates/ruff_server/src/server/api/notifications/did_change.rs index 5ac7a1f606..8e77cb593f 100644 --- a/crates/ruff_server/src/server/api/notifications/did_change.rs +++ b/crates/ruff_server/src/server/api/notifications/did_change.rs @@ -31,7 +31,11 @@ impl super::SyncNotificationHandler for DidChange { .update_text_document(&key, content_changes, new_version) .with_failure_code(ErrorCode::InternalError)?; - publish_diagnostics_for_document(session, &key.into_url(), client)?; + // Publish diagnostics if the client doesn't support pull diagnostics + if !session.resolved_client_capabilities().pull_diagnostics { + let snapshot = session.take_snapshot(key.into_url()).unwrap(); + publish_diagnostics_for_document(&snapshot, client)?; + } Ok(()) } diff --git a/crates/ruff_server/src/server/api/notifications/did_change_notebook.rs b/crates/ruff_server/src/server/api/notifications/did_change_notebook.rs index da11755d71..d092ccacb8 100644 --- a/crates/ruff_server/src/server/api/notifications/did_change_notebook.rs +++ b/crates/ruff_server/src/server/api/notifications/did_change_notebook.rs @@ -27,7 +27,10 @@ impl super::SyncNotificationHandler for DidChangeNotebook { .with_failure_code(ErrorCode::InternalError)?; // publish new diagnostics - publish_diagnostics_for_document(session, &key.into_url(), client)?; + let snapshot = session + .take_snapshot(key.into_url()) + .expect("snapshot should be available"); + publish_diagnostics_for_document(&snapshot, client)?; Ok(()) } diff --git a/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs b/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs index cb157d81f9..bc97231411 100644 --- a/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs +++ b/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs @@ -31,13 +31,19 @@ impl super::SyncNotificationHandler for DidChangeWatchedFiles { } else { // publish diagnostics for text documents for url in session.text_document_urls() { - publish_diagnostics_for_document(session, url, client)?; + let snapshot = session + .take_snapshot(url.clone()) + .expect("snapshot should be available"); + publish_diagnostics_for_document(&snapshot, client)?; } } // always publish diagnostics for notebook files (since they don't use pull diagnostics) for url in session.notebook_document_urls() { - publish_diagnostics_for_document(session, url, client)?; + let snapshot = session + .take_snapshot(url.clone()) + .expect("snapshot should be available"); + publish_diagnostics_for_document(&snapshot, client)?; } } diff --git a/crates/ruff_server/src/server/api/notifications/did_close.rs b/crates/ruff_server/src/server/api/notifications/did_close.rs index 5a482c4fcc..a3075a4846 100644 --- a/crates/ruff_server/src/server/api/notifications/did_close.rs +++ b/crates/ruff_server/src/server/api/notifications/did_close.rs @@ -27,7 +27,7 @@ impl super::SyncNotificationHandler for DidClose { ); return Ok(()); }; - clear_diagnostics_for_document(session, snapshot.query(), client)?; + clear_diagnostics_for_document(snapshot.query(), client)?; session .close_document(&key) diff --git a/crates/ruff_server/src/server/api/notifications/did_open.rs b/crates/ruff_server/src/server/api/notifications/did_open.rs index fa5f6b92df..41a6fb6cf8 100644 --- a/crates/ruff_server/src/server/api/notifications/did_open.rs +++ b/crates/ruff_server/src/server/api/notifications/did_open.rs @@ -1,5 +1,6 @@ use crate::TextDocument; use crate::server::Result; +use crate::server::api::LSPResult; use crate::server::api::diagnostics::publish_diagnostics_for_document; use crate::session::{Client, Session}; use lsp_types as types; @@ -29,7 +30,16 @@ impl super::SyncNotificationHandler for DidOpen { session.open_text_document(uri.clone(), document); - publish_diagnostics_for_document(session, &uri, client)?; + // Publish diagnostics if the client doesn't support pull diagnostics + if !session.resolved_client_capabilities().pull_diagnostics { + let snapshot = session + .take_snapshot(uri.clone()) + .ok_or_else(|| { + anyhow::anyhow!("Unable to take snapshot for document with URL {uri}") + }) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + publish_diagnostics_for_document(&snapshot, client)?; + } Ok(()) } diff --git a/crates/ruff_server/src/server/api/notifications/did_open_notebook.rs b/crates/ruff_server/src/server/api/notifications/did_open_notebook.rs index 3ce27168e4..a75e88ecc5 100644 --- a/crates/ruff_server/src/server/api/notifications/did_open_notebook.rs +++ b/crates/ruff_server/src/server/api/notifications/did_open_notebook.rs @@ -40,7 +40,10 @@ impl super::SyncNotificationHandler for DidOpenNotebook { session.open_notebook_document(uri.clone(), notebook); // publish diagnostics - publish_diagnostics_for_document(session, &uri, client)?; + let snapshot = session + .take_snapshot(uri) + .expect("snapshot should be available"); + publish_diagnostics_for_document(&snapshot, client)?; Ok(()) } From 3585c96ea551366f97f5d1b4743b2f0d648b9e9c Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Fri, 31 Oct 2025 12:53:18 +0000 Subject: [PATCH 090/188] Update etcetera to 0.11.0 (#21160) --- Cargo.lock | 38 ++++++++++++++------------------------ Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38eff20a3f..af119dab7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,7 +243,7 @@ dependencies = [ "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -633,7 +633,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -642,7 +642,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1007,7 +1007,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -1093,7 +1093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -1115,13 +1115,12 @@ dependencies = [ [[package]] name = "etcetera" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" dependencies = [ "cfg-if", - "home", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -1366,15 +1365,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "html-escape" version = "0.2.13" @@ -1563,7 +1553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "serde", "serde_core", ] @@ -1690,7 +1680,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1754,7 +1744,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3545,7 +3535,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -3941,7 +3931,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -5021,7 +5011,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.61.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1cce423668..935196f6a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ dashmap = { version = "6.0.1" } dir-test = { version = "0.4.0" } dunce = { version = "1.0.5" } drop_bomb = { version = "0.1.5" } -etcetera = { version = "0.10.0" } +etcetera = { version = "0.11.0" } fern = { version = "0.7.0" } filetime = { version = "0.2.23" } getrandom = { version = "0.3.1" } From 735ec0c1f97d5b80f1161835ffb3a784d3eebcac Mon Sep 17 00:00:00 2001 From: Mahmoud Saada Date: Fri, 31 Oct 2025 08:55:17 -0400 Subject: [PATCH 091/188] [ty] Fix generic inference for non-dataclass inheriting from generic dataclass (#21159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes https://github.com/astral-sh/ty/issues/1427 This PR fixes a regression introduced in alpha.24 where non-dataclass children of generic dataclasses lost generic type parameter information during `__init__` synthesis. The issue occurred because when looking up inherited members in the MRO, the child class's `inherited_generic_context` was correctly passed down, but `own_synthesized_member()` (which synthesizes dataclass `__init__` methods) didn't accept this parameter. It only used `self.inherited_generic_context(db)`, which returned the parent's context instead of the child's. The fix threads the child's generic context through to the synthesis logic, allowing proper generic type inference for inherited dataclass constructors. ## Test Plan - Added regression test for non-dataclass inheriting from generic dataclass - Verified the exact repro case from the issue now works - All 277 mdtest tests passing - Clippy clean - Manually verified with Python runtime, mypy, and pyright - all accept this code pattern ## Verification Tested against multiple type checkers: - ✅ Python runtime: Code works correctly - ✅ mypy: No issues found - ✅ pyright: 0 errors, 0 warnings - ✅ ty alpha.23: Worked (before regression) - ❌ ty alpha.24: Regression - ✅ ty with this fix: Works correctly --------- Co-authored-by: Claude Co-authored-by: David Peter --- .../mdtest/dataclasses/dataclasses.md | 34 +++++++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 8 +++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index e7171b6dd4..d8619851a2 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -838,6 +838,40 @@ class WrappedIntAndExtraData[T](Wrap[int]): reveal_type(WrappedIntAndExtraData[bytes].__init__) ``` +### Non-dataclass inheriting from generic dataclass + +This is a regression test for . + +When a non-dataclass inherits from a generic dataclass, the generic type parameters should still be +properly inferred when calling the inherited `__init__` method. + +```py +from dataclasses import dataclass + +@dataclass +class ParentDataclass[T]: + value: T + +# Non-dataclass inheriting from generic dataclass +class ChildOfParentDataclass[T](ParentDataclass[T]): ... + +def uses_dataclass[T](x: T) -> ChildOfParentDataclass[T]: + return ChildOfParentDataclass(x) + +# TODO: ParentDataclass.__init__ should show generic types, not Unknown +# revealed: (self: ParentDataclass[Unknown], value: Unknown) -> None +reveal_type(ParentDataclass.__init__) + +# revealed: (self: ParentDataclass[T@ChildOfParentDataclass], value: T@ChildOfParentDataclass) -> None +reveal_type(ChildOfParentDataclass.__init__) + +result_int = uses_dataclass(42) +reveal_type(result_int) # revealed: ChildOfParentDataclass[Literal[42]] + +result_str = uses_dataclass("hello") +reveal_type(result_str) # revealed: ChildOfParentDataclass[Literal["hello"]] +``` + ## Descriptor-typed fields ### Same type in `__get__` and `__set__` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 75190a3c3a..4f8ee4c1fc 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2176,7 +2176,8 @@ impl<'db> ClassLiteral<'db> { }); if member.is_undefined() { - if let Some(synthesized_member) = self.own_synthesized_member(db, specialization, name) + if let Some(synthesized_member) = + self.own_synthesized_member(db, specialization, inherited_generic_context, name) { return Member::definitely_declared(synthesized_member); } @@ -2192,6 +2193,7 @@ impl<'db> ClassLiteral<'db> { self, db: &'db dyn Db, specialization: Option>, + inherited_generic_context: Option>, name: &str, ) -> Option> { let dataclass_params = self.dataclass_params(db); @@ -2320,7 +2322,7 @@ impl<'db> ClassLiteral<'db> { let signature = match name { "__new__" | "__init__" => Signature::new_generic( - self.inherited_generic_context(db), + inherited_generic_context.or_else(|| self.inherited_generic_context(db)), Parameters::new(parameters), return_ty, ), @@ -2702,7 +2704,7 @@ impl<'db> ClassLiteral<'db> { name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - if let Some(member) = self.own_synthesized_member(db, specialization, name) { + if let Some(member) = self.own_synthesized_member(db, specialization, None, name) { Place::bound(member).into() } else { KnownClass::TypedDictFallback From 172e8d4ae060f2d1749b4627d3672254c6bb1366 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Fri, 31 Oct 2025 10:29:24 -0400 Subject: [PATCH 092/188] [ty] Support implicit imports of submodules in `__init__.pyi` (#20855) This is a second take at the implicit imports approach, allowing `from . import submodule` in an `__init__.pyi` to create the `mypackage.submodule` attribute everyhere. This implementation operates inside of the available_submodule_attributes subsystem instead of as a re-export rule. The upside of this is we are no longer purely syntactic, and absolute from imports that happen to target submodules work (an intentional discussed deviation from pyright which demands a relative from import). Also we don't re-export functions or classes. The downside(?) of this is star imports no longer see these attributes (this may be either good or bad. I believe it's not a huge lift to make it work with star imports but it's some non-trivial reworking). I've also intentionally made `import mypackage.submodule` not trigger this rule although it's trivial to change that. I've tried to cover as many relevant cases as possible for discussion in the new test file I've added (there are some random overlaps with existing tests but trying to add them piecemeal felt confusing and weird, so I just made a dedicated file for this extension to the rules). Fixes https://github.com/astral-sh/ty/issues/133 ## Summary ## Test Plan --- crates/ruff_db/src/files.rs | 5 + .../mdtest/import/nonstandard_conventions.md | 824 ++++++++++++++++++ .../ty_python_semantic/src/semantic_index.rs | 87 +- .../src/semantic_index/builder.rs | 17 +- crates/ty_python_semantic/src/types.rs | 61 +- 5 files changed, 982 insertions(+), 12 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md diff --git a/crates/ruff_db/src/files.rs b/crates/ruff_db/src/files.rs index 754b65642a..4d57162c7c 100644 --- a/crates/ruff_db/src/files.rs +++ b/crates/ruff_db/src/files.rs @@ -470,6 +470,11 @@ impl File { self.source_type(db).is_stub() } + /// Returns `true` if the file is an `__init__.pyi` + pub fn is_package_stub(self, db: &dyn Db) -> bool { + self.path(db).as_str().ends_with("__init__.pyi") + } + pub fn source_type(self, db: &dyn Db) -> PySourceType { match self.path(db) { FilePath::System(path) => path diff --git a/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md b/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md new file mode 100644 index 0000000000..848eaae387 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/import/nonstandard_conventions.md @@ -0,0 +1,824 @@ +# Nonstandard Import Conventions + +This document covers ty-specific extensions to the +[standard import conventions](https://typing.python.org/en/latest/spec/distributing.html#import-conventions). + +It's a common idiom for a package's `__init__.py(i)` to include several imports like +`from . import mysubmodule`, with the intent that the `mypackage.mysubmodule` attribute should work +for anyone who only imports `mypackage`. + +In the context of a `.py` we handle this well through our general attempts to faithfully implement +import side-effects. However for `.pyi` files we are expected to apply +[a more strict set of rules](https://typing.python.org/en/latest/spec/distributing.html#import-conventions) +to encourage intentional API design. Although `.pyi` files are explicitly designed to work with +typecheckers, which ostensibly should all enforce these strict rules, every typechecker has its own +defacto "extensions" to them and so a few idioms like `from . import mysubmodule` have found their +way into `.pyi` files too. + +Thus for the sake of compatibility, we need to define our own "extensions". Any extensions we define +here have several competing concerns: + +- Extensions should ideally be kept narrow to continue to encourage explicit API design +- Extensions should be easy to explain, document, and understand +- Extensions should ideally still be a subset of runtime behaviour (if it works in a stub, it works + at runtime) +- Extensions should ideally not make `.pyi` files more permissive than `.py` files (if it works in a + stub, it works in an impl) + +To that end we define the following extension: + +> If an `__init__.pyi` for `mypackage` contains a `from...import` targetting a direct submodule of +> `mypackage`, then that submodule should be available as an attribute of `mypackage`. + +## Relative `from` Import of Direct Submodule in `__init__` + +The `from . import submodule` idiom in an `__init__.pyi` is fairly explicit and we should definitely +support it. + +`mypackage/__init__.pyi`: + +```pyi +from . import imported +``` + +`mypackage/imported.pyi`: + +```pyi +X: int = 42 +``` + +`mypackage/fails.pyi`: + +```pyi +Y: int = 47 +``` + +`main.py`: + +```py +import mypackage + +reveal_type(mypackage.imported.X) # revealed: int +# error: "has no member `fails`" +reveal_type(mypackage.fails.Y) # revealed: Unknown +``` + +## Relative `from` Import of Direct Submodule in `__init__` (Non-Stub Check) + +`mypackage/__init__.py`: + +```py +from . import imported +``` + +`mypackage/imported.py`: + +```py +X: int = 42 +``` + +`mypackage/fails.py`: + +```py +Y: int = 47 +``` + +`main.py`: + +```py +import mypackage + +reveal_type(mypackage.imported.X) # revealed: int +# error: "has no member `fails`" +reveal_type(mypackage.fails.Y) # revealed: Unknown +``` + +## Absolute `from` Import of Direct Submodule in `__init__` + +If an absolute `from...import` happens to import a submodule, it works just as well as a relative +one. + +`mypackage/__init__.pyi`: + +```pyi +from mypackage import imported +``` + +`mypackage/imported.pyi`: + +```pyi +X: int = 42 +``` + +`mypackage/fails.pyi`: + +```pyi +Y: int = 47 +``` + +`main.py`: + +```py +import mypackage + +reveal_type(mypackage.imported.X) # revealed: int +# error: "has no member `fails`" +reveal_type(mypackage.fails.Y) # revealed: Unknown +``` + +## Absolute `from` Import of Direct Submodule in `__init__` (Non-Stub Check) + +`mypackage/__init__.py`: + +```py +from mypackage import imported +``` + +`mypackage/imported.py`: + +```py +X: int = 42 +``` + +`mypackage/fails.py`: + +```py +Y: int = 47 +``` + +`main.py`: + +```py +import mypackage + +reveal_type(mypackage.imported.X) # revealed: int +# error: "has no member `fails`" +reveal_type(mypackage.fails.Y) # revealed: Unknown +``` + +## Import of Direct Submodule in `__init__` + +An `import` that happens to import a submodule does not expose the submodule as an attribute. (This +is an arbitrary decision and can be changed easily!) + +`mypackage/__init__.pyi`: + +```pyi +import mypackage.imported +``` + +`mypackage/imported.pyi`: + +```pyi +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +# TODO: this is probably safe to allow, as it's an unambiguous import of a submodule +# error: "has no member `imported`" +reveal_type(mypackage.imported.X) # revealed: Unknown +``` + +## Import of Direct Submodule in `__init__` (Non-Stub Check) + +`mypackage/__init__.py`: + +```py +import mypackage.imported +``` + +`mypackage/imported.py`: + +```py +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +# TODO: this is probably safe to allow, as it's an unambiguous import of a submodule +# error: "has no member `imported`" +reveal_type(mypackage.imported.X) # revealed: Unknown +``` + +## Relative `from` Import of Nested Submodule in `__init__` + +`from .submodule import nested` in an `__init__.pyi` is currently not supported as a way to expose +`mypackage.submodule` or `mypackage.submodule.nested` but it could be. + +`mypackage/__init__.pyi`: + +```pyi +from .submodule import nested +``` + +`mypackage/submodule/__init__.pyi`: + +```pyi +``` + +`mypackage/submodule/nested.pyi`: + +```pyi +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +# TODO: this would be nice to allow +# error: "has no member `submodule`" +reveal_type(mypackage.submodule) # revealed: Unknown +# error: "has no member `submodule`" +reveal_type(mypackage.submodule.nested) # revealed: Unknown +# error: "has no member `submodule`" +reveal_type(mypackage.submodule.nested.X) # revealed: Unknown +``` + +## Relative `from` Import of Nested Submodule in `__init__` (Non-Stub Check) + +`mypackage/__init__.py`: + +```py +from .submodule import nested +``` + +`mypackage/submodule/__init__.py`: + +```py +``` + +`mypackage/submodule/nested.py`: + +```py +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +# TODO: this would be nice to support +# error: "has no member `submodule`" +reveal_type(mypackage.submodule) # revealed: Unknown +# error: "has no member `submodule`" +reveal_type(mypackage.submodule.nested) # revealed: Unknown +# error: "has no member `submodule`" +reveal_type(mypackage.submodule.nested.X) # revealed: Unknown +``` + +## Absolute `from` Import of Nested Submodule in `__init__` + +`from mypackage.submodule import nested` in an `__init__.pyi` is currently not supported as a way to +expose `mypackage.submodule` or `mypackage.submodule.nested` but it could be. + +`mypackage/__init__.pyi`: + +```pyi +from mypackage.submodule import nested +``` + +`mypackage/submodule/__init__.pyi`: + +```pyi +``` + +`mypackage/submodule/nested.pyi`: + +```pyi +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +# TODO: this would be nice to support +# error: "has no member `submodule`" +reveal_type(mypackage.submodule) # revealed: Unknown +# error: "has no member `submodule`" +reveal_type(mypackage.submodule.nested) # revealed: Unknown +# error: "has no member `submodule`" +reveal_type(mypackage.submodule.nested.X) # revealed: Unknown +``` + +## Absolute `from` Import of Nested Submodule in `__init__` (Non-Stub Check) + +`mypackage/__init__.py`: + +```py +from mypackage.submodule import nested +``` + +`mypackage/submodule/__init__.py`: + +```py +``` + +`mypackage/submodule/nested.py`: + +```py +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +# TODO: this would be nice to support +# error: "has no member `submodule`" +reveal_type(mypackage.submodule) # revealed: Unknown +# error: "has no member `submodule`" +reveal_type(mypackage.submodule.nested) # revealed: Unknown +# error: "has no member `submodule`" +reveal_type(mypackage.submodule.nested.X) # revealed: Unknown +``` + +## Import of Nested Submodule in `__init__` + +`import mypackage.submodule.nested` in an `__init__.pyi` is currently not supported as a way to +expose `mypackage.submodule` or `mypackage.submodule.nested` but it could be. + +`mypackage/__init__.pyi`: + +```pyi +import mypackage.submodule.nested +``` + +`mypackage/submodule/__init__.pyi`: + +```pyi +``` + +`mypackage/submodule/nested.pyi`: + +```pyi +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +# TODO: this would be nice to support, and is probably safe to do as it's unambiguous +# error: "has no member `submodule`" +reveal_type(mypackage.submodule) # revealed: Unknown +# error: "has no member `submodule`" +reveal_type(mypackage.submodule.nested) # revealed: Unknown +# error: "has no member `submodule`" +reveal_type(mypackage.submodule.nested.X) # revealed: Unknown +``` + +## Import of Nested Submodule in `__init__` (Non-Stub Check) + +`mypackage/__init__.py`: + +```py +import mypackage.submodule.nested +``` + +`mypackage/submodule/__init__.py`: + +```py +``` + +`mypackage/submodule/nested.py`: + +```py +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +# TODO: this would be nice to support, and is probably safe to do as it's unambiguous +# error: "has no member `submodule`" +reveal_type(mypackage.submodule) # revealed: Unknown +# error: "has no member `submodule`" +reveal_type(mypackage.submodule.nested) # revealed: Unknown +# error: "has no member `submodule`" +reveal_type(mypackage.submodule.nested.X) # revealed: Unknown +``` + +## Relative `from` Import of Direct Submodule in `__init__`, Mismatched Alias + +Renaming the submodule to something else disables the `__init__.pyi` idiom. + +`mypackage/__init__.pyi`: + +```pyi +from . import imported as imported_m +``` + +`mypackage/imported.pyi`: + +```pyi +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +# error: "has no member `imported`" +reveal_type(mypackage.imported.X) # revealed: Unknown +# error: "has no member `imported_m`" +reveal_type(mypackage.imported_m.X) # revealed: Unknown +``` + +## Relative `from` Import of Direct Submodule in `__init__`, Mismatched Alias (Non-Stub Check) + +`mypackage/__init__.py`: + +```py +from . import imported as imported_m +``` + +`mypackage/imported.py`: + +```py +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +# TODO: this would be nice to support, as it works at runtime +# error: "has no member `imported`" +reveal_type(mypackage.imported.X) # revealed: Unknown +reveal_type(mypackage.imported_m.X) # revealed: int +``` + +## Relative `from` Import of Direct Submodule in `__init__`, Matched Alias + +The `__init__.pyi` idiom should definitely always work if the submodule is renamed to itself, as +this is the re-export idiom. + +`mypackage/__init__.pyi`: + +```pyi +from . import imported as imported +``` + +`mypackage/imported.pyi`: + +```pyi +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +reveal_type(mypackage.imported.X) # revealed: int +``` + +## Relative `from` Import of Direct Submodule in `__init__`, Matched Alias (Non-Stub Check) + +`mypackage/__init__.py`: + +```py +from . import imported as imported +``` + +`mypackage/imported.py`: + +```py +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +reveal_type(mypackage.imported.X) # revealed: int +``` + +## Star Import Unaffected + +Even if the `__init__` idiom is in effect, star imports do not pick it up. (This is an arbitrary +decision that mostly fell out of the implementation details and can be changed!) + +`mypackage/__init__.pyi`: + +```pyi +from . import imported +Z: int = 17 +``` + +`mypackage/imported.pyi`: + +```pyi +X: int = 42 +``` + +`main.py`: + +```py +from mypackage import * + +# TODO: this would be nice to support (available_submodule_attributes isn't visible to `*` imports) +# error: "`imported` used when not defined" +reveal_type(imported.X) # revealed: Unknown +reveal_type(Z) # revealed: int +``` + +## Star Import Unaffected (Non-Stub Check) + +`mypackage/__init__.py`: + +```py +from . import imported + +Z: int = 17 +``` + +`mypackage/imported.py`: + +```py +X: int = 42 +``` + +`main.py`: + +```py +from mypackage import * + +reveal_type(imported.X) # revealed: int +reveal_type(Z) # revealed: int +``` + +## `from` Import of Non-Submodule + +A from import that terminates in a non-submodule should not expose the intermediate submodules as +attributes. This is an arbitrary decision but on balance probably safe and correct, as otherwise it +would be hard for a stub author to be intentional about the submodules being exposed as attributes. + +`mypackage/__init__.pyi`: + +```pyi +from .imported import X +``` + +`mypackage/imported.pyi`: + +```pyi +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +# error: "has no member `imported`" +reveal_type(mypackage.imported.X) # revealed: Unknown +``` + +## `from` Import of Non-Submodule (Non-Stub Check) + +`mypackage/__init__.py`: + +```py +from .imported import X +``` + +`mypackage/imported.py`: + +```py +X: int = 42 +``` + +`main.py`: + +```py +import mypackage + +# TODO: this would be nice to support, as it works at runtime +# error: "has no member `imported`" +reveal_type(mypackage.imported.X) # revealed: Unknown +``` + +## `from` Import of Other Package's Submodule + +`from mypackage import submodule` from outside the package is not modeled as a side-effect on +`mypackage`, even in the importing file (this could be changed!). + +`mypackage/__init__.pyi`: + +```pyi +``` + +`mypackage/imported.pyi`: + +```pyi +X: int = 42 +``` + +`main.py`: + +```py +import mypackage +from mypackage import imported + +# TODO: this would be nice to support, but it's dangerous with available_submodule_attributes +reveal_type(imported.X) # revealed: int +# error: "has no member `imported`" +reveal_type(mypackage.imported.X) # revealed: Unknown +``` + +## `from` Import of Other Package's Submodule (Non-Stub Check) + +`mypackage/__init__.py`: + +```py +``` + +`mypackage/imported.py`: + +```py +X: int = 42 +``` + +`main.py`: + +```py +import mypackage +from mypackage import imported + +# TODO: this would be nice to support, as it works at runtime +reveal_type(imported.X) # revealed: int +# error: "has no member `imported`" +reveal_type(mypackage.imported.X) # revealed: Unknown +``` + +## `from` Import of Sibling Module + +`from . import submodule` from a sibling module is not modeled as a side-effect on `mypackage` or a +re-export from `submodule`. + +`mypackage/__init__.pyi`: + +```pyi +``` + +`mypackage/imported.pyi`: + +```pyi +from . import fails +X: int = 42 +``` + +`mypackage/fails.pyi`: + +```pyi +Y: int = 47 +``` + +`main.py`: + +```py +import mypackage +from mypackage import imported + +reveal_type(imported.X) # revealed: int +# error: "has no member `fails`" +reveal_type(imported.fails.Y) # revealed: Unknown +# error: "has no member `fails`" +reveal_type(mypackage.fails.Y) # revealed: Unknown +``` + +## `from` Import of Sibling Module (Non-Stub Check) + +`mypackage/__init__.py`: + +```py +``` + +`mypackage/imported.py`: + +```py +from . import fails + +X: int = 42 +``` + +`mypackage/fails.py`: + +```py +Y: int = 47 +``` + +`main.py`: + +```py +import mypackage +from mypackage import imported + +reveal_type(imported.X) # revealed: int +reveal_type(imported.fails.Y) # revealed: int +# error: "has no member `fails`" +reveal_type(mypackage.fails.Y) # revealed: Unknown +``` + +## Fractal Re-export Nameclash Problems + +This precise configuration of: + +- a subpackage that defines a submodule with its own name +- that in turn defines a function/class with its own name +- and re-exporting that name through every layer using `from` imports and `__all__` + +Can easily result in the typechecker getting "confused" and thinking imports of the name from the +top-level package are referring to the subpackage and not the function/class. This issue can be +found with the `lobpcg` function in `scipy.sparse.linalg`. + +This kind of failure mode is why the rule is restricted to *direct* submodule imports, as anything +more powerful than that in the current implementation strategy quickly gets the functions and +submodules mixed up. + +`mypackage/__init__.pyi`: + +```pyi +from .funcmod import funcmod + +__all__ = ["funcmod"] +``` + +`mypackage/funcmod/__init__.pyi`: + +```pyi +from .funcmod import funcmod + +__all__ = ["funcmod"] +``` + +`mypackage/funcmod/funcmod.pyi`: + +```pyi +__all__ = ["funcmod"] + +def funcmod(x: int) -> int: ... +``` + +`main.py`: + +```py +from mypackage import funcmod + +x = funcmod(1) +``` + +## Fractal Re-export Nameclash Problems (Non-Stub Check) + +`mypackage/__init__.py`: + +```py +from .funcmod import funcmod + +__all__ = ["funcmod"] +``` + +`mypackage/funcmod/__init__.py`: + +```py +from .funcmod import funcmod + +__all__ = ["funcmod"] +``` + +`mypackage/funcmod/funcmod.py`: + +```py +__all__ = ["funcmod"] + +def funcmod(x: int) -> int: + return x +``` + +`main.py`: + +```py +from mypackage import funcmod + +x = funcmod(1) +``` diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index 558243f59c..a654873db3 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -6,12 +6,12 @@ use ruff_db::parsed::parsed_module; use ruff_index::{IndexSlice, IndexVec}; use ruff_python_ast::NodeIndex; +use ruff_python_ast::name::Name; use ruff_python_parser::semantic_errors::SemanticSyntaxError; use rustc_hash::{FxHashMap, FxHashSet}; use salsa::Update; use salsa::plumbing::AsId; -use crate::Db; use crate::module_name::ModuleName; use crate::node_key::NodeKey; use crate::semantic_index::ast_ids::AstIds; @@ -28,6 +28,7 @@ use crate::semantic_index::scope::{ use crate::semantic_index::symbol::ScopedSymbolId; use crate::semantic_index::use_def::{EnclosingSnapshotKey, ScopedEnclosingSnapshotId, UseDefMap}; use crate::semantic_model::HasTrackedScope; +use crate::{Db, Module, resolve_module}; pub mod ast_ids; mod builder; @@ -75,20 +76,73 @@ pub(crate) fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc(db: &'db dyn Db, file: File) -> Arc> { semantic_index(db, file).imported_modules.clone() } +/// Returns the set of relative submodules that are explicitly imported anywhere in +/// `importing_module`. +/// +/// This set only considers `from...import` statements (but it could also include `import`). +/// It also only returns a non-empty result for `__init__.pyi` files. +/// See [`ModuleLiteralType::available_submodule_attributes`] for discussion +/// of why this analysis is intentionally limited. +/// +/// This function specifically implements the rule that if an `__init__.pyi` file +/// contains a `from...import` that imports a direct submodule of the package, +/// that submodule should be available as an attribute of the package. +/// +/// While we endeavour to accurately model import side-effects for `.py` files, we intentionally +/// limit them for `.pyi` files to encourage more intentional API design. The standard escape +/// hatches for this are the `import x as x` idiom or listing them in `__all__`, but in practice +/// some other idioms are popular. +/// +/// In particular, many packages have their `__init__` include lines like +/// `from . import subpackage`, with the intent that `mypackage.subpackage` should be +/// available for anyone who only does `import mypackage`. +#[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] +pub(crate) fn imported_relative_submodules_of_stub_package<'db>( + db: &'db dyn Db, + importing_module: Module<'db>, +) -> Box<[ModuleName]> { + let Some(file) = importing_module.file(db) else { + return Box::default(); + }; + if !file.is_package_stub(db) { + return Box::default(); + } + semantic_index(db, file) + .maybe_imported_modules + .iter() + .filter_map(|import| { + let mut submodule = ModuleName::from_identifier_parts( + db, + file, + import.from_module.as_deref(), + import.level, + ) + .ok()?; + // We only actually care if this is a direct submodule of the package + // so this part should actually be exactly the importing module. + let importing_module_name = importing_module.name(db); + if importing_module_name != &submodule { + return None; + } + submodule.extend(&ModuleName::new(import.submodule.as_str())?); + // Throw out the result if this doesn't resolve to an actual module. + // This is quite expensive, but we've gone through a lot of hoops to + // get here so it won't happen too much. + resolve_module(db, &submodule)?; + // Return only the relative part + submodule.relative_to(importing_module_name) + }) + .collect() +} + /// Returns the use-def map for a specific `scope`. /// /// Using [`use_def_map`] over [`semantic_index`] has the advantage that @@ -230,6 +284,9 @@ pub(crate) struct SemanticIndex<'db> { /// The set of modules that are imported anywhere within this file. imported_modules: Arc>, + /// `from...import` statements within this file that might import a submodule. + maybe_imported_modules: FxHashSet, + /// Flags about the global scope (code usage impacting inference) has_future_annotations: bool, @@ -243,6 +300,16 @@ pub(crate) struct SemanticIndex<'db> { generator_functions: FxHashSet, } +/// A `from...import` that may be an import of a module +/// +/// Later analysis will determine if it is. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, get_size2::GetSize)] +pub(crate) struct MaybeModuleImport { + level: u32, + from_module: Option, + submodule: Name, +} + impl<'db> SemanticIndex<'db> { /// Returns the place table for a specific scope. /// diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 8107f9c122..5645fed7d4 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -47,7 +47,9 @@ use crate::semantic_index::symbol::{ScopedSymbolId, Symbol}; use crate::semantic_index::use_def::{ EnclosingSnapshotKey, FlowSnapshot, ScopedEnclosingSnapshotId, UseDefMapBuilder, }; -use crate::semantic_index::{ExpressionsScopeMap, SemanticIndex, VisibleAncestorsIter}; +use crate::semantic_index::{ + ExpressionsScopeMap, MaybeModuleImport, SemanticIndex, VisibleAncestorsIter, +}; use crate::semantic_model::HasTrackedScope; use crate::unpack::{EvaluationMode, Unpack, UnpackKind, UnpackPosition, UnpackValue}; use crate::{Db, Program}; @@ -111,6 +113,7 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> { definitions_by_node: FxHashMap>, expressions_by_node: FxHashMap>, imported_modules: FxHashSet, + maybe_imported_modules: FxHashSet, /// Hashset of all [`FileScopeId`]s that correspond to [generator functions]. /// /// [generator functions]: https://docs.python.org/3/glossary.html#term-generator @@ -148,6 +151,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { definitions_by_node: FxHashMap::default(), expressions_by_node: FxHashMap::default(), + maybe_imported_modules: FxHashSet::default(), imported_modules: FxHashSet::default(), generator_functions: FxHashSet::default(), @@ -1262,6 +1266,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.scopes_by_node.shrink_to_fit(); self.generator_functions.shrink_to_fit(); self.enclosing_snapshots.shrink_to_fit(); + self.maybe_imported_modules.shrink_to_fit(); SemanticIndex { place_tables, @@ -1274,6 +1279,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { scopes_by_node: self.scopes_by_node, use_def_maps, imported_modules: Arc::new(self.imported_modules), + maybe_imported_modules: self.maybe_imported_modules, has_future_annotations: self.has_future_annotations, enclosing_snapshots: self.enclosing_snapshots, semantic_syntax_errors: self.semantic_syntax_errors.into_inner(), @@ -1558,6 +1564,15 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { (&alias.name.id, false) }; + // If there's no alias or a redundant alias, record this as a potential import of a submodule + if alias.asname.is_none() || is_reexported { + self.maybe_imported_modules.insert(MaybeModuleImport { + level: node.level, + from_module: node.module.clone().map(Into::into), + submodule: alias.name.clone().into(), + }); + } + // Look for 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__` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 6b48499e9b..be3816ac12 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -39,7 +39,9 @@ use crate::place::{ use crate::semantic_index::definition::{Definition, DefinitionKind}; use crate::semantic_index::place::ScopedPlaceId; use crate::semantic_index::scope::ScopeId; -use crate::semantic_index::{imported_modules, place_table, semantic_index}; +use crate::semantic_index::{ + imported_modules, imported_relative_submodules_of_stub_package, place_table, semantic_index, +}; use crate::suppression::check_suppressions; use crate::types::bound_super::BoundSuperType; use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding}; @@ -10830,11 +10832,68 @@ impl<'db> ModuleLiteralType<'db> { self._importing_file(db) } + /// Get the submodule attributes we believe to be defined on this module. + /// + /// Note that `ModuleLiteralType` is per-importing-file, so this analysis + /// includes "imports the importing file has performed". + /// + /// + /// # Danger! Powerful Hammer! + /// + /// These results immediately make the attribute always defined in the importing file, + /// shadowing any other attribute in the module with the same name, even if the + /// non-submodule-attribute is in fact always the one defined in practice. + /// + /// Intuitively this means `available_submodule_attributes` "win all tie-breaks", + /// with the idea that if we're ever confused about complicated code then usually + /// the import is the thing people want in scope. + /// + /// However this "always defined, always shadows" rule if applied too aggressively + /// creates VERY confusing conclusions that break perfectly reasonable code. + /// + /// For instance, consider a package which has a `myfunc` submodule which defines a + /// `myfunc` function (a common idiom). If the package "re-exports" this function + /// (`from .myfunc import myfunc`), then at runtime in python + /// `from mypackage import myfunc` should import the function and not the submodule. + /// + /// However, if we were to consider `from mypackage import myfunc` as introducing + /// the attribute `mypackage.myfunc` in `available_submodule_attributes`, we would + /// fail to ever resolve the function. This is because `available_submodule_attributes` + /// is *so early* and *so powerful* in our analysis that **this conclusion would be + /// used when actually resolving `from mypackage import myfunc`**! + /// + /// This currently cannot be fixed by considering the actual symbols defined in `mypackage`, + /// because `available_submodule_attributes` is an *input* to that analysis. + /// + /// We should therefore avoid marking something as an `available_submodule_attribute` + /// when the import could be importing a non-submodule (a function, class, or value). + /// + /// + /// # Rules + /// + /// We have two rules for whether a submodule attribute is defined: + /// + /// * If the importing file include `import x.y` then `x.y` is defined in the importing file. + /// This is an easy rule to justify because `import` can only ever import a module, and so + /// *should* shadow any non-submodule of the same name. + /// + /// * If the module is an `__init__.pyi` for `mypackage`, and it contains a `from...import` + /// that normalizes to `from mypackage import submodule`, then `mypackage.submodule` is + /// defined in all files. This supports the `from . import submodule` idiom. Critically, + /// we do *not* allow `from mypackage.nested import submodule` to affect `mypackage`. + /// The idea here is that `from mypackage import submodule` *from mypackage itself* can + /// only ever reasonably be an import of a submodule. It doesn't make any sense to import + /// a function or class from yourself! (You *can* do it but... why? Don't? Please?) fn available_submodule_attributes(&self, db: &'db dyn Db) -> impl Iterator { self.importing_file(db) .into_iter() .flat_map(|file| imported_modules(db, file)) .filter_map(|submodule_name| submodule_name.relative_to(self.module(db).name(db))) + .chain( + imported_relative_submodules_of_stub_package(db, self.module(db)) + .iter() + .cloned(), + ) .filter_map(|relative_submodule| relative_submodule.components().next().map(Name::from)) } From 3179b052215260e1c573b67d7d48afe20135c994 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 31 Oct 2025 10:49:59 -0400 Subject: [PATCH 093/188] [ty] don't assume in diagnostic messages that a TypedDict key error is about subscript access (#21166) ## Summary Before this PR, we would emit diagnostics like "Invalid key access" for a TypedDict literal with invalid key, which doesn't make sense since there's no "access" in that case. This PR just adjusts the wording to be more general, and adjusts the documentation of the lint rule too. I noticed this in the playground and thought it would be a quick fix. As usual, it turned out to be a bit more subtle than I expected, but for now I chose to punt on the complexity. We may ultimately want to have different rules for invalid subscript vs invalid TypedDict literal, because an invalid key in a TypedDict literal is low severity: it's a typo detector, but not actually a type error. But then there's another wrinkle there: if the TypedDict is `closed=True`, then it _is_ a type error. So would we want to separate the open and closed cases into separate rules, too? I decided to leave this as a question for future. If we wanted to use separate rules, or use specific wording for each case instead of the generalized wording I chose here, that would also involve a bit of extra work to distinguish the cases, since we use a generic set of functions for reporting these errors. ## Test Plan Added and updated mdtests. --- crates/ty/docs/rules.md | 110 ++++++++++-------- ...ict`_-_Diagnostics_(e5289abf5c570c29).snap | 74 +++++++++--- .../resources/mdtest/typed_dict.md | 48 ++++---- .../src/types/diagnostic.rs | 18 ++- ty.schema.json | 4 +- 5 files changed, 155 insertions(+), 99 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 858f1f0c7c..4218eee1af 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -474,7 +474,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -501,7 +501,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -529,7 +529,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -563,7 +563,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 @@ -599,7 +599,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -623,7 +623,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -650,7 +650,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -679,7 +679,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -723,7 +723,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -762,11 +762,15 @@ Added in 0 **What it does** -Checks for subscript accesses with invalid keys. +Checks for subscript accesses with invalid keys and `TypedDict` construction with an +unknown key. **Why is this bad?** -Using an invalid key will raise a `KeyError` at runtime. +Subscripting with an invalid key will raise a `KeyError` at runtime. + +Creating a `TypedDict` with an unknown key is likely a mistake; if the `TypedDict` is +`closed=true` it also violates the expectations of the type. **Examples** @@ -779,6 +783,10 @@ class Person(TypedDict): alice = Person(name="Alice", age=30) alice["height"] # KeyError: 'height' + +bob: Person = { "name": "Bob", "age": 30 } # typo! + +carol = Person(name="Carol", age=25) # typo! ``` ## `invalid-legacy-type-variable` @@ -787,7 +795,7 @@ alice["height"] # KeyError: 'height' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -822,7 +830,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -888,7 +896,7 @@ TypeError: can only inherit from a NamedTuple type and Generic Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -938,7 +946,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -998,7 +1006,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 @@ -1047,7 +1055,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1072,7 +1080,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1130,7 +1138,7 @@ TODO #14889 Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1157,7 +1165,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1187,7 +1195,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1217,7 +1225,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 @@ -1251,7 +1259,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1285,7 +1293,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1320,7 +1328,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1345,7 +1353,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 @@ -1378,7 +1386,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1407,7 +1415,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1431,7 +1439,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1457,7 +1465,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 @@ -1484,7 +1492,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1542,7 +1550,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1572,7 +1580,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 @@ -1601,7 +1609,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1628,7 +1636,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1656,7 +1664,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1702,7 +1710,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1729,7 +1737,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 @@ -1757,7 +1765,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1782,7 +1790,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1807,7 +1815,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1844,7 +1852,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1872,7 +1880,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2026,7 +2034,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2086,7 +2094,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2118,7 +2126,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2145,7 +2153,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2169,7 +2177,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2227,7 +2235,7 @@ def g(): Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2266,7 +2274,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2353,7 +2361,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap index b80700fa08..155b4ea618 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap @@ -37,20 +37,24 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md 23 | 24 | def write_to_non_literal_string_key(person: Person, str_key: str): 25 | person[str_key] = "Alice" # error: [invalid-key] -26 | from typing_extensions import ReadOnly -27 | -28 | class Employee(TypedDict): -29 | id: ReadOnly[int] -30 | name: str +26 | +27 | def create_with_invalid_string_key(): +28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key] +29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key] +30 | from typing_extensions import ReadOnly 31 | -32 | def write_to_readonly_key(employee: Employee): -33 | employee["id"] = 42 # error: [invalid-assignment] +32 | class Employee(TypedDict): +33 | id: ReadOnly[int] +34 | name: str +35 | +36 | def write_to_readonly_key(employee: Employee): +37 | employee["id"] = 42 # error: [invalid-assignment] ``` # Diagnostics ``` -error[invalid-key]: Invalid key access on TypedDict `Person` +error[invalid-key]: Invalid key for TypedDict `Person` --> src/mdtest_snippet.py:8:5 | 7 | def access_invalid_literal_string_key(person: Person): @@ -66,7 +70,7 @@ info: rule `invalid-key` is enabled by default ``` ``` -error[invalid-key]: Invalid key access on TypedDict `Person` +error[invalid-key]: Invalid key for TypedDict `Person` --> src/mdtest_snippet.py:13:5 | 12 | def access_invalid_key(person: Person): @@ -82,7 +86,7 @@ info: rule `invalid-key` is enabled by default ``` ``` -error[invalid-key]: TypedDict `Person` cannot be indexed with a key of type `str` +error[invalid-key]: Invalid key for TypedDict `Person` of type `str` --> src/mdtest_snippet.py:16:12 | 15 | def access_with_str_key(person: Person, str_key: str): @@ -123,7 +127,7 @@ info: rule `invalid-assignment` is enabled by default ``` ``` -error[invalid-key]: Invalid key access on TypedDict `Person` +error[invalid-key]: Invalid key for TypedDict `Person` --> src/mdtest_snippet.py:22:5 | 21 | def write_to_non_existing_key(person: Person): @@ -145,7 +149,39 @@ error[invalid-key]: Cannot access `Person` with a key of type `str`. Only string 24 | def write_to_non_literal_string_key(person: Person, str_key: str): 25 | person[str_key] = "Alice" # error: [invalid-key] | ^^^^^^^ -26 | from typing_extensions import ReadOnly +26 | +27 | def create_with_invalid_string_key(): + | +info: rule `invalid-key` is enabled by default + +``` + +``` +error[invalid-key]: Invalid key for TypedDict `Person` + --> src/mdtest_snippet.py:28:21 + | +27 | def create_with_invalid_string_key(): +28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key] + | -----------------------------^^^^^^^^^-------- + | | | + | | Unknown key "unknown" + | TypedDict `Person` +29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key] +30 | from typing_extensions import ReadOnly + | +info: rule `invalid-key` is enabled by default + +``` + +``` +error[invalid-key]: Invalid key for TypedDict `Person` + --> src/mdtest_snippet.py:29:11 + | +27 | def create_with_invalid_string_key(): +28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key] +29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key] + | ------ TypedDict `Person` ^^^^^^^^^^^^^ Unknown key "unknown" +30 | from typing_extensions import ReadOnly | info: rule `invalid-key` is enabled by default @@ -153,21 +189,21 @@ info: rule `invalid-key` is enabled by default ``` error[invalid-assignment]: Cannot assign to key "id" on TypedDict `Employee` - --> src/mdtest_snippet.py:33:5 + --> src/mdtest_snippet.py:37:5 | -32 | def write_to_readonly_key(employee: Employee): -33 | employee["id"] = 42 # error: [invalid-assignment] +36 | def write_to_readonly_key(employee: Employee): +37 | employee["id"] = 42 # error: [invalid-assignment] | -------- ^^^^ key is marked read-only | | | TypedDict `Employee` | info: Item declaration - --> src/mdtest_snippet.py:29:5 + --> src/mdtest_snippet.py:33:5 | -28 | class Employee(TypedDict): -29 | id: ReadOnly[int] +32 | class Employee(TypedDict): +33 | id: ReadOnly[int] | ----------------- Read-only item declared here -30 | name: str +34 | name: str | info: rule `invalid-assignment` 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 d810a79efe..042d6317a2 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -29,7 +29,7 @@ alice: Person = {"name": "Alice", "age": 30} reveal_type(alice["name"]) # revealed: str reveal_type(alice["age"]) # revealed: int | None -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing"" reveal_type(alice["non_existing"]) # revealed: Unknown ``` @@ -41,7 +41,7 @@ bob = Person(name="Bob", age=25) reveal_type(bob["name"]) # revealed: str reveal_type(bob["age"]) # revealed: int | None -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing"" reveal_type(bob["non_existing"]) # revealed: Unknown ``` @@ -69,7 +69,7 @@ def name_or_age() -> Literal["name", "age"]: carol: Person = {NAME: "Carol", AGE: 20} reveal_type(carol[NAME]) # revealed: str -# error: [invalid-key] "TypedDict `Person` cannot be indexed with a key of type `str`" +# error: [invalid-key] "Invalid key for TypedDict `Person` of type `str`" reveal_type(carol[non_literal()]) # revealed: Unknown reveal_type(carol[name_or_age()]) # revealed: str | int | None @@ -81,7 +81,7 @@ def _(): CAPITALIZED_NAME = "Name" -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "Name" - did you mean "name"?" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "Name" - did you mean "name"?" # error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor" dave: Person = {CAPITALIZED_NAME: "Dave", "age": 20} @@ -104,9 +104,9 @@ eve2a: Person = {"age": 22} # error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor" eve2b = Person(age=22) -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" eve3a: Person = {"name": "Eve", "age": 25, "extra": True} -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" eve3b = Person(name="Eve", age=25, extra=True) ``` @@ -157,10 +157,10 @@ bob["name"] = None Assignments to non-existing keys are disallowed: ```py -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" alice["extra"] = True -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" bob["extra"] = True ``` @@ -185,10 +185,10 @@ alice: Person = {"inner": {"name": "Alice", "age": 30}} reveal_type(alice["inner"]["name"]) # revealed: str reveal_type(alice["inner"]["age"]) # revealed: int | None -# error: [invalid-key] "Invalid key access on TypedDict `Inner`: Unknown key "non_existing"" +# error: [invalid-key] "Invalid key for TypedDict `Inner`: Unknown key "non_existing"" reveal_type(alice["inner"]["non_existing"]) # revealed: Unknown -# error: [invalid-key] "Invalid key access on TypedDict `Inner`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Inner`: Unknown key "extra"" alice: Person = {"inner": {"name": "Alice", "age": 30, "extra": 1}} ``` @@ -267,22 +267,22 @@ a_person = {"name": None, "age": 30} All of these have an extra field that is not defined in the `TypedDict`: ```py -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" alice4: Person = {"name": "Alice", "age": 30, "extra": True} -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" Person(name="Alice", age=30, extra=True) -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" Person({"name": "Alice", "age": 30, "extra": True}) -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" accepts_person({"name": "Alice", "age": 30, "extra": True}) # TODO: this should be an error house.owner = {"name": "Alice", "age": 30, "extra": True} a_person: Person -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" a_person = {"name": "Alice", "age": 30, "extra": True} -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" (a_person := {"name": "Alice", "age": 30, "extra": True}) ``` @@ -323,7 +323,7 @@ user2 = User({"name": "Bob"}) # error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `User`: value of type `None`" user3 = User({"name": None, "age": 25}) -# error: [invalid-key] "Invalid key access on TypedDict `User`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `User`: Unknown key "extra"" user4 = User({"name": "Charlie", "age": 30, "extra": True}) ``` @@ -360,7 +360,7 @@ invalid = OptionalPerson(name=123) Extra fields are still not allowed, even with `total=False`: ```py -# error: [invalid-key] "Invalid key access on TypedDict `OptionalPerson`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `OptionalPerson`: Unknown key "extra"" invalid_extra = OptionalPerson(name="George", extra=True) ``` @@ -503,10 +503,10 @@ def _(person: Person, literal_key: Literal["age"], union_of_keys: Literal["age", reveal_type(person[union_of_keys]) # revealed: int | None | str - # error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing"" + # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing"" reveal_type(person["non_existing"]) # revealed: Unknown - # error: [invalid-key] "TypedDict `Person` cannot be indexed with a key of type `str`" + # error: [invalid-key] "Invalid key for TypedDict `Person` of type `str`" reveal_type(person[str_key]) # revealed: Unknown # No error here: @@ -530,7 +530,7 @@ def _(person: Person): person["name"] = "Alice" person["age"] = 30 - # error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "naem" - did you mean "name"?" + # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "naem" - did you mean "name"?" person["naem"] = "Alice" def _(person: Person): @@ -646,7 +646,7 @@ def _(p: Person) -> None: reveal_type(p.setdefault("name", "Alice")) # revealed: str reveal_type(p.setdefault("extra", "default")) # revealed: str - # error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extraz" - did you mean "extra"?" + # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extraz" - did you mean "extra"?" reveal_type(p.setdefault("extraz", "value")) # revealed: Unknown ``` @@ -1015,6 +1015,10 @@ def write_to_non_existing_key(person: Person): def write_to_non_literal_string_key(person: Person, str_key: str): person[str_key] = "Alice" # error: [invalid-key] + +def create_with_invalid_string_key(): + alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key] + bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key] ``` Assignment to `ReadOnly` keys: diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 7db83b9b88..2dd75e57aa 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -572,10 +572,14 @@ declare_lint! { // Added in #19763. declare_lint! { /// ## What it does - /// Checks for subscript accesses with invalid keys. + /// Checks for subscript accesses with invalid keys and `TypedDict` construction with an + /// unknown key. /// /// ## Why is this bad? - /// Using an invalid key will raise a `KeyError` at runtime. + /// Subscripting with an invalid key will raise a `KeyError` at runtime. + /// + /// Creating a `TypedDict` with an unknown key is likely a mistake; if the `TypedDict` is + /// `closed=true` it also violates the expectations of the type. /// /// ## Examples /// ```python @@ -587,9 +591,13 @@ declare_lint! { /// /// alice = Person(name="Alice", age=30) /// alice["height"] # KeyError: 'height' + /// + /// bob: Person = { "name": "Bob", "age": 30 } # typo! + /// + /// carol = Person(name="Carol", age=25) # typo! /// ``` pub(crate) static INVALID_KEY = { - summary: "detects invalid subscript accesses", + summary: "detects invalid subscript accesses or TypedDict literal keys", status: LintStatus::stable("0.0.1-alpha.17"), default_level: Level::Error, } @@ -2966,7 +2974,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( let typed_dict_name = typed_dict_ty.display(db); let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid key access on TypedDict `{typed_dict_name}`", + "Invalid key for TypedDict `{typed_dict_name}`", )); diagnostic.annotate( @@ -2989,7 +2997,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( diagnostic } _ => builder.into_diagnostic(format_args!( - "TypedDict `{}` cannot be indexed with a key of type `{}`", + "Invalid key for TypedDict `{}` of type `{}`", typed_dict_ty.display(db), key_ty.display(db), )), diff --git a/ty.schema.json b/ty.schema.json index 270241fb28..55d5bdf996 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -584,8 +584,8 @@ ] }, "invalid-key": { - "title": "detects invalid subscript accesses", - "description": "## What it does\nChecks for subscript accesses with invalid keys.\n\n## Why is this bad?\nUsing an invalid key will raise a `KeyError` at runtime.\n\n## Examples\n```python\nfrom typing import TypedDict\n\nclass Person(TypedDict):\n name: str\n age: int\n\nalice = Person(name=\"Alice\", age=30)\nalice[\"height\"] # KeyError: 'height'\n```", + "title": "detects invalid subscript accesses or TypedDict literal keys", + "description": "## What it does\nChecks for subscript accesses with invalid keys and `TypedDict` construction with an\nunknown key.\n\n## Why is this bad?\nSubscripting with an invalid key will raise a `KeyError` at runtime.\n\nCreating a `TypedDict` with an unknown key is likely a mistake; if the `TypedDict` is\n`closed=true` it also violates the expectations of the type.\n\n## Examples\n```python\nfrom typing import TypedDict\n\nclass Person(TypedDict):\n name: str\n age: int\n\nalice = Person(name=\"Alice\", age=30)\nalice[\"height\"] # KeyError: 'height'\n\nbob: Person = { \"name\": \"Bob\", \"age\": 30 } # typo!\n\ncarol = Person(name=\"Carol\", age=25) # typo!\n```", "default": "error", "oneOf": [ { From 1baf98aab3e07355c62390668865c54be6258f5a Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Fri, 31 Oct 2025 10:50:54 -0400 Subject: [PATCH 094/188] [ty] Fix `is_disjoint_from` with `@final` classes (#21167) ## Summary We currently perform a subtyping check instead of the intended subclass check (and the subtyping check is confusingly named `is_subclass_of`). This showed up in https://github.com/astral-sh/ruff/pull/21070. --- .../type_properties/is_disjoint_from.md | 25 +++++++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 11 +++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index d80a2b5b82..dfad076726 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -87,6 +87,31 @@ static_assert(is_disjoint_from(memoryview, Foo)) static_assert(is_disjoint_from(type[memoryview], type[Foo])) ``` +## Specialized `@final` types + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import final +from ty_extensions import static_assert, is_disjoint_from + +@final +class Foo[T]: + def get(self) -> T: + raise NotImplementedError + +class A: ... +class B: ... + +static_assert(not is_disjoint_from(Foo[A], Foo[B])) + +# TODO: `int` and `str` are disjoint bases, so these should be disjoint. +static_assert(not is_disjoint_from(Foo[int], Foo[str])) +``` + ## "Disjoint base" builtin types Most other builtins can be subclassed and can even be used in multiple inheritance. However, builtin diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 4f8ee4c1fc..c3ff51e47f 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -637,12 +637,17 @@ impl<'db> ClassType<'db> { return true; } - // Optimisation: if either class is `@final`, we only need to do one `is_subclass_of` call. if self.is_final(db) { - return self.is_subclass_of(db, other); + return self + .iter_mro(db) + .filter_map(ClassBase::into_class) + .any(|class| class.class_literal(db).0 == other.class_literal(db).0); } if other.is_final(db) { - return other.is_subclass_of(db, self); + return other + .iter_mro(db) + .filter_map(ClassBase::into_class) + .any(|class| class.class_literal(db).0 == self.class_literal(db).0); } // Two disjoint bases can only coexist in an MRO if one is a subclass of the other. From cf4e82d4b0ea4087b91ef3aade1159127689ca85 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 31 Oct 2025 10:53:37 -0400 Subject: [PATCH 095/188] [ty] Add and test when constraint sets are satisfied by their typevars (#21129) This PR adds a new `satisfied_by_all_typevar` method, which implements one of the final steps of actually using these dang constraint sets. Constraint sets exist to help us check assignability and subtyping of types in the presence of typevars. We construct a constraint set describing the conditions under which assignability holds between the two types. Then we check whether that constraint set is satisfied for the valid specializations of the relevant typevars (which is this new method). We also add a new `ty_extensions.ConstraintSet` method so that we can test this method's behavior in mdtests, before hooking it up to the rest of the specialization inference machinery. --- .../satisfied_by_all_typevars.md | 220 ++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 57 ++++- .../ty_python_semantic/src/types/call/bind.rs | 44 +++- .../src/types/constraints.rs | 107 ++++++++- .../ty_python_semantic/src/types/display.rs | 3 + .../ty_extensions/ty_extensions.pyi | 10 + 6 files changed, 425 insertions(+), 16 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md b/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md new file mode 100644 index 0000000000..8d9f563250 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md @@ -0,0 +1,220 @@ +# Constraint set satisfaction + +```toml +[environment] +python-version = "3.12" +``` + +Constraint sets exist to help us check assignability and subtyping of types in the presence of +typevars. We construct a constraint set describing the conditions under which assignability holds +between the two types. Then we check whether that constraint set is satisfied for the valid +specializations of the relevant typevars. This file tests that final step. + +## Inferable vs non-inferable typevars + +Typevars can appear in _inferable_ or _non-inferable_ positions. + +When a typevar is in an inferable position, the constraint set only needs to be satisfied for _some_ +valid specialization. The most common inferable position occurs when invoking a generic function: +all of the function's typevars are inferable, because we want to use the argument types to infer +which specialization is being invoked. + +When a typevar is in a non-inferable position, the constraint set must be satisfied for _every_ +valid specialization. The most common non-inferable position occurs in the body of a generic +function or class: here we don't know in advance what type the typevar will be specialized to, and +so we have to ensure that the body is valid for all possible specializations. + +```py +def f[T](t: T) -> T: + # In the function body, T is non-inferable. All assignability checks involving T must be + # satisfied for _all_ valid specializations of T. + return t + +# When invoking the function, T is inferable — we attempt to infer a specialization that is valid +# for the particular arguments that are passed to the function. Assignability checks (in particular, +# that the argument type is assignable to the parameter type) only need to succeed for _at least +# one_ specialization. +f(1) +``` + +In all of the examples below, for ease of reproducibility, we explicitly list the typevars that are +inferable in each `satisfied_by_all_typevars` call; any typevar not listed is assumed to be +non-inferable. + +## Unbounded typevar + +If a typevar has no bound or constraints, then it can specialize to any type. In an inferable +position, that means we just need a single type (any type at all!) that satisfies the constraint +set. In a non-inferable position, that means the constraint set must be satisfied for every possible +type. + +```py +from typing import final, Never +from ty_extensions import ConstraintSet, static_assert + +class Super: ... +class Base(Super): ... +class Sub(Base): ... + +@final +class Unrelated: ... + +def unbounded[T](): + static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.always().satisfied_by_all_typevars()) + + static_assert(not ConstraintSet.never().satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(not ConstraintSet.never().satisfied_by_all_typevars()) + + # (T = Never) is a valid specialization, which satisfies (T ≤ Unrelated). + static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + # (T = Base) is a valid specialization, which does not satisfy (T ≤ Unrelated). + static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + + # (T = Base) is a valid specialization, which satisfies (T ≤ Super). + static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T])) + # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Super). + static_assert(not ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars()) + + # (T = Base) is a valid specialization, which satisfies (T ≤ Base). + static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Base). + static_assert(not ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + + # (T = Sub) is a valid specialization, which satisfies (T ≤ Sub). + static_assert(ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) + # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Sub). + static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars()) +``` + +## 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. +For an inferable typevar, that means we need a single type that satisfies both the constraint set +and the upper bound. For a non-inferable typevar, that means the constraint set must be satisfied +for every type that satisfies the upper bound. + +```py +from typing import final, Never +from ty_extensions import ConstraintSet, static_assert + +class Super: ... +class Base(Super): ... +class Sub(Base): ... + +@final +class Unrelated: ... + +def bounded[T: Base](): + static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.always().satisfied_by_all_typevars()) + + static_assert(not ConstraintSet.never().satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(not ConstraintSet.never().satisfied_by_all_typevars()) + + # (T = Base) is a valid specialization, which satisfies (T ≤ Super). + static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T])) + # Every valid specialization satisfies (T ≤ Base). Since (Base ≤ Super), every valid + # specialization also satisfies (T ≤ Super). + static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars()) + + # (T = Base) is a valid specialization, which satisfies (T ≤ Base). + static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + # Every valid specialization satisfies (T ≤ Base). + static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + + # (T = Sub) is a valid specialization, which satisfies (T ≤ Sub). + static_assert(ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) + # (T = Base) is a valid specialization, which does not satisfy (T ≤ Sub). + static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars()) + + # (T = Never) is a valid specialization, which satisfies (T ≤ Unrelated). + constraints = ConstraintSet.range(Never, T, Unrelated) + static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) + # (T = Base) is a valid specialization, which does not satisfy (T ≤ Unrelated). + static_assert(not constraints.satisfied_by_all_typevars()) + + # Never is the only type that satisfies both (T ≤ Base) and (T ≤ Unrelated). So there is no + # valid specialization that satisfies (T ≤ Unrelated ∧ T ≠ Never). + constraints = constraints & ~ConstraintSet.range(Never, T, Never) + static_assert(not constraints.satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(not constraints.satisfied_by_all_typevars()) +``` + +## 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!) For an inferable typevar, that means we need the constraint set to +be satisfied by any one of the constraints. For a non-inferable typevar, that means we need the +constraint set to be satisfied by all of those constraints. + +```py +from typing import final, Never +from ty_extensions import ConstraintSet, static_assert + +class Super: ... +class Base(Super): ... +class Sub(Base): ... + +@final +class Unrelated: ... + +def constrained[T: (Base, Unrelated)](): + static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.always().satisfied_by_all_typevars()) + + static_assert(not ConstraintSet.never().satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(not ConstraintSet.never().satisfied_by_all_typevars()) + + # (T = Unrelated) is a valid specialization, which satisfies (T ≤ Unrelated). + static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + # (T = Base) is a valid specialization, which does not satisfy (T ≤ Unrelated). + static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + + # (T = Base) is a valid specialization, which satisfies (T ≤ Super). + static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T])) + # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Super). + static_assert(not ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars()) + + # (T = Base) is a valid specialization, which satisfies (T ≤ Base). + static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Base). + static_assert(not ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + + # Neither (T = Base) nor (T = Unrelated) satisfy (T ≤ Sub). + static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars()) + + # (T = Base) and (T = Unrelated) both satisfy (T ≤ Super ∨ T ≤ Unrelated). + constraints = ConstraintSet.range(Never, T, Super) | ConstraintSet.range(Never, T, Unrelated) + static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(constraints.satisfied_by_all_typevars()) + + # (T = Base) and (T = Unrelated) both satisfy (T ≤ Base ∨ T ≤ Unrelated). + constraints = ConstraintSet.range(Never, T, Base) | ConstraintSet.range(Never, T, Unrelated) + static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(constraints.satisfied_by_all_typevars()) + + # (T = Unrelated) is a valid specialization, which satisfies (T ≤ Sub ∨ T ≤ Unrelated). + constraints = ConstraintSet.range(Never, T, Sub) | ConstraintSet.range(Never, T, Unrelated) + static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) + # (T = Base) is a valid specialization, which does not satisfy (T ≤ Sub ∨ T ≤ Unrelated). + static_assert(not constraints.satisfied_by_all_typevars()) + + # (T = Unrelated) is a valid specialization, which satisfies (T = Super ∨ T = Unrelated). + constraints = ConstraintSet.range(Super, T, Super) | ConstraintSet.range(Unrelated, T, Unrelated) + static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) + # (T = Base) is a valid specialization, which does not satisfy (T = Super ∨ T = Unrelated). + static_assert(not constraints.satisfied_by_all_typevars()) + + # (T = Base) and (T = Unrelated) both satisfy (T = Base ∨ T = Unrelated). + constraints = ConstraintSet.range(Base, T, Base) | ConstraintSet.range(Unrelated, T, Unrelated) + static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(constraints.satisfied_by_all_typevars()) + + # (T = Unrelated) is a valid specialization, which satisfies (T = Sub ∨ T = Unrelated). + constraints = ConstraintSet.range(Sub, T, Sub) | ConstraintSet.range(Unrelated, T, Unrelated) + static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) + # (T = Base) is a valid specialization, which does not satisfy (T = Sub ∨ T = Unrelated). + static_assert(not constraints.satisfied_by_all_typevars()) +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index be3816ac12..a4eb563e6a 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4161,6 +4161,14 @@ impl<'db> Type<'db> { )) .into() } + Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked)) + if name == "satisfied_by_all_typevars" => + { + Place::bound(Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(tracked), + )) + .into() + } Type::ClassLiteral(class) if name == "__get__" && class.is_known(db, KnownClass::FunctionType) => @@ -6923,6 +6931,7 @@ impl<'db> Type<'db> { | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) ) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_) @@ -7074,7 +7083,8 @@ impl<'db> Type<'db> { | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), ) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_) @@ -10339,6 +10349,7 @@ pub enum KnownBoundMethodType<'db> { ConstraintSetAlways, ConstraintSetNever, ConstraintSetImpliesSubtypeOf(TrackedConstraintSet<'db>), + ConstraintSetSatisfiedByAllTypeVars(TrackedConstraintSet<'db>), } pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -10366,7 +10377,8 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => {} + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {} } } @@ -10434,6 +10446,10 @@ impl<'db> KnownBoundMethodType<'db> { | ( KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), + ) + | ( + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), ) => ConstraintSet::from(true), ( @@ -10446,7 +10462,8 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), KnownBoundMethodType::FunctionTypeDunderGet(_) | KnownBoundMethodType::FunctionTypeDunderCall(_) | KnownBoundMethodType::PropertyDunderGet(_) @@ -10456,7 +10473,8 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), ) => ConstraintSet::from(false), } } @@ -10509,6 +10527,10 @@ impl<'db> KnownBoundMethodType<'db> { ( KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(left_constraints), KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(right_constraints), + ) + | ( + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(left_constraints), + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(right_constraints), ) => left_constraints .constraints(db) .iff(db, right_constraints.constraints(db)), @@ -10523,7 +10545,8 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), KnownBoundMethodType::FunctionTypeDunderGet(_) | KnownBoundMethodType::FunctionTypeDunderCall(_) | KnownBoundMethodType::PropertyDunderGet(_) @@ -10533,7 +10556,8 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), ) => ConstraintSet::from(false), } } @@ -10557,7 +10581,8 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => self, + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => self, } } @@ -10573,7 +10598,10 @@ impl<'db> KnownBoundMethodType<'db> { KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => KnownClass::ConstraintSet, + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => { + KnownClass::ConstraintSet + } } } @@ -10712,6 +10740,19 @@ impl<'db> KnownBoundMethodType<'db> { Some(KnownClass::ConstraintSet.to_instance(db)), ))) } + + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => { + Either::Right(std::iter::once(Signature::new( + Parameters::new([Parameter::keyword_only(Name::new_static("inferable")) + .type_form() + .with_annotated_type(UnionType::from_elements( + db, + [Type::homogeneous_tuple(db, Type::any()), Type::none(db)], + )) + .with_default_type(Type::none(db))]), + Some(KnownClass::Bool.to_instance(db)), + ))) + } } } } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 1b4629b301..b0a5cc1b91 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -9,6 +9,7 @@ use std::fmt; use itertools::{Either, Itertools}; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; +use rustc_hash::FxHashSet; use smallvec::{SmallVec, smallvec, smallvec_inline}; use super::{Argument, CallArguments, CallError, CallErrorKind, InferContext, Signature, Type}; @@ -35,9 +36,10 @@ use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Paramete use crate::types::tuple::{TupleLength, TupleType}; use crate::types::{ BoundMethodType, ClassLiteral, DataclassFlags, DataclassParams, FieldInstance, - KnownBoundMethodType, KnownClass, KnownInstanceType, MemberLookupPolicy, PropertyInstanceType, - SpecialFormType, TrackedConstraintSet, TypeAliasType, TypeContext, UnionBuilder, UnionType, - WrapperDescriptorKind, enums, ide_support, infer_isolated_expression, todo_type, + KnownBoundMethodType, KnownClass, KnownInstanceType, MemberLookupPolicy, NominalInstanceType, + PropertyInstanceType, SpecialFormType, TrackedConstraintSet, TypeAliasType, TypeContext, + UnionBuilder, UnionType, WrapperDescriptorKind, enums, ide_support, infer_isolated_expression, + todo_type, }; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity}; use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion}; @@ -1174,6 +1176,42 @@ impl<'db> Bindings<'db> { )); } + Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(tracked), + ) => { + let extract_inferable = |instance: &NominalInstanceType<'db>| { + if instance.has_known_class(db, KnownClass::NoneType) { + // Caller explicitly passed None, so no typevars are inferable. + return Some(FxHashSet::default()); + } + instance + .tuple_spec(db)? + .fixed_elements() + .map(|ty| { + ty.as_typevar() + .map(|bound_typevar| bound_typevar.identity(db)) + }) + .collect() + }; + + let inferable = match overload.parameter_types() { + // Caller did not provide argument, so no typevars are inferable. + [None] => FxHashSet::default(), + [Some(Type::NominalInstance(instance))] => { + match extract_inferable(instance) { + Some(inferable) => inferable, + None => continue, + } + } + _ => continue, + }; + + let result = tracked + .constraints(db) + .satisfied_by_all_typevars(db, InferableTypeVars::One(&inferable)); + overload.set_return_type(Type::BooleanLiteral(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 ef7632ff2e..ee66cd85f3 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -65,7 +65,10 @@ use salsa::plumbing::AsId; use crate::Db; use crate::types::generics::InferableTypeVars; -use crate::types::{BoundTypeVarInstance, IntersectionType, Type, TypeRelation, UnionType}; +use crate::types::{ + BoundTypeVarInstance, IntersectionType, Type, TypeRelation, TypeVarBoundOrConstraints, + UnionType, +}; /// An extension trait for building constraint sets from [`Option`] values. pub(crate) trait OptionConstraintsExtension { @@ -256,6 +259,28 @@ impl<'db> ConstraintSet<'db> { } } + /// Returns whether this constraint set is satisfied by all of the typevars that it mentions. + /// + /// Each typevar has a set of _valid specializations_, which is defined by any upper bound or + /// constraints that the typevar has. + /// + /// Each typevar is also either _inferable_ or _non-inferable_. (You provide a list of the + /// `inferable` typevars; all others are considered non-inferable.) For an inferable typevar, + /// then there must be _some_ valid specialization that satisfies the constraint set. For a + /// non-inferable typevar, then _all_ valid specializations must satisfy it. + /// + /// Note that we don't have to consider typevars that aren't mentioned in the constraint set, + /// 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( + self, + db: &'db dyn Db, + inferable: InferableTypeVars<'_, 'db>, + ) -> bool { + self.node.satisfied_by_all_typevars(db, inferable) + } + /// Updates this constraint set to hold the union of itself and another constraint set. pub(crate) fn union(&mut self, db: &'db dyn Db, other: Self) -> Self { self.node = self.node.or(db, other.node); @@ -746,6 +771,13 @@ impl<'db> Node<'db> { .or(db, self.negate(db).and(db, else_node)) } + fn satisfies(self, db: &'db dyn Db, other: Self) -> Self { + let simplified_self = self.simplify(db); + let implication = simplified_self.implies(db, other); + let (simplified, domain) = implication.simplify_and_domain(db); + simplified.and(db, domain) + } + fn when_subtype_of_given( self, db: &'db dyn Db, @@ -767,10 +799,48 @@ impl<'db> Node<'db> { _ => return lhs.when_subtype_of(db, rhs, inferable).node, }; - let simplified_self = self.simplify(db); - let implication = simplified_self.implies(db, constraint); - let (simplified, domain) = implication.simplify_and_domain(db); - simplified.and(db, domain) + self.satisfies(db, constraint) + } + + fn satisfied_by_all_typevars( + self, + db: &'db dyn Db, + inferable: InferableTypeVars<'_, 'db>, + ) -> bool { + match self { + 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)); + }); + + for typevar in typevars { + // Determine which valid specializations of this typevar satisfy the constraint set. + let valid_specializations = typevar.valid_specializations(db).node; + let when_satisfied = valid_specializations + .satisfies(db, self) + .and(db, valid_specializations); + let satisfied = if typevar.is_inferable(db, inferable) { + // If the typevar is inferable, then we only need one valid specialization to + // satisfy the constraint set. + !when_satisfied.is_never_satisfied() + } else { + // If the typevar is non-inferable, then we need _all_ valid specializations to + // satisfy the constraint set. + when_satisfied + .iff(db, valid_specializations) + .is_always_satisfied(db) + }; + if !satisfied { + return false; + } + } + + true } /// Returns a new BDD that returns the same results as `self`, but with some inputs fixed to @@ -1861,6 +1931,33 @@ impl<'db> SatisfiedClauses<'db> { } } +/// Returns a constraint set describing the valid specializations of a typevar. +impl<'db> BoundTypeVarInstance<'db> { + pub(crate) fn valid_specializations(self, db: &'db dyn Db) -> ConstraintSet<'db> { + match self.typevar(db).bound_or_constraints(db) { + None => ConstraintSet::from(true), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => ConstraintSet::constrain_typevar( + db, + self, + Type::Never, + bound, + TypeRelation::Assignability, + ), + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + constraints.elements(db).iter().when_any(db, |constraint| { + ConstraintSet::constrain_typevar( + db, + self, + *constraint, + *constraint, + TypeRelation::Assignability, + ) + }) + } + } + } +} + #[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 7748dd3ab5..8500c142e8 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -535,6 +535,9 @@ impl Display for DisplayRepresentation<'_> { Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)) => { f.write_str("bound method `ConstraintSet.implies_subtype_of`") } + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars( + _, + )) => f.write_str("bound method `ConstraintSet.satisfied_by_all_typevars`"), Type::WrapperDescriptor(kind) => { let (method, object) = match kind { WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"), diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi index 79cda64bef..d23554f0ae 100644 --- a/crates/ty_vendored/ty_extensions/ty_extensions.pyi +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi @@ -67,6 +67,16 @@ class ConstraintSet: .. _subtype: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence """ + def satisfied_by_all_typevars( + self, *, inferable: tuple[Any, ...] | None = None + ) -> bool: + """ + Returns whether this constraint set is satisfied by all of the typevars + that it mentions. You must provide a tuple of the typevars that should + be considered `inferable`. All other typevars mentioned in the + constraint set will be considered non-inferable. + """ + def __bool__(self) -> bool: ... def __eq__(self, other: ConstraintSet) -> bool: ... def __ne__(self, other: ConstraintSet) -> bool: ... From 1d6ae8596a0acd2d84582a2c1cb29db4e89d505f Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Fri, 31 Oct 2025 10:58:09 -0400 Subject: [PATCH 096/188] [ty] Prefer exact matches when solving constrained type variables (#21165) ## Summary The solver is currently order-dependent, and will choose a supertype over the exact type if it appears earlier in the list of constraints. We could be smarter and try to choose the most precise subtype, but I imagine this is something the new constraint solver will fix anyways, and this fixes the issue showing up on https://github.com/astral-sh/ruff/pull/21070. --- .../mdtest/generics/legacy/functions.md | 25 +++++++++++++++++++ .../ty_python_semantic/src/types/generics.rs | 8 ++++++ 2 files changed, 33 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index 9745fdca21..2bbe85b5ec 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -545,3 +545,28 @@ def f(x: T, y: Not[T]) -> T: y = x # error: [invalid-assignment] return x ``` + +## Prefer exact matches for constrained typevars + +```py +from typing import TypeVar + +class Base: ... +class Sub(Base): ... + +# We solve to `Sub`, regardless of the order of constraints. +T = TypeVar("T", Base, Sub) +T2 = TypeVar("T2", Sub, Base) + +def f(x: T) -> list[T]: + return [x] + +def f2(x: T2) -> list[T2]: + return [x] + +x: list[Sub] = f(Sub()) +reveal_type(x) # revealed: list[Sub] + +y: list[Sub] = f2(Sub()) +reveal_type(y) # revealed: list[Sub] +``` diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 59216ca607..8485931ff2 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1483,6 +1483,14 @@ impl<'db> SpecializationBuilder<'db> { self.add_type_mapping(bound_typevar, ty); } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + // Prefer an exact match first. + for constraint in constraints.elements(self.db) { + if ty == *constraint { + self.add_type_mapping(bound_typevar, ty); + return Ok(()); + } + } + for constraint in constraints.elements(self.db) { if ty .when_assignable_to(self.db, *constraint, self.inferable) From 0c2cf7586903040436237b03aebc5bc9f0c62735 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Oct 2025 16:00:30 +0100 Subject: [PATCH 097/188] [ty] Do not promote literals in contravariant position (#21164) ## Summary closes https://github.com/astral-sh/ty/issues/1463 ## Test Plan Regression tests --- .../resources/mdtest/literal_promotion.md | 32 ++++++++++ crates/ty_python_semantic/src/types.rs | 60 ++++++++++++++----- .../src/types/signatures.rs | 15 ++--- 3 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/literal_promotion.md diff --git a/crates/ty_python_semantic/resources/mdtest/literal_promotion.md b/crates/ty_python_semantic/resources/mdtest/literal_promotion.md new file mode 100644 index 0000000000..726ca59d20 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/literal_promotion.md @@ -0,0 +1,32 @@ +# Literal promotion + +There are certain places where we promote literals to their common supertype: + +```py +reveal_type([1, 2, 3]) # revealed: list[Unknown | int] +reveal_type({"a", "b", "c"}) # revealed: set[Unknown | str] +``` + +This promotion should not take place if the literal type appears in contravariant position: + +```py +from typing import Callable, Literal + +def in_negated_position(non_zero_number: int): + if non_zero_number == 0: + raise ValueError() + + reveal_type(non_zero_number) # revealed: int & ~Literal[0] + + reveal_type([non_zero_number]) # revealed: list[Unknown | (int & ~Literal[0])] + +def in_parameter_position(callback: Callable[[Literal[1]], None]): + reveal_type(callback) # revealed: (Literal[1], /) -> None + + reveal_type([callback]) # revealed: list[Unknown | ((Literal[1], /) -> None)] + +def double_negation(callback: Callable[[Callable[[Literal[1]], None]], None]): + reveal_type(callback) # revealed: ((Literal[1], /) -> None, /) -> None + + reveal_type([callback]) # revealed: list[Unknown | (((int, /) -> None, /) -> None)] +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index a4eb563e6a..be2fb264d8 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1270,7 +1270,11 @@ impl<'db> Type<'db> { /// /// It also avoids literal promotion if a literal type annotation was provided as type context. pub(crate) fn promote_literals(self, db: &'db dyn Db, tcx: TypeContext<'db>) -> Type<'db> { - self.apply_type_mapping(db, &TypeMapping::PromoteLiterals, tcx) + self.apply_type_mapping( + db, + &TypeMapping::PromoteLiterals(PromoteLiteralsMode::On), + tcx, + ) } /// Like [`Type::promote_literals`], but does not recurse into nested types. @@ -6765,7 +6769,7 @@ impl<'db> Type<'db> { self } } - TypeMapping::PromoteLiterals + TypeMapping::PromoteLiterals(_) | TypeMapping::ReplaceParameterDefaults | TypeMapping::BindLegacyTypevars(_) => self, TypeMapping::Materialize(materialization_kind) => { @@ -6779,7 +6783,7 @@ impl<'db> Type<'db> { } TypeMapping::Specialization(_) | TypeMapping::PartialSpecialization(_) | - TypeMapping::PromoteLiterals | + TypeMapping::PromoteLiterals(_) | TypeMapping::BindSelf(_) | TypeMapping::ReplaceSelf { .. } | TypeMapping::Materialize(_) | @@ -6790,7 +6794,7 @@ impl<'db> Type<'db> { let function = Type::FunctionLiteral(function.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); match type_mapping { - TypeMapping::PromoteLiterals => function.promote_literals_impl(db, tcx), + TypeMapping::PromoteLiterals(PromoteLiteralsMode::On) => function.promote_literals_impl(db, tcx), _ => function } } @@ -6867,13 +6871,9 @@ impl<'db> Type<'db> { builder = builder.add_positive(positive.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); } - let flipped_mapping = match type_mapping { - TypeMapping::Materialize(materialization_kind) => &TypeMapping::Materialize(materialization_kind.flip()), - _ => type_mapping, - }; for negative in intersection.negative(db) { builder = - builder.add_negative(negative.apply_type_mapping_impl(db, flipped_mapping, tcx, visitor)); + builder.add_negative(negative.apply_type_mapping_impl(db, &type_mapping.flip(), tcx, visitor)); } builder.build() } @@ -6902,8 +6902,9 @@ impl<'db> Type<'db> { TypeMapping::BindSelf(_) | TypeMapping::ReplaceSelf { .. } | TypeMapping::Materialize(_) | - TypeMapping::ReplaceParameterDefaults => self, - TypeMapping::PromoteLiterals => self.promote_literals_impl(db, tcx) + TypeMapping::ReplaceParameterDefaults | + TypeMapping::PromoteLiterals(PromoteLiteralsMode::Off) => self, + TypeMapping::PromoteLiterals(PromoteLiteralsMode::On) => self.promote_literals_impl(db, tcx) } Type::Dynamic(_) => match type_mapping { @@ -6912,7 +6913,7 @@ impl<'db> Type<'db> { TypeMapping::BindLegacyTypevars(_) | TypeMapping::BindSelf(_) | TypeMapping::ReplaceSelf { .. } | - TypeMapping::PromoteLiterals | + TypeMapping::PromoteLiterals(_) | TypeMapping::ReplaceParameterDefaults => self, TypeMapping::Materialize(materialization_kind) => match materialization_kind { MaterializationKind::Top => Type::object(), @@ -7456,6 +7457,21 @@ fn apply_specialization_cycle_initial<'db>( Type::Never } +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] +pub enum PromoteLiteralsMode { + On, + Off, +} + +impl PromoteLiteralsMode { + const fn flip(self) -> Self { + match self { + PromoteLiteralsMode::On => PromoteLiteralsMode::Off, + PromoteLiteralsMode::Off => PromoteLiteralsMode::On, + } + } +} + /// A mapping that can be applied to a type, producing another type. This is applied inductively to /// the components of complex types. /// @@ -7470,7 +7486,7 @@ pub enum TypeMapping<'a, 'db> { PartialSpecialization(PartialSpecialization<'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, + PromoteLiterals(PromoteLiteralsMode), /// Binds a legacy typevar with the generic context (class, function, type alias) that it is /// being used in. BindLegacyTypevars(BindingContext<'db>), @@ -7495,7 +7511,7 @@ impl<'db> TypeMapping<'_, 'db> { match self { TypeMapping::Specialization(_) | TypeMapping::PartialSpecialization(_) - | TypeMapping::PromoteLiterals + | TypeMapping::PromoteLiterals(_) | TypeMapping::BindLegacyTypevars(_) | TypeMapping::Materialize(_) | TypeMapping::ReplaceParameterDefaults => context, @@ -7521,6 +7537,22 @@ impl<'db> TypeMapping<'_, 'db> { ), } } + + /// Returns a new `TypeMapping` that should be applied in contravariant positions. + pub(crate) fn flip(&self) -> Self { + match self { + TypeMapping::Materialize(materialization_kind) => { + TypeMapping::Materialize(materialization_kind.flip()) + } + TypeMapping::PromoteLiterals(mode) => TypeMapping::PromoteLiterals(mode.flip()), + TypeMapping::Specialization(_) + | TypeMapping::PartialSpecialization(_) + | TypeMapping::BindLegacyTypevars(_) + | TypeMapping::BindSelf(_) + | TypeMapping::ReplaceSelf { .. } + | TypeMapping::ReplaceParameterDefaults => self.clone(), + } + } } /// A Salsa-tracked constraint set. This is only needed to have something appropriately small to diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index b0ff205e48..11979100bb 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -509,20 +509,17 @@ impl<'db> Signature<'db> { tcx: TypeContext<'db>, visitor: &ApplyTypeMappingVisitor<'db>, ) -> Self { - let flipped_mapping = match type_mapping { - TypeMapping::Materialize(materialization_kind) => { - &TypeMapping::Materialize(materialization_kind.flip()) - } - _ => type_mapping, - }; Self { generic_context: self .generic_context .map(|context| type_mapping.update_signature_generic_context(db, context)), definition: self.definition, - parameters: self - .parameters - .apply_type_mapping_impl(db, flipped_mapping, tcx, visitor), + parameters: self.parameters.apply_type_mapping_impl( + db, + &type_mapping.flip(), + tcx, + visitor, + ), return_ty: self .return_ty .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)), From 9d7da914b9c7fdc11f3334f45fb00a58c70b0bd2 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Oct 2025 18:10:14 +0300 Subject: [PATCH 098/188] Improve `extend` docs (#21135) Co-authored-by: Micha Reiser --- crates/ruff_workspace/src/options.rs | 15 +++++++++++---- ruff.schema.json | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 47ee0fe738..708d6dcf0b 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -59,13 +59,20 @@ pub struct Options { )] pub cache_dir: Option, - /// A path to a local `pyproject.toml` file to merge into this + /// A path to a local `pyproject.toml` or `ruff.toml` file to merge into this /// configuration. User home directory and environment variables will be /// expanded. /// - /// To resolve the current `pyproject.toml` file, Ruff will first resolve - /// this base configuration file, then merge in any properties defined - /// in the current configuration file. + /// To resolve the current configuration file, Ruff will first load + /// this base configuration file, then merge in properties defined + /// in the current configuration file. Most settings follow simple override + /// behavior where the child value replaces the parent value. However, + /// rule selection (`lint.select` and `lint.ignore`) has special merging + /// behavior: if the child configuration specifies `lint.select`, it + /// establishes a new baseline rule set and the parent's `lint.ignore` + /// rules are discarded; if the child configuration omits `lint.select`, + /// the parent's rule selection is inherited and both parent and child + /// `lint.ignore` rules are accumulated together. #[option( default = r#"null"#, value_type = "str", diff --git a/ruff.schema.json b/ruff.schema.json index 04ef3fcc3d..a16e91fbd7 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -71,7 +71,7 @@ "deprecated": true }, "extend": { - "description": "A path to a local `pyproject.toml` file to merge into this\nconfiguration. User home directory and environment variables will be\nexpanded.\n\nTo resolve the current `pyproject.toml` file, Ruff will first resolve\nthis base configuration file, then merge in any properties defined\nin the current configuration file.", + "description": "A path to a local `pyproject.toml` or `ruff.toml` file to merge into this\nconfiguration. User home directory and environment variables will be\nexpanded.\n\nTo resolve the current configuration file, Ruff will first load\nthis base configuration file, then merge in properties defined\nin the current configuration file. Most settings follow simple override\nbehavior where the child value replaces the parent value. However,\nrule selection (`lint.select` and `lint.ignore`) has special merging\nbehavior: if the child configuration specifies `lint.select`, it\nestablishes a new baseline rule set and the parent's `lint.ignore`\nrules are discarded; if the child configuration omits `lint.select`,\nthe parent's rule selection is inherited and both parent and child\n`lint.ignore` rules are accumulated together.", "type": [ "string", "null" From 1d111c878085eed772315aa0fa440b78c4977ed0 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 31 Oct 2025 11:12:06 -0400 Subject: [PATCH 099/188] [ty] prefer declared type on invalid TypedDict creation (#21168) ## Summary In general, when we have an invalid assignment (inferred assigned type is not assignable to declared type), we fall back to inferring the declared type, since the declared type is a more explicit declaration of the programmer's intent. This also maintains the invariant that our inferred type for a name is always assignable to the declared type for that same name. For example: ```py x: str = 1 reveal_type(x) # revealed: str ``` We weren't following this pattern for dictionary literals inferred (via type context) as a typed dictionary; if the literal was not valid for the annotated TypedDict type, we would just fall back to the normal inferred type of the dict literal, effectively ignoring the annotation, and resulting in inferred type not assignable to declared type. ## Test Plan Added mdtest assertions. --- .../resources/mdtest/typed_dict.md | 9 +++++++++ .../ty_python_semantic/src/types/infer/builder.rs | 10 ++++------ crates/ty_python_semantic/src/types/typed_dict.rs | 15 ++++----------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 042d6317a2..8be6de4ef3 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -99,15 +99,24 @@ eve1a: Person = {"name": b"Eve", "age": None} # error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`" eve1b = Person(name=b"Eve", age=None) +reveal_type(eve1a) # revealed: Person +reveal_type(eve1b) # revealed: Person + # error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor" eve2a: Person = {"age": 22} # error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor" eve2b = Person(age=22) +reveal_type(eve2a) # revealed: Person +reveal_type(eve2b) # revealed: Person + # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" eve3a: Person = {"name": "Eve", "age": 25, "extra": True} # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" eve3b = Person(name="Eve", age=25, extra=True) + +reveal_type(eve3a) # revealed: Person +reveal_type(eve3b) # revealed: Person ``` Also, the value types ​​declared in a `TypedDict` affect generic call inference: diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index edf8581bcd..ea3f739f22 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -6103,9 +6103,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && let Some(typed_dict) = tcx .filter_union(self.db(), Type::is_typed_dict) .as_typed_dict() - && let Some(ty) = self.infer_typed_dict_expression(dict, typed_dict) { - return ty; + self.infer_typed_dict_expression(dict, typed_dict); + return Type::TypedDict(typed_dict); } // Avoid false positives for the functional `TypedDict` form, which is currently @@ -6130,7 +6130,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, dict: &ast::ExprDict, typed_dict: TypedDictType<'db>, - ) -> Option> { + ) { let ast::ExprDict { range: _, node_index: _, @@ -6153,9 +6153,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { validate_typed_dict_dict_literal(&self.context, typed_dict, dict, dict.into(), |expr| { self.expression_type(expr) - }) - .ok() - .map(|_| Type::TypedDict(typed_dict)) + }); } // Infer the type of a collection literal expression. diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index e29b836d8a..632d2a2933 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -389,7 +389,7 @@ fn validate_from_keywords<'db, 'ast>( provided_keys } -/// Validates a `TypedDict` dictionary literal assignment, +/// Validates a `TypedDict` dictionary literal assignment, emitting any needed diagnostics. /// e.g. `person: Person = {"name": "Alice", "age": 30}` pub(super) fn validate_typed_dict_dict_literal<'db>( context: &InferContext<'db, '_>, @@ -397,8 +397,7 @@ pub(super) fn validate_typed_dict_dict_literal<'db>( dict_expr: &ast::ExprDict, error_node: AnyNodeRef, expression_type_fn: impl Fn(&ast::Expr) -> Type<'db>, -) -> Result, OrderSet<&'db str>> { - let mut valid = true; +) { let mut provided_keys = OrderSet::new(); // Validate each key-value pair in the dictionary literal @@ -411,7 +410,7 @@ pub(super) fn validate_typed_dict_dict_literal<'db>( let value_type = expression_type_fn(&item.value); - valid &= validate_typed_dict_key_assignment( + validate_typed_dict_key_assignment( context, typed_dict, key_str, @@ -424,11 +423,5 @@ pub(super) fn validate_typed_dict_dict_literal<'db>( } } - valid &= validate_typed_dict_required_keys(context, typed_dict, &provided_keys, error_node); - - if valid { - Ok(provided_keys) - } else { - Err(provided_keys) - } + validate_typed_dict_required_keys(context, typed_dict, &provided_keys, error_node); } From b93d8f2b9fa834ab7c672d801c725c0031d9408e Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Oct 2025 18:16:09 +0300 Subject: [PATCH 100/188] [`refurb`] Preserve argument ordering in autofix (`FURB103`) (#20790) Fixes https://github.com/astral-sh/ruff/issues/20785 --- .../resources/test/fixtures/refurb/FURB103.py | 8 +++++++ .../rules/refurb/rules/write_whole_file.rs | 1 - ...es__refurb__tests__FURB103_FURB103.py.snap | 11 +++++++++ ...rb__tests__preview_FURB103_FURB103.py.snap | 23 ++++++++++++++++++- ...rb__tests__write_whole_file_python_39.snap | 11 +++++++++ crates/ruff_python_ast/src/nodes.rs | 2 +- 6 files changed, 53 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB103.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB103.py index b6d8e1d034..35d9600d41 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB103.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB103.py @@ -145,3 +145,11 @@ with open("file.txt", "w") as f: with open("file.txt", "w") as f: for line in text: f.write(line) + +# See: https://github.com/astral-sh/ruff/issues/20785 +import json + +data = {"price": 100} + +with open("test.json", "wb") as f: + f.write(json.dumps(data, indent=4).encode("utf-8")) \ No newline at end of file diff --git a/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs b/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs index bbee6dcb5a..da99733efd 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs @@ -5,7 +5,6 @@ use ruff_python_ast::{ relocate::relocate_expr, visitor::{self, Visitor}, }; - use ruff_python_codegen::Generator; use ruff_text_size::{Ranged, TextRange}; diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103.py.snap index dfb111341e..74f3749953 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103.py.snap @@ -134,3 +134,14 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(fo 75 | f.write(foobar) | help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")` + +FURB103 `open` and `write` should be replaced by `Path("test.json")....` + --> FURB103.py:154:6 + | +152 | data = {"price": 100} +153 | +154 | with open("test.json", "wb") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +155 | f.write(json.dumps(data, indent=4).encode("utf-8")) + | +help: Replace with `Path("test.json")....` diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview_FURB103_FURB103.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview_FURB103_FURB103.py.snap index eef0992839..8148035435 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview_FURB103_FURB103.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview_FURB103_FURB103.py.snap @@ -257,4 +257,25 @@ help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")` 75 + pathlib.Path("file.txt").write_text(foobar, newline="\r\n") 76 | 77 | # Non-errors. -78 | +78 | + +FURB103 [*] `open` and `write` should be replaced by `Path("test.json")....` + --> FURB103.py:154:6 + | +152 | data = {"price": 100} +153 | +154 | with open("test.json", "wb") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +155 | f.write(json.dumps(data, indent=4).encode("utf-8")) + | +help: Replace with `Path("test.json")....` +148 | +149 | # See: https://github.com/astral-sh/ruff/issues/20785 +150 | import json +151 + import pathlib +152 | +153 | data = {"price": 100} +154 | + - with open("test.json", "wb") as f: + - f.write(json.dumps(data, indent=4).encode("utf-8")) +155 + pathlib.Path("test.json").write_bytes(json.dumps(data, indent=4).encode("utf-8")) diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__write_whole_file_python_39.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__write_whole_file_python_39.snap index 81eea0c159..140a274468 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__write_whole_file_python_39.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__write_whole_file_python_39.snap @@ -104,3 +104,14 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(ba 51 | # writes a single time to file and that bit they can replace. | help: Replace with `Path("file.txt").write_text(bar(bar(a + x)))` + +FURB103 `open` and `write` should be replaced by `Path("test.json")....` + --> FURB103.py:154:6 + | +152 | data = {"price": 100} +153 | +154 | with open("test.json", "wb") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +155 | f.write(json.dumps(data, indent=4).encode("utf-8")) + | +help: Replace with `Path("test.json")....` diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index f71f420d09..5cb58e7f05 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -3372,7 +3372,7 @@ impl Arguments { pub fn arguments_source_order(&self) -> impl Iterator> { let args = self.args.iter().map(ArgOrKeyword::Arg); let keywords = self.keywords.iter().map(ArgOrKeyword::Keyword); - args.merge_by(keywords, |left, right| left.start() < right.start()) + args.merge_by(keywords, |left, right| left.start() <= right.start()) } pub fn inner_range(&self) -> TextRange { From bb40c3436120defba5cf72da9c771d585f7f62ae Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Fri, 31 Oct 2025 11:48:28 -0400 Subject: [PATCH 101/188] [ty] Use declared attribute types as type context (#21143) ## Summary For example: ```py class X: x: list[int | str] def _(x: X): x.x = [1] ``` Resolves https://github.com/astral-sh/ty/issues/1375. --- .../resources/mdtest/bidirectional.md | 112 +++++++- .../resources/mdtest/call/union.md | 43 --- .../src/types/infer/builder.rs | 256 ++++++++++++++---- 3 files changed, 304 insertions(+), 107 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index 627492855f..1cc3dba162 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -185,12 +185,12 @@ Declared attribute types: ```py class E: - e: list[Literal[1]] + a: list[Literal[1]] + b: list[Literal[1]] def _(e: E): - # TODO: Implement attribute type context. - # error: [invalid-assignment] "Object of type `list[Unknown | int]` is not assignable to attribute `e` of type `list[Literal[1]]`" - e.e = [1] + e.a = [1] + E.b = [1] ``` Function return types: @@ -200,6 +200,41 @@ def f() -> list[Literal[1]]: return [1] ``` +## Instance attribute + +```toml +[environment] +python-version = "3.12" +``` + +Both meta and class/instance attribute annotations are used as type context: + +```py +from typing import Literal, Any + +class DataDescriptor: + def __get__(self, instance: object, owner: type | None = None) -> list[Literal[1]]: + return [] + + def __set__(self, instance: object, value: list[Literal[1]]) -> None: + pass + +def lst[T](x: T) -> list[T]: + return [x] + +def _(flag: bool): + class Meta(type): + if flag: + x: DataDescriptor = DataDescriptor() + + class C(metaclass=Meta): + x: list[int | None] + + def _(c: C): + c.x = lst(1) + C.x = lst(1) +``` + ## Class constructor parameters ```toml @@ -226,3 +261,72 @@ A(f(1)) # error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `list[int | None]`, found `list[list[Unknown]]`" A(f([])) ``` + +## Multi-inference diagnostics + +```toml +[environment] +python-version = "3.12" +``` + +Diagnostics unrelated to the type-context are only reported once: + +`call.py`: + +```py +def f[T](x: T) -> list[T]: + return [x] + +def a(x: list[bool], y: list[bool]): ... +def b(x: list[int], y: list[int]): ... +def c(x: list[int], y: list[int]): ... +def _(x: int): + if x == 0: + y = a + elif x == 1: + y = b + else: + y = c + + if x == 0: + z = True + + y(f(True), [True]) + + # error: [possibly-unresolved-reference] "Name `z` used when possibly not defined" + y(f(True), [z]) +``` + +`call_standalone_expression.py`: + +```py +def f(_: str): ... +def g(_: str): ... +def _(a: object, b: object, flag: bool): + if flag: + x = f + else: + x = g + + # error: [unsupported-operator] "Operator `>` is not supported for types `object` and `object`" + x(f"{'a' if a > b else 'b'}") +``` + +`attribute_assignment.py`: + +```py +from typing import TypedDict + +class TD(TypedDict): + y: int + +class X: + td: TD + +def _(x: X, flag: bool): + if flag: + y = 1 + + # error: [possibly-unresolved-reference] "Name `y` used when possibly not defined" + x.td = {"y": y} +``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md index 1a4079204d..69695c3f5c 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/union.md +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md @@ -281,46 +281,3 @@ def _(flag: bool): # we currently consider `TypedDict` instances to be subtypes of `dict` f({"y": 1}) ``` - -Diagnostics unrelated to the type-context are only reported once: - -`expression.py`: - -```py -def f[T](x: T) -> list[T]: - return [x] - -def a(x: list[bool], y: list[bool]): ... -def b(x: list[int], y: list[int]): ... -def c(x: list[int], y: list[int]): ... -def _(x: int): - if x == 0: - y = a - elif x == 1: - y = b - else: - y = c - - if x == 0: - z = True - - y(f(True), [True]) - - # error: [possibly-unresolved-reference] "Name `z` used when possibly not defined" - y(f(True), [z]) -``` - -`standalone_expression.py`: - -```py -def f(_: str): ... -def g(_: str): ... -def _(a: object, b: object, flag: bool): - if flag: - x = f - else: - x = g - - # error: [unsupported-operator] "Operator `>` is not supported for types `object` and `object`" - x(f"{'a' if a > b else 'b'}") -``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index ea3f739f22..f58f093a44 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2924,12 +2924,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for item in items { let target = item.optional_vars.as_deref(); if let Some(target) = target { - self.infer_target(target, &item.context_expr, |builder| { + 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, TypeContext::default()) + .infer_standalone_expression(&item.context_expr, tcx) .enter(builder.db()) }); } else { @@ -3393,8 +3393,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } = assignment; for target in targets { - self.infer_target(target, value, |builder| { - builder.infer_standalone_expression(value, TypeContext::default()) + self.infer_target(target, value, |builder, tcx| { + builder.infer_standalone_expression(value, tcx) }); } } @@ -3410,13 +3410,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// `target`. fn infer_target(&mut self, target: &ast::Expr, value: &ast::Expr, infer_value_expr: F) where - F: Fn(&mut Self) -> Type<'db>, + F: Fn(&mut Self, TypeContext<'db>) -> Type<'db>, { - let assigned_ty = match target { - ast::Expr::Name(_) => None, - _ => Some(infer_value_expr(self)), - }; - self.infer_target_impl(target, value, assigned_ty); + match target { + ast::Expr::Name(_) => { + self.infer_target_impl(target, value, None); + } + + _ => self.infer_target_impl( + target, + value, + Some(&|builder, tcx| infer_value_expr(builder, tcx)), + ), + } } /// Make sure that the subscript assignment `obj[slice] = value` is valid. @@ -3568,30 +3574,68 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { target: &ast::ExprAttribute, object_ty: Type<'db>, attribute: &str, - value_ty: Type<'db>, + infer_value_ty: &dyn Fn(&mut Self, TypeContext<'db>) -> Type<'db>, emit_diagnostics: bool, ) -> bool { let db = self.db(); - let ensure_assignable_to = |attr_ty| -> bool { - let assignable = value_ty.is_assignable_to(db, attr_ty); - if !assignable && emit_diagnostics { - report_invalid_attribute_assignment( - &self.context, - target.into(), - attr_ty, - value_ty, - attribute, - ); - } - assignable + 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 = mem::replace( + &mut builder.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 + }; + // Return true (and emit a diagnostic) if this is an invalid assignment to a `Final` attribute. - let invalid_assignment_to_final = |qualifiers: TypeQualifiers| -> bool { + let invalid_assignment_to_final = |builder: &Self, qualifiers: TypeQualifiers| -> bool { if qualifiers.contains(TypeQualifiers::FINAL) { if emit_diagnostics { - if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) { + if let Some(builder) = builder.context.report_lint(&INVALID_ASSIGNMENT, target) + { builder.into_diagnostic(format_args!( "Cannot assign to final attribute `{attribute}` \ on type `{}`", @@ -3607,8 +3651,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match object_ty { Type::Union(union) => { + // TODO: We could perform multi-inference here with each element of the union as type context. + let value_ty = infer_value_ty(self, TypeContext::default()); + if union.elements(self.db()).iter().all(|elem| { - self.validate_attribute_assignment(target, *elem, attribute, value_ty, false) + self.validate_attribute_assignment( + target, + *elem, + attribute, + &|_, _| value_ty, + false, + ) }) { true } else { @@ -3631,9 +3684,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } Type::Intersection(intersection) => { + // TODO: We could perform multi-inference here with each element of the union as type context. + 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, value_ty, false) + self.validate_attribute_assignment( + target, + *elem, + attribute, + &|_, _| value_ty, + false, + ) }) { true } else { @@ -3657,12 +3719,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { target, alias.value_type(self.db()), attribute, - value_ty, + 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 { if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) { builder.into_diagnostic(format_args!( @@ -3674,6 +3738,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { false } Type::BoundSuper(_) => { + infer_value_ty(self, TypeContext::default()); + if emit_diagnostics { if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) { builder.into_diagnostic(format_args!( @@ -3685,7 +3751,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { false } - Type::Dynamic(..) | Type::Never => true, + Type::Dynamic(..) | Type::Never => { + infer_value_ty(self, TypeContext::default()); + true + } Type::NominalInstance(..) | Type::ProtocolInstance(_) @@ -3710,6 +3779,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::AlwaysFalsy | Type::TypeIs(_) | Type::TypedDict(_) => { + // 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()); + // First, try to call the `__setattr__` dunder method. If this is present/defined, overrides // assigning the attributed by the normal mechanism. let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy( @@ -3811,7 +3884,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { place: Place::Defined(meta_attr_ty, _, meta_attr_boundness), qualifiers, } => { - if invalid_assignment_to_final(qualifiers) { + if invalid_assignment_to_final(self, qualifiers) { return false; } @@ -3819,6 +3892,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Place::Defined(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([ @@ -3844,7 +3919,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { dunder_set_result.is_ok() } else { - ensure_assignable_to(meta_attr_ty) + 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 @@ -3857,12 +3937,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } = object_ty.instance_member(db, attribute) { - if invalid_assignment_to_final(qualifiers) { + 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(instance_attr_ty), + ensure_assignable_to(self, value_ty, instance_attr_ty), instance_attr_boundness, ) } else { @@ -3896,7 +3980,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { qualifiers, } = object_ty.instance_member(db, attribute) { - if invalid_assignment_to_final(qualifiers) { + let value_ty = infer_value_ty( + self, + TypeContext::new(Some(instance_attr_ty)), + ); + if invalid_assignment_to_final(self, qualifiers) { return false; } @@ -3909,7 +3997,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } - ensure_assignable_to(instance_attr_ty) + ensure_assignable_to(self, value_ty, instance_attr_ty) } else { if emit_diagnostics { if let Some(builder) = @@ -3937,13 +4025,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { place: Place::Defined(meta_attr_ty, _, meta_attr_boundness), qualifiers, } => { - if invalid_assignment_to_final(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(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]), @@ -3963,7 +4057,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { dunder_set_result.is_ok() } else { - ensure_assignable_to(meta_attr_ty) + 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 @@ -3976,7 +4072,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .expect("called on Type::ClassLiteral or Type::SubclassOf") .place { - (ensure_assignable_to(class_attr_ty), class_attr_boundness) + 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) }; @@ -4008,7 +4109,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .find_name_in_mro(db, attribute) .expect("called on Type::ClassLiteral or Type::SubclassOf") { - if invalid_assignment_to_final(qualifiers) { + let value_ty = + infer_value_ty(self, TypeContext::new(Some(class_attr_ty))); + if invalid_assignment_to_final(self, qualifiers) { return false; } @@ -4021,8 +4124,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } - ensure_assignable_to(class_attr_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 @@ -4064,6 +4169,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::ModuleLiteral(module) => { if let Place::Defined(attr_ty, _, _) = module.static_member(db, attribute).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 @@ -4080,6 +4187,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { false } } else { + infer_value_ty(self, TypeContext::default()); + if emit_diagnostics { if let Some(builder) = self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) @@ -4098,22 +4207,35 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + #[expect(clippy::type_complexity)] fn infer_target_impl( &mut self, target: &ast::Expr, value: &ast::Expr, - assigned_ty: Option>, + infer_assigned_ty: Option<&dyn Fn(&mut Self, TypeContext<'db>) -> Type<'db>>, ) { match target { - ast::Expr::Name(name) => self.infer_definition(name), + 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::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 mut assigned_tys = tuple_spec.all_elements(); - for element in elts { - self.infer_target_impl(element, value, assigned_tys.next().copied()); + let assigned_tys = tuple_spec.all_elements().copied().collect::>(); + + 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 { @@ -4129,29 +4251,39 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .. }, ) => { - self.store_expression_type(target, assigned_ty.unwrap_or(Type::unknown())); - let object_ty = self.infer_expression(object, TypeContext::default()); - if let Some(assigned_ty) = assigned_ty { + if let Some(infer_assigned_ty) = infer_assigned_ty { + let infer_assigned_ty = &|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(), - assigned_ty, + infer_assigned_ty, true, ); } } ast::Expr::Subscript(subscript_expr) => { + let assigned_ty = infer_assigned_ty.map(|f| f(self, TypeContext::default())); self.store_expression_type(target, assigned_ty.unwrap_or(Type::unknown())); if let Some(assigned_ty) = assigned_ty { self.validate_subscript_assignment(subscript_expr, value, assigned_ty); } } + + // TODO: Remove this once we handle all possible assignment targets. _ => { - // 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()); } } @@ -4836,12 +4968,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { is_async: _, } = for_statement; - self.infer_target(target, iter, |builder| { + 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, TypeContext::default()) + .infer_standalone_expression(iter, tcx) .iterate(builder.db()) .homogeneous_element_type(builder.db()) }); @@ -5863,6 +5995,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { assert_eq!(previous, None); } + MultiInferenceState::Overwrite => { + self.expressions.insert(expression.into(), ty); + } + MultiInferenceState::Intersect => { self.expressions .entry(expression.into()) @@ -6430,7 +6566,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { is_async: _, } = comprehension; - self.infer_target(target, iter, |builder| { + self.infer_target(target, iter, |builder, tcx| { // TODO: `infer_comprehension_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] @@ -6438,11 +6574,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { infer_same_file_expression_type( builder.db(), builder.index.expression(iter), - TypeContext::default(), + tcx, builder.module(), ) } else { - builder.infer_standalone_expression(iter, TypeContext::default()) + builder.infer_standalone_expression(iter, tcx) } .iterate(builder.db()) .homogeneous_element_type(builder.db()) @@ -10153,16 +10289,16 @@ enum MultiInferenceState { #[default] Panic, + /// Overwrite the previously inferred value. + Overwrite, + /// Store the intersection of all types inferred for the expression. Intersect, } impl MultiInferenceState { - fn is_panic(self) -> bool { - match self { - MultiInferenceState::Panic => true, - MultiInferenceState::Intersect => false, - } + const fn is_panic(self) -> bool { + matches!(self, MultiInferenceState::Panic) } } From 69b4c29924c75047655868c9895d8018d11e34e3 Mon Sep 17 00:00:00 2001 From: Luca Chiodini Date: Fri, 31 Oct 2025 16:59:11 +0100 Subject: [PATCH 102/188] Consistently wrap tokens in parser diagnostics in `backticks` instead of 'quotes' (#21163) The parser currently uses single quotes to wrap tokens. This is inconsistent with the rest of ruff/ty, which use backticks. For example, see the inconsistent diagnostics produced in this simple example: https://play.ty.dev/0a9d6eab-6599-4a1d-8e40-032091f7f50f Consistently wrapping tokens in backticks produces uniform diagnostics. Following the style decision of #723, in #2889 some quotes were already switched into backticks. This is also in line with Rust's guide on diagnostics (https://rustc-dev-guide.rust-lang.org/diagnostics.html#diagnostic-structure): > When code or an identifier must appear in a message or label, it should be surrounded with backticks --- ...essage__grouped__tests__syntax_errors.snap | 2 +- ...ules__pycodestyle__tests__E231_E23.py.snap | 92 ++++----- ...tyle__tests__E301_E30_syntax_error.py.snap | 8 +- ...tyle__tests__E302_E30_syntax_error.py.snap | 8 +- ...tyle__tests__E303_E30_syntax_error.py.snap | 8 +- ...tyle__tests__E305_E30_syntax_error.py.snap | 8 +- ...tyle__tests__E306_E30_syntax_error.py.snap | 8 +- crates/ruff_python_parser/src/error.rs | 10 +- crates/ruff_python_parser/src/token.rs | 174 +++++++++--------- ...id_syntax@assert_invalid_test_expr.py.snap | 2 +- ..._syntax@assign_stmt_keyword_target.py.snap | 4 +- ...alid_syntax@async_unexpected_token.py.snap | 10 +- ...tax@aug_assign_stmt_invalid_target.py.snap | 2 +- ...class_def_unclosed_type_param_list.py.snap | 2 +- ...ntax@comma_separated_missing_comma.py.snap | 2 +- ...ted_missing_comma_between_elements.py.snap | 2 +- ...prehension_missing_for_after_async.py.snap | 4 +- ...yntax@decorator_missing_expression.py.snap | 2 +- ...d_syntax@decorator_missing_newline.py.snap | 6 +- ...essions__arguments__double_starred.py.snap | 2 +- ...ressions__arguments__missing_comma.py.snap | 2 +- ...expressions__arguments__unclosed_0.py.snap | 2 +- ...expressions__arguments__unclosed_1.py.snap | 2 +- ...expressions__arguments__unclosed_2.py.snap | 2 +- ...xpressions__compare__invalid_order.py.snap | 2 +- ...tax@expressions__dict__double_star.py.snap | 4 +- ...s__dict__double_star_comprehension.py.snap | 8 +- ...ons__dict__missing_closing_brace_0.py.snap | 6 +- ...ons__dict__missing_closing_brace_2.py.snap | 2 +- ...ressions__dict__named_expression_0.py.snap | 2 +- ...ressions__dict__named_expression_1.py.snap | 8 +- ..._syntax@expressions__dict__recover.py.snap | 2 +- ...sions__lambda_duplicate_parameters.py.snap | 2 +- ...s__list__missing_closing_bracket_3.py.snap | 2 +- ..._syntax@expressions__list__recover.py.snap | 2 +- ...sions__named__missing_expression_2.py.snap | 6 +- ...ressions__parenthesized__generator.py.snap | 4 +- ...nthesized__missing_closing_paren_3.py.snap | 2 +- ...@expressions__parenthesized__tuple.py.snap | 8 +- ..._parenthesized__tuple_starred_expr.py.snap | 2 +- ...set__missing_closing_curly_brace_3.py.snap | 2 +- ...d_syntax@expressions__set__recover.py.snap | 2 +- ...sions__subscript__unclosed_slice_1.py.snap | 6 +- ...pressions__yield__named_expression.py.snap | 2 +- ..._string_lambda_without_parentheses.py.snap | 2 +- ...id_syntax@f_string_unclosed_lbrace.py.snap | 4 +- ...ing_unclosed_lbrace_in_format_spec.py.snap | 4 +- ..._syntax@for_stmt_invalid_iter_expr.py.snap | 2 +- ...lid_syntax@for_stmt_invalid_target.py.snap | 2 +- ...syntax@for_stmt_missing_in_keyword.py.snap | 4 +- ...lid_syntax@for_stmt_missing_target.py.snap | 4 +- ...id_syntax@from_import_dotted_names.py.snap | 8 +- ...id_syntax@from_import_missing_rpar.py.snap | 4 +- ...nction_def_unclosed_parameter_list.py.snap | 4 +- ...ction_def_unclosed_type_param_list.py.snap | 2 +- ..._syntax@if_stmt_elif_missing_colon.py.snap | 2 +- ...valid_syntax@if_stmt_missing_colon.py.snap | 4 +- ...nvalid_syntax@match_expected_colon.py.snap | 2 +- ...@match_stmt_no_newline_before_case.py.snap | 2 +- ...ntax@multiple_clauses_on_same_line.py.snap | 24 +-- .../invalid_syntax@named_expr_slice.py.snap | 4 +- ...@nested_quote_in_format_spec_py312.py.snap | 2 +- ...nvalid_syntax@node_range_with_gaps.py.snap | 8 +- ...ntax@param_with_invalid_annotation.py.snap | 2 +- ...rams_expected_after_star_separator.py.snap | 10 +- ...@params_kwarg_after_star_separator.py.snap | 2 +- ...ax@params_var_keyword_with_default.py.snap | 6 +- ...params_var_positional_with_default.py.snap | 6 +- .../invalid_syntax@pos_only_py37.py.snap | 2 +- ...nvalid_syntax@re_lex_logical_token.py.snap | 24 +-- ...yntax@re_lex_logical_token_mac_eol.py.snap | 2 +- ...x@re_lex_logical_token_windows_eol.py.snap | 2 +- ...x@re_lexing__fstring_format_spec_1.py.snap | 6 +- ...tax@re_lexing__line_continuation_1.py.snap | 2 +- ...ing__line_continuation_windows_eol.py.snap | 2 +- ...re_lexing__triple_quoted_fstring_1.py.snap | 2 +- ...re_lexing__triple_quoted_fstring_2.py.snap | 2 +- ...re_lexing__triple_quoted_fstring_3.py.snap | 4 +- ...atements__function_type_parameters.py.snap | 8 +- ...ents__if_extra_closing_parentheses.py.snap | 2 +- ...ax@statements__match__as_pattern_2.py.snap | 2 +- ...ax@statements__match__as_pattern_3.py.snap | 4 +- ...ax@statements__match__as_pattern_4.py.snap | 4 +- ...ts__match__invalid_mapping_pattern.py.snap | 6 +- ...tements__match__star_pattern_usage.py.snap | 2 +- ...s__with__ambiguous_lpar_with_items.py.snap | 32 ++-- ...__with__unparenthesized_with_items.py.snap | 2 +- ..._string_lambda_without_parentheses.py.snap | 2 +- ...id_syntax@t_string_unclosed_lbrace.py.snap | 4 +- ...ing_unclosed_lbrace_in_format_spec.py.snap | 4 +- ...ntax@type_param_invalid_bound_expr.py.snap | 2 +- ...syntax@type_param_param_spec_bound.py.snap | 2 +- ...am_param_spec_invalid_default_expr.py.snap | 2 +- ...aram_type_var_invalid_default_expr.py.snap | 2 +- ...ax@type_param_type_var_tuple_bound.py.snap | 2 +- ...ype_var_tuple_invalid_default_expr.py.snap | 2 +- ...erminated_fstring_newline_recovery.py.snap | 2 +- ...yntax@while_stmt_invalid_test_expr.py.snap | 4 +- ...id_syntax@while_stmt_missing_colon.py.snap | 2 +- ..._items_parenthesized_missing_colon.py.snap | 2 +- ..._items_parenthesized_missing_comma.py.snap | 10 +- .../mdtest/comprehensions/invalid_syntax.md | 12 +- .../resources/mdtest/import/invalid_syntax.md | 2 +- 103 files changed, 359 insertions(+), 359 deletions(-) diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__grouped__tests__syntax_errors.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__grouped__tests__syntax_errors.snap index 1d077b7321..f22a079523 100644 --- a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__grouped__tests__syntax_errors.snap +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__grouped__tests__syntax_errors.snap @@ -4,4 +4,4 @@ expression: content --- syntax_errors.py: 1:15 invalid-syntax: Expected one or more symbol names after import - 3:12 invalid-syntax: Expected ')', found newline + 3:12 invalid-syntax: Expected `)`, found newline diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E231_E23.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E231_E23.py.snap index c210a6768b..d436a9826a 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E231_E23.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E231_E23.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- -E231 [*] Missing whitespace after ',' +E231 [*] Missing whitespace after `,` --> E23.py:2:7 | 1 | #: E231 @@ -18,7 +18,7 @@ help: Add missing whitespace 4 | a[b1,:] 5 | #: E231 -E231 [*] Missing whitespace after ',' +E231 [*] Missing whitespace after `,` --> E23.py:4:5 | 2 | a = (1,2) @@ -38,7 +38,7 @@ help: Add missing whitespace 6 | a = [{'a':''}] 7 | #: Okay -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:6:10 | 4 | a[b1,:] @@ -58,7 +58,7 @@ help: Add missing whitespace 8 | a = (4,) 9 | b = (5, ) -E231 [*] Missing whitespace after ',' +E231 [*] Missing whitespace after `,` --> E23.py:19:10 | 17 | def foo() -> None: @@ -77,7 +77,7 @@ help: Add missing whitespace 21 | 22 | #: Okay -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:29:20 | 27 | mdtypes_template = { @@ -96,7 +96,7 @@ help: Add missing whitespace 31 | 32 | # E231 -E231 [*] Missing whitespace after ',' +E231 [*] Missing whitespace after `,` --> E23.py:33:6 | 32 | # E231 @@ -115,7 +115,7 @@ help: Add missing whitespace 35 | # Okay because it's hard to differentiate between the usages of a colon in a f-string 36 | f"{a:=1}" -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:47:37 | 46 | #: E231 @@ -134,7 +134,7 @@ help: Add missing whitespace 49 | #: Okay 50 | a = (1,) -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:60:13 | 58 | results = { @@ -154,7 +154,7 @@ help: Add missing whitespace 62 | results_in_tuple = ( 63 | { -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:65:17 | 63 | { @@ -174,7 +174,7 @@ help: Add missing whitespace 67 | ) 68 | results_in_list = [ -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:71:17 | 69 | { @@ -194,7 +194,7 @@ help: Add missing whitespace 73 | ] 74 | results_in_list_first = [ -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:76:17 | 74 | results_in_list_first = [ @@ -214,7 +214,7 @@ help: Add missing whitespace 78 | ] 79 | -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:82:13 | 80 | x = [ @@ -234,7 +234,7 @@ help: Add missing whitespace 84 | "k3":[2], # E231 85 | "k4": [2], -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:84:13 | 82 | "k1":[2], # E231 @@ -254,7 +254,7 @@ help: Add missing whitespace 86 | "k5": [2], 87 | "k6": [1, 2, 3, 4,5,6,7] # E231 -E231 [*] Missing whitespace after ',' +E231 [*] Missing whitespace after `,` --> E23.py:87:26 | 85 | "k4": [2], @@ -274,7 +274,7 @@ help: Add missing whitespace 89 | { 90 | "k1": [ -E231 [*] Missing whitespace after ',' +E231 [*] Missing whitespace after `,` --> E23.py:87:28 | 85 | "k4": [2], @@ -294,7 +294,7 @@ help: Add missing whitespace 89 | { 90 | "k1": [ -E231 [*] Missing whitespace after ',' +E231 [*] Missing whitespace after `,` --> E23.py:87:30 | 85 | "k4": [2], @@ -314,7 +314,7 @@ help: Add missing whitespace 89 | { 90 | "k1": [ -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:92:21 | 90 | "k1": [ @@ -334,7 +334,7 @@ help: Add missing whitespace 94 | { 95 | "kb": [2,3], # E231 -E231 [*] Missing whitespace after ',' +E231 [*] Missing whitespace after `,` --> E23.py:92:24 | 90 | "k1": [ @@ -354,7 +354,7 @@ help: Add missing whitespace 94 | { 95 | "kb": [2,3], # E231 -E231 [*] Missing whitespace after ',' +E231 [*] Missing whitespace after `,` --> E23.py:95:25 | 93 | }, @@ -374,7 +374,7 @@ help: Add missing whitespace 97 | { 98 | "ka":[2, 3], # E231 -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:98:21 | 96 | }, @@ -394,7 +394,7 @@ help: Add missing whitespace 100 | "kc": [2, 3], # Ok 101 | "kd": [2,3], # E231 -E231 [*] Missing whitespace after ',' +E231 [*] Missing whitespace after `,` --> E23.py:101:25 | 99 | "kb": [2, 3], # Ok @@ -414,7 +414,7 @@ help: Add missing whitespace 103 | }, 104 | ] -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:102:21 | 100 | "kc": [2, 3], # Ok @@ -434,7 +434,7 @@ help: Add missing whitespace 104 | ] 105 | } -E231 [*] Missing whitespace after ',' +E231 [*] Missing whitespace after `,` --> E23.py:102:24 | 100 | "kc": [2, 3], # Ok @@ -454,7 +454,7 @@ help: Add missing whitespace 104 | ] 105 | } -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:109:18 | 108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults @@ -473,7 +473,7 @@ help: Add missing whitespace 111 | y:B = [[["foo", "bar"]]], 112 | z:object = "fooo", -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:109:40 | 108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults @@ -492,7 +492,7 @@ help: Add missing whitespace 111 | y:B = [[["foo", "bar"]]], 112 | z:object = "fooo", -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:109:70 | 108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults @@ -511,7 +511,7 @@ help: Add missing whitespace 111 | y:B = [[["foo", "bar"]]], 112 | z:object = "fooo", -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:110:6 | 108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults @@ -531,7 +531,7 @@ help: Add missing whitespace 112 | z:object = "fooo", 113 | ): -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:111:6 | 109 | def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]( @@ -551,7 +551,7 @@ help: Add missing whitespace 113 | ): 114 | pass -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:112:6 | 110 | x:A = "foo"[::-1], @@ -571,7 +571,7 @@ help: Add missing whitespace 114 | pass 115 | -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:116:18 | 114 | pass @@ -591,7 +591,7 @@ help: Add missing whitespace 118 | self, 119 | x:A = "foo"[::-1], -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:116:40 | 114 | pass @@ -611,7 +611,7 @@ help: Add missing whitespace 118 | self, 119 | x:A = "foo"[::-1], -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:116:70 | 114 | pass @@ -631,7 +631,7 @@ help: Add missing whitespace 118 | self, 119 | x:A = "foo"[::-1], -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:117:29 | 116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]: @@ -650,7 +650,7 @@ help: Add missing whitespace 119 | x:A = "foo"[::-1], 120 | y:B = [[["foo", "bar"]]], -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:117:51 | 116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]: @@ -669,7 +669,7 @@ help: Add missing whitespace 119 | x:A = "foo"[::-1], 120 | y:B = [[["foo", "bar"]]], -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:117:81 | 116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]: @@ -688,7 +688,7 @@ help: Add missing whitespace 119 | x:A = "foo"[::-1], 120 | y:B = [[["foo", "bar"]]], -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:119:10 | 117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]( @@ -708,7 +708,7 @@ help: Add missing whitespace 121 | z:object = "fooo", 122 | ): -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:120:10 | 118 | self, @@ -728,7 +728,7 @@ help: Add missing whitespace 122 | ): 123 | pass -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:121:10 | 119 | x:A = "foo"[::-1], @@ -748,7 +748,7 @@ help: Add missing whitespace 123 | pass 124 | -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:125:32 | 123 | pass @@ -768,7 +768,7 @@ help: Add missing whitespace 127 | pass 128 | -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:125:54 | 123 | pass @@ -788,7 +788,7 @@ help: Add missing whitespace 127 | pass 128 | -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:125:84 | 123 | pass @@ -808,7 +808,7 @@ help: Add missing whitespace 127 | pass 128 | -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:126:47 | 125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](): @@ -826,7 +826,7 @@ help: Add missing whitespace 128 | 129 | # Should be no E231 errors on any of these: -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:126:69 | 125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](): @@ -844,7 +844,7 @@ help: Add missing whitespace 128 | 129 | # Should be no E231 errors on any of these: -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:126:99 | 125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](): @@ -862,7 +862,7 @@ help: Add missing whitespace 128 | 129 | # Should be no E231 errors on any of these: -E231 [*] Missing whitespace after ',' +E231 [*] Missing whitespace after `,` --> E23.py:147:6 | 146 | # E231 @@ -881,7 +881,7 @@ help: Add missing whitespace 149 | # Okay because it's hard to differentiate between the usages of a colon in a t-string 150 | t"{a:=1}" -E231 [*] Missing whitespace after ':' +E231 [*] Missing whitespace after `:` --> E23.py:161:37 | 160 | #: E231 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E301_E30_syntax_error.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E301_E30_syntax_error.py.snap index 7cf04e5cc7..b8c6413c1d 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E301_E30_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E301_E30_syntax_error.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- -invalid-syntax: Expected ']', found '(' +invalid-syntax: Expected `]`, found `(` --> E30_syntax_error.py:4:15 | 2 | # parenthesis. @@ -11,7 +11,7 @@ invalid-syntax: Expected ']', found '(' 5 | pass | -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:13:18 | 12 | class Foo: @@ -32,7 +32,7 @@ E301 Expected 1 blank line, found 0 | help: Add missing blank line -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:18:11 | 16 | pass @@ -41,7 +41,7 @@ invalid-syntax: Expected ')', found newline | ^ | -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:21:9 | 21 | def top( diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E302_E30_syntax_error.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E302_E30_syntax_error.py.snap index e28bb8562d..76c3d31211 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E302_E30_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E302_E30_syntax_error.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- -invalid-syntax: Expected ']', found '(' +invalid-syntax: Expected `]`, found `(` --> E30_syntax_error.py:4:15 | 2 | # parenthesis. @@ -22,7 +22,7 @@ E302 Expected 2 blank lines, found 1 | help: Add missing blank line(s) -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:13:18 | 12 | class Foo: @@ -32,7 +32,7 @@ invalid-syntax: Expected ')', found newline 15 | def method(): | -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:18:11 | 16 | pass @@ -41,7 +41,7 @@ invalid-syntax: Expected ')', found newline | ^ | -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:21:9 | 21 | def top( diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30_syntax_error.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30_syntax_error.py.snap index c70c94baad..af23f16de9 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30_syntax_error.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- -invalid-syntax: Expected ']', found '(' +invalid-syntax: Expected `]`, found `(` --> E30_syntax_error.py:4:15 | 2 | # parenthesis. @@ -21,7 +21,7 @@ E303 Too many blank lines (3) | help: Remove extraneous blank line(s) -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:13:18 | 12 | class Foo: @@ -31,7 +31,7 @@ invalid-syntax: Expected ')', found newline 15 | def method(): | -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:18:11 | 16 | pass @@ -40,7 +40,7 @@ invalid-syntax: Expected ')', found newline | ^ | -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:21:9 | 21 | def top( diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E305_E30_syntax_error.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E305_E30_syntax_error.py.snap index dd97fe9010..f72c198e1e 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E305_E30_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E305_E30_syntax_error.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- -invalid-syntax: Expected ']', found '(' +invalid-syntax: Expected `]`, found `(` --> E30_syntax_error.py:4:15 | 2 | # parenthesis. @@ -11,7 +11,7 @@ invalid-syntax: Expected ']', found '(' 5 | pass | -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:13:18 | 12 | class Foo: @@ -31,7 +31,7 @@ E305 Expected 2 blank lines after class or function definition, found (1) | help: Add missing blank line(s) -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:18:11 | 16 | pass @@ -40,7 +40,7 @@ invalid-syntax: Expected ')', found newline | ^ | -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:21:9 | 21 | def top( diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E306_E30_syntax_error.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E306_E30_syntax_error.py.snap index d3a6b15d4e..98d00f77af 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E306_E30_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E306_E30_syntax_error.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- -invalid-syntax: Expected ']', found '(' +invalid-syntax: Expected `]`, found `(` --> E30_syntax_error.py:4:15 | 2 | # parenthesis. @@ -11,7 +11,7 @@ invalid-syntax: Expected ']', found '(' 5 | pass | -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:13:18 | 12 | class Foo: @@ -21,7 +21,7 @@ invalid-syntax: Expected ')', found newline 15 | def method(): | -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:18:11 | 16 | pass @@ -30,7 +30,7 @@ invalid-syntax: Expected ')', found newline | ^ | -invalid-syntax: Expected ')', found newline +invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:21:9 | 21 | def top( diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 2c2baa8dd7..8b02546d3b 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -78,9 +78,9 @@ pub enum InterpolatedStringErrorType { impl std::fmt::Display for InterpolatedStringErrorType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - Self::UnclosedLbrace => write!(f, "expecting '}}'"), + Self::UnclosedLbrace => write!(f, "expecting `}}`"), Self::InvalidConversionFlag => write!(f, "invalid conversion character"), - Self::SingleRbrace => write!(f, "single '}}' is not allowed"), + Self::SingleRbrace => write!(f, "single `}}` is not allowed"), Self::UnterminatedString => write!(f, "unterminated string"), Self::UnterminatedTripleQuotedString => write!(f, "unterminated triple-quoted string"), Self::LambdaWithoutParentheses => { @@ -232,7 +232,7 @@ impl std::fmt::Display for ParseErrorType { ParseErrorType::UnexpectedTokenAfterAsync(kind) => { write!( f, - "Expected 'def', 'with' or 'for' to follow 'async', found {kind}", + "Expected `def`, `with` or `for` to follow `async`, found {kind}", ) } ParseErrorType::InvalidArgumentUnpackingOrder => { @@ -286,10 +286,10 @@ impl std::fmt::Display for ParseErrorType { f.write_str("Parameter without a default cannot follow a parameter with a default") } ParseErrorType::ExpectedKeywordParam => { - f.write_str("Expected one or more keyword parameter after '*' separator") + f.write_str("Expected one or more keyword parameter after `*` separator") } ParseErrorType::VarParameterWithDefault => { - f.write_str("Parameter with '*' or '**' cannot have default value") + f.write_str("Parameter with `*` or `**` cannot have default value") } ParseErrorType::InvalidStarPatternUsage => { f.write_str("Star pattern cannot be used here") diff --git a/crates/ruff_python_parser/src/token.rs b/crates/ruff_python_parser/src/token.rs index 18b7648c4c..a5790a9597 100644 --- a/crates/ruff_python_parser/src/token.rs +++ b/crates/ruff_python_parser/src/token.rs @@ -635,93 +635,93 @@ impl fmt::Display for TokenKind { TokenKind::TStringEnd => "TStringEnd", TokenKind::IpyEscapeCommand => "IPython escape command", TokenKind::Comment => "comment", - TokenKind::Question => "'?'", - TokenKind::Exclamation => "'!'", - TokenKind::Lpar => "'('", - TokenKind::Rpar => "')'", - TokenKind::Lsqb => "'['", - TokenKind::Rsqb => "']'", - TokenKind::Lbrace => "'{'", - TokenKind::Rbrace => "'}'", - TokenKind::Equal => "'='", - TokenKind::ColonEqual => "':='", - TokenKind::Dot => "'.'", - TokenKind::Colon => "':'", - TokenKind::Semi => "';'", - TokenKind::Comma => "','", - TokenKind::Rarrow => "'->'", - TokenKind::Plus => "'+'", - TokenKind::Minus => "'-'", - TokenKind::Star => "'*'", - TokenKind::DoubleStar => "'**'", - TokenKind::Slash => "'/'", - TokenKind::DoubleSlash => "'//'", - TokenKind::Percent => "'%'", - TokenKind::Vbar => "'|'", - TokenKind::Amper => "'&'", - TokenKind::CircumFlex => "'^'", - TokenKind::LeftShift => "'<<'", - TokenKind::RightShift => "'>>'", - TokenKind::Tilde => "'~'", - TokenKind::At => "'@'", - TokenKind::Less => "'<'", - TokenKind::Greater => "'>'", - TokenKind::EqEqual => "'=='", - TokenKind::NotEqual => "'!='", - TokenKind::LessEqual => "'<='", - TokenKind::GreaterEqual => "'>='", - TokenKind::PlusEqual => "'+='", - TokenKind::MinusEqual => "'-='", - TokenKind::StarEqual => "'*='", - TokenKind::DoubleStarEqual => "'**='", - TokenKind::SlashEqual => "'/='", - TokenKind::DoubleSlashEqual => "'//='", - TokenKind::PercentEqual => "'%='", - TokenKind::VbarEqual => "'|='", - TokenKind::AmperEqual => "'&='", - TokenKind::CircumflexEqual => "'^='", - TokenKind::LeftShiftEqual => "'<<='", - TokenKind::RightShiftEqual => "'>>='", - TokenKind::AtEqual => "'@='", - TokenKind::Ellipsis => "'...'", - TokenKind::False => "'False'", - TokenKind::None => "'None'", - TokenKind::True => "'True'", - TokenKind::And => "'and'", - TokenKind::As => "'as'", - TokenKind::Assert => "'assert'", - TokenKind::Async => "'async'", - TokenKind::Await => "'await'", - TokenKind::Break => "'break'", - TokenKind::Class => "'class'", - TokenKind::Continue => "'continue'", - TokenKind::Def => "'def'", - TokenKind::Del => "'del'", - TokenKind::Elif => "'elif'", - TokenKind::Else => "'else'", - TokenKind::Except => "'except'", - TokenKind::Finally => "'finally'", - TokenKind::For => "'for'", - TokenKind::From => "'from'", - TokenKind::Global => "'global'", - TokenKind::If => "'if'", - TokenKind::Import => "'import'", - TokenKind::In => "'in'", - TokenKind::Is => "'is'", - TokenKind::Lambda => "'lambda'", - TokenKind::Nonlocal => "'nonlocal'", - TokenKind::Not => "'not'", - TokenKind::Or => "'or'", - TokenKind::Pass => "'pass'", - TokenKind::Raise => "'raise'", - TokenKind::Return => "'return'", - TokenKind::Try => "'try'", - TokenKind::While => "'while'", - TokenKind::Match => "'match'", - TokenKind::Type => "'type'", - TokenKind::Case => "'case'", - TokenKind::With => "'with'", - TokenKind::Yield => "'yield'", + TokenKind::Question => "`?`", + TokenKind::Exclamation => "`!`", + TokenKind::Lpar => "`(`", + TokenKind::Rpar => "`)`", + TokenKind::Lsqb => "`[`", + TokenKind::Rsqb => "`]`", + TokenKind::Lbrace => "`{`", + TokenKind::Rbrace => "`}`", + TokenKind::Equal => "`=`", + TokenKind::ColonEqual => "`:=`", + TokenKind::Dot => "`.`", + TokenKind::Colon => "`:`", + TokenKind::Semi => "`;`", + TokenKind::Comma => "`,`", + TokenKind::Rarrow => "`->`", + TokenKind::Plus => "`+`", + TokenKind::Minus => "`-`", + TokenKind::Star => "`*`", + TokenKind::DoubleStar => "`**`", + TokenKind::Slash => "`/`", + TokenKind::DoubleSlash => "`//`", + TokenKind::Percent => "`%`", + TokenKind::Vbar => "`|`", + TokenKind::Amper => "`&`", + TokenKind::CircumFlex => "`^`", + TokenKind::LeftShift => "`<<`", + TokenKind::RightShift => "`>>`", + TokenKind::Tilde => "`~`", + TokenKind::At => "`@`", + TokenKind::Less => "`<`", + TokenKind::Greater => "`>`", + TokenKind::EqEqual => "`==`", + TokenKind::NotEqual => "`!=`", + TokenKind::LessEqual => "`<=`", + TokenKind::GreaterEqual => "`>=`", + TokenKind::PlusEqual => "`+=`", + TokenKind::MinusEqual => "`-=`", + TokenKind::StarEqual => "`*=`", + TokenKind::DoubleStarEqual => "`**=`", + TokenKind::SlashEqual => "`/=`", + TokenKind::DoubleSlashEqual => "`//=`", + TokenKind::PercentEqual => "`%=`", + TokenKind::VbarEqual => "`|=`", + TokenKind::AmperEqual => "`&=`", + TokenKind::CircumflexEqual => "`^=`", + TokenKind::LeftShiftEqual => "`<<=`", + TokenKind::RightShiftEqual => "`>>=`", + TokenKind::AtEqual => "`@=`", + TokenKind::Ellipsis => "`...`", + TokenKind::False => "`False`", + TokenKind::None => "`None`", + TokenKind::True => "`True`", + TokenKind::And => "`and`", + TokenKind::As => "`as`", + TokenKind::Assert => "`assert`", + TokenKind::Async => "`async`", + TokenKind::Await => "`await`", + TokenKind::Break => "`break`", + TokenKind::Class => "`class`", + TokenKind::Continue => "`continue`", + TokenKind::Def => "`def`", + TokenKind::Del => "`del`", + TokenKind::Elif => "`elif`", + TokenKind::Else => "`else`", + TokenKind::Except => "`except`", + TokenKind::Finally => "`finally`", + TokenKind::For => "`for`", + TokenKind::From => "`from`", + TokenKind::Global => "`global`", + TokenKind::If => "`if`", + TokenKind::Import => "`import`", + TokenKind::In => "`in`", + TokenKind::Is => "`is`", + TokenKind::Lambda => "`lambda`", + TokenKind::Nonlocal => "`nonlocal`", + TokenKind::Not => "`not`", + TokenKind::Or => "`or`", + TokenKind::Pass => "`pass`", + TokenKind::Raise => "`raise`", + TokenKind::Return => "`return`", + TokenKind::Try => "`try`", + TokenKind::While => "`while`", + TokenKind::Match => "`match`", + TokenKind::Type => "`type`", + TokenKind::Case => "`case`", + TokenKind::With => "`with`", + TokenKind::Yield => "`yield`", }; f.write_str(value) } diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_test_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_test_expr.py.snap index 1a843b29c9..87c0dcf672 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_test_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_test_expr.py.snap @@ -131,7 +131,7 @@ Module( | 1 | assert *x 2 | assert assert x - | ^^^^^^ Syntax Error: Expected an identifier, but found a keyword 'assert' that cannot be used here + | ^^^^^^ Syntax Error: Expected an identifier, but found a keyword `assert` that cannot be used here 3 | assert yield x 4 | assert x := 1 | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_keyword_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_keyword_target.py.snap index 6264d907b5..e59c71cea8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_keyword_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_keyword_target.py.snap @@ -148,7 +148,7 @@ Module( | 1 | a = pass = c - | ^^^^ Syntax Error: Expected an identifier, but found a keyword 'pass' that cannot be used here + | ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here 2 | a + b 3 | a = b = pass = c | @@ -158,6 +158,6 @@ Module( 1 | a = pass = c 2 | a + b 3 | a = b = pass = c - | ^^^^ Syntax Error: Expected an identifier, but found a keyword 'pass' that cannot be used here + | ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here 4 | a + b | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@async_unexpected_token.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@async_unexpected_token.py.snap index 2dd2bddfc4..dc62f1b446 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@async_unexpected_token.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@async_unexpected_token.py.snap @@ -181,7 +181,7 @@ Module( | 1 | async class Foo: ... - | ^^^^^ Syntax Error: Expected 'def', 'with' or 'for' to follow 'async', found 'class' + | ^^^^^ Syntax Error: Expected `def`, `with` or `for` to follow `async`, found `class` 2 | async while test: ... 3 | async x = 1 | @@ -190,7 +190,7 @@ Module( | 1 | async class Foo: ... 2 | async while test: ... - | ^^^^^ Syntax Error: Expected 'def', 'with' or 'for' to follow 'async', found 'while' + | ^^^^^ Syntax Error: Expected `def`, `with` or `for` to follow `async`, found `while` 3 | async x = 1 4 | async async def foo(): ... | @@ -200,7 +200,7 @@ Module( 1 | async class Foo: ... 2 | async while test: ... 3 | async x = 1 - | ^ Syntax Error: Expected 'def', 'with' or 'for' to follow 'async', found name + | ^ Syntax Error: Expected `def`, `with` or `for` to follow `async`, found name 4 | async async def foo(): ... 5 | async match test: | @@ -210,7 +210,7 @@ Module( 2 | async while test: ... 3 | async x = 1 4 | async async def foo(): ... - | ^^^^^ Syntax Error: Expected 'def', 'with' or 'for' to follow 'async', found 'async' + | ^^^^^ Syntax Error: Expected `def`, `with` or `for` to follow `async`, found `async` 5 | async match test: 6 | case _: ... | @@ -220,6 +220,6 @@ Module( 3 | async x = 1 4 | async async def foo(): ... 5 | async match test: - | ^^^^^ Syntax Error: Expected 'def', 'with' or 'for' to follow 'async', found 'match' + | ^^^^^ Syntax Error: Expected `def`, `with` or `for` to follow `async`, found `match` 6 | case _: ... | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_target.py.snap index 0d1311ca25..dbe201539e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_target.py.snap @@ -245,7 +245,7 @@ Module( 3 | *x += 1 4 | pass += 1 5 | x += pass - | ^^^^ Syntax Error: Expected an identifier, but found a keyword 'pass' that cannot be used here + | ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here 6 | (x + y) += 1 | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unclosed_type_param_list.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unclosed_type_param_list.py.snap index 3246bdb0ce..515534ed26 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unclosed_type_param_list.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unclosed_type_param_list.py.snap @@ -121,7 +121,7 @@ Module( | 1 | class Foo[T1, *T2(a, b): - | ^ Syntax Error: Expected ']', found '(' + | ^ Syntax Error: Expected `]`, found `(` 2 | pass 3 | x = 10 | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma.py.snap index bf33de094f..84c88e8d1f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma.py.snap @@ -68,7 +68,7 @@ Module( | 1 | call(**x := 1) - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma_between_elements.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma_between_elements.py.snap index 5f39e94515..4da8a91dea 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma_between_elements.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma_between_elements.py.snap @@ -61,5 +61,5 @@ Module( | 1 | # The comma between the first two elements is expected in `parse_list_expression`. 2 | [0, 1 2] - | ^ Syntax Error: Expected ',', found int + | ^ Syntax Error: Expected `,`, found int | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comprehension_missing_for_after_async.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comprehension_missing_for_after_async.py.snap index aee9bf7056..a31a055919 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comprehension_missing_for_after_async.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comprehension_missing_for_after_async.py.snap @@ -77,7 +77,7 @@ Module( | 1 | (async) - | ^^^^^ Syntax Error: Expected an identifier, but found a keyword 'async' that cannot be used here + | ^^^^^ Syntax Error: Expected an identifier, but found a keyword `async` that cannot be used here 2 | (x async x in iter) | @@ -85,5 +85,5 @@ Module( | 1 | (async) 2 | (x async x in iter) - | ^ Syntax Error: Expected 'for', found name + | ^ Syntax Error: Expected `for`, found name | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap index 2c5bbd3a03..dd7225493f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap @@ -169,7 +169,7 @@ Module( | 1 | @def foo(): ... - | ^^^ Syntax Error: Expected an identifier, but found a keyword 'def' that cannot be used here + | ^^^ Syntax Error: Expected an identifier, but found a keyword `def` that cannot be used here 2 | @ 3 | def foo(): ... | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_newline.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_newline.py.snap index 948fc24fe2..ea573c4cde 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_newline.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_newline.py.snap @@ -161,7 +161,7 @@ Module( | 1 | @x def foo(): ... - | ^^^ Syntax Error: Expected newline, found 'def' + | ^^^ Syntax Error: Expected newline, found `def` 2 | @x async def foo(): ... 3 | @x class Foo: ... | @@ -170,7 +170,7 @@ Module( | 1 | @x def foo(): ... 2 | @x async def foo(): ... - | ^^^^^ Syntax Error: Expected newline, found 'async' + | ^^^^^ Syntax Error: Expected newline, found `async` 3 | @x class Foo: ... | @@ -179,5 +179,5 @@ Module( 1 | @x def foo(): ... 2 | @x async def foo(): ... 3 | @x class Foo: ... - | ^^^^^ Syntax Error: Expected newline, found 'class' + | ^^^^^ Syntax Error: Expected newline, found `class` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__double_starred.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__double_starred.py.snap index e3f633b879..8656ee03e8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__double_starred.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__double_starred.py.snap @@ -238,7 +238,7 @@ Module( 3 | call(***x) 4 | 5 | call(**x := 1) - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_comma.py.snap index 37e891b89a..0f781f1e53 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_comma.py.snap @@ -61,5 +61,5 @@ Module( | 1 | call(x y) - | ^ Syntax Error: Expected ',', found name + | ^ Syntax Error: Expected `,`, found name | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_0.py.snap index 655f45ed24..cc6fba138b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_0.py.snap @@ -76,7 +76,7 @@ Module( | 1 | call( - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 2 | 3 | def foo(): 4 | pass diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_1.py.snap index 99e0e4fbcd..cdb11a8ebc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_1.py.snap @@ -85,7 +85,7 @@ Module( | 1 | call(x - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 2 | 3 | def foo(): 4 | pass diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_2.py.snap index 2b4270bf2c..e28cecdd9f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_2.py.snap @@ -85,7 +85,7 @@ Module( | 1 | call(x, - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 2 | 3 | def foo(): 4 | pass diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_order.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_order.py.snap index 419cc7854b..ccc649ea7c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_order.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_order.py.snap @@ -175,7 +175,7 @@ Module( | 6 | # Same here as well, `not` without `in` is considered to be a unary operator 7 | x not is y - | ^^ Syntax Error: Expected an identifier, but found a keyword 'is' that cannot be used here + | ^^ Syntax Error: Expected an identifier, but found a keyword `is` that cannot be used here | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star.py.snap index a579afac89..64fb0233e4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star.py.snap @@ -544,7 +544,7 @@ Module( 2 | # the ones which are higher than that. 3 | 4 | {**x := 1} - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` 5 | {a: 1, **x if True else y} 6 | {**lambda x: x, b: 2} | @@ -554,7 +554,7 @@ Module( 2 | # the ones which are higher than that. 3 | 4 | {**x := 1} - | ^ Syntax Error: Expected ':', found '}' + | ^ Syntax Error: Expected `:`, found `}` 5 | {a: 1, **x if True else y} 6 | {**lambda x: x, b: 2} | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star_comprehension.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star_comprehension.py.snap index 9c0cde63d3..e5e7f7e3ee 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star_comprehension.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star_comprehension.py.snap @@ -134,7 +134,7 @@ Module( 2 | # it's actually a comprehension. 3 | 4 | {**x: y for x, y in data} - | ^^^ Syntax Error: Expected ':', found 'for' + | ^^^ Syntax Error: Expected `:`, found `for` 5 | 6 | # TODO(dhruvmanila): This test case fails because there's no way to represent `**y` | @@ -144,7 +144,7 @@ Module( 2 | # it's actually a comprehension. 3 | 4 | {**x: y for x, y in data} - | ^ Syntax Error: Expected ',', found name + | ^ Syntax Error: Expected `,`, found name 5 | 6 | # TODO(dhruvmanila): This test case fails because there's no way to represent `**y` | @@ -154,7 +154,7 @@ Module( 2 | # it's actually a comprehension. 3 | 4 | {**x: y for x, y in data} - | ^ Syntax Error: Expected ':', found ',' + | ^ Syntax Error: Expected `:`, found `,` 5 | 6 | # TODO(dhruvmanila): This test case fails because there's no way to represent `**y` | @@ -164,7 +164,7 @@ Module( 2 | # it's actually a comprehension. 3 | 4 | {**x: y for x, y in data} - | ^ Syntax Error: Expected ':', found '}' + | ^ Syntax Error: Expected `:`, found `}` 5 | 6 | # TODO(dhruvmanila): This test case fails because there's no way to represent `**y` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_0.py.snap index 99d310bc87..31bd7feb9f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_0.py.snap @@ -86,7 +86,7 @@ Module( 1 | {x: 2 | 3 | def foo(): - | ^^^ Syntax Error: Expected an identifier, but found a keyword 'def' that cannot be used here + | ^^^ Syntax Error: Expected an identifier, but found a keyword `def` that cannot be used here 4 | pass | @@ -95,7 +95,7 @@ Module( 1 | {x: 2 | 3 | def foo(): - | ^^^ Syntax Error: Expected ',', found name + | ^^^ Syntax Error: Expected `,`, found name 4 | pass | @@ -103,7 +103,7 @@ Module( | 3 | def foo(): 4 | pass - | ^^^^ Syntax Error: Expected an identifier, but found a keyword 'pass' that cannot be used here + | ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_2.py.snap index a5a08be0be..a54264becd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_2.py.snap @@ -85,7 +85,7 @@ Module( | 1 | {x: 1, - | ^ Syntax Error: Expected '}', found newline + | ^ Syntax Error: Expected `}`, found newline 2 | 3 | def foo(): 4 | pass diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_0.py.snap index 5db7a61381..824f3261f7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_0.py.snap @@ -149,7 +149,7 @@ Module( 1 | # Unparenthesized named expression not allowed in key 2 | 3 | {x := 1: y, z := 2: a} - | ^^ Syntax Error: Expected ':', found ':=' + | ^^ Syntax Error: Expected `:`, found `:=` 4 | 5 | x + y | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_1.py.snap index 58509cc935..ccf8ead1b9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_1.py.snap @@ -145,7 +145,7 @@ Module( 1 | # Unparenthesized named expression not allowed in value 2 | 3 | {x: y := 1, z: a := 2} - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` 4 | 5 | x + y | @@ -155,7 +155,7 @@ Module( 1 | # Unparenthesized named expression not allowed in value 2 | 3 | {x: y := 1, z: a := 2} - | ^ Syntax Error: Expected ':', found ',' + | ^ Syntax Error: Expected `:`, found `,` 4 | 5 | x + y | @@ -165,7 +165,7 @@ Module( 1 | # Unparenthesized named expression not allowed in value 2 | 3 | {x: y := 1, z: a := 2} - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` 4 | 5 | x + y | @@ -175,7 +175,7 @@ Module( 1 | # Unparenthesized named expression not allowed in value 2 | 3 | {x: y := 1, z: a := 2} - | ^ Syntax Error: Expected ':', found '}' + | ^ Syntax Error: Expected `:`, found `}` 4 | 5 | x + y | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap index c4c4f242c8..b1ad5d8255 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap @@ -504,7 +504,7 @@ Module( | 9 | # Missing comma 10 | {1: 2 3: 4} - | ^ Syntax Error: Expected ',', found int + | ^ Syntax Error: Expected `,`, found int 11 | 12 | # No value | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_duplicate_parameters.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_duplicate_parameters.py.snap index 986a07de03..39ddc3c4c5 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_duplicate_parameters.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_duplicate_parameters.py.snap @@ -338,7 +338,7 @@ Module( 7 | lambda a, *a: 1 8 | 9 | lambda a, *, **a: 1 - | ^^^ Syntax Error: Expected one or more keyword parameter after '*' separator + | ^^^ Syntax Error: Expected one or more keyword parameter after `*` separator | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_3.py.snap index 4f21ca4ddf..1870af8aa0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_3.py.snap @@ -85,7 +85,7 @@ Module( 2 | # token starts a statement. 3 | 4 | [1, 2 - | ^ Syntax Error: Expected ']', found newline + | ^ Syntax Error: Expected `]`, found newline 5 | 6 | def foo(): 7 | pass diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__recover.py.snap index 3b1ba32aac..3fa2a32578 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__recover.py.snap @@ -305,7 +305,7 @@ Module( | 9 | # Missing comma 10 | [1 2] - | ^ Syntax Error: Expected ',', found int + | ^ Syntax Error: Expected `,`, found int 11 | 12 | # Dictionary element in a list | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_2.py.snap index 884fb234b2..057477b761 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_2.py.snap @@ -84,7 +84,7 @@ Module( 3 | (x := 4 | 5 | def foo(): - | ^^^ Syntax Error: Expected an identifier, but found a keyword 'def' that cannot be used here + | ^^^ Syntax Error: Expected an identifier, but found a keyword `def` that cannot be used here 6 | pass | @@ -93,7 +93,7 @@ Module( 3 | (x := 4 | 5 | def foo(): - | ^^^ Syntax Error: Expected ')', found name + | ^^^ Syntax Error: Expected `)`, found name 6 | pass | @@ -101,7 +101,7 @@ Module( | 5 | def foo(): 6 | pass - | ^^^^ Syntax Error: Expected an identifier, but found a keyword 'pass' that cannot be used here + | ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__generator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__generator.py.snap index 3e8a85dc97..7ebe57ede2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__generator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__generator.py.snap @@ -142,14 +142,14 @@ Module( | 1 | (*x for x in y) 2 | (x := 1, for x in y) - | ^^^ Syntax Error: Expected ')', found 'for' + | ^^^ Syntax Error: Expected `)`, found `for` | | 1 | (*x for x in y) 2 | (x := 1, for x in y) - | ^ Syntax Error: Expected ':', found ')' + | ^ Syntax Error: Expected `:`, found `)` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_3.py.snap index b98aae283e..24ae8dd4bb 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_3.py.snap @@ -86,7 +86,7 @@ Module( 2 | # token starts a statement. 3 | 4 | (1, 2 - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 5 | 6 | def foo(): 7 | pass diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple.py.snap index 768381c483..7670ed0edd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple.py.snap @@ -315,7 +315,7 @@ Module( | 9 | # Missing comma 10 | (1 2) - | ^ Syntax Error: Expected ')', found int + | ^ Syntax Error: Expected `)`, found int 11 | 12 | # Dictionary element in a list | @@ -343,7 +343,7 @@ Module( | 12 | # Dictionary element in a list 13 | (1: 2) - | ^ Syntax Error: Expected ')', found ':' + | ^ Syntax Error: Expected `)`, found `:` 14 | 15 | # Missing expression | @@ -390,7 +390,7 @@ Module( 16 | (1, x + ) 17 | 18 | (1; 2) - | ^ Syntax Error: Expected ')', found ';' + | ^ Syntax Error: Expected `)`, found `;` 19 | 20 | # Unparenthesized named expression is not allowed | @@ -420,5 +420,5 @@ Module( | 20 | # Unparenthesized named expression is not allowed 21 | x, y := 2, z - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap index da92fa1991..05cd9dbaca 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap @@ -1542,5 +1542,5 @@ Module( 18 | *x if True else y, z, *x if True else y 19 | *lambda x: x, z, *lambda x: x 20 | *x := 2, z, *x := 2 - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_3.py.snap index 311eaae530..0be8d06138 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_3.py.snap @@ -84,7 +84,7 @@ Module( 2 | # token starts a statement. 3 | 4 | {1, 2 - | ^ Syntax Error: Expected '}', found newline + | ^ Syntax Error: Expected `}`, found newline 5 | 6 | def foo(): 7 | pass diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__recover.py.snap index b489b1c64f..74e95fe8c7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__recover.py.snap @@ -302,7 +302,7 @@ Module( | 11 | # Missing comma 12 | {1 2} - | ^ Syntax Error: Expected ',', found int + | ^ Syntax Error: Expected `,`, found int 13 | 14 | # Dictionary element in a list | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_1.py.snap index d3e57ddfc0..05de28c275 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_1.py.snap @@ -95,7 +95,7 @@ Module( 1 | x[:: 2 | 3 | def foo(): - | ^^^ Syntax Error: Expected an identifier, but found a keyword 'def' that cannot be used here + | ^^^ Syntax Error: Expected an identifier, but found a keyword `def` that cannot be used here 4 | pass | @@ -104,7 +104,7 @@ Module( 1 | x[:: 2 | 3 | def foo(): - | ^^^ Syntax Error: Expected ']', found name + | ^^^ Syntax Error: Expected `]`, found name 4 | pass | @@ -112,7 +112,7 @@ Module( | 3 | def foo(): 4 | pass - | ^^^^ Syntax Error: Expected an identifier, but found a keyword 'pass' that cannot be used here + | ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__named_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__named_expression.py.snap index 5feebcc55f..49a4e5362b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__named_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__named_expression.py.snap @@ -125,5 +125,5 @@ Module( 2 | yield x := 1 3 | 4 | yield 1, x := 2, 3 - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap index b7da154352..2f5d767448 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap @@ -117,7 +117,7 @@ Module( | 1 | f"{lambda x: x}" - | ^^ Syntax Error: f-string: expecting '}' + | ^^ Syntax Error: f-string: expecting `}` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap index c8b75ce3f9..004ae87faa 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap @@ -267,7 +267,7 @@ Module( | 1 | f"{" 2 | f"{foo!r" - | ^ Syntax Error: f-string: expecting '}' + | ^ Syntax Error: f-string: expecting `}` 3 | f"{foo=" 4 | f"{" | @@ -277,7 +277,7 @@ Module( 1 | f"{" 2 | f"{foo!r" 3 | f"{foo=" - | ^ Syntax Error: f-string: expecting '}' + | ^ Syntax Error: f-string: expecting `}` 4 | f"{" 5 | f"""{""" | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap index cf843119c2..ac1d7c98f4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap @@ -146,7 +146,7 @@ Module( | 1 | f"hello {x:" - | ^ Syntax Error: f-string: expecting '}' + | ^ Syntax Error: f-string: expecting `}` 2 | f"hello {x:.3f" | @@ -154,5 +154,5 @@ Module( | 1 | f"hello {x:" 2 | f"hello {x:.3f" - | ^ Syntax Error: f-string: expecting '}' + | ^ Syntax Error: f-string: expecting `}` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_iter_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_iter_expr.py.snap index 907c07e8ce..ab00df4afe 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_iter_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_iter_expr.py.snap @@ -192,7 +192,7 @@ Module( 1 | for x in *a and b: ... 2 | for x in yield a: ... 3 | for target in x := 1: ... - | ^^ Syntax Error: Expected ':', found ':=' + | ^^ Syntax Error: Expected `:`, found `:=` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target.py.snap index 04caa94916..88050de12e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target.py.snap @@ -498,7 +498,7 @@ Module( 4 | for *x | y in z: ... 5 | for await x in z: ... 6 | for yield x in y: ... - | ^ Syntax Error: Expected 'in', found ':' + | ^ Syntax Error: Expected `in`, found `:` 7 | for [x, 1, y, *["a"]] in z: ... | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_in_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_in_keyword.py.snap index 8052c314b5..a2bf0f699d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_in_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_in_keyword.py.snap @@ -94,7 +94,7 @@ Module( | 1 | for a b: ... - | ^ Syntax Error: Expected 'in', found name + | ^ Syntax Error: Expected `in`, found name 2 | for a: ... | @@ -102,5 +102,5 @@ Module( | 1 | for a b: ... 2 | for a: ... - | ^ Syntax Error: Expected 'in', found ':' + | ^ Syntax Error: Expected `in`, found `:` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_target.py.snap index 84d8b4f8cd..7742223cd0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_target.py.snap @@ -56,11 +56,11 @@ Module( | 1 | for in x: ... - | ^^ Syntax Error: Expected an identifier, but found a keyword 'in' that cannot be used here + | ^^ Syntax Error: Expected an identifier, but found a keyword `in` that cannot be used here | | 1 | for in x: ... - | ^ Syntax Error: Expected 'in', found name + | ^ Syntax Error: Expected `in`, found name | 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 2520cfbe49..a0fbe287f6 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 @@ -166,7 +166,7 @@ Module( | 1 | from x import a. - | ^ Syntax Error: Expected ',', found '.' + | ^ Syntax Error: Expected `,`, found `.` 2 | from x import a.b 3 | from x import a, b.c, d, e.f, g | @@ -175,7 +175,7 @@ Module( | 1 | from x import a. 2 | from x import a.b - | ^ Syntax Error: Expected ',', found '.' + | ^ Syntax Error: Expected `,`, found `.` 3 | from x import a, b.c, d, e.f, g | @@ -184,7 +184,7 @@ Module( 1 | from x import a. 2 | from x import a.b 3 | from x import a, b.c, d, e.f, g - | ^ Syntax Error: Expected ',', found '.' + | ^ Syntax Error: Expected `,`, found `.` | @@ -192,5 +192,5 @@ Module( 1 | from x import a. 2 | from x import a.b 3 | from x import a, b.c, d, e.f, g - | ^ Syntax Error: Expected ',', found '.' + | ^ Syntax Error: Expected `,`, found `.` | 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 f53eb5aeff..d1792e0e09 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 @@ -152,7 +152,7 @@ Module( | 1 | from x import (a, b - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 2 | 1 + 1 3 | from x import (a, b, 4 | 2 + 2 @@ -163,6 +163,6 @@ Module( 1 | from x import (a, b 2 | 1 + 1 3 | from x import (a, b, - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 4 | 2 + 2 | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_parameter_list.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_parameter_list.py.snap index 9028296eeb..9efb6d3fac 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_parameter_list.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_parameter_list.py.snap @@ -234,7 +234,7 @@ Module( | 1 | def foo(a: int, b: - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 2 | def foo(): 3 | return 42 4 | def foo(a: int, b: str @@ -254,7 +254,7 @@ Module( 3 | return 42 4 | def foo(a: int, b: str 5 | x = 10 - | ^ Syntax Error: Expected ',', found name + | ^ Syntax Error: Expected `,`, found name | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_type_param_list.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_type_param_list.py.snap index fa71509d1f..dd2412c32a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_type_param_list.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_type_param_list.py.snap @@ -163,7 +163,7 @@ Module( | 1 | def foo[T1, *T2(a, b): - | ^ Syntax Error: Expected ']', found '(' + | ^ Syntax Error: Expected `]`, found `(` 2 | return a + b 3 | x = 10 | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_elif_missing_colon.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_elif_missing_colon.py.snap index d8ac7c86be..4238e23e7a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_elif_missing_colon.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_elif_missing_colon.py.snap @@ -79,7 +79,7 @@ Module( 1 | if x: 2 | pass 3 | elif y - | ^ Syntax Error: Expected ':', found newline + | ^ Syntax Error: Expected `:`, found newline 4 | pass 5 | else: 6 | pass diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_colon.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_colon.py.snap index 8092bc7d7c..5dc30d6ce6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_colon.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_colon.py.snap @@ -82,7 +82,7 @@ Module( | 1 | if x - | ^ Syntax Error: Expected ':', found newline + | ^ Syntax Error: Expected `:`, found newline 2 | if x 3 | pass 4 | a = 1 @@ -101,7 +101,7 @@ Module( | 1 | if x 2 | if x - | ^ Syntax Error: Expected ':', found newline + | ^ Syntax Error: Expected `:`, found newline 3 | pass 4 | a = 1 | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_expected_colon.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_expected_colon.py.snap index f352512262..da778df7a1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_expected_colon.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_expected_colon.py.snap @@ -80,6 +80,6 @@ Module( | 1 | match [1, 2] - | ^ Syntax Error: Expected ':', found newline + | ^ Syntax Error: Expected `:`, found newline 2 | case _: ... | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap index 324f3480ff..7888bcd48e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap @@ -61,7 +61,7 @@ Module( | 1 | match foo: case _: ... - | ^^^^ Syntax Error: Expected newline, found 'case' + | ^^^^ Syntax Error: Expected newline, found `case` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_clauses_on_same_line.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_clauses_on_same_line.py.snap index 0fb6c83f46..be571b2cd2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_clauses_on_same_line.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_clauses_on_same_line.py.snap @@ -326,7 +326,7 @@ Module( | 1 | if True: pass elif False: pass else: pass - | ^^^^ Syntax Error: Expected newline, found 'elif' + | ^^^^ Syntax Error: Expected newline, found `elif` 2 | if True: pass; elif False: pass; else: pass 3 | for x in iter: break else: pass | @@ -334,7 +334,7 @@ Module( | 1 | if True: pass elif False: pass else: pass - | ^^^^ Syntax Error: Expected newline, found 'else' + | ^^^^ Syntax Error: Expected newline, found `else` 2 | if True: pass; elif False: pass; else: pass 3 | for x in iter: break else: pass | @@ -343,7 +343,7 @@ Module( | 1 | if True: pass elif False: pass else: pass 2 | if True: pass; elif False: pass; else: pass - | ^^^^ Syntax Error: Expected newline, found 'elif' + | ^^^^ Syntax Error: Expected newline, found `elif` 3 | for x in iter: break else: pass 4 | for x in iter: break; else: pass | @@ -352,7 +352,7 @@ Module( | 1 | if True: pass elif False: pass else: pass 2 | if True: pass; elif False: pass; else: pass - | ^^^^ Syntax Error: Expected newline, found 'else' + | ^^^^ Syntax Error: Expected newline, found `else` 3 | for x in iter: break else: pass 4 | for x in iter: break; else: pass | @@ -362,7 +362,7 @@ Module( 1 | if True: pass elif False: pass else: pass 2 | if True: pass; elif False: pass; else: pass 3 | for x in iter: break else: pass - | ^^^^ Syntax Error: Expected newline, found 'else' + | ^^^^ Syntax Error: Expected newline, found `else` 4 | for x in iter: break; else: pass 5 | try: pass except exc: pass else: pass finally: pass | @@ -372,7 +372,7 @@ Module( 2 | if True: pass; elif False: pass; else: pass 3 | for x in iter: break else: pass 4 | for x in iter: break; else: pass - | ^^^^ Syntax Error: Expected newline, found 'else' + | ^^^^ Syntax Error: Expected newline, found `else` 5 | try: pass except exc: pass else: pass finally: pass 6 | try: pass; except exc: pass; else: pass; finally: pass | @@ -382,7 +382,7 @@ Module( 3 | for x in iter: break else: pass 4 | for x in iter: break; else: pass 5 | try: pass except exc: pass else: pass finally: pass - | ^^^^^^ Syntax Error: Expected newline, found 'except' + | ^^^^^^ Syntax Error: Expected newline, found `except` 6 | try: pass; except exc: pass; else: pass; finally: pass | @@ -391,7 +391,7 @@ Module( 3 | for x in iter: break else: pass 4 | for x in iter: break; else: pass 5 | try: pass except exc: pass else: pass finally: pass - | ^^^^ Syntax Error: Expected newline, found 'else' + | ^^^^ Syntax Error: Expected newline, found `else` 6 | try: pass; except exc: pass; else: pass; finally: pass | @@ -400,7 +400,7 @@ Module( 3 | for x in iter: break else: pass 4 | for x in iter: break; else: pass 5 | try: pass except exc: pass else: pass finally: pass - | ^^^^^^^ Syntax Error: Expected newline, found 'finally' + | ^^^^^^^ Syntax Error: Expected newline, found `finally` 6 | try: pass; except exc: pass; else: pass; finally: pass | @@ -409,7 +409,7 @@ Module( 4 | for x in iter: break; else: pass 5 | try: pass except exc: pass else: pass finally: pass 6 | try: pass; except exc: pass; else: pass; finally: pass - | ^^^^^^ Syntax Error: Expected newline, found 'except' + | ^^^^^^ Syntax Error: Expected newline, found `except` | @@ -417,7 +417,7 @@ Module( 4 | for x in iter: break; else: pass 5 | try: pass except exc: pass else: pass finally: pass 6 | try: pass; except exc: pass; else: pass; finally: pass - | ^^^^ Syntax Error: Expected newline, found 'else' + | ^^^^ Syntax Error: Expected newline, found `else` | @@ -425,5 +425,5 @@ Module( 4 | for x in iter: break; else: pass 5 | try: pass except exc: pass else: pass finally: pass 6 | try: pass; except exc: pass; else: pass; finally: pass - | ^^^^^^^ Syntax Error: Expected newline, found 'finally' + | ^^^^^^^ Syntax Error: Expected newline, found `finally` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap index d521c935f4..ab0bcdf9ca 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap @@ -238,7 +238,7 @@ Module( 1 | # even after 3.9, an unparenthesized named expression is not allowed in a slice 2 | lst[x:=1:-1] 3 | lst[1:x:=1] - | ^^ Syntax Error: Expected ']', found ':=' + | ^^ Syntax Error: Expected `]`, found `:=` 4 | lst[1:3:x:=1] | @@ -265,7 +265,7 @@ Module( 2 | lst[x:=1:-1] 3 | lst[1:x:=1] 4 | lst[1:3:x:=1] - | ^^ Syntax Error: Expected ']', found ':=' + | ^^ Syntax Error: Expected `]`, found `:=` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_quote_in_format_spec_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_quote_in_format_spec_py312.py.snap index c39b322387..7eec20f80b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_quote_in_format_spec_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_quote_in_format_spec_py312.py.snap @@ -88,5 +88,5 @@ Module( | 1 | # parse_options: {"target-version": "3.12"} 2 | f"{1:""}" # this is a ParseError on all versions - | ^ Syntax Error: f-string: expecting '}' + | ^ Syntax Error: f-string: expecting `}` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@node_range_with_gaps.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@node_range_with_gaps.py.snap index c3a8cdca25..361fe1288b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@node_range_with_gaps.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@node_range_with_gaps.py.snap @@ -106,7 +106,7 @@ Module( | 1 | def foo # comment - | ^ Syntax Error: Expected '(', found newline + | ^ Syntax Error: Expected `(`, found newline 2 | def bar(): ... 3 | def baz | @@ -115,7 +115,7 @@ Module( | 1 | def foo # comment 2 | def bar(): ... - | ^^^ Syntax Error: Expected ')', found 'def' + | ^^^ Syntax Error: Expected `)`, found `def` 3 | def baz | @@ -124,12 +124,12 @@ Module( 1 | def foo # comment 2 | def bar(): ... 3 | def baz - | ^ Syntax Error: Expected '(', found newline + | ^ Syntax Error: Expected `(`, found newline | | 2 | def bar(): ... 3 | def baz - | ^ Syntax Error: Expected ')', found end of file + | ^ Syntax Error: Expected `)`, found end of file | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap index 149cc7b4ce..d6b1a5944e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap @@ -255,7 +255,7 @@ Module( 1 | def foo(arg: *int): ... 2 | def foo(arg: yield int): ... 3 | def foo(arg: x := int): ... - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_expected_after_star_separator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_expected_after_star_separator.py.snap index b1d8bdaa92..f343b562c6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_expected_after_star_separator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_expected_after_star_separator.py.snap @@ -251,7 +251,7 @@ Module( | 1 | def foo(*): ... - | ^ Syntax Error: Expected one or more keyword parameter after '*' separator + | ^ Syntax Error: Expected one or more keyword parameter after `*` separator 2 | def foo(*,): ... 3 | def foo(a, *): ... | @@ -260,7 +260,7 @@ Module( | 1 | def foo(*): ... 2 | def foo(*,): ... - | ^ Syntax Error: Expected one or more keyword parameter after '*' separator + | ^ Syntax Error: Expected one or more keyword parameter after `*` separator 3 | def foo(a, *): ... 4 | def foo(a, *,): ... | @@ -270,7 +270,7 @@ Module( 1 | def foo(*): ... 2 | def foo(*,): ... 3 | def foo(a, *): ... - | ^ Syntax Error: Expected one or more keyword parameter after '*' separator + | ^ Syntax Error: Expected one or more keyword parameter after `*` separator 4 | def foo(a, *,): ... 5 | def foo(*, **kwargs): ... | @@ -280,7 +280,7 @@ Module( 2 | def foo(*,): ... 3 | def foo(a, *): ... 4 | def foo(a, *,): ... - | ^ Syntax Error: Expected one or more keyword parameter after '*' separator + | ^ Syntax Error: Expected one or more keyword parameter after `*` separator 5 | def foo(*, **kwargs): ... | @@ -289,5 +289,5 @@ Module( 3 | def foo(a, *): ... 4 | def foo(a, *,): ... 5 | def foo(*, **kwargs): ... - | ^^^^^^^^ Syntax Error: Expected one or more keyword parameter after '*' separator + | ^^^^^^^^ Syntax Error: Expected one or more keyword parameter after `*` separator | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_kwarg_after_star_separator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_kwarg_after_star_separator.py.snap index 324b7246ea..589d8905af 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_kwarg_after_star_separator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_kwarg_after_star_separator.py.snap @@ -67,5 +67,5 @@ Module( | 1 | def foo(*, **kwargs): ... - | ^^^^^^^^ Syntax Error: Expected one or more keyword parameter after '*' separator + | ^^^^^^^^ Syntax Error: Expected one or more keyword parameter after `*` separator | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_keyword_with_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_keyword_with_default.py.snap index 3a8644b3a8..87adc006a3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_keyword_with_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_keyword_with_default.py.snap @@ -165,19 +165,19 @@ Module( | 1 | def foo(a, **kwargs={'b': 1, 'c': 2}): ... - | ^ Syntax Error: Parameter with '*' or '**' cannot have default value + | ^ Syntax Error: Parameter with `*` or `**` cannot have default value | | 1 | def foo(a, **kwargs={'b': 1, 'c': 2}): ... - | ^ Syntax Error: Expected ')', found '{' + | ^ Syntax Error: Expected `)`, found `{` | | 1 | def foo(a, **kwargs={'b': 1, 'c': 2}): ... - | ^ Syntax Error: Expected newline, found ')' + | ^ Syntax Error: Expected newline, found `)` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_positional_with_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_positional_with_default.py.snap index 1e84abfa40..e965d4a0b4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_positional_with_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_positional_with_default.py.snap @@ -117,19 +117,19 @@ Module( | 1 | def foo(a, *args=(1, 2)): ... - | ^ Syntax Error: Parameter with '*' or '**' cannot have default value + | ^ Syntax Error: Parameter with `*` or `**` cannot have default value | | 1 | def foo(a, *args=(1, 2)): ... - | ^ Syntax Error: Expected ')', found '(' + | ^ Syntax Error: Expected `)`, found `(` | | 1 | def foo(a, *args=(1, 2)): ... - | ^ Syntax Error: Expected newline, found ')' + | ^ Syntax Error: Expected newline, found `)` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap index f6650bb5d9..5328fcf7dd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap @@ -298,7 +298,7 @@ Module( 3 | def foo(a, /, b, /): ... 4 | def foo(a, *args, /, b): ... 5 | def foo(a, //): ... - | ^^ Syntax Error: Expected ',', found '//' + | ^^ Syntax Error: Expected `,`, found `//` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap index 45a5b27c78..61f3230855 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap @@ -785,7 +785,7 @@ Module( | 1 | # No indentation before the function definition 2 | if call(foo - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 3 | def bar(): 4 | pass | @@ -803,7 +803,7 @@ Module( | 7 | # Indented function definition 8 | if call(foo - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 9 | def bar(): 10 | pass | @@ -812,7 +812,7 @@ Module( | 13 | # There are multiple non-logical newlines (blank lines) in the `if` body 14 | if call(foo - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 15 | 16 | 17 | def bar(): @@ -822,7 +822,7 @@ Module( | 21 | # There are trailing whitespaces in the blank line inside the `if` body 22 | if call(foo - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 23 | 24 | def bar(): 25 | pass @@ -832,7 +832,7 @@ Module( | 28 | # The lexer is nested with multiple levels of parentheses 29 | if call(foo, [a, b - | ^ Syntax Error: Expected ']', found NonLogicalNewline + | ^ Syntax Error: Expected `]`, found NonLogicalNewline 30 | def bar(): 31 | pass | @@ -841,7 +841,7 @@ Module( | 34 | # The outer parenthesis is closed but the inner bracket isn't 35 | if call(foo, [a, b) - | ^ Syntax Error: Expected ']', found ')' + | ^ Syntax Error: Expected `]`, found `)` 36 | def bar(): 37 | pass | @@ -850,7 +850,7 @@ Module( | 34 | # The outer parenthesis is closed but the inner bracket isn't 35 | if call(foo, [a, b) - | ^ Syntax Error: Expected ':', found newline + | ^ Syntax Error: Expected `:`, found newline 36 | def bar(): 37 | pass | @@ -860,7 +860,7 @@ Module( 41 | # test is to make sure it emits a `NonLogicalNewline` token after `b`. 42 | if call(foo, [a, 43 | b - | ^ Syntax Error: Expected ']', found NonLogicalNewline + | ^ Syntax Error: Expected `]`, found NonLogicalNewline 44 | ) 45 | def bar(): 46 | pass @@ -871,7 +871,7 @@ Module( 42 | if call(foo, [a, 43 | b 44 | ) - | ^ Syntax Error: Expected ':', found newline + | ^ Syntax Error: Expected `:`, found newline 45 | def bar(): 46 | pass | @@ -890,7 +890,7 @@ Module( 49 | # F-strings uses normal list parsing, so test those as well 50 | if call(f"hello {x 51 | def bar(): - | ^^^ Syntax Error: f-string: expecting '}' + | ^^^ Syntax Error: f-string: expecting `}` 52 | pass | @@ -923,7 +923,7 @@ Module( | 55 | if call(f"hello 56 | def bar(): - | ^^^^ Syntax Error: Expected ',', found indent + | ^^^^ Syntax Error: Expected `,`, found indent 57 | pass | @@ -931,7 +931,7 @@ Module( | 55 | if call(f"hello 56 | def bar(): - | ^^^ Syntax Error: Expected ')', found 'def' + | ^^^ Syntax Error: Expected `)`, found `def` 57 | pass | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap index d9082066b8..5567459c70 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap @@ -113,5 +113,5 @@ Module( | 1 | if call(foo, [a, b def bar(): pass - | ^ Syntax Error: Expected ']', found NonLogicalNewline + | ^ Syntax Error: Expected `]`, found NonLogicalNewline | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap index c3b23f4fbd..ae03fee095 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap @@ -113,7 +113,7 @@ Module( | 1 | if call(foo, [a, b - | ^ Syntax Error: Expected ']', found NonLogicalNewline + | ^ Syntax Error: Expected `]`, found NonLogicalNewline 2 | def bar(): 3 | pass | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap index 1e2ad65fcf..d836318aea 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap @@ -399,7 +399,7 @@ Module( | 5 | f'middle {'string':\ 6 | 'format spec'} - | ^ Syntax Error: f-string: expecting '}' + | ^ Syntax Error: f-string: expecting `}` 7 | 8 | f'middle {'string':\\ | @@ -445,7 +445,7 @@ Module( 6 | 'format spec'} 7 | 8 | f'middle {'string':\\ - | ^ Syntax Error: f-string: expecting '}' + | ^ Syntax Error: f-string: expecting `}` 9 | 'format spec'} 10 | 11 | f'middle {'string':\\\ @@ -492,7 +492,7 @@ Module( | 11 | f'middle {'string':\\\ 12 | 'format spec'} - | ^ Syntax Error: f-string: expecting '}' + | ^ Syntax Error: f-string: expecting `}` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_1.py.snap index 54c66a5216..21f4465c4e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_1.py.snap @@ -110,7 +110,7 @@ Module( | 1 | call(a, b, \\\ 2 | - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 3 | def bar(): 4 | pass | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_windows_eol.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_windows_eol.py.snap index f1ae6a18c9..cde70bca42 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_windows_eol.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_windows_eol.py.snap @@ -93,7 +93,7 @@ Module( | 1 | call(a, b, # comment \ - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 2 | 3 | def bar(): 4 | pass diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap index 1a9af6dacc..f58010350b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap @@ -83,7 +83,7 @@ Module( | 5 | f"""hello {x # comment 6 | y = 1 - | ^ Syntax Error: f-string: expecting '}' + | ^ Syntax Error: f-string: expecting `}` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap index 50bb114c7e..7a5f85ab4a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap @@ -80,5 +80,5 @@ Module( | 5 | f'''{foo:.3f 6 | ''' - | ^^^ Syntax Error: f-string: expecting '}' + | ^^^ Syntax Error: f-string: expecting `}` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap index 174ebceee4..3c611d4fbe 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap @@ -110,7 +110,7 @@ Module( | 5 | if call(f'''{x:.3f 6 | ''' - | ^^^ Syntax Error: f-string: expecting '}' + | ^^^ Syntax Error: f-string: expecting `}` 7 | pass | @@ -118,6 +118,6 @@ Module( | 5 | if call(f'''{x:.3f 6 | ''' - | ^ Syntax Error: Expected ')', found newline + | ^ Syntax Error: Expected `)`, found newline 7 | pass | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap index 9618f4200b..18b90424fa 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap @@ -375,7 +375,7 @@ Module( 9 | # on following lines. 10 | 11 | def keyword[A, await](): ... - | ^^^^^ Syntax Error: Expected an identifier, but found a keyword 'await' that cannot be used here + | ^^^^^ Syntax Error: Expected an identifier, but found a keyword `await` that cannot be used here 12 | 13 | def not_a_type_param[A, |, B](): ... | @@ -385,7 +385,7 @@ Module( 11 | def keyword[A, await](): ... 12 | 13 | def not_a_type_param[A, |, B](): ... - | ^ Syntax Error: Expected ',', found '|' + | ^ Syntax Error: Expected `,`, found `|` 14 | 15 | def multiple_commas[A,,B](): ... | @@ -433,7 +433,7 @@ Module( 17 | def multiple_trailing_commas[A,,](): ... 18 | 19 | def multiple_commas_and_recovery[A,,100](): ... - | ^^^ Syntax Error: Expected ']', found int + | ^^^ Syntax Error: Expected `]`, found int | @@ -441,7 +441,7 @@ Module( 17 | def multiple_trailing_commas[A,,](): ... 18 | 19 | def multiple_commas_and_recovery[A,,100](): ... - | ^ Syntax Error: Expected newline, found ']' + | ^ Syntax Error: Expected newline, found `]` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap index 886c21590c..780f943a96 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap @@ -40,7 +40,7 @@ Module( | 1 | # FIXME(micha): This creates two syntax errors instead of just one (and overlapping ones) 2 | if True)): - | ^ Syntax Error: Expected ':', found ')' + | ^ Syntax Error: Expected `:`, found `)` 3 | pass | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap index 34e3500f26..4bd787208c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap @@ -111,7 +111,7 @@ Module( 2 | # This `as` pattern is unparenthesied so the parser never takes the path 3 | # where it might be confused as a complex literal pattern. 4 | case x as y + 1j: - | ^ Syntax Error: Expected ':', found '+' + | ^ Syntax Error: Expected `:`, found `+` 5 | pass | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap index 07cd1a64e6..ad4dbb0ec8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap @@ -113,7 +113,7 @@ Module( 2 | # Not in the mapping start token set, so the list parsing bails 3 | # v 4 | case {(x as y): 1}: - | ^ Syntax Error: Expected '}', found '(' + | ^ Syntax Error: Expected `}`, found `(` 5 | pass | @@ -131,7 +131,7 @@ Module( 2 | # Not in the mapping start token set, so the list parsing bails 3 | # v 4 | case {(x as y): 1}: - | ^ Syntax Error: Expected newline, found '}' + | ^ Syntax Error: Expected newline, found `}` 5 | pass | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_4.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_4.py.snap index 771706eaed..fde6679f01 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_4.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_4.py.snap @@ -114,7 +114,7 @@ Module( 2 | # This `as` pattern is unparenthesized so the parser never takes the path 3 | # where it might be confused as a mapping key pattern. 4 | case {x as y: 1}: - | ^^ Syntax Error: Expected ':', found 'as' + | ^^ Syntax Error: Expected `:`, found `as` 5 | pass | @@ -123,6 +123,6 @@ Module( 2 | # This `as` pattern is unparenthesized so the parser never takes the path 3 | # where it might be confused as a mapping key pattern. 4 | case {x as y: 1}: - | ^ Syntax Error: Expected ',', found name + | ^ Syntax Error: Expected `,`, found name 5 | pass | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap index 5592524488..1fbaa9df86 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap @@ -540,7 +540,7 @@ Module( 1 | # Starred expression is not allowed as a mapping pattern key 2 | match subject: 3 | case {*key}: - | ^ Syntax Error: Expected ':', found '}' + | ^ Syntax Error: Expected `:`, found `}` 4 | pass 5 | case {*key: 1}: | @@ -570,7 +570,7 @@ Module( 5 | case {*key: 1}: 6 | pass 7 | case {*key 1}: - | ^ Syntax Error: Expected ':', found int + | ^ Syntax Error: Expected `:`, found int 8 | pass 9 | case {*key, None: 1}: | @@ -589,7 +589,7 @@ Module( 7 | case {*key 1}: 8 | pass 9 | case {*key, None: 1}: - | ^ Syntax Error: Expected ':', found ',' + | ^ Syntax Error: Expected `:`, found `,` 10 | pass | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__star_pattern_usage.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__star_pattern_usage.py.snap index 364d382ff2..d87687110b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__star_pattern_usage.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__star_pattern_usage.py.snap @@ -580,7 +580,7 @@ Module( 15 | case Foo(x=*_): 16 | pass 17 | case {*_}: - | ^ Syntax Error: Expected ':', found '}' + | ^ Syntax Error: Expected `:`, found `}` 18 | pass 19 | case {*_: 1}: | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__ambiguous_lpar_with_items.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__ambiguous_lpar_with_items.py.snap index 9d872e920e..61ff19b25d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__ambiguous_lpar_with_items.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__ambiguous_lpar_with_items.py.snap @@ -1580,7 +1580,7 @@ Module( | 4 | with (item1, item2),: ... 5 | with (item1, item2), as f: ... - | ^^ Syntax Error: Expected ',', found 'as' + | ^^ Syntax Error: Expected `,`, found `as` 6 | with (item1, item2), item3,: ... 7 | with (*item): ... | @@ -1640,7 +1640,7 @@ Module( 9 | with (item := 10 as f): ... 10 | with (item1, item2 := 10 as f): ... 11 | with (x for x in range(10), item): ... - | ^ Syntax Error: Expected ')', found ',' + | ^ Syntax Error: Expected `)`, found `,` 12 | with (item, x for x in range(10)): ... | @@ -1649,7 +1649,7 @@ Module( 9 | with (item := 10 as f): ... 10 | with (item1, item2 := 10 as f): ... 11 | with (x for x in range(10), item): ... - | ^ Syntax Error: Expected ',', found ')' + | ^ Syntax Error: Expected `,`, found `)` 12 | with (item, x for x in range(10)): ... | @@ -1658,7 +1658,7 @@ Module( 10 | with (item1, item2 := 10 as f): ... 11 | with (x for x in range(10), item): ... 12 | with (item, x for x in range(10)): ... - | ^^^ Syntax Error: Expected ')', found 'for' + | ^^^ Syntax Error: Expected `)`, found `for` 13 | 14 | # Make sure the parser doesn't report the same error twice | @@ -1668,7 +1668,7 @@ Module( 10 | with (item1, item2 := 10 as f): ... 11 | with (x for x in range(10), item): ... 12 | with (item, x for x in range(10)): ... - | ^ Syntax Error: Expected ':', found ')' + | ^ Syntax Error: Expected `:`, found `)` 13 | 14 | # Make sure the parser doesn't report the same error twice | @@ -1707,7 +1707,7 @@ Module( 15 | with ((*item)): ... 16 | 17 | with (*x for x in iter, item): ... - | ^ Syntax Error: Expected ')', found ',' + | ^ Syntax Error: Expected `)`, found `,` 18 | with (item1, *x for x in iter, item2): ... 19 | with (x as f, *y): ... | @@ -1717,7 +1717,7 @@ Module( 15 | with ((*item)): ... 16 | 17 | with (*x for x in iter, item): ... - | ^ Syntax Error: Expected ',', found ')' + | ^ Syntax Error: Expected `,`, found `)` 18 | with (item1, *x for x in iter, item2): ... 19 | with (x as f, *y): ... | @@ -1726,7 +1726,7 @@ Module( | 17 | with (*x for x in iter, item): ... 18 | with (item1, *x for x in iter, item2): ... - | ^^^ Syntax Error: Expected ')', found 'for' + | ^^^ Syntax Error: Expected `)`, found `for` 19 | with (x as f, *y): ... 20 | with (*x, y as f): ... | @@ -1735,7 +1735,7 @@ Module( | 17 | with (*x for x in iter, item): ... 18 | with (item1, *x for x in iter, item2): ... - | ^ Syntax Error: Expected ':', found ')' + | ^ Syntax Error: Expected `:`, found `)` 19 | with (x as f, *y): ... 20 | with (*x, y as f): ... | @@ -1804,7 +1804,7 @@ Module( 22 | with (x, yield y, z): ... 23 | with (x, yield from y): ... 24 | with (x as f, y) as f: ... - | ^^ Syntax Error: Expected ':', found 'as' + | ^^ Syntax Error: Expected `:`, found `as` 25 | with (x for x in iter as y): ... | @@ -1813,7 +1813,7 @@ Module( 23 | with (x, yield from y): ... 24 | with (x as f, y) as f: ... 25 | with (x for x in iter as y): ... - | ^^ Syntax Error: Expected ')', found 'as' + | ^^ Syntax Error: Expected `)`, found `as` 26 | 27 | # The inner `(...)` is parsed as parenthesized expression | @@ -1823,7 +1823,7 @@ Module( 23 | with (x, yield from y): ... 24 | with (x as f, y) as f: ... 25 | with (x for x in iter as y): ... - | ^ Syntax Error: Expected ',', found ')' + | ^ Syntax Error: Expected `,`, found `)` 26 | 27 | # The inner `(...)` is parsed as parenthesized expression | @@ -1832,7 +1832,7 @@ Module( | 27 | # The inner `(...)` is parsed as parenthesized expression 28 | with ((item as f)): ... - | ^^ Syntax Error: Expected ')', found 'as' + | ^^ Syntax Error: Expected `)`, found `as` 29 | 30 | with (item as f), x: ... | @@ -1841,7 +1841,7 @@ Module( | 27 | # The inner `(...)` is parsed as parenthesized expression 28 | with ((item as f)): ... - | ^ Syntax Error: Expected ':', found ')' + | ^ Syntax Error: Expected `:`, found `)` 29 | 30 | with (item as f), x: ... | @@ -1860,7 +1860,7 @@ Module( 28 | with ((item as f)): ... 29 | 30 | with (item as f), x: ... - | ^ Syntax Error: Expected ':', found ',' + | ^ Syntax Error: Expected `:`, found `,` 31 | with (item as f1) as f2: ... 32 | with (item1 as f, item2 := 0): ... | @@ -1869,7 +1869,7 @@ Module( | 30 | with (item as f), x: ... 31 | with (item as f1) as f2: ... - | ^^ Syntax Error: Expected ':', found 'as' + | ^^ Syntax Error: Expected `:`, found `as` 32 | with (item1 as f, item2 := 0): ... | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unparenthesized_with_items.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unparenthesized_with_items.py.snap index 7e2d808bc6..daebbd0a96 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unparenthesized_with_items.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unparenthesized_with_items.py.snap @@ -401,5 +401,5 @@ Module( 7 | with *item1, item2 as f: pass 8 | with item1 as f, *item2: pass 9 | with item := 0 as f: pass - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap index 0d23f0c0d2..0de7376e01 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap @@ -118,7 +118,7 @@ Module( | 1 | # parse_options: {"target-version": "3.14"} 2 | t"{lambda x: x}" - | ^^ Syntax Error: t-string: expecting '}' + | ^^ Syntax Error: t-string: expecting `}` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap index 4ff0a7d78f..f39f719d1d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap @@ -259,7 +259,7 @@ Module( 1 | # parse_options: {"target-version": "3.14"} 2 | t"{" 3 | t"{foo!r" - | ^ Syntax Error: t-string: expecting '}' + | ^ Syntax Error: t-string: expecting `}` 4 | t"{foo=" 5 | t"{" | @@ -269,7 +269,7 @@ Module( 2 | t"{" 3 | t"{foo!r" 4 | t"{foo=" - | ^ Syntax Error: t-string: expecting '}' + | ^ Syntax Error: t-string: expecting `}` 5 | t"{" 6 | t"""{""" | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap index bc20f6172c..9789ed8922 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap @@ -143,7 +143,7 @@ Module( | 1 | # parse_options: {"target-version": "3.14"} 2 | t"hello {x:" - | ^ Syntax Error: t-string: expecting '}' + | ^ Syntax Error: t-string: expecting `}` 3 | t"hello {x:.3f" | @@ -152,5 +152,5 @@ Module( 1 | # parse_options: {"target-version": "3.14"} 2 | t"hello {x:" 3 | t"hello {x:.3f" - | ^ Syntax Error: t-string: expecting '}' + | ^ Syntax Error: t-string: expecting `}` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap index 321704cd05..7876c6b6ec 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap @@ -288,7 +288,7 @@ Module( 2 | type X[T: yield x] = int 3 | type X[T: yield from x] = int 4 | type X[T: x := int] = int - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap index de9da4848d..351a141b60 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap @@ -88,7 +88,7 @@ Module( | 1 | type X[**T: int] = int - | ^ Syntax Error: Expected ']', found ':' + | ^ Syntax Error: Expected `]`, found `:` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap index dad7c709de..0bcbd3cc52 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap @@ -343,7 +343,7 @@ Module( 2 | type X[**P = yield x] = int 3 | type X[**P = yield from x] = int 4 | type X[**P = x := int] = int - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` 5 | type X[**P = *int] = int | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap index 2831009c20..c3e38b8e9a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap @@ -417,7 +417,7 @@ Module( 3 | type X[T = (yield x)] = int 4 | type X[T = yield from x] = int 5 | type X[T = x := int] = int - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` 6 | type X[T: int = *int] = int | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap index e1693e1722..ae228c0e30 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap @@ -88,7 +88,7 @@ Module( | 1 | type X[*T: int] = int - | ^ Syntax Error: Expected ']', found ':' + | ^ Syntax Error: Expected `]`, found `:` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap index 9b2d1c6de9..4aff137a73 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap @@ -361,7 +361,7 @@ Module( 3 | type X[*Ts = yield x] = int 4 | type X[*Ts = yield from x] = int 5 | type X[*Ts = x := int] = int - | ^^ Syntax Error: Expected ',', found ':=' + | ^^ Syntax Error: Expected `,`, found `:=` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap index 0595f124f2..7ed7f32534 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap @@ -376,7 +376,7 @@ Module( 2 | 1 + 1 3 | f"hello {x 4 | 2 + 2 - | ^ Syntax Error: f-string: expecting '}' + | ^ Syntax Error: f-string: expecting `}` 5 | f"hello {x: 6 | 3 + 3 | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_invalid_test_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_invalid_test_expr.py.snap index 0af23c288c..acdd7532ad 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_invalid_test_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_invalid_test_expr.py.snap @@ -201,7 +201,7 @@ Module( 1 | while *x: ... 2 | while yield x: ... 3 | while a, b: ... - | ^ Syntax Error: Expected ':', found ',' + | ^ Syntax Error: Expected `:`, found `,` 4 | while a := 1, b: ... | @@ -210,5 +210,5 @@ Module( 2 | while yield x: ... 3 | while a, b: ... 4 | while a := 1, b: ... - | ^ Syntax Error: Expected ':', found ',' + | ^ Syntax Error: Expected `:`, found `,` | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_missing_colon.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_missing_colon.py.snap index 67fb75a824..9ad6e62132 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_missing_colon.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_missing_colon.py.snap @@ -63,6 +63,6 @@ Module( 1 | while ( 2 | a < 30 # comment 3 | ) - | ^ Syntax Error: Expected ':', found newline + | ^ Syntax Error: Expected `:`, found newline 4 | pass | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_colon.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_colon.py.snap index 668c7c2c08..817368aa22 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_colon.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_colon.py.snap @@ -62,6 +62,6 @@ Module( | 1 | # `)` followed by a newline 2 | with (item1, item2) - | ^ Syntax Error: Expected ':', found newline + | ^ Syntax Error: Expected `:`, found newline 3 | pass | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_comma.py.snap index ea060453a9..0c4b117dcf 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_comma.py.snap @@ -338,7 +338,7 @@ Module( | 1 | with (item1 item2): ... - | ^^^^^ Syntax Error: Expected ',', found name + | ^^^^^ Syntax Error: Expected `,`, found name 2 | with (item1 as f1 item2): ... 3 | with (item1, item2 item3, item4): ... | @@ -347,7 +347,7 @@ Module( | 1 | with (item1 item2): ... 2 | with (item1 as f1 item2): ... - | ^^^^^ Syntax Error: Expected ',', found name + | ^^^^^ Syntax Error: Expected `,`, found name 3 | with (item1, item2 item3, item4): ... 4 | with (item1, item2 as f1 item3, item4): ... | @@ -357,7 +357,7 @@ Module( 1 | with (item1 item2): ... 2 | with (item1 as f1 item2): ... 3 | with (item1, item2 item3, item4): ... - | ^^^^^ Syntax Error: Expected ',', found name + | ^^^^^ Syntax Error: Expected `,`, found name 4 | with (item1, item2 as f1 item3, item4): ... 5 | with (item1, item2: ... | @@ -367,7 +367,7 @@ Module( 2 | with (item1 as f1 item2): ... 3 | with (item1, item2 item3, item4): ... 4 | with (item1, item2 as f1 item3, item4): ... - | ^^^^^ Syntax Error: Expected ',', found name + | ^^^^^ Syntax Error: Expected `,`, found name 5 | with (item1, item2: ... | @@ -376,5 +376,5 @@ Module( 3 | with (item1, item2 item3, item4): ... 4 | with (item1, item2 as f1 item3, item4): ... 5 | with (item1, item2: ... - | ^ Syntax Error: Expected ')', found ':' + | ^ Syntax Error: Expected `)`, found `:` | diff --git a/crates/ty_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md index f16ec0505d..fd2a6ef0cf 100644 --- a/crates/ty_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md +++ b/crates/ty_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md @@ -1,20 +1,20 @@ # Comprehensions with invalid syntax ```py -# Missing 'in' keyword. +# Missing `in` keyword. # It's reasonably clear here what they *meant* to write, # so we'll still infer the correct type: -# error: [invalid-syntax] "Expected 'in', found name" +# error: [invalid-syntax] "Expected `in`, found name" # revealed: int [reveal_type(a) for a range(3)] # Missing iteration variable -# error: [invalid-syntax] "Expected an identifier, but found a keyword 'in' that cannot be used here" -# error: [invalid-syntax] "Expected 'in', found name" +# error: [invalid-syntax] "Expected an identifier, but found a keyword `in` that cannot be used here" +# error: [invalid-syntax] "Expected `in`, found name" # error: [unresolved-reference] # revealed: Unknown [reveal_type(b) for in range(3)] @@ -27,9 +27,9 @@ [reveal_type(c) for c in] -# Missing 'in' keyword and missing iterable +# Missing `in` keyword and missing iterable -# error: [invalid-syntax] "Expected 'in', found ']'" +# error: [invalid-syntax] "Expected `in`, found `]`" # revealed: Unknown [reveal_type(d) for d] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/import/invalid_syntax.md index 6b7423f86d..3b8ef67e72 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/invalid_syntax.md +++ b/crates/ty_python_semantic/resources/mdtest/import/invalid_syntax.md @@ -14,7 +14,7 @@ TODO: This is correctly flagged as an error, but we could clean up the diagnosti ```py # TODO: No second diagnostic -# error: [invalid-syntax] "Expected ',', found '.'" +# error: [invalid-syntax] "Expected `,`, found `.`" # error: [unresolved-import] "Module `a` has no member `c`" from a import b.c From 9664474c5172c996a0fe5b9053e14a345629e166 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 31 Oct 2025 12:06:47 -0400 Subject: [PATCH 103/188] [ty] rollback preferring declared type on invalid TypedDict creation (#21169) ## Summary Discussion with @ibraheemdev clarified that https://github.com/astral-sh/ruff/pull/21168 was incorrect. In a case of failed inference of a dict literal as a `TypedDict`, we should store the context-less inferred type of the dict literal as the type of the dict literal expression itself; the fallback to declared type should happen at the level of the overall assignment definition. The reason the latter isn't working yet is because currently we (wrongly) consider a homogeneous dict type as assignable to a `TypedDict`, so we don't actually consider the assignment itself as failed. So the "bug" I observed (and tried to fix) will naturally be fixed by implementing TypedDict assignability rules. Rollback https://github.com/astral-sh/ruff/pull/21168 except for the tests, and modify the tests to include TODOs as needed. ## Test Plan Updated mdtests. --- .../resources/mdtest/typed_dict.md | 9 ++++++--- .../ty_python_semantic/src/types/infer/builder.rs | 10 ++++++---- crates/ty_python_semantic/src/types/typed_dict.rs | 15 +++++++++++---- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 8be6de4ef3..14142020a2 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -99,7 +99,8 @@ eve1a: Person = {"name": b"Eve", "age": None} # error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`" eve1b = Person(name=b"Eve", age=None) -reveal_type(eve1a) # revealed: Person +# TODO should reveal Person (should be fixed by implementing assignability for TypedDicts) +reveal_type(eve1a) # revealed: dict[Unknown | str, Unknown | bytes | None] reveal_type(eve1b) # revealed: Person # error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor" @@ -107,7 +108,8 @@ eve2a: Person = {"age": 22} # error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor" eve2b = Person(age=22) -reveal_type(eve2a) # revealed: Person +# TODO should reveal Person (should be fixed by implementing assignability for TypedDicts) +reveal_type(eve2a) # revealed: dict[Unknown | str, Unknown | int] reveal_type(eve2b) # revealed: Person # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" @@ -115,7 +117,8 @@ eve3a: Person = {"name": "Eve", "age": 25, "extra": True} # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" eve3b = Person(name="Eve", age=25, extra=True) -reveal_type(eve3a) # revealed: Person +# TODO should reveal Person (should be fixed by implementing assignability for TypedDicts) +reveal_type(eve3a) # revealed: dict[Unknown | str, Unknown | str | int] reveal_type(eve3b) # revealed: Person ``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index f58f093a44..7094fdea07 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -6239,9 +6239,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && let Some(typed_dict) = tcx .filter_union(self.db(), Type::is_typed_dict) .as_typed_dict() + && let Some(ty) = self.infer_typed_dict_expression(dict, typed_dict) { - self.infer_typed_dict_expression(dict, typed_dict); - return Type::TypedDict(typed_dict); + return ty; } // Avoid false positives for the functional `TypedDict` form, which is currently @@ -6266,7 +6266,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, dict: &ast::ExprDict, typed_dict: TypedDictType<'db>, - ) { + ) -> Option> { let ast::ExprDict { range: _, node_index: _, @@ -6289,7 +6289,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { validate_typed_dict_dict_literal(&self.context, typed_dict, dict, dict.into(), |expr| { self.expression_type(expr) - }); + }) + .ok() + .map(|_| Type::TypedDict(typed_dict)) } // Infer the type of a collection literal expression. diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index 632d2a2933..e29b836d8a 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -389,7 +389,7 @@ fn validate_from_keywords<'db, 'ast>( provided_keys } -/// Validates a `TypedDict` dictionary literal assignment, emitting any needed diagnostics. +/// Validates a `TypedDict` dictionary literal assignment, /// e.g. `person: Person = {"name": "Alice", "age": 30}` pub(super) fn validate_typed_dict_dict_literal<'db>( context: &InferContext<'db, '_>, @@ -397,7 +397,8 @@ pub(super) fn validate_typed_dict_dict_literal<'db>( dict_expr: &ast::ExprDict, error_node: AnyNodeRef, expression_type_fn: impl Fn(&ast::Expr) -> Type<'db>, -) { +) -> Result, OrderSet<&'db str>> { + let mut valid = true; let mut provided_keys = OrderSet::new(); // Validate each key-value pair in the dictionary literal @@ -410,7 +411,7 @@ pub(super) fn validate_typed_dict_dict_literal<'db>( let value_type = expression_type_fn(&item.value); - validate_typed_dict_key_assignment( + valid &= validate_typed_dict_key_assignment( context, typed_dict, key_str, @@ -423,5 +424,11 @@ pub(super) fn validate_typed_dict_dict_literal<'db>( } } - validate_typed_dict_required_keys(context, typed_dict, &provided_keys, error_node); + valid &= validate_typed_dict_required_keys(context, typed_dict, &provided_keys, error_node); + + if valid { + Ok(provided_keys) + } else { + Err(provided_keys) + } } From ff3a6a8fbd5af1b3e9b42b4c5adb9a8954968de9 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Fri, 31 Oct 2025 12:41:14 -0400 Subject: [PATCH 104/188] [ty] Support type context of union attribute assignments (#21170) ## Summary Turns out this is easy to implement. Resolves https://github.com/astral-sh/ty/issues/1375. --- .../resources/mdtest/bidirectional.md | 20 ++++++++++++++++++- .../src/types/infer/builder.rs | 14 +++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index 1cc3dba162..3fee0513ed 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -200,7 +200,7 @@ def f() -> list[Literal[1]]: return [1] ``` -## Instance attribute +## Instance attributes ```toml [environment] @@ -235,6 +235,24 @@ def _(flag: bool): C.x = lst(1) ``` +For union targets, each element of the union is considered as a separate type context: + +```py +from typing import Literal + +class X: + x: list[int | str] + +class Y: + x: list[int | None] + +def lst[T](x: T) -> list[T]: + return [x] + +def _(xy: X | Y): + xy.x = lst(1) +``` + ## Class constructor parameters ```toml diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 7094fdea07..f6055c0a0e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3574,7 +3574,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { target: &ast::ExprAttribute, object_ty: Type<'db>, attribute: &str, - infer_value_ty: &dyn Fn(&mut Self, TypeContext<'db>) -> Type<'db>, + infer_value_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, emit_diagnostics: bool, ) -> bool { let db = self.db(); @@ -3651,7 +3651,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match object_ty { Type::Union(union) => { - // TODO: We could perform multi-inference here with each element of the union as type context. + // 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| { @@ -3659,7 +3659,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { target, *elem, attribute, - &|_, _| value_ty, + // Note that `infer_value_ty` silences diagnostics after the first inference. + &mut infer_value_ty, false, ) }) { @@ -3684,7 +3685,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } Type::Intersection(intersection) => { - // TODO: We could perform multi-inference here with each element of the union as type context. + // 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 @@ -3693,7 +3694,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { target, *elem, attribute, - &|_, _| value_ty, + // Note that `infer_value_ty` silences diagnostics after the first inference. + &mut infer_value_ty, false, ) }) { @@ -4254,7 +4256,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let object_ty = self.infer_expression(object, TypeContext::default()); if let Some(infer_assigned_ty) = infer_assigned_ty { - let infer_assigned_ty = &|builder: &mut Self, tcx| { + 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 From 1734ddfb3e6393b0cd45b2b1d3f170cc102b2fcf Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Oct 2025 17:48:34 +0100 Subject: [PATCH 105/188] [ty] Do not promote literals in contravariant positions of generic specializations (#21171) ## Summary closes https://github.com/astral-sh/ty/issues/1284 supersedes https://github.com/astral-sh/ruff/pull/20950 by @ibraheemdev ## Test Plan New regression test --- .../resources/mdtest/literal_promotion.md | 36 +++++++++++++++++++ .../ty_python_semantic/src/types/generics.rs | 9 +++-- .../ty_python_semantic/src/types/variance.rs | 7 ++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/literal_promotion.md b/crates/ty_python_semantic/resources/mdtest/literal_promotion.md index 726ca59d20..f13d3229ee 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal_promotion.md +++ b/crates/ty_python_semantic/resources/mdtest/literal_promotion.md @@ -1,5 +1,10 @@ # Literal promotion +```toml +[environment] +python-version = "3.12" +``` + There are certain places where we promote literals to their common supertype: ```py @@ -30,3 +35,34 @@ def double_negation(callback: Callable[[Callable[[Literal[1]], None]], None]): reveal_type([callback]) # revealed: list[Unknown | (((int, /) -> None, /) -> None)] ``` + +Literal promotion should also not apply recursively to type arguments in contravariant/invariant +position: + +```py +class Bivariant[T]: + pass + +class Covariant[T]: + def pop(self) -> T: + raise NotImplementedError + +class Contravariant[T]: + def push(self, value: T) -> None: + pass + +class Invariant[T]: + x: T + +def _( + bivariant: Bivariant[Literal[1]], + covariant: Covariant[Literal[1]], + contravariant: Contravariant[Literal[1]], + invariant: Invariant[Literal[1]], +): + reveal_type([bivariant]) # revealed: list[Unknown | Bivariant[int]] + reveal_type([covariant]) # revealed: list[Unknown | Covariant[int]] + + reveal_type([contravariant]) # revealed: list[Unknown | Contravariant[Literal[1]]] + reveal_type([invariant]) # revealed: list[Unknown | Invariant[Literal[1]]] +``` diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 8485931ff2..98f7cb736f 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -969,10 +969,15 @@ impl<'db> Specialization<'db> { let types: Box<[_]> = self .types(db) .iter() + .zip(self.generic_context(db).variables(db)) .enumerate() - .map(|(i, ty)| { + .map(|(i, (ty, typevar))| { let tcx = TypeContext::new(tcx.get(i).copied()); - ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + if typevar.variance(db).is_covariant() { + ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + } else { + ty.apply_type_mapping_impl(db, &type_mapping.flip(), tcx, visitor) + } }) .collect(); diff --git a/crates/ty_python_semantic/src/types/variance.rs b/crates/ty_python_semantic/src/types/variance.rs index fb9c87d062..5ec1d5a8ff 100644 --- a/crates/ty_python_semantic/src/types/variance.rs +++ b/crates/ty_python_semantic/src/types/variance.rs @@ -85,6 +85,13 @@ impl TypeVarVariance { TypeVarVariance::Bivariant => TypeVarVariance::Bivariant, } } + + pub(crate) const fn is_covariant(self) -> bool { + matches!( + self, + TypeVarVariance::Covariant | TypeVarVariance::Bivariant + ) + } } impl std::iter::FromIterator for TypeVarVariance { From 827d8ae5d4b67c33c567a855236b4d058d366b20 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:53:40 -0400 Subject: [PATCH 106/188] Allow newlines after function headers without docstrings (#21110) Summary -- This is a first step toward fixing #9745. After reviewing our open issues and several Black issues and PRs, I personally found the function case the most compelling, especially with very long argument lists: ```py def func( self, arg1: int, arg2: bool, arg3: bool, arg4: float, arg5: bool, ) -> tuple[...]: if arg2 and arg3: raise ValueError ``` or many annotations: ```py def function( self, data: torch.Tensor | tuple[torch.Tensor, ...], other_argument: int ) -> torch.Tensor | tuple[torch.Tensor, ...]: do_something(data) return something ``` I think docstrings help the situation substantially both because syntax highlighting will usually give a very clear separation between the annotations and the docstring and because we already allow a blank line _after_ the docstring: ```py def function( self, data: torch.Tensor | tuple[torch.Tensor, ...], other_argument: int ) -> torch.Tensor | tuple[torch.Tensor, ...]: """ A function doing something. And a longer description of the things it does. """ do_something(data) return something ``` There are still other comments on #9745, such as [this one] with 9 upvotes, where users specifically request blank lines in all block types, or at least including conditionals and loops. I'm sympathetic to that case as well, even if personally I don't find an [example] like this: ```py if blah: # Do some stuff that is logically related data = get_data() # Do some different stuff that is logically related results = calculate_results() return results ``` to be much more readable than: ```py if blah: # Do some stuff that is logically related data = get_data() # Do some different stuff that is logically related results = calculate_results() return results ``` I'm probably just used to the latter from the formatters I've used, but I do prefer it. I also think that functions are the least susceptible to the accidental introduction of a newline after refactoring described in Micha's [comment] on #8893. I actually considered further restricting this change to functions with multiline headers. I don't think very short functions like: ```py def foo(): return 1 ``` benefit nearly as much from the allowed newline, but I just went with any function without a docstring for now. I guess a marginal case like: ```py def foo(a_long_parameter: ALongType, b_long_parameter: BLongType) -> CLongType: return 1 ``` might be a good argument for not restricting it. I caused a couple of syntax errors before adding special handling for the ellipsis-only case, so I suspect that there are some other interesting edge cases that may need to be handled better. Test Plan -- Existing tests, plus a few simple new ones. As noted above, I suspect that we may need a few more for edge cases I haven't considered. [this one]: https://github.com/astral-sh/ruff/issues/9745#issuecomment-2876771400 [example]: https://github.com/psf/black/issues/902#issuecomment-1562154809 [comment]: https://github.com/astral-sh/ruff/issues/8893#issuecomment-1867259744 --- .../resources/test/fixtures/ruff/newlines.py | 93 +++++++ .../fixtures/ruff/range_formatting/indent.py | 6 + crates/ruff_python_formatter/src/preview.rs | 7 + .../src/statement/clause.rs | 13 +- .../src/statement/suite.rs | 46 +++- .../tests/snapshots/format@newlines.py.snap | 254 +++++++++++++++++- .../format@range_formatting__indent.py.snap | 69 +++++ 7 files changed, 460 insertions(+), 28 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/newlines.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/newlines.py index 2afbd18229..18c810ead8 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/newlines.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/newlines.py @@ -335,3 +335,96 @@ def overload4(): # trailing comment def overload4(a: int): ... + + +# In preview, we preserve these newlines at the start of functions: +def preserved1(): + + return 1 + +def preserved2(): + + pass + +def preserved3(): + + def inner(): ... + +def preserved4(): + + def inner(): + print("with a body") + return 1 + + return 2 + +def preserved5(): + + ... + # trailing comment prevents collapsing the stub + + +def preserved6(): + + # Comment + + return 1 + + +def preserved7(): + + # comment + # another line + # and a third + + return 0 + + +def preserved8(): # this also prevents collapsing the stub + + ... + + +# But we still discard these newlines: +def removed1(): + + "Docstring" + + return 1 + + +def removed2(): + + ... + + +def removed3(): + + ... # trailing same-line comment does not prevent collapsing the stub + + +# And we discard empty lines after the first: +def partially_preserved1(): + + + return 1 + + +# We only preserve blank lines, not add new ones +def untouched1(): + # comment + + return 0 + + +def untouched2(): + # comment + return 0 + + +def untouched3(): + # comment + # another line + # and a third + + return 0 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/indent.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/indent.py index 1fb1522aa0..e10ffe55ee 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/indent.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/indent.py @@ -61,3 +61,9 @@ def test6 (): print("Format" ) print(3 + 4) print("Format to fix indentation" ) + + +def test7 (): + print("Format" ) + print(3 + 4) + print("Format to fix indentation" ) diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs index b6479ab1b4..5455fa9a12 100644 --- a/crates/ruff_python_formatter/src/preview.rs +++ b/crates/ruff_python_formatter/src/preview.rs @@ -36,3 +36,10 @@ pub(crate) const fn is_remove_parens_around_except_types_enabled( ) -> bool { context.is_preview() } + +/// Returns `true` if the +/// [`allow_newline_after_block_open`](https://github.com/astral-sh/ruff/pull/21110) preview style +/// is enabled. +pub(crate) const fn is_allow_newline_after_block_open_enabled(context: &PyFormatContext) -> bool { + context.is_preview() +} diff --git a/crates/ruff_python_formatter/src/statement/clause.rs b/crates/ruff_python_formatter/src/statement/clause.rs index a5c172f4f8..1554c30d0f 100644 --- a/crates/ruff_python_formatter/src/statement/clause.rs +++ b/crates/ruff_python_formatter/src/statement/clause.rs @@ -8,7 +8,7 @@ use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::comments::{SourceComment, leading_alternate_branch_comments, trailing_comments}; -use crate::statement::suite::{SuiteKind, contains_only_an_ellipsis}; +use crate::statement::suite::{SuiteKind, as_only_an_ellipsis}; use crate::verbatim::write_suppressed_clause_header; use crate::{has_skip_comment, prelude::*}; @@ -449,17 +449,10 @@ impl Format> for FormatClauseBody<'_> { || matches!(self.kind, SuiteKind::Function | SuiteKind::Class); if should_collapse_stub - && contains_only_an_ellipsis(self.body, f.context().comments()) + && let Some(ellipsis) = as_only_an_ellipsis(self.body, f.context().comments()) && self.trailing_comments.is_empty() { - write!( - f, - [ - space(), - self.body.format().with_options(self.kind), - hard_line_break() - ] - ) + write!(f, [space(), ellipsis.format(), hard_line_break()]) } else { write!( f, diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 4071b4ba1f..9ed32beb76 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -13,7 +13,9 @@ use crate::comments::{ use crate::context::{NodeLevel, TopLevelStatementPosition, WithIndentLevel, WithNodeLevel}; use crate::other::string_literal::StringLiteralKind; use crate::prelude::*; -use crate::preview::is_blank_line_before_decorated_class_in_stub_enabled; +use crate::preview::{ + is_allow_newline_after_block_open_enabled, is_blank_line_before_decorated_class_in_stub_enabled, +}; use crate::statement::stmt_expr::FormatStmtExpr; use crate::verbatim::{ suppressed_node, write_suppressed_statements_starting_with_leading_comment, @@ -169,6 +171,22 @@ impl FormatRule> for FormatSuite { false, ) } else { + // Allow an empty line after a function header in preview, if the function has no + // docstring and no initial comment. + let allow_newline_after_block_open = + is_allow_newline_after_block_open_enabled(f.context()) + && matches!(self.kind, SuiteKind::Function) + && matches!(first, SuiteChildStatement::Other(_)); + + let start = comments + .leading(first) + .first() + .map_or_else(|| first.start(), Ranged::start); + + if allow_newline_after_block_open && lines_before(start, f.context().source()) > 1 { + empty_line().fmt(f)?; + } + first.fmt(f)?; let empty_line_after_docstring = if matches!(first, SuiteChildStatement::Docstring(_)) @@ -218,7 +236,7 @@ impl FormatRule> for FormatSuite { )?; } else { // Preserve empty lines after a stub implementation but don't insert a new one if there isn't any present in the source. - // This is useful when having multiple function overloads that should be grouped to getter by omitting new lines between them. + // This is useful when having multiple function overloads that should be grouped together by omitting new lines between them. let is_preceding_stub_function_without_empty_line = following .is_function_def_stmt() && preceding @@ -728,17 +746,21 @@ fn stub_suite_can_omit_empty_line(preceding: &Stmt, following: &Stmt, f: &PyForm /// Returns `true` if a function or class body contains only an ellipsis with no comments. pub(crate) fn contains_only_an_ellipsis(body: &[Stmt], comments: &Comments) -> bool { - match body { - [Stmt::Expr(ast::StmtExpr { value, .. })] => { - let [node] = body else { - return false; - }; - value.is_ellipsis_literal_expr() - && !comments.has_leading(node) - && !comments.has_trailing_own_line(node) - } - _ => false, + as_only_an_ellipsis(body, comments).is_some() +} + +/// Returns `Some(Stmt::Ellipsis)` if a function or class body contains only an ellipsis with no +/// comments. +pub(crate) fn as_only_an_ellipsis<'a>(body: &'a [Stmt], comments: &Comments) -> Option<&'a Stmt> { + if let [node @ Stmt::Expr(ast::StmtExpr { value, .. })] = body + && value.is_ellipsis_literal_expr() + && !comments.has_leading(node) + && !comments.has_trailing_own_line(node) + { + return Some(node); } + + None } /// Returns `true` if a [`Stmt`] is a class or function definition. diff --git a/crates/ruff_python_formatter/tests/snapshots/format@newlines.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@newlines.py.snap index 84bd4283c4..260de915fc 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@newlines.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@newlines.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/newlines.py -snapshot_kind: text --- ## Input ```python @@ -342,6 +341,99 @@ def overload4(): # trailing comment def overload4(a: int): ... + + +# In preview, we preserve these newlines at the start of functions: +def preserved1(): + + return 1 + +def preserved2(): + + pass + +def preserved3(): + + def inner(): ... + +def preserved4(): + + def inner(): + print("with a body") + return 1 + + return 2 + +def preserved5(): + + ... + # trailing comment prevents collapsing the stub + + +def preserved6(): + + # Comment + + return 1 + + +def preserved7(): + + # comment + # another line + # and a third + + return 0 + + +def preserved8(): # this also prevents collapsing the stub + + ... + + +# But we still discard these newlines: +def removed1(): + + "Docstring" + + return 1 + + +def removed2(): + + ... + + +def removed3(): + + ... # trailing same-line comment does not prevent collapsing the stub + + +# And we discard empty lines after the first: +def partially_preserved1(): + + + return 1 + + +# We only preserve blank lines, not add new ones +def untouched1(): + # comment + + return 0 + + +def untouched2(): + # comment + return 0 + + +def untouched3(): + # comment + # another line + # and a third + + return 0 ``` ## Output @@ -732,6 +824,88 @@ def overload4(): def overload4(a: int): ... + + +# In preview, we preserve these newlines at the start of functions: +def preserved1(): + return 1 + + +def preserved2(): + pass + + +def preserved3(): + def inner(): ... + + +def preserved4(): + def inner(): + print("with a body") + return 1 + + return 2 + + +def preserved5(): + ... + # trailing comment prevents collapsing the stub + + +def preserved6(): + # Comment + + return 1 + + +def preserved7(): + # comment + # another line + # and a third + + return 0 + + +def preserved8(): # this also prevents collapsing the stub + ... + + +# But we still discard these newlines: +def removed1(): + "Docstring" + + return 1 + + +def removed2(): ... + + +def removed3(): ... # trailing same-line comment does not prevent collapsing the stub + + +# And we discard empty lines after the first: +def partially_preserved1(): + return 1 + + +# We only preserve blank lines, not add new ones +def untouched1(): + # comment + + return 0 + + +def untouched2(): + # comment + return 0 + + +def untouched3(): + # comment + # another line + # and a third + + return 0 ``` @@ -739,7 +913,15 @@ def overload4(a: int): ... ```diff --- Stable +++ Preview -@@ -277,6 +277,7 @@ +@@ -253,6 +253,7 @@ + + + def fakehttp(): ++ + class FakeHTTPConnection: + if mock_close: + +@@ -277,6 +278,7 @@ def a(): return 1 @@ -747,7 +929,7 @@ def overload4(a: int): ... else: pass -@@ -293,6 +294,7 @@ +@@ -293,6 +295,7 @@ def a(): return 1 @@ -755,7 +937,7 @@ def overload4(a: int): ... case 1: def a(): -@@ -303,6 +305,7 @@ +@@ -303,6 +306,7 @@ def a(): return 1 @@ -763,7 +945,7 @@ def overload4(a: int): ... except RuntimeError: def a(): -@@ -313,6 +316,7 @@ +@@ -313,6 +317,7 @@ def a(): return 1 @@ -771,7 +953,7 @@ def overload4(a: int): ... finally: def a(): -@@ -323,18 +327,22 @@ +@@ -323,18 +328,22 @@ def a(): return 1 @@ -794,4 +976,64 @@ def overload4(a: int): ... finally: def a(): +@@ -388,18 +397,22 @@ + + # In preview, we preserve these newlines at the start of functions: + def preserved1(): ++ + return 1 + + + def preserved2(): ++ + pass + + + def preserved3(): ++ + def inner(): ... + + + def preserved4(): ++ + def inner(): + print("with a body") + return 1 +@@ -408,17 +421,20 @@ + + + def preserved5(): ++ + ... + # trailing comment prevents collapsing the stub + + + def preserved6(): ++ + # Comment + + return 1 + + + def preserved7(): ++ + # comment + # another line + # and a third +@@ -427,6 +443,7 @@ + + + def preserved8(): # this also prevents collapsing the stub ++ + ... + + +@@ -445,6 +462,7 @@ + + # And we discard empty lines after the first: + def partially_preserved1(): ++ + return 1 + + ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__indent.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__indent.py.snap index 1609cf657e..213c843da1 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__indent.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__indent.py.snap @@ -67,6 +67,12 @@ def test6 (): print("Format" ) print(3 + 4) print("Format to fix indentation" ) + + +def test7 (): + print("Format" ) + print(3 + 4) + print("Format to fix indentation" ) ``` ## Outputs @@ -146,6 +152,27 @@ def test6 (): print("Format") print(3 + 4) print("Format to fix indentation" ) + + +def test7 (): + print("Format") + print(3 + 4) + print("Format to fix indentation" ) +``` + + +#### Preview changes +```diff +--- Stable ++++ Preview +@@ -55,6 +55,7 @@ + + + def test6 (): ++ + print("Format") + print(3 + 4) + print("Format to fix indentation" ) ``` @@ -225,6 +252,27 @@ def test6 (): print("Format") print(3 + 4) print("Format to fix indentation") + + +def test7 (): + print("Format") + print(3 + 4) + print("Format to fix indentation") +``` + + +#### Preview changes +```diff +--- Stable ++++ Preview +@@ -55,6 +55,7 @@ + + + def test6 (): ++ + print("Format") + print(3 + 4) + print("Format to fix indentation") ``` @@ -304,4 +352,25 @@ def test6 (): print("Format") print(3 + 4) print("Format to fix indentation") + + +def test7 (): + print("Format") + print(3 + 4) + print("Format to fix indentation") +``` + + +#### Preview changes +```diff +--- Stable ++++ Preview +@@ -55,6 +55,7 @@ + + + def test6 (): ++ + print("Format") + print(3 + 4) + print("Format to fix indentation") ``` From 6337e22f0c158767ae13b2ed5744424bc2291b20 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 31 Oct 2025 21:00:04 +0100 Subject: [PATCH 107/188] [ty] Smaller refactors to server API in prep for notebook support (#21095) --- crates/ruff_db/src/system/path.rs | 13 +- crates/ty_server/src/document.rs | 90 +++-- crates/ty_server/src/document/notebook.rs | 100 ++++-- .../ty_server/src/document/text_document.rs | 15 +- crates/ty_server/src/lib.rs | 2 +- crates/ty_server/src/server/api.rs | 23 +- .../ty_server/src/server/api/diagnostics.rs | 52 +-- .../server/api/notifications/did_change.rs | 23 +- .../notifications/did_change_watched_files.rs | 17 +- .../src/server/api/notifications/did_close.rs | 28 +- .../api/notifications/did_close_notebook.rs | 22 +- .../src/server/api/notifications/did_open.rs | 27 +- .../api/notifications/did_open_notebook.rs | 23 +- .../src/server/api/requests/completion.rs | 2 +- .../src/server/api/requests/doc_highlights.rs | 2 +- .../server/api/requests/document_symbols.rs | 2 +- .../server/api/requests/execute_command.rs | 2 +- .../server/api/requests/goto_declaration.rs | 2 +- .../server/api/requests/goto_definition.rs | 2 +- .../server/api/requests/goto_references.rs | 2 +- .../api/requests/goto_type_definition.rs | 2 +- .../src/server/api/requests/hover.rs | 2 +- .../src/server/api/requests/inlay_hints.rs | 2 +- .../src/server/api/requests/prepare_rename.rs | 2 +- .../src/server/api/requests/rename.rs | 2 +- .../server/api/requests/selection_range.rs | 2 +- .../server/api/requests/semantic_tokens.rs | 2 +- .../api/requests/semantic_tokens_range.rs | 2 +- .../src/server/api/requests/signature_help.rs | 2 +- .../api/requests/workspace_diagnostic.rs | 31 +- crates/ty_server/src/session.rs | 230 +++++++++---- crates/ty_server/src/session/index.rs | 314 +++++++----------- crates/ty_server/src/system.rs | 74 +---- 33 files changed, 570 insertions(+), 546 deletions(-) diff --git a/crates/ruff_db/src/system/path.rs b/crates/ruff_db/src/system/path.rs index 71a92fb4c8..a387ae54f6 100644 --- a/crates/ruff_db/src/system/path.rs +++ b/crates/ruff_db/src/system/path.rs @@ -723,10 +723,11 @@ impl ruff_cache::CacheKey for SystemPathBuf { /// A slice of a virtual path on [`System`](super::System) (akin to [`str`]). #[repr(transparent)] +#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)] pub struct SystemVirtualPath(str); impl SystemVirtualPath { - pub fn new(path: &str) -> &SystemVirtualPath { + pub const fn new(path: &str) -> &SystemVirtualPath { // SAFETY: SystemVirtualPath is marked as #[repr(transparent)] so the conversion from a // *const str to a *const SystemVirtualPath is valid. unsafe { &*(path as *const str as *const SystemVirtualPath) } @@ -767,8 +768,8 @@ pub struct SystemVirtualPathBuf(String); impl SystemVirtualPathBuf { #[inline] - pub fn as_path(&self) -> &SystemVirtualPath { - SystemVirtualPath::new(&self.0) + pub const fn as_path(&self) -> &SystemVirtualPath { + SystemVirtualPath::new(self.0.as_str()) } } @@ -852,6 +853,12 @@ impl ruff_cache::CacheKey for SystemVirtualPathBuf { } } +impl Borrow for SystemVirtualPathBuf { + fn borrow(&self) -> &SystemVirtualPath { + self.as_path() + } +} + /// Deduplicates identical paths and removes nested paths. /// /// # Examples diff --git a/crates/ty_server/src/document.rs b/crates/ty_server/src/document.rs index fff51d2f49..e2c582475b 100644 --- a/crates/ty_server/src/document.rs +++ b/crates/ty_server/src/document.rs @@ -11,6 +11,7 @@ use lsp_types::{PositionEncodingKind, Url}; use crate::system::AnySystemPath; pub use notebook::NotebookDocument; pub(crate) use range::{FileRangeExt, PositionExt, RangeExt, TextSizeExt, ToRangeExt}; +use ruff_db::system::{SystemPathBuf, SystemVirtualPath}; pub(crate) use text_document::DocumentVersion; pub use text_document::TextDocument; @@ -41,39 +42,75 @@ impl From for ruff_source_file::PositionEncoding { /// A unique document ID, derived from a URL passed as part of an LSP request. /// This document ID can point to either be a standalone Python file, a full notebook, or a cell within a notebook. -#[derive(Clone, Debug)] -pub(crate) enum DocumentKey { - Notebook(AnySystemPath), - NotebookCell { - cell_url: Url, - notebook_path: AnySystemPath, - }, - Text(AnySystemPath), +/// +/// The `DocumentKey` is very similar to `AnySystemPath`. The important distinction is that +/// ty doesn't know about individual notebook cells, instead, ty operates on full notebook documents. +/// ty also doesn't support resolving settings per cell, instead, settings are resolved per file or notebook. +/// +/// Thus, the motivation of `DocumentKey` is to prevent accidental use of Cell keys for operations +/// that expect to work on a file path level. That's what [`DocumentHandle::to_file_path`] +/// is for, it returns a file path for any document, taking into account that these methods should +/// return the notebook for cell documents and notebooks. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub(super) enum DocumentKey { + /// A URI using the `file` schema and maps to a valid path. + File(SystemPathBuf), + + /// Any other URI. + /// + /// Used for Notebook-cells, URI's with non-`file` schemes, or invalid `file` URI's. + Opaque(String), } impl DocumentKey { - /// Returns the file path associated with the key. - pub(crate) fn path(&self) -> &AnySystemPath { - match self { - DocumentKey::Notebook(path) | DocumentKey::Text(path) => path, - DocumentKey::NotebookCell { notebook_path, .. } => notebook_path, + /// Converts the given [`Url`] to an [`DocumentKey`]. + /// + /// If the URL scheme is `file`, then the path is converted to a [`SystemPathBuf`] unless + /// the url isn't a valid file path. + /// + /// In all other cases, the URL is kept as an opaque identifier ([`Self::Opaque`]). + pub(crate) fn from_url(url: &Url) -> Self { + if url.scheme() == "file" { + if let Ok(path) = url.to_file_path() { + Self::File(SystemPathBuf::from_path_buf(path).expect("URL to be valid UTF-8")) + } else { + tracing::warn!( + "Treating `file:` url `{url}` as opaque URL as it isn't a valid file path" + ); + Self::Opaque(url.to_string()) + } + } else { + Self::Opaque(url.to_string()) } } - pub(crate) fn from_path(path: AnySystemPath) -> Self { - // For text documents, we assume it's a text document unless it's a notebook file. - match path.extension() { - Some("ipynb") => Self::Notebook(path), - _ => Self::Text(path), + pub(crate) fn as_opaque(&self) -> Option<&str> { + match self { + Self::Opaque(uri) => Some(uri), + Self::File(_) => None, } } - /// Returns the URL for this document key. For notebook cells, returns the cell URL. - /// For other document types, converts the path to a URL. - pub(crate) fn to_url(&self) -> Option { + /// Returns the corresponding [`AnySystemPath`] for this document key. + /// + /// Note, calling this method on a `DocumentKey::Opaque` representing a cell document + /// will return a `SystemVirtualPath` corresponding to the cell URI but not the notebook file path. + /// That's most likely not what you want. + pub(super) fn to_file_path(&self) -> AnySystemPath { match self { - DocumentKey::NotebookCell { cell_url, .. } => Some(cell_url.clone()), - DocumentKey::Notebook(path) | DocumentKey::Text(path) => path.to_url(), + Self::File(path) => AnySystemPath::System(path.clone()), + Self::Opaque(uri) => { + AnySystemPath::SystemVirtual(SystemVirtualPath::new(uri).to_path_buf()) + } + } + } +} + +impl From for DocumentKey { + fn from(value: AnySystemPath) -> Self { + match value { + AnySystemPath::System(system_path) => Self::File(system_path), + AnySystemPath::SystemVirtual(virtual_path) => Self::Opaque(virtual_path.to_string()), } } } @@ -81,11 +118,8 @@ impl DocumentKey { impl std::fmt::Display for DocumentKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::NotebookCell { cell_url, .. } => cell_url.fmt(f), - Self::Notebook(path) | Self::Text(path) => match path { - AnySystemPath::System(system_path) => system_path.fmt(f), - AnySystemPath::SystemVirtual(virtual_path) => virtual_path.fmt(f), - }, + Self::File(path) => path.fmt(f), + Self::Opaque(uri) => uri.fmt(f), } } } diff --git a/crates/ty_server/src/document/notebook.rs b/crates/ty_server/src/document/notebook.rs index 2616cffd70..d1e07648e2 100644 --- a/crates/ty_server/src/document/notebook.rs +++ b/crates/ty_server/src/document/notebook.rs @@ -3,9 +3,8 @@ use lsp_types::NotebookCellKind; use ruff_notebook::CellMetadata; use rustc_hash::{FxBuildHasher, FxHashMap}; -use crate::{PositionEncoding, TextDocument}; - use super::DocumentVersion; +use crate::{PositionEncoding, TextDocument}; pub(super) type CellId = usize; @@ -13,16 +12,25 @@ pub(super) type CellId = usize; /// contents are internally represented by [`TextDocument`]s. #[derive(Clone, Debug)] pub struct NotebookDocument { + url: lsp_types::Url, cells: Vec, metadata: ruff_notebook::RawNotebookMetadata, version: DocumentVersion, // Used to quickly find the index of a cell for a given URL. - cell_index: FxHashMap, + cell_index: FxHashMap, } /// A single cell within a notebook, which has text contents represented as a `TextDocument`. #[derive(Clone, Debug)] struct NotebookCell { + /// The URL uniquely identifying the cell. + /// + /// > Cell text documents have a URI, but servers should not rely on any + /// > format for this URI, since it is up to the client on how it will + /// > create these URIs. The URIs must be unique across ALL notebook + /// > cells and can therefore be used to uniquely identify a notebook cell + /// > or the cell’s text document. + /// > url: lsp_types::Url, kind: NotebookCellKind, document: TextDocument, @@ -30,32 +38,45 @@ struct NotebookCell { impl NotebookDocument { pub fn new( - version: DocumentVersion, + url: lsp_types::Url, + notebook_version: DocumentVersion, cells: Vec, metadata: serde_json::Map, cell_documents: Vec, ) -> crate::Result { - let mut cell_contents: FxHashMap<_, _> = cell_documents - .into_iter() - .map(|document| (document.uri, document.text)) - .collect(); + let mut cells: Vec<_> = cells.into_iter().map(NotebookCell::empty).collect(); - let cells: Vec<_> = cells - .into_iter() - .map(|cell| { - let contents = cell_contents.remove(&cell.document).unwrap_or_default(); - NotebookCell::new(cell, contents, version) - }) - .collect(); + let cell_index = Self::make_cell_index(&cells); + + for cell_document in cell_documents { + let index = cell_index + .get(cell_document.uri.as_str()) + .copied() + .ok_or_else(|| { + anyhow::anyhow!( + "Received content for cell `{}` that isn't present in the metadata", + cell_document.uri + ) + })?; + + cells[index].document = + TextDocument::new(cell_document.uri, cell_document.text, cell_document.version) + .with_language_id(&cell_document.language_id); + } Ok(Self { - version, - cell_index: Self::make_cell_index(cells.as_slice()), - metadata: serde_json::from_value(serde_json::Value::Object(metadata))?, + url, + version: notebook_version, + cell_index, cells, + metadata: serde_json::from_value(serde_json::Value::Object(metadata))?, }) } + pub(crate) fn url(&self) -> &lsp_types::Url { + &self.url + } + /// Generates a pseudo-representation of a notebook that lacks per-cell metadata and contextual information /// but should still work with Ruff's linter. pub fn make_ruff_notebook(&self) -> ruff_notebook::Notebook { @@ -127,7 +148,7 @@ impl NotebookDocument { // First, delete the cells and remove them from the index. if delete > 0 { for cell in self.cells.drain(start..start + delete) { - self.cell_index.remove(&cell.url); + self.cell_index.remove(cell.url.as_str()); deleted_cells.insert(cell.url, cell.document); } } @@ -150,7 +171,7 @@ impl NotebookDocument { // Third, register the new cells in the index and update existing ones that came // after the insertion. for (index, cell) in self.cells.iter().enumerate().skip(start) { - self.cell_index.insert(cell.url.clone(), index); + self.cell_index.insert(cell.url.to_string(), index); } // Finally, update the text document that represents the cell with the actual @@ -158,8 +179,9 @@ impl NotebookDocument { // `cell_index` are updated before we start applying the changes to the cells. if let Some(did_open) = structure.did_open { for cell_text_document in did_open { - if let Some(cell) = self.cell_by_uri_mut(&cell_text_document.uri) { + if let Some(cell) = self.cell_by_uri_mut(cell_text_document.uri.as_str()) { cell.document = TextDocument::new( + cell_text_document.uri, cell_text_document.text, cell_text_document.version, ); @@ -170,7 +192,7 @@ impl NotebookDocument { if let Some(cell_data) = data { for cell in cell_data { - if let Some(existing_cell) = self.cell_by_uri_mut(&cell.document) { + if let Some(existing_cell) = self.cell_by_uri_mut(cell.document.as_str()) { existing_cell.kind = cell.kind; } } @@ -178,7 +200,7 @@ impl NotebookDocument { if let Some(content_changes) = text_content { for content_change in content_changes { - if let Some(cell) = self.cell_by_uri_mut(&content_change.document.uri) { + if let Some(cell) = self.cell_by_uri_mut(content_change.document.uri.as_str()) { cell.document .apply_changes(content_change.changes, version, encoding); } @@ -204,7 +226,8 @@ impl NotebookDocument { } /// Get the text document representing the contents of a cell by the cell URI. - pub(crate) fn cell_document_by_uri(&self, uri: &lsp_types::Url) -> Option<&TextDocument> { + #[expect(unused)] + pub(crate) fn cell_document_by_uri(&self, uri: &str) -> Option<&TextDocument> { self.cells .get(*self.cell_index.get(uri)?) .map(|cell| &cell.document) @@ -215,29 +238,41 @@ impl NotebookDocument { self.cells.iter().map(|cell| &cell.url) } - fn cell_by_uri_mut(&mut self, uri: &lsp_types::Url) -> Option<&mut NotebookCell> { + fn cell_by_uri_mut(&mut self, uri: &str) -> Option<&mut NotebookCell> { self.cells.get_mut(*self.cell_index.get(uri)?) } - fn make_cell_index(cells: &[NotebookCell]) -> FxHashMap { + fn make_cell_index(cells: &[NotebookCell]) -> FxHashMap { let mut index = FxHashMap::with_capacity_and_hasher(cells.len(), FxBuildHasher); for (i, cell) in cells.iter().enumerate() { - index.insert(cell.url.clone(), i); + index.insert(cell.url.to_string(), i); } index } } impl NotebookCell { + pub(crate) fn empty(cell: lsp_types::NotebookCell) -> Self { + Self { + kind: cell.kind, + document: TextDocument::new( + cell.document.clone(), + String::new(), + DocumentVersion::default(), + ), + url: cell.document, + } + } + pub(crate) fn new( cell: lsp_types::NotebookCell, contents: String, version: DocumentVersion, ) -> Self { Self { + document: TextDocument::new(cell.document.clone(), contents, version), url: cell.document, kind: cell.kind, - document: TextDocument::new(contents, version), } } } @@ -294,7 +329,14 @@ mod tests { } } - NotebookDocument::new(0, cells, serde_json::Map::default(), cell_documents).unwrap() + NotebookDocument::new( + lsp_types::Url::parse("file://test.ipynb").unwrap(), + 0, + cells, + serde_json::Map::default(), + cell_documents, + ) + .unwrap() } /// This test case checks that for a notebook with three code cells, when the client sends a diff --git a/crates/ty_server/src/document/text_document.rs b/crates/ty_server/src/document/text_document.rs index e5d00ff0cf..9898dd670b 100644 --- a/crates/ty_server/src/document/text_document.rs +++ b/crates/ty_server/src/document/text_document.rs @@ -1,4 +1,4 @@ -use lsp_types::TextDocumentContentChangeEvent; +use lsp_types::{TextDocumentContentChangeEvent, Url}; use ruff_source_file::LineIndex; use crate::PositionEncoding; @@ -11,6 +11,9 @@ pub(crate) type DocumentVersion = i32; /// with changes made by the user, including unsaved changes. #[derive(Debug, Clone)] pub struct TextDocument { + /// The URL as sent by the client + url: Url, + /// The string contents of the document. contents: String, /// A computed line index for the document. This should always reflect @@ -40,9 +43,10 @@ impl From<&str> for LanguageId { } impl TextDocument { - pub fn new(contents: String, version: DocumentVersion) -> Self { + pub fn new(url: Url, contents: String, version: DocumentVersion) -> Self { let index = LineIndex::from_source_text(&contents); Self { + url, contents, index, version, @@ -60,6 +64,10 @@ impl TextDocument { self.contents } + pub(crate) fn url(&self) -> &Url { + &self.url + } + pub fn contents(&self) -> &str { &self.contents } @@ -154,11 +162,12 @@ impl TextDocument { #[cfg(test)] mod tests { use crate::{PositionEncoding, TextDocument}; - use lsp_types::{Position, TextDocumentContentChangeEvent}; + use lsp_types::{Position, TextDocumentContentChangeEvent, Url}; #[test] fn redo_edit() { let mut document = TextDocument::new( + Url::parse("file:///test").unwrap(), r#"""" 测试comment 一些测试内容 diff --git a/crates/ty_server/src/lib.rs b/crates/ty_server/src/lib.rs index a56a95cb38..374c8421cf 100644 --- a/crates/ty_server/src/lib.rs +++ b/crates/ty_server/src/lib.rs @@ -8,7 +8,7 @@ pub use crate::logging::{LogLevel, init_logging}; pub use crate::server::{PartialWorkspaceProgress, PartialWorkspaceProgressParams, Server}; pub use crate::session::{ClientOptions, DiagnosticMode}; pub use document::{NotebookDocument, PositionEncoding, TextDocument}; -pub(crate) use session::{DocumentQuery, Session}; +pub(crate) use session::Session; mod capabilities; mod document; diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index 6fd1cde43a..a56866791b 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -1,6 +1,5 @@ use crate::server::schedule::Task; use crate::session::Session; -use crate::system::AnySystemPath; use anyhow::anyhow; use lsp_server as server; use lsp_server::RequestId; @@ -208,7 +207,7 @@ where // SAFETY: The `snapshot` is safe to move across the unwind boundary because it is not used // after unwinding. - let snapshot = AssertUnwindSafe(session.take_session_snapshot()); + let snapshot = AssertUnwindSafe(session.snapshot_session()); Box::new(move |client| { let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered(); @@ -253,10 +252,10 @@ where .cancellation_token(&id) .expect("request should have been tested for cancellation before scheduling"); - let url = R::document_url(¶ms).into_owned(); + let url = R::document_url(¶ms); - let Ok(path) = AnySystemPath::try_from_url(&url) else { - let reason = format!("URL `{url}` isn't a valid system path"); + let Ok(document) = session.snapshot_document(&url) else { + let reason = format!("Document {url} is not open in the session"); tracing::warn!( "Ignoring request id={id} method={} because {reason}", R::METHOD @@ -274,8 +273,8 @@ where }); }; + let path = document.to_file_path(); let db = session.project_db(&path).clone(); - let snapshot = session.take_document_snapshot(url); Box::new(move |client| { let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered(); @@ -294,7 +293,7 @@ where } if let Err(error) = ruff_db::panic::catch_unwind(|| { - R::handle_request(&id, &db, snapshot, client, params); + R::handle_request(&id, &db, document, client, params); }) { panic_response::(&id, client, &error, retry); } @@ -371,7 +370,15 @@ where let (id, params) = cast_notification::(req)?; Ok(Task::background(schedule, move |session: &Session| { let url = N::document_url(¶ms); - let snapshot = session.take_document_snapshot((*url).clone()); + let Ok(snapshot) = session.snapshot_document(&url) else { + let reason = format!("Document {url} is not open in the session"); + tracing::warn!( + "Ignoring notification id={id} method={} because {reason}", + N::METHOD + ); + return Box::new(|_| {}); + }; + Box::new(move |client| { let _span = tracing::debug_span!("notification", method = N::METHOD).entered(); diff --git a/crates/ty_server/src/server/api/diagnostics.rs b/crates/ty_server/src/server/api/diagnostics.rs index d43b176a9b..7680dc1bad 100644 --- a/crates/ty_server/src/server/api/diagnostics.rs +++ b/crates/ty_server/src/server/api/diagnostics.rs @@ -13,16 +13,16 @@ use ruff_db::source::{line_index, source_text}; use ruff_db::system::SystemPathBuf; use ty_project::{Db, ProjectDatabase}; -use crate::document::{DocumentKey, FileRangeExt, ToRangeExt}; +use crate::document::{FileRangeExt, ToRangeExt}; use crate::session::DocumentSnapshot; use crate::session::client::Client; use crate::system::{AnySystemPath, file_to_url}; -use crate::{DocumentQuery, PositionEncoding, Session}; +use crate::{NotebookDocument, PositionEncoding, Session}; pub(super) struct Diagnostics<'a> { items: Vec, encoding: PositionEncoding, - document: &'a DocumentQuery, + notebook: Option<&'a NotebookDocument>, } impl Diagnostics<'_> { @@ -53,7 +53,7 @@ impl Diagnostics<'_> { } pub(super) fn to_lsp_diagnostics(&self, db: &ProjectDatabase) -> LspDiagnostics { - if let Some(notebook) = self.document.as_notebook() { + if let Some(notebook) = self.notebook { let mut cell_diagnostics: FxHashMap> = FxHashMap::default(); // Populates all relevant URLs with an empty diagnostic list. This ensures that documents @@ -115,23 +115,18 @@ impl LspDiagnostics { } } -/// Clears the diagnostics for the document identified by `key`. +/// Clears the diagnostics for the document identified by `uri`. /// /// This is done by notifying the client with an empty list of diagnostics for the document. /// For notebook cells, this clears diagnostics for the specific cell. /// For other document types, this clears diagnostics for the main document. -pub(super) fn clear_diagnostics(session: &Session, key: &DocumentKey, client: &Client) { +pub(super) fn clear_diagnostics(session: &Session, uri: &lsp_types::Url, client: &Client) { if session.client_capabilities().supports_pull_diagnostics() { return; } - let Some(uri) = key.to_url() else { - // If we can't convert to URL, we can't clear diagnostics - return; - }; - client.send_notification::(PublishDiagnosticsParams { - uri, + uri: uri.clone(), diagnostics: vec![], version: None, }); @@ -143,18 +138,12 @@ pub(super) fn clear_diagnostics(session: &Session, key: &DocumentKey, client: &C /// This function is a no-op if the client supports pull diagnostics. /// /// [publish diagnostics notification]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics -pub(super) fn publish_diagnostics(session: &Session, key: &DocumentKey, client: &Client) { +pub(super) fn publish_diagnostics(session: &Session, url: &lsp_types::Url, client: &Client) { if session.client_capabilities().supports_pull_diagnostics() { return; } - let Some(url) = key.to_url() else { - return; - }; - - let snapshot = session.take_document_snapshot(url.clone()); - - let document = match snapshot.document() { + let snapshot = match session.snapshot_document(url) { Ok(document) => document, Err(err) => { tracing::debug!("Failed to resolve document for URL `{}`: {}", url, err); @@ -162,7 +151,7 @@ pub(super) fn publish_diagnostics(session: &Session, key: &DocumentKey, client: } }; - let db = session.project_db(key.path()); + let db = session.project_db(&snapshot.to_file_path()); let Some(diagnostics) = compute_diagnostics(db, &snapshot) else { return; @@ -173,13 +162,13 @@ pub(super) fn publish_diagnostics(session: &Session, key: &DocumentKey, client: client.send_notification::(PublishDiagnosticsParams { uri, diagnostics, - version: Some(document.version()), + version: Some(snapshot.document().version()), }); }; match diagnostics.to_lsp_diagnostics(db) { LspDiagnostics::TextDocument(diagnostics) => { - publish_diagnostics_notification(url, diagnostics); + publish_diagnostics_notification(url.clone(), diagnostics); } LspDiagnostics::NotebookDocument(cell_diagnostics) => { for (cell_url, diagnostics) in cell_diagnostics { @@ -264,16 +253,11 @@ pub(super) fn compute_diagnostics<'a>( db: &ProjectDatabase, snapshot: &'a DocumentSnapshot, ) -> Option> { - let document = match snapshot.document() { - Ok(document) => document, - Err(err) => { - tracing::info!("Failed to resolve document for snapshot: {}", err); - return None; - } - }; - - let Some(file) = document.file(db) else { - tracing::info!("No file found for snapshot for `{}`", document.file_path()); + let Some(file) = snapshot.to_file(db) else { + tracing::info!( + "No file found for snapshot for `{}`", + snapshot.to_file_path() + ); return None; }; @@ -282,7 +266,7 @@ pub(super) fn compute_diagnostics<'a>( Some(Diagnostics { items: diagnostics, encoding: snapshot.encoding(), - document, + notebook: snapshot.notebook(), }) } diff --git a/crates/ty_server/src/server/api/notifications/did_change.rs b/crates/ty_server/src/server/api/notifications/did_change.rs index 68f6f883e0..3cb52c3daa 100644 --- a/crates/ty_server/src/server/api/notifications/did_change.rs +++ b/crates/ty_server/src/server/api/notifications/did_change.rs @@ -28,19 +28,16 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler { content_changes, } = params; - let key = match session.key_from_url(uri) { - Ok(key) => key, - Err(uri) => { - tracing::debug!("Failed to create document key from URI: {}", uri); - return Ok(()); - } - }; - - session - .update_text_document(&key, content_changes, version) + let document = session + .document_handle(&uri) .with_failure_code(ErrorCode::InternalError)?; - let changes = match key.path() { + document + .update_text_document(session, content_changes, version) + .with_failure_code(ErrorCode::InternalError)?; + + let path = document.to_file_path(); + let changes = match &*path { AnySystemPath::System(system_path) => { vec![ChangeEvent::file_content_changed(system_path.clone())] } @@ -49,9 +46,9 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler { } }; - session.apply_changes(key.path(), changes); + session.apply_changes(&path, changes); - publish_diagnostics(session, &key, client); + publish_diagnostics(session, document.url(), client); Ok(()) } diff --git a/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs b/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs index 21285f461f..ce55100dee 100644 --- a/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs +++ b/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs @@ -1,3 +1,4 @@ +use crate::document::DocumentKey; use crate::server::Result; use crate::server::api::diagnostics::{publish_diagnostics, publish_settings_diagnostics}; use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; @@ -25,16 +26,8 @@ impl SyncNotificationHandler for DidChangeWatchedFiles { let mut events_by_db: FxHashMap<_, Vec> = FxHashMap::default(); for change in params.changes { - let path = match AnySystemPath::try_from_url(&change.uri) { - Ok(path) => path, - Err(err) => { - tracing::warn!( - "Failed to convert URI '{}` to system path: {err:?}", - change.uri - ); - continue; - } - }; + let key = DocumentKey::from_url(&change.uri); + let path = key.to_file_path(); let system_path = match path { AnySystemPath::System(system) => system, @@ -99,8 +92,8 @@ impl SyncNotificationHandler for DidChangeWatchedFiles { |_, ()| {}, ); } else { - for key in session.text_document_keys() { - publish_diagnostics(session, &key, client); + for key in session.text_document_handles() { + publish_diagnostics(session, key.url(), client); } } // TODO: always publish diagnostics for notebook files (since they don't use pull diagnostics) diff --git a/crates/ty_server/src/server/api/notifications/did_close.rs b/crates/ty_server/src/server/api/notifications/did_close.rs index 60097df67b..5c5747ee05 100644 --- a/crates/ty_server/src/server/api/notifications/did_close.rs +++ b/crates/ty_server/src/server/api/notifications/did_close.rs @@ -27,22 +27,20 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler { text_document: TextDocumentIdentifier { uri }, } = params; - let key = match session.key_from_url(uri) { - Ok(key) => key, - Err(uri) => { - tracing::debug!("Failed to create document key from URI: {}", uri); - return Ok(()); - } - }; - - session - .close_document(&key) + let document = session + .document_handle(&uri) .with_failure_code(ErrorCode::InternalError)?; - let path = key.path(); - let db = session.project_db_mut(path); + let path = document.to_file_path().into_owned(); + let url = document.url().clone(); - match path { + document + .close(session) + .with_failure_code(ErrorCode::InternalError)?; + + let db = session.project_db_mut(&path); + + match &path { AnySystemPath::System(system_path) => { if let Some(file) = db.files().try_system(db, system_path) { db.project().close_file(db, file); @@ -65,7 +63,7 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler { .diagnostic_mode() .is_open_files_only() { - clear_diagnostics(session, &key, client); + clear_diagnostics(session, &url, client); } } AnySystemPath::SystemVirtual(virtual_path) => { @@ -78,7 +76,7 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler { // Always clear diagnostics for virtual files, as they don't really exist on disk // which means closing them is like deleting the file. - clear_diagnostics(session, &key, client); + clear_diagnostics(session, &url, client); } } diff --git a/crates/ty_server/src/server/api/notifications/did_close_notebook.rs b/crates/ty_server/src/server/api/notifications/did_close_notebook.rs index f934f6832e..9b03651496 100644 --- a/crates/ty_server/src/server/api/notifications/did_close_notebook.rs +++ b/crates/ty_server/src/server/api/notifications/did_close_notebook.rs @@ -26,21 +26,19 @@ impl SyncNotificationHandler for DidCloseNotebookHandler { .. } = params; - let key = match session.key_from_url(uri) { - Ok(key) => key, - Err(uri) => { - tracing::debug!("Failed to create document key from URI: {}", uri); - return Ok(()); - } - }; - - session - .close_document(&key) + let document = session + .document_handle(&uri) .with_failure_code(lsp_server::ErrorCode::InternalError)?; - if let AnySystemPath::SystemVirtual(virtual_path) = key.path() { + let path = document.to_file_path().into_owned(); + + document + .close(session) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + + if let AnySystemPath::SystemVirtual(virtual_path) = &path { session.apply_changes( - key.path(), + &path, vec![ChangeEvent::DeletedVirtual(virtual_path.clone())], ); } 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 5647bb2781..b2561e9c6c 100644 --- a/crates/ty_server/src/server/api/notifications/did_open.rs +++ b/crates/ty_server/src/server/api/notifications/did_open.rs @@ -35,30 +35,23 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler { }, } = params; - let key = match session.key_from_url(uri) { - Ok(key) => key, - Err(uri) => { - tracing::debug!("Failed to create document key from URI: {}", uri); - return Ok(()); - } - }; + let document = session.open_text_document( + TextDocument::new(uri, text, version).with_language_id(&language_id), + ); - let document = TextDocument::new(text, version).with_language_id(&language_id); - session.open_text_document(key.path(), document); - - let path = key.path(); + let path = document.to_file_path(); // This is a "maybe" because the `File` might've not been interned yet i.e., the // `try_system` call will return `None` which doesn't mean that the file is new, it's just // that the server didn't need the file yet. let is_maybe_new_system_file = path.as_system().is_some_and(|system_path| { - let db = session.project_db(path); + let db = session.project_db(&path); db.files() .try_system(db, system_path) .is_none_or(|file| !file.exists(db)) }); - match path { + match &*path { AnySystemPath::System(system_path) => { let event = if is_maybe_new_system_file { ChangeEvent::Created { @@ -68,22 +61,22 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler { } else { ChangeEvent::Opened(system_path.clone()) }; - session.apply_changes(path, vec![event]); + session.apply_changes(&path, vec![event]); - let db = session.project_db_mut(path); + let db = session.project_db_mut(&path); match system_path_to_file(db, system_path) { Ok(file) => db.project().open_file(db, file), Err(err) => tracing::warn!("Failed to open file {system_path}: {err}"), } } AnySystemPath::SystemVirtual(virtual_path) => { - let db = session.project_db_mut(path); + let db = session.project_db_mut(&path); let virtual_file = db.files().virtual_file(db, virtual_path); db.project().open_file(db, virtual_file.file()); } } - publish_diagnostics(session, &key, client); + publish_diagnostics(session, document.url(), client); Ok(()) } 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 201add9587..b61f2aeef6 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 @@ -25,20 +25,27 @@ impl SyncNotificationHandler for DidOpenNotebookHandler { _client: &Client, params: DidOpenNotebookDocumentParams, ) -> Result<()> { - let Ok(path) = AnySystemPath::try_from_url(¶ms.notebook_document.uri) else { - return Ok(()); - }; + let lsp_types::NotebookDocument { + version, + cells, + metadata, + uri: notebook_uri, + .. + } = params.notebook_document; let notebook = NotebookDocument::new( - params.notebook_document.version, - params.notebook_document.cells, - params.notebook_document.metadata.unwrap_or_default(), + notebook_uri, + version, + cells, + metadata.unwrap_or_default(), params.cell_text_documents, ) .with_failure_code(ErrorCode::InternalError)?; - session.open_notebook_document(&path, notebook); - match &path { + let document = session.open_notebook_document(notebook); + let path = document.to_file_path(); + + match &*path { AnySystemPath::System(system_path) => { session.apply_changes(&path, vec![ChangeEvent::Opened(system_path.clone())]); } diff --git a/crates/ty_server/src/server/api/requests/completion.rs b/crates/ty_server/src/server/api/requests/completion.rs index a3e7d91f94..bf712c5efb 100644 --- a/crates/ty_server/src/server/api/requests/completion.rs +++ b/crates/ty_server/src/server/api/requests/completion.rs @@ -45,7 +45,7 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/doc_highlights.rs b/crates/ty_server/src/server/api/requests/doc_highlights.rs index 9750bdc190..b5b6d0d9ab 100644 --- a/crates/ty_server/src/server/api/requests/doc_highlights.rs +++ b/crates/ty_server/src/server/api/requests/doc_highlights.rs @@ -37,7 +37,7 @@ impl BackgroundDocumentRequestHandler for DocumentHighlightRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/document_symbols.rs b/crates/ty_server/src/server/api/requests/document_symbols.rs index 46c4c3eb2e..ea5ee312c6 100644 --- a/crates/ty_server/src/server/api/requests/document_symbols.rs +++ b/crates/ty_server/src/server/api/requests/document_symbols.rs @@ -39,7 +39,7 @@ impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/execute_command.rs b/crates/ty_server/src/server/api/requests/execute_command.rs index 8a2fc52fd1..a51ece8598 100644 --- a/crates/ty_server/src/server/api/requests/execute_command.rs +++ b/crates/ty_server/src/server/api/requests/execute_command.rs @@ -52,7 +52,7 @@ fn debug_information(session: &Session) -> crate::Result { writeln!( buffer, "Open text documents: {}", - session.text_document_keys().count() + session.text_document_handles().count() )?; writeln!(buffer)?; diff --git a/crates/ty_server/src/server/api/requests/goto_declaration.rs b/crates/ty_server/src/server/api/requests/goto_declaration.rs index 07444746f7..1c16a74bc5 100644 --- a/crates/ty_server/src/server/api/requests/goto_declaration.rs +++ b/crates/ty_server/src/server/api/requests/goto_declaration.rs @@ -37,7 +37,7 @@ impl BackgroundDocumentRequestHandler for GotoDeclarationRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/goto_definition.rs b/crates/ty_server/src/server/api/requests/goto_definition.rs index 793ae54bf1..bc33411778 100644 --- a/crates/ty_server/src/server/api/requests/goto_definition.rs +++ b/crates/ty_server/src/server/api/requests/goto_definition.rs @@ -37,7 +37,7 @@ impl BackgroundDocumentRequestHandler for GotoDefinitionRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/goto_references.rs b/crates/ty_server/src/server/api/requests/goto_references.rs index 129afcecdc..3afaf28b14 100644 --- a/crates/ty_server/src/server/api/requests/goto_references.rs +++ b/crates/ty_server/src/server/api/requests/goto_references.rs @@ -37,7 +37,7 @@ impl BackgroundDocumentRequestHandler for ReferencesRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/goto_type_definition.rs b/crates/ty_server/src/server/api/requests/goto_type_definition.rs index 5695c5a6ab..379defa344 100644 --- a/crates/ty_server/src/server/api/requests/goto_type_definition.rs +++ b/crates/ty_server/src/server/api/requests/goto_type_definition.rs @@ -37,7 +37,7 @@ impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/hover.rs b/crates/ty_server/src/server/api/requests/hover.rs index be81eca472..cc8f8e0dab 100644 --- a/crates/ty_server/src/server/api/requests/hover.rs +++ b/crates/ty_server/src/server/api/requests/hover.rs @@ -37,7 +37,7 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/inlay_hints.rs b/crates/ty_server/src/server/api/requests/inlay_hints.rs index ec8464fc6b..21eb1d09b6 100644 --- a/crates/ty_server/src/server/api/requests/inlay_hints.rs +++ b/crates/ty_server/src/server/api/requests/inlay_hints.rs @@ -36,7 +36,7 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/prepare_rename.rs b/crates/ty_server/src/server/api/requests/prepare_rename.rs index 7f11961bee..a12541729d 100644 --- a/crates/ty_server/src/server/api/requests/prepare_rename.rs +++ b/crates/ty_server/src/server/api/requests/prepare_rename.rs @@ -37,7 +37,7 @@ impl BackgroundDocumentRequestHandler for PrepareRenameRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/rename.rs b/crates/ty_server/src/server/api/requests/rename.rs index 117891ebba..d434cb733e 100644 --- a/crates/ty_server/src/server/api/requests/rename.rs +++ b/crates/ty_server/src/server/api/requests/rename.rs @@ -38,7 +38,7 @@ impl BackgroundDocumentRequestHandler for RenameRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/selection_range.rs b/crates/ty_server/src/server/api/requests/selection_range.rs index 684b230cd3..516ea6aeda 100644 --- a/crates/ty_server/src/server/api/requests/selection_range.rs +++ b/crates/ty_server/src/server/api/requests/selection_range.rs @@ -37,7 +37,7 @@ impl BackgroundDocumentRequestHandler for SelectionRangeRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/semantic_tokens.rs b/crates/ty_server/src/server/api/requests/semantic_tokens.rs index 58f245d4ae..adc6142189 100644 --- a/crates/ty_server/src/server/api/requests/semantic_tokens.rs +++ b/crates/ty_server/src/server/api/requests/semantic_tokens.rs @@ -33,7 +33,7 @@ impl BackgroundDocumentRequestHandler for SemanticTokensRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/semantic_tokens_range.rs b/crates/ty_server/src/server/api/requests/semantic_tokens_range.rs index 6112405249..03193b32a6 100644 --- a/crates/ty_server/src/server/api/requests/semantic_tokens_range.rs +++ b/crates/ty_server/src/server/api/requests/semantic_tokens_range.rs @@ -35,7 +35,7 @@ impl BackgroundDocumentRequestHandler for SemanticTokensRangeRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/signature_help.rs b/crates/ty_server/src/server/api/requests/signature_help.rs index e9b9f160b6..f9b20cccd9 100644 --- a/crates/ty_server/src/server/api/requests/signature_help.rs +++ b/crates/ty_server/src/server/api/requests/signature_help.rs @@ -39,7 +39,7 @@ impl BackgroundDocumentRequestHandler for SignatureHelpRequestHandler { return Ok(None); } - let Some(file) = snapshot.file(db) else { + let Some(file) = snapshot.to_file(db) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs index c990d4f4af..2d37436116 100644 --- a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs +++ b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs @@ -1,4 +1,5 @@ use crate::PositionEncoding; +use crate::document::DocumentKey; use crate::server::api::diagnostics::{Diagnostics, to_lsp_diagnostic}; use crate::server::api::traits::{ BackgroundRequestHandler, RequestHandler, RetriableRequestHandler, @@ -8,7 +9,7 @@ use crate::server::{Action, Result}; use crate::session::client::Client; use crate::session::index::Index; use crate::session::{SessionSnapshot, SuspendedWorkspaceDiagnosticRequest}; -use crate::system::{AnySystemPath, file_to_url}; +use crate::system::file_to_url; use lsp_server::RequestId; use lsp_types::request::WorkspaceDiagnosticRequest; use lsp_types::{ @@ -317,7 +318,7 @@ struct ResponseWriter<'a> { // It's important that we use `AnySystemPath` over `Url` here because // `file_to_url` isn't guaranteed to return the exact same URL as the one provided // by the client. - previous_result_ids: FxHashMap, + previous_result_ids: FxHashMap, } impl<'a> ResponseWriter<'a> { @@ -346,12 +347,7 @@ impl<'a> ResponseWriter<'a> { let previous_result_ids = previous_result_ids .into_iter() - .filter_map(|prev| { - Some(( - AnySystemPath::try_from_url(&prev.uri).ok()?, - (prev.uri, prev.value), - )) - }) + .map(|prev| (DocumentKey::from_url(&prev.uri), (prev.uri, prev.value))) .collect(); Self { @@ -367,20 +363,16 @@ impl<'a> ResponseWriter<'a> { tracing::debug!("Failed to convert file path to URL at {}", file.path(db)); return; }; - + let key = DocumentKey::from_url(&url); let version = self .index - .key_from_url(url.clone()) - .ok() - .and_then(|key| self.index.make_document_ref(key).ok()) - .map(|doc| i64::from(doc.version())); + .document_handle(&url) + .map(|doc| i64::from(doc.version())) + .ok(); let result_id = Diagnostics::result_id_from_hash(diagnostics); - let previous_result_id = AnySystemPath::try_from_url(&url) - .ok() - .and_then(|path| self.previous_result_ids.remove(&path)) - .map(|(_url, id)| id); + let previous_result_id = self.previous_result_ids.remove(&key).map(|(_url, id)| id); let report = match result_id { Some(new_id) if Some(&new_id) == previous_result_id.as_ref() => { @@ -444,13 +436,12 @@ impl<'a> ResponseWriter<'a> { // Handle files that had diagnostics in previous request but no longer have any // Any remaining entries in previous_results are files that were fixed - for (previous_url, previous_result_id) in self.previous_result_ids.into_values() { + for (key, (previous_url, previous_result_id)) in self.previous_result_ids { // This file had diagnostics before but doesn't now, so we need to report it as having no diagnostics let version = self .index - .key_from_url(previous_url.clone()) + .document(&key) .ok() - .and_then(|key| self.index.make_document_ref(key).ok()) .map(|doc| i64::from(doc.version())); let new_result_id = Diagnostics::result_id_from_hash(&[]); diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index 24ad0ef55e..c5daec77e3 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -1,7 +1,7 @@ //! Data model, state management, and configuration resolution. use anyhow::{Context, anyhow}; -use index::DocumentQueryError; +use index::DocumentError; use lsp_server::{Message, RequestId}; use lsp_types::notification::{DidChangeWatchedFiles, Exit, Notification}; use lsp_types::request::{ @@ -15,8 +15,9 @@ use lsp_types::{ }; use options::GlobalOptions; use ruff_db::Db; -use ruff_db::files::File; +use ruff_db::files::{File, system_path_to_file}; use ruff_db::system::{System, SystemPath, SystemPathBuf}; +use std::borrow::Cow; use std::collections::{BTreeMap, HashSet, VecDeque}; use std::ops::{Deref, DerefMut}; use std::panic::RefUnwindSafe; @@ -26,7 +27,6 @@ use ty_project::metadata::Options; use ty_project::watch::ChangeEvent; use ty_project::{ChangeResult, CheckMode, Db as _, ProjectDatabase, ProjectMetadata}; -pub(crate) use self::index::DocumentQuery; pub(crate) use self::options::InitializationOptions; pub use self::options::{ClientOptions, DiagnosticMode}; pub(crate) use self::settings::{GlobalSettings, WorkspaceSettings}; @@ -439,13 +439,6 @@ impl Session { self.projects.values_mut().chain(default_project) } - /// Returns the [`DocumentKey`] for the given URL. - /// - /// Refer to [`Index::key_from_url`] for more details. - pub(crate) fn key_from_url(&self, url: Url) -> Result { - self.index().key_from_url(url) - } - pub(crate) fn initialize_workspaces( &mut self, workspace_settings: Vec<(Url, ClientOptions)>, @@ -819,25 +812,34 @@ impl Session { } /// Creates a document snapshot with the URL referencing the document to snapshot. - pub(crate) fn take_document_snapshot(&self, url: Url) -> DocumentSnapshot { - let key = self - .key_from_url(url) - .map_err(DocumentQueryError::InvalidUrl); - DocumentSnapshot { + pub(crate) fn snapshot_document(&self, url: &Url) -> Result { + let index = self.index(); + let document_handle = index.document_handle(url)?; + + let notebook = if let Some(notebook_path) = &document_handle.notebook_path { + index + .notebook_arc(&DocumentKey::from(notebook_path.clone())) + .ok() + } else { + None + }; + + Ok(DocumentSnapshot { resolved_client_capabilities: self.resolved_client_capabilities, global_settings: self.global_settings.clone(), - workspace_settings: key - .as_ref() - .ok() - .and_then(|key| self.workspaces.settings_for_path(key.path().as_system()?)) + workspace_settings: document_handle + .to_file_path() + .as_system() + .and_then(|path| self.workspaces.settings_for_path(path)) .unwrap_or_else(|| Arc::new(WorkspaceSettings::default())), position_encoding: self.position_encoding, - document_query_result: key.and_then(|key| self.index().make_document_ref(key)), - } + document: document_handle, + notebook, + }) } /// Creates a snapshot of the current state of the [`Session`]. - pub(crate) fn take_session_snapshot(&self) -> SessionSnapshot { + pub(crate) fn snapshot_session(&self) -> SessionSnapshot { SessionSnapshot { projects: self .projects @@ -855,56 +857,49 @@ impl Session { } /// Iterates over the document keys for all open text documents. - pub(super) fn text_document_keys(&self) -> impl Iterator + '_ { + pub(super) fn text_document_handles(&self) -> impl Iterator + '_ { self.index() - .text_document_paths() - .map(|path| DocumentKey::Text(path.clone())) + .text_documents() + .map(|(key, document)| DocumentHandle { + key: key.clone(), + url: document.url().clone(), + version: document.version(), + // TODO: Set notebook path if text document is part of a notebook + notebook_path: None, + }) + } + + /// Returns a handle to the document specified by its URL. + /// + /// # Errors + /// + /// If the document is not found. + pub(crate) fn document_handle( + &self, + url: &lsp_types::Url, + ) -> Result { + self.index().document_handle(url) } /// Registers a notebook document at the provided `path`. /// If a document is already open here, it will be overwritten. - pub(crate) fn open_notebook_document( - &mut self, - path: &AnySystemPath, - document: NotebookDocument, - ) { - self.index_mut().open_notebook_document(path, document); + /// + /// 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.bump_revision(); + handle } /// Registers a text document at the provided `path`. /// If a document is already open here, it will be overwritten. - pub(crate) fn open_text_document(&mut self, path: &AnySystemPath, document: TextDocument) { - self.index_mut().open_text_document(path, document); - self.bump_revision(); - } - - /// Updates a text document at the associated `key`. /// - /// The document key must point to a text document, or this will throw an error. - pub(crate) fn update_text_document( - &mut self, - key: &DocumentKey, - content_changes: Vec, - new_version: DocumentVersion, - ) -> crate::Result<()> { - let position_encoding = self.position_encoding; - self.index_mut().update_text_document( - key, - content_changes, - new_version, - position_encoding, - )?; - self.bump_revision(); - Ok(()) - } + /// Returns a handle to the opened document. + pub(crate) fn open_text_document(&mut self, document: TextDocument) -> DocumentHandle { + let handle = self.index_mut().open_text_document(document); - /// De-registers a document, specified by its key. - /// Calling this multiple times for the same document is a logic error. - pub(crate) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> { - self.index_mut().close_document(key)?; self.bump_revision(); - Ok(()) + handle } /// Returns a reference to the index. @@ -1003,7 +998,8 @@ pub(crate) struct DocumentSnapshot { global_settings: Arc, workspace_settings: Arc, position_encoding: PositionEncoding, - document_query_result: Result, + document: DocumentHandle, + notebook: Option>, } impl DocumentSnapshot { @@ -1028,27 +1024,28 @@ impl DocumentSnapshot { } /// Returns the result of the document query for this snapshot. - pub(crate) fn document(&self) -> Result<&DocumentQuery, &DocumentQueryError> { - self.document_query_result.as_ref() + pub(crate) fn document(&self) -> &DocumentHandle { + &self.document } - pub(crate) fn file(&self, db: &dyn Db) -> Option { - let document = match self.document() { - Ok(document) => document, - Err(err) => { - tracing::debug!("Failed to resolve file: {}", err); - return None; - } - }; - let file = document.file(db); + pub(crate) fn notebook(&self) -> Option<&NotebookDocument> { + self.notebook.as_deref() + } + + pub(crate) fn to_file(&self, db: &dyn Db) -> Option { + let file = self.document.to_file(db); if file.is_none() { tracing::debug!( - "Failed to resolve file: file not found for path `{}`", - document.file_path() + "Failed to resolve file: file not found for `{}`", + self.document.url() ); } file } + + pub(crate) fn to_file_path(&self) -> Cow<'_, AnySystemPath> { + self.document.to_file_path() + } } /// An immutable snapshot of the current state of [`Session`]. @@ -1320,3 +1317,90 @@ impl SuspendedWorkspaceDiagnosticRequest { None } } + +/// A handle to a document stored within [`Index`]. +/// +/// Allows identifying the document within the index but it also carries the URL used by the +/// client to reference the document as well as the version of the document. +/// +/// It also exposes methods to get the file-path of the corresponding ty-file. +#[derive(Clone, Debug)] +pub(crate) struct DocumentHandle { + /// The key that uniquely identifies this document in the index. + key: DocumentKey, + url: lsp_types::Url, + /// The path to the enclosing notebook file if this document is a notebook or a notebook cell. + notebook_path: Option, + version: DocumentVersion, +} + +impl DocumentHandle { + pub(crate) const fn version(&self) -> DocumentVersion { + self.version + } + + /// The URL as used by the client to reference this document. + pub(crate) fn url(&self) -> &lsp_types::Url { + &self.url + } + + /// The path to the enclosing file for this document. + /// + /// This is the path corresponding to the URL, except for notebook cells where the + /// path corresponds to the notebook file. + pub(crate) fn to_file_path(&self) -> Cow<'_, AnySystemPath> { + if let Some(path) = self.notebook_path.as_ref() { + Cow::Borrowed(path) + } else { + Cow::Owned(self.key.to_file_path()) + } + } + + /// Returns the salsa interned [`File`] for the document selected by this query. + /// + /// It returns [`None`] for the following cases: + /// - For virtual file, if it's not yet opened + /// - For regular file, if it does not exists or is a directory + pub(crate) fn to_file(&self, db: &dyn Db) -> Option { + match &*self.to_file_path() { + AnySystemPath::System(path) => system_path_to_file(db, path).ok(), + AnySystemPath::SystemVirtual(virtual_path) => db + .files() + .try_virtual_file(virtual_path) + .map(|virtual_file| virtual_file.file()), + } + } + + pub(crate) fn update_text_document( + &self, + session: &mut Session, + content_changes: Vec, + new_version: DocumentVersion, + ) -> crate::Result<()> { + let position_encoding = session.position_encoding(); + let mut index = session.index_mut(); + + let document_mut = index.document_mut(&self.key)?; + + let Some(document) = document_mut.as_text_mut() else { + anyhow::bail!("Text document path does not point to a text document"); + }; + + if content_changes.is_empty() { + document.update_version(new_version); + return Ok(()); + } + + document.apply_changes(content_changes, new_version, position_encoding); + + Ok(()) + } + + /// De-registers a document, specified by its key. + /// Calling this multiple times for the same document is a logic error. + pub(crate) fn close(self, session: &mut Session) -> crate::Result<()> { + session.index_mut().close_document(&self.key)?; + session.bump_revision(); + Ok(()) + } +} diff --git a/crates/ty_server/src/session/index.rs b/crates/ty_server/src/session/index.rs index 89d310f2ab..95cc515a35 100644 --- a/crates/ty_server/src/session/index.rs +++ b/crates/ty_server/src/session/index.rs @@ -1,24 +1,24 @@ use std::sync::Arc; -use lsp_types::Url; -use ruff_db::Db; -use ruff_db::files::{File, system_path_to_file}; -use rustc_hash::FxHashMap; - +use crate::document::DocumentKey; +use crate::session::DocumentHandle; use crate::{ PositionEncoding, TextDocument, - document::{DocumentKey, DocumentVersion, NotebookDocument}, + document::{DocumentVersion, NotebookDocument}, system::AnySystemPath, }; +use ruff_db::system::SystemVirtualPath; +use rustc_hash::FxHashMap; + /// Stores and tracks all open documents in a session, along with their associated settings. #[derive(Debug)] pub(crate) struct Index { /// Maps all document file paths to the associated document controller - documents: FxHashMap, + documents: FxHashMap, /// Maps opaque cell URLs to a notebook path (document) - notebook_cells: FxHashMap, + notebook_cells: FxHashMap, } impl Index { @@ -29,68 +29,55 @@ impl Index { } } - pub(super) fn text_document_paths(&self) -> impl Iterator + '_ { - self.documents - .iter() - .filter_map(|(path, doc)| doc.as_text().and(Some(path))) + pub(super) fn text_documents( + &self, + ) -> impl Iterator + '_ { + self.documents.iter().filter_map(|(key, doc)| { + let text_document = doc.as_text()?; + Some((key, text_document)) + }) + } + + pub(crate) fn document_handle( + &self, + url: &lsp_types::Url, + ) -> Result { + let key = DocumentKey::from_url(url); + let Some(document) = self.documents.get(&key) else { + return Err(DocumentError::NotFound(key)); + }; + + if let Some(path) = key.as_opaque() { + if let Some(notebook_path) = self.notebook_cells.get(path) { + return Ok(DocumentHandle { + key: key.clone(), + notebook_path: Some(notebook_path.clone()), + url: url.clone(), + version: document.version(), + }); + } + } + + Ok(DocumentHandle { + key: key.clone(), + notebook_path: None, + url: url.clone(), + version: document.version(), + }) } #[expect(dead_code)] - pub(super) fn notebook_document_paths(&self) -> impl Iterator + '_ { + pub(super) fn notebook_document_keys(&self) -> impl Iterator + '_ { self.documents .iter() .filter(|(_, doc)| doc.as_notebook().is_some()) - .map(|(path, _)| path) - } - - pub(super) fn update_text_document( - &mut self, - key: &DocumentKey, - content_changes: Vec, - new_version: DocumentVersion, - encoding: PositionEncoding, - ) -> crate::Result<()> { - let controller = self.document_controller_for_key(key)?; - let Some(document) = controller.as_text_mut() else { - anyhow::bail!("Text document path does not point to a text document"); - }; - - if content_changes.is_empty() { - document.update_version(new_version); - return Ok(()); - } - - document.apply_changes(content_changes, new_version, encoding); - - Ok(()) - } - - /// Returns the [`DocumentKey`] corresponding to the given URL. - /// - /// It returns [`Err`] with the original URL if it cannot be converted to a [`AnySystemPath`]. - pub(crate) fn key_from_url(&self, url: Url) -> Result { - if let Some(notebook_path) = self.notebook_cells.get(&url) { - Ok(DocumentKey::NotebookCell { - cell_url: url, - notebook_path: notebook_path.clone(), - }) - } else { - let path = AnySystemPath::try_from_url(&url).map_err(|()| url)?; - if path - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("ipynb")) - { - Ok(DocumentKey::Notebook(path)) - } else { - Ok(DocumentKey::Text(path)) - } - } + .map(|(key, _)| key) } #[expect(dead_code)] pub(super) fn update_notebook_document( &mut self, - key: &DocumentKey, + notebook_key: &DocumentKey, cells: Option, metadata: Option>, new_version: DocumentVersion, @@ -102,17 +89,16 @@ impl Index { .. }) = cells.as_ref().and_then(|cells| cells.structure.as_ref()) { - let notebook_path = key.path().clone(); - for opened_cell in did_open { + let cell_path = SystemVirtualPath::new(opened_cell.uri.as_str()); self.notebook_cells - .insert(opened_cell.uri.clone(), notebook_path.clone()); + .insert(cell_path.to_string(), notebook_key.to_file_path()); } // deleted notebook cells are closed via textDocument/didClose - we don't close them here. } - let controller = self.document_controller_for_key(key)?; - let Some(notebook) = controller.as_notebook_mut() else { + let document = self.document_mut(notebook_key)?; + let Some(notebook) = document.as_notebook_mut() else { anyhow::bail!("Notebook document path does not point to a notebook document"); }; @@ -123,44 +109,64 @@ impl Index { /// Create a document reference corresponding to the given document key. /// /// Returns an error if the document is not found or if the path cannot be converted to a URL. - pub(crate) fn make_document_ref( + pub(crate) fn document(&self, key: &DocumentKey) -> Result<&Document, DocumentError> { + let Some(document) = self.documents.get(key) else { + return Err(DocumentError::NotFound(key.clone())); + }; + + Ok(document) + } + + pub(crate) fn notebook_arc( &self, - key: DocumentKey, - ) -> Result { - let path = key.path(); - let Some(controller) = self.documents.get(path) else { - return Err(DocumentQueryError::NotFound(key)); + key: &DocumentKey, + ) -> Result, DocumentError> { + let Some(document) = self.documents.get(key) else { + return Err(DocumentError::NotFound(key.clone())); }; - // TODO: The `to_url` conversion shouldn't be an error because the paths themselves are - // constructed from the URLs but the `Index` APIs don't maintain this invariant. - let (cell_url, file_path) = match key { - DocumentKey::NotebookCell { - cell_url, - notebook_path, - } => (Some(cell_url), notebook_path), - DocumentKey::Notebook(path) | DocumentKey::Text(path) => (None, path), - }; - Ok(controller.make_ref(cell_url, file_path)) + + if let Document::Notebook(notebook) = document { + Ok(notebook.clone()) + } else { + Err(DocumentError::NotFound(key.clone())) + } } - pub(super) fn open_text_document(&mut self, path: &AnySystemPath, document: TextDocument) { - self.documents - .insert(path.clone(), DocumentController::new_text(document)); + pub(super) fn open_text_document(&mut self, document: TextDocument) -> DocumentHandle { + let key = DocumentKey::from_url(document.url()); + + // TODO: Fix file path for notebook cells + let handle = DocumentHandle { + key: key.clone(), + notebook_path: None, + url: document.url().clone(), + version: document.version(), + }; + + self.documents.insert(key, Document::new_text(document)); + + handle } - pub(super) fn open_notebook_document( - &mut self, - notebook_path: &AnySystemPath, - document: NotebookDocument, - ) { + pub(super) fn open_notebook_document(&mut self, document: NotebookDocument) -> DocumentHandle { + let notebook_key = DocumentKey::from_url(document.url()); + let url = document.url().clone(); + let version = document.version(); + for cell_url in document.cell_urls() { self.notebook_cells - .insert(cell_url.clone(), notebook_path.clone()); + .insert(cell_url.to_string(), notebook_key.to_file_path()); + } + + self.documents + .insert(notebook_key.clone(), Document::new_notebook(document)); + + DocumentHandle { + notebook_path: Some(notebook_key.to_file_path()), + key: notebook_key, + url, + version, } - self.documents.insert( - notebook_path.clone(), - DocumentController::new_notebook(document), - ); } pub(super) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> { @@ -169,27 +175,23 @@ impl Index { // is requested to be `closed` by VS Code after the notebook gets updated. // This is not documented in the LSP specification explicitly, and this assumption // may need revisiting in the future as we support more editors with notebook support. - if let DocumentKey::NotebookCell { cell_url, .. } = key { - if self.notebook_cells.remove(cell_url).is_none() { - tracing::warn!("Tried to remove a notebook cell that does not exist: {cell_url}"); - } - return Ok(()); + if let DocumentKey::Opaque(uri) = key { + self.notebook_cells.remove(uri); } - let path = key.path(); - let Some(_) = self.documents.remove(path) else { + let Some(_) = self.documents.remove(key) else { anyhow::bail!("tried to close document that didn't exist at {key}") }; + Ok(()) } - fn document_controller_for_key( + pub(super) fn document_mut( &mut self, key: &DocumentKey, - ) -> crate::Result<&mut DocumentController> { - let path = key.path(); - let Some(controller) = self.documents.get_mut(path) else { - anyhow::bail!("Document controller not available at `{key}`"); + ) -> Result<&mut Document, DocumentError> { + let Some(controller) = self.documents.get_mut(key) else { + return Err(DocumentError::NotFound(key.clone())); }; Ok(controller) } @@ -197,31 +199,24 @@ impl Index { /// A mutable handler to an underlying document. #[derive(Debug)] -enum DocumentController { +pub(crate) enum Document { Text(Arc), Notebook(Arc), } -impl DocumentController { - fn new_text(document: TextDocument) -> Self { +impl Document { + pub(super) fn new_text(document: TextDocument) -> Self { Self::Text(Arc::new(document)) } - fn new_notebook(document: NotebookDocument) -> Self { + pub(super) fn new_notebook(document: NotebookDocument) -> Self { Self::Notebook(Arc::new(document)) } - fn make_ref(&self, cell_url: Option, file_path: AnySystemPath) -> DocumentQuery { - match &self { - Self::Notebook(notebook) => DocumentQuery::Notebook { - cell_url, - file_path, - notebook: notebook.clone(), - }, - Self::Text(document) => DocumentQuery::Text { - file_path, - document: document.clone(), - }, + pub(crate) fn version(&self) -> DocumentVersion { + match self { + Self::Text(document) => document.version(), + Self::Notebook(notebook) => notebook.version(), } } @@ -254,85 +249,8 @@ impl DocumentController { } } -/// A read-only query to an open document. -/// -/// This query can 'select' a text document, full notebook, or a specific notebook cell. -/// It also includes document settings. -#[derive(Debug, Clone)] -pub(crate) enum DocumentQuery { - Text { - file_path: AnySystemPath, - document: Arc, - }, - Notebook { - /// The selected notebook cell, if it exists. - cell_url: Option, - /// The path to the notebook. - file_path: AnySystemPath, - notebook: Arc, - }, -} - -impl DocumentQuery { - /// Attempts to access the underlying notebook document that this query is selecting. - pub(crate) fn as_notebook(&self) -> Option<&NotebookDocument> { - match self { - Self::Notebook { notebook, .. } => Some(notebook), - Self::Text { .. } => None, - } - } - - /// Get the version of document selected by this query. - pub(crate) fn version(&self) -> DocumentVersion { - match self { - Self::Text { document, .. } => document.version(), - Self::Notebook { notebook, .. } => notebook.version(), - } - } - - /// Get the system path for the document selected by this query. - pub(crate) fn file_path(&self) -> &AnySystemPath { - match self { - Self::Text { file_path, .. } | Self::Notebook { file_path, .. } => file_path, - } - } - - /// Attempt to access the single inner text document selected by the query. - /// If this query is selecting an entire notebook document, this will return `None`. - #[expect(dead_code)] - pub(crate) fn as_single_document(&self) -> Option<&TextDocument> { - match self { - Self::Text { document, .. } => Some(document), - Self::Notebook { - notebook, - cell_url: cell_uri, - .. - } => cell_uri - .as_ref() - .and_then(|cell_uri| notebook.cell_document_by_uri(cell_uri)), - } - } - - /// Returns the salsa interned [`File`] for the document selected by this query. - /// - /// It returns [`None`] for the following cases: - /// - For virtual file, if it's not yet opened - /// - For regular file, if it does not exists or is a directory - pub(crate) fn file(&self, db: &dyn Db) -> Option { - match self.file_path() { - AnySystemPath::System(path) => system_path_to_file(db, path).ok(), - AnySystemPath::SystemVirtual(virtual_path) => db - .files() - .try_virtual_file(virtual_path) - .map(|virtual_file| virtual_file.file()), - } - } -} - #[derive(Debug, Clone, thiserror::Error)] -pub(crate) enum DocumentQueryError { - #[error("invalid URL: {0}")] - InvalidUrl(Url), +pub(crate) enum DocumentError { #[error("document not found for key: {0}")] NotFound(DocumentKey), } diff --git a/crates/ty_server/src/system.rs b/crates/ty_server/src/system.rs index 323e4a6846..17b9bcbde6 100644 --- a/crates/ty_server/src/system.rs +++ b/crates/ty_server/src/system.rs @@ -4,6 +4,8 @@ use std::fmt::Display; use std::panic::RefUnwindSafe; use std::sync::Arc; +use crate::document::DocumentKey; +use crate::session::index::{Document, Index}; use lsp_types::Url; use ruff_db::file_revision::FileRevision; use ruff_db::files::{File, FilePath}; @@ -16,10 +18,6 @@ use ruff_notebook::{Notebook, NotebookError}; use ty_ide::cached_vendored_path; use ty_python_semantic::Db; -use crate::DocumentQuery; -use crate::document::DocumentKey; -use crate::session::index::Index; - /// Returns a [`Url`] for the given [`File`]. pub(crate) fn file_to_url(db: &dyn Db, file: File) -> Option { match file.path(db) { @@ -41,26 +39,6 @@ pub(crate) enum AnySystemPath { } impl AnySystemPath { - /// Converts the given [`Url`] to an [`AnySystemPath`]. - /// - /// If the URL scheme is `file`, then the path is converted to a [`SystemPathBuf`]. Otherwise, the - /// URL is converted to a [`SystemVirtualPathBuf`]. - /// - /// This fails in the following cases: - /// * The URL cannot be converted to a file path (refer to [`Url::to_file_path`]). - /// * If the URL is not a valid UTF-8 string. - pub(crate) fn try_from_url(url: &Url) -> std::result::Result { - if url.scheme() == "file" { - Ok(AnySystemPath::System( - SystemPathBuf::from_path_buf(url.to_file_path()?).map_err(|_| ())?, - )) - } else { - Ok(AnySystemPath::SystemVirtual( - SystemVirtualPath::new(url.as_str()).to_path_buf(), - )) - } - } - pub(crate) const fn as_system(&self) -> Option<&SystemPathBuf> { match self { AnySystemPath::System(system_path_buf) => Some(system_path_buf), @@ -68,21 +46,11 @@ impl AnySystemPath { } } - /// Returns the extension of the path, if any. - pub(crate) fn extension(&self) -> Option<&str> { + #[expect(unused)] + pub(crate) const fn as_virtual(&self) -> Option<&SystemVirtualPath> { match self { - AnySystemPath::System(system_path) => system_path.extension(), - AnySystemPath::SystemVirtual(virtual_path) => virtual_path.extension(), - } - } - - /// Converts the path to a URL. - pub(crate) fn to_url(&self) -> Option { - match self { - AnySystemPath::System(system_path) => { - Url::from_file_path(system_path.as_std_path()).ok() - } - AnySystemPath::SystemVirtual(virtual_path) => Url::parse(virtual_path.as_str()).ok(), + AnySystemPath::SystemVirtual(path) => Some(path.as_path()), + AnySystemPath::System(_) => None, } } } @@ -144,21 +112,17 @@ impl LSPSystem { self.index.as_ref().unwrap() } - fn make_document_ref(&self, path: AnySystemPath) -> Option { + fn make_document_ref(&self, path: AnySystemPath) -> Option<&Document> { let index = self.index(); - let key = DocumentKey::from_path(path); - index.make_document_ref(key).ok() + index.document(&DocumentKey::from(path)).ok() } - fn system_path_to_document_ref(&self, path: &SystemPath) -> Option { + fn system_path_to_document_ref(&self, path: &SystemPath) -> Option<&Document> { let any_path = AnySystemPath::System(path.to_path_buf()); self.make_document_ref(any_path) } - fn system_virtual_path_to_document_ref( - &self, - path: &SystemVirtualPath, - ) -> Option { + fn system_virtual_path_to_document_ref(&self, path: &SystemVirtualPath) -> Option<&Document> { let any_path = AnySystemPath::SystemVirtual(path.to_path_buf()); self.make_document_ref(any_path) } @@ -170,7 +134,7 @@ impl System for LSPSystem { if let Some(document) = document { Ok(Metadata::new( - document_revision(&document), + document_revision(document), None, FileType::File, )) @@ -191,7 +155,7 @@ impl System for LSPSystem { let document = self.system_path_to_document_ref(path); match document { - Some(DocumentQuery::Text { document, .. }) => Ok(document.contents().to_string()), + Some(Document::Text(document)) => Ok(document.contents().to_string()), _ => self.native_system.read_to_string(path), } } @@ -200,10 +164,8 @@ impl System for LSPSystem { let document = self.system_path_to_document_ref(path); match document { - Some(DocumentQuery::Text { document, .. }) => { - Notebook::from_source_code(document.contents()) - } - Some(DocumentQuery::Notebook { notebook, .. }) => Ok(notebook.make_ruff_notebook()), + Some(Document::Text(document)) => Notebook::from_source_code(document.contents()), + Some(Document::Notebook(notebook)) => Ok(notebook.make_ruff_notebook()), None => self.native_system.read_to_notebook(path), } } @@ -213,7 +175,7 @@ impl System for LSPSystem { .system_virtual_path_to_document_ref(path) .ok_or_else(|| virtual_path_not_found(path))?; - if let DocumentQuery::Text { document, .. } = &document { + if let Document::Text(document) = &document { Ok(document.contents().to_string()) } else { Err(not_a_text_document(path)) @@ -229,8 +191,8 @@ impl System for LSPSystem { .ok_or_else(|| virtual_path_not_found(path))?; match document { - DocumentQuery::Text { document, .. } => Notebook::from_source_code(document.contents()), - DocumentQuery::Notebook { notebook, .. } => Ok(notebook.make_ruff_notebook()), + Document::Text(document) => Notebook::from_source_code(document.contents()), + Document::Notebook(notebook) => Ok(notebook.make_ruff_notebook()), } } @@ -307,7 +269,7 @@ fn virtual_path_not_found(path: impl Display) -> std::io::Error { } /// Helper function to get the [`FileRevision`] of the given document. -fn document_revision(document: &DocumentQuery) -> FileRevision { +fn document_revision(document: &Document) -> FileRevision { // The file revision is just an opaque number which doesn't have any significant meaning other // than that the file has changed if the revisions are different. #[expect(clippy::cast_sign_loss)] From a32d5b8dc416c719bbc7217f4e1563237d510e9a Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 31 Oct 2025 16:51:11 -0400 Subject: [PATCH 108/188] [ty] Improve exhaustiveness analysis for type variables with bounds or constraints (#21172) --- .../mdtest/exhaustiveness_checking.md | 52 +++++++++++++++ .../ty_python_semantic/src/types/builder.rs | 64 ++++++++++--------- 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md index 29b267024b..4379498f2d 100644 --- a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -417,3 +417,55 @@ class Answer(Enum): case Answer.NO: return False ``` + +## Exhaustiveness checking for type variables with bounds or constraints + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import assert_never, Literal + +def f[T: bool](x: T) -> T: + match x: + case True: + return x + case False: + return x + case _: + reveal_type(x) # revealed: Never + assert_never(x) + +def g[T: Literal["foo", "bar"]](x: T) -> T: + match x: + case "foo": + return x + case "bar": + return x + case _: + reveal_type(x) # revealed: Never + assert_never(x) + +def h[T: int | str](x: T) -> T: + if isinstance(x, int): + return x + elif isinstance(x, str): + return x + else: + reveal_type(x) # revealed: Never + assert_never(x) + +def i[T: (int, str)](x: T) -> T: + match x: + case int(): + pass + case str(): + pass + case _: + reveal_type(x) # revealed: Never + assert_never(x) + + return x +``` diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index 6b555b6fdb..11017d1571 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -781,37 +781,6 @@ impl<'db> IntersectionBuilder<'db> { seen_aliases, ) } - Type::EnumLiteral(enum_literal) => { - let enum_class = enum_literal.enum_class(self.db); - let metadata = - enum_metadata(self.db, enum_class).expect("Class of enum literal is an enum"); - - let enum_members_in_negative_part = self - .intersections - .iter() - .flat_map(|intersection| &intersection.negative) - .filter_map(|ty| ty.as_enum_literal()) - .filter(|lit| lit.enum_class(self.db) == enum_class) - .map(|lit| lit.name(self.db)) - .chain(std::iter::once(enum_literal.name(self.db))) - .collect::>(); - - let all_members_are_in_negative_part = metadata - .members - .keys() - .all(|name| enum_members_in_negative_part.contains(name)); - - if all_members_are_in_negative_part { - for inner in &mut self.intersections { - inner.add_negative(self.db, enum_literal.enum_class_instance(self.db)); - } - } else { - for inner in &mut self.intersections { - inner.add_negative(self.db, ty); - } - } - self - } _ => { for inner in &mut self.intersections { inner.add_negative(self.db, ty); @@ -1177,6 +1146,39 @@ impl<'db> InnerIntersectionBuilder<'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 + // to their upper bound and all constrained type variables to the union of their constraints. + // If that speculative intersection simplifies to `Never`, this intersection must also simplify + // to `Never`. + if self.positive.iter().any(|ty| ty.is_type_var()) { + let mut speculative = IntersectionBuilder::new(db); + for pos in &self.positive { + match pos { + Type::TypeVar(type_var) => { + match type_var.typevar(db).bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + speculative = speculative.add_positive(bound); + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + speculative = speculative.add_positive(Type::Union(constraints)); + } + // TypeVars without a bound or constraint implicitly have `object` as their + // upper bound, and it is always a no-op to add `object` to an intersection. + None => {} + } + } + _ => speculative = speculative.add_positive(*pos), + } + } + for neg in &self.negative { + speculative = speculative.add_negative(*neg); + } + if speculative.build().is_never() { + return Type::Never; + } + } + match (self.positive.len(), self.negative.len()) { (0, 0) => Type::object(), (1, 0) => self.positive[0], From 521217bb904a03542f06306c462379a6bcbb54fc Mon Sep 17 00:00:00 2001 From: Gautham Venkataraman <26820345+gauthsvenkat@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:47:01 +0100 Subject: [PATCH 109/188] [ruff]: Make `ruff analyze graph` work with jupyter notebooks (#21161) Co-authored-by: Gautham Venkataraman Co-authored-by: Micha Reiser --- crates/ruff/src/commands/analyze_graph.rs | 38 +++++-- crates/ruff/tests/analyze_graph.rs | 130 ++++++++++++++++++++++ crates/ruff_graph/src/lib.rs | 10 +- 3 files changed, 164 insertions(+), 14 deletions(-) diff --git a/crates/ruff/src/commands/analyze_graph.rs b/crates/ruff/src/commands/analyze_graph.rs index ffd7cc2d15..d4085e8ed0 100644 --- a/crates/ruff/src/commands/analyze_graph.rs +++ b/crates/ruff/src/commands/analyze_graph.rs @@ -7,6 +7,7 @@ use path_absolutize::CWD; use ruff_db::system::{SystemPath, SystemPathBuf}; use ruff_graph::{Direction, ImportMap, ModuleDb, ModuleImports}; use ruff_linter::package::PackageRoot; +use ruff_linter::source_kind::SourceKind; use ruff_linter::{warn_user, warn_user_once}; use ruff_python_ast::{PySourceType, SourceType}; use ruff_workspace::resolver::{ResolvedFile, match_exclusion, python_files_in_path}; @@ -127,10 +128,6 @@ pub(crate) fn analyze_graph( }, Some(language) => PySourceType::from(language), }; - if matches!(source_type, PySourceType::Ipynb) { - debug!("Ignoring Jupyter notebook: {}", path.display()); - continue; - } // Convert to system paths. let Ok(package) = package.map(SystemPathBuf::from_path_buf).transpose() else { @@ -147,13 +144,34 @@ pub(crate) fn analyze_graph( let root = root.clone(); 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(), source_type) { + Ok(Some(source_kind)) => source_kind, + Ok(None) => { + debug!("Skipping non-Python notebook: {path}"); + return; + } + Err(err) => { + warn!("Failed to read source for {path}: {err}"); + return; + } + }; + + let source_code = source_kind.source_code(); + // Identify any imports via static analysis. - let mut imports = - ModuleImports::detect(&db, &path, package.as_deref(), string_imports) - .unwrap_or_else(|err| { - warn!("Failed to generate import map for {path}: {err}"); - ModuleImports::default() - }); + let mut imports = ModuleImports::detect( + &db, + source_code, + source_type, + &path, + package.as_deref(), + string_imports, + ) + .unwrap_or_else(|err| { + warn!("Failed to generate import map for {path}: {err}"); + ModuleImports::default() + }); debug!("Discovered {} imports for {}", imports.len(), path); diff --git a/crates/ruff/tests/analyze_graph.rs b/crates/ruff/tests/analyze_graph.rs index 2c300029ea..993ebf3b59 100644 --- a/crates/ruff/tests/analyze_graph.rs +++ b/crates/ruff/tests/analyze_graph.rs @@ -653,3 +653,133 @@ fn venv() -> Result<()> { Ok(()) } + +#[test] +fn notebook_basic() -> Result<()> { + let tempdir = TempDir::new()?; + let root = ChildPath::new(tempdir.path()); + + root.child("ruff").child("__init__.py").write_str("")?; + root.child("ruff") + .child("a.py") + .write_str(indoc::indoc! {r#" + def helper(): + pass + "#})?; + + // Create a basic notebook with a simple import + root.child("notebook.ipynb").write_str(indoc::indoc! {r#" + { + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ruff.a import helper" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 + } + "#})?; + + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec(), + }, { + assert_cmd_snapshot!(command().current_dir(&root), @r###" + success: true + exit_code: 0 + ----- stdout ----- + { + "notebook.ipynb": [ + "ruff/a.py" + ], + "ruff/__init__.py": [], + "ruff/a.py": [] + } + + ----- stderr ----- + "###); + }); + + Ok(()) +} + +#[test] +fn notebook_with_magic() -> Result<()> { + let tempdir = TempDir::new()?; + let root = ChildPath::new(tempdir.path()); + + root.child("ruff").child("__init__.py").write_str("")?; + root.child("ruff") + .child("a.py") + .write_str(indoc::indoc! {r#" + def helper(): + pass + "#})?; + + // Create a notebook with IPython magic commands and imports + root.child("notebook.ipynb").write_str(indoc::indoc! {r#" + { + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ruff.a import helper" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 + } + "#})?; + + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec(), + }, { + assert_cmd_snapshot!(command().current_dir(&root), @r###" + success: true + exit_code: 0 + ----- stdout ----- + { + "notebook.ipynb": [ + "ruff/a.py" + ], + "ruff/__init__.py": [], + "ruff/a.py": [] + } + + ----- stderr ----- + "###); + }); + + Ok(()) +} diff --git a/crates/ruff_graph/src/lib.rs b/crates/ruff_graph/src/lib.rs index eaf307018d..377f1e89e9 100644 --- a/crates/ruff_graph/src/lib.rs +++ b/crates/ruff_graph/src/lib.rs @@ -3,8 +3,9 @@ use std::collections::{BTreeMap, BTreeSet}; use anyhow::Result; use ruff_db::system::{SystemPath, SystemPathBuf}; +use ruff_python_ast::PySourceType; use ruff_python_ast::helpers::to_module_path; -use ruff_python_parser::{Mode, ParseOptions, parse}; +use ruff_python_parser::{ParseOptions, parse}; use crate::collector::Collector; pub use crate::db::ModuleDb; @@ -24,13 +25,14 @@ impl ModuleImports { /// Detect the [`ModuleImports`] for a given Python file. pub fn detect( db: &ModuleDb, + source: &str, + source_type: PySourceType, path: &SystemPath, package: Option<&SystemPath>, string_imports: StringImports, ) -> Result { - // Read and parse the source code. - let source = std::fs::read_to_string(path)?; - let parsed = parse(&source, ParseOptions::from(Mode::Module))?; + // Parse the source code. + let parsed = parse(source, ParseOptions::from(source_type))?; let module_path = package.and_then(|package| to_module_path(package.as_std_path(), path.as_std_path())); From a151f9746d202647d1ce63a39fe357175395e182 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 21:03:40 -0400 Subject: [PATCH 110/188] [ty] Sync vendored typeshed stubs (#21178) Close and reopen this PR to trigger CI --------- Co-authored-by: typeshedbot <> --- crates/ty_vendored/vendor/typeshed/README.md | 12 +-- .../vendor/typeshed/source_commit.txt | 2 +- .../vendor/typeshed/stdlib/builtins.pyi | 4 + .../vendor/typeshed/stdlib/cmath.pyi | 2 +- .../vendor/typeshed/stdlib/contextlib.pyi | 21 ++++-- .../vendor/typeshed/stdlib/enum.pyi | 2 + .../vendor/typeshed/stdlib/os/__init__.pyi | 3 + .../vendor/typeshed/stdlib/sys/__init__.pyi | 24 ++++++ .../vendor/typeshed/stdlib/sysconfig.pyi | 8 +- .../typeshed/stdlib/tkinter/__init__.pyi | 75 ++++++++----------- .../vendor/typeshed/stdlib/turtle.pyi | 8 +- .../vendor/typeshed/stdlib/zlib.pyi | 4 +- 12 files changed, 97 insertions(+), 68 deletions(-) diff --git a/crates/ty_vendored/vendor/typeshed/README.md b/crates/ty_vendored/vendor/typeshed/README.md index 1467aa20b4..d295b56bc0 100644 --- a/crates/ty_vendored/vendor/typeshed/README.md +++ b/crates/ty_vendored/vendor/typeshed/README.md @@ -7,10 +7,10 @@ ## About Typeshed contains external type annotations for the Python standard library -and Python builtins, as well as third party packages as contributed by +and Python builtins, as well as third-party packages that are contributed by people external to those projects. -This data can e.g. be used for static analysis, type checking, type inference, +This data can, e.g., be used for static analysis, type checking, type inference, and autocompletion. For information on how to use typeshed, read below. Information for @@ -29,8 +29,8 @@ If you're just using a type checker (e.g. [mypy](https://github.com/python/mypy/ [pyright](https://github.com/microsoft/pyright), or PyCharm's built-in type checker), as opposed to developing it, you don't need to interact with the typeshed repo at -all: a copy of standard library part of typeshed is bundled with type checkers. -And type stubs for third party packages and modules you are using can +all: a copy of the standard library part of typeshed is bundled with type checkers. +And type stubs for third-party packages and modules you are using can be installed from PyPI. For example, if you are using `html5lib` and `requests`, you can install the type stubs using @@ -70,7 +70,7 @@ package you're using, each with its own tradeoffs: type checking due to changes in the stubs. Another risk of this strategy is that stubs often lag behind - the package being stubbed. You might want to force the package being stubbed + the package that is being stubbed. You might want to force the package being stubbed to a certain minimum version because it fixes a critical bug, but if correspondingly updated stubs have not been released, your type checking results may not be fully accurate. @@ -119,6 +119,6 @@ a review of your type annotations or stubs outside of typeshed, head over to [our discussion forum](https://github.com/python/typing/discussions). For less formal discussion, try the typing chat room on [gitter.im](https://gitter.im/python/typing). Some typeshed maintainers -are almost always present; feel free to find us there and we're happy +are almost always present; feel free to find us there, and we're happy to chat. Substantive technical discussion will be directed to the issue tracker. diff --git a/crates/ty_vendored/vendor/typeshed/source_commit.txt b/crates/ty_vendored/vendor/typeshed/source_commit.txt index 54a8607d25..d0fd6efd8e 100644 --- a/crates/ty_vendored/vendor/typeshed/source_commit.txt +++ b/crates/ty_vendored/vendor/typeshed/source_commit.txt @@ -1 +1 @@ -d6f4a0f7102b1400a21742cf9b7ea93614e2b6ec +bf7214784877c52638844c065360d4814fae4c65 diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi index bcacb3857b..4859bbe675 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi @@ -4525,6 +4525,10 @@ class BaseException: def __setstate__(self, state: dict[str, Any] | None, /) -> None: ... def with_traceback(self, tb: TracebackType | None, /) -> Self: """Set self.__traceback__ to tb and return self.""" + # Necessary for security-focused static analyzers (e.g, pysa) + # See https://github.com/python/typeshed/pull/14900 + def __str__(self) -> str: ... # noqa: Y029 + def __repr__(self) -> str: ... # noqa: Y029 if sys.version_info >= (3, 11): # only present after add_note() is called __notes__: list[str] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/cmath.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/cmath.pyi index 575f2bf95d..659595046b 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/cmath.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/cmath.pyi @@ -67,7 +67,7 @@ def isinf(z: _C, /) -> bool: def isnan(z: _C, /) -> bool: """Checks if the real or imaginary part of z not a number (NaN).""" -def log(x: _C, base: _C = ..., /) -> complex: +def log(z: _C, base: _C = ..., /) -> complex: """log(z[, base]) -> the logarithm of z to the given base. If the base is not specified, returns the natural logarithm (base e) of z. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.pyi index 2b05511c33..85baf55925 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.pyi @@ -6,7 +6,7 @@ from _typeshed import FileDescriptorOrPath, Unused from abc import ABC, abstractmethod from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Generator, Iterator from types import TracebackType -from typing import IO, Any, Generic, Protocol, TypeVar, overload, runtime_checkable, type_check_only +from typing import Any, Generic, Protocol, TypeVar, overload, runtime_checkable, type_check_only from typing_extensions import ParamSpec, Self, TypeAlias __all__ = [ @@ -32,7 +32,6 @@ if sys.version_info >= (3, 11): _T = TypeVar("_T") _T_co = TypeVar("_T_co", covariant=True) -_T_io = TypeVar("_T_io", bound=IO[str] | None) _ExitT_co = TypeVar("_ExitT_co", covariant=True, bound=bool | None, default=bool | None) _F = TypeVar("_F", bound=Callable[..., Any]) _G_co = TypeVar("_G_co", bound=Generator[Any, Any, Any] | AsyncGenerator[Any, Any], covariant=True) @@ -275,13 +274,23 @@ class suppress(AbstractContextManager[None, bool]): self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None ) -> bool: ... -class _RedirectStream(AbstractContextManager[_T_io, None]): - def __init__(self, new_target: _T_io) -> None: ... +# This is trying to describe what is needed for (most?) uses +# of `redirect_stdout` and `redirect_stderr`. +# https://github.com/python/typeshed/issues/14903 +@type_check_only +class _SupportsRedirect(Protocol): + def write(self, s: str, /) -> int: ... + def flush(self) -> None: ... + +_SupportsRedirectT = TypeVar("_SupportsRedirectT", bound=_SupportsRedirect | None) + +class _RedirectStream(AbstractContextManager[_SupportsRedirectT, None]): + def __init__(self, new_target: _SupportsRedirectT) -> None: ... def __exit__( self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None ) -> None: ... -class redirect_stdout(_RedirectStream[_T_io]): +class redirect_stdout(_RedirectStream[_SupportsRedirectT]): """Context manager for temporarily redirecting stdout to another file. # How to send help() to stderr @@ -294,7 +303,7 @@ class redirect_stdout(_RedirectStream[_T_io]): help(pow) """ -class redirect_stderr(_RedirectStream[_T_io]): +class redirect_stderr(_RedirectStream[_SupportsRedirectT]): """Context manager for temporarily redirecting stderr to another file.""" class _BaseExitStack(Generic[_ExitT_co]): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/enum.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/enum.pyi index b9933de380..825340e75b 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/enum.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/enum.pyi @@ -623,6 +623,8 @@ if sys.version_info >= (3, 11): the module is the last module in case of a multi-module name """ + def show_flag_values(value: int) -> list[int]: ... + if sys.version_info >= (3, 12): # The body of the class is the same, but the base classes are different. class IntFlag(int, ReprEnum, Flag, boundary=KEEP): # type: ignore[misc] # complaints about incompatible bases diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.pyi index 88f6a919a1..1ea3e4ea80 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.pyi @@ -752,6 +752,9 @@ environ: _Environ[str] if sys.platform != "win32": environb: _Environ[bytes] +if sys.version_info >= (3, 14): + def reload_environ() -> None: ... + if sys.version_info >= (3, 11) or sys.platform != "win32": EX_OK: Final[int] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.pyi index 21514c7609..0ecc8e2693 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.pyi @@ -578,6 +578,21 @@ def _getframe(depth: int = 0, /) -> FrameType: only. """ +# documented -- see https://docs.python.org/3/library/sys.html#sys._current_exceptions +if sys.version_info >= (3, 12): + def _current_exceptions() -> dict[int, BaseException | None]: + """Return a dict mapping each thread's identifier to its current raised exception. + + This function should be used for specialized purposes only. + """ + +else: + def _current_exceptions() -> dict[int, OptExcInfo]: + """Return a dict mapping each thread's identifier to its current raised exception. + + This function should be used for specialized purposes only. + """ + if sys.version_info >= (3, 12): def _getframemodulename(depth: int = 0) -> str | None: """Return the name of the module for a calling frame. @@ -627,6 +642,9 @@ def exit(status: _ExitCode = None, /) -> NoReturn: exit status will be one (i.e., failure). """ +if sys.platform == "android": # noqa: Y008 + def getandroidapilevel() -> int: ... + def getallocatedblocks() -> int: """Return the number of memory blocks currently allocated.""" @@ -949,3 +967,9 @@ if sys.version_info >= (3, 14): script (str|bytes): The path to a file containing the Python code to be executed. """ + + def _is_immortal(op: object, /) -> bool: + """Return True if the given object is "immortal" per PEP 683. + + This function should be used for specialized purposes only. + """ diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.pyi index 8cdd3b1b2f..1dfb9f3cfe 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.pyi @@ -2,7 +2,7 @@ import sys from typing import IO, Any, Literal, overload -from typing_extensions import deprecated +from typing_extensions import LiteralString, deprecated __all__ = [ "get_config_h_filename", @@ -47,8 +47,10 @@ def get_scheme_names() -> tuple[str, ...]: """Return a tuple containing the schemes names.""" if sys.version_info >= (3, 10): - def get_default_scheme() -> str: ... - def get_preferred_scheme(key: Literal["prefix", "home", "user"]) -> str: ... + def get_default_scheme() -> LiteralString: ... + def get_preferred_scheme(key: Literal["prefix", "home", "user"]) -> LiteralString: ... + # Documented -- see https://docs.python.org/3/library/sysconfig.html#sysconfig._get_preferred_schemes + def _get_preferred_schemes() -> dict[Literal["prefix", "home", "user"], LiteralString]: ... def get_path_names() -> tuple[str, ...]: """Return a tuple containing the paths names.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi index 1f31c1fbb4..1d8e299023 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi @@ -1721,17 +1721,22 @@ class Wm: if sys.platform == "darwin": @overload def wm_attributes(self, option: Literal["-modified"], /) -> bool: - """Return or sets platform specific attributes. + """This subcommand returns or sets platform specific attributes - When called with a single argument return_python_dict=True, - return a dict of the platform specific attributes and their values. - When called without arguments or with a single argument - return_python_dict=False, return a tuple containing intermixed - attribute names with the minus prefix and their values. + The first form returns a list of the platform specific flags and + their values. The second form returns the value for the specific + option. The third form sets one or more of the values. The values + are as follows: - When called with a single string value, return the value for the - specific option. When called with keyword arguments, set the - corresponding attributes. + On Windows, -disabled gets or sets whether the window is in a + disabled state. -toolwindow gets or sets the style of the window + to toolwindow (as defined in the MSDN). -topmost gets or sets + whether this is a topmost window (displays above all other + windows). + + On Macintosh, XXXXX + + On Unix, there are currently no special attribute values. """ @overload @@ -1803,20 +1808,7 @@ class Wm: def wm_attributes(self, option: Literal["topmost"], /) -> bool: ... if sys.platform == "darwin": @overload - def wm_attributes(self, option: Literal["modified"], /) -> bool: - """Return or sets platform specific attributes. - - When called with a single argument return_python_dict=True, - return a dict of the platform specific attributes and their values. - When called without arguments or with a single argument - return_python_dict=False, return a tuple containing intermixed - attribute names with the minus prefix and their values. - - When called with a single string value, return the value for the - specific option. When called with keyword arguments, set the - corresponding attributes. - """ - + def wm_attributes(self, option: Literal["modified"], /) -> bool: ... @overload def wm_attributes(self, option: Literal["notify"], /) -> bool: ... @overload @@ -1876,17 +1868,22 @@ class Wm: if sys.platform == "darwin": @overload def wm_attributes(self, option: Literal["-modified"], value: bool, /) -> Literal[""]: - """Return or sets platform specific attributes. + """This subcommand returns or sets platform specific attributes - When called with a single argument return_python_dict=True, - return a dict of the platform specific attributes and their values. - When called without arguments or with a single argument - return_python_dict=False, return a tuple containing intermixed - attribute names with the minus prefix and their values. + The first form returns a list of the platform specific flags and + their values. The second form returns the value for the specific + option. The third form sets one or more of the values. The values + are as follows: - When called with a single string value, return the value for the - specific option. When called with keyword arguments, set the - corresponding attributes. + On Windows, -disabled gets or sets whether the window is in a + disabled state. -toolwindow gets or sets the style of the window + to toolwindow (as defined in the MSDN). -topmost gets or sets + whether this is a topmost window (displays above all other + windows). + + On Macintosh, XXXXX + + On Unix, there are currently no special attribute values. """ @overload @@ -1950,19 +1947,7 @@ class Wm: titlepath: str = ..., topmost: bool = ..., transparent: bool = ..., - ) -> None: - """Return or sets platform specific attributes. - - When called with a single argument return_python_dict=True, - return a dict of the platform specific attributes and their values. - When called without arguments or with a single argument - return_python_dict=False, return a tuple containing intermixed - attribute names with the minus prefix and their values. - - When called with a single string value, return the value for the - specific option. When called with keyword arguments, set the - corresponding attributes. - """ + ) -> None: ... elif sys.platform == "win32": @overload def wm_attributes( diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/turtle.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/turtle.pyi index b0e7c1bf29..61cd2e44b5 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/turtle.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/turtle.pyi @@ -669,7 +669,7 @@ class TurtleScreen(TurtleScreenBase): ['arrow', 'blank', 'circle', ... , 'turtle'] """ - def onclick(self, fun: Callable[[float, float], object], btn: int = 1, add: Any | None = None) -> None: + def onclick(self, fun: Callable[[float, float], object], btn: int = 1, add: bool | None = None) -> None: """Bind fun to mouse-click event on canvas. Arguments: @@ -2540,7 +2540,7 @@ def getshapes() -> list[str]: ['arrow', 'blank', 'circle', ... , 'turtle'] """ -def onclick(fun: Callable[[float, float], object], btn: int = 1, add: Any | None = None) -> None: +def onclick(fun: Callable[[float, float], object], btn: int = 1, add: bool | None = None) -> None: """Bind fun to mouse-click event on this turtle on canvas. Arguments: @@ -3960,7 +3960,7 @@ def getturtle() -> Turtle: getpen = getturtle -def onrelease(fun: Callable[[float, float], object], btn: int = 1, add: Any | None = None) -> None: +def onrelease(fun: Callable[[float, float], object], btn: int = 1, add: bool | None = None) -> None: """Bind fun to mouse-button-release event on this turtle on canvas. Arguments: @@ -3983,7 +3983,7 @@ def onrelease(fun: Callable[[float, float], object], btn: int = 1, add: Any | No transparent. """ -def ondrag(fun: Callable[[float, float], object], btn: int = 1, add: Any | None = None) -> None: +def ondrag(fun: Callable[[float, float], object], btn: int = 1, add: bool | None = None) -> None: """Bind fun to mouse-move event on this turtle on canvas. Arguments: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/zlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zlib.pyi index 97d70804a3..a8231f62ee 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/zlib.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/zlib.pyi @@ -41,8 +41,8 @@ Z_RLE: Final = 3 Z_SYNC_FLUSH: Final = 2 Z_TREES: Final = 6 -if sys.version_info >= (3, 14) and sys.platform == "win32": - # Available when zlib was built with zlib-ng, usually only on Windows +if sys.version_info >= (3, 14): + # Available when zlib was built with zlib-ng ZLIBNG_VERSION: Final[str] class error(Exception): ... From 921f409ee8fe1a166294a13cad3fccd0db870c7b Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sat, 1 Nov 2025 02:50:58 +0100 Subject: [PATCH 111/188] Update Rust toolchain to 1.91 (#21179) --- crates/ruff/src/commands/format.rs | 2 +- crates/ruff_dev/src/generate_options.rs | 2 +- crates/ruff_dev/src/generate_ty_options.rs | 2 +- crates/ruff_formatter/src/builders.rs | 2 +- .../rules/unconventional_import_alias.rs | 2 +- .../src/rules/flake8_pytest_style/types.rs | 24 +++++-------------- .../src/rules/flake8_quotes/settings.rs | 8 ++----- .../rules/reimplemented_builtin.rs | 4 ++-- .../flake8_tidy_imports/rules/banned_api.rs | 6 ++--- .../ruff_linter/src/rules/isort/settings.rs | 8 ++----- .../pycodestyle/rules/literal_comparisons.rs | 2 +- .../src/rules/pydocstyle/rules/capitalized.rs | 2 +- .../src/rules/refurb/rules/bit_count.rs | 2 +- crates/ruff_python_formatter/src/lib.rs | 2 +- .../ty_python_semantic/src/python_platform.rs | 2 +- .../src/semantic_index/use_def.rs | 2 +- rust-toolchain.toml | 2 +- 17 files changed, 27 insertions(+), 47 deletions(-) diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 1f79e59339..0e245efa8c 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -370,7 +370,7 @@ pub(crate) fn format_source( let line_index = LineIndex::from_source_text(unformatted); let byte_range = range.to_text_range(unformatted, &line_index); format_range(unformatted, byte_range, options).map(|formatted_range| { - let mut formatted = unformatted.to_string(); + let mut formatted = unformatted.clone(); formatted.replace_range( std::ops::Range::::from(formatted_range.source_range()), formatted_range.as_code(), diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index 8b8579d730..49a898d6fe 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -62,7 +62,7 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec) { generate_set( output, Set::Named { - name: set_name.to_string(), + name: set_name.clone(), set: *sub_set, }, parents, diff --git a/crates/ruff_dev/src/generate_ty_options.rs b/crates/ruff_dev/src/generate_ty_options.rs index af7794a0b2..4e4ab0a949 100644 --- a/crates/ruff_dev/src/generate_ty_options.rs +++ b/crates/ruff_dev/src/generate_ty_options.rs @@ -104,7 +104,7 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec) { generate_set( output, Set::Named { - name: set_name.to_string(), + name: set_name.clone(), set: *sub_set, }, parents, diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index 14da643355..ab60103d99 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -1006,7 +1006,7 @@ impl std::fmt::Debug for Align<'_, Context> { /// Block indents indent a block of code, such as in a function body, and therefore insert a line /// break before and after the content. /// -/// Doesn't create an indentation if the passed in content is [`FormatElement.is_empty`]. +/// Doesn't create an indentation if the passed in content is empty. /// /// # Examples /// diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs index e0684056dc..6827e99b93 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs @@ -78,7 +78,7 @@ pub(crate) fn unconventional_import_alias( let mut diagnostic = checker.report_diagnostic( UnconventionalImportAlias { name: qualified_name, - asname: expected_alias.to_string(), + asname: expected_alias.clone(), }, binding.range(), ); diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/types.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/types.rs index 0de6758635..bd57ad080c 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/types.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/types.rs @@ -6,21 +6,17 @@ use ruff_macros::CacheKey; #[derive(Clone, Copy, Debug, CacheKey, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Default)] pub enum ParametrizeNameType { #[serde(rename = "csv")] Csv, #[serde(rename = "tuple")] + #[default] Tuple, #[serde(rename = "list")] List, } -impl Default for ParametrizeNameType { - fn default() -> Self { - Self::Tuple - } -} - impl Display for ParametrizeNameType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -33,19 +29,15 @@ impl Display for ParametrizeNameType { #[derive(Clone, Copy, Debug, CacheKey, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Default)] pub enum ParametrizeValuesType { #[serde(rename = "tuple")] Tuple, #[serde(rename = "list")] + #[default] List, } -impl Default for ParametrizeValuesType { - fn default() -> Self { - Self::List - } -} - impl Display for ParametrizeValuesType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -57,19 +49,15 @@ impl Display for ParametrizeValuesType { #[derive(Clone, Copy, Debug, CacheKey, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Default)] pub enum ParametrizeValuesRowType { #[serde(rename = "tuple")] + #[default] Tuple, #[serde(rename = "list")] List, } -impl Default for ParametrizeValuesRowType { - fn default() -> Self { - Self::Tuple - } -} - impl Display for ParametrizeValuesRowType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/ruff_linter/src/rules/flake8_quotes/settings.rs b/crates/ruff_linter/src/rules/flake8_quotes/settings.rs index b241e70b49..fe5129d6e3 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/settings.rs @@ -9,19 +9,15 @@ use ruff_macros::CacheKey; #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Default)] pub enum Quote { /// Use double quotes. + #[default] Double, /// Use single quotes. Single, } -impl Default for Quote { - fn default() -> Self { - Self::Double - } -} - impl From for Quote { fn from(value: ruff_python_ast::str::Quote) -> Self { match value { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs index 9c216311ed..4c858fb799 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -116,7 +116,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) { let mut diagnostic = checker.report_diagnostic( ReimplementedBuiltin { - replacement: contents.to_string(), + replacement: contents.clone(), }, TextRange::new(stmt.start(), terminal.stmt.end()), ); @@ -212,7 +212,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) { let mut diagnostic = checker.report_diagnostic( ReimplementedBuiltin { - replacement: contents.to_string(), + replacement: contents.clone(), }, TextRange::new(stmt.start(), terminal.stmt.end()), ); diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs index 6ada015222..6379304d5c 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs @@ -47,7 +47,7 @@ pub(crate) fn banned_api(checker: &Checker, policy: &NameMatchPolicy, checker.report_diagnostic( BannedApi { name: banned_module, - message: reason.msg.to_string(), + message: reason.msg.clone(), }, node.range(), ); @@ -74,8 +74,8 @@ pub(crate) fn banned_attribute_access(checker: &Checker, expr: &Expr) { { checker.report_diagnostic( BannedApi { - name: banned_path.to_string(), - message: ban.msg.to_string(), + name: banned_path.clone(), + message: ban.msg.clone(), }, expr.range(), ); diff --git a/crates/ruff_linter/src/rules/isort/settings.rs b/crates/ruff_linter/src/rules/isort/settings.rs index 05a4dddf08..cab9ab35ed 100644 --- a/crates/ruff_linter/src/rules/isort/settings.rs +++ b/crates/ruff_linter/src/rules/isort/settings.rs @@ -20,21 +20,17 @@ use super::categorize::ImportSection; #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Default)] pub enum RelativeImportsOrder { /// Place "closer" imports (fewer `.` characters, most local) before /// "further" imports (more `.` characters, least local). ClosestToFurthest, /// Place "further" imports (more `.` characters, least local) imports /// before "closer" imports (fewer `.` characters, most local). + #[default] FurthestToClosest, } -impl Default for RelativeImportsOrder { - fn default() -> Self { - Self::FurthestToClosest - } -} - impl Display for RelativeImportsOrder { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs index 6ae6cea817..a68e492846 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs @@ -427,7 +427,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) for diagnostic in &mut diagnostics { diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( - content.to_string(), + content.clone(), compare.range(), ))); } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs index 32cfa89406..23faabc2ec 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs @@ -94,7 +94,7 @@ pub(crate) fn capitalized(checker: &Checker, docstring: &Docstring) { let mut diagnostic = checker.report_diagnostic( FirstWordUncapitalized { first_word: first_word.to_string(), - capitalized_word: capitalized_word.to_string(), + capitalized_word: capitalized_word.clone(), }, docstring.range(), ); diff --git a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs index b86c6e9d9e..0690ca5449 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs @@ -188,7 +188,7 @@ pub(crate) fn bit_count(checker: &Checker, call: &ExprCall) { let mut diagnostic = checker.report_diagnostic( BitCount { existing: SourceCodeSnippet::from_str(literal_text), - replacement: SourceCodeSnippet::new(replacement.to_string()), + replacement: SourceCodeSnippet::new(replacement.clone()), }, call.range(), ); diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index e6b2f9e7b8..bf68598e13 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -334,7 +334,7 @@ class A: ... let options = PyFormatOptions::from_source_type(source_type); let printed = format_range(&source, TextRange::new(start, end), options).unwrap(); - let mut formatted = source.to_string(); + let mut formatted = source.clone(); formatted.replace_range( std::ops::Range::::from(printed.source_range()), printed.as_code(), diff --git a/crates/ty_python_semantic/src/python_platform.rs b/crates/ty_python_semantic/src/python_platform.rs index b21424ee33..04f7fa3598 100644 --- a/crates/ty_python_semantic/src/python_platform.rs +++ b/crates/ty_python_semantic/src/python_platform.rs @@ -24,7 +24,7 @@ impl From for PythonPlatform { fn from(platform: String) -> Self { match platform.as_str() { "all" => PythonPlatform::All, - _ => PythonPlatform::Identifier(platform.to_string()), + _ => PythonPlatform::Identifier(platform.clone()), } } } 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 39f3a1a8ec..dcca102b87 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -233,7 +233,7 @@ //! have two live bindings of `x`: `x = 3` and `x = 4`. //! //! Another piece of information that the `UseDefMap` needs to provide are reachability constraints. -//! See [`reachability_constraints.rs`] for more details, in particular how they apply to bindings. +//! See `reachability_constraints.rs` for more details, in particular how they apply to bindings. //! //! The [`UseDefMapBuilder`] itself just exposes methods for taking a snapshot, resetting to a //! snapshot, and merging a snapshot into the current state. The logic using these methods lives in diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 73328e053b..1a35d66439 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.90" +channel = "1.91" From 17c7b3cde1bef999be944c0b924c2021fa696cb8 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sat, 1 Nov 2025 03:26:38 +0100 Subject: [PATCH 112/188] Bump MSRV to Rust 1.89 (#21180) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 935196f6a5..d12718ea12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.package] # Please update rustfmt.toml when bumping the Rust edition edition = "2024" -rust-version = "1.88" +rust-version = "1.89" homepage = "https://docs.astral.sh/ruff" documentation = "https://docs.astral.sh/ruff" repository = "https://github.com/astral-sh/ruff" From bff32a41dc440b30764e9414767794e01d25c265 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 1 Nov 2025 22:06:03 -0400 Subject: [PATCH 113/188] [ty] Increase timeout-minutes to 10 for py-fuzzer job (#21196) --- .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 806949d81e..5661ff48b7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -277,8 +277,8 @@ jobs: run: cargo test -p ty_python_semantic --test mdtest || true - name: "Run tests" run: cargo insta test --all-features --unreferenced reject --test-runner nextest - # Dogfood ty on py-fuzzer - - run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer + - name: Dogfood ty on py-fuzzer + run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer # Check for broken links in the documentation. - run: cargo doc --all --no-deps env: @@ -649,7 +649,7 @@ jobs: - determine_changes # Only runs on pull requests, since that is the only we way we can find the base version for comparison. if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && (needs.determine_changes.outputs.ty == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }} - timeout-minutes: ${{ github.repository == 'astral-sh/ruff' && 5 || 20 }} + timeout-minutes: ${{ github.repository == 'astral-sh/ruff' && 10 || 20 }} steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: From de1a6fb8ad0fd6b72b9956f7c9c8061e4fc1f413 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Sun, 2 Nov 2025 13:01:06 +0000 Subject: [PATCH 114/188] Clean up definition completions docs and tests (#21183) ## Summary @BurntSushi provided some feedback in #21146 so i address it here. --- crates/ty_ide/src/completion.rs | 34 ++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 273a148ef3..7ca15362b0 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -830,16 +830,14 @@ fn find_typed_text( Some(source[last.range()].to_string()) } -/// Whether the given offset within the parsed module is within -/// a comment or not. +/// Whether the last token is within a comment or not. fn is_in_comment(tokens: &[Token]) -> bool { tokens.last().is_some_and(|t| t.kind().is_comment()) } -/// Returns true when the cursor at `offset` is positioned within -/// a string token (regular, f-string, t-string, etc). +/// Whether the last token is positioned within a string token (regular, f-string, t-string, etc). /// -/// Note that this will return `false` when positioned within an +/// Note that this will return `false` when the last token is positioned within an /// interpolation block in an f-string or a t-string. fn is_in_string(tokens: &[Token]) -> bool { tokens.last().is_some_and(|t| { @@ -850,9 +848,7 @@ fn is_in_string(tokens: &[Token]) -> bool { }) } -/// If the tokens end with `class f` or `def f` we return true. -/// If the tokens end with `class` or `def`, we return false. -/// This is fine because we don't provide completions anyway. +/// Returns true when the tokens indicate that the definition of a new name is being introduced at the end. fn is_in_definition_place(db: &dyn Db, tokens: &[Token], file: File) -> bool { let is_definition_keyword = |token: &Token| { if matches!( @@ -4088,11 +4084,13 @@ def f[T](x: T): fn no_completions_in_function_def_name() { let builder = completion_test_builder( "\ +foo = 1 + def f ", ); - assert!(builder.auto_import().build().completions().is_empty()); + assert!(builder.build().completions().is_empty()); } #[test] @@ -4104,18 +4102,20 @@ def ); // This is okay because the ide will not request completions when the cursor is in this position. - assert!(!builder.auto_import().build().completions().is_empty()); + assert!(!builder.build().completions().is_empty()); } #[test] fn no_completions_in_class_def_name() { let builder = completion_test_builder( "\ +foo = 1 + class f ", ); - assert!(builder.auto_import().build().completions().is_empty()); + assert!(builder.build().completions().is_empty()); } #[test] @@ -4127,29 +4127,33 @@ class ); // This is okay because the ide will not request completions when the cursor is in this position. - assert!(!builder.auto_import().build().completions().is_empty()); + assert!(!builder.build().completions().is_empty()); } #[test] fn no_completions_in_type_def_name() { let builder = completion_test_builder( "\ +foo = 1 + type f = int ", ); - assert!(builder.auto_import().build().completions().is_empty()); + assert!(builder.build().completions().is_empty()); } #[test] fn no_completions_in_maybe_type_def_name() { let builder = completion_test_builder( "\ +foo = 1 + type f ", ); - assert!(builder.auto_import().build().completions().is_empty()); + assert!(builder.build().completions().is_empty()); } #[test] @@ -4161,7 +4165,7 @@ type ); // This is okay because the ide will not request completions when the cursor is in this position. - assert!(!builder.auto_import().build().completions().is_empty()); + assert!(!builder.build().completions().is_empty()); } /// A way to create a simple single-file (named `main.py`) completion test From 73107a083c5f74c7da1a2e85349df78f0a73c3b1 Mon Sep 17 00:00:00 2001 From: David Peter Date: Sun, 2 Nov 2025 14:35:33 +0100 Subject: [PATCH 115/188] [ty] Type inference for comprehensions (#20962) ## Summary Adds type inference for list/dict/set comprehensions, including bidirectional inference: ```py reveal_type({k: v for k, v in [("a", 1), ("b", 2)]}) # dict[Unknown | str, Unknown | int] squares: list[int | None] = [x for x in range(10)] reveal_type(squares) # list[int | None] ``` ## Ecosystem impact I did spot check the changes and most of them seem like known limitations or true positives. Without proper bidirectional inference, we saw a lot of false positives. ## Test Plan New Markdown tests --- .../corpus/88_regression_pr_20962.py | 18 +++ .../resources/mdtest/comprehensions/basic.md | 89 ++++++++++++++ .../mdtest/literal/collections/dictionary.md | 2 +- .../mdtest/literal/collections/list.md | 2 +- .../mdtest/literal/collections/set.md | 2 +- .../pr_20962_comprehension_panics.md | 50 ++++++++ .../ty_python_semantic/src/types/function.rs | 10 +- .../src/types/infer/builder.rs | 115 ++++++++++++++---- .../types/infer/builder/type_expression.rs | 6 +- 9 files changed, 266 insertions(+), 28 deletions(-) create mode 100644 crates/ty_python_semantic/resources/corpus/88_regression_pr_20962.py create mode 100644 crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md diff --git a/crates/ty_python_semantic/resources/corpus/88_regression_pr_20962.py b/crates/ty_python_semantic/resources/corpus/88_regression_pr_20962.py new file mode 100644 index 0000000000..d0b9f706ce --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/88_regression_pr_20962.py @@ -0,0 +1,18 @@ +name_1 +{0: 0 for unique_name_0 in unique_name_1 if name_1} + + +@[name_2 for unique_name_2 in name_2] +def name_2(): + pass + + +def name_2(): + pass + + +match 0: + case name_2(): + pass + case []: + name_1 = 0 diff --git a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md index bdd9ec435c..254ac03d73 100644 --- a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md @@ -103,3 +103,92 @@ async def _(): # revealed: Unknown [reveal_type(x) async for x in range(3)] ``` + +## Comprehension expression types + +The type of the comprehension expression itself should reflect the inferred element type: + +```py +from typing import TypedDict, Literal + +# revealed: list[int | Unknown] +reveal_type([x for x in range(10)]) + +# revealed: set[int | Unknown] +reveal_type({x for x in range(10)}) + +# revealed: dict[int | Unknown, str | Unknown] +reveal_type({x: str(x) for x in range(10)}) + +# revealed: list[tuple[int, Unknown | str] | Unknown] +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)] +reveal_type(squares) # revealed: list[int | None] +``` + +Inference for comprehensions takes the type context into account: + +```py +# 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, str | Unknown] +reveal_type({str(x): x for x in [1, 2, 3]}) # revealed: dict[str | Unknown, Unknown | int] +reveal_type({x for x in [1, 2, 3]}) # revealed: set[Unknown | int] + +# With type context: +xs: list[int] = [x for x in [1, 2, 3]] +reveal_type(xs) # revealed: list[int] + +ys: dict[int, str] = {x: str(x) for x in [1, 2, 3]} +reveal_type(ys) # revealed: dict[int, str] + +zs: set[int] = {x for x in [1, 2, 3]} +``` + +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[list[tuple[int, int] | Unknown] | Unknown] + +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]]] +``` + +The type context is propagated down into the comprehension: + +```py +class Person(TypedDict): + name: str + +persons: list[Person] = [{"name": n} for n in ["Alice", "Bob"]] +reveal_type(persons) # revealed: list[Person] + +# TODO: This should be an error +invalid: 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[str | Unknown] +reveal_type({x for x in (1, 2, 3)}) # revealed: set[int | Unknown] +reveal_type({k: 0 for k in ("a", "b", "c")}) # revealed: dict[str | Unknown, int | Unknown] +``` + +Type context can prevent this promotion from happening: + +```py +list_of_literals: list[Literal["a", "b", "c"]] = [x for x in ("a", "b", "c")] +reveal_type(list_of_literals) # revealed: list[Literal["a", "b", "c"]] + +dict_with_literal_keys: dict[Literal["a", "b", "c"], int] = {k: 0 for k in ("a", "b", "c")} +reveal_type(dict_with_literal_keys) # revealed: dict[Literal["a", "b", "c"], int] + +dict_with_literal_values: dict[str, Literal[1, 2, 3]] = {str(k): k for k in (1, 2, 3)} +reveal_type(dict_with_literal_values) # revealed: dict[str, Literal[1, 2, 3]] + +set_with_literals: set[Literal[1, 2, 3]] = {k for k in (1, 2, 3)} +reveal_type(set_with_literals) # revealed: set[Literal[1, 2, 3]] +``` 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 7e1acf4efb..ad5829da1f 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md @@ -51,6 +51,6 @@ reveal_type({"a": 1, "b": (1, 2), "c": (1, 2, 3)}) ## Dict comprehensions ```py -# revealed: dict[@Todo(dict comprehension key type), @Todo(dict comprehension value type)] +# revealed: dict[int | Unknown, int | Unknown] reveal_type({x: y for x, y in enumerate(range(42))}) ``` 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 15f385fa88..325caba10d 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md @@ -41,5 +41,5 @@ reveal_type([1, (1, 2), (1, 2, 3)]) ## List comprehensions ```py -reveal_type([x for x in range(42)]) # revealed: list[@Todo(list comprehension element type)] +reveal_type([x for x in range(42)]) # revealed: list[int | Unknown] ``` 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 6c6855e40e..d80112ee84 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md @@ -35,5 +35,5 @@ reveal_type({1, (1, 2), (1, 2, 3)}) ## Set comprehensions ```py -reveal_type({x for x in range(42)}) # revealed: set[@Todo(set comprehension element type)] +reveal_type({x for x in range(42)}) # revealed: set[int | Unknown] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md b/crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md new file mode 100644 index 0000000000..b011d95e8c --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md @@ -0,0 +1,50 @@ +# Documentation of two fuzzer panics involving comprehensions + +Type inference for comprehensions was added in . It +added two new fuzzer panics that are documented here for regression testing. + +## Too many cycle iterations in `place_by_id` + + + +```py +name_5(name_3) +[0 for unique_name_0 in unique_name_1 for unique_name_2 in name_3] + +@{name_3 for unique_name_3 in unique_name_4} +class name_4[**name_3](0, name_2=name_5): + pass + +try: + name_0 = name_4 +except* 0: + pass +else: + match unique_name_12: + case 0: + from name_2 import name_3 + case name_0(): + + @name_4 + def name_3(): + pass + +(name_3 := 0) + +@name_3 +async def name_5(): + pass +``` + +## Too many cycle iterations in `infer_definition_types` + + + +```py +for name_1 in { + {{0: name_4 for unique_name_0 in unique_name_1}: 0 for unique_name_2 in unique_name_3 if name_4}: 0 + for unique_name_4 in name_1 + for name_4 in name_1 +}: + pass +``` diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 0f5797ae7a..6244b0a85a 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -534,6 +534,14 @@ pub struct FunctionLiteral<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for FunctionLiteral<'_> {} +fn overloads_and_implementation_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self: FunctionLiteral<'db>, +) -> (Box<[OverloadLiteral<'db>]>, Option>) { + (Box::new([]), None) +} + #[salsa::tracked] impl<'db> FunctionLiteral<'db> { fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { @@ -576,7 +584,7 @@ impl<'db> FunctionLiteral<'db> { self.last_definition(db).spans(db) } - #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size, cycle_initial=overloads_and_implementation_cycle_initial)] fn overloads_and_implementation( self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index f6055c0a0e..ad0a103319 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -5943,9 +5943,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::Expr::Set(set) => self.infer_set_expression(set, tcx), ast::Expr::Dict(dict) => self.infer_dict_expression(dict, tcx), ast::Expr::Generator(generator) => self.infer_generator_expression(generator), - ast::Expr::ListComp(listcomp) => self.infer_list_comprehension_expression(listcomp), - ast::Expr::DictComp(dictcomp) => self.infer_dict_comprehension_expression(dictcomp), - ast::Expr::SetComp(setcomp) => self.infer_set_comprehension_expression(setcomp), + ast::Expr::ListComp(listcomp) => { + self.infer_list_comprehension_expression(listcomp, tcx) + } + ast::Expr::DictComp(dictcomp) => { + self.infer_dict_comprehension_expression(dictcomp, tcx) + } + ast::Expr::SetComp(setcomp) => self.infer_set_comprehension_expression(setcomp, tcx), ast::Expr::Name(name) => self.infer_name_expression(name), ast::Expr::Attribute(attribute) => self.infer_attribute_expression(attribute), ast::Expr::UnaryOp(unary_op) => self.infer_unary_expression(unary_op), @@ -6450,52 +6454,121 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) } - fn infer_list_comprehension_expression(&mut self, listcomp: &ast::ExprListComp) -> Type<'db> { + /// Return a specialization of the collection class (list, dict, set) based on the type context and the inferred + /// element / key-value types from the comprehension expression. + fn infer_comprehension_specialization( + &self, + collection_class: KnownClass, + inferred_element_types: &[Type<'db>], + tcx: TypeContext<'db>, + ) -> Type<'db> { + // Remove any union elements of that are unrelated to the collection type. + let tcx = tcx.map(|annotation| { + annotation.filter_disjoint_elements( + self.db(), + collection_class.to_instance(self.db()), + InferableTypeVars::None, + ) + }); + + if let Some(annotated_element_types) = tcx + .known_specialization(self.db(), collection_class) + .map(|specialization| specialization.types(self.db())) + && annotated_element_types + .iter() + .zip(inferred_element_types.iter()) + .all(|(annotated, inferred)| inferred.is_assignable_to(self.db(), *annotated)) + { + collection_class + .to_specialized_instance(self.db(), annotated_element_types.iter().copied()) + } else { + collection_class.to_specialized_instance( + self.db(), + inferred_element_types.iter().map(|ty| { + UnionType::from_elements( + self.db(), + [ + ty.promote_literals(self.db(), TypeContext::default()), + Type::unknown(), + ], + ) + }), + ) + } + } + + fn infer_list_comprehension_expression( + &mut self, + listcomp: &ast::ExprListComp, + tcx: TypeContext<'db>, + ) -> Type<'db> { let ast::ExprListComp { range: _, node_index: _, - elt: _, + elt, generators, } = listcomp; self.infer_first_comprehension_iter(generators); - KnownClass::List - .to_specialized_instance(self.db(), [todo_type!("list comprehension element type")]) + let scope_id = self + .index + .node_scope(NodeWithScopeRef::ListComprehension(listcomp)); + let scope = scope_id.to_scope_id(self.db(), self.file()); + let inference = infer_scope_types(self.db(), scope); + let element_type = inference.expression_type(elt.as_ref()); + + self.infer_comprehension_specialization(KnownClass::List, &[element_type], tcx) } - fn infer_dict_comprehension_expression(&mut self, dictcomp: &ast::ExprDictComp) -> Type<'db> { + fn infer_dict_comprehension_expression( + &mut self, + dictcomp: &ast::ExprDictComp, + tcx: TypeContext<'db>, + ) -> Type<'db> { let ast::ExprDictComp { range: _, node_index: _, - key: _, - value: _, + key, + value, generators, } = dictcomp; self.infer_first_comprehension_iter(generators); - KnownClass::Dict.to_specialized_instance( - self.db(), - [ - todo_type!("dict comprehension key type"), - todo_type!("dict comprehension value type"), - ], - ) + let scope_id = self + .index + .node_scope(NodeWithScopeRef::DictComprehension(dictcomp)); + let scope = scope_id.to_scope_id(self.db(), self.file()); + let inference = infer_scope_types(self.db(), scope); + let key_type = inference.expression_type(key.as_ref()); + let value_type = inference.expression_type(value.as_ref()); + + self.infer_comprehension_specialization(KnownClass::Dict, &[key_type, value_type], tcx) } - fn infer_set_comprehension_expression(&mut self, setcomp: &ast::ExprSetComp) -> Type<'db> { + fn infer_set_comprehension_expression( + &mut self, + setcomp: &ast::ExprSetComp, + tcx: TypeContext<'db>, + ) -> Type<'db> { let ast::ExprSetComp { range: _, node_index: _, - elt: _, + elt, generators, } = setcomp; self.infer_first_comprehension_iter(generators); - KnownClass::Set - .to_specialized_instance(self.db(), [todo_type!("set comprehension element type")]) + let scope_id = self + .index + .node_scope(NodeWithScopeRef::SetComprehension(setcomp)); + let scope = scope_id.to_scope_id(self.db(), self.file()); + let inference = infer_scope_types(self.db(), scope); + let element_type = inference.expression_type(elt.as_ref()); + + self.infer_comprehension_specialization(KnownClass::Set, &[element_type], tcx) } fn infer_generator_expression_scope(&mut self, generator: &ast::ExprGenerator) { 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 3c7bdb5464..0d72548e49 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 @@ -346,7 +346,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ast::Expr::DictComp(dictcomp) => { - self.infer_dict_comprehension_expression(dictcomp); + self.infer_dict_comprehension_expression(dictcomp, TypeContext::default()); self.report_invalid_type_expression( expression, format_args!("Dict comprehensions are not allowed in type expressions"), @@ -355,7 +355,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ast::Expr::ListComp(listcomp) => { - self.infer_list_comprehension_expression(listcomp); + self.infer_list_comprehension_expression(listcomp, TypeContext::default()); self.report_invalid_type_expression( expression, format_args!("List comprehensions are not allowed in type expressions"), @@ -364,7 +364,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ast::Expr::SetComp(setcomp) => { - self.infer_set_comprehension_expression(setcomp); + self.infer_set_comprehension_expression(setcomp, TypeContext::default()); self.report_invalid_type_expression( expression, format_args!("Set comprehensions are not allowed in type expressions"), From 6c3d6124c88fe8d7dcde9e8aa16f9e4193ed780b Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sun, 2 Nov 2025 15:58:36 +0100 Subject: [PATCH 116/188] [ty] Fix range filtering for tokens starting at the end of the requested range (#21193) Co-authored-by: David Peter --- crates/ty_ide/src/semantic_tokens.rs | 44 +++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index 12e5e6581b..ca736acd4c 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -11,7 +11,7 @@ use ruff_python_ast::{ AnyNodeRef, BytesLiteral, Expr, FString, InterpolatedStringElement, Stmt, StringLiteral, TypeParam, }; -use ruff_text_size::{Ranged, TextLen, TextRange}; +use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use std::ops::Deref; use ty_python_semantic::{ HasType, SemanticModel, semantic_index::definition::DefinitionKind, types::Type, @@ -226,7 +226,12 @@ impl<'db> SemanticTokenVisitor<'db> { let range = ranged.range(); // Only emit tokens that intersect with the range filter, if one is specified if let Some(range_filter) = self.range_filter { - if range.intersect(range_filter).is_none() { + // Only include ranges that have a non-empty overlap. Adjacent ranges + // should be excluded. + if range + .intersect(range_filter) + .is_none_or(TextRange::is_empty) + { return; } } @@ -446,11 +451,11 @@ impl<'db> SemanticTokenVisitor<'db> { let name_start = name.start(); // Split the dotted name and calculate positions for each part - let mut current_offset = ruff_text_size::TextSize::default(); + let mut current_offset = TextSize::default(); for part in name_str.split('.') { if !part.is_empty() { self.add_token( - ruff_text_size::TextRange::at(name_start + current_offset, part.text_len()), + TextRange::at(name_start + current_offset, part.text_len()), token_type, SemanticTokenModifier::empty(), ); @@ -926,6 +931,7 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> { mod tests { use super::*; use crate::tests::cursor_test; + use insta::assert_snapshot; /// Helper function to get semantic tokens for full file (for testing) @@ -1231,10 +1237,7 @@ def function2(): // Get the range that covers only the second function // Hardcoded offsets: function2 starts at position 42, source ends at position 108 - let range = ruff_text_size::TextRange::new( - ruff_text_size::TextSize::from(42u32), - ruff_text_size::TextSize::from(108u32), - ); + let range = TextRange::new(TextSize::from(42u32), TextSize::from(108u32)); let range_tokens = semantic_tokens(&test.db, test.cursor.file, Some(range)); @@ -1278,6 +1281,31 @@ def function2(): } } + /// When a token starts right at where the requested range ends, + /// don't include it in the semantic tokens. + #[test] + fn test_semantic_tokens_range_excludes_boundary_tokens() { + let test = cursor_test( + " +x = 1 +y = 2 +z = 3 +", + ); + + // Range [6..13) starts where "1" ends and ends where "z" starts. + // Expected: only "y" @ 7..8 and "2" @ 11..12 (non-empty overlap with target range). + // Not included: "1" @ 5..6 and "z" @ 13..14 (adjacent, but not overlapping at offsets 6 and 13). + let range = TextRange::new(TextSize::from(6), TextSize::from(13)); + + let range_tokens = semantic_tokens(&test.db, test.cursor.file, Some(range)); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &range_tokens), @r#" + "y" @ 7..8: Variable + "2" @ 11..12: Number + "#); + } + #[test] fn test_dotted_module_names() { let test = cursor_test( From 566d1d649768234ad281a4334da81925a3c7dde5 Mon Sep 17 00:00:00 2001 From: David Peter Date: Sun, 2 Nov 2025 17:33:31 +0100 Subject: [PATCH 117/188] [ty] Update to the latest version of the conformance suite (#21205) ## Summary There have been some larger-scale updates to the conformance suite since we introduced our CI job, so it seems sensible to bump the version of the conformance suite to the latest state. ## Test plan This is a bit awkward to test. Here is the diff of running ty on the conformance suite before and after this bump. I filtered out line/column information (`sed -re 's/\.py:[0-9]+:[0-9]+:/.py/'`) to avoid spurious changes from content that has simply been moved around. ```diff 1,2c1 < fatal[panic] Panicked at /home/shark/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/cdd0b85/src/function/execute.rs:419:17 when checking `/home/shark/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(1a99c)): execute: too many cycle iterations` < src/type_checker.py error[unresolved-import] Cannot resolve imported module `tqdm` --- > fatal[panic] Panicked at /home/shark/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/cdd0b85/src/function/execute.rs:419:17 when checking `/home/shark/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(6e4c)): execute: too many cycle iterations` 205,206d203 < tests/constructors_call_metaclass.py error[type-assertion-failure] Argument does not have asserted type `Never` < tests/constructors_call_metaclass.py error[missing-argument] No argument provided for required parameter `x` of function `__new__` 268a266,273 > tests/dataclasses_match_args.py error[type-assertion-failure] Argument does not have asserted type `tuple[Literal["x"]]` > tests/dataclasses_match_args.py error[unresolved-attribute] Class `DC1` has no attribute `__match_args__` > tests/dataclasses_match_args.py error[type-assertion-failure] Argument does not have asserted type `tuple[Literal["x"]]` > tests/dataclasses_match_args.py error[unresolved-attribute] Class `DC2` has no attribute `__match_args__` > tests/dataclasses_match_args.py error[type-assertion-failure] Argument does not have asserted type `tuple[Literal["x"]]` > tests/dataclasses_match_args.py error[unresolved-attribute] Class `DC3` has no attribute `__match_args__` > tests/dataclasses_match_args.py error[unresolved-attribute] Class `DC4` has no attribute `__match_args__` > tests/dataclasses_match_args.py error[type-assertion-failure] Argument does not have asserted type `tuple[()]` 339a345 > tests/directives_assert_type.py error[type-assertion-failure] Argument does not have asserted type `Any` 424a431 > tests/generics_defaults.py error[type-assertion-failure] Argument does not have asserted type `Any` 520a528,529 > tests/generics_syntax_infer_variance.py error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `T@ShouldBeCovariant2 | Sequence[T@ShouldBeCovariant2]` > tests/generics_syntax_infer_variance.py error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int` 711a721 > tests/namedtuples_define_class.py error[too-many-positional-arguments] Too many positional arguments: expected 3, got 4 795d804 < tests/protocols_explicit.py error[invalid-attribute-access] Cannot assign to ClassVar `cm1` from an instance of type `Self@__init__` 822,823d830 < tests/qualifiers_annotated.py error[invalid-syntax] named expression cannot be used within a type annotation < tests/qualifiers_annotated.py error[invalid-syntax] await expression cannot be used within a type annotation 922a930,953 > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `Movie`: Unknown key "novel_adaptation" > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `Movie`: Unknown key "year" > tests/typeddicts_extra_items.py error[type-assertion-failure] Argument does not have asserted type `bool` > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `Movie`: Unknown key "novel_adaptation" > tests/typeddicts_extra_items.py error[invalid-argument-type] Invalid argument to key "year" with declared type `int` on TypedDict `InheritedMovie`: value of type `None` > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `InheritedMovie`: Unknown key "other_extra_key" > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `MovieEI`: Unknown key "year" > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `MovieExtraInt`: Unknown key "year" > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `MovieExtraStr`: Unknown key "description" > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `MovieExtraInt`: Unknown key "year" > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `NonClosedMovie`: Unknown key "year" > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `ExtraMovie`: Unknown key "year" > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `ExtraMovie`: Unknown key "language" > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `ClosedMovie`: Unknown key "year" > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `MovieExtraStr`: Unknown key "summary" > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `MovieExtraInt`: Unknown key "year" > tests/typeddicts_extra_items.py error[invalid-assignment] Object of type `dict[Unknown | str, Unknown | str | int]` is not assignable to `Mapping[str, int]` > tests/typeddicts_extra_items.py error[type-assertion-failure] Argument does not have asserted type `list[tuple[str, int | str]]` > tests/typeddicts_extra_items.py error[type-assertion-failure] Argument does not have asserted type `list[int | str]` > tests/typeddicts_extra_items.py error[unresolved-attribute] Object of type `IntDict` has no attribute `clear` > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `IntDictWithNum`: Unknown key "bar" - did you mean "num"? > tests/typeddicts_extra_items.py error[type-assertion-failure] Argument does not have asserted type `tuple[str, int]` > tests/typeddicts_extra_items.py error[invalid-key] Cannot access `IntDictWithNum` with a key of type `str`. Only string literals are allowed as keys on TypedDicts. > tests/typeddicts_extra_items.py error[invalid-key] Invalid key for TypedDict `IntDictWithNum` of type `str` 950c981 < Found 949 diagnostics --- > Found 980 diagnostics ``` --- .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 ed23e6c084..aa99f6dd72 100644 --- a/.github/workflows/typing_conformance.yaml +++ b/.github/workflows/typing_conformance.yaml @@ -24,7 +24,7 @@ env: CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 RUST_BACKTRACE: 1 - CONFORMANCE_SUITE_COMMIT: d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc + CONFORMANCE_SUITE_COMMIT: 9f6d8ced7cd1c8d92687a4e9c96d7716452e471e jobs: typing_conformance: From c32234cf0d8cb10fbab23060c9c65a20417f6a85 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sun, 2 Nov 2025 12:39:55 -0500 Subject: [PATCH 118/188] [ty] support subscripting typing.Literal with a type alias (#21207) Fixes https://github.com/astral-sh/ty/issues/1368 ## Summary Add support for patterns like this, where a type alias to a literal type (or union of literal types) is used to subscript `typing.Literal`: ```py type MyAlias = Literal[1] def _(x: Literal[MyAlias]): ... ``` This shows up in the ecosystem report for PEP 613 type alias support. One interesting case is an alias to `bool` or an enum type. `bool` is an equivalent type to `Literal[True, False]`, which is a union of literal types. Similarly an enum type `E` is also equivalent to a union of its member literal types. Since (for explicit type aliases) we infer the RHS directly as a type expression, this makes it difficult for us to distinguish between `bool` and `Literal[True, False]`, so we allow either one to (or an alias to either one) to appear inside `Literal`, where other type checkers allow only the latter. I think for implicit type aliases it may be simpler to support only types derived from actually subscripting `typing.Literal`, though, so I didn't make a TODO-comment commitment here. ## Test Plan Added mdtests, including TODO-filled tests for PEP 613 and implicit type aliases. ### Conformance suite All changes here are positive -- we now emit errors on lines that should be errors. This is a side effect of the new implementation, not the primary purpose of this PR, but it's still a positive change. ### Ecosystem Eliminates one ecosystem false positive, where a PEP 695 type alias for a union of literal types is used to subscript `typing.Literal`. --- .../resources/mdtest/annotations/literal.md | 204 ++++++++++++++++++ .../mdtest/assignment/annotations.md | 2 +- crates/ty_python_semantic/src/types.rs | 17 ++ .../types/infer/builder/type_expression.rs | 52 +++-- 4 files changed, 251 insertions(+), 24 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/literal.md b/crates/ty_python_semantic/resources/mdtest/annotations/literal.md index 3bd9e54c85..897be97e77 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/literal.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/literal.md @@ -39,6 +39,8 @@ def f(): reveal_type(a7) # revealed: None reveal_type(a8) # revealed: Literal[1] reveal_type(b1) # revealed: Literal[Color.RED] + # TODO should be `Literal[MissingT.MISSING]` + reveal_type(b2) # revealed: @Todo(functional `Enum` syntax) # error: [invalid-type-form] invalid1: Literal[3 + 4] @@ -66,6 +68,208 @@ a_list: list[int] = [1, 2, 3] invalid6: Literal[a_list[0]] ``` +## Parameterizing with a type alias + +`typing.Literal` can also be parameterized with a type alias for any literal type or union of +literal types. + +### PEP 695 type alias + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Literal +from enum import Enum + +import mod + +class E(Enum): + A = 1 + B = 2 + +type SingleInt = Literal[1] +type SingleStr = Literal["foo"] +type SingleBytes = Literal[b"bar"] +type SingleBool = Literal[True] +type SingleNone = Literal[None] +type SingleEnum = Literal[E.A] +type UnionLiterals = Literal[1, "foo", b"bar", True, None, E.A] +# We support this because it is an equivalent type to the following union of literals, but maybe +# we should not, because it doesn't use `Literal` form? Other type checkers do not. +type AnEnum1 = E +type AnEnum2 = Literal[E.A, E.B] +# Similarly, we support this because it is equivalent to `Literal[True, False]`. +type Bool1 = bool +type Bool2 = Literal[True, False] + +def _( + single_int: Literal[SingleInt], + single_str: Literal[SingleStr], + single_bytes: Literal[SingleBytes], + single_bool: Literal[SingleBool], + single_none: Literal[SingleNone], + single_enum: Literal[SingleEnum], + union_literals: Literal[UnionLiterals], + an_enum1: Literal[AnEnum1], + an_enum2: Literal[AnEnum2], + bool1: Literal[Bool1], + bool2: Literal[Bool2], + multiple: Literal[SingleInt, SingleStr, SingleEnum], + single_int_other_module: Literal[mod.SingleInt], +): + reveal_type(single_int) # revealed: Literal[1] + reveal_type(single_str) # revealed: Literal["foo"] + reveal_type(single_bytes) # revealed: Literal[b"bar"] + reveal_type(single_bool) # revealed: Literal[True] + reveal_type(single_none) # revealed: None + reveal_type(single_enum) # revealed: Literal[E.A] + reveal_type(union_literals) # revealed: Literal[1, "foo", b"bar", True, E.A] | None + reveal_type(an_enum1) # revealed: E + reveal_type(an_enum2) # revealed: E + reveal_type(bool1) # revealed: bool + reveal_type(bool2) # revealed: bool + reveal_type(multiple) # revealed: Literal[1, "foo", E.A] + reveal_type(single_int_other_module) # revealed: Literal[2] +``` + +`mod.py`: + +```py +from typing import Literal + +type SingleInt = Literal[2] +``` + +### PEP 613 type alias + +```py +from typing import Literal, TypeAlias +from enum import Enum + +class E(Enum): + A = 1 + B = 2 + +SingleInt: TypeAlias = Literal[1] +SingleStr: TypeAlias = Literal["foo"] +SingleBytes: TypeAlias = Literal[b"bar"] +SingleBool: TypeAlias = Literal[True] +SingleNone: TypeAlias = Literal[None] +SingleEnum: TypeAlias = Literal[E.A] +UnionLiterals: TypeAlias = Literal[1, "foo", b"bar", True, None, E.A] +AnEnum1: TypeAlias = E +AnEnum2: TypeAlias = Literal[E.A, E.B] +Bool1: TypeAlias = bool +Bool2: TypeAlias = Literal[True, False] + +def _( + single_int: Literal[SingleInt], + single_str: Literal[SingleStr], + single_bytes: Literal[SingleBytes], + single_bool: Literal[SingleBool], + single_none: Literal[SingleNone], + single_enum: Literal[SingleEnum], + union_literals: Literal[UnionLiterals], + # Could also not error + an_enum1: Literal[AnEnum1], # error: [invalid-type-form] + an_enum2: Literal[AnEnum2], + # Could also not error + bool1: Literal[Bool1], # error: [invalid-type-form] + bool2: Literal[Bool2], + multiple: Literal[SingleInt, SingleStr, SingleEnum], +): + # TODO should be `Literal[1]` + reveal_type(single_int) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `Literal["foo"]` + reveal_type(single_str) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `Literal[b"bar"]` + reveal_type(single_bytes) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `Literal[True]` + reveal_type(single_bool) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `None` + reveal_type(single_none) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `Literal[E.A]` + reveal_type(single_enum) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `Literal[1, "foo", b"bar", True, E.A] | None` + reveal_type(union_literals) # revealed: @Todo(Inference of subscript on special form) + # Could also be `E` + reveal_type(an_enum1) # revealed: Unknown + # TODO should be `E` + reveal_type(an_enum2) # revealed: @Todo(Inference of subscript on special form) + # Could also be `bool` + reveal_type(bool1) # revealed: Unknown + # TODO should be `bool` + reveal_type(bool2) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `Literal[1, "foo", E.A]` + reveal_type(multiple) # revealed: @Todo(Inference of subscript on special form) +``` + +### Implicit type alias + +```py +from typing import Literal +from enum import Enum + +class E(Enum): + A = 1 + B = 2 + +SingleInt = Literal[1] +SingleStr = Literal["foo"] +SingleBytes = Literal[b"bar"] +SingleBool = Literal[True] +SingleNone = Literal[None] +SingleEnum = Literal[E.A] +UnionLiterals = Literal[1, "foo", b"bar", True, None, E.A] +# For implicit type aliases, we may not want to support this. It's simpler not to, and no other +# type checker does. +AnEnum1 = E +AnEnum2 = Literal[E.A, E.B] +# For implicit type aliases, we may not want to support this. +Bool1 = bool +Bool2 = Literal[True, False] + +def _( + single_int: Literal[SingleInt], + single_str: Literal[SingleStr], + single_bytes: Literal[SingleBytes], + single_bool: Literal[SingleBool], + single_none: Literal[SingleNone], + single_enum: Literal[SingleEnum], + union_literals: Literal[UnionLiterals], + an_enum1: Literal[AnEnum1], # error: [invalid-type-form] + an_enum2: Literal[AnEnum2], + bool1: Literal[Bool1], # error: [invalid-type-form] + bool2: Literal[Bool2], + multiple: Literal[SingleInt, SingleStr, SingleEnum], +): + # TODO should be `Literal[1]` + reveal_type(single_int) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `Literal["foo"]` + reveal_type(single_str) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `Literal[b"bar"]` + reveal_type(single_bytes) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `Literal[True]` + reveal_type(single_bool) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `None` + reveal_type(single_none) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `Literal[E.A]` + reveal_type(single_enum) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `Literal[1, "foo", b"bar", True, E.A] | None` + reveal_type(union_literals) # revealed: @Todo(Inference of subscript on special form) + reveal_type(an_enum1) # revealed: Unknown + # TODO should be `E` + reveal_type(an_enum2) # revealed: @Todo(Inference of subscript on special form) + reveal_type(bool1) # revealed: Unknown + # TODO should be `bool` + reveal_type(bool2) # revealed: @Todo(Inference of subscript on special form) + # TODO should be `Literal[1, "foo", E.A]` + reveal_type(multiple) # revealed: @Todo(Inference of subscript on special form) +``` + ## Shortening unions of literals When a Literal is parameterized with more than one value, it’s treated as exactly to equivalent to diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md index adf0de358d..ceb588d7fe 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md @@ -259,7 +259,7 @@ class Color(Enum): RED = "red" f: dict[list[Literal[1]], list[Literal[Color.RED]]] = {[1]: [Color.RED, Color.RED]} -reveal_type(f) # revealed: dict[list[Literal[1]], list[Literal[Color.RED]]] +reveal_type(f) # revealed: dict[list[Literal[1]], list[Color]] class X[T]: def __init__(self, value: T): ... diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index be2fb264d8..b20f332999 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1153,6 +1153,23 @@ impl<'db> Type<'db> { matches!(self, Type::FunctionLiteral(..)) } + /// 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 { + match self { + Type::Union(union) => union + .elements(db) + .iter() + .all(|ty| ty.is_literal_or_union_of_literals(db)), + Type::StringLiteral(_) + | Type::BytesLiteral(_) + | Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::EnumLiteral(_) => true, + Type::NominalInstance(_) => self.is_none(db) || self.is_bool(db) || self.is_enum(db), + _ => false, + } + } + pub(crate) fn is_union_of_single_valued(&self, db: &'db dyn Db) -> bool { self.as_union().is_some_and(|union| { union.elements(db).iter().all(|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 0d72548e49..50d22fac10 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 @@ -6,7 +6,6 @@ use crate::types::diagnostic::{ self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable, }; -use crate::types::enums::is_enum_class; use crate::types::signatures::Signature; use crate::types::string_annotation::parse_string_annotation; use crate::types::tuple::{TupleSpecBuilder, TupleType}; @@ -1369,7 +1368,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { parameters: &'param ast::Expr, ) -> Result, Vec<&'param ast::Expr>> { Ok(match parameters { - // TODO handle type aliases ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { let value_ty = self.infer_expression(value, TypeContext::default()); if matches!(value_ty, Type::SpecialForm(SpecialFormType::Literal)) { @@ -1421,27 +1419,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { literal @ ast::Expr::NumberLiteral(number) if number.value.is_int() => { self.infer_expression(literal, TypeContext::default()) } - // For enum values - ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => { - let value_ty = self.infer_expression(value, TypeContext::default()); - - if is_enum_class(self.db(), value_ty) { - let ty = value_ty - .member(self.db(), &attr.id) - .place - .ignore_possibly_undefined() - .unwrap_or(Type::unknown()); - self.store_expression_type(parameters, ty); - ty - } else { - self.store_expression_type(parameters, Type::unknown()); - if value_ty.is_todo() { - value_ty - } else { - return Err(vec![parameters]); - } - } - } // for negative and positive numbers ast::Expr::UnaryOp(u) if matches!(u.op, ast::UnaryOp::USub | ast::UnaryOp::UAdd) @@ -1451,6 +1428,35 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.store_expression_type(parameters, ty); ty } + // enum members and aliases to literal types + ast::Expr::Name(_) | ast::Expr::Attribute(_) => { + let subscript_ty = self.infer_expression(parameters, TypeContext::default()); + // TODO handle implicit type aliases also + match subscript_ty { + // type aliases to literal types + Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => { + let value_ty = type_alias.value_type(self.db()); + if value_ty.is_literal_or_union_of_literals(self.db()) { + return Ok(value_ty); + } + } + // `Literal[SomeEnum.Member]` + Type::EnumLiteral(_) => { + return Ok(subscript_ty); + } + // `Literal[SingletonEnum.Member]`, where `SingletonEnum.Member` simplifies to + // just `SingletonEnum`. + Type::NominalInstance(_) if subscript_ty.is_enum(self.db()) => { + return Ok(subscript_ty); + } + // suppress false positives for e.g. members of functional-syntax enums + Type::Dynamic(DynamicType::Todo(_)) => { + return Ok(subscript_ty); + } + _ => {} + } + return Err(vec![parameters]); + } _ => { self.infer_expression(parameters, TypeContext::default()); return Err(vec![parameters]); From 0454a72674e3f55fe081b94e7e60e72090b62489 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sun, 2 Nov 2025 18:21:54 -0500 Subject: [PATCH 119/188] [ty] don't union in default type for annotated parameters (#21208) --- .../resources/mdtest/function/parameters.md | 15 +++---- .../resources/mdtest/ty_extensions.md | 2 +- .../src/types/infer/builder.rs | 44 ++++++------------- 3 files changed, 21 insertions(+), 40 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/function/parameters.md b/crates/ty_python_semantic/resources/mdtest/function/parameters.md index eb7316fe91..c629f26565 100644 --- a/crates/ty_python_semantic/resources/mdtest/function/parameters.md +++ b/crates/ty_python_semantic/resources/mdtest/function/parameters.md @@ -1,12 +1,9 @@ # Function parameter types Within a function scope, the declared type of each parameter is its annotated type (or Unknown if -not annotated). The initial inferred type is the union of the declared type with the type of the -default value expression (if any). If both are fully static types, this union should simplify to the -annotated type (since the default value type must be assignable to the annotated type, and for fully -static types this means subtype-of, which simplifies in unions). But if the annotated type is -Unknown or another non-fully-static type, the default value type may still be relevant as lower -bound. +not annotated). The initial inferred type is the annotated type of the parameter, if any. If there +is no annotation, it is the union of `Unknown` with the type of the default value expression (if +any). The variadic parameter is a variadic tuple of its annotated type; the variadic-keywords parameter is a dictionary from strings to its annotated type. @@ -41,13 +38,13 @@ def g(*args, **kwargs): ## Annotation is present but not a fully static type -The default value type should be a lower bound on the inferred type. +If there is an annotation, we respect it fully and don't union in the default value type. ```py from typing import Any def f(x: Any = 1): - reveal_type(x) # revealed: Any | Literal[1] + reveal_type(x) # revealed: Any ``` ## Default value type must be assignable to annotated type @@ -64,7 +61,7 @@ def f(x: int = "foo"): from typing import Any def g(x: Any = "foo"): - reveal_type(x) # revealed: Any | Literal["foo"] + reveal_type(x) # revealed: Any ``` ## Stub functions diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md index ba88851015..22d92b54af 100644 --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md @@ -99,7 +99,7 @@ static_assert(is_assignable_to(int, Unknown)) def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None: reveal_type(x) # revealed: Unknown reveal_type(y) # revealed: tuple[str, Unknown] - reveal_type(z) # revealed: Unknown | Literal[1] + reveal_type(z) # revealed: Unknown ``` `Unknown` can be subclassed, just like `Any`: diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index ad0a103319..b74ff75404 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2423,15 +2423,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// /// The declared type is the annotated type, if any, or `Unknown`. /// - /// The inferred type is the annotated type, unioned with the type of the default value, if - /// any. If both types are fully static, this union is a no-op (it should simplify to just the - /// annotated type.) But in a case like `f(x=None)` with no annotated type, we want to infer - /// the type `Unknown | None` for `x`, not just `Unknown`, so that we can error on usage of `x` - /// that would not be valid for `None`. - /// - /// If the default-value type is not assignable to the declared (annotated) type, we ignore the - /// default-value type and just infer the annotated type; this is the same way we handle - /// assignments, and allows an explicit annotation to override a bad inference. + /// 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 @@ -2460,23 +2453,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(|default| self.file_expression_type(default)); if let Some(annotation) = parameter.annotation.as_ref() { let declared_ty = self.file_expression_type(annotation); - let declared_and_inferred_ty = if let Some(default_ty) = default_ty { - if default_ty.is_assignable_to(self.db(), declared_ty) { - DeclaredAndInferredType::MightBeDifferent { - declared_ty: TypeAndQualifiers::declared(declared_ty), - inferred_ty: UnionType::from_elements(self.db(), [declared_ty, default_ty]), - } - } else if (self.in_stub() - || self.in_function_overload_or_abstractmethod() - || 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(default_ty) = default_ty { + if !default_ty.is_assignable_to(self.db(), declared_ty) + && !((self.in_stub() + || self.in_function_overload_or_abstractmethod() + || 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())) { - DeclaredAndInferredType::are_the_same_type(declared_ty) - } else { if let Some(builder) = self .context .report_lint(&INVALID_PARAMETER_DEFAULT, parameter_with_default) @@ -2488,15 +2475,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { declared_ty.display(self.db()) )); } - DeclaredAndInferredType::are_the_same_type(declared_ty) } - } else { - DeclaredAndInferredType::are_the_same_type(declared_ty) - }; + } self.add_declaration_with_binding( parameter.into(), definition, - &declared_and_inferred_ty, + &DeclaredAndInferredType::are_the_same_type(declared_ty), ); } else { let ty = if let Some(default_ty) = default_ty { From 02f2dba28e312d0297e8658e97c53a64c403e497 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 01:45:20 +0000 Subject: [PATCH 120/188] Update Rust crate indoc to v2.0.7 (#21219) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af119dab7e..410dba03fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1574,9 +1574,12 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] [[package]] name = "inotify" From f14631e1ccf3862d5360c376382b04f2cedbed63 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 01:46:05 +0000 Subject: [PATCH 121/188] Update Rust crate indicatif to v0.18.2 (#21218) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 70 ++++++++++++++++++++---------------------------------- 1 file changed, 26 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 410dba03fe..0c0402be2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,7 +45,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" dependencies = [ "anstyle", - "unicode-width 0.2.1", + "unicode-width", ] [[package]] @@ -106,7 +106,7 @@ dependencies = [ "anstyle-lossy", "anstyle-parse", "html-escape", - "unicode-width 0.2.1", + "unicode-width", ] [[package]] @@ -687,7 +687,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.1", + "unicode-width", "windows-sys 0.61.0", ] @@ -1007,7 +1007,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -1093,7 +1093,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]] @@ -1250,7 +1250,7 @@ version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ - "unicode-width 0.2.1", + "unicode-width", ] [[package]] @@ -1560,13 +1560,13 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" +checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65" dependencies = [ "console 0.16.1", "portable-atomic", - "unicode-width 0.2.1", + "unicode-width", "unit-prefix", "vt100", "web-time", @@ -2324,7 +2324,7 @@ checksum = "31095ca1f396e3de32745f42b20deef7bc09077f918b085307e8eab6ddd8fb9c" dependencies = [ "once_cell", "serde", - "unicode-width 0.2.1", + "unicode-width", "unscanny", "version-ranges", ] @@ -2345,7 +2345,7 @@ dependencies = [ "serde", "smallvec", "thiserror 1.0.69", - "unicode-width 0.2.1", + "unicode-width", "url", "urlencoding", "version-ranges", @@ -2899,7 +2899,7 @@ dependencies = [ "snapbox", "toml", "tryfn", - "unicode-width 0.2.1", + "unicode-width", ] [[package]] @@ -3050,7 +3050,7 @@ dependencies = [ "serde", "static_assertions", "tracing", - "unicode-width 0.2.1", + "unicode-width", ] [[package]] @@ -3140,7 +3140,7 @@ dependencies = [ "toml", "typed-arena", "unicode-normalization", - "unicode-width 0.2.1", + "unicode-width", "unicode_names2", "url", ] @@ -3538,7 +3538,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -3934,7 +3934,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -4657,12 +4657,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.1" @@ -4798,25 +4792,13 @@ checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" [[package]] name = "vt100" -version = "0.15.2" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de" +checksum = "054ff75fb8fa83e609e685106df4faeffdf3a735d3c74ebce97ec557d5d36fd9" dependencies = [ "itoa", - "log", - "unicode-width 0.1.14", - "vte 0.11.1", -] - -[[package]] -name = "vte" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" -dependencies = [ - "arrayvec", - "utf8parse", - "vte_generate_state_changes", + "unicode-width", + "vte 0.15.0", ] [[package]] @@ -4829,13 +4811,13 @@ dependencies = [ ] [[package]] -name = "vte_generate_state_changes" -version = "0.1.2" +name = "vte" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" +checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" dependencies = [ - "proc-macro2", - "quote", + "arrayvec", + "memchr", ] [[package]] @@ -5014,7 +4996,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 41fe4d7f8c2dc519b7b05d433b6eeea4e21a8ba8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 01:47:12 +0000 Subject: [PATCH 122/188] Update Rust crate ignore to v0.4.25 (#21217) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c0402be2b..483b873fe5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1286,9 +1286,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab69130804d941f8075cfd713bf8848a2c3b3f201a9457a11e6f87e1ab62305" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", @@ -1513,9 +1513,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.24" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81776e6f9464432afcc28d03e52eb101c93b6f0566f52aef2427663e700f0403" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ "crossbeam-deque", "globset", From b754abff1b4a2deb3bb899e60782a138925d8076 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:47:30 +0100 Subject: [PATCH 123/188] Update Rust crate aho-corasick to v1.1.4 (#21213) 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 | |---|---|---|---| | [aho-corasick](https://redirect.github.com/BurntSushi/aho-corasick) | workspace.dependencies | patch | `1.1.3` -> `1.1.4` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
    BurntSushi/aho-corasick (aho-corasick) ### [`v1.1.4`](https://redirect.github.com/BurntSushi/aho-corasick/compare/1.1.3...1.1.4) [Compare Source](https://redirect.github.com/BurntSushi/aho-corasick/compare/1.1.3...1.1.4)
    --- ### 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 483b873fe5..ef4d55d8b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] From 222c6fd49652b2f221f6a4a0ebc213b8c1689e44 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 01:48:02 +0000 Subject: [PATCH 124/188] Update Rust crate ctrlc to v3.5.1 (#21215) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef4d55d8b7..c624f57e47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -287,6 +287,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "boxcar" version = "0.2.14" @@ -877,11 +886,11 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.5.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3" +checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" dependencies = [ - "dispatch", + "dispatch2", "nix 0.30.1", "windows-sys 0.61.0", ] @@ -1011,10 +1020,16 @@ dependencies = [ ] [[package]] -name = "dispatch" -version = "0.2.0" +name = "dispatch2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.4", + "block2", + "libc", + "objc2", +] [[package]] name = "displaydoc" @@ -2176,6 +2191,21 @@ dependencies = [ "libc", ] +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "once_cell" version = "1.21.3" From 1c4a9d6a069da0ac362a456679d175bfb224a460 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:48:22 +0100 Subject: [PATCH 125/188] Update Rust crate globset to v0.4.18 (#21216) 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 | |---|---|---|---| | [globset](https://redirect.github.com/BurntSushi/ripgrep/tree/master/crates/globset) ([source](https://redirect.github.com/BurntSushi/ripgrep/tree/HEAD/crates/globset)) | workspace.dependencies | patch | `0.4.17` -> `0.4.18` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
    BurntSushi/ripgrep (globset) ### [`v0.4.18`](https://redirect.github.com/BurntSushi/ripgrep/compare/globset-0.4.17...globset-0.4.18) [Compare Source](https://redirect.github.com/BurntSushi/ripgrep/compare/globset-0.4.17...globset-0.4.18)
    --- ### 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> From 50b75cfcc68f28c470b085b62b11be7069fa8bb8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 01:49:44 +0000 Subject: [PATCH 126/188] Update cargo-bins/cargo-binstall action to v1.15.10 (#21212) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .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 5661ff48b7..0fc4eeb444 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -438,7 +438,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo-binstall" - uses: cargo-bins/cargo-binstall@afcf9780305558bcc9e4bc94b7589ab2bb8b6106 # v1.15.9 + uses: cargo-bins/cargo-binstall@b3f755e95653da9a2d25b99154edfdbd5b356d0a # v1.15.10 - 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 @@ -698,7 +698,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - - uses: cargo-bins/cargo-binstall@afcf9780305558bcc9e4bc94b7589ab2bb8b6106 # v1.15.9 + - uses: cargo-bins/cargo-binstall@b3f755e95653da9a2d25b99154edfdbd5b356d0a # v1.15.10 - run: cargo binstall --no-confirm cargo-shear - run: cargo shear From 80eeb1d64fc4cf7e09237ce8617df7850f8af92a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 01:50:12 +0000 Subject: [PATCH 127/188] Update Rust crate clap to v4.5.51 (#21214) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c624f57e47..a51d68804d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -442,9 +442,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.50" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -452,9 +452,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.50" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", From 73b9b8eb6b7c2f791b326b3ab8a1154f753ec224 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:15:56 +0000 Subject: [PATCH 128/188] Update Rust crate proc-macro2 to v1.0.103 (#21221) 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 a51d68804d..86f223bc84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2583,9 +2583,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] From c0bd092fa9a856c272c379f1b3783f5e7502b7fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:20:19 +0000 Subject: [PATCH 129/188] Update Rust crate snapbox to v0.6.23 (#21223) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86f223bc84..11e23ab78a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -642,7 +642,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]] @@ -651,7 +651,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1016,7 +1016,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -1698,7 +1698,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1762,7 +1762,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3841,9 +3841,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "snapbox" -version = "0.6.22" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "805d09a74586d9b17061e5be6ee5f8cc37e5982c349948114ffc5f68093fe5ec" +checksum = "96fa1ce81be900d083b30ec2d481e6658c2acfaa2cfc7be45ccc2cc1b820edb3" dependencies = [ "anstream", "anstyle", @@ -3861,9 +3861,9 @@ dependencies = [ [[package]] name = "snapbox-macros" -version = "0.3.10" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16569f53ca23a41bb6f62e0a5084aa1661f4814a67fa33696a79073e03a664af" +checksum = "3b750c344002d7cc69afb9da00ebd9b5c0f8ac2eb7d115d9d45d5b5f47718d74" dependencies = [ "anstream", ] From f477e11d26d9d096886346df07f2d5d14da3fe43 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:21:21 +0000 Subject: [PATCH 130/188] Update Rust crate thiserror to v2.0.17 (#21225) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11e23ab78a..5287ab0e9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -519,7 +519,7 @@ checksum = "85a8ab73a1c02b0c15597b22e09c7dc36e63b2f601f9d1e83ac0c3decd38b1ae" dependencies = [ "nix 0.29.0", "terminfo", - "thiserror 2.0.16", + "thiserror 2.0.17", "which", "windows-sys 0.59.0", ] @@ -1861,7 +1861,7 @@ dependencies = [ "paste", "peg", "regex", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -2394,7 +2394,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", - "thiserror 2.0.16", + "thiserror 2.0.17", "ucd-trie", ] @@ -2600,7 +2600,7 @@ dependencies = [ "pep440_rs", "pep508_rs", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "toml", ] @@ -2615,7 +2615,7 @@ dependencies = [ "newtype-uuid", "quick-xml", "strip-ansi-escapes", - "thiserror 2.0.16", + "thiserror 2.0.17", "uuid", ] @@ -2787,7 +2787,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -2909,7 +2909,7 @@ dependencies = [ "strum", "tempfile", "test-case", - "thiserror 2.0.16", + "thiserror 2.0.17", "tikv-jemallocator", "toml", "tracing", @@ -3005,7 +3005,7 @@ dependencies = [ "serde_json", "similar", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "tracing-subscriber", "ty_static", @@ -3166,7 +3166,7 @@ dependencies = [ "strum_macros", "tempfile", "test-case", - "thiserror 2.0.16", + "thiserror 2.0.17", "toml", "typed-arena", "unicode-normalization", @@ -3209,7 +3209,7 @@ dependencies = [ "serde_json", "serde_with", "test-case", - "thiserror 2.0.16", + "thiserror 2.0.17", "uuid", ] @@ -3241,7 +3241,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -3295,7 +3295,7 @@ dependencies = [ "similar", "smallvec", "static_assertions", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", ] @@ -3440,7 +3440,7 @@ dependencies = [ "serde", "serde_json", "shellexpand", - "thiserror 2.0.16", + "thiserror 2.0.17", "toml", "tracing", "tracing-log", @@ -4054,11 +4054,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -4074,9 +4074,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -4432,7 +4432,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "toml", "tracing", "ty_combine", @@ -4489,7 +4489,7 @@ dependencies = [ "strum_macros", "tempfile", "test-case", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "ty_python_semantic", "ty_static", @@ -4523,7 +4523,7 @@ dependencies = [ "serde_json", "shellexpand", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "tracing-subscriber", "ty_combine", @@ -4563,7 +4563,7 @@ dependencies = [ "serde", "smallvec", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.17", "toml", "tracing", "ty_python_semantic", From 3bef60f69a6249589bd17bbfea6b9587640c026d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:21:47 +0000 Subject: [PATCH 131/188] Update Rust crate toml to v0.9.8 (#21227) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5287ab0e9a..3ea34392f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1568,7 +1568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.15.5", "serde", "serde_core", ] @@ -3747,9 +3747,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ "serde_core", ] @@ -4158,9 +4158,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "indexmap", "serde_core", @@ -4173,9 +4173,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] @@ -4194,18 +4194,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tracing" From cb98175a363180eb6e7fd1ddb7cb9a0f5679bae8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:22:04 +0000 Subject: [PATCH 132/188] Update Rust crate syn to v2.0.108 (#21224) 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 3ea34392f1..04018a5b06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3928,9 +3928,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ "proc-macro2", "quote", From c596a78c08bfd0a5aed04eb9d3a2df5c66fb5386 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:27:40 +0000 Subject: [PATCH 133/188] Update Rust crate schemars to v1.0.5 (#21222) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04018a5b06..df4540ff31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3634,9 +3634,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "1317c3bf3e7df961da95b0a56a172a02abead31276215a0497241a7624b487ce" dependencies = [ "dyn-clone", "ref-cast", @@ -3647,9 +3647,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" +checksum = "5f760a6150d45dd66ec044983c124595ae76912e77ed0b44124cb3e415cce5d9" dependencies = [ "proc-macro2", "quote", From 770b4d12abcee57ff0d0b37e8e0dadefe29ddbbc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:35:50 +0000 Subject: [PATCH 134/188] Update Rust crate tikv-jemallocator to v0.6.1 (#21226) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df4540ff31..6bda5cfd9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4103,9 +4103,9 @@ dependencies = [ [[package]] name = "tikv-jemalloc-sys" -version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" +version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d" +checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b" dependencies = [ "cc", "libc", @@ -4113,9 +4113,9 @@ dependencies = [ [[package]] name = "tikv-jemallocator" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865" +checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a" dependencies = [ "libc", "tikv-jemalloc-sys", From c11b00bea0aea32c6dadcf0d2f302ea0e698ffe8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:57:13 +0000 Subject: [PATCH 135/188] Update Rust crate wasm-bindgen-test to v0.3.55 (#21232) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 68 ++++++++++++++++++++++-------------------------------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6bda5cfd9d..2174b2788a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -642,7 +642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -651,7 +651,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1108,7 +1108,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]] @@ -1698,7 +1698,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1762,7 +1762,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1809,9 +1809,9 @@ checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24" [[package]] name = "js-sys" -version = "0.3.80" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -3568,7 +3568,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3964,7 +3964,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4886,9 +4886,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -4897,25 +4897,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.53" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -4926,9 +4912,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4936,31 +4922,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aee0a0f5343de9221a0d233b04520ed8dc2e6728dce180b1dcd9288ec9d9fa3c" +checksum = "bfc379bfb624eb59050b509c13e77b4eb53150c350db69628141abce842f2373" dependencies = [ "js-sys", "minicov", @@ -4971,9 +4957,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a369369e4360c2884c3168d22bded735c43cccae97bbc147586d4b480edd138d" +checksum = "085b2df989e1e6f9620c1311df6c996e83fe16f57792b272ce1e024ac16a90f1" dependencies = [ "proc-macro2", "quote", @@ -4982,9 +4968,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.80" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -5026,7 +5012,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 fc71c90de6f1019eb9a5889424b46ad854ec7adb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:02:16 +0000 Subject: [PATCH 136/188] Update taiki-e/install-action action to v2.62.45 (#21233) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 16 ++++++++-------- .github/workflows/sync_typeshed.yaml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0fc4eeb444..e537aaa11b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -256,11 +256,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: tool: cargo-insta - name: "Install uv" @@ -320,11 +320,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: tool: cargo-insta - name: "Install uv" @@ -353,7 +353,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: tool: cargo-nextest - name: "Install uv" @@ -944,7 +944,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: tool: cargo-codspeed @@ -982,7 +982,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: tool: cargo-codspeed @@ -1020,7 +1020,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: tool: cargo-codspeed diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index f7bb4c5426..18df7ecf94 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -207,12 +207,12 @@ jobs: uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: "Install cargo nextest" if: ${{ success() }} - uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: tool: cargo-nextest - name: "Install cargo insta" if: ${{ success() }} - uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: tool: cargo-insta - name: Update snapshots From dc373e639e681c02c7e8aa1a3e25b225908ec451 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:04:20 +0000 Subject: [PATCH 137/188] Update CodSpeedHQ/action action to v4.3.1 (#21234) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .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 e537aaa11b..f4f1fbffff 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -952,7 +952,7 @@ jobs: run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser - name: "Run benchmarks" - uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1 + uses: CodSpeedHQ/action@4348f634fa7309fe23aac9502e88b999ec90a164 # v4.3.1 with: mode: instrumentation run: cargo codspeed run @@ -990,7 +990,7 @@ jobs: run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench ty - name: "Run benchmarks" - uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1 + uses: CodSpeedHQ/action@4348f634fa7309fe23aac9502e88b999ec90a164 # v4.3.1 with: mode: instrumentation run: cargo codspeed run @@ -1028,7 +1028,7 @@ jobs: run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark - name: "Run benchmarks" - uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1 + uses: CodSpeedHQ/action@4348f634fa7309fe23aac9502e88b999ec90a164 # v4.3.1 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 bb055273506497448741707b8da4a3e3b2212f9f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:06:16 +0000 Subject: [PATCH 138/188] Update dependency monaco-editor to ^0.54.0 (#21235) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- playground/package-lock.json | 37 ++++++++++++++++++++++++------------ playground/ruff/package.json | 2 +- playground/ty/package.json | 2 +- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/playground/package-lock.json b/playground/package-lock.json index c8ddfc60a9..908e07dadb 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -1789,12 +1789,6 @@ "@types/react": "^19.0.0" } }, - "node_modules/@types/trusted-types": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz", - "integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==", - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.38.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", @@ -2732,6 +2726,12 @@ "node": ">=0.10.0" } }, + "node_modules/dompurify": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", + "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", + "license": "(MPL-2.0 OR Apache-2.0)" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4733,6 +4733,18 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4841,13 +4853,14 @@ } }, "node_modules/monaco-editor": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.53.0.tgz", - "integrity": "sha512-0WNThgC6CMWNXXBxTbaYYcunj08iB5rnx4/G56UOPeL9UVIUGGHA1GR0EWIh9Ebabj7NpCRawQ5b0hfN1jQmYQ==", + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz", + "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==", "license": "MIT", "peer": true, "dependencies": { - "@types/trusted-types": "^1.0.6" + "dompurify": "3.1.7", + "marked": "14.0.0" } }, "node_modules/ms": { @@ -6546,7 +6559,7 @@ "@monaco-editor/react": "^4.4.6", "classnames": "^2.3.2", "lz-string": "^1.5.0", - "monaco-editor": "^0.53.0", + "monaco-editor": "^0.54.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^3.0.0", @@ -6575,7 +6588,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.5.1", "lz-string": "^1.5.0", - "monaco-editor": "^0.53.0", + "monaco-editor": "^0.54.0", "pyodide": "^0.28.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/playground/ruff/package.json b/playground/ruff/package.json index 50f87a38d1..abb46f73e9 100644 --- a/playground/ruff/package.json +++ b/playground/ruff/package.json @@ -18,7 +18,7 @@ "@monaco-editor/react": "^4.4.6", "classnames": "^2.3.2", "lz-string": "^1.5.0", - "monaco-editor": "^0.53.0", + "monaco-editor": "^0.54.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^3.0.0", diff --git a/playground/ty/package.json b/playground/ty/package.json index ad2b546980..f5b1fa8f0f 100644 --- a/playground/ty/package.json +++ b/playground/ty/package.json @@ -18,7 +18,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.5.1", "lz-string": "^1.5.0", - "monaco-editor": "^0.53.0", + "monaco-editor": "^0.54.0", "pyodide": "^0.28.0", "react": "^19.0.0", "react-dom": "^19.0.0", From 666dd5fef128c0886b00ebfebe099146fa05c044 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:13:38 +0000 Subject: [PATCH 139/188] Update dependency ruff to v0.14.3 (#21239) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/requirements-insiders.txt | 2 +- docs/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/requirements-insiders.txt b/docs/requirements-insiders.txt index 127d4bfaa2..5d0d47a756 100644 --- a/docs/requirements-insiders.txt +++ b/docs/requirements-insiders.txt @@ -1,5 +1,5 @@ PyYAML==6.0.3 -ruff==0.13.3 +ruff==0.14.3 mkdocs==1.6.1 mkdocs-material @ git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@39da7a5e761410349e9a1b8abf593b0cdd5453ff mkdocs-redirects==1.2.2 diff --git a/docs/requirements.txt b/docs/requirements.txt index 9742b48785..9ccce87029 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ PyYAML==6.0.3 -ruff==0.13.3 +ruff==0.14.3 mkdocs==1.6.1 mkdocs-material==9.5.38 mkdocs-redirects==1.2.2 From 3493c9b67ae6c3698f13ec1aa9f9f2def782886e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:17:38 +0000 Subject: [PATCH 140/188] Update dependency tomli to v2.3.0 (#21240) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- python/ruff-ecosystem/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ruff-ecosystem/pyproject.toml b/python/ruff-ecosystem/pyproject.toml index f688f10094..dd5af5cfeb 100644 --- a/python/ruff-ecosystem/pyproject.toml +++ b/python/ruff-ecosystem/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "ruff-ecosystem" version = "0.0.0" requires-python = ">=3.11" -dependencies = ["unidiff==0.7.5", "tomli_w==1.2.0", "tomli==2.2.1"] +dependencies = ["unidiff==0.7.5", "tomli_w==1.2.0", "tomli==2.3.0"] [project.scripts] ruff-ecosystem = "ruff_ecosystem.cli:entrypoint" From ade727ce662a51b2ce83aafd148ac47c0d9d240c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:20:42 +0000 Subject: [PATCH 141/188] Update Rust crate csv to v1.4.0 (#21243) 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 2174b2788a..32ddac77b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -865,14 +865,14 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] From 884c3b178e7e92c251e60db2fed17fcb1c85e665 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:21:03 +0000 Subject: [PATCH 142/188] Update Rust crate indexmap to v2.12.0 (#21244) 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 32ddac77b5..dba454d334 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1563,12 +1563,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "serde", "serde_core", ] From 61c1007137a2407fde8ebf917c871107d0c6f428 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:21:41 +0000 Subject: [PATCH 143/188] Update Rust crate bitflags to v2.10.0 (#21242) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dba454d334..8e465bfb40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,7 +240,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -262,9 +262,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bitvec" @@ -1025,7 +1025,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "libc", "objc2", @@ -1318,7 +1318,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "ignore", "walkdir", ] @@ -1602,7 +1602,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "inotify-sys", "libc", ] @@ -1900,7 +1900,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "redox_syscall", ] @@ -2105,7 +2105,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -2117,7 +2117,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -2145,7 +2145,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "fsevent-sys", "inotify", "kqueue", @@ -2776,7 +2776,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -2864,7 +2864,7 @@ dependencies = [ "argfile", "assert_fs", "bincode", - "bitflags 2.9.4", + "bitflags 2.10.0", "cachedir", "clap", "clap_complete_command", @@ -3119,7 +3119,7 @@ version = "0.14.3" dependencies = [ "aho-corasick", "anyhow", - "bitflags 2.9.4", + "bitflags 2.10.0", "clap", "colored 3.0.0", "fern", @@ -3225,7 +3225,7 @@ name = "ruff_python_ast" version = "0.0.0" dependencies = [ "aho-corasick", - "bitflags 2.9.4", + "bitflags 2.10.0", "compact_str", "get-size2", "is-macro", @@ -3329,7 +3329,7 @@ dependencies = [ name = "ruff_python_literal" version = "0.0.0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "itertools 0.14.0", "ruff_python_ast", "unic-ucd-category", @@ -3340,7 +3340,7 @@ name = "ruff_python_parser" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.9.4", + "bitflags 2.10.0", "bstr", "compact_str", "get-size2", @@ -3365,7 +3365,7 @@ dependencies = [ name = "ruff_python_semantic" version = "0.0.0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "insta", "is-macro", "ruff_cache", @@ -3386,7 +3386,7 @@ dependencies = [ name = "ruff_python_stdlib" version = "0.0.0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "unicode-ident", ] @@ -3564,7 +3564,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -4375,7 +4375,7 @@ dependencies = [ name = "ty_ide" version = "0.0.0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "camino", "get-size2", "insta", @@ -4446,7 +4446,7 @@ name = "ty_python_semantic" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.9.4", + "bitflags 2.10.0", "bitvec", "camino", "colored 3.0.0", @@ -4502,7 +4502,7 @@ name = "ty_server" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.9.4", + "bitflags 2.10.0", "crossbeam", "dunce", "insta", From fe95ff6b066afbdd44ba7c4605569c1b04e275a5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:23:24 +0000 Subject: [PATCH 144/188] Update dependency pyodide to ^0.29.0 (#21236) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- playground/package-lock.json | 15 +++++++++++---- playground/ty/package.json | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/playground/package-lock.json b/playground/package-lock.json index 908e07dadb..3de5b851bf 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -1747,6 +1747,12 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@types/emscripten": { + "version": "1.41.5", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz", + "integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -5278,11 +5284,12 @@ } }, "node_modules/pyodide": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.28.0.tgz", - "integrity": "sha512-QML/Gh8eu50q5zZKLNpW6rgS0XUdK+94OSL54AUSKV8eJAxgwZrMebqj+CyM0EbF3EUX8JFJU3ryaxBViHammQ==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.29.0.tgz", + "integrity": "sha512-ObIvsTmcrxAWKg+FT1GjfSdDmQc5CabnYe/nn5BCuhr9BVVITeQ24DBdZuG5B2tIiAZ9YonBpnDB7cmHZyd2Rw==", "license": "MPL-2.0", "dependencies": { + "@types/emscripten": "^1.41.4", "ws": "^8.5.0" }, "engines": { @@ -6589,7 +6596,7 @@ "classnames": "^2.5.1", "lz-string": "^1.5.0", "monaco-editor": "^0.54.0", - "pyodide": "^0.28.0", + "pyodide": "^0.29.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^3.0.0", diff --git a/playground/ty/package.json b/playground/ty/package.json index f5b1fa8f0f..cf36dceece 100644 --- a/playground/ty/package.json +++ b/playground/ty/package.json @@ -19,7 +19,7 @@ "classnames": "^2.5.1", "lz-string": "^1.5.0", "monaco-editor": "^0.54.0", - "pyodide": "^0.28.0", + "pyodide": "^0.29.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^3.0.0", From 31194d048df70f2a4e80cc47e0991ab6fd4eae49 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:31:24 +0000 Subject: [PATCH 145/188] Update Rust crate serde_with to v3.15.1 (#21247) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e465bfb40..a4c079fd6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3765,20 +3765,19 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.1" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" dependencies = [ - "serde", - "serde_derive", + "serde_core", "serde_with_macros", ] [[package]] name = "serde_with_macros" -version = "3.14.1" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" dependencies = [ "darling", "proc-macro2", From 14fce1f788764e4ecb4897393eb5dbd7fb9a2c7f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:32:04 +0000 Subject: [PATCH 146/188] Update Rust crate regex to v1.12.2 (#21246) 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 a4c079fd6b..44533f19d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2812,9 +2812,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", From f97c38dd881aac85367f5b925e45e1646cc5047f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:32:49 +0000 Subject: [PATCH 147/188] Update Rust crate matchit to 0.9.0 (#21245) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 44533f19d7..929a838dca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1108,7 +1108,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]] @@ -2019,9 +2019,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "matchit" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f926ade0c4e170215ae43342bf13b9310a437609c81f29f86c5df6657582ef9" +checksum = "9ea5f97102eb9e54ab99fb70bb175589073f554bdadfb74d9bd656482ea73e2a" [[package]] name = "memchr" @@ -3568,7 +3568,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -3963,7 +3963,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -5011,7 +5011,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 d12718ea12..ed7fbf4fcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,7 @@ lsp-server = { version = "0.7.6" } lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = [ "proposed", ] } -matchit = { version = "0.8.1" } +matchit = { version = "0.9.0" } memchr = { version = "2.7.1" } mimalloc = { version = "0.1.39" } natord = { version = "1.0.9" } From 7dbfb56c3ded7f8bff552f1e984d503cae13456c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:33:16 +0000 Subject: [PATCH 148/188] Update astral-sh/setup-uv action to v7 (#21250) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 26 ++++++++++---------- .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, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f4f1fbffff..3454de57af 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -264,7 +264,7 @@ jobs: with: tool: cargo-insta - name: "Install uv" - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: "true" - name: ty mdtests (GitHub annotations) @@ -328,7 +328,7 @@ jobs: with: tool: cargo-insta - name: "Install uv" - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: "true" - name: "Run tests" @@ -357,7 +357,7 @@ jobs: with: tool: cargo-nextest - name: "Install uv" - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: "true" - name: "Run tests" @@ -458,7 +458,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 name: Download Ruff binary to test id: download-cached-binary @@ -494,7 +494,7 @@ jobs: with: persist-credentials: false - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - name: "Install Rust toolchain" run: rustup component add rustfmt # Run all code generation scripts, and verify that the current output is @@ -529,7 +529,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: python-version: ${{ env.PYTHON_VERSION }} activate-environment: true @@ -667,7 +667,7 @@ jobs: branch: ${{ github.event.pull_request.base.ref }} workflow: "ci.yaml" check_artifacts: true - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - name: Fuzz env: FORCE_COLOR: 1 @@ -711,7 +711,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 - name: "Install Rust toolchain" run: rustup show @@ -756,7 +756,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: @@ -796,7 +796,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: Install uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: python-version: 3.13 activate-environment: true @@ -938,7 +938,7 @@ jobs: persist-credentials: false - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - name: "Install Rust toolchain" run: rustup show @@ -976,7 +976,7 @@ jobs: persist-credentials: false - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - name: "Install Rust toolchain" run: rustup show @@ -1014,7 +1014,7 @@ jobs: persist-credentials: false - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - name: "Install Rust toolchain" run: rustup show diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index c299e00fda..171f0c481a 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -34,7 +34,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - 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 672a038537..89028a2235 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -43,7 +43,7 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 with: @@ -85,7 +85,7 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 with: diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index e5473f80a3..5bfeee7f5b 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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: pattern: wheels-* diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index 18df7ecf94..3d84483c2b 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -77,7 +77,7 @@ jobs: run: | git config --global user.name typeshedbot git config --global user.email '<>' - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - name: Sync typeshed stubs run: | rm -rf "ruff/${VENDORED_TYPESHED}" @@ -131,7 +131,7 @@ jobs: with: persist-credentials: true ref: ${{ env.UPSTREAM_BRANCH}} - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - name: Setup git run: | git config --global user.name typeshedbot @@ -170,7 +170,7 @@ jobs: with: persist-credentials: true ref: ${{ env.UPSTREAM_BRANCH}} - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - 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 a59cc6c947..cd763c3db1 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -33,7 +33,7 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 30b3bc93ab..2078478505 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -29,7 +29,7 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 with: enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact From 02879fa3777892c7cdbfc8ef518edda87e61c601 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:34:13 +0000 Subject: [PATCH 149/188] Update actions/setup-node action to v6 (#21249) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 6 +++--- .github/workflows/publish-playground.yml | 2 +- .github/workflows/publish-ty-playground.yml | 2 +- .github/workflows/publish-wasm.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3454de57af..ce8c15f345 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -378,7 +378,7 @@ jobs: - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: 22 cache: "npm" @@ -758,7 +758,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2 - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 - - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: 22 - name: "Cache pre-commit" @@ -900,7 +900,7 @@ jobs: - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 - - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: 22 cache: "npm" diff --git a/.github/workflows/publish-playground.yml b/.github/workflows/publish-playground.yml index 24bf4b4fef..8986a6d130 100644 --- a/.github/workflows/publish-playground.yml +++ b/.github/workflows/publish-playground.yml @@ -31,7 +31,7 @@ jobs: persist-credentials: false - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: 22 package-manager-cache: false diff --git a/.github/workflows/publish-ty-playground.yml b/.github/workflows/publish-ty-playground.yml index f28086517c..a745e80794 100644 --- a/.github/workflows/publish-ty-playground.yml +++ b/.github/workflows/publish-ty-playground.yml @@ -35,7 +35,7 @@ jobs: persist-credentials: false - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: 22 package-manager-cache: false diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml index a51c888286..a0c6226406 100644 --- a/.github/workflows/publish-wasm.yml +++ b/.github/workflows/publish-wasm.yml @@ -45,7 +45,7 @@ jobs: jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json 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 - - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: 22 registry-url: "https://registry.npmjs.org" From f947c23cd7ac079934ef7bd9e01b516906179ff7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:41:20 +0000 Subject: [PATCH 150/188] Update Rust crate tempfile to v3.23.0 (#21248) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 929a838dca..bfe02a8697 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1016,7 +1016,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -1108,7 +1108,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]] @@ -3568,7 +3568,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -3955,15 +3955,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.22.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -5011,7 +5011,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 0dfd55babfc7ea1c980c05e516ac9e07075952e9 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 3 Nov 2025 08:38:34 -0500 Subject: [PATCH 151/188] Delete unused `AsciiCharSet` in `FURB156` (#21181) Summary -- This code has been unused since #14233 but not detected by clippy I guess. This should help to remove the temptation to use the set comparison again like I suggested in #21144. And we shouldn't do the set comparison because of #13802, which #14233 fixed. Test Plan -- Existing tests --- .../refurb/rules/hardcoded_string_charset.rs | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/crates/ruff_linter/src/rules/refurb/rules/hardcoded_string_charset.rs b/crates/ruff_linter/src/rules/refurb/rules/hardcoded_string_charset.rs index 151bdc3113..8194ac87d4 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/hardcoded_string_charset.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/hardcoded_string_charset.rs @@ -62,40 +62,11 @@ pub(crate) fn hardcoded_string_charset_literal(checker: &Checker, expr: &ExprStr struct NamedCharset { name: &'static str, bytes: &'static [u8], - ascii_char_set: AsciiCharSet, -} - -/// Represents the set of ascii characters in form of a bitset. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -struct AsciiCharSet(u128); - -impl AsciiCharSet { - /// Creates the set of ascii characters from `bytes`. - /// Returns None if there is non-ascii byte. - const fn from_bytes(bytes: &[u8]) -> Option { - // TODO: simplify implementation, when const-traits are supported - // https://github.com/rust-lang/rust-project-goals/issues/106 - let mut bitset = 0; - let mut i = 0; - while i < bytes.len() { - if !bytes[i].is_ascii() { - return None; - } - bitset |= 1 << bytes[i]; - i += 1; - } - Some(Self(bitset)) - } } impl NamedCharset { const fn new(name: &'static str, bytes: &'static [u8]) -> Self { - Self { - name, - bytes, - // SAFETY: The named charset is guaranteed to have only ascii bytes. - ascii_char_set: AsciiCharSet::from_bytes(bytes).unwrap(), - } + Self { name, bytes } } } From e017b039df5f17102e7ccfd7820814e029563102 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Mon, 3 Nov 2025 14:33:05 +0000 Subject: [PATCH 152/188] [ty] Favor in scope completions (#21194) ## Summary Resolves https://github.com/astral-sh/ty/issues/1464 We sort the completions before we add the unimported ones, meaning that imported completions show up before unimported ones. This is also spoken about in https://github.com/astral-sh/ty/issues/1274, and this is probably a duplicate of that. @AlexWaygood mentions this [here](https://github.com/astral-sh/ty/issues/1274#issuecomment-3345942698) too. ## Test Plan Add a test showing even if an unimported completion "should" (alphabetically before) come first, we favor the imported one. --- .../completion-evaluation-tasks.csv | 8 ++--- crates/ty_ide/src/completion.rs | 34 +++++++++++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/crates/ty_completion_eval/completion-evaluation-tasks.csv b/crates/ty_completion_eval/completion-evaluation-tasks.csv index 01b8ca4373..4bea881bf6 100644 --- a/crates/ty_completion_eval/completion-evaluation-tasks.csv +++ b/crates/ty_completion_eval/completion-evaluation-tasks.csv @@ -1,5 +1,5 @@ name,file,index,rank -auto-import-skips-current-module,main.py,0,4 +auto-import-skips-current-module,main.py,0,1 fstring-completions,main.py,0,1 higher-level-symbols-preferred,main.py,0, higher-level-symbols-preferred,main.py,1,1 @@ -10,17 +10,17 @@ import-deprioritizes-type_check_only,main.py,1,1 import-deprioritizes-type_check_only,main.py,2,1 import-deprioritizes-type_check_only,main.py,3,2 import-deprioritizes-type_check_only,main.py,4,3 -internal-typeshed-hidden,main.py,0,4 +internal-typeshed-hidden,main.py,0,5 none-completion,main.py,0,11 numpy-array,main.py,0, numpy-array,main.py,1,1 object-attr-instance-methods,main.py,0,1 object-attr-instance-methods,main.py,1,1 raise-uses-base-exception,main.py,0,2 -scope-existing-over-new-import,main.py,0,474 +scope-existing-over-new-import,main.py,0,13 scope-prioritize-closer,main.py,0,2 scope-simple-long-identifier,main.py,0,1 tstring-completions,main.py,0,1 ty-extensions-lower-stdlib,main.py,0,8 type-var-typing-over-ast,main.py,0,3 -type-var-typing-over-ast,main.py,1,270 +type-var-typing-over-ast,main.py,1,277 diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 7ca15362b0..5b84e199f0 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -883,9 +883,16 @@ fn is_in_definition_place(db: &dyn Db, tokens: &[Token], file: File) -> bool { /// This has the effect of putting all dunder attributes after "normal" /// attributes, and all single-underscore attributes after dunder attributes. fn compare_suggestions(c1: &Completion, c2: &Completion) -> Ordering { - let (kind1, kind2) = (NameKind::classify(&c1.name), NameKind::classify(&c2.name)); + fn key<'a>(completion: &'a Completion) -> (bool, NameKind, bool, &'a Name) { + ( + completion.module_name.is_some(), + NameKind::classify(&completion.name), + completion.is_type_check_only, + &completion.name, + ) + } - (kind1, c1.is_type_check_only, &c1.name).cmp(&(kind2, c2.is_type_check_only, &c2.name)) + key(c1).cmp(&key(c2)) } #[cfg(test)] @@ -3440,8 +3447,8 @@ from os. .build() .snapshot(); assert_snapshot!(snapshot, @r" - AbraKadabra :: Unavailable :: package Kadabra :: Literal[1] :: Current module + AbraKadabra :: Unavailable :: package "); } @@ -4168,6 +4175,27 @@ type assert!(!builder.build().completions().is_empty()); } + #[test] + fn favour_symbols_currently_imported() { + let snapshot = CursorTest::builder() + .source("main.py", "long_nameb = 1\nlong_name") + .source("foo.py", "def long_namea(): ...") + .completion_test_builder() + .type_signatures() + .auto_import() + .module_names() + .filter(|c| c.name.contains("long_name")) + .build() + .snapshot(); + + // Even though long_namea is alphabetically before long_nameb, + // long_nameb is currently imported and should be preferred. + assert_snapshot!(snapshot, @r" + long_nameb :: Literal[1] :: Current module + long_namea :: Unavailable :: foo + "); + } + /// A way to create a simple single-file (named `main.py`) completion test /// builder. /// From 6ddfb51d71f511cfc35858d2d08b7e94cc5d1903 Mon Sep 17 00:00:00 2001 From: Dan Parizher <105245560+danparizher@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:45:23 -0500 Subject: [PATCH 153/188] [`flake8-bugbear`] Mark fix as unsafe for non-NFKC attribute names (`B009`, `B010`) (#21131) --- .../test/fixtures/flake8_bugbear/B009_B010.py | 9 +++++ .../rules/getattr_with_constant.rs | 32 +++++++++++++++-- .../rules/setattr_with_constant.rs | 34 +++++++++++++++++-- ...ke8_bugbear__tests__B009_B009_B010.py.snap | 18 ++++++++++ ...ke8_bugbear__tests__B010_B009_B010.py.snap | 16 +++++++++ 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B009_B010.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B009_B010.py index ce6e5c291e..3562a5a989 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B009_B010.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B009_B010.py @@ -70,3 +70,12 @@ builtins.getattr(foo, "bar") # Regression test for: https://github.com/astral-sh/ruff/issues/18353 setattr(foo, "__debug__", 0) + +# Regression test for: https://github.com/astral-sh/ruff/issues/21126 +# Non-NFKC attribute names should be marked as unsafe. Python normalizes identifiers in +# attribute access (obj.attr) using NFKC, but does not normalize string +# arguments passed to getattr/setattr. Rewriting `getattr(ns, "ſ")` to +# `ns.ſ` would be interpreted as `ns.s` at runtime, changing behavior. +# Example: the long s character "ſ" normalizes to "s" under NFKC. +getattr(foo, "ſ") +setattr(foo, "ſ", 1) diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/getattr_with_constant.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/getattr_with_constant.rs index 7905de14ca..9271d2b01a 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/getattr_with_constant.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/getattr_with_constant.rs @@ -3,6 +3,7 @@ use ruff_python_ast::{self as ast, Expr}; use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private}; use ruff_source_file::LineRanges; use ruff_text_size::Ranged; +use unicode_normalization::UnicodeNormalization; use crate::checkers::ast::Checker; use crate::fix::edits::pad; @@ -29,6 +30,21 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// obj.foo /// ``` /// +/// ## Fix safety +/// The fix is marked as unsafe for attribute names that are not in NFKC (Normalization Form KC) +/// normalization. Python normalizes identifiers using NFKC when using attribute access syntax +/// (e.g., `obj.attr`), but does not normalize string arguments passed to `getattr`. Rewriting +/// `getattr(obj, "ſ")` to `obj.ſ` would be interpreted as `obj.s` at runtime, changing behavior. +/// +/// For example, the long s character `"ſ"` normalizes to `"s"` under NFKC, so: +/// ```python +/// # This accesses an attribute with the exact name "ſ" (if it exists) +/// value = getattr(obj, "ſ") +/// +/// # But this would normalize to "s" and access a different attribute +/// obj.ſ # This is interpreted as obj.s, not obj.ſ +/// ``` +/// /// ## References /// - [Python documentation: `getattr`](https://docs.python.org/3/library/functions.html#getattr) #[derive(ViolationMetadata)] @@ -69,8 +85,14 @@ pub(crate) fn getattr_with_constant(checker: &Checker, expr: &Expr, func: &Expr, return; } + // Mark fixes as unsafe for non-NFKC attribute names. Python normalizes identifiers using NFKC, so using + // attribute syntax (e.g., `obj.attr`) would normalize the name and potentially change + // program behavior. + let attr_name = value.to_str(); + let is_unsafe = attr_name.nfkc().collect::() != attr_name; + let mut diagnostic = checker.report_diagnostic(GetAttrWithConstant, expr.range()); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + let edit = Edit::range_replacement( pad( if matches!( obj, @@ -88,5 +110,11 @@ pub(crate) fn getattr_with_constant(checker: &Checker, expr: &Expr, func: &Expr, checker.locator(), ), expr.range(), - ))); + ); + let fix = if is_unsafe { + Fix::unsafe_edit(edit) + } else { + Fix::safe_edit(edit) + }; + diagnostic.set_fix(fix); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/setattr_with_constant.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/setattr_with_constant.rs index d3ba5b953e..51fee45110 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/setattr_with_constant.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/setattr_with_constant.rs @@ -4,6 +4,7 @@ use ruff_text_size::{Ranged, TextRange}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_codegen::Generator; use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private}; +use unicode_normalization::UnicodeNormalization; use crate::checkers::ast::Checker; use crate::{AlwaysFixableViolation, Edit, Fix}; @@ -28,6 +29,23 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// obj.foo = 42 /// ``` /// +/// ## Fix safety +/// The fix is marked as unsafe for attribute names that are not in NFKC (Normalization Form KC) +/// normalization. Python normalizes identifiers using NFKC when using attribute access syntax +/// (e.g., `obj.attr = value`), but does not normalize string arguments passed to `setattr`. +/// Rewriting `setattr(obj, "ſ", 1)` to `obj.ſ = 1` would be interpreted as `obj.s = 1` at +/// runtime, changing behavior. +/// +/// For example, the long s character `"ſ"` normalizes to `"s"` under NFKC, so: +/// ```python +/// # This creates an attribute with the exact name "ſ" +/// setattr(obj, "ſ", 1) +/// getattr(obj, "ſ") # Returns 1 +/// +/// # But this would normalize to "s" and set a different attribute +/// obj.ſ = 1 # This is interpreted as obj.s = 1, not obj.ſ = 1 +/// ``` +/// /// ## References /// - [Python documentation: `setattr`](https://docs.python.org/3/library/functions.html#setattr) #[derive(ViolationMetadata)] @@ -89,6 +107,12 @@ pub(crate) fn setattr_with_constant(checker: &Checker, expr: &Expr, func: &Expr, return; } + // Mark fixes as unsafe for non-NFKC attribute names. Python normalizes identifiers using NFKC, so using + // attribute syntax (e.g., `obj.attr = value`) would normalize the name and potentially change + // program behavior. + let attr_name = name.to_str(); + let is_unsafe = attr_name.nfkc().collect::() != attr_name; + // We can only replace a `setattr` call (which is an `Expr`) with an assignment // (which is a `Stmt`) if the `Expr` is already being used as a `Stmt` // (i.e., it's directly within an `Stmt::Expr`). @@ -100,10 +124,16 @@ pub(crate) fn setattr_with_constant(checker: &Checker, expr: &Expr, func: &Expr, { if expr == child.as_ref() { let mut diagnostic = checker.report_diagnostic(SetAttrWithConstant, expr.range()); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + let edit = Edit::range_replacement( assignment(obj, name.to_str(), value, checker.generator()), expr.range(), - ))); + ); + let fix = if is_unsafe { + Fix::unsafe_edit(edit) + } else { + Fix::safe_edit(edit) + }; + diagnostic.set_fix(fix); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B009_B009_B010.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B009_B009_B010.py.snap index ab05bd0966..febc145dd7 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B009_B009_B010.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B009_B009_B010.py.snap @@ -360,3 +360,21 @@ help: Replace `getattr` with attribute access 70 | 71 | # Regression test for: https://github.com/astral-sh/ruff/issues/18353 72 | setattr(foo, "__debug__", 0) + +B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access. + --> B009_B010.py:80:1 + | +78 | # `ns.ſ` would be interpreted as `ns.s` at runtime, changing behavior. +79 | # Example: the long s character "ſ" normalizes to "s" under NFKC. +80 | getattr(foo, "ſ") + | ^^^^^^^^^^^^^^^^^ +81 | setattr(foo, "ſ", 1) + | +help: Replace `getattr` with attribute access +77 | # arguments passed to getattr/setattr. Rewriting `getattr(ns, "ſ")` to +78 | # `ns.ſ` would be interpreted as `ns.s` at runtime, changing behavior. +79 | # Example: the long s character "ſ" normalizes to "s" under NFKC. + - getattr(foo, "ſ") +80 + foo.ſ +81 | setattr(foo, "ſ", 1) +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B010_B009_B010.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B010_B009_B010.py.snap index 87c2f01bfe..8dab4338a1 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B010_B009_B010.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B010_B009_B010.py.snap @@ -118,3 +118,19 @@ help: Replace `setattr` with assignment 56 | 57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885 58 | assert getattr(func, '_rpc')is True + +B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access. + --> B009_B010.py:81:1 + | +79 | # Example: the long s character "ſ" normalizes to "s" under NFKC. +80 | getattr(foo, "ſ") +81 | setattr(foo, "ſ", 1) + | ^^^^^^^^^^^^^^^^^^^^ + | +help: Replace `setattr` with assignment +78 | # `ns.ſ` would be interpreted as `ns.s` at runtime, changing behavior. +79 | # Example: the long s character "ſ" normalizes to "s" under NFKC. +80 | getattr(foo, "ſ") + - setattr(foo, "ſ", 1) +81 + foo.ſ = 1 +note: This is an unsafe fix and may change runtime behavior From e2e83acd2f2929cb382b4f8eccd92b63f0d31944 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Mon, 3 Nov 2025 14:49:58 +0000 Subject: [PATCH 154/188] [ty] Remove mentions of VS Code from server logs (#21155) Co-authored-by: Micha Reiser --- crates/ty_project/src/metadata/options.rs | 16 ++++----- crates/ty_project/src/metadata/value.rs | 21 ++++++----- crates/ty_python_semantic/src/diagnostic.rs | 4 +-- crates/ty_python_semantic/src/program.rs | 7 ++-- .../ty_python_semantic/src/site_packages.rs | 35 +++++++++++++------ crates/ty_server/src/session/options.rs | 4 +-- 6 files changed, 50 insertions(+), 37 deletions(-) diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index bcd821c53d..1e498f6bf8 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -131,9 +131,7 @@ impl Options { ValueSource::File(path) => PythonVersionSource::ConfigFile( PythonVersionFileSource::new(path.clone(), ranged_version.range()), ), - ValueSource::PythonVSCodeExtension => { - PythonVersionSource::PythonVSCodeExtension - } + ValueSource::Editor => PythonVersionSource::Editor, }, }); @@ -153,7 +151,7 @@ impl Options { ValueSource::File(path) => { SysPrefixPathOrigin::ConfigFileSetting(path.clone(), python_path.range()) } - ValueSource::PythonVSCodeExtension => SysPrefixPathOrigin::PythonVSCodeExtension, + ValueSource::Editor => SysPrefixPathOrigin::Editor, }; Some(PythonEnvironment::new( @@ -819,8 +817,8 @@ impl Rules { ValueSource::File(_) => LintSource::File, ValueSource::Cli => LintSource::Cli, - ValueSource::PythonVSCodeExtension => { - unreachable!("Can't configure rules from the Python VSCode extension") + ValueSource::Editor => { + unreachable!("Can't configure rules from the user's editor") } }; if let Ok(severity) = Severity::try_from(**level) { @@ -957,7 +955,7 @@ fn build_include_filter( SubDiagnosticSeverity::Info, "The pattern was specified on the CLI", )), - ValueSource::PythonVSCodeExtension => unreachable!("Can't configure includes from the Python VSCode extension"), + ValueSource::Editor => unreachable!("Can't configure includes from the user's editor"), } })?; } @@ -1040,8 +1038,8 @@ fn build_exclude_filter( SubDiagnosticSeverity::Info, "The pattern was specified on the CLI", )), - ValueSource::PythonVSCodeExtension => unreachable!( - "Can't configure excludes from the Python VSCode extension" + ValueSource::Editor => unreachable!( + "Can't configure excludes from the user's editor" ) } })?; diff --git a/crates/ty_project/src/metadata/value.rs b/crates/ty_project/src/metadata/value.rs index f1f08d718a..22d940df58 100644 --- a/crates/ty_project/src/metadata/value.rs +++ b/crates/ty_project/src/metadata/value.rs @@ -28,8 +28,11 @@ pub enum ValueSource { /// long argument (`--extra-paths`) or `--config key=value`. Cli, - /// The value comes from an LSP client configuration. - PythonVSCodeExtension, + /// The value comes from the user's editor, + /// while it's left open if specified as a setting + /// or if the value was auto-discovered by the editor + /// (e.g., the Python environment) + Editor, } impl ValueSource { @@ -37,7 +40,7 @@ impl ValueSource { match self { ValueSource::File(path) => Some(&**path), ValueSource::Cli => None, - ValueSource::PythonVSCodeExtension => None, + ValueSource::Editor => None, } } @@ -137,11 +140,7 @@ impl RangedValue { } pub fn python_extension(value: T) -> Self { - Self::with_range( - value, - ValueSource::PythonVSCodeExtension, - TextRange::default(), - ) + Self::with_range(value, ValueSource::Editor, TextRange::default()) } pub fn with_range(value: T, source: ValueSource, range: TextRange) -> Self { @@ -368,7 +367,7 @@ impl RelativePathBuf { } pub fn python_extension(path: impl AsRef) -> Self { - Self::new(path, ValueSource::PythonVSCodeExtension) + Self::new(path, ValueSource::Editor) } /// Returns the relative path as specified by the user. @@ -398,7 +397,7 @@ impl RelativePathBuf { pub fn absolute(&self, project_root: &SystemPath, system: &dyn System) -> SystemPathBuf { let relative_to = match &self.0.source { ValueSource::File(_) => project_root, - ValueSource::Cli | ValueSource::PythonVSCodeExtension => system.current_directory(), + ValueSource::Cli | ValueSource::Editor => system.current_directory(), }; SystemPath::absolute(&self.0, relative_to) @@ -454,7 +453,7 @@ impl RelativeGlobPattern { ) -> Result { let relative_to = match &self.0.source { ValueSource::File(_) => project_root, - ValueSource::Cli | ValueSource::PythonVSCodeExtension => system.current_directory(), + ValueSource::Cli | ValueSource::Editor => system.current_directory(), }; let pattern = PortableGlobPattern::parse(&self.0, kind)?; diff --git a/crates/ty_python_semantic/src/diagnostic.rs b/crates/ty_python_semantic/src/diagnostic.rs index 5936d0874d..f58c90191c 100644 --- a/crates/ty_python_semantic/src/diagnostic.rs +++ b/crates/ty_python_semantic/src/diagnostic.rs @@ -88,10 +88,10 @@ pub fn add_inferred_python_version_hint_to_diagnostic( or in a configuration file", ); } - crate::PythonVersionSource::PythonVSCodeExtension => { + crate::PythonVersionSource::Editor => { diagnostic.info(format_args!( "Python {version} was assumed when {action} \ - because it's the version of the selected Python interpreter in the VS Code Python extension", + because it's the version of the selected Python interpreter in your editor", )); } crate::PythonVersionSource::InstallationDirectoryLayout { diff --git a/crates/ty_python_semantic/src/program.rs b/crates/ty_python_semantic/src/program.rs index 8f06527951..1a977de985 100644 --- a/crates/ty_python_semantic/src/program.rs +++ b/crates/ty_python_semantic/src/program.rs @@ -113,8 +113,11 @@ pub enum PythonVersionSource { /// long argument (`--extra-paths`) or `--config key=value`. Cli, - /// The value comes from the Python VS Code extension (the selected interpreter). - PythonVSCodeExtension, + /// The value comes from the user's editor, + /// while it's left open if specified as a setting + /// or if the value was auto-discovered by the editor + /// (e.g., the Python environment) + Editor, /// We fell back to a default value because the value was not specified via the CLI or a config file. #[default] diff --git a/crates/ty_python_semantic/src/site_packages.rs b/crates/ty_python_semantic/src/site_packages.rs index 9062228363..c162dfc70a 100644 --- a/crates/ty_python_semantic/src/site_packages.rs +++ b/crates/ty_python_semantic/src/site_packages.rs @@ -111,6 +111,12 @@ impl SitePackagesPaths { } } +impl fmt::Display for SitePackagesPaths { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.0.iter()).finish() + } +} + impl From<[SystemPathBuf; N]> for SitePackagesPaths { fn from(paths: [SystemPathBuf; N]) -> Self { Self(IndexSet::from(paths)) @@ -543,7 +549,7 @@ System site-packages will not be used for module resolution.", } tracing::debug!( - "Resolved site-packages directories for this virtual environment are: {site_packages_directories:?}" + "Resolved site-packages directories for this virtual environment are: {site_packages_directories}" ); Ok(site_packages_directories) } @@ -823,7 +829,7 @@ impl SystemEnvironment { )?; tracing::debug!( - "Resolved site-packages directories for this environment are: {site_packages_directories:?}" + "Resolved site-packages directories for this environment are: {site_packages_directories}" ); Ok(site_packages_directories) } @@ -1567,8 +1573,8 @@ pub enum SysPrefixPathOrigin { ConfigFileSetting(Arc, Option), /// The `sys.prefix` path came from a `--python` CLI flag PythonCliFlag, - /// The selected interpreter in the VS Code's Python extension. - PythonVSCodeExtension, + /// The selected interpreter in the user's editor. + Editor, /// The `sys.prefix` path came from the `VIRTUAL_ENV` environment variable VirtualEnvVar, /// The `sys.prefix` path came from the `CONDA_PREFIX` environment variable @@ -1590,7 +1596,7 @@ impl SysPrefixPathOrigin { Self::LocalVenv | Self::VirtualEnvVar => true, Self::ConfigFileSetting(..) | Self::PythonCliFlag - | Self::PythonVSCodeExtension + | Self::Editor | Self::DerivedFromPyvenvCfg | Self::CondaPrefixVar => false, } @@ -1602,9 +1608,7 @@ impl SysPrefixPathOrigin { /// the `sys.prefix` directory, e.g. the `--python` CLI flag. pub(crate) const fn must_point_directly_to_sys_prefix(&self) -> bool { match self { - Self::PythonCliFlag | Self::ConfigFileSetting(..) | Self::PythonVSCodeExtension => { - false - } + Self::PythonCliFlag | Self::ConfigFileSetting(..) | Self::Editor => false, Self::VirtualEnvVar | Self::CondaPrefixVar | Self::DerivedFromPyvenvCfg @@ -1622,9 +1626,7 @@ impl std::fmt::Display for SysPrefixPathOrigin { Self::CondaPrefixVar => f.write_str("`CONDA_PREFIX` environment variable"), Self::DerivedFromPyvenvCfg => f.write_str("derived `sys.prefix` path"), Self::LocalVenv => f.write_str("local virtual environment"), - Self::PythonVSCodeExtension => { - f.write_str("selected interpreter in the VS Code Python extension") - } + Self::Editor => f.write_str("selected interpreter in your editor"), } } } @@ -2377,4 +2379,15 @@ mod tests { assert_eq!(&pyvenv_cfg[version.1], version.0); assert_eq!(parsed.implementation, PythonImplementation::PyPy); } + + #[test] + fn site_packages_paths_display() { + let paths = SitePackagesPaths::default(); + assert_eq!(paths.to_string(), "[]"); + + let mut paths = SitePackagesPaths::default(); + paths.insert(SystemPathBuf::from("/path/to/site/packages")); + + assert_eq!(paths.to_string(), r#"["/path/to/site/packages"]"#); + } } diff --git a/crates/ty_server/src/session/options.rs b/crates/ty_server/src/session/options.rs index c646c767de..982dccd484 100644 --- a/crates/ty_server/src/session/options.rs +++ b/crates/ty_server/src/session/options.rs @@ -213,7 +213,7 @@ impl WorkspaceOptions { if let Some(python) = &overrides.fallback_python { tracing::debug!( - "Using the Python environment selected in the VS Code Python extension \ + "Using the Python environment selected in your editor \ in case the configuration doesn't specify a Python environment: {python}", python = python.path() ); @@ -221,7 +221,7 @@ impl WorkspaceOptions { if let Some(version) = &overrides.fallback_python_version { tracing::debug!( - "Using the Python version selected in the VS Code Python extension: {version} \ + "Using the Python version selected in your editor: {version} \ in case the configuration doesn't specify a Python version", ); } From de9df1b326ad039d82903e44d5e090a1677ad910 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 3 Nov 2025 15:53:04 +0100 Subject: [PATCH 155/188] Revert "Update CodSpeedHQ/action action to v4.3.1" (#21252) --- .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 ce8c15f345..7130f15b29 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -952,7 +952,7 @@ jobs: run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser - name: "Run benchmarks" - uses: CodSpeedHQ/action@4348f634fa7309fe23aac9502e88b999ec90a164 # v4.3.1 + uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1 with: mode: instrumentation run: cargo codspeed run @@ -990,7 +990,7 @@ jobs: run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench ty - name: "Run benchmarks" - uses: CodSpeedHQ/action@4348f634fa7309fe23aac9502e88b999ec90a164 # v4.3.1 + uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1 with: mode: instrumentation run: cargo codspeed run @@ -1028,7 +1028,7 @@ jobs: run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark - name: "Run benchmarks" - uses: CodSpeedHQ/action@4348f634fa7309fe23aac9502e88b999ec90a164 # v4.3.1 + uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1 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 1b2ed6a503ffad946796e4be1c7c55c7dc79f808 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 3 Nov 2025 16:00:30 +0100 Subject: [PATCH 156/188] [ty] Fix caching of imported modules in playground (#21251) --- playground/ty/src/Editor/SecondaryPanel.tsx | 68 ++++++++++----------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/playground/ty/src/Editor/SecondaryPanel.tsx b/playground/ty/src/Editor/SecondaryPanel.tsx index 7f68bb7153..a73cb57961 100644 --- a/playground/ty/src/Editor/SecondaryPanel.tsx +++ b/playground/ty/src/Editor/SecondaryPanel.tsx @@ -2,7 +2,7 @@ import MonacoEditor from "@monaco-editor/react"; import { AstralButton, Theme } from "shared"; import { ReadonlyFiles } from "../Playground"; import { Suspense, use, useState } from "react"; -import { loadPyodide, PyodideInterface } from "pyodide"; +import { loadPyodide } from "pyodide"; import classNames from "classnames"; export enum SecondaryTool { @@ -103,41 +103,12 @@ function Content({ } } -let pyodidePromise: Promise | null = null; - function Run({ files, theme }: { files: ReadonlyFiles; theme: Theme }) { - if (pyodidePromise == null) { - pyodidePromise = loadPyodide(); - } + const [runOutput, setRunOutput] = useState | null>(null); + const handleRun = () => { + const output = (async () => { + const pyodide = await loadPyodide(); - return ( - Loading} - > - - - ); -} - -function RunWithPyiodide({ - files, - pyodidePromise, - theme, -}: { - files: ReadonlyFiles; - theme: Theme; - pyodidePromise: Promise; -}) { - const pyodide = use(pyodidePromise); - - const [output, setOutput] = useState(null); - - if (output == null) { - const handleRun = () => { let combined_output = ""; const outputHandler = (output: string) => { @@ -179,14 +150,18 @@ function RunWithPyiodide({ filename: fileName, }); - setOutput(combined_output); + return combined_output; } catch (e) { - setOutput(`Failed to run Python script: ${e}`); + return `Failed to run Python script: ${e}`; } finally { globals.destroy(); dict.destroy(); } - }; + })(); + setRunOutput(output); + }; + + if (runOutput == null) { return (
    ); } + + return ( + Loading
    } + > + + + ); +} + +function RunOutput({ + runOutput, + theme, +}: { + theme: Theme; + runOutput: Promise; +}) { + const output = use(runOutput); + return (
    Date: Mon, 3 Nov 2025 16:35:42 +0100
    Subject: [PATCH 157/188] [ty] Simplify semantic token tests (#21206)
    
    ---
     crates/ty_ide/src/inlay_hints.rs     |  19 +-
     crates/ty_ide/src/lib.rs             |  16 +-
     crates/ty_ide/src/semantic_tokens.rs | 755 ++++++++++++++-------------
     crates/ty_project/src/db.rs          |  27 +-
     crates/ty_project/src/lib.rs         |  16 +-
     5 files changed, 422 insertions(+), 411 deletions(-)
    
    diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs
    index 1e3f545aff..47859c39e3 100644
    --- a/crates/ty_ide/src/inlay_hints.rs
    +++ b/crates/ty_ide/src/inlay_hints.rs
    @@ -302,7 +302,6 @@ mod tests {
     
         use insta::assert_snapshot;
         use ruff_db::{
    -        Db as _,
             files::{File, system_path_to_file},
             source::source_text,
         };
    @@ -311,9 +310,6 @@ mod tests {
     
         use ruff_db::system::{DbWithWritableSystem, SystemPathBuf};
         use ty_project::ProjectMetadata;
    -    use ty_python_semantic::{
    -        Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
    -    };
     
         pub(super) fn inlay_hint_test(source: &str) -> InlayHintTest {
             const START: &str = "";
    @@ -324,6 +320,8 @@ mod tests {
                 SystemPathBuf::from("/"),
             ));
     
    +        db.init_program().unwrap();
    +
             let source = dedent(source);
     
             let start = source.find(START);
    @@ -345,19 +343,6 @@ mod tests {
     
             let file = system_path_to_file(&db, "main.py").expect("newly written file to existing");
     
    -        let search_paths = SearchPathSettings::new(vec![SystemPathBuf::from("/")])
    -            .to_search_paths(db.system(), db.vendored())
    -            .expect("Valid search path settings");
    -
    -        Program::from_settings(
    -            &db,
    -            ProgramSettings {
    -                python_version: PythonVersionWithSource::default(),
    -                python_platform: PythonPlatform::default(),
    -                search_paths,
    -            },
    -        );
    -
             InlayHintTest { db, file, range }
         }
     
    diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs
    index 6a23302561..057d75b688 100644
    --- a/crates/ty_ide/src/lib.rs
    +++ b/crates/ty_ide/src/lib.rs
    @@ -338,9 +338,6 @@ mod tests {
         use ruff_python_trivia::textwrap::dedent;
         use ruff_text_size::TextSize;
         use ty_project::ProjectMetadata;
    -    use ty_python_semantic::{
    -        Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
    -    };
     
         /// A way to create a simple single-file (named `main.py`) cursor test.
         ///
    @@ -417,18 +414,7 @@ mod tests {
                     SystemPathBuf::from("/"),
                 ));
     
    -            let search_paths = SearchPathSettings::new(vec![SystemPathBuf::from("/")])
    -                .to_search_paths(db.system(), db.vendored())
    -                .expect("Valid search path settings");
    -
    -            Program::from_settings(
    -                &db,
    -                ProgramSettings {
    -                    python_version: PythonVersionWithSource::default(),
    -                    python_platform: PythonPlatform::default(),
    -                    search_paths,
    -                },
    -            );
    +            db.init_program().unwrap();
     
                 let mut cursor: Option = None;
                 for &Source {
    diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs
    index ca736acd4c..df1bb88b37 100644
    --- a/crates/ty_ide/src/semantic_tokens.rs
    +++ b/crates/ty_ide/src/semantic_tokens.rs
    @@ -930,88 +930,48 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
     #[cfg(test)]
     mod tests {
         use super::*;
    -    use crate::tests::cursor_test;
     
         use insta::assert_snapshot;
    -
    -    /// Helper function to get semantic tokens for full file (for testing)
    -    fn semantic_tokens_full_file(db: &dyn Db, file: File) -> SemanticTokens {
    -        semantic_tokens(db, file, None)
    -    }
    -
    -    /// Helper function to convert semantic tokens to a snapshot-friendly text format
    -    fn semantic_tokens_to_snapshot(db: &dyn Db, file: File, tokens: &SemanticTokens) -> String {
    -        use std::fmt::Write;
    -        let source = ruff_db::source::source_text(db, file);
    -        let mut result = String::new();
    -
    -        for token in tokens.iter() {
    -            let token_text = &source[token.range()];
    -            let modifiers_text = if token.modifiers.is_empty() {
    -                String::new()
    -            } else {
    -                let mut mods = Vec::new();
    -                if token.modifiers.contains(SemanticTokenModifier::DEFINITION) {
    -                    mods.push("definition");
    -                }
    -                if token.modifiers.contains(SemanticTokenModifier::READONLY) {
    -                    mods.push("readonly");
    -                }
    -                if token.modifiers.contains(SemanticTokenModifier::ASYNC) {
    -                    mods.push("async");
    -                }
    -                format!(" [{}]", mods.join(", "))
    -            };
    -
    -            writeln!(
    -                result,
    -                "{:?} @ {}..{}: {:?}{}",
    -                token_text,
    -                u32::from(token.range().start()),
    -                u32::from(token.range().end()),
    -                token.token_type,
    -                modifiers_text
    -            )
    -            .unwrap();
    -        }
    -
    -        result
    -    }
    +    use ruff_db::{
    +        files::system_path_to_file,
    +        system::{DbWithWritableSystem, SystemPath, SystemPathBuf},
    +    };
    +    use ty_project::ProjectMetadata;
     
         #[test]
         fn test_semantic_tokens_basic() {
    -        let test = cursor_test("def foo(): pass");
    +        let test = SemanticTokenTest::new("def foo(): pass");
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "foo" @ 4..7: Function [definition]
             "###);
         }
     
         #[test]
         fn test_semantic_tokens_class() {
    -        let test = cursor_test("class MyClass: pass");
    +        let test = SemanticTokenTest::new("class MyClass: pass");
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "MyClass" @ 6..13: Class [definition]
             "###);
         }
     
         #[test]
         fn test_semantic_tokens_variables() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     x = 42
    -y = 'hello'
    +y = 'hello'
     ",
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "x" @ 1..2: Variable
             "42" @ 5..7: Number
             "y" @ 8..9: Variable
    @@ -1021,16 +981,16 @@ y = 'hello'
     
         #[test]
         fn test_semantic_tokens_self_parameter() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     class MyClass:
    -    def method(self, x): pass
    +    def method(self, x): pass
     ",
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "MyClass" @ 7..14: Class [definition]
             "method" @ 24..30: Method [definition]
             "self" @ 31..35: SelfParameter
    @@ -1040,17 +1000,17 @@ class MyClass:
     
         #[test]
         fn test_semantic_tokens_cls_parameter() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     class MyClass:
         @classmethod
    -    def method(cls, x): pass
    +    def method(cls, x): pass
     ",
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "MyClass" @ 7..14: Class [definition]
             "classmethod" @ 21..32: Decorator
             "method" @ 41..47: Method [definition]
    @@ -1061,17 +1021,17 @@ class MyClass:
     
         #[test]
         fn test_semantic_tokens_staticmethod_parameter() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     class MyClass:
         @staticmethod
    -    def method(x, y): pass
    +    def method(x, y): pass
     ",
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "MyClass" @ 7..14: Class [definition]
             "staticmethod" @ 21..33: Decorator
             "method" @ 42..48: Method [definition]
    @@ -1082,19 +1042,19 @@ class MyClass:
     
         #[test]
         fn test_semantic_tokens_custom_self_cls_names() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     class MyClass:
         def method(instance, x): pass
         @classmethod
         def other(klass, y): pass
    -    def complex_method(instance, posonly, /, regular, *args, kwonly, **kwargs): pass
    +    def complex_method(instance, posonly, /, regular, *args, kwonly, **kwargs): pass
     ",
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "MyClass" @ 7..14: Class [definition]
             "method" @ 24..30: Method [definition]
             "instance" @ 31..39: SelfParameter
    @@ -1115,17 +1075,17 @@ class MyClass:
     
         #[test]
         fn test_semantic_tokens_modifiers() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     class MyClass:
         CONSTANT = 42
    -    async def method(self): pass
    +    async def method(self): pass
     ",
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "MyClass" @ 7..14: Class [definition]
             "CONSTANT" @ 20..28: Variable [readonly]
             "42" @ 31..33: Number
    @@ -1136,7 +1096,7 @@ class MyClass:
     
         #[test]
         fn test_semantic_classification_vs_heuristic() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     import sys
     class MyClass:
    @@ -1147,13 +1107,13 @@ def my_function():
     
     x = MyClass()
     y = my_function()
    -z = sys.version
    +z = sys.version
     ",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "sys" @ 8..11: Namespace
             "MyClass" @ 18..25: Class [definition]
             "my_function" @ 41..52: Function [definition]
    @@ -1170,17 +1130,17 @@ z = sys.version
     
         #[test]
         fn test_builtin_constants() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     x = True
     y = False
    -z = None
    +z = None
     ",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "x" @ 1..2: Variable
             "True" @ 5..9: BuiltinConstant
             "y" @ 10..11: Variable
    @@ -1192,20 +1152,20 @@ z = None
     
         #[test]
         fn test_builtin_constants_in_expressions() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     def check(value):
         if value is None:
             return False
         return True
     
    -result = check(None)
    +result = check(None)
     ",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "check" @ 5..10: Function [definition]
             "value" @ 11..16: Parameter
             "value" @ 26..31: Variable
    @@ -1220,7 +1180,7 @@ result = check(None)
     
         #[test]
         fn test_semantic_tokens_range() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     def function1():
         x = 42
    @@ -1229,46 +1189,46 @@ def function1():
     def function2():
         y = \"hello\"
         z = True
    -    return y + z
    +    return y + z
     ",
             );
     
    -        let full_tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let full_tokens = test.highlight_file();
     
             // Get the range that covers only the second function
             // Hardcoded offsets: function2 starts at position 42, source ends at position 108
             let range = TextRange::new(TextSize::from(42u32), TextSize::from(108u32));
     
    -        let range_tokens = semantic_tokens(&test.db, test.cursor.file, Some(range));
    +        let range_tokens = test.highlight_range(range);
     
             // Range-based tokens should have fewer tokens than full scan
             // (should exclude tokens from function1)
             assert!(range_tokens.len() < full_tokens.len());
     
             // Test both full tokens and range tokens with snapshots
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &full_tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&full_tokens), @r###"
             "function1" @ 5..14: Function [definition]
             "x" @ 22..23: Variable
             "42" @ 26..28: Number
             "x" @ 40..41: Variable
             "function2" @ 47..56: Function [definition]
             "y" @ 64..65: Variable
    -        "/"hello/"" @ 68..75: String
    +        "\"hello\"" @ 68..75: String
             "z" @ 80..81: Variable
             "True" @ 84..88: BuiltinConstant
             "y" @ 100..101: Variable
             "z" @ 104..105: Variable
    -        "#);
    +        "###);
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &range_tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&range_tokens), @r###"
             "function2" @ 47..56: Function [definition]
             "y" @ 64..65: Variable
    -        "/"hello/"" @ 68..75: String
    +        "\"hello\"" @ 68..75: String
             "z" @ 80..81: Variable
             "True" @ 84..88: BuiltinConstant
             "y" @ 100..101: Variable
             "z" @ 104..105: Variable
    -        "#);
    +        "###);
     
             // Verify that no tokens from range_tokens have ranges outside the requested range
             for token in range_tokens.iter() {
    @@ -1285,11 +1245,11 @@ def function2():
         /// don't include it in the semantic tokens.
         #[test]
         fn test_semantic_tokens_range_excludes_boundary_tokens() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     x = 1
     y = 2
    -z = 3
    +z = 3
     ",
             );
     
    @@ -1298,9 +1258,9 @@ z = 3
             // Not included: "1" @ 5..6 and "z" @ 13..14 (adjacent, but not overlapping at offsets 6 and 13).
             let range = TextRange::new(TextSize::from(6), TextSize::from(13));
     
    -        let range_tokens = semantic_tokens(&test.db, test.cursor.file, Some(range));
    +        let range_tokens = test.highlight_range(range);
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &range_tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&range_tokens), @r#"
             "y" @ 7..8: Variable
             "2" @ 11..12: Number
             "#);
    @@ -1308,18 +1268,18 @@ z = 3
     
         #[test]
         fn test_dotted_module_names() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     import os.path
     import sys.version_info
     from urllib.parse import urlparse
    -from collections.abc import Mapping
    +from collections.abc import Mapping
     ",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "os" @ 8..10: Namespace
             "path" @ 11..15: Namespace
             "sys" @ 23..26: Namespace
    @@ -1335,7 +1295,7 @@ from collections.abc import Mapping
     
         #[test]
         fn test_module_type_classification() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     import os
     import sys
    @@ -1343,13 +1303,13 @@ from collections import defaultdict
     
     # os and sys should be classified as namespace/module types
     x = os
    -y = sys
    +y = sys
     ",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "os" @ 8..10: Namespace
             "sys" @ 18..21: Namespace
             "collections" @ 27..38: Namespace
    @@ -1363,18 +1323,18 @@ y = sys
     
         #[test]
         fn test_import_classification() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     from os import path
     from collections import defaultdict, OrderedDict, Counter
     from typing import List, Dict, Optional
    -from mymodule import CONSTANT, my_function, MyClass
    +from mymodule import CONSTANT, my_function, MyClass
     ",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "os" @ 6..8: Namespace
             "path" @ 16..20: Namespace
             "collections" @ 26..37: Namespace
    @@ -1394,7 +1354,7 @@ from mymodule import CONSTANT, my_function, MyClass
     
         #[test]
         fn test_attribute_classification() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     import os
     import sys
    @@ -1403,10 +1363,10 @@ from typing import List
     
     class MyClass:
         CONSTANT = 42
    -    
    +
         def method(self):
             return \"hello\"
    -    
    +
         @property
         def prop(self):
             return self.CONSTANT
    @@ -1419,13 +1379,13 @@ y = obj.method           # method should be method (bound method)
     z = obj.CONSTANT         # CONSTANT should be variable with readonly modifier
     w = obj.prop             # prop should be property
     v = MyClass.method       # method should be method (function)
    -u = List.__name__        # __name__ should be variable
    +u = List.__name__        # __name__ should be variable
     ",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "os" @ 8..10: Namespace
             "sys" @ 18..21: Namespace
             "collections" @ 27..38: Namespace
    @@ -1437,7 +1397,7 @@ u = List.__name__        # __name__ should be variable
             "42" @ 113..115: Number
             "method" @ 125..131: Method [definition]
             "self" @ 132..136: SelfParameter
    -        "/"hello/"" @ 154..161: String
    +        "\"hello\"" @ 154..161: String
             "property" @ 168..176: Decorator
             "prop" @ 185..189: Method [definition]
             "self" @ 190..194: SelfParameter
    @@ -1463,29 +1423,29 @@ u = List.__name__        # __name__ should be variable
             "u" @ 596..597: Variable
             "List" @ 600..604: Variable
             "__name__" @ 605..613: Variable
    -        "#);
    +        "###);
         }
     
         #[test]
         fn test_attribute_fallback_classification() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     class MyClass:
         some_attr = \"value\"
    -    
    +
     obj = MyClass()
     # Test attribute that might not have detailed semantic info
     x = obj.some_attr        # Should fall back to variable, not property
    -y = obj.unknown_attr     # Should fall back to variable
    +y = obj.unknown_attr     # Should fall back to variable
     ",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "MyClass" @ 7..14: Class [definition]
             "some_attr" @ 20..29: Variable
    -        "/"value/"" @ 32..39: String
    +        "\"value\"" @ 32..39: String
             "obj" @ 41..44: Variable
             "MyClass" @ 47..54: Class
             "x" @ 117..118: Variable
    @@ -1494,30 +1454,30 @@ y = obj.unknown_attr     # Should fall back to variable
             "y" @ 187..188: Variable
             "obj" @ 191..194: Variable
             "unknown_attr" @ 195..207: Variable
    -        "#);
    +        "###);
         }
     
         #[test]
         fn test_constant_name_detection() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     class MyClass:
         UPPER_CASE = 42
         lower_case = 24
         MixedCase = 12
         A = 1
    -    
    +
     obj = MyClass()
     x = obj.UPPER_CASE    # Should have readonly modifier
    -y = obj.lower_case    # Should not have readonly modifier  
    +y = obj.lower_case    # Should not have readonly modifier
     z = obj.MixedCase     # Should not have readonly modifier
    -w = obj.A             # Should not have readonly modifier (length == 1)
    +w = obj.A             # Should not have readonly modifier (length == 1)
     ",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "MyClass" @ 7..14: Class [definition]
             "UPPER_CASE" @ 20..30: Variable [readonly]
             "42" @ 33..35: Number
    @@ -1535,18 +1495,18 @@ w = obj.A             # Should not have readonly modifier (length == 1)
             "y" @ 156..157: Variable
             "obj" @ 160..163: Variable
             "lower_case" @ 164..174: Variable
    -        "z" @ 216..217: Variable
    -        "obj" @ 220..223: Variable
    -        "MixedCase" @ 224..233: Variable
    -        "w" @ 274..275: Variable
    -        "obj" @ 278..281: Variable
    -        "A" @ 282..283: Variable
    -        "#);
    +        "z" @ 214..215: Variable
    +        "obj" @ 218..221: Variable
    +        "MixedCase" @ 222..231: Variable
    +        "w" @ 272..273: Variable
    +        "obj" @ 276..279: Variable
    +        "A" @ 280..281: Variable
    +        "###);
         }
     
         #[test]
         fn test_type_annotations() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 r#"
     from typing import List, Optional
     
    @@ -1554,13 +1514,13 @@ def function_with_annotations(param1: int, param2: str) -> Optional[List[str]]:
         pass
     
     x: int = 42
    -y: Optional[str] = None
    +y: Optional[str] = None
     "#,
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "typing" @ 6..12: Namespace
             "List" @ 20..24: Variable
             "Optional" @ 26..34: Variable
    @@ -1584,15 +1544,15 @@ y: Optional[str] = None
     
         #[test]
         fn test_debug_int_classification() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
    -x: int = 42
    +x: int = 42
     ",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "x" @ 1..2: Variable
             "int" @ 4..7: Class
             "42" @ 10..12: Number
    @@ -1601,18 +1561,18 @@ x: int = 42
     
         #[test]
         fn test_debug_user_defined_type_classification() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     class MyClass:
         pass
     
    -x: MyClass = MyClass()
    +x: MyClass = MyClass()
     ",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "MyClass" @ 7..14: Class [definition]
             "x" @ 26..27: Variable
             "MyClass" @ 29..36: Class
    @@ -1622,7 +1582,7 @@ x: MyClass = MyClass()
     
         #[test]
         fn test_type_annotation_vs_variable_classification() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     from typing import List, Optional
     
    @@ -1634,16 +1594,16 @@ def test_function(param: int, other: MyClass) -> Optional[List[str]]:
         x: int = 42
         y: MyClass = MyClass()
         z: List[str] = [\"hello\"]
    -    
    +
         # Type annotations should be Class tokens:
         # int, MyClass, Optional, List, str
    -    return None
    +    return None
     ",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "typing" @ 6..12: Namespace
             "List" @ 20..24: Variable
             "Optional" @ 26..34: Variable
    @@ -1665,14 +1625,14 @@ def test_function(param: int, other: MyClass) -> Optional[List[str]]:
             "z" @ 233..234: Variable
             "List" @ 236..240: Variable
             "str" @ 241..244: Class
    -        "/"hello/"" @ 249..256: String
    +        "\"hello\"" @ 249..256: String
             "None" @ 357..361: BuiltinConstant
    -        "#);
    +        "###);
         }
     
         #[test]
         fn test_protocol_types_in_annotations() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     from typing import Protocol
     
    @@ -1681,12 +1641,12 @@ class MyProtocol(Protocol):
     
     def test_function(param: MyProtocol) -> None:
         pass
    -",
    +",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "typing" @ 6..12: Namespace
             "Protocol" @ 20..28: Variable
             "MyProtocol" @ 36..46: Class [definition]
    @@ -1703,7 +1663,7 @@ def test_function(param: MyProtocol) -> None:
     
         #[test]
         fn test_protocol_type_annotation_vs_value_context() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     from typing import Protocol
     
    @@ -1713,15 +1673,15 @@ class MyProtocol(Protocol):
     # Value context - MyProtocol is still a class literal, so should be Class
     my_protocol_var = MyProtocol
     
    -# Type annotation context - should be Class  
    +# Type annotation context - should be Class
     def test_function(param: MyProtocol) -> MyProtocol:
         return param
    -",
    +",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "typing" @ 6..12: Namespace
             "Protocol" @ 20..28: Variable
             "MyProtocol" @ 36..46: Class [definition]
    @@ -1731,17 +1691,17 @@ def test_function(param: MyProtocol) -> MyProtocol:
             "int" @ 82..85: Class
             "my_protocol_var" @ 166..181: Class
             "MyProtocol" @ 184..194: Class
    -        "test_function" @ 246..259: Function [definition]
    -        "param" @ 260..265: Parameter
    -        "MyProtocol" @ 267..277: Class
    -        "MyProtocol" @ 282..292: Class
    -        "param" @ 305..310: Parameter
    -        "#);
    +        "test_function" @ 244..257: Function [definition]
    +        "param" @ 258..263: Parameter
    +        "MyProtocol" @ 265..275: Class
    +        "MyProtocol" @ 280..290: Class
    +        "param" @ 303..308: Parameter
    +        "###);
         }
     
         #[test]
         fn test_type_parameters_pep695() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     # Test Python 3.12 PEP 695 type parameter syntax
     
    @@ -1749,7 +1709,7 @@ def test_function(param: MyProtocol) -> MyProtocol:
     def func[T](x: T) -> T:
         return x
     
    -# Generic function with TypeVarTuple  
    +# Generic function with TypeVarTuple
     def func_tuple[*Ts](args: tuple[*Ts]) -> tuple[*Ts]:
         return args
     
    @@ -1764,10 +1724,10 @@ class Container[T, U]:
         def __init__(self, value1: T, value2: U):
             self.value1: T = value1
             self.value2: U = value2
    -    
    +
         def get_first(self) -> T:
             return self.value1
    -    
    +
         def get_second(self) -> U:
             return self.value2
     
    @@ -1775,109 +1735,109 @@ class Container[T, U]:
     class BoundedContainer[T: int, U = str]:
         def process(self, x: T, y: U) -> tuple[T, U]:
             return (x, y)
    -",
    +",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "func" @ 87..91: Function [definition]
             "T" @ 92..93: TypeParameter [definition]
             "x" @ 95..96: Parameter
             "T" @ 98..99: TypeParameter
             "T" @ 104..105: TypeParameter
             "x" @ 118..119: Parameter
    -        "func_tuple" @ 164..174: Function [definition]
    -        "Ts" @ 176..178: TypeParameter [definition]
    -        "args" @ 180..184: Parameter
    -        "tuple" @ 186..191: Class
    -        "Ts" @ 193..195: Variable
    -        "tuple" @ 201..206: Class
    -        "Ts" @ 208..210: Variable
    -        "args" @ 224..228: Parameter
    -        "func_paramspec" @ 268..282: Function [definition]
    -        "P" @ 285..286: TypeParameter [definition]
    -        "func" @ 288..292: Parameter
    -        "Callable" @ 294..302: Variable
    -        "P" @ 303..304: Variable
    -        "int" @ 306..309: Class
    -        "Callable" @ 315..323: Variable
    -        "P" @ 324..325: Variable
    -        "str" @ 327..330: Class
    -        "wrapper" @ 341..348: Function [definition]
    -        "args" @ 350..354: Parameter
    -        "P" @ 356..357: Variable
    -        "args" @ 358..362: Variable
    -        "kwargs" @ 366..372: Parameter
    -        "P" @ 374..375: Variable
    -        "kwargs" @ 376..382: Variable
    -        "str" @ 387..390: Class
    -        "str" @ 407..410: Class
    -        "func" @ 411..415: Variable
    -        "args" @ 417..421: Parameter
    -        "kwargs" @ 425..431: Parameter
    -        "wrapper" @ 445..452: Function
    -        "Container" @ 506..515: Class [definition]
    -        "T" @ 516..517: TypeParameter [definition]
    -        "U" @ 519..520: TypeParameter [definition]
    -        "__init__" @ 531..539: Method [definition]
    -        "self" @ 540..544: SelfParameter
    -        "value1" @ 546..552: Parameter
    -        "T" @ 554..555: TypeParameter
    -        "value2" @ 557..563: Parameter
    -        "U" @ 565..566: TypeParameter
    -        "self" @ 577..581: TypeParameter
    -        "value1" @ 582..588: Variable
    -        "T" @ 590..591: TypeParameter
    -        "value1" @ 594..600: Parameter
    -        "self" @ 609..613: TypeParameter
    -        "value2" @ 614..620: Variable
    -        "U" @ 622..623: TypeParameter
    -        "value2" @ 626..632: Parameter
    -        "get_first" @ 642..651: Method [definition]
    -        "self" @ 652..656: SelfParameter
    -        "T" @ 661..662: TypeParameter
    -        "self" @ 679..683: TypeParameter
    -        "value1" @ 684..690: Variable
    -        "get_second" @ 700..710: Method [definition]
    -        "self" @ 711..715: SelfParameter
    -        "U" @ 720..721: TypeParameter
    -        "self" @ 738..742: TypeParameter
    -        "value2" @ 743..749: Variable
    -        "BoundedContainer" @ 798..814: Class [definition]
    -        "T" @ 815..816: TypeParameter [definition]
    -        "int" @ 818..821: Class
    -        "U" @ 823..824: TypeParameter [definition]
    -        "str" @ 827..830: Class
    -        "process" @ 841..848: Method [definition]
    -        "self" @ 849..853: SelfParameter
    -        "x" @ 855..856: Parameter
    -        "T" @ 858..859: TypeParameter
    -        "y" @ 861..862: Parameter
    -        "U" @ 864..865: TypeParameter
    -        "tuple" @ 870..875: Class
    -        "T" @ 876..877: TypeParameter
    -        "U" @ 879..880: TypeParameter
    -        "x" @ 899..900: Parameter
    -        "y" @ 902..903: Parameter
    -        "#);
    +        "func_tuple" @ 162..172: Function [definition]
    +        "Ts" @ 174..176: TypeParameter [definition]
    +        "args" @ 178..182: Parameter
    +        "tuple" @ 184..189: Class
    +        "Ts" @ 191..193: Variable
    +        "tuple" @ 199..204: Class
    +        "Ts" @ 206..208: Variable
    +        "args" @ 222..226: Parameter
    +        "func_paramspec" @ 266..280: Function [definition]
    +        "P" @ 283..284: TypeParameter [definition]
    +        "func" @ 286..290: Parameter
    +        "Callable" @ 292..300: Variable
    +        "P" @ 301..302: Variable
    +        "int" @ 304..307: Class
    +        "Callable" @ 313..321: Variable
    +        "P" @ 322..323: Variable
    +        "str" @ 325..328: Class
    +        "wrapper" @ 339..346: Function [definition]
    +        "args" @ 348..352: Parameter
    +        "P" @ 354..355: Variable
    +        "args" @ 356..360: Variable
    +        "kwargs" @ 364..370: Parameter
    +        "P" @ 372..373: Variable
    +        "kwargs" @ 374..380: Variable
    +        "str" @ 385..388: Class
    +        "str" @ 405..408: Class
    +        "func" @ 409..413: Variable
    +        "args" @ 415..419: Parameter
    +        "kwargs" @ 423..429: Parameter
    +        "wrapper" @ 443..450: Function
    +        "Container" @ 504..513: Class [definition]
    +        "T" @ 514..515: TypeParameter [definition]
    +        "U" @ 517..518: TypeParameter [definition]
    +        "__init__" @ 529..537: Method [definition]
    +        "self" @ 538..542: SelfParameter
    +        "value1" @ 544..550: Parameter
    +        "T" @ 552..553: TypeParameter
    +        "value2" @ 555..561: Parameter
    +        "U" @ 563..564: TypeParameter
    +        "self" @ 575..579: TypeParameter
    +        "value1" @ 580..586: Variable
    +        "T" @ 588..589: TypeParameter
    +        "value1" @ 592..598: Parameter
    +        "self" @ 607..611: TypeParameter
    +        "value2" @ 612..618: Variable
    +        "U" @ 620..621: TypeParameter
    +        "value2" @ 624..630: Parameter
    +        "get_first" @ 640..649: Method [definition]
    +        "self" @ 650..654: SelfParameter
    +        "T" @ 659..660: TypeParameter
    +        "self" @ 677..681: TypeParameter
    +        "value1" @ 682..688: Variable
    +        "get_second" @ 698..708: Method [definition]
    +        "self" @ 709..713: SelfParameter
    +        "U" @ 718..719: TypeParameter
    +        "self" @ 736..740: TypeParameter
    +        "value2" @ 741..747: Variable
    +        "BoundedContainer" @ 796..812: Class [definition]
    +        "T" @ 813..814: TypeParameter [definition]
    +        "int" @ 816..819: Class
    +        "U" @ 821..822: TypeParameter [definition]
    +        "str" @ 825..828: Class
    +        "process" @ 839..846: Method [definition]
    +        "self" @ 847..851: SelfParameter
    +        "x" @ 853..854: Parameter
    +        "T" @ 856..857: TypeParameter
    +        "y" @ 859..860: Parameter
    +        "U" @ 862..863: TypeParameter
    +        "tuple" @ 868..873: Class
    +        "T" @ 874..875: TypeParameter
    +        "U" @ 877..878: TypeParameter
    +        "x" @ 897..898: Parameter
    +        "y" @ 900..901: Parameter
    +        "###);
         }
     
         #[test]
         fn test_type_parameters_usage_in_function_body() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 "
     def generic_function[T](value: T) -> T:
         # Type parameter T should be recognized here too
         result: T = value
         temp = result  # This could potentially be T as well
         return result
    -",
    +",
             );
     
    -        let tokens = semantic_tokens(&test.db, test.cursor.file, None);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "generic_function" @ 5..21: Function [definition]
             "T" @ 22..23: TypeParameter [definition]
             "value" @ 25..30: Parameter
    @@ -1894,7 +1854,7 @@ def generic_function[T](value: T) -> T:
     
         #[test]
         fn test_decorator_classification() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 r#"
     @staticmethod
     @property
    @@ -1904,117 +1864,117 @@ def my_function():
     
     @dataclass
     class MyClass:
    -    pass
    +    pass
     "#,
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "staticmethod" @ 2..14: Decorator
             "property" @ 16..24: Decorator
             "app" @ 26..29: Variable
             "route" @ 30..35: Variable
    -        "/"/path/"" @ 36..43: String
    +        "\"/path\"" @ 36..43: String
             "my_function" @ 49..60: Function [definition]
             "dataclass" @ 75..84: Decorator
             "MyClass" @ 91..98: Class [definition]
    -        "#);
    +        "###);
         }
     
         #[test]
         fn test_implicitly_concatenated_strings() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 r#"x = "hello" "world"
    -y = ("multi" 
    -     "line" 
    +y = ("multi"
    +     "line"
          "string")
    -z = 'single' "mixed" 'quotes'"#,
    +z = 'single' "mixed" 'quotes'"#,
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "x" @ 0..1: Variable
    -        "/"hello/"" @ 4..11: String
    -        "/"world/"" @ 12..19: String
    +        "\"hello\"" @ 4..11: String
    +        "\"world\"" @ 12..19: String
             "y" @ 20..21: Variable
    -        "/"multi/"" @ 25..32: String
    -        "/"line/"" @ 39..45: String
    -        "/"string/"" @ 52..60: String
    -        "z" @ 62..63: Variable
    -        "'single'" @ 66..74: String
    -        "/"mixed/"" @ 75..82: String
    -        "'quotes'" @ 83..91: String
    -        "#);
    +        "\"multi\"" @ 25..32: String
    +        "\"line\"" @ 38..44: String
    +        "\"string\"" @ 50..58: String
    +        "z" @ 60..61: Variable
    +        "'single'" @ 64..72: String
    +        "\"mixed\"" @ 73..80: String
    +        "'quotes'" @ 81..89: String
    +        "###);
         }
     
         #[test]
         fn test_bytes_literals() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 r#"x = b"hello" b"world"
    -y = (b"multi" 
    -     b"line" 
    +y = (b"multi"
    +     b"line"
          b"bytes")
    -z = b'single' b"mixed" b'quotes'"#,
    +z = b'single' b"mixed" b'quotes'"#,
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "x" @ 0..1: Variable
    -        "b/"hello/"" @ 4..12: String
    -        "b/"world/"" @ 13..21: String
    +        "b\"hello\"" @ 4..12: String
    +        "b\"world\"" @ 13..21: String
             "y" @ 22..23: Variable
    -        "b/"multi/"" @ 27..35: String
    -        "b/"line/"" @ 42..49: String
    -        "b/"bytes/"" @ 56..64: String
    -        "z" @ 66..67: Variable
    -        "b'single'" @ 70..79: String
    -        "b/"mixed/"" @ 80..88: String
    -        "b'quotes'" @ 89..98: String
    -        "#);
    +        "b\"multi\"" @ 27..35: String
    +        "b\"line\"" @ 41..48: String
    +        "b\"bytes\"" @ 54..62: String
    +        "z" @ 64..65: Variable
    +        "b'single'" @ 68..77: String
    +        "b\"mixed\"" @ 78..86: String
    +        "b'quotes'" @ 87..96: String
    +        "###);
         }
     
         #[test]
         fn test_mixed_string_and_bytes_literals() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 r#"# Test mixed string and bytes literals
     string_concat = "hello" "world"
     bytes_concat = b"hello" b"world"
     mixed_quotes_str = 'single' "double" 'single'
     mixed_quotes_bytes = b'single' b"double" b'single'
     regular_string = "just a string"
    -regular_bytes = b"just bytes""#,
    +regular_bytes = b"just bytes""#,
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "string_concat" @ 39..52: Variable
    -        "/"hello/"" @ 55..62: String
    -        "/"world/"" @ 63..70: String
    +        "\"hello\"" @ 55..62: String
    +        "\"world\"" @ 63..70: String
             "bytes_concat" @ 71..83: Variable
    -        "b/"hello/"" @ 86..94: String
    -        "b/"world/"" @ 95..103: String
    +        "b\"hello\"" @ 86..94: String
    +        "b\"world\"" @ 95..103: String
             "mixed_quotes_str" @ 104..120: Variable
             "'single'" @ 123..131: String
    -        "/"double/"" @ 132..140: String
    +        "\"double\"" @ 132..140: String
             "'single'" @ 141..149: String
             "mixed_quotes_bytes" @ 150..168: Variable
             "b'single'" @ 171..180: String
    -        "b/"double/"" @ 181..190: String
    +        "b\"double\"" @ 181..190: String
             "b'single'" @ 191..200: String
             "regular_string" @ 201..215: Variable
    -        "/"just a string/"" @ 218..233: String
    +        "\"just a string\"" @ 218..233: String
             "regular_bytes" @ 234..247: Variable
    -        "b/"just bytes/"" @ 250..263: String
    -        "#);
    +        "b\"just bytes\"" @ 250..263: String
    +        "###);
         }
     
         #[test]
         fn test_fstring_with_mixed_literals() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 r#"
     # Test f-strings with various literal types
     name = "Alice"
    @@ -2028,17 +1988,17 @@ result = f"Hello {name}! Value: {value}, Data: {data!r}"
     mixed = f"prefix" + b"suffix"
     
     # Complex f-string with nested expressions
    -complex_fstring = f"User: {name.upper()}, Count: {len(data)}, Hex: {value:x}"
    +complex_fstring = f"User: {name.upper()}, Count: {len(data)}, Hex: {value:x}"
     "#,
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "name" @ 45..49: Variable
    -        "/"Alice/"" @ 52..59: String
    +        "\"Alice\"" @ 52..59: String
             "data" @ 60..64: Variable
    -        "b/"hello/"" @ 67..75: String
    +        "b\"hello\"" @ 67..75: String
             "value" @ 76..81: Variable
             "42" @ 84..86: Number
             "result" @ 153..159: Variable
    @@ -2050,7 +2010,7 @@ complex_fstring = f"User: {name.upper()}, Count: {len(data)}, Hex: {value:x}"
    +
    +    return inner
     "#,
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "x" @ 1..2: Variable
    -        "/"global_value/"" @ 5..19: String
    +        "\"global_value\"" @ 5..19: String
             "y" @ 20..21: Variable
    -        "/"another_global/"" @ 24..40: String
    +        "\"another_global\"" @ 24..40: String
             "outer" @ 46..51: Function [definition]
             "x" @ 59..60: Variable
    -        "/"outer_value/"" @ 63..76: String
    +        "\"outer_value\"" @ 63..76: String
             "z" @ 81..82: Variable
    -        "/"outer_local/"" @ 85..98: String
    +        "\"outer_local\"" @ 85..98: String
             "inner" @ 108..113: Function [definition]
             "x" @ 134..135: Variable
             "z" @ 137..138: Variable
             "y" @ 189..190: Variable
             "x" @ 239..240: Variable
    -        "/"modified/"" @ 243..253: String
    +        "\"modified\"" @ 243..253: String
             "y" @ 262..263: Variable
    -        "/"modified_global/"" @ 266..283: String
    +        "\"modified_global\"" @ 266..283: String
             "z" @ 292..293: Variable
    -        "/"modified_local/"" @ 296..312: String
    +        "\"modified_local\"" @ 296..312: String
             "deeper" @ 326..332: Function [definition]
             "x" @ 357..358: Variable
             "y" @ 398..399: Variable
    @@ -2123,29 +2083,29 @@ def outer():
             "y" @ 461..462: Variable
             "deeper" @ 479..485: Function
             "inner" @ 498..503: Function
    -        "#);
    +        "###);
         }
     
         #[test]
         fn test_nonlocal_global_edge_cases() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 r#"
     # Single variable statements
     def test():
         global x
         nonlocal y
    -    
    +
         # Multiple variables in one statement
         global a, b, c
         nonlocal d, e, f
    -    
    -    return x + y + a + b + c + d + e + f
    +
    +    return x + y + a + b + c + d + e + f
     "#,
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "test" @ 34..38: Function [definition]
             "x" @ 53..54: Variable
             "y" @ 68..69: Variable
    @@ -2168,7 +2128,7 @@ def test():
     
         #[test]
         fn test_pattern_matching() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 r#"
     def process_data(data):
         match data:
    @@ -2180,19 +2140,19 @@ def process_data(data):
                 return sequence
             case value as fallback:
                 print(f"Fallback: {fallback}")
    -            return fallback
    +            return fallback
     "#,
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r###"
             "process_data" @ 5..17: Function [definition]
             "data" @ 18..22: Parameter
             "data" @ 35..39: Variable
    -        "/"name/"" @ 55..61: String
    +        "\"name\"" @ 55..61: String
             "name" @ 63..67: Variable
    -        "/"age/"" @ 69..74: String
    +        "\"age\"" @ 69..74: String
             "age" @ 76..79: Variable
             "rest" @ 83..87: Variable
             "person" @ 92..98: Variable
    @@ -2218,12 +2178,12 @@ def process_data(data):
             "Fallback: " @ 375..385: String
             "fallback" @ 386..394: Variable
             "fallback" @ 417..425: Variable
    -        "#);
    +        "###);
         }
     
         #[test]
         fn test_exception_handlers() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 r#"
     try:
         x = 1 / 0
    @@ -2234,13 +2194,13 @@ except (TypeError, RuntimeError) as re:
     except Exception as e:
         print(e)
     finally:
    -    pass
    +    pass
     "#,
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "x" @ 10..11: Variable
             "1" @ 14..15: Number
             "0" @ 18..19: Number
    @@ -2262,7 +2222,7 @@ finally:
     
         #[test]
         fn test_self_attribute_expression() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 r#"
     from typing import Self
     
    @@ -2272,15 +2232,13 @@ class C:
             self.annotated: int = 1
             self.non_annotated = 1
             self.x.test()
    -        self.x()
    -
    -
    +        self.x()
     "#,
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "typing" @ 6..12: Namespace
             "Self" @ 20..24: Variable
             "C" @ 33..34: Class [definition]
    @@ -2305,16 +2263,16 @@ class C:
         /// Regression test for 
         #[test]
         fn test_invalid_kwargs() {
    -        let test = cursor_test(
    +        let test = SemanticTokenTest::new(
                 r#"
    -def foo(self, **key, value=10):
    +def foo(self, **key, value=10):
         return
     "#,
             );
     
    -        let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
    +        let tokens = test.highlight_file();
     
    -        assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
    +        assert_snapshot!(test.to_snapshot(&tokens), @r#"
             "foo" @ 5..8: Function [definition]
             "self" @ 9..13: Parameter
             "key" @ 17..20: Parameter
    @@ -2322,4 +2280,77 @@ def foo(self, **key, value=10):
             "10" @ 28..30: Number
             "#);
         }
    +
    +    pub(super) struct SemanticTokenTest {
    +        pub(super) db: ty_project::TestDb,
    +        file: File,
    +    }
    +
    +    impl SemanticTokenTest {
    +        fn new(source: &str) -> Self {
    +            let mut db = ty_project::TestDb::new(ProjectMetadata::new(
    +                "test".into(),
    +                SystemPathBuf::from("/"),
    +            ));
    +
    +            db.init_program().unwrap();
    +
    +            let path = SystemPath::new("src/main.py");
    +            db.write_file(path, ruff_python_trivia::textwrap::dedent(source))
    +                .expect("Write to memory file system to always succeed");
    +
    +            let file = system_path_to_file(&db, path).expect("newly written file to existing");
    +
    +            Self { db, file }
    +        }
    +
    +        /// Get semantic tokens for the entire file
    +        fn highlight_file(&self) -> SemanticTokens {
    +            semantic_tokens(&self.db, self.file, None)
    +        }
    +
    +        /// Get semantic tokens for a specific range in the file
    +        fn highlight_range(&self, range: TextRange) -> SemanticTokens {
    +            semantic_tokens(&self.db, self.file, Some(range))
    +        }
    +
    +        /// Helper function to convert semantic tokens to a snapshot-friendly text format
    +        fn to_snapshot(&self, tokens: &SemanticTokens) -> String {
    +            use std::fmt::Write;
    +            let source = ruff_db::source::source_text(&self.db, self.file);
    +            let mut result = String::new();
    +
    +            for token in tokens.iter() {
    +                let token_text = &source[token.range()];
    +                let modifiers_text = if token.modifiers.is_empty() {
    +                    String::new()
    +                } else {
    +                    let mut mods = Vec::new();
    +                    if token.modifiers.contains(SemanticTokenModifier::DEFINITION) {
    +                        mods.push("definition");
    +                    }
    +                    if token.modifiers.contains(SemanticTokenModifier::READONLY) {
    +                        mods.push("readonly");
    +                    }
    +                    if token.modifiers.contains(SemanticTokenModifier::ASYNC) {
    +                        mods.push("async");
    +                    }
    +                    format!(" [{}]", mods.join(", "))
    +                };
    +
    +                writeln!(
    +                    result,
    +                    "{:?} @ {}..{}: {:?}{}",
    +                    token_text,
    +                    u32::from(token.start()),
    +                    u32::from(token.end()),
    +                    token.token_type,
    +                    modifiers_text
    +                )
    +                .unwrap();
    +            }
    +
    +            result
    +        }
    +    }
     }
    diff --git a/crates/ty_project/src/db.rs b/crates/ty_project/src/db.rs
    index 5e2105839e..8f6ec20c95 100644
    --- a/crates/ty_project/src/db.rs
    +++ b/crates/ty_project/src/db.rs
    @@ -516,11 +516,13 @@ pub(crate) mod tests {
         use std::sync::{Arc, Mutex};
     
         use ruff_db::Db as SourceDb;
    -    use ruff_db::files::Files;
    +    use ruff_db::files::{FileRootKind, Files};
         use ruff_db::system::{DbWithTestSystem, System, TestSystem};
         use ruff_db::vendored::VendoredFileSystem;
    -    use ty_python_semantic::Program;
         use ty_python_semantic::lint::{LintRegistry, RuleSelection};
    +    use ty_python_semantic::{
    +        Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
    +    };
     
         use crate::db::Db;
         use crate::{Project, ProjectMetadata};
    @@ -560,6 +562,27 @@ pub(crate) mod tests {
                 db.project = Some(project);
                 db
             }
    +
    +        pub fn init_program(&mut self) -> anyhow::Result<()> {
    +            let root = self.project().root(self);
    +
    +            let search_paths = SearchPathSettings::new(vec![root.to_path_buf()])
    +                .to_search_paths(self.system(), self.vendored())
    +                .expect("Valid search path settings");
    +
    +            Program::from_settings(
    +                self,
    +                ProgramSettings {
    +                    python_version: PythonVersionWithSource::default(),
    +                    python_platform: PythonPlatform::default(),
    +                    search_paths,
    +                },
    +            );
    +
    +            self.files().try_add_root(self, root, FileRootKind::Project);
    +
    +            Ok(())
    +        }
         }
     
         impl TestDb {
    diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs
    index d47476c7dc..4c7688d47f 100644
    --- a/crates/ty_project/src/lib.rs
    +++ b/crates/ty_project/src/lib.rs
    @@ -751,34 +751,20 @@ mod tests {
         use crate::ProjectMetadata;
         use crate::check_file_impl;
         use crate::db::tests::TestDb;
    -    use ruff_db::Db as _;
         use ruff_db::files::system_path_to_file;
         use ruff_db::source::source_text;
         use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
         use ruff_db::testing::assert_function_query_was_not_run;
         use ruff_python_ast::name::Name;
         use ty_python_semantic::types::check_types;
    -    use ty_python_semantic::{
    -        Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
    -    };
     
         #[test]
         fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
             let project = ProjectMetadata::new(Name::new_static("test"), SystemPathBuf::from("/"));
             let mut db = TestDb::new(project);
    +        db.init_program().unwrap();
             let path = SystemPath::new("test.py");
     
    -        Program::from_settings(
    -            &db,
    -            ProgramSettings {
    -                python_version: PythonVersionWithSource::default(),
    -                python_platform: PythonPlatform::default(),
    -                search_paths: SearchPathSettings::new(vec![SystemPathBuf::from(".")])
    -                    .to_search_paths(db.system(), db.vendored())
    -                    .expect("Valid search path settings"),
    -            },
    -        );
    -
             db.write_file(path, "x = 10")?;
             let file = system_path_to_file(&db, path).unwrap();
     
    
    From 39f105bc4ac71ec37372c7fc535ae034fafa23ed Mon Sep 17 00:00:00 2001
    From: Alex Waygood 
    Date: Mon, 3 Nov 2025 10:38:20 -0500
    Subject: [PATCH 158/188] [ty] Use "cannot" consistently over "can not"
     (#21255)
    
    ---
     crates/ruff_formatter/src/format_element.rs               | 2 +-
     crates/ruff_python_formatter/src/pattern/mod.rs           | 2 +-
     .../resources/mdtest/annotations/any.md                   | 2 +-
     crates/ty_python_semantic/resources/mdtest/attributes.md  | 8 ++++----
     crates/ty_python_semantic/resources/mdtest/call/dunder.md | 2 +-
     .../resources/mdtest/call/getattr_static.md               | 2 +-
     .../ty_python_semantic/resources/mdtest/call/methods.md   | 2 +-
     .../resources/mdtest/dataclasses/dataclasses.md           | 2 +-
     .../resources/mdtest/expression/yield_and_yield_from.md   | 2 +-
     .../resources/mdtest/ide_support/all_members.md           | 2 +-
     .../resources/mdtest/intersection_types.md                | 2 +-
     .../ty_python_semantic/resources/mdtest/ty_extensions.md  | 2 +-
     .../resources/mdtest/type_compendium/any.md               | 4 ++--
     .../resources/mdtest/type_compendium/integer_literals.md  | 6 +++---
     .../resources/mdtest/type_properties/is_disjoint_from.md  | 2 +-
     crates/ty_python_semantic/resources/mdtest/typed_dict.md  | 6 +++---
     crates/ty_python_semantic/resources/mdtest/unreachable.md | 4 ++--
     crates/ty_python_semantic/src/place.rs                    | 2 +-
     .../src/semantic_index/reachability_constraints.rs        | 8 ++++----
     crates/ty_python_semantic/src/subscript.rs                | 2 +-
     crates/ty_python_semantic/src/types.rs                    | 6 +++---
     crates/ty_python_semantic/src/types/diagnostic.rs         | 2 +-
     crates/ty_python_semantic/src/types/generics.rs           | 2 +-
     crates/ty_python_semantic/src/types/infer/builder.rs      | 4 ++--
     24 files changed, 39 insertions(+), 39 deletions(-)
    
    diff --git a/crates/ruff_formatter/src/format_element.rs b/crates/ruff_formatter/src/format_element.rs
    index 529992c642..715eeb3cfd 100644
    --- a/crates/ruff_formatter/src/format_element.rs
    +++ b/crates/ruff_formatter/src/format_element.rs
    @@ -487,7 +487,7 @@ pub trait FormatElements {
     /// Represents the width by adding 1 to the actual width so that the width can be represented by a [`NonZeroU32`],
     /// allowing [`TextWidth`] or [`Option`] fit in 4 bytes rather than 8.
     ///
    -/// This means that 2^32 can not be precisely represented and instead has the same value as 2^32-1.
    +/// This means that 2^32 cannot be precisely represented and instead has the same value as 2^32-1.
     /// This imprecision shouldn't matter in practice because either text are longer than any configured line width
     /// and thus, the text should break.
     #[derive(Copy, Clone, Debug, Eq, PartialEq)]
    diff --git a/crates/ruff_python_formatter/src/pattern/mod.rs b/crates/ruff_python_formatter/src/pattern/mod.rs
    index e255d59359..557337ddc5 100644
    --- a/crates/ruff_python_formatter/src/pattern/mod.rs
    +++ b/crates/ruff_python_formatter/src/pattern/mod.rs
    @@ -299,7 +299,7 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> {
                     }
     
                     // `case 4+3j:` or `case 4-3j:
    -                // Can not contain arbitrary expressions. Limited to complex numbers.
    +                // Cannot contain arbitrary expressions. Limited to complex numbers.
                     Expr::BinOp(_) => {
                         self.update_max_precedence(OperatorPrecedence::Additive, 1);
                     }
    diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/any.md b/crates/ty_python_semantic/resources/mdtest/annotations/any.md
    index d6baf8cbf9..c2cc2d2461 100644
    --- a/crates/ty_python_semantic/resources/mdtest/annotations/any.md
    +++ b/crates/ty_python_semantic/resources/mdtest/annotations/any.md
    @@ -118,7 +118,7 @@ def takes_other_protocol(f: OtherProtocol): ...
     takes_other_protocol(SubclassOfAny())
     ```
     
    -A subclass of `Any` cannot be assigned to literal types, since those can not be subclassed:
    +A subclass of `Any` cannot be assigned to literal types, since those cannot be subclassed:
     
     ```py
     from typing import Any, Literal
    diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md
    index 10b6d42318..b1dbd57c78 100644
    --- a/crates/ty_python_semantic/resources/mdtest/attributes.md
    +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md
    @@ -1878,7 +1878,7 @@ date.day = 8
     date.month = 4
     date.year = 2025
     
    -# error: [unresolved-attribute] "Can not assign object of type `Literal["UTC"]` to attribute `tz` on type `Date` with custom `__setattr__` method."
    +# error: [unresolved-attribute] "Cannot assign object of type `Literal["UTC"]` to attribute `tz` on type `Date` with custom `__setattr__` method."
     date.tz = "UTC"
     ```
     
    @@ -1894,10 +1894,10 @@ class Frozen:
         existing: int = 1
     
         def __setattr__(self, name, value) -> Never:
    -        raise AttributeError("Attributes can not be modified")
    +        raise AttributeError("Attributes cannot be modified")
     
     instance = Frozen()
    -instance.non_existing = 2  # error: [invalid-assignment] "Can not assign to unresolved attribute `non_existing` on type `Frozen`"
    +instance.non_existing = 2  # error: [invalid-assignment] "Cannot assign to unresolved attribute `non_existing` on type `Frozen`"
     instance.existing = 2  # error: [invalid-assignment] "Cannot assign to attribute `existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`"
     ```
     
    @@ -1949,7 +1949,7 @@ def flag() -> bool:
     class Frozen:
         if flag():
             def __setattr__(self, name, value) -> Never:
    -            raise AttributeError("Attributes can not be modified")
    +            raise AttributeError("Attributes cannot be modified")
     
     instance = Frozen()
     instance.non_existing = 2  # error: [invalid-assignment]
    diff --git a/crates/ty_python_semantic/resources/mdtest/call/dunder.md b/crates/ty_python_semantic/resources/mdtest/call/dunder.md
    index 721517eac4..f7be30464c 100644
    --- a/crates/ty_python_semantic/resources/mdtest/call/dunder.md
    +++ b/crates/ty_python_semantic/resources/mdtest/call/dunder.md
    @@ -194,7 +194,7 @@ class_with_descriptor_dunder = ClassWithDescriptorDunder()
     reveal_type(class_with_descriptor_dunder[0])  # revealed: str
     ```
     
    -## Dunders can not be overwritten on instances
    +## Dunders cannot be overwritten on instances
     
     If we attempt to overwrite a dunder method on an instance, it does not affect the behavior of
     implicit dunder calls:
    diff --git a/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md b/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md
    index a8d87bbfa6..7841d04f79 100644
    --- a/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md
    +++ b/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md
    @@ -84,7 +84,7 @@ class E(metaclass=Meta): ...
     reveal_type(inspect.getattr_static(E, "attr"))  # revealed: int
     ```
     
    -Metaclass attributes can not be added when probing an instance of the class:
    +Metaclass attributes cannot be added when probing an instance of the class:
     
     ```py
     reveal_type(inspect.getattr_static(E(), "attr", "non_existent"))  # revealed: Literal["non_existent"]
    diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md
    index f101aa6e64..07740c2f89 100644
    --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md
    +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md
    @@ -308,7 +308,7 @@ reveal_type(C.f)  # revealed: bound method .f(arg: int) -> str
     reveal_type(C.f(1))  # revealed: str
     ```
     
    -The method `f` can not be accessed from an instance of the class:
    +The method `f` cannot be accessed from an instance of the class:
     
     ```py
     # error: [unresolved-attribute] "Object of type `C` has no attribute `f`"
    diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
    index d8619851a2..8548085302 100644
    --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
    +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
    @@ -424,7 +424,7 @@ from dataclasses import dataclass
     class MyFrozenClass: ...
     
     frozen = MyFrozenClass()
    -frozen.x = 2  # error: [invalid-assignment] "Can not assign to unresolved attribute `x` on type `MyFrozenClass`"
    +frozen.x = 2  # error: [invalid-assignment] "Cannot assign to unresolved attribute `x` on type `MyFrozenClass`"
     ```
     
     A diagnostic is also emitted if a frozen dataclass is inherited, and an attempt is made to mutate an
    diff --git a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md
    index 629fd2b554..a207b3414f 100644
    --- a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md
    +++ b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md
    @@ -26,7 +26,7 @@ def outer_generator():
     ## `yield from` with a custom iterable
     
     `yield from` can also be used with custom iterable types. In that case, the type of the `yield from`
    -expression can not be determined
    +expression cannot be determined
     
     ```py
     from typing import Generator, TypeVar, Generic
    diff --git a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md
    index 03ea95b4a2..e8c19625ca 100644
    --- a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md
    +++ b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md
    @@ -130,7 +130,7 @@ static_assert(has_member(C, "base_attr"))
     static_assert(not has_member(C, "non_existent"))
     ```
     
    -But instance attributes can not be accessed this way:
    +But instance attributes cannot be accessed this way:
     
     ```py
     static_assert(not has_member(C, "instance_attr"))
    diff --git a/crates/ty_python_semantic/resources/mdtest/intersection_types.md b/crates/ty_python_semantic/resources/mdtest/intersection_types.md
    index 0f5b37eb88..022e09c43b 100644
    --- a/crates/ty_python_semantic/resources/mdtest/intersection_types.md
    +++ b/crates/ty_python_semantic/resources/mdtest/intersection_types.md
    @@ -444,7 +444,7 @@ def _(
         reveal_type(i07)  # revealed: Never
         reveal_type(i08)  # revealed: Never
     
    -# `bool` is final and can not be subclassed, so `type[bool]` is equivalent to `Literal[bool]`, which
    +# `bool` is final and cannot be subclassed, so `type[bool]` is equivalent to `Literal[bool]`, which
     # is disjoint from `type[str]`:
     def example_type_bool_type_str(
         i: Intersection[type[bool], type[str]],
    diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md
    index 22d92b54af..4ff580954e 100644
    --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md
    +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md
    @@ -390,7 +390,7 @@ static_assert(not is_single_valued(Literal["a"] | Literal["b"]))
     
     We use `TypeOf` to get the inferred type of an expression. This is useful when we want to refer to
     it in a type expression. For example, if we want to make sure that the class literal type `str` is a
    -subtype of `type[str]`, we can not use `is_subtype_of(str, type[str])`, as that would test if the
    +subtype of `type[str]`, we cannot use `is_subtype_of(str, type[str])`, as that would test if the
     type `str` itself is a subtype of `type[str]`. Instead, we can use `TypeOf[str]` to get the type of
     the expression `str`:
     
    diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md
    index a0de2576b9..255e744af9 100644
    --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md
    +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md
    @@ -54,7 +54,7 @@ class Small(Medium): ...
     static_assert(is_assignable_to(Any | Medium, Big))
     static_assert(is_assignable_to(Any | Medium, Medium))
     
    -# `Any | Medium` is at least as large as `Medium`, so we can not assign it to `Small`:
    +# `Any | Medium` is at least as large as `Medium`, so we cannot assign it to `Small`:
     static_assert(not is_assignable_to(Any | Medium, Small))
     ```
     
    @@ -84,7 +84,7 @@ static_assert(is_assignable_to(Small, Intersection[Any, Medium]))
     static_assert(is_assignable_to(Medium, Intersection[Any, Medium]))
     ```
     
    -`Any & Medium` is no larger than `Medium`, so we can not assign `Big` to it. There is no possible
    +`Any & Medium` is no larger than `Medium`, so we cannot assign `Big` to it. There is no possible
     materialization of `Any & Medium` that would make it as big as `Big`:
     
     ```py
    diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md
    index d8d42ae7ad..66b759b9ac 100644
    --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md
    +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md
    @@ -32,8 +32,8 @@ static_assert(not is_singleton(Literal[1]))
     static_assert(not is_singleton(Literal[54165]))
     ```
     
    -This has implications for type-narrowing. For example, you can not use the `is not` operator to
    -check whether a variable has a specific integer literal type, but this is not a recommended practice
    +This has implications for type-narrowing. For example, you cannot use the `is not` operator to check
    +whether a variable has a specific integer literal type, but this is not a recommended practice
     anyway.
     
     ```py
    @@ -44,7 +44,7 @@ def f(x: int):
             reveal_type(x)  # revealed: Literal[54165]
     
         if x is not 54165:
    -        # But here, we can not narrow the type (to `int & ~Literal[54165]`), because `x` might also
    +        # But here, we cannot narrow the type (to `int & ~Literal[54165]`), because `x` might also
             # have the value `54165`, but a different object identity.
             reveal_type(x)  # revealed: int
     ```
    diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md
    index dfad076726..db4b0f5f98 100644
    --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md
    +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md
    @@ -45,7 +45,7 @@ class C(B1, B2): ...
     # ... which lies in their intersection:
     static_assert(is_subtype_of(C, Intersection[B1, B2]))
     
    -# However, if a class is marked final, it can not be subclassed ...
    +# However, if a class is marked final, it cannot be subclassed ...
     @final
     class FinalSubclass(A): ...
     
    diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md
    index 14142020a2..30bbb2132b 100644
    --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md
    +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md
    @@ -680,7 +680,7 @@ def _(p: Person) -> None:
         reveal_type(p.__class__)  # revealed: 
     ```
     
    -Also, the "attributes" on the class definition can not be accessed. Neither on the class itself, nor
    +Also, the "attributes" on the class definition cannot be accessed. Neither on the class itself, nor
     on inhabitants of the type defined by the class:
     
     ```py
    @@ -714,7 +714,7 @@ reveal_type(Person.__required_keys__)  # revealed: frozenset[str]
     reveal_type(Person.__optional_keys__)  # revealed: frozenset[str]
     ```
     
    -These attributes can not be accessed on inhabitants:
    +These attributes cannot be accessed on inhabitants:
     
     ```py
     def _(person: Person) -> None:
    @@ -723,7 +723,7 @@ def _(person: Person) -> None:
         person.__optional_keys__  # error: [unresolved-attribute]
     ```
     
    -Also, they can not be accessed on `type(person)`, as that would be `dict` at runtime:
    +Also, they cannot be accessed on `type(person)`, as that would be `dict` at runtime:
     
     ```py
     def _(person: Person) -> None:
    diff --git a/crates/ty_python_semantic/resources/mdtest/unreachable.md b/crates/ty_python_semantic/resources/mdtest/unreachable.md
    index 7321ed9b01..73e174f6a1 100644
    --- a/crates/ty_python_semantic/resources/mdtest/unreachable.md
    +++ b/crates/ty_python_semantic/resources/mdtest/unreachable.md
    @@ -187,8 +187,8 @@ python-platform = "all"
     
     If `python-platform` is set to `all`, we treat the platform as unspecified. This means that we do
     not infer a literal type like `Literal["win32"]` for `sys.platform`, but instead fall back to
    -`LiteralString` (the `typeshed` annotation for `sys.platform`). This means that we can not
    -statically determine the truthiness of a branch like `sys.platform == "win32"`.
    +`LiteralString` (the `typeshed` annotation for `sys.platform`). This means that we cannot statically
    +determine the truthiness of a branch like `sys.platform == "win32"`.
     
     See  for a plan on how this
     could be improved.
    diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs
    index 3989942b04..c0b3428345 100644
    --- a/crates/ty_python_semantic/src/place.rs
    +++ b/crates/ty_python_semantic/src/place.rs
    @@ -733,7 +733,7 @@ pub(crate) fn place_by_id<'db>(
         };
     
         // If a symbol is undeclared, but qualified with `typing.Final`, we use the right-hand side
    -    // inferred type, without unioning with `Unknown`, because it can not be modified.
    +    // inferred type, without unioning with `Unknown`, because it cannot be modified.
         if let Some(qualifiers) = declared.is_bare_final() {
             let bindings = all_considered_bindings();
             return place_from_bindings_impl(db, bindings, requires_explicit_reexport)
    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 1224190209..9e6d60668f 100644
    --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs
    +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs
    @@ -13,7 +13,7 @@
     //! of `test`. When evaluating a constraint, there are three possible outcomes: always true, always
     //! false, or ambiguous. For a simple constraint like this, always-true and always-false correspond
     //! to the case in which we can infer that the type of `test` is `Literal[True]` or `Literal[False]`.
    -//! In any other case, like if the type of `test` is `bool` or `Unknown`, we can not statically
    +//! In any other case, like if the type of `test` is `bool` or `Unknown`, we cannot statically
     //! determine whether `test` is truthy or falsy, so the outcome would be "ambiguous".
     //!
     //!
    @@ -29,7 +29,7 @@
     //! Here, we would accumulate a reachability constraint of `test1 AND test2`. We can statically
     //! determine that this position is *always* reachable only if both `test1` and `test2` are
     //! always true. On the other hand, we can statically determine that this position is *never*
    -//! reachable if *either* `test1` or `test2` is always false. In any other case, we can not
    +//! reachable if *either* `test1` or `test2` is always false. In any other case, we cannot
     //! determine whether this position is reachable or not, so the outcome is "ambiguous". This
     //! corresponds to a ternary *AND* operation in [Kleene] logic:
     //!
    @@ -60,7 +60,7 @@
     //! The third branch ends in a terminal statement [^1]. When we merge control flow, we need to consider
     //! the reachability through either the first or the second branch. The current position is only
     //! *definitely* unreachable if both `test1` and `test2` are always false. It is definitely
    -//! reachable if *either* `test1` or `test2` is always true. In any other case, we can not statically
    +//! reachable if *either* `test1` or `test2` is always true. In any other case, we cannot statically
     //! determine whether it is reachable or not. This operation corresponds to a ternary *OR* operation:
     //!
     //! ```text
    @@ -91,7 +91,7 @@
     //! ## Explicit ambiguity
     //!
     //! In some cases, we explicitly record an “ambiguous” constraint. We do this when branching on
    -//! something that we can not (or intentionally do not want to) analyze statically. `for` loops are
    +//! something that we cannot (or intentionally do not want to) analyze statically. `for` loops are
     //! one example:
     //! ```py
     //! def _():
    diff --git a/crates/ty_python_semantic/src/subscript.rs b/crates/ty_python_semantic/src/subscript.rs
    index b7ea13db10..b51a9e597b 100644
    --- a/crates/ty_python_semantic/src/subscript.rs
    +++ b/crates/ty_python_semantic/src/subscript.rs
    @@ -27,7 +27,7 @@ fn from_negative_i32(index: i32) -> usize {
         static_assertions::const_assert!(usize::BITS >= 32);
     
         index.checked_neg().map(from_nonnegative_i32).unwrap_or({
    -        // 'checked_neg' only fails for i32::MIN. We can not
    +        // 'checked_neg' only fails for i32::MIN. We cannot
             // represent -i32::MIN as a i32, but we can represent
             // it as a usize, since usize is at least 32 bits.
             from_nonnegative_i32(i32::MAX) + 1
    diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
    index b20f332999..ffe0f3066b 100644
    --- a/crates/ty_python_semantic/src/types.rs
    +++ b/crates/ty_python_semantic/src/types.rs
    @@ -297,7 +297,7 @@ impl AttributeKind {
     /// When invoked on a class object, the fallback type (a class attribute) can shadow a
     /// non-data descriptor of the meta-type (the class's metaclass). However, this is not
     /// true for instances. When invoked on an instance, the fallback type (an attribute on
    -/// the instance) can not completely shadow a non-data descriptor of the meta-type (the
    +/// the instance) cannot completely shadow a non-data descriptor of the meta-type (the
     /// class), because we do not currently attempt to statically infer if an instance
     /// attribute is definitely defined (i.e. to check whether a particular method has been
     /// called).
    @@ -4412,7 +4412,7 @@ impl<'db> Type<'db> {
                     };
     
                     if result.is_class_var() && self.is_typed_dict() {
    -                    // `ClassVar`s on `TypedDictFallback` can not be accessed on inhabitants of `SomeTypedDict`.
    +                    // `ClassVar`s on `TypedDictFallback` cannot be accessed on inhabitants of `SomeTypedDict`.
                         // They can only be accessed on `SomeTypedDict` directly.
                         return Place::Undefined.into();
                     }
    @@ -12050,7 +12050,7 @@ pub(crate) mod tests {
             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 can not
    +        // 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.
    diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
    index 2dd75e57aa..6ab6f2a447 100644
    --- a/crates/ty_python_semantic/src/types/diagnostic.rs
    +++ b/crates/ty_python_semantic/src/types/diagnostic.rs
    @@ -2000,7 +2000,7 @@ pub(super) fn report_slice_step_size_zero(context: &InferContext, node: AnyNodeR
         let Some(builder) = context.report_lint(&ZERO_STEPSIZE_IN_SLICE, node) else {
             return;
         };
    -    builder.into_diagnostic("Slice step size can not be zero");
    +    builder.into_diagnostic("Slice step size cannot be zero");
     }
     
     fn report_invalid_assignment_with_message(
    diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs
    index 98f7cb736f..444c5badd6 100644
    --- a/crates/ty_python_semantic/src/types/generics.rs
    +++ b/crates/ty_python_semantic/src/types/generics.rs
    @@ -714,7 +714,7 @@ fn is_subtype_in_invariant_position<'db>(
             // TODO:
             // This should be removed and properly handled in the respective
             // `(Type::TypeVar(_), _) | (_, Type::TypeVar(_))` branch of
    -        // `Type::has_relation_to_impl`. Right now, we can not generally
    +        // `Type::has_relation_to_impl`. Right now, we cannot generally
             // return `ConstraintSet::from(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
    diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
    index b74ff75404..078d49fe5e 100644
    --- a/crates/ty_python_semantic/src/types/infer/builder.rs
    +++ b/crates/ty_python_semantic/src/types/infer/builder.rs
    @@ -3804,7 +3804,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
     
                                         let msg = if !member_exists {
                                             format!(
    -                                            "Can not assign to unresolved attribute `{attribute}` on type `{}`",
    +                                            "Cannot assign to unresolved attribute `{attribute}` on type `{}`",
                                                 object_ty.display(db)
                                             )
                                         } else if is_setattr_synthesized {
    @@ -3840,7 +3840,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                                     self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
                                 {
                                     builder.into_diagnostic(format_args!(
    -                                    "Can not assign object of type `{}` to attribute \
    +                                    "Cannot assign object of type `{}` to attribute \
                                          `{attribute}` on type `{}` with \
                                          custom `__setattr__` method.",
                                         value_ty.display(db),
    
    From b5305b5f328b03e3ba0ab1ba629dedb5c9f4415e Mon Sep 17 00:00:00 2001
    From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com>
    Date: Tue, 4 Nov 2025 00:41:11 +0900
    Subject: [PATCH 159/188] [ty] Fix panic due to simplifying `Divergent` types
     out of intersections types (#21253)
    
    ---
     .../resources/corpus/cyclic_comprehensions.py | 10 ++++
     .../pr_20962_comprehension_panics.md          | 13 ----
     crates/ty_python_semantic/src/types.rs        | 60 +++++++++++++++----
     3 files changed, 60 insertions(+), 23 deletions(-)
     create mode 100644 crates/ty_python_semantic/resources/corpus/cyclic_comprehensions.py
    
    diff --git a/crates/ty_python_semantic/resources/corpus/cyclic_comprehensions.py b/crates/ty_python_semantic/resources/corpus/cyclic_comprehensions.py
    new file mode 100644
    index 0000000000..28ba9d9091
    --- /dev/null
    +++ b/crates/ty_python_semantic/resources/corpus/cyclic_comprehensions.py
    @@ -0,0 +1,10 @@
    +# Regression test for https://github.com/astral-sh/ruff/pull/20962
    +# error message:
    +# `infer_definition_types(Id(1804)): execute: too many cycle iterations`
    +
    +for name_1 in {
    +    {{0: name_4 for unique_name_0 in unique_name_1}: 0 for unique_name_2 in unique_name_3 if name_4}: 0
    +    for unique_name_4 in name_1
    +    for name_4 in name_1
    +}:
    +    pass
    diff --git a/crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md b/crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md
    index b011d95e8c..97bbf21049 100644
    --- a/crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md
    +++ b/crates/ty_python_semantic/resources/mdtest/regression/pr_20962_comprehension_panics.md
    @@ -35,16 +35,3 @@ else:
     async def name_5():
         pass
     ```
    -
    -## Too many cycle iterations in `infer_definition_types`
    -
    -
    -
    -```py
    -for name_1 in {
    -    {{0: name_4 for unique_name_0 in unique_name_1}: 0 for unique_name_2 in unique_name_3 if name_4}: 0
    -    for unique_name_4 in name_1
    -    for name_4 in name_1
    -}:
    -    pass
    -```
    diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
    index ffe0f3066b..e79282a35d 100644
    --- a/crates/ty_python_semantic/src/types.rs
    +++ b/crates/ty_python_semantic/src/types.rs
    @@ -873,6 +873,10 @@ impl<'db> Type<'db> {
             matches!(self, Type::Dynamic(_))
         }
     
    +    const fn is_non_divergent_dynamic(&self) -> bool {
    +        self.is_dynamic() && !self.is_divergent()
    +    }
    +
         /// Is a value of this type only usable in typing contexts?
         pub(crate) fn is_type_check_only(&self, db: &'db dyn Db) -> bool {
             match self {
    @@ -1695,22 +1699,33 @@ impl<'db> Type<'db> {
                 // holds true if `T` is also a dynamic type or a union that contains a dynamic type.
                 // Similarly, `T <: Any` only holds true if `T` is a dynamic type or an intersection
                 // that contains a dynamic type.
    -            (Type::Dynamic(_), _) => ConstraintSet::from(match relation {
    -                TypeRelation::Subtyping => false,
    -                TypeRelation::Assignability => true,
    -                TypeRelation::Redundancy => match target {
    -                    Type::Dynamic(_) => true,
    -                    Type::Union(union) => union.elements(db).iter().any(Type::is_dynamic),
    -                    _ => false,
    -                },
    -            }),
    +            (Type::Dynamic(dynamic), _) => {
    +                // If a `Divergent` type is involved, it must not be eliminated.
    +                debug_assert!(
    +                    !matches!(dynamic, DynamicType::Divergent(_)),
    +                    "DynamicType::Divergent should have been handled in an earlier branch"
    +                );
    +                ConstraintSet::from(match relation {
    +                    TypeRelation::Subtyping => false,
    +                    TypeRelation::Assignability => true,
    +                    TypeRelation::Redundancy => match target {
    +                        Type::Dynamic(_) => true,
    +                        Type::Union(union) => union.elements(db).iter().any(Type::is_dynamic),
    +                        _ => false,
    +                    },
    +                })
    +            }
                 (_, Type::Dynamic(_)) => ConstraintSet::from(match relation {
                     TypeRelation::Subtyping => false,
                     TypeRelation::Assignability => true,
                     TypeRelation::Redundancy => match self {
                         Type::Dynamic(_) => true,
                         Type::Intersection(intersection) => {
    -                        intersection.positive(db).iter().any(Type::is_dynamic)
    +                        // If a `Divergent` type is involved, it must not be eliminated.
    +                        intersection
    +                            .positive(db)
    +                            .iter()
    +                            .any(Type::is_non_divergent_dynamic)
                         }
                         _ => false,
                     },
    @@ -9991,6 +10006,10 @@ pub(crate) enum TypeRelation {
         /// materialization of `Any` and `int | Any` may be the same type (`object`), but the
         /// two differ in their bottom materializations (`Never` and `int`, respectively).
         ///
    +    /// Despite the above principles, there is one exceptional type that should never be union-simplified: the `Divergent` type.
    +    /// This is a kind of dynamic type, but it acts as a marker to track recursive type structures.
    +    /// If this type is accidentally eliminated by simplification, the fixed-point iteration will not converge.
    +    ///
         /// [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,
    @@ -12103,6 +12122,27 @@ pub(crate) mod tests {
             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));
    +
    +        let truthy_div = IntersectionBuilder::new(&db)
    +            .add_positive(div)
    +            .add_negative(Type::AlwaysFalsy)
    +            .build();
    +
    +        let union = UnionType::from_elements(&db, [Type::unknown(), truthy_div]);
    +        assert!(!truthy_div.is_redundant_with(&db, Type::unknown()));
    +        assert_eq!(
    +            union.display(&db).to_string(),
    +            "Unknown | (Divergent & ~AlwaysFalsy)"
    +        );
    +
    +        let union = UnionType::from_elements(&db, [truthy_div, Type::unknown()]);
    +        assert!(!Type::unknown().is_redundant_with(&db, truthy_div));
    +        assert_eq!(
    +            union.display(&db).to_string(),
    +            "(Divergent & ~AlwaysFalsy) | Unknown"
    +        );
     
             // 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`)
    
    From 64a255df497bede66fa4cdac80b9464d73a787ba Mon Sep 17 00:00:00 2001
    From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
    Date: Mon, 3 Nov 2025 15:55:50 +0000
    Subject: [PATCH 160/188] Update to Unicode 17 for line-width calculations
     (#21231)
    
    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 bfe02a8697..db64440a3d 100644
    --- a/Cargo.lock
    +++ b/Cargo.lock
    @@ -4688,9 +4688,9 @@ dependencies = [
     
     [[package]]
     name = "unicode-width"
    -version = "0.2.1"
    +version = "0.2.2"
     source = "registry+https://github.com/rust-lang/crates.io-index"
    -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
    +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
     
     [[package]]
     name = "unicode_names2"
    
    From 21ec8aa7d494ec1314806f4f781b18c801c40794 Mon Sep 17 00:00:00 2001
    From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
    Date: Mon, 3 Nov 2025 16:10:49 +0000
    Subject: [PATCH 161/188] Update Rust crate unicode-ident to v1.0.22 (#21228)
    
    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 db64440a3d..14312dfa75 100644
    --- a/Cargo.lock
    +++ b/Cargo.lock
    @@ -4673,9 +4673,9 @@ checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580"
     
     [[package]]
     name = "unicode-ident"
    -version = "1.0.19"
    +version = "1.0.22"
     source = "registry+https://github.com/rust-lang/crates.io-index"
    -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
    +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
     
     [[package]]
     name = "unicode-normalization"
    
    From 78ee7ae9254a08e49c9c6443512235f74a027584 Mon Sep 17 00:00:00 2001
    From: Tom Kuson 
    Date: Mon, 3 Nov 2025 19:04:59 +0000
    Subject: [PATCH 162/188] [`flake8-comprehensions`] Fix typo in `C416`
     documentation (#21184)
    
    ## Summary
    
    Adds missing curly brace to the C416 documentation.
    
    ## Test Plan
    
    Build the docs
    ---
     .../flake8_comprehensions/rules/unnecessary_comprehension.rs    | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs
    index f384b32e6e..70108c1ba7 100644
    --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs
    +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs
    @@ -43,7 +43,7 @@ use crate::rules::flake8_comprehensions::fixes;
     /// >>> {x: y for x, y in d1}  # Iterates over the keys of a mapping
     /// {1: 2, 4: 5}
     /// >>> dict(d1)               # Ruff's incorrect suggested fix
    -/// (1, 2): 3, (4, 5): 6}
    +/// {(1, 2): 3, (4, 5): 6}
     /// >>> dict(d1.keys())        # Correct fix
     /// {1: 2, 4: 5}
     /// ```
    
    From 04335268970408aa6c81d8dae1c5279498c100e0 Mon Sep 17 00:00:00 2001
    From: Wei Lee 
    Date: Tue, 4 Nov 2025 04:20:20 +0800
    Subject: [PATCH 163/188] [`airflow`] extend deprecated argument `concurrency`
     in `airflow..DAG` (`AIR301`) (#21220)
    
    
    
    ## Summary
    
    
    * extend AIR301 to include deprecated argument `concurrency` in
    `airflow....DAG`
    
    ## Test Plan
    
    
    
    update the existing test fixture in the first commit and then reorganize
    in the second one
    ---
     .../test/fixtures/airflow/AIR301_args.py      |   1 +
     .../src/rules/airflow/rules/removal_in_3.rs   |   1 +
     ...airflow__tests__AIR301_AIR301_args.py.snap | 316 ++++++++++--------
     3 files changed, 172 insertions(+), 146 deletions(-)
    
    diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_args.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_args.py
    index ce35d79338..e275a54bcd 100644
    --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_args.py
    +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_args.py
    @@ -22,6 +22,7 @@ DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
     
     DAG(dag_id="class_timetable", timetable=NullTimetable())
     
    +DAG(dag_id="class_concurrency", concurrency=12)
     
     DAG(dag_id="class_fail_stop", fail_stop=True)
     
    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 78c89da0b4..562f37230d 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
    @@ -196,6 +196,7 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
         match qualified_name.segments() {
             ["airflow", .., "DAG" | "dag"] => {
                 // with replacement
    +            diagnostic_for_argument(checker, arguments, "concurrency", Some("max_active_tasks"));
                 diagnostic_for_argument(checker, arguments, "fail_stop", Some("fail_fast"));
                 diagnostic_for_argument(checker, arguments, "schedule_interval", Some("schedule"));
                 diagnostic_for_argument(checker, arguments, "timetable", Some("schedule"));
    diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_args.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_args.py.snap
    index 6f783edc9f..e0daf99000 100644
    --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_args.py.snap
    +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_args.py.snap
    @@ -28,6 +28,8 @@ AIR301 [*] `timetable` is removed in Airflow 3.0
     22 |
     23 | DAG(dag_id="class_timetable", timetable=NullTimetable())
        |                               ^^^^^^^^^
    +24 |
    +25 | DAG(dag_id="class_concurrency", concurrency=12)
        |
     help: Use `schedule` instead
     20 | 
    @@ -36,249 +38,271 @@ help: Use `schedule` instead
        - DAG(dag_id="class_timetable", timetable=NullTimetable())
     23 + DAG(dag_id="class_timetable", schedule=NullTimetable())
     24 | 
    -25 | 
    -26 | DAG(dag_id="class_fail_stop", fail_stop=True)
    +25 | DAG(dag_id="class_concurrency", concurrency=12)
    +26 | 
     
    -AIR301 [*] `fail_stop` is removed in Airflow 3.0
    -  --> AIR301_args.py:26:31
    +AIR301 [*] `concurrency` is removed in Airflow 3.0
    +  --> AIR301_args.py:25:33
        |
    -26 | DAG(dag_id="class_fail_stop", fail_stop=True)
    -   |                               ^^^^^^^^^
    -27 |
    -28 | DAG(dag_id="class_default_view", default_view="dag_default_view")
    +23 | DAG(dag_id="class_timetable", timetable=NullTimetable())
    +24 |
    +25 | DAG(dag_id="class_concurrency", concurrency=12)
    +   |                                 ^^^^^^^^^^^
    +26 |
    +27 | DAG(dag_id="class_fail_stop", fail_stop=True)
        |
    -help: Use `fail_fast` instead
    +help: Use `max_active_tasks` instead
    +22 | 
     23 | DAG(dag_id="class_timetable", timetable=NullTimetable())
     24 | 
    -25 | 
    +   - DAG(dag_id="class_concurrency", concurrency=12)
    +25 + DAG(dag_id="class_concurrency", max_active_tasks=12)
    +26 | 
    +27 | DAG(dag_id="class_fail_stop", fail_stop=True)
    +28 | 
    +
    +AIR301 [*] `fail_stop` is removed in Airflow 3.0
    +  --> AIR301_args.py:27:31
    +   |
    +25 | DAG(dag_id="class_concurrency", concurrency=12)
    +26 |
    +27 | DAG(dag_id="class_fail_stop", fail_stop=True)
    +   |                               ^^^^^^^^^
    +28 |
    +29 | DAG(dag_id="class_default_view", default_view="dag_default_view")
    +   |
    +help: Use `fail_fast` instead
    +24 | 
    +25 | DAG(dag_id="class_concurrency", concurrency=12)
    +26 | 
        - DAG(dag_id="class_fail_stop", fail_stop=True)
    -26 + DAG(dag_id="class_fail_stop", fail_fast=True)
    -27 | 
    -28 | DAG(dag_id="class_default_view", default_view="dag_default_view")
    -29 | 
    +27 + DAG(dag_id="class_fail_stop", fail_fast=True)
    +28 | 
    +29 | DAG(dag_id="class_default_view", default_view="dag_default_view")
    +30 | 
     
     AIR301 `default_view` is removed in Airflow 3.0
    -  --> AIR301_args.py:28:34
    +  --> AIR301_args.py:29:34
        |
    -26 | DAG(dag_id="class_fail_stop", fail_stop=True)
    -27 |
    -28 | DAG(dag_id="class_default_view", default_view="dag_default_view")
    +27 | DAG(dag_id="class_fail_stop", fail_stop=True)
    +28 |
    +29 | DAG(dag_id="class_default_view", default_view="dag_default_view")
        |                                  ^^^^^^^^^^^^
    -29 |
    -30 | DAG(dag_id="class_orientation", orientation="BT")
    +30 |
    +31 | DAG(dag_id="class_orientation", orientation="BT")
        |
     
     AIR301 `orientation` is removed in Airflow 3.0
    -  --> AIR301_args.py:30:33
    +  --> AIR301_args.py:31:33
        |
    -28 | DAG(dag_id="class_default_view", default_view="dag_default_view")
    -29 |
    -30 | DAG(dag_id="class_orientation", orientation="BT")
    +29 | DAG(dag_id="class_default_view", default_view="dag_default_view")
    +30 |
    +31 | DAG(dag_id="class_orientation", orientation="BT")
        |                                 ^^^^^^^^^^^
    -31 |
    -32 | allow_future_exec_dates_dag = DAG(dag_id="class_allow_future_exec_dates")
    +32 |
    +33 | allow_future_exec_dates_dag = DAG(dag_id="class_allow_future_exec_dates")
        |
     
     AIR301 [*] `schedule_interval` is removed in Airflow 3.0
    -  --> AIR301_args.py:41:6
    +  --> AIR301_args.py:42:6
        |
    -41 | @dag(schedule_interval="0 * * * *")
    +42 | @dag(schedule_interval="0 * * * *")
        |      ^^^^^^^^^^^^^^^^^
    -42 | def decorator_schedule_interval():
    -43 |     pass
    +43 | def decorator_schedule_interval():
    +44 |     pass
        |
     help: Use `schedule` instead
    -38 |     pass
    -39 | 
    +39 |     pass
     40 | 
    +41 | 
        - @dag(schedule_interval="0 * * * *")
    -41 + @dag(schedule="0 * * * *")
    -42 | def decorator_schedule_interval():
    -43 |     pass
    -44 | 
    +42 + @dag(schedule="0 * * * *")
    +43 | def decorator_schedule_interval():
    +44 |     pass
    +45 | 
     
     AIR301 [*] `timetable` is removed in Airflow 3.0
    -  --> AIR301_args.py:46:6
    +  --> AIR301_args.py:47:6
        |
    -46 | @dag(timetable=NullTimetable())
    +47 | @dag(timetable=NullTimetable())
        |      ^^^^^^^^^
    -47 | def decorator_timetable():
    -48 |     pass
    +48 | def decorator_timetable():
    +49 |     pass
        |
     help: Use `schedule` instead
    -43 |     pass
    -44 | 
    +44 |     pass
     45 | 
    +46 | 
        - @dag(timetable=NullTimetable())
    -46 + @dag(schedule=NullTimetable())
    -47 | def decorator_timetable():
    -48 |     pass
    -49 | 
    +47 + @dag(schedule=NullTimetable())
    +48 | def decorator_timetable():
    +49 |     pass
    +50 | 
     
     AIR301 [*] `execution_date` is removed in Airflow 3.0
    -  --> AIR301_args.py:54:62
    +  --> AIR301_args.py:55:62
        |
    -52 | def decorator_deprecated_operator_args():
    -53 |     trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
    -54 |         task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04"
    +53 | def decorator_deprecated_operator_args():
    +54 |     trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
    +55 |         task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04"
        |                                                              ^^^^^^^^^^^^^^
    -55 |     )
    -56 |     trigger_dagrun_op2 = TriggerDagRunOperator(
    +56 |     )
    +57 |     trigger_dagrun_op2 = TriggerDagRunOperator(
        |
     help: Use `logical_date` instead
    -51 | @dag()
    -52 | def decorator_deprecated_operator_args():
    -53 |     trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
    +52 | @dag()
    +53 | def decorator_deprecated_operator_args():
    +54 |     trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
        -         task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04"
    -54 +         task_id="trigger_dagrun_op1", trigger_dag_id="test", logical_date="2024-12-04"
    -55 |     )
    -56 |     trigger_dagrun_op2 = TriggerDagRunOperator(
    -57 |         task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04"
    +55 +         task_id="trigger_dagrun_op1", trigger_dag_id="test", logical_date="2024-12-04"
    +56 |     )
    +57 |     trigger_dagrun_op2 = TriggerDagRunOperator(
    +58 |         task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04"
     
     AIR301 [*] `execution_date` is removed in Airflow 3.0
    -  --> AIR301_args.py:57:62
    +  --> AIR301_args.py:58:62
        |
    -55 |     )
    -56 |     trigger_dagrun_op2 = TriggerDagRunOperator(
    -57 |         task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04"
    +56 |     )
    +57 |     trigger_dagrun_op2 = TriggerDagRunOperator(
    +58 |         task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04"
        |                                                              ^^^^^^^^^^^^^^
    -58 |     )
    +59 |     )
        |
     help: Use `logical_date` instead
    -54 |         task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04"
    -55 |     )
    -56 |     trigger_dagrun_op2 = TriggerDagRunOperator(
    +55 |         task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04"
    +56 |     )
    +57 |     trigger_dagrun_op2 = TriggerDagRunOperator(
        -         task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04"
    -57 +         task_id="trigger_dagrun_op2", trigger_dag_id="test", logical_date="2024-12-04"
    -58 |     )
    -59 | 
    -60 |     branch_dt_op = datetime.BranchDateTimeOperator(
    +58 +         task_id="trigger_dagrun_op2", trigger_dag_id="test", logical_date="2024-12-04"
    +59 |     )
    +60 | 
    +61 |     branch_dt_op = datetime.BranchDateTimeOperator(
     
     AIR301 [*] `use_task_execution_day` is removed in Airflow 3.0
    -  --> AIR301_args.py:61:33
    +  --> AIR301_args.py:62:33
        |
    -60 |     branch_dt_op = datetime.BranchDateTimeOperator(
    -61 |         task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
    +61 |     branch_dt_op = datetime.BranchDateTimeOperator(
    +62 |         task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
        |                                 ^^^^^^^^^^^^^^^^^^^^^^
    -62 |     )
    -63 |     branch_dt_op2 = BranchDateTimeOperator(
    +63 |     )
    +64 |     branch_dt_op2 = BranchDateTimeOperator(
        |
     help: Use `use_task_logical_date` instead
    -58 |     )
    -59 | 
    -60 |     branch_dt_op = datetime.BranchDateTimeOperator(
    +59 |     )
    +60 | 
    +61 |     branch_dt_op = datetime.BranchDateTimeOperator(
        -         task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
    -61 +         task_id="branch_dt_op", use_task_logical_date=True, task_concurrency=5
    -62 |     )
    -63 |     branch_dt_op2 = BranchDateTimeOperator(
    -64 |         task_id="branch_dt_op2",
    +62 +         task_id="branch_dt_op", use_task_logical_date=True, task_concurrency=5
    +63 |     )
    +64 |     branch_dt_op2 = BranchDateTimeOperator(
    +65 |         task_id="branch_dt_op2",
     
     AIR301 [*] `task_concurrency` is removed in Airflow 3.0
    -  --> AIR301_args.py:61:62
    +  --> AIR301_args.py:62:62
        |
    -60 |     branch_dt_op = datetime.BranchDateTimeOperator(
    -61 |         task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
    +61 |     branch_dt_op = datetime.BranchDateTimeOperator(
    +62 |         task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
        |                                                              ^^^^^^^^^^^^^^^^
    -62 |     )
    -63 |     branch_dt_op2 = BranchDateTimeOperator(
    +63 |     )
    +64 |     branch_dt_op2 = BranchDateTimeOperator(
        |
     help: Use `max_active_tis_per_dag` instead
    -58 |     )
    -59 | 
    -60 |     branch_dt_op = datetime.BranchDateTimeOperator(
    +59 |     )
    +60 | 
    +61 |     branch_dt_op = datetime.BranchDateTimeOperator(
        -         task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
    -61 +         task_id="branch_dt_op", use_task_execution_day=True, max_active_tis_per_dag=5
    -62 |     )
    -63 |     branch_dt_op2 = BranchDateTimeOperator(
    -64 |         task_id="branch_dt_op2",
    +62 +         task_id="branch_dt_op", use_task_execution_day=True, max_active_tis_per_dag=5
    +63 |     )
    +64 |     branch_dt_op2 = BranchDateTimeOperator(
    +65 |         task_id="branch_dt_op2",
     
     AIR301 [*] `use_task_execution_day` is removed in Airflow 3.0
    -  --> AIR301_args.py:65:9
    +  --> AIR301_args.py:66:9
        |
    -63 |     branch_dt_op2 = BranchDateTimeOperator(
    -64 |         task_id="branch_dt_op2",
    -65 |         use_task_execution_day=True,
    +64 |     branch_dt_op2 = BranchDateTimeOperator(
    +65 |         task_id="branch_dt_op2",
    +66 |         use_task_execution_day=True,
        |         ^^^^^^^^^^^^^^^^^^^^^^
    -66 |         sla=timedelta(seconds=10),
    -67 |     )
    +67 |         sla=timedelta(seconds=10),
    +68 |     )
        |
     help: Use `use_task_logical_date` instead
    -62 |     )
    -63 |     branch_dt_op2 = BranchDateTimeOperator(
    -64 |         task_id="branch_dt_op2",
    +63 |     )
    +64 |     branch_dt_op2 = BranchDateTimeOperator(
    +65 |         task_id="branch_dt_op2",
        -         use_task_execution_day=True,
    -65 +         use_task_logical_date=True,
    -66 |         sla=timedelta(seconds=10),
    -67 |     )
    -68 | 
    +66 +         use_task_logical_date=True,
    +67 |         sla=timedelta(seconds=10),
    +68 |     )
    +69 | 
     
     AIR301 [*] `use_task_execution_day` is removed in Airflow 3.0
    -  --> AIR301_args.py:92:9
    +  --> AIR301_args.py:93:9
        |
    -90 |         follow_task_ids_if_true=None,
    -91 |         week_day=1,
    -92 |         use_task_execution_day=True,
    +91 |         follow_task_ids_if_true=None,
    +92 |         week_day=1,
    +93 |         use_task_execution_day=True,
        |         ^^^^^^^^^^^^^^^^^^^^^^
    -93 |     )
    +94 |     )
        |
     help: Use `use_task_logical_date` instead
    -89 |         follow_task_ids_if_false=None,
    -90 |         follow_task_ids_if_true=None,
    -91 |         week_day=1,
    +90 |         follow_task_ids_if_false=None,
    +91 |         follow_task_ids_if_true=None,
    +92 |         week_day=1,
        -         use_task_execution_day=True,
    -92 +         use_task_logical_date=True,
    -93 |     )
    -94 | 
    -95 |     trigger_dagrun_op >> trigger_dagrun_op2
    +93 +         use_task_logical_date=True,
    +94 |     )
    +95 | 
    +96 |     trigger_dagrun_op >> trigger_dagrun_op2
     
     AIR301 `filename_template` is removed in Airflow 3.0
    -   --> AIR301_args.py:102:15
    +   --> AIR301_args.py:103:15
         |
    -101 | # deprecated filename_template argument in FileTaskHandler
    -102 | S3TaskHandler(filename_template="/tmp/test")
    +102 | # deprecated filename_template argument in FileTaskHandler
    +103 | S3TaskHandler(filename_template="/tmp/test")
         |               ^^^^^^^^^^^^^^^^^
    -103 | HdfsTaskHandler(filename_template="/tmp/test")
    -104 | ElasticsearchTaskHandler(filename_template="/tmp/test")
    +104 | HdfsTaskHandler(filename_template="/tmp/test")
    +105 | ElasticsearchTaskHandler(filename_template="/tmp/test")
         |
     
     AIR301 `filename_template` is removed in Airflow 3.0
    -   --> AIR301_args.py:103:17
    +   --> AIR301_args.py:104:17
         |
    -101 | # deprecated filename_template argument in FileTaskHandler
    -102 | S3TaskHandler(filename_template="/tmp/test")
    -103 | HdfsTaskHandler(filename_template="/tmp/test")
    +102 | # deprecated filename_template argument in FileTaskHandler
    +103 | S3TaskHandler(filename_template="/tmp/test")
    +104 | HdfsTaskHandler(filename_template="/tmp/test")
         |                 ^^^^^^^^^^^^^^^^^
    -104 | ElasticsearchTaskHandler(filename_template="/tmp/test")
    -105 | GCSTaskHandler(filename_template="/tmp/test")
    +105 | ElasticsearchTaskHandler(filename_template="/tmp/test")
    +106 | GCSTaskHandler(filename_template="/tmp/test")
         |
     
     AIR301 `filename_template` is removed in Airflow 3.0
    -   --> AIR301_args.py:104:26
    +   --> AIR301_args.py:105:26
         |
    -102 | S3TaskHandler(filename_template="/tmp/test")
    -103 | HdfsTaskHandler(filename_template="/tmp/test")
    -104 | ElasticsearchTaskHandler(filename_template="/tmp/test")
    +103 | S3TaskHandler(filename_template="/tmp/test")
    +104 | HdfsTaskHandler(filename_template="/tmp/test")
    +105 | ElasticsearchTaskHandler(filename_template="/tmp/test")
         |                          ^^^^^^^^^^^^^^^^^
    -105 | GCSTaskHandler(filename_template="/tmp/test")
    +106 | GCSTaskHandler(filename_template="/tmp/test")
         |
     
     AIR301 `filename_template` is removed in Airflow 3.0
    -   --> AIR301_args.py:105:16
    +   --> AIR301_args.py:106:16
         |
    -103 | HdfsTaskHandler(filename_template="/tmp/test")
    -104 | ElasticsearchTaskHandler(filename_template="/tmp/test")
    -105 | GCSTaskHandler(filename_template="/tmp/test")
    +104 | HdfsTaskHandler(filename_template="/tmp/test")
    +105 | ElasticsearchTaskHandler(filename_template="/tmp/test")
    +106 | GCSTaskHandler(filename_template="/tmp/test")
         |                ^^^^^^^^^^^^^^^^^
    -106 |
    -107 | FabAuthManager(None)
    +107 |
    +108 | FabAuthManager(None)
         |
     
     AIR301 `appbuilder` is removed in Airflow 3.0
    -   --> AIR301_args.py:107:15
    +   --> AIR301_args.py:108:15
         |
    -105 | GCSTaskHandler(filename_template="/tmp/test")
    -106 |
    -107 | FabAuthManager(None)
    +106 | GCSTaskHandler(filename_template="/tmp/test")
    +107 |
    +108 | FabAuthManager(None)
         |               ^^^^^^
         |
     help: The constructor takes no parameter now
    
    From fe4ee81b9749bd326845aec30e694af8568549e6 Mon Sep 17 00:00:00 2001
    From: Carl Meyer 
    Date: Mon, 3 Nov 2025 15:24:01 -0500
    Subject: [PATCH 164/188] [ty] prefer submodule over module __getattr__ in
     from-imports (#21260)
    
    Fixes https://github.com/astral-sh/ty/issues/1053
    
    ## Summary
    
    Other type checkers prioritize a submodule over a package `__getattr__`
    in `from mod import sub`, even though the runtime precedence is the
    other direction. In effect, this is making an implicit assumption that a
    module `__getattr__` will not handle (that is, will raise
    `AttributeError`) for names that are also actual submodules, rather than
    shadowing them. In practice this seems like a realistic assumption in
    the ecosystem? Or at least the ecosystem has adapted to it, and we need
    to adapt this precedence also, for ecosystem compatibility.
    
    The implementation is a bit ugly, precisely because it departs from the
    runtime semantics, and our implementation is oriented toward modeling
    runtime semantics accurately. That is, `__getattr__` is modeled within
    the member-lookup code, so it's hard to split "member lookup result from
    module `__getattr__`" apart from other member lookup results. I did this
    via a synthetic `TypeQualifier::FROM_MODULE_GETATTR` that we attach to a
    type resulting from a member lookup, which isn't beautiful but it works
    well and doesn't introduce inefficiency (e.g. redundant member lookups).
    
    ## Test Plan
    
    Updated mdtests.
    
    Also added a related mdtest formalizing our support for a module
    `__getattr__` that is explicitly annotated to accept a limited set of
    names. In principle this could be an alternative (more explicit) way to
    handle the precedence problem without departing from runtime semantics,
    if the ecosystem would adopt it.
    
    ### Ecosystem analysis
    
    Lots of removed diagnostics which are an improvement because we now
    infer the expected submodule.
    
    Added diagnostics are mostly unrelated issues surfaced now because we
    previously had an earlier attribute error resulting in `Unknown`; now we
    correctly resolve the module so that earlier attribute error goes away,
    we get an actual type instead of `Unknown`, and that triggers a new
    error.
    
    In scipy and sklearn, the module `__getattr__` which we were respecting
    previously is un-annotated so returned a forgiving `Unknown`; now we
    correctly see the actual module, which reveals some cases of
    https://github.com/astral-sh/ty/issues/133 that were previously hidden
    (`scipy/optimize/__init__.py` [imports `from
    ._tnc`](https://github.com/scipy/scipy/blob/eff82ca575668d2d7a4bc12b6afba98daaf6d5d0/scipy/optimize/__init__.py#L429).)
    
    ---------
    
    Co-authored-by: Alex Waygood 
    ---
     .../resources/mdtest/import/module_getattr.md | 51 +++++++++++++++++--
     crates/ty_python_semantic/src/types.rs        |  9 +++-
     .../src/types/infer/builder.rs                | 50 +++++++++++++-----
     3 files changed, 92 insertions(+), 18 deletions(-)
    
    diff --git a/crates/ty_python_semantic/resources/mdtest/import/module_getattr.md b/crates/ty_python_semantic/resources/mdtest/import/module_getattr.md
    index 4c493f4b74..79cce812fe 100644
    --- a/crates/ty_python_semantic/resources/mdtest/import/module_getattr.md
    +++ b/crates/ty_python_semantic/resources/mdtest/import/module_getattr.md
    @@ -60,11 +60,6 @@ def __getattr__(name: str) -> str:
     If a package's `__init__.py` (e.g. `mod/__init__.py`) defines a `__getattr__` function, and there is
     also a submodule file present (e.g. `mod/sub.py`), then:
     
    -- If you do `import mod` (without importing the submodule directly), accessing `mod.sub` will call
    -    `mod.__getattr__('sub')`, so `reveal_type(mod.sub)` will show the return type of `__getattr__`.
    -- If you do `import mod.sub` (importing the submodule directly), then `mod.sub` refers to the actual
    -    submodule, so `reveal_type(mod.sub)` will show the type of the submodule itself.
    -
     `mod/__init__.py`:
     
     ```py
    @@ -78,6 +73,9 @@ def __getattr__(name: str) -> str:
     value = 42
     ```
     
    +If you `import mod` (without importing the submodule directly), accessing `mod.sub` will call
    +`mod.__getattr__('sub')`, so `reveal_type(mod.sub)` will show the return type of `__getattr__`.
    +
     `test_import_mod.py`:
     
     ```py
    @@ -86,6 +84,9 @@ import mod
     reveal_type(mod.sub)  # revealed: str
     ```
     
    +If you `import mod.sub` (importing the submodule directly), then `mod.sub` refers to the actual
    +submodule, so `reveal_type(mod.sub)` will show the type of the submodule itself.
    +
     `test_import_mod_sub.py`:
     
     ```py
    @@ -93,3 +94,43 @@ import mod.sub
     
     reveal_type(mod.sub)  # revealed: 
     ```
    +
    +If you `from mod import sub`, at runtime `sub` will be the value returned by the module
    +`__getattr__`, but other type checkers do not model the precedence this way. They will always prefer
    +a submodule over a package `__getattr__`, and thus this is the current expectation in the ecosystem.
    +Effectively, this assumes that a well-implemented package `__getattr__` will always raise
    +`AttributeError` for a name that also exists as a submodule (and in fact this is the case for many
    +module `__getattr__` in the ecosystem.)
    +
    +`test_from_import.py`:
    +
    +```py
    +from mod import sub
    +
    +reveal_type(sub)  # revealed: 
    +```
    +
    +## Limiting names handled by `__getattr__`
    +
    +If a module `__getattr__` is annotated to only accept certain string literals, then the module
    +`__getattr__` will be ignored for other names. (In principle this could be a more explicit way to
    +handle the precedence issues discussed above, but it's not currently used in the ecosystem.)
    +
    +```py
    +from limited_getattr_module import known_attr
    +
    +# error: [unresolved-import]
    +from limited_getattr_module import unknown_attr
    +
    +reveal_type(known_attr)  # revealed: int
    +reveal_type(unknown_attr)  # revealed: Unknown
    +```
    +
    +`limited_getattr_module.py`:
    +
    +```py
    +from typing import Literal
    +
    +def __getattr__(name: Literal["known_attr"]) -> int:
    +    return 3
    +```
    diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
    index e79282a35d..3e3c241925 100644
    --- a/crates/ty_python_semantic/src/types.rs
    +++ b/crates/ty_python_semantic/src/types.rs
    @@ -7909,6 +7909,10 @@ bitflags! {
             /// instance attributes that are only implicitly defined via `self.x = …` in
             /// the body of a class method.
             const IMPLICIT_INSTANCE_ATTRIBUTE = 1 << 6;
    +        /// A non-standard type qualifier that marks a type returned from a module-level
    +        /// `__getattr__` function. We need this in order to implement precedence of submodules
    +        /// over module-level `__getattr__`, for compatibility with other type checkers.
    +        const FROM_MODULE_GETATTR = 1 << 7;
         }
     }
     
    @@ -11026,7 +11030,10 @@ impl<'db> ModuleLiteralType<'db> {
                         db,
                         &CallArguments::positional([Type::string_literal(db, name)]),
                     ) {
    -                    return Place::Defined(outcome.return_type(db), origin, boundness).into();
    +                    return PlaceAndQualifiers {
    +                        place: Place::Defined(outcome.return_type(db), origin, boundness),
    +                        qualifiers: TypeQualifiers::FROM_MODULE_GETATTR,
    +                    };
                     }
                 }
             }
    diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
    index 078d49fe5e..cbb2fe8236 100644
    --- a/crates/ty_python_semantic/src/types/infer/builder.rs
    +++ b/crates/ty_python_semantic/src/types/infer/builder.rs
    @@ -5347,6 +5347,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                 .as_module_literal()
                 .is_some_and(|module| Some(self.file()) == module.module(self.db()).file(self.db()));
     
    +        // 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 !import_is_self_referential {
                 if let PlaceAndQualifiers {
    @@ -5366,19 +5370,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                             ));
                         }
                     }
    -                self.add_declaration_with_binding(
    -                    alias.into(),
    -                    definition,
    -                    &DeclaredAndInferredType::MightBeDifferent {
    -                        declared_ty: TypeAndQualifiers {
    -                            inner: ty,
    -                            origin: TypeOrigin::Declared,
    -                            qualifiers,
    +                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,
                             },
    -                        inferred_ty: ty,
    -                    },
    -                );
    -                return;
    +                    );
    +                    return;
    +                }
                 }
             }
     
    @@ -5418,6 +5426,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                 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 == "*" {
    
    From 1fe958c694422fc283f1139533845e1ebe147356 Mon Sep 17 00:00:00 2001
    From: David Peter 
    Date: Mon, 3 Nov 2025 21:50:25 +0100
    Subject: [PATCH 165/188] [ty] Implicit type aliases: Support for PEP 604
     unions (#21195)
    
    ## Summary
    
    Add support for implicit type aliases that use PEP 604 unions:
    ```py
    IntOrStr = int | str
    
    reveal_type(IntOrStr)  # UnionType
    
    def _(int_or_str: IntOrStr):
        reveal_type(int_or_str)  # int | str
    ```
    
    ## Typing conformance
    
    The changes are either removed false positives, or new diagnostics due
    to known limitations unrelated to this PR.
    
    ## Ecosystem impact
    
    Spot checked, a mix of true positives and known limitations.
    
    ## Test Plan
    
    New Markdown tests.
    ---
     crates/ruff_benchmark/benches/ty_walltime.rs  |   2 +-
     .../resources/mdtest/annotations/union.md     |   5 +-
     .../resources/mdtest/implicit_type_aliases.md | 203 ++++++++++++++++++
     .../resources/mdtest/mro.md                   |  14 ++
     .../resources/mdtest/narrow/isinstance.md     |   6 +-
     .../resources/mdtest/narrow/issubclass.md     |   3 +-
     crates/ty_python_semantic/src/types.rs        |  63 +++++-
     crates/ty_python_semantic/src/types/class.rs  |   4 +-
     .../src/types/class_base.rs                   |   3 +-
     .../src/types/ide_support.rs                  |   4 +-
     .../src/types/infer/builder.rs                |  51 ++++-
     .../types/infer/builder/type_expression.rs    |   4 +
     .../ty_python_semantic/src/types/instance.rs  |   8 +
     13 files changed, 334 insertions(+), 36 deletions(-)
    
    diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs
    index 47bff641d7..55b2415990 100644
    --- a/crates/ruff_benchmark/benches/ty_walltime.rs
    +++ b/crates/ruff_benchmark/benches/ty_walltime.rs
    @@ -146,7 +146,7 @@ static FREQTRADE: Benchmark = Benchmark::new(
             max_dep_date: "2025-06-17",
             python_version: PythonVersion::PY312,
         },
    -    400,
    +    500,
     );
     
     static PANDAS: Benchmark = Benchmark::new(
    diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/union.md b/crates/ty_python_semantic/resources/mdtest/annotations/union.md
    index 776d077e27..8313d7142a 100644
    --- a/crates/ty_python_semantic/resources/mdtest/annotations/union.md
    +++ b/crates/ty_python_semantic/resources/mdtest/annotations/union.md
    @@ -72,9 +72,6 @@ def f(x: Union) -> None:
     
     ## Implicit type aliases using new-style unions
     
    -We don't recognize these as type aliases yet, but we also don't emit false-positive diagnostics if
    -you use them in type expressions:
    -
     ```toml
     [environment]
     python-version = "3.10"
    @@ -84,5 +81,5 @@ python-version = "3.10"
     X = int | str
     
     def f(y: X):
    -    reveal_type(y)  # revealed: @Todo(Support for `types.UnionType` instances in type expressions)
    +    reveal_type(y)  # revealed: int | str
     ```
    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 404d308083..904921e7b3 100644
    --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md
    +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md
    @@ -17,6 +17,209 @@ def f(x: MyInt):
     f(1)
     ```
     
    +## None
    +
    +```py
    +MyNone = None
    +
    +# TODO: this should not be an error
    +# error: [invalid-type-form] "Variable of type `None` is not allowed in a type expression"
    +def g(x: MyNone):
    +    # TODO: this should be `None`
    +    reveal_type(x)  # revealed: Unknown
    +
    +g(None)
    +```
    +
    +## Unions
    +
    +We also support unions in type aliases:
    +
    +```py
    +from typing_extensions import Any, Never
    +from ty_extensions import Unknown
    +
    +IntOrStr = int | str
    +IntOrStrOrBytes1 = int | str | bytes
    +IntOrStrOrBytes2 = (int | str) | bytes
    +IntOrStrOrBytes3 = int | (str | bytes)
    +IntOrStrOrBytes4 = IntOrStr | bytes
    +BytesOrIntOrStr = bytes | IntOrStr
    +IntOrNone = int | None
    +NoneOrInt = None | int
    +IntOrStrOrNone = IntOrStr | None
    +NoneOrIntOrStr = None | IntOrStr
    +IntOrAny = int | Any
    +AnyOrInt = Any | int
    +NoneOrAny = None | Any
    +AnyOrNone = Any | None
    +NeverOrAny = Never | Any
    +AnyOrNever = Any | Never
    +UnknownOrInt = Unknown | int
    +IntOrUnknown = int | Unknown
    +
    +reveal_type(IntOrStr)  # revealed: UnionType
    +reveal_type(IntOrStrOrBytes1)  # revealed: UnionType
    +reveal_type(IntOrStrOrBytes2)  # revealed: UnionType
    +reveal_type(IntOrStrOrBytes3)  # revealed: UnionType
    +reveal_type(IntOrStrOrBytes4)  # revealed: UnionType
    +reveal_type(BytesOrIntOrStr)  # revealed: UnionType
    +reveal_type(IntOrNone)  # revealed: UnionType
    +reveal_type(NoneOrInt)  # revealed: UnionType
    +reveal_type(IntOrStrOrNone)  # revealed: UnionType
    +reveal_type(NoneOrIntOrStr)  # revealed: UnionType
    +reveal_type(IntOrAny)  # revealed: UnionType
    +reveal_type(AnyOrInt)  # revealed: UnionType
    +reveal_type(NoneOrAny)  # revealed: UnionType
    +reveal_type(AnyOrNone)  # revealed: UnionType
    +reveal_type(NeverOrAny)  # revealed: UnionType
    +reveal_type(AnyOrNever)  # revealed: UnionType
    +reveal_type(UnknownOrInt)  # revealed: UnionType
    +reveal_type(IntOrUnknown)  # revealed: UnionType
    +
    +def _(
    +    int_or_str: IntOrStr,
    +    int_or_str_or_bytes1: IntOrStrOrBytes1,
    +    int_or_str_or_bytes2: IntOrStrOrBytes2,
    +    int_or_str_or_bytes3: IntOrStrOrBytes3,
    +    int_or_str_or_bytes4: IntOrStrOrBytes4,
    +    bytes_or_int_or_str: BytesOrIntOrStr,
    +    int_or_none: IntOrNone,
    +    none_or_int: NoneOrInt,
    +    int_or_str_or_none: IntOrStrOrNone,
    +    none_or_int_or_str: NoneOrIntOrStr,
    +    int_or_any: IntOrAny,
    +    any_or_int: AnyOrInt,
    +    none_or_any: NoneOrAny,
    +    any_or_none: AnyOrNone,
    +    never_or_any: NeverOrAny,
    +    any_or_never: AnyOrNever,
    +    unknown_or_int: UnknownOrInt,
    +    int_or_unknown: IntOrUnknown,
    +):
    +    reveal_type(int_or_str)  # revealed: int | str
    +    reveal_type(int_or_str_or_bytes1)  # revealed: int | str | bytes
    +    reveal_type(int_or_str_or_bytes2)  # revealed: int | str | bytes
    +    reveal_type(int_or_str_or_bytes3)  # revealed: int | str | bytes
    +    reveal_type(int_or_str_or_bytes4)  # revealed: int | str | bytes
    +    reveal_type(bytes_or_int_or_str)  # revealed: bytes | int | str
    +    reveal_type(int_or_none)  # revealed: int | None
    +    reveal_type(none_or_int)  # revealed: None | int
    +    reveal_type(int_or_str_or_none)  # revealed: int | str | None
    +    reveal_type(none_or_int_or_str)  # revealed: None | int | str
    +    reveal_type(int_or_any)  # revealed: int | Any
    +    reveal_type(any_or_int)  # revealed: Any | int
    +    reveal_type(none_or_any)  # revealed: None | Any
    +    reveal_type(any_or_none)  # revealed: Any | None
    +    reveal_type(never_or_any)  # revealed: Any
    +    reveal_type(any_or_never)  # revealed: Any
    +    reveal_type(unknown_or_int)  # revealed: Unknown | int
    +    reveal_type(int_or_unknown)  # revealed: int | Unknown
    +```
    +
    +If a type is unioned with itself in a value expression, the result is just that type. No
    +`types.UnionType` instance is created:
    +
    +```py
    +IntOrInt = int | int
    +ListOfIntOrListOfInt = list[int] | list[int]
    +
    +reveal_type(IntOrInt)  # revealed: 
    +reveal_type(ListOfIntOrListOfInt)  # revealed: 
    +
    +def _(int_or_int: IntOrInt, list_of_int_or_list_of_int: ListOfIntOrListOfInt):
    +    reveal_type(int_or_int)  # revealed: int
    +    reveal_type(list_of_int_or_list_of_int)  # revealed: list[int]
    +```
    +
    +`NoneType` has no special or-operator behavior, so this is an error:
    +
    +```py
    +None | None  # error: [unsupported-operator] "Operator `|` is unsupported between objects of type `None` and `None`"
    +```
    +
    +When constructing something non-sensical like `int | 1`, we could ideally emit a diagnostic for the
    +expression itself, as it leads to a `TypeError` at runtime. No other type checker supports this, so
    +for now we only emit an error when it is used in a type expression:
    +
    +```py
    +IntOrOne = int | 1
    +
    +# error: [invalid-type-form] "Variable of type `Literal[1]` is not allowed in a type expression"
    +def _(int_or_one: IntOrOne):
    +    reveal_type(int_or_one)  # revealed: Unknown
    +```
    +
    +If you were to somehow get hold of an opaque instance of `types.UnionType`, that could not be used
    +as a type expression:
    +
    +```py
    +from types import UnionType
    +
    +def f(SomeUnionType: UnionType):
    +    # error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression"
    +    some_union: SomeUnionType
    +
    +f(int | str)
    +```
    +
    +## Generic types
    +
    +Implicit type aliases can also refer to generic types:
    +
    +```py
    +from typing_extensions import TypeVar
    +
    +T = TypeVar("T")
    +
    +MyList = list[T]
    +
    +def _(my_list: MyList[int]):
    +    # TODO: This should be `list[int]`
    +    reveal_type(my_list)  # revealed: @Todo(unknown type subscript)
    +
    +ListOrTuple = list[T] | tuple[T, ...]
    +
    +reveal_type(ListOrTuple)  # revealed: UnionType
    +
    +def _(list_or_tuple: ListOrTuple[int]):
    +    reveal_type(list_or_tuple)  # revealed: @Todo(Generic specialization of types.UnionType)
    +```
    +
    +## Stringified annotations?
    +
    +From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/aliases.html):
    +
    +> Type aliases may be as complex as type hints in annotations – anything that is acceptable as a
    +> type hint is acceptable in a type alias
    +
    +However, no other type checker seems to support stringified annotations in implicit type aliases. We
    +currently also do not support them:
    +
    +```py
    +AliasForStr = "str"
    +
    +# error: [invalid-type-form] "Variable of type `Literal["str"]` is not allowed in a type expression"
    +def _(s: AliasForStr):
    +    reveal_type(s)  # revealed: Unknown
    +
    +IntOrStr = int | "str"
    +
    +# error: [invalid-type-form] "Variable of type `Literal["str"]` is not allowed in a type expression"
    +def _(int_or_str: IntOrStr):
    +    reveal_type(int_or_str)  # revealed: Unknown
    +```
    +
    +We *do* support stringified annotations if they appear in a position where a type expression is
    +syntactically expected:
    +
    +```py
    +ListOfInts = list["int"]
    +
    +def _(list_of_ints: ListOfInts):
    +    reveal_type(list_of_ints)  # revealed: list[int]
    +```
    +
     ## Recursive
     
     ### Old union syntax
    diff --git a/crates/ty_python_semantic/resources/mdtest/mro.md b/crates/ty_python_semantic/resources/mdtest/mro.md
    index da9a40b4a7..81a1cfe667 100644
    --- a/crates/ty_python_semantic/resources/mdtest/mro.md
    +++ b/crates/ty_python_semantic/resources/mdtest/mro.md
    @@ -291,6 +291,20 @@ class Foo(x): ...
     reveal_mro(Foo)  # revealed: (, Unknown, )
     ```
     
    +## `UnionType` instances are now allowed as a base
    +
    +This is not legal:
    +
    +```py
    +class A: ...
    +class B: ...
    +
    +EitherOr = A | B
    +
    +# error: [invalid-base] "Invalid class base with type `UnionType`"
    +class Foo(EitherOr): ...
    +```
    +
     ## `__bases__` is a union of a dynamic type and valid bases
     
     If a dynamic type such as `Any` or `Unknown` is one of the elements in the union, and all other
    diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md
    index 60ec2fa844..375cc55b29 100644
    --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md
    +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md
    @@ -146,13 +146,11 @@ def _(flag: bool):
     def _(flag: bool):
         x = 1 if flag else "a"
     
    -    # TODO: this should cause us to emit a diagnostic during
    -    # type checking
    +    # error: [invalid-argument-type] "Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Unknown, ...]`, found `Literal["a"]"
         if isinstance(x, "a"):
             reveal_type(x)  # revealed: Literal[1, "a"]
     
    -    # TODO: this should cause us to emit a diagnostic during
    -    # type checking
    +    # error: [invalid-argument-type] "Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Unknown, ...]`, found `Literal["int"]"
         if isinstance(x, "int"):
             reveal_type(x)  # revealed: Literal[1, "a"]
     ```
    diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md
    index ce77126d32..052b4de2fe 100644
    --- a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md
    +++ b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md
    @@ -214,8 +214,7 @@ def flag() -> bool:
     
     t = int if flag() else str
     
    -# TODO: this should cause us to emit a diagnostic during
    -# type checking
    +# error: [invalid-argument-type] "Argument to function `issubclass` is incorrect: Expected `type | UnionType | tuple[Unknown, ...]`, found `Literal["str"]"
     if issubclass(t, "str"):
         reveal_type(t)  # revealed:  | 
     
    diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
    index 3e3c241925..60da8a8f72 100644
    --- a/crates/ty_python_semantic/src/types.rs
    +++ b/crates/ty_python_semantic/src/types.rs
    @@ -817,13 +817,11 @@ impl<'db> Type<'db> {
         }
     
         fn is_none(&self, db: &'db dyn Db) -> bool {
    -        self.as_nominal_instance()
    -            .is_some_and(|instance| instance.has_known_class(db, KnownClass::NoneType))
    +        self.is_instance_of(db, KnownClass::NoneType)
         }
     
         fn is_bool(&self, db: &'db dyn Db) -> bool {
    -        self.as_nominal_instance()
    -            .is_some_and(|instance| instance.has_known_class(db, KnownClass::Bool))
    +        self.is_instance_of(db, KnownClass::Bool)
         }
     
         fn is_enum(&self, db: &'db dyn Db) -> bool {
    @@ -857,8 +855,7 @@ impl<'db> Type<'db> {
         }
     
         pub(crate) fn is_notimplemented(&self, db: &'db dyn Db) -> bool {
    -        self.as_nominal_instance()
    -            .is_some_and(|instance| instance.has_known_class(db, KnownClass::NotImplementedType))
    +        self.is_instance_of(db, KnownClass::NotImplementedType)
         }
     
         pub(crate) const fn is_todo(&self) -> bool {
    @@ -6436,6 +6433,17 @@ impl<'db> Type<'db> {
                         invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::Generic],
                         fallback_type: Type::unknown(),
                     }),
    +                KnownInstanceType::UnionType(union_type) => {
    +                    let mut builder = UnionBuilder::new(db);
    +                    for element in union_type.elements(db) {
    +                        builder = builder.add(element.in_type_expression(
    +                            db,
    +                            scope_id,
    +                            typevar_binding_context,
    +                        )?);
    +                    }
    +                    Ok(builder.build())
    +                }
                 },
     
                 Type::SpecialForm(special_form) => match special_form {
    @@ -6604,9 +6612,6 @@ impl<'db> Type<'db> {
                     Some(KnownClass::GenericAlias) => Ok(todo_type!(
                         "Support for `typing.GenericAlias` instances in type expressions"
                     )),
    -                Some(KnownClass::UnionType) => Ok(todo_type!(
    -                    "Support for `types.UnionType` instances in type expressions"
    -                )),
                     _ => Err(InvalidTypeExpressionError {
                         invalid_expressions: smallvec::smallvec_inline![
                             InvalidTypeExpression::InvalidType(*self, scope_id)
    @@ -7646,6 +7651,10 @@ pub enum KnownInstanceType<'db> {
         /// A constraint set, which is exposed in mdtests as an instance of
         /// `ty_extensions.ConstraintSet`.
         ConstraintSet(TrackedConstraintSet<'db>),
    +
    +    /// A single instance of `types.UnionType`, which stores the left- and
    +    /// right-hand sides of a PEP 604 union.
    +    UnionType(UnionTypeInstance<'db>),
     }
     
     fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
    @@ -7672,6 +7681,11 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
                     visitor.visit_type(db, default_ty);
                 }
             }
    +        KnownInstanceType::UnionType(union_type) => {
    +            for element in union_type.elements(db) {
    +                visitor.visit_type(db, element);
    +            }
    +        }
         }
     }
     
    @@ -7708,6 +7722,7 @@ impl<'db> KnownInstanceType<'db> {
                     // Nothing to normalize
                     Self::ConstraintSet(set)
                 }
    +            Self::UnionType(union_type) => Self::UnionType(union_type.normalized_impl(db, visitor)),
             }
         }
     
    @@ -7722,6 +7737,7 @@ impl<'db> KnownInstanceType<'db> {
                 Self::Deprecated(_) => KnownClass::Deprecated,
                 Self::Field(_) => KnownClass::Field,
                 Self::ConstraintSet(_) => KnownClass::ConstraintSet,
    +            Self::UnionType(_) => KnownClass::UnionType,
             }
         }
     
    @@ -7795,6 +7811,7 @@ impl<'db> KnownInstanceType<'db> {
                                 constraints.display(self.db)
                             )
                         }
    +                    KnownInstanceType::UnionType(_) => f.write_str("UnionType"),
                     }
                 }
             }
    @@ -8918,6 +8935,34 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
         }
     }
     
    +/// An instance of `types.UnionType`.
    +///
    +/// # 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)]
    +#[derive(PartialOrd, Ord)]
    +pub struct UnionTypeInstance<'db> {
    +    left: Type<'db>,
    +    right: Type<'db>,
    +}
    +
    +impl get_size2::GetSize for UnionTypeInstance<'_> {}
    +
    +impl<'db> UnionTypeInstance<'db> {
    +    pub(crate) fn elements(self, db: &'db dyn Db) -> [Type<'db>; 2] {
    +        [self.left(db), self.right(db)]
    +    }
    +
    +    pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
    +        UnionTypeInstance::new(
    +            db,
    +            self.left(db).normalized_impl(db, visitor),
    +            self.right(db).normalized_impl(db, visitor),
    +        )
    +    }
    +}
    +
     /// Error returned if a type is not awaitable.
     #[derive(Debug)]
     enum AwaitError<'db> {
    diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs
    index c3ff51e47f..1b794aefe0 100644
    --- a/crates/ty_python_semantic/src/types/class.rs
    +++ b/crates/ty_python_semantic/src/types/class.rs
    @@ -1307,9 +1307,7 @@ 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 {
    -        self.declared_ty
    -            .as_nominal_instance()
    -            .is_some_and(|instance| instance.has_known_class(db, KnownClass::KwOnly))
    +        self.declared_ty.is_instance_of(db, KnownClass::KwOnly)
         }
     }
     
    diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs
    index 4d43b58d06..bed18de8b9 100644
    --- a/crates/ty_python_semantic/src/types/class_base.rs
    +++ b/crates/ty_python_semantic/src/types/class_base.rs
    @@ -170,7 +170,8 @@ impl<'db> ClassBase<'db> {
                     | KnownInstanceType::TypeVar(_)
                     | KnownInstanceType::Deprecated(_)
                     | KnownInstanceType::Field(_)
    -                | KnownInstanceType::ConstraintSet(_) => None,
    +                | KnownInstanceType::ConstraintSet(_)
    +                | KnownInstanceType::UnionType(_) => None,
                 },
     
                 Type::SpecialForm(special_form) => match special_form {
    diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs
    index 02a9330299..94cb15993c 100644
    --- a/crates/ty_python_semantic/src/types/ide_support.rs
    +++ b/crates/ty_python_semantic/src/types/ide_support.rs
    @@ -290,7 +290,9 @@ impl<'db> AllMembers<'db> {
                                 }
                                 Type::ClassLiteral(class) if class.is_protocol(db) => continue,
                                 Type::KnownInstance(
    -                                KnownInstanceType::TypeVar(_) | KnownInstanceType::TypeAliasType(_),
    +                                KnownInstanceType::TypeVar(_)
    +                                | KnownInstanceType::TypeAliasType(_)
    +                                | KnownInstanceType::UnionType(_),
                                 ) => continue,
                                 Type::Dynamic(DynamicType::TodoTypeAlias) => continue,
                                 _ => {}
    diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
    index cbb2fe8236..1ad7d18482 100644
    --- a/crates/ty_python_semantic/src/types/infer/builder.rs
    +++ b/crates/ty_python_semantic/src/types/infer/builder.rs
    @@ -103,7 +103,7 @@ use crate::types::{
         TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
         TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity,
         TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType,
    -    binding_type, todo_type,
    +    UnionTypeInstance, binding_type, todo_type,
     };
     use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
     use crate::unpack::{EvaluationMode, UnpackPosition};
    @@ -8449,19 +8449,48 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                     )))
                 }
     
    -            // Special-case `X | Y` with `X` and `Y` instances of `type` to produce a `types.UnionType` instance, in order to
    -            // overwrite the typeshed return type for `type.__or__`, which would result in `types.UnionType | X`. We currently
    -            // do this to avoid false positives when a legacy type alias like `IntOrStr = int | str` is later used in a type
    -            // expression, because `types.UnionType` will result in a `@Todo` type, while `types.UnionType | ` does
    -            // not.
    -            //
    -            // TODO: Remove this special case once we add support for legacy type aliases.
    +            // PEP 604-style union types using the `|` operator.
                 (
    -                Type::ClassLiteral(..) | Type::SubclassOf(..) | Type::GenericAlias(..),
    -                Type::ClassLiteral(..) | Type::SubclassOf(..) | Type::GenericAlias(..),
    +                Type::ClassLiteral(..)
    +                | Type::SubclassOf(..)
    +                | Type::GenericAlias(..)
    +                | Type::SpecialForm(_)
    +                | Type::KnownInstance(KnownInstanceType::UnionType(_)),
    +                _,
    +                ast::Operator::BitOr,
    +            )
    +            | (
    +                _,
    +                Type::ClassLiteral(..)
    +                | Type::SubclassOf(..)
    +                | Type::GenericAlias(..)
    +                | Type::SpecialForm(_)
    +                | Type::KnownInstance(KnownInstanceType::UnionType(_)),
                     ast::Operator::BitOr,
                 ) if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310 => {
    -                Some(KnownClass::UnionType.to_instance(self.db()))
    +                // For a value expression like `int | None`, the inferred type for `None` will be
    +                // a nominal instance of `NoneType`, so we need to convert it to a class literal
    +                // such that it can later be converted back to a nominal instance type when calling
    +                // `.in_type_expression` on the `UnionType` instance.
    +                let convert_none_type = |ty: Type<'db>| {
    +                    if ty.is_none(self.db()) {
    +                        KnownClass::NoneType.to_class_literal(self.db())
    +                    } else {
    +                        ty
    +                    }
    +                };
    +
    +                if left_ty.is_equivalent_to(self.db(), right_ty) {
    +                    Some(left_ty)
    +                } else {
    +                    Some(Type::KnownInstance(KnownInstanceType::UnionType(
    +                        UnionTypeInstance::new(
    +                            self.db(),
    +                            convert_none_type(left_ty),
    +                            convert_none_type(right_ty),
    +                        ),
    +                    )))
    +                }
                 }
     
                 // We've handled all of the special cases that we support for literals, so we need to
    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 50d22fac10..1e1ff82c0b 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
    @@ -810,6 +810,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
                         self.infer_type_expression(slice);
                         todo_type!("Generic manual PEP-695 type alias")
                     }
    +                KnownInstanceType::UnionType(_) => {
    +                    self.infer_type_expression(slice);
    +                    todo_type!("Generic specialization of types.UnionType")
    +                }
                 },
                 Type::Dynamic(DynamicType::Todo(_)) => {
                     self.infer_type_expression(slice);
    diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs
    index f6c7b8406d..8c5adc9e0d 100644
    --- a/crates/ty_python_semantic/src/types/instance.rs
    +++ b/crates/ty_python_semantic/src/types/instance.rs
    @@ -95,6 +95,14 @@ 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 {
    +        match self {
    +            Type::NominalInstance(instance) => instance.class(db).is_known(db, known_class),
    +            _ => false,
    +        }
    +    }
    +
         /// 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
         where
    
    From d2fe6347fb9d6c4b2c27d2fda02a1b857452fb66 Mon Sep 17 00:00:00 2001
    From: David Peter 
    Date: Mon, 3 Nov 2025 22:06:56 +0100
    Subject: [PATCH 166/188] [ty] Rename `UnionType` to `types.UnionType` (#21262)
    
    ---
     .../resources/mdtest/binary/classes.md        | 12 +++---
     .../resources/mdtest/implicit_type_aliases.md | 38 +++++++++----------
     .../resources/mdtest/mro.md                   |  2 +-
     crates/ty_python_semantic/src/types.rs        |  2 +-
     4 files changed, 27 insertions(+), 27 deletions(-)
    
    diff --git a/crates/ty_python_semantic/resources/mdtest/binary/classes.md b/crates/ty_python_semantic/resources/mdtest/binary/classes.md
    index 7ae4c23e60..db42286c84 100644
    --- a/crates/ty_python_semantic/resources/mdtest/binary/classes.md
    +++ b/crates/ty_python_semantic/resources/mdtest/binary/classes.md
    @@ -13,7 +13,7 @@ python-version = "3.10"
     class A: ...
     class B: ...
     
    -reveal_type(A | B)  # revealed: UnionType
    +reveal_type(A | B)  # revealed: types.UnionType
     ```
     
     ## Union of two classes (prior to 3.10)
    @@ -43,14 +43,14 @@ class A: ...
     class B: ...
     
     def _(sub_a: type[A], sub_b: type[B]):
    -    reveal_type(A | sub_b)  # revealed: UnionType
    -    reveal_type(sub_a | B)  # revealed: UnionType
    -    reveal_type(sub_a | sub_b)  # revealed: UnionType
    +    reveal_type(A | sub_b)  # revealed: types.UnionType
    +    reveal_type(sub_a | B)  # revealed: types.UnionType
    +    reveal_type(sub_a | sub_b)  # revealed: types.UnionType
     
     class C[T]: ...
     class D[T]: ...
     
    -reveal_type(C | D)  # revealed: UnionType
    +reveal_type(C | D)  # revealed: types.UnionType
     
    -reveal_type(C[int] | D[str])  # revealed: UnionType
    +reveal_type(C[int] | D[str])  # revealed: types.UnionType
     ```
    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 904921e7b3..a3e0319f5a 100644
    --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md
    +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md
    @@ -58,24 +58,24 @@ AnyOrNever = Any | Never
     UnknownOrInt = Unknown | int
     IntOrUnknown = int | Unknown
     
    -reveal_type(IntOrStr)  # revealed: UnionType
    -reveal_type(IntOrStrOrBytes1)  # revealed: UnionType
    -reveal_type(IntOrStrOrBytes2)  # revealed: UnionType
    -reveal_type(IntOrStrOrBytes3)  # revealed: UnionType
    -reveal_type(IntOrStrOrBytes4)  # revealed: UnionType
    -reveal_type(BytesOrIntOrStr)  # revealed: UnionType
    -reveal_type(IntOrNone)  # revealed: UnionType
    -reveal_type(NoneOrInt)  # revealed: UnionType
    -reveal_type(IntOrStrOrNone)  # revealed: UnionType
    -reveal_type(NoneOrIntOrStr)  # revealed: UnionType
    -reveal_type(IntOrAny)  # revealed: UnionType
    -reveal_type(AnyOrInt)  # revealed: UnionType
    -reveal_type(NoneOrAny)  # revealed: UnionType
    -reveal_type(AnyOrNone)  # revealed: UnionType
    -reveal_type(NeverOrAny)  # revealed: UnionType
    -reveal_type(AnyOrNever)  # revealed: UnionType
    -reveal_type(UnknownOrInt)  # revealed: UnionType
    -reveal_type(IntOrUnknown)  # revealed: UnionType
    +reveal_type(IntOrStr)  # revealed: types.UnionType
    +reveal_type(IntOrStrOrBytes1)  # revealed: types.UnionType
    +reveal_type(IntOrStrOrBytes2)  # revealed: types.UnionType
    +reveal_type(IntOrStrOrBytes3)  # revealed: types.UnionType
    +reveal_type(IntOrStrOrBytes4)  # revealed: types.UnionType
    +reveal_type(BytesOrIntOrStr)  # revealed: types.UnionType
    +reveal_type(IntOrNone)  # revealed: types.UnionType
    +reveal_type(NoneOrInt)  # revealed: types.UnionType
    +reveal_type(IntOrStrOrNone)  # revealed: types.UnionType
    +reveal_type(NoneOrIntOrStr)  # revealed: types.UnionType
    +reveal_type(IntOrAny)  # revealed: types.UnionType
    +reveal_type(AnyOrInt)  # revealed: types.UnionType
    +reveal_type(NoneOrAny)  # revealed: types.UnionType
    +reveal_type(AnyOrNone)  # revealed: types.UnionType
    +reveal_type(NeverOrAny)  # revealed: types.UnionType
    +reveal_type(AnyOrNever)  # revealed: types.UnionType
    +reveal_type(UnknownOrInt)  # revealed: types.UnionType
    +reveal_type(IntOrUnknown)  # revealed: types.UnionType
     
     def _(
         int_or_str: IntOrStr,
    @@ -180,7 +180,7 @@ def _(my_list: MyList[int]):
     
     ListOrTuple = list[T] | tuple[T, ...]
     
    -reveal_type(ListOrTuple)  # revealed: UnionType
    +reveal_type(ListOrTuple)  # revealed: types.UnionType
     
     def _(list_or_tuple: ListOrTuple[int]):
         reveal_type(list_or_tuple)  # revealed: @Todo(Generic specialization of types.UnionType)
    diff --git a/crates/ty_python_semantic/resources/mdtest/mro.md b/crates/ty_python_semantic/resources/mdtest/mro.md
    index 81a1cfe667..761fb9892d 100644
    --- a/crates/ty_python_semantic/resources/mdtest/mro.md
    +++ b/crates/ty_python_semantic/resources/mdtest/mro.md
    @@ -301,7 +301,7 @@ class B: ...
     
     EitherOr = A | B
     
    -# error: [invalid-base] "Invalid class base with type `UnionType`"
    +# error: [invalid-base] "Invalid class base with type `types.UnionType`"
     class Foo(EitherOr): ...
     ```
     
    diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
    index 60da8a8f72..bdc859b095 100644
    --- a/crates/ty_python_semantic/src/types.rs
    +++ b/crates/ty_python_semantic/src/types.rs
    @@ -7811,7 +7811,7 @@ impl<'db> KnownInstanceType<'db> {
                                 constraints.display(self.db)
                             )
                         }
    -                    KnownInstanceType::UnionType(_) => f.write_str("UnionType"),
    +                    KnownInstanceType::UnionType(_) => f.write_str("types.UnionType"),
                     }
                 }
             }
    
    From 79a02711c12d5381a640c7b07639e058917a3234 Mon Sep 17 00:00:00 2001
    From: chiri 
    Date: Tue, 4 Nov 2025 00:09:02 +0300
    Subject: [PATCH 167/188] [`refurb`] Expand fix safety for keyword arguments
     and `Decimal`s (`FURB164`) (#21259)
    
    ## Summary
    
    Fixes https://github.com/astral-sh/ruff/issues/21257
    
    ## Test Plan
    
    `cargo nextest run furb164`
    ---
     .../resources/test/fixtures/refurb/FURB164.py |  5 +++
     .../refurb/rules/unnecessary_from_float.rs    | 39 +++++++++--------
     ...es__refurb__tests__FURB164_FURB164.py.snap | 43 +++++++++++++++++--
     3 files changed, 67 insertions(+), 20 deletions(-)
    
    diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py
    index 9a03919ca9..81422d2cf8 100644
    --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py
    +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py
    @@ -64,3 +64,8 @@ _ = Decimal.from_float(True)
     _ = Decimal.from_float(float("-nan"))
     _ = Decimal.from_float(float("\x2dnan"))
     _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
    +
    +# See: https://github.com/astral-sh/ruff/issues/21257
    +# fixes must be safe
    +_ = Fraction.from_float(f=4.2)
    +_ = Fraction.from_decimal(dec=4)
    \ No newline at end of file
    diff --git a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs
    index e34357bd55..38184c8fd3 100644
    --- a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs
    +++ b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs
    @@ -149,10 +149,9 @@ pub(crate) fn unnecessary_from_float(checker: &Checker, call: &ExprCall) {
     
         // Check if we should suppress the fix due to type validation concerns
         let is_type_safe = is_valid_argument_type(arg_value, method_name, constructor, checker);
    -    let has_keywords = !call.arguments.keywords.is_empty();
     
         // Determine fix safety
    -    let applicability = if is_type_safe && !has_keywords {
    +    let applicability = if is_type_safe {
             Applicability::Safe
         } else {
             Applicability::Unsafe
    @@ -210,21 +209,27 @@ fn is_valid_argument_type(
                 _ => false,
             },
             // Fraction.from_decimal accepts int, bool, Decimal
    -        (MethodName::FromDecimal, Constructor::Fraction) => match resolved_type {
    -            ResolvedPythonType::Atom(PythonType::Number(
    -                NumberLike::Integer | NumberLike::Bool,
    -            )) => true,
    -            ResolvedPythonType::Unknown => is_int,
    -            _ => {
    -                // Check if it's a Decimal instance
    -                arg_expr
    -                    .as_call_expr()
    -                    .and_then(|call| semantic.resolve_qualified_name(&call.func))
    -                    .is_some_and(|qualified_name| {
    -                        matches!(qualified_name.segments(), ["decimal", "Decimal"])
    -                    })
    +        (MethodName::FromDecimal, Constructor::Fraction) => {
    +            // First check if it's a Decimal constructor call
    +            let is_decimal_call = arg_expr
    +                .as_call_expr()
    +                .and_then(|call| semantic.resolve_qualified_name(&call.func))
    +                .is_some_and(|qualified_name| {
    +                    matches!(qualified_name.segments(), ["decimal", "Decimal"])
    +                });
    +
    +            if is_decimal_call {
    +                return true;
                 }
    -        },
    +
    +            match resolved_type {
    +                ResolvedPythonType::Atom(PythonType::Number(
    +                    NumberLike::Integer | NumberLike::Bool,
    +                )) => true,
    +                ResolvedPythonType::Unknown => is_int,
    +                _ => false,
    +            }
    +        }
             _ => false,
         }
     }
    @@ -274,7 +279,7 @@ fn handle_non_finite_float_special_case(
             return None;
         }
     
    -    let Expr::Call(ast::ExprCall {
    +    let Expr::Call(ExprCall {
             func, arguments, ..
         }) = arg_value
         else {
    diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap
    index e917928a64..7bd2ce8225 100644
    --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap
    +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap
    @@ -99,7 +99,6 @@ help: Replace with `Fraction` constructor
     12 | _ = Fraction.from_decimal(Decimal("-4.2"))
     13 | _ = Fraction.from_decimal(Decimal.from_float(4.2))
     14 | _ = Decimal.from_float(0.1)
    -note: This is an unsafe fix and may change runtime behavior
     
     FURB164 [*] Verbose method `from_decimal` in `Fraction` construction
       --> FURB164.py:12:5
    @@ -120,7 +119,6 @@ help: Replace with `Fraction` constructor
     13 | _ = Fraction.from_decimal(Decimal.from_float(4.2))
     14 | _ = Decimal.from_float(0.1)
     15 | _ = Decimal.from_float(-0.5)
    -note: This is an unsafe fix and may change runtime behavior
     
     FURB164 [*] Verbose method `from_decimal` in `Fraction` construction
       --> FURB164.py:13:5
    @@ -484,7 +482,6 @@ help: Replace with `Fraction` constructor
     32 | _ = Decimal.from_float(f=4.2)
     33 | 
     34 | # Cases with invalid argument counts - should not get fixes
    -note: This is an unsafe fix and may change runtime behavior
     
     FURB164 Verbose method `from_float` in `Decimal` construction
       --> FURB164.py:32:5
    @@ -658,6 +655,7 @@ help: Replace with `Decimal` constructor
     64 + _ = Decimal("nan")
     65 | _ = Decimal.from_float(float("\x2dnan"))
     66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
    +67 | 
     
     FURB164 [*] Verbose method `from_float` in `Decimal` construction
       --> FURB164.py:65:5
    @@ -675,6 +673,8 @@ help: Replace with `Decimal` constructor
        - _ = Decimal.from_float(float("\x2dnan"))
     65 + _ = Decimal("nan")
     66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
    +67 | 
    +68 | # See: https://github.com/astral-sh/ruff/issues/21257
     
     FURB164 [*] Verbose method `from_float` in `Decimal` construction
       --> FURB164.py:66:5
    @@ -683,6 +683,8 @@ FURB164 [*] Verbose method `from_float` in `Decimal` construction
     65 | _ = Decimal.from_float(float("\x2dnan"))
     66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
        |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    +67 |
    +68 | # See: https://github.com/astral-sh/ruff/issues/21257
        |
     help: Replace with `Decimal` constructor
     63 | # Cases with non-finite floats - should produce safe fixes
    @@ -690,3 +692,38 @@ help: Replace with `Decimal` constructor
     65 | _ = Decimal.from_float(float("\x2dnan"))
        - _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
     66 + _ = Decimal("nan")
    +67 | 
    +68 | # See: https://github.com/astral-sh/ruff/issues/21257
    +69 | # fixes must be safe
    +
    +FURB164 [*] Verbose method `from_float` in `Fraction` construction
    +  --> FURB164.py:70:5
    +   |
    +68 | # See: https://github.com/astral-sh/ruff/issues/21257
    +69 | # fixes must be safe
    +70 | _ = Fraction.from_float(f=4.2)
    +   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
    +71 | _ = Fraction.from_decimal(dec=4)
    +   |
    +help: Replace with `Fraction` constructor
    +67 | 
    +68 | # See: https://github.com/astral-sh/ruff/issues/21257
    +69 | # fixes must be safe
    +   - _ = Fraction.from_float(f=4.2)
    +70 + _ = Fraction(4.2)
    +71 | _ = Fraction.from_decimal(dec=4)
    +
    +FURB164 [*] Verbose method `from_decimal` in `Fraction` construction
    +  --> FURB164.py:71:5
    +   |
    +69 | # fixes must be safe
    +70 | _ = Fraction.from_float(f=4.2)
    +71 | _ = Fraction.from_decimal(dec=4)
    +   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    +   |
    +help: Replace with `Fraction` constructor
    +68 | # See: https://github.com/astral-sh/ruff/issues/21257
    +69 | # fixes must be safe
    +70 | _ = Fraction.from_float(f=4.2)
    +   - _ = Fraction.from_decimal(dec=4)
    +71 + _ = Fraction(4)
    
    From 42adfd40ea432b4e8dfff89155b3afc7dd803f92 Mon Sep 17 00:00:00 2001
    From: Alex Waygood 
    Date: Mon, 3 Nov 2025 16:53:42 -0500
    Subject: [PATCH 168/188] Run py-fuzzer with `--profile=profiling` locally and
     in CI (#21266)
    
    ---
     .github/workflows/ci.yaml | 42 +++++++++++++++++++++------------------
     python/py-fuzzer/fuzz.py  |  5 +++--
     2 files changed, 26 insertions(+), 21 deletions(-)
    
    diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
    index 7130f15b29..44d9f2f542 100644
    --- a/.github/workflows/ci.yaml
    +++ b/.github/workflows/ci.yaml
    @@ -437,6 +437,8 @@ jobs:
               workspaces: "fuzz -> target"
           - name: "Install Rust toolchain"
             run: rustup show
    +      - name: "Install mold"
    +        uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
           - name: "Install cargo-binstall"
             uses: cargo-bins/cargo-binstall@b3f755e95653da9a2d25b99154edfdbd5b356d0a # v1.15.10
           - name: "Install cargo-fuzz"
    @@ -645,7 +647,6 @@ jobs:
         name: "Fuzz for new ty panics"
         runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
         needs:
    -      - cargo-test-linux
           - determine_changes
         # Only runs on pull requests, since that is the only we way we can find the base version for comparison.
         if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && (needs.determine_changes.outputs.ty == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }}
    @@ -653,28 +654,29 @@ jobs:
         steps:
           - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
             with:
    +          fetch-depth: 0
               persist-credentials: false
    -      - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
    -        name: Download new ty binary
    -        id: ty-new
    -        with:
    -          name: ty
    -          path: target/debug
    -      - uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
    -        name: Download baseline ty binary
    -        with:
    -          name: ty
    -          branch: ${{ github.event.pull_request.base.ref }}
    -          workflow: "ci.yaml"
    -          check_artifacts: true
           - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
    +      - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
    +      - name: "Install Rust toolchain"
    +        run: rustup show
    +      - name: "Install mold"
    +        uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
           - name: Fuzz
             env:
               FORCE_COLOR: 1
    -          NEW_TY: ${{ steps.ty-new.outputs.download-path }}
             run: |
    -          # Make executable, since artifact download doesn't preserve this
    -          chmod +x "${PWD}/ty" "${NEW_TY}/ty"
    +          echo "new commit"
    +          git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
    +          cargo build --profile=profiling --bin=ty
    +          mv target/profiling/ty ty-new
    +
    +          MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
    +          git checkout -b old_commit "$MERGE_BASE"
    +          echo "old commit (merge base)"
    +          git rev-list --format=%s --max-count=1 old_commit
    +          cargo build --profile=profiling --bin=ty
    +          mv target/profiling/ty ty-old
     
               (
                 uv run \
    @@ -682,8 +684,8 @@ jobs:
                 --project=./python/py-fuzzer \
                 --locked \
                 fuzz \
    -            --test-executable="${NEW_TY}/ty" \
    -            --baseline-executable="${PWD}/ty" \
    +            --test-executable=ty-new \
    +            --baseline-executable=ty-old \
                 --only-new-bugs \
                 --bin=ty \
                 0-1000
    @@ -715,6 +717,8 @@ jobs:
           - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
           - name: "Install Rust toolchain"
             run: rustup show
    +      - name: "Install mold"
    +        uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
           - name: "Run ty completion evaluation"
             run: cargo run --release --package ty_completion_eval -- all --threshold 0.4 --tasks /tmp/completion-evaluation-tasks.csv
           - name: "Ensure there are no changes"
    diff --git a/python/py-fuzzer/fuzz.py b/python/py-fuzzer/fuzz.py
    index e113d7e179..035838a6d1 100644
    --- a/python/py-fuzzer/fuzz.py
    +++ b/python/py-fuzzer/fuzz.py
    @@ -395,13 +395,14 @@ def parse_args() -> ResolvedCliArgs:
     
         if not args.test_executable:
             print(
    -            "Running `cargo build --release` since no test executable was specified...",
    +            "Running `cargo build --profile=profiling` since no test executable was specified...",
                 flush=True,
             )
             cmd: list[str] = [
                 "cargo",
                 "build",
    -            "--release",
    +            "--profile",
    +            "profiling",
                 "--locked",
                 "--color",
                 "always",
    
    From 3c8fb68765eafe9b43766fd64d5fd9a0297bc0e4 Mon Sep 17 00:00:00 2001
    From: Ibraheem Ahmed 
    Date: Mon, 3 Nov 2025 16:57:49 -0500
    Subject: [PATCH 169/188] [ty] `dict` is not assignable to `TypedDict` (#21238)
    
    ## Summary
    
    A lot of the bidirectional inference work relies on `dict` not being
    assignable to `TypedDict`, so I think it makes sense to add this before
    fully implementing https://github.com/astral-sh/ty/issues/1387.
    ---
     .../resources/mdtest/bidirectional.md         |  1 +
     .../resources/mdtest/call/overloads.md        |  3 +-
     .../resources/mdtest/call/union.md            |  3 +-
     .../resources/mdtest/comprehensions/basic.md  |  5 +-
     ...ict`_-_Diagnostics_(e5289abf5c570c29).snap | 55 ++++++++--------
     .../resources/mdtest/typed_dict.md            | 52 +++++++++++----
     crates/ty_python_semantic/src/types.rs        |  5 +-
     .../ty_python_semantic/src/types/call/bind.rs |  5 ++
     .../src/types/diagnostic.rs                   | 39 +++++++++--
     .../src/types/infer/builder.rs                | 66 ++++++++++++-------
     .../src/types/typed_dict.rs                   | 10 ++-
     11 files changed, 169 insertions(+), 75 deletions(-)
    
    diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md
    index 3fee0513ed..6b90873728 100644
    --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md
    +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md
    @@ -76,6 +76,7 @@ def _() -> TD:
     
     def _() -> TD:
         # error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
    +    # error: [invalid-return-type]
         return {}
     ```
     
    diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md
    index 726d74a630..e6ef48276a 100644
    --- a/crates/ty_python_semantic/resources/mdtest/call/overloads.md
    +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md
    @@ -1685,8 +1685,7 @@ def int_or_str() -> int | str:
     x = f([{"x": 1}], int_or_str())
     reveal_type(x)  # revealed: int | str
     
    -# TODO: error: [no-matching-overload] "No overload of function `f` matches arguments"
    -# we currently incorrectly consider `list[dict[str, int]]` a subtype of `list[T]`
    +# error: [no-matching-overload] "No overload of function `f` matches arguments"
     f([{"y": 1}], int_or_str())
     ```
     
    diff --git a/crates/ty_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md
    index 69695c3f5c..7bb4e02044 100644
    --- a/crates/ty_python_semantic/resources/mdtest/call/union.md
    +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md
    @@ -277,7 +277,6 @@ def _(flag: bool):
         x = f({"x": 1})
         reveal_type(x)  # revealed: int
     
    -    # TODO: error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[str, int]`"
    -    # we currently consider `TypedDict` instances to be subtypes of `dict`
    +    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[Unknown | str, Unknown | 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 254ac03d73..5fac394404 100644
    --- a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md
    +++ b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md
    @@ -162,10 +162,13 @@ The type context is propagated down into the comprehension:
     class Person(TypedDict):
         name: str
     
    +# TODO: This should not error.
    +# error: [invalid-assignment]
     persons: list[Person] = [{"name": n} for n in ["Alice", "Bob"]]
     reveal_type(persons)  # revealed: list[Person]
     
    -# TODO: This should be an error
    +# TODO: This should be an invalid-key error.
    +# error: [invalid-assignment]
     invalid: list[Person] = [{"misspelled": n} for n in ["Alice", "Bob"]]
     ```
     
    diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap
    index 155b4ea618..a5b9456acd 100644
    --- a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap
    +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap
    @@ -39,16 +39,19 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md
     25 |     person[str_key] = "Alice"  # error: [invalid-key]
     26 | 
     27 | def create_with_invalid_string_key():
    -28 |     alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}  # error: [invalid-key]
    -29 |     bob = Person(name="Bob", age=25, unknown="Bar")  # error: [invalid-key]
    -30 | from typing_extensions import ReadOnly
    -31 | 
    -32 | class Employee(TypedDict):
    -33 |     id: ReadOnly[int]
    -34 |     name: str
    -35 | 
    -36 | def write_to_readonly_key(employee: Employee):
    -37 |     employee["id"] = 42  # error: [invalid-assignment]
    +28 |     # error: [invalid-key]
    +29 |     alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}
    +30 | 
    +31 |     # error: [invalid-key]
    +32 |     bob = Person(name="Bob", age=25, unknown="Bar")
    +33 | from typing_extensions import ReadOnly
    +34 | 
    +35 | class Employee(TypedDict):
    +36 |     id: ReadOnly[int]
    +37 |     name: str
    +38 | 
    +39 | def write_to_readonly_key(employee: Employee):
    +40 |     employee["id"] = 42  # error: [invalid-assignment]
     ```
     
     # Diagnostics
    @@ -158,16 +161,17 @@ info: rule `invalid-key` is enabled by default
     
     ```
     error[invalid-key]: Invalid key for TypedDict `Person`
    -  --> src/mdtest_snippet.py:28:21
    +  --> src/mdtest_snippet.py:29:21
        |
     27 | def create_with_invalid_string_key():
    -28 |     alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}  # error: [invalid-key]
    +28 |     # error: [invalid-key]
    +29 |     alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}
        |                     -----------------------------^^^^^^^^^--------
        |                     |                            |
        |                     |                            Unknown key "unknown"
        |                     TypedDict `Person`
    -29 |     bob = Person(name="Bob", age=25, unknown="Bar")  # error: [invalid-key]
    -30 | from typing_extensions import ReadOnly
    +30 |
    +31 |     # error: [invalid-key]
        |
     info: rule `invalid-key` is enabled by default
     
    @@ -175,13 +179,12 @@ info: rule `invalid-key` is enabled by default
     
     ```
     error[invalid-key]: Invalid key for TypedDict `Person`
    -  --> src/mdtest_snippet.py:29:11
    +  --> src/mdtest_snippet.py:32:11
        |
    -27 | def create_with_invalid_string_key():
    -28 |     alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}  # error: [invalid-key]
    -29 |     bob = Person(name="Bob", age=25, unknown="Bar")  # error: [invalid-key]
    +31 |     # error: [invalid-key]
    +32 |     bob = Person(name="Bob", age=25, unknown="Bar")
        |           ------ TypedDict `Person`  ^^^^^^^^^^^^^ Unknown key "unknown"
    -30 | from typing_extensions import ReadOnly
    +33 | from typing_extensions import ReadOnly
        |
     info: rule `invalid-key` is enabled by default
     
    @@ -189,21 +192,21 @@ info: rule `invalid-key` is enabled by default
     
     ```
     error[invalid-assignment]: Cannot assign to key "id" on TypedDict `Employee`
    -  --> src/mdtest_snippet.py:37:5
    +  --> src/mdtest_snippet.py:40:5
        |
    -36 | def write_to_readonly_key(employee: Employee):
    -37 |     employee["id"] = 42  # error: [invalid-assignment]
    +39 | def write_to_readonly_key(employee: Employee):
    +40 |     employee["id"] = 42  # error: [invalid-assignment]
        |     -------- ^^^^ key is marked read-only
        |     |
        |     TypedDict `Employee`
        |
     info: Item declaration
    -  --> src/mdtest_snippet.py:33:5
    +  --> src/mdtest_snippet.py:36:5
        |
    -32 | class Employee(TypedDict):
    -33 |     id: ReadOnly[int]
    +35 | class Employee(TypedDict):
    +36 |     id: ReadOnly[int]
        |     ----------------- Read-only item declared here
    -34 |     name: str
    +37 |     name: str
        |
     info: rule `invalid-assignment` 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 30bbb2132b..b4203ce2b6 100644
    --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md
    +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md
    @@ -96,29 +96,29 @@ The construction of a `TypedDict` is checked for type correctness:
     ```py
     # error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`"
     eve1a: Person = {"name": b"Eve", "age": None}
    +
     # error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`"
     eve1b = Person(name=b"Eve", age=None)
     
    -# TODO should reveal Person (should be fixed by implementing assignability for TypedDicts)
    -reveal_type(eve1a)  # revealed: dict[Unknown | str, Unknown | bytes | None]
    +reveal_type(eve1a)  # revealed: Person
     reveal_type(eve1b)  # revealed: Person
     
     # error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
     eve2a: Person = {"age": 22}
    +
     # error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
     eve2b = Person(age=22)
     
    -# TODO should reveal Person (should be fixed by implementing assignability for TypedDicts)
    -reveal_type(eve2a)  # revealed: dict[Unknown | str, Unknown | int]
    +reveal_type(eve2a)  # revealed: Person
     reveal_type(eve2b)  # revealed: Person
     
     # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
     eve3a: Person = {"name": "Eve", "age": 25, "extra": True}
    +
     # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
     eve3b = Person(name="Eve", age=25, extra=True)
     
    -# TODO should reveal Person (should be fixed by implementing assignability for TypedDicts)
    -reveal_type(eve3a)  # revealed: dict[Unknown | str, Unknown | str | int]
    +reveal_type(eve3a)  # revealed: Person
     reveal_type(eve3b)  # revealed: Person
     ```
     
    @@ -238,15 +238,19 @@ All of these are missing the required `age` field:
     ```py
     # error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
     alice2: Person = {"name": "Alice"}
    +
     # error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
     Person(name="Alice")
    +
     # error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
     Person({"name": "Alice"})
     
     # error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
    +# error: [invalid-argument-type]
     accepts_person({"name": "Alice"})
     
    -# TODO: this should be an error, similar to the above
    +# TODO: this should be an invalid-key error, similar to the above
    +# error: [invalid-assignment]
     house.owner = {"name": "Alice"}
     
     a_person: Person
    @@ -259,19 +263,25 @@ All of these have an invalid type for the `name` field:
     ```py
     # error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
     alice3: Person = {"name": None, "age": 30}
    +
     # error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
     Person(name=None, age=30)
    +
     # error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
     Person({"name": None, "age": 30})
     
     # error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
    +# error: [invalid-argument-type]
     accepts_person({"name": None, "age": 30})
    -# TODO: this should be an error, similar to the above
    +
    +# TODO: this should be an invalid-key error
    +# error: [invalid-assignment]
     house.owner = {"name": None, "age": 30}
     
     a_person: Person
     # error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
     a_person = {"name": None, "age": 30}
    +
     # error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
     (a_person := {"name": None, "age": 30})
     ```
    @@ -281,19 +291,25 @@ All of these have an extra field that is not defined in the `TypedDict`:
     ```py
     # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
     alice4: Person = {"name": "Alice", "age": 30, "extra": True}
    +
     # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
     Person(name="Alice", age=30, extra=True)
    +
     # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
     Person({"name": "Alice", "age": 30, "extra": True})
     
     # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
    +# error: [invalid-argument-type]
     accepts_person({"name": "Alice", "age": 30, "extra": True})
    -# TODO: this should be an error
    +
    +# TODO: this should be an invalid-key error
    +# error: [invalid-assignment]
     house.owner = {"name": "Alice", "age": 30, "extra": True}
     
     a_person: Person
     # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
     a_person = {"name": "Alice", "age": 30, "extra": True}
    +
     # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
     (a_person := {"name": "Alice", "age": 30, "extra": True})
     ```
    @@ -490,6 +506,15 @@ dangerous(alice)
     reveal_type(alice["name"])  # revealed: str
     ```
     
    +Likewise, `dict`s are not assignable to typed dictionaries:
    +
    +```py
    +alice: dict[str, str] = {"name": "Alice"}
    +
    +# error: [invalid-assignment] "Object of type `dict[str, str]` is not assignable to `Person`"
    +alice: Person = alice
    +```
    +
     ## Key-based access
     
     ### Reading
    @@ -977,7 +1002,7 @@ class Person(TypedDict):
         name: str
         age: int | None
     
    -# TODO: this should be an error
    +# error: [invalid-assignment] "Object of type `MyDict` is not assignable to `Person`"
     x: Person = MyDict({"name": "Alice", "age": 30})
     ```
     
    @@ -1029,8 +1054,11 @@ def write_to_non_literal_string_key(person: Person, str_key: str):
         person[str_key] = "Alice"  # error: [invalid-key]
     
     def create_with_invalid_string_key():
    -    alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}  # error: [invalid-key]
    -    bob = Person(name="Bob", age=25, unknown="Bar")  # error: [invalid-key]
    +    # error: [invalid-key]
    +    alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}
    +
    +    # error: [invalid-key]
    +    bob = Person(name="Bob", age=25, unknown="Bar")
     ```
     
     Assignment to `ReadOnly` keys:
    diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
    index bdc859b095..bee767b763 100644
    --- a/crates/ty_python_semantic/src/types.rs
    +++ b/crates/ty_python_semantic/src/types.rs
    @@ -1987,11 +1987,14 @@ impl<'db> Type<'db> {
                     ConstraintSet::from(false)
                 }
     
    -            (Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
    +            (Type::TypedDict(_), _) => {
                     // TODO: Implement assignability and subtyping for TypedDict
                     ConstraintSet::from(relation.is_assignability())
                 }
     
    +            // A non-`TypedDict` cannot subtype a `TypedDict`
    +            (_, Type::TypedDict(_)) => ConstraintSet::from(false),
    +
                 // 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()),
    diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs
    index b0a5cc1b91..d2739fa696 100644
    --- a/crates/ty_python_semantic/src/types/call/bind.rs
    +++ b/crates/ty_python_semantic/src/types/call/bind.rs
    @@ -3582,6 +3582,11 @@ impl<'db> BindingError<'db> {
                     expected_ty,
                     provided_ty,
                 } => {
    +                // TODO: Ideally we would not emit diagnostics for `TypedDict` literal arguments
    +                // here (see `diagnostic::is_invalid_typed_dict_literal`). However, we may have
    +                // silenced diagnostics during overload evaluation, and rely on the assignability
    +                // diagnostic being emitted here.
    +
                     let range = Self::get_node(node, *argument_index);
                     let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, range) else {
                         return;
    diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
    index 6ab6f2a447..5d647f108f 100644
    --- a/crates/ty_python_semantic/src/types/diagnostic.rs
    +++ b/crates/ty_python_semantic/src/types/diagnostic.rs
    @@ -2003,6 +2003,20 @@ pub(super) fn report_slice_step_size_zero(context: &InferContext, node: AnyNodeR
         builder.into_diagnostic("Slice step size cannot be zero");
     }
     
    +// 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 {
    +    target_ty
    +        .filter_union(db, Type::is_typed_dict)
    +        .as_typed_dict()
    +        .is_some()
    +        && matches!(source, AnyNodeRef::ExprDict(_))
    +}
    +
     fn report_invalid_assignment_with_message(
         context: &InferContext,
         node: AnyNodeRef,
    @@ -2040,15 +2054,27 @@ pub(super) fn report_invalid_assignment<'db>(
         target_ty: Type,
         mut source_ty: Type<'db>,
     ) {
    +    let value_expr = match definition.kind(context.db()) {
    +        DefinitionKind::Assignment(def) => Some(def.value(context.module())),
    +        DefinitionKind::AnnotatedAssignment(def) => def.value(context.module()),
    +        DefinitionKind::NamedExpression(def) => Some(&*def.node(context.module()).value),
    +        _ => None,
    +    };
    +
    +    if let Some(value_expr) = value_expr
    +        && is_invalid_typed_dict_literal(context.db(), target_ty, value_expr.into())
    +    {
    +        return;
    +    }
    +
         let settings =
             DisplaySettings::from_possibly_ambiguous_type_pair(context.db(), target_ty, source_ty);
     
    -    if let DefinitionKind::AnnotatedAssignment(annotated_assignment) = definition.kind(context.db())
    -        && let Some(value) = annotated_assignment.value(context.module())
    -    {
    +    if let Some(value_expr) = value_expr {
             // Re-infer the RHS of the annotated assignment, ignoring the type context for more precise
             // error messages.
    -        source_ty = infer_isolated_expression(context.db(), definition.scope(context.db()), value);
    +        source_ty =
    +            infer_isolated_expression(context.db(), definition.scope(context.db()), value_expr);
         }
     
         report_invalid_assignment_with_message(
    @@ -2070,6 +2096,11 @@ pub(super) fn report_invalid_attribute_assignment(
         source_ty: Type,
         attribute_name: &'_ str,
     ) {
    +    // TODO: Ideally we would not emit diagnostics for `TypedDict` literal arguments
    +    // here (see `diagnostic::is_invalid_typed_dict_literal`). However, we may have
    +    // silenced diagnostics during attribute resolution, and rely on the assignability
    +    // diagnostic being emitted here.
    +
         report_invalid_assignment_with_message(
             context,
             node,
    diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
    index 1ad7d18482..b7608bbfff 100644
    --- a/crates/ty_python_semantic/src/types/infer/builder.rs
    +++ b/crates/ty_python_semantic/src/types/infer/builder.rs
    @@ -5,7 +5,9 @@ use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity};
     use ruff_db::files::File;
     use ruff_db::parsed::ParsedModuleRef;
     use ruff_python_ast::visitor::{Visitor, walk_expr};
    -use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext, PythonVersion};
    +use ruff_python_ast::{
    +    self as ast, AnyNodeRef, ExprContext, HasNodeIndex, NodeIndex, PythonVersion,
    +};
     use ruff_python_stdlib::builtins::version_builtin_was_added;
     use ruff_text_size::{Ranged, TextRange};
     use rustc_hash::{FxHashMap, FxHashSet};
    @@ -5859,15 +5861,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
             expression.map(|expr| self.infer_expression(expr, tcx))
         }
     
    -    fn get_or_infer_expression(
    -        &mut self,
    -        expression: &ast::Expr,
    -        tcx: TypeContext<'db>,
    -    ) -> Type<'db> {
    -        self.try_expression_type(expression)
    -            .unwrap_or_else(|| self.infer_expression(expression, tcx))
    -    }
    -
         #[track_caller]
         fn infer_expression(&mut self, expression: &ast::Expr, tcx: TypeContext<'db>) -> Type<'db> {
             debug_assert!(
    @@ -6223,7 +6216,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
             } = list;
     
             let elts = elts.iter().map(|elt| [Some(elt)]);
    -        self.infer_collection_literal(elts, tcx, KnownClass::List)
    +        let infer_elt_ty = |builder: &mut Self, elt, tcx| builder.infer_expression(elt, tcx);
    +        self.infer_collection_literal(elts, tcx, infer_elt_ty, KnownClass::List)
                 .unwrap_or_else(|| {
                     KnownClass::List.to_specialized_instance(self.db(), [Type::unknown()])
                 })
    @@ -6237,7 +6231,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
             } = set;
     
             let elts = elts.iter().map(|elt| [Some(elt)]);
    -        self.infer_collection_literal(elts, tcx, KnownClass::Set)
    +        let infer_elt_ty = |builder: &mut Self, elt, tcx| builder.infer_expression(elt, tcx);
    +        self.infer_collection_literal(elts, tcx, infer_elt_ty, KnownClass::Set)
                 .unwrap_or_else(|| {
                     KnownClass::Set.to_specialized_instance(self.db(), [Type::unknown()])
                 })
    @@ -6250,12 +6245,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                 items,
             } = dict;
     
    +        let mut item_types = FxHashMap::default();
    +
             // Validate `TypedDict` dictionary literal assignments.
             if let Some(tcx) = tcx.annotation
                 && let Some(typed_dict) = tcx
                     .filter_union(self.db(), Type::is_typed_dict)
                     .as_typed_dict()
    -            && let Some(ty) = self.infer_typed_dict_expression(dict, typed_dict)
    +            && let Some(ty) = self.infer_typed_dict_expression(dict, typed_dict, &mut item_types)
             {
                 return ty;
             }
    @@ -6271,7 +6268,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                 .iter()
                 .map(|item| [item.key.as_ref(), Some(&item.value)]);
     
    -        self.infer_collection_literal(items, tcx, KnownClass::Dict)
    +        // Avoid inferring the items multiple times if we already attempted to infer the
    +        // dictionary literal as a `TypedDict`. This also allows us to infer using the
    +        // type context of the expected `TypedDict` field.
    +        let infer_elt_ty = |builder: &mut Self, elt: &ast::Expr, tcx| {
    +            item_types
    +                .get(&elt.node_index().load())
    +                .copied()
    +                .unwrap_or_else(|| builder.infer_expression(elt, tcx))
    +        };
    +
    +        self.infer_collection_literal(items, tcx, infer_elt_ty, KnownClass::Dict)
                 .unwrap_or_else(|| {
                     KnownClass::Dict
                         .to_specialized_instance(self.db(), [Type::unknown(), Type::unknown()])
    @@ -6282,6 +6289,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
             &mut self,
             dict: &ast::ExprDict,
             typed_dict: TypedDictType<'db>,
    +        item_types: &mut FxHashMap>,
         ) -> Option> {
             let ast::ExprDict {
                 range: _,
    @@ -6293,14 +6301,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
     
             for item in items {
                 let key_ty = self.infer_optional_expression(item.key.as_ref(), TypeContext::default());
    +            if let Some((key, key_ty)) = item.key.as_ref().zip(key_ty) {
    +                item_types.insert(key.node_index().load(), key_ty);
    +            }
     
    -            if let Some(Type::StringLiteral(key)) = key_ty
    +            let value_ty = if let Some(Type::StringLiteral(key)) = key_ty
                     && let Some(field) = typed_dict_items.get(key.value(self.db()))
                 {
    -                self.infer_expression(&item.value, TypeContext::new(Some(field.declared_ty)));
    +                self.infer_expression(&item.value, TypeContext::new(Some(field.declared_ty)))
                 } else {
    -                self.infer_expression(&item.value, TypeContext::default());
    -            }
    +                self.infer_expression(&item.value, TypeContext::default())
    +            };
    +
    +            item_types.insert(item.value.node_index().load(), value_ty);
             }
     
             validate_typed_dict_dict_literal(&self.context, typed_dict, dict, dict.into(), |expr| {
    @@ -6311,12 +6324,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
         }
     
         // Infer the type of a collection literal expression.
    -    fn infer_collection_literal<'expr, const N: usize>(
    +    fn infer_collection_literal<'expr, const N: usize, F, I>(
             &mut self,
    -        elts: impl Iterator; N]>,
    +        elts: I,
             tcx: TypeContext<'db>,
    +        mut infer_elt_expression: F,
             collection_class: KnownClass,
    -    ) -> Option> {
    +    ) -> Option>
    +    where
    +        I: Iterator; N]>,
    +        F: FnMut(&mut Self, &'expr ast::Expr, TypeContext<'db>) -> Type<'db>,
    +    {
             // Extract the type variable `T` from `list[T]` in typeshed.
             let elt_tys = |collection_class: KnownClass| {
                 let class_literal = collection_class.try_to_class_literal(self.db())?;
    @@ -6332,7 +6350,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                 // Infer the element types without type context, and fallback to unknown for
                 // custom typesheds.
                 for elt in elts.flatten().flatten() {
    -                self.get_or_infer_expression(elt, TypeContext::default());
    +                infer_elt_expression(self, elt, TypeContext::default());
                 }
     
                 return None;
    @@ -6387,7 +6405,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
             for elts in elts {
                 // An unpacking expression for a dictionary.
                 if let &[None, Some(value)] = elts.as_slice() {
    -                let inferred_value_ty = self.get_or_infer_expression(value, TypeContext::default());
    +                let inferred_value_ty = infer_elt_expression(self, value, TypeContext::default());
     
                     // Merge the inferred type of the nested dictionary.
                     if let Some(specialization) =
    @@ -6410,7 +6428,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                 {
                     let Some(elt) = elt else { continue };
     
    -                let inferred_elt_ty = self.get_or_infer_expression(elt, elt_tcx);
    +                let inferred_elt_ty = infer_elt_expression(self, elt, elt_tcx);
     
                     // Simplify the inference based on the declared type of the element.
                     if let Some(elt_tcx) = elt_tcx.annotation {
    diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs
    index e29b836d8a..83b4ae946e 100644
    --- a/crates/ty_python_semantic/src/types/typed_dict.rs
    +++ b/crates/ty_python_semantic/src/types/typed_dict.rs
    @@ -8,7 +8,7 @@ use ruff_text_size::Ranged;
     use super::class::{ClassType, CodeGeneratorKind, Field};
     use super::context::InferContext;
     use super::diagnostic::{
    -    INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, report_invalid_key_on_typed_dict,
    +    self, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, report_invalid_key_on_typed_dict,
         report_missing_typed_dict_key,
     };
     use super::{ApplyTypeMappingVisitor, Type, TypeMapping, visitor};
    @@ -213,9 +213,13 @@ pub(super) fn validate_typed_dict_key_assignment<'db, 'ast>(
             return true;
         }
     
    +    let value_node = value_node.into();
    +    if diagnostic::is_invalid_typed_dict_literal(context.db(), item.declared_ty, value_node) {
    +        return false;
    +    }
    +
         // Invalid assignment - emit diagnostic
    -    if let Some(builder) = context.report_lint(assignment_kind.diagnostic_type(), value_node.into())
    -    {
    +    if let Some(builder) = context.report_lint(assignment_kind.diagnostic_type(), value_node) {
             let typed_dict_ty = Type::TypedDict(typed_dict);
             let typed_dict_d = typed_dict_ty.display(db);
             let value_d = value_ty.display(db);
    
    From 3c5e4e147779ee016320993dc3b2406c2c3fde34 Mon Sep 17 00:00:00 2001
    From: Micha Reiser 
    Date: Mon, 3 Nov 2025 23:00:30 +0100
    Subject: [PATCH 170/188] [ty] Update salsa (#21265)
    
    ---
     Cargo.lock      | 6 +++---
     Cargo.toml      | 2 +-
     fuzz/Cargo.toml | 2 +-
     3 files changed, 5 insertions(+), 5 deletions(-)
    
    diff --git a/Cargo.lock b/Cargo.lock
    index 14312dfa75..2da7d9ff0e 100644
    --- a/Cargo.lock
    +++ b/Cargo.lock
    @@ -3586,7 +3586,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
     [[package]]
     name = "salsa"
     version = "0.24.0"
    -source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198"
    +source = "git+https://github.com/salsa-rs/salsa.git?rev=664750a6e588ed23a0d2d9105a02cb5993c8e178#664750a6e588ed23a0d2d9105a02cb5993c8e178"
     dependencies = [
      "boxcar",
      "compact_str",
    @@ -3610,12 +3610,12 @@ dependencies = [
     [[package]]
     name = "salsa-macro-rules"
     version = "0.24.0"
    -source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198"
    +source = "git+https://github.com/salsa-rs/salsa.git?rev=664750a6e588ed23a0d2d9105a02cb5993c8e178#664750a6e588ed23a0d2d9105a02cb5993c8e178"
     
     [[package]]
     name = "salsa-macros"
     version = "0.24.0"
    -source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198"
    +source = "git+https://github.com/salsa-rs/salsa.git?rev=664750a6e588ed23a0d2d9105a02cb5993c8e178#664750a6e588ed23a0d2d9105a02cb5993c8e178"
     dependencies = [
      "proc-macro2",
      "quote",
    diff --git a/Cargo.toml b/Cargo.toml
    index ed7fbf4fcb..b2122cea97 100644
    --- a/Cargo.toml
    +++ b/Cargo.toml
    @@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
     rustc-hash = { version = "2.0.0" }
     rustc-stable-hash = { version = "0.1.2" }
     # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
    -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "cdd0b85516a52c18b8a6d17a2279a96ed6c3e198", default-features = false, features = [
    +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "664750a6e588ed23a0d2d9105a02cb5993c8e178", default-features = false, features = [
         "compact_str",
         "macros",
         "salsa_unstable",
    diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
    index 359e59d1e1..278267fc15 100644
    --- a/fuzz/Cargo.toml
    +++ b/fuzz/Cargo.toml
    @@ -30,7 +30,7 @@ ty_python_semantic = { path = "../crates/ty_python_semantic" }
     ty_vendored = { path = "../crates/ty_vendored" }
     
     libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false }
    -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "cdd0b85516a52c18b8a6d17a2279a96ed6c3e198", default-features = false, features = [
    +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "664750a6e588ed23a0d2d9105a02cb5993c8e178", default-features = false, features = [
         "compact_str",
         "macros",
         "salsa_unstable",
    
    From 63b1c1ea8bd2b0b08661167c20492d2cb6e2889c Mon Sep 17 00:00:00 2001
    From: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
    Date: Mon, 3 Nov 2025 17:06:52 -0500
    Subject: [PATCH 171/188] Avoid extra parentheses for long `match` patterns
     with `as` captures (#21176)
    
    Summary
    --
    
    This PR fixes #17796 by taking the approach mentioned in
    https://github.com/astral-sh/ruff/issues/17796#issuecomment-2847943862
    of simply recursing into the `MatchAs` patterns when checking if we need
    parentheses. This allows us to reuse the parentheses in the inner
    pattern before also breaking the `MatchAs` pattern itself:
    
    ```diff
     match class_pattern:
         case Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture:
             pass
    -    case (
    -        Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture
    -    ):
    +    case Class(
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ) as capture:
             pass
    -    case (
    -        Class(
    -            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    -        ) as capture
    -    ):
    +    case Class(
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ) as capture:
             pass
         case (
             Class(
    @@ -685,13 +683,11 @@
     match sequence_pattern_brackets:
         case [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture:
             pass
    -    case (
    -        [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture
    -    ):
    +    case [
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ] as capture:
             pass
    -    case (
    -        [
    -            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    -        ] as capture
    -    ):
    +    case [
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ] as capture:
             pass
    ```
    
    I haven't really resolved the question of whether or not it's okay
    always to recurse, but I'm hoping the ecosystem check on this PR might
    shed some light on that.
    
    Test Plan
    --
    
    New tests based on the issue and then reviewing the ecosystem check here
    ---
     .../test/fixtures/ruff/statement/match.py     |  55 +++++
     .../ruff_python_formatter/src/pattern/mod.rs  |  20 +-
     crates/ruff_python_formatter/src/preview.rs   |   9 +
     .../snapshots/format@statement__match.py.snap | 227 +++++++++++++++++-
     4 files changed, 307 insertions(+), 4 deletions(-)
    
    diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/match.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/match.py
    index 4067d508c0..4a403b1541 100644
    --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/match.py
    +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/match.py
    @@ -613,3 +613,58 @@ match guard_comments:
         ):
             pass
     
    +
    +# regression tests from https://github.com/astral-sh/ruff/issues/17796
    +match class_pattern:
    +    case Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture:
    +        pass
    +    case Class(
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ) as capture:
    +        pass
    +    case Class(
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ) as capture:
    +        pass
    +    case Class(
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture:
    +        pass
    +
    +match sequence_pattern_brackets:
    +    case [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture:
    +        pass
    +    case [
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ] as capture:
    +        pass
    +    case [
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ] as capture:
    +        pass
    +
    +
    +match class_pattern:
    +    # 1
    +    case Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture:  # 2
    +        # 3
    +        pass # 4
    +    # 5
    +    case Class( # 6
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 7
    +    ) as capture: # 8
    +        pass
    +    case Class( # 9
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 10
    +    ) as capture: # 11
    +        pass
    +    case Class( # 12
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 13
    +    ) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture: # 14
    +        pass
    +    case Class( # 0
    +            # 1
    +            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 2
    +            # 3
    +    ) as capture:
    +        pass
    diff --git a/crates/ruff_python_formatter/src/pattern/mod.rs b/crates/ruff_python_formatter/src/pattern/mod.rs
    index 557337ddc5..a379aeb849 100644
    --- a/crates/ruff_python_formatter/src/pattern/mod.rs
    +++ b/crates/ruff_python_formatter/src/pattern/mod.rs
    @@ -1,5 +1,5 @@
     use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions};
    -use ruff_python_ast::{AnyNodeRef, Expr};
    +use ruff_python_ast::{AnyNodeRef, Expr, PatternMatchAs};
     use ruff_python_ast::{MatchCase, Pattern};
     use ruff_python_trivia::CommentRanges;
     use ruff_python_trivia::{
    @@ -14,6 +14,7 @@ use crate::expression::parentheses::{
         NeedsParentheses, OptionalParentheses, Parentheses, optional_parentheses, parenthesized,
     };
     use crate::prelude::*;
    +use crate::preview::is_avoid_parens_for_long_as_captures_enabled;
     
     pub(crate) mod pattern_arguments;
     pub(crate) mod pattern_keyword;
    @@ -242,8 +243,14 @@ pub(crate) fn can_pattern_omit_optional_parentheses(
                     Pattern::MatchValue(_)
                     | Pattern::MatchSingleton(_)
                     | Pattern::MatchStar(_)
    -                | Pattern::MatchAs(_)
                     | Pattern::MatchOr(_) => false,
    +                Pattern::MatchAs(PatternMatchAs { pattern, .. }) => match pattern {
    +                    Some(pattern) => {
    +                        is_avoid_parens_for_long_as_captures_enabled(context)
    +                            && has_parentheses_and_is_non_empty(pattern, context)
    +                    }
    +                    None => false,
    +                },
                     Pattern::MatchSequence(sequence) => {
                         !sequence.patterns.is_empty() || context.comments().has_dangling(pattern)
                     }
    @@ -318,7 +325,14 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> {
                     // The pattern doesn't start with a parentheses pattern, but with the class's identifier.
                     self.first.set_if_none(First::Token);
                 }
    -            Pattern::MatchStar(_) | Pattern::MatchSingleton(_) | Pattern::MatchAs(_) => {}
    +            Pattern::MatchAs(PatternMatchAs { pattern, .. }) => {
    +                if let Some(pattern) = pattern
    +                    && is_avoid_parens_for_long_as_captures_enabled(context)
    +                {
    +                    self.visit_sub_pattern(pattern, context);
    +                }
    +            }
    +            Pattern::MatchStar(_) | Pattern::MatchSingleton(_) => {}
                 Pattern::MatchOr(or_pattern) => {
                     self.update_max_precedence(
                         OperatorPrecedence::Or,
    diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs
    index 5455fa9a12..9d307390d6 100644
    --- a/crates/ruff_python_formatter/src/preview.rs
    +++ b/crates/ruff_python_formatter/src/preview.rs
    @@ -43,3 +43,12 @@ pub(crate) const fn is_remove_parens_around_except_types_enabled(
     pub(crate) const fn is_allow_newline_after_block_open_enabled(context: &PyFormatContext) -> bool {
         context.is_preview()
     }
    +
    +/// Returns `true` if the
    +/// [`avoid_parens_for_long_as_captures`](https://github.com/astral-sh/ruff/pull/21176) preview
    +/// style is enabled.
    +pub(crate) const fn is_avoid_parens_for_long_as_captures_enabled(
    +    context: &PyFormatContext,
    +) -> bool {
    +    context.is_preview()
    +}
    diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap
    index 852740fa6d..a94ee5e636 100644
    --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap
    +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap
    @@ -1,7 +1,6 @@
     ---
     source: crates/ruff_python_formatter/tests/fixtures.rs
     input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/match.py
    -snapshot_kind: text
     ---
     ## Input
     ```python
    @@ -620,6 +619,61 @@ match guard_comments:
         ):
             pass
     
    +
    +# regression tests from https://github.com/astral-sh/ruff/issues/17796
    +match class_pattern:
    +    case Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture:
    +        pass
    +    case Class(
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ) as capture:
    +        pass
    +    case Class(
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ) as capture:
    +        pass
    +    case Class(
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture:
    +        pass
    +
    +match sequence_pattern_brackets:
    +    case [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture:
    +        pass
    +    case [
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ] as capture:
    +        pass
    +    case [
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +    ] as capture:
    +        pass
    +
    +
    +match class_pattern:
    +    # 1
    +    case Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture:  # 2
    +        # 3
    +        pass # 4
    +    # 5
    +    case Class( # 6
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 7
    +    ) as capture: # 8
    +        pass
    +    case Class( # 9
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 10
    +    ) as capture: # 11
    +        pass
    +    case Class( # 12
    +        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 13
    +    ) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture: # 14
    +        pass
    +    case Class( # 0
    +            # 1
    +            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 2
    +            # 3
    +    ) as capture:
    +        pass
     ```
     
     ## Output
    @@ -1285,4 +1339,175 @@ match guard_comments:
             # trailing own line comment
         ):
             pass
    +
    +
    +# regression tests from https://github.com/astral-sh/ruff/issues/17796
    +match class_pattern:
    +    case Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture:
    +        pass
    +    case (
    +        Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture
    +    ):
    +        pass
    +    case (
    +        Class(
    +            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +        ) as capture
    +    ):
    +        pass
    +    case (
    +        Class(
    +            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +        ) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture
    +    ):
    +        pass
    +
    +match sequence_pattern_brackets:
    +    case [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture:
    +        pass
    +    case (
    +        [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture
    +    ):
    +        pass
    +    case (
    +        [
    +            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +        ] as capture
    +    ):
    +        pass
    +
    +
    +match class_pattern:
    +    # 1
    +    case (
    +        Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture
    +    ):  # 2
    +        # 3
    +        pass  # 4
    +    # 5
    +    case (
    +        Class(  # 6
    +            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  # 7
    +        ) as capture
    +    ):  # 8
    +        pass
    +    case (
    +        Class(  # 9
    +            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  # 10
    +        ) as capture
    +    ):  # 11
    +        pass
    +    case (
    +        Class(  # 12
    +            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  # 13
    +        ) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture
    +    ):  # 14
    +        pass
    +    case (
    +        Class(  # 0
    +            # 1
    +            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  # 2
    +            # 3
    +        ) as capture
    +    ):
    +        pass
    +```
    +
    +
    +## Preview changes
    +```diff
    +--- Stable
    ++++ Preview
    +@@ -665,15 +665,13 @@
    + match class_pattern:
    +     case Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture:
    +         pass
    +-    case (
    +-        Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture
    +-    ):
    ++    case Class(
    ++        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ++    ) as capture:
    +         pass
    +-    case (
    +-        Class(
    +-            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +-        ) as capture
    +-    ):
    ++    case Class(
    ++        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ++    ) as capture:
    +         pass
    +     case (
    +         Class(
    +@@ -685,37 +683,31 @@
    + match sequence_pattern_brackets:
    +     case [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture:
    +         pass
    +-    case (
    +-        [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture
    +-    ):
    ++    case [
    ++        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ++    ] as capture:
    +         pass
    +-    case (
    +-        [
    +-            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    +-        ] as capture
    +-    ):
    ++    case [
    ++        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ++    ] as capture:
    +         pass
    + 
    + 
    + match class_pattern:
    +     # 1
    +-    case (
    +-        Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture
    +-    ):  # 2
    ++    case Class(
    ++        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ++    ) as capture:  # 2
    +         # 3
    +         pass  # 4
    +     # 5
    +-    case (
    +-        Class(  # 6
    +-            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  # 7
    +-        ) as capture
    +-    ):  # 8
    ++    case Class(  # 6
    ++        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  # 7
    ++    ) as capture:  # 8
    +         pass
    +-    case (
    +-        Class(  # 9
    +-            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  # 10
    +-        ) as capture
    +-    ):  # 11
    ++    case Class(  # 9
    ++        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  # 10
    ++    ) as capture:  # 11
    +         pass
    +     case (
    +         Class(  # 12
    +@@ -723,11 +715,9 @@
    +         ) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture
    +     ):  # 14
    +         pass
    +-    case (
    +-        Class(  # 0
    +-            # 1
    +-            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  # 2
    +-            # 3
    +-        ) as capture
    +-    ):
    ++    case Class(  # 0
    ++        # 1
    ++        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  # 2
    ++        # 3
    ++    ) as capture:
    +         pass
     ```
    
    From 4fd8d4b0ee3e20c673c175455d0ad9637183c67d Mon Sep 17 00:00:00 2001
    From: Micha Reiser 
    Date: Tue, 4 Nov 2025 04:18:12 +0100
    Subject: [PATCH 172/188] [ty] Update expected diagnostic count in benchmarks
     (#21269)
    
    ---
     crates/ruff_benchmark/benches/ty_walltime.rs | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs
    index 55b2415990..2b02230fd6 100644
    --- a/crates/ruff_benchmark/benches/ty_walltime.rs
    +++ b/crates/ruff_benchmark/benches/ty_walltime.rs
    @@ -146,7 +146,7 @@ static FREQTRADE: Benchmark = Benchmark::new(
             max_dep_date: "2025-06-17",
             python_version: PythonVersion::PY312,
         },
    -    500,
    +    525,
     );
     
     static PANDAS: Benchmark = Benchmark::new(
    
    From d8106d38a06fe4d957c10f075a2bb432a1d938fd Mon Sep 17 00:00:00 2001
    From: Ibraheem Ahmed 
    Date: Tue, 4 Nov 2025 09:59:40 -0500
    Subject: [PATCH 173/188] Run codspeed benchmarks with `profiling` profile
     (#21261)
    
    ## Summary
    
    This reduces the walltime benchmarks from 15m to 10m, and we should see an even bigger improvement once build caching kicks in, so I think it's worth the downsides.
    ---
     .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 44d9f2f542..64c1d9f9b6 100644
    --- a/.github/workflows/ci.yaml
    +++ b/.github/workflows/ci.yaml
    @@ -953,7 +953,7 @@ jobs:
               tool: cargo-codspeed
     
           - name: "Build benchmarks"
    -        run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
    +        run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
     
           - name: "Run benchmarks"
             uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1
    @@ -991,7 +991,7 @@ jobs:
               tool: cargo-codspeed
     
           - name: "Build benchmarks"
    -        run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench ty
    +        run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty
     
           - name: "Run benchmarks"
             uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1
    @@ -1029,7 +1029,7 @@ jobs:
               tool: cargo-codspeed
     
           - name: "Build benchmarks"
    -        run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark
    +        run: cargo codspeed build --features "codspeed,walltime" --profile profiling --no-default-features -p ruff_benchmark
     
           - name: "Run benchmarks"
             uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1
    
    From 2e7ab00d51fae2e6c1fa6842b5dda4dee28b3730 Mon Sep 17 00:00:00 2001
    From: David Peter 
    Date: Tue, 4 Nov 2025 16:29:55 +0100
    Subject: [PATCH 174/188] [ty] Allow values of type `None` in type expressions
     (#21263)
    
    ## Summary
    
    Allow values of type `None` in type expressions. The [typing
    spec](https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions)
    could be more explicit on whether this is actually allowed or not, but
    it seems relatively harmless and does help in some use cases like:
    
    ```py
    try:
        from module import MyClass
    except ImportError:
        MyClass = None  # ty: ignore
    
    
    def f(m: MyClass):
        pass
    ```
    
    ## Test Plan
    
    Updated tests, ecosystem check.
    ---
     .../resources/mdtest/implicit_type_aliases.md                | 5 +----
     crates/ty_python_semantic/src/types.rs                       | 1 +
     2 files changed, 2 insertions(+), 4 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 a3e0319f5a..9ce736e235 100644
    --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md
    +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md
    @@ -22,11 +22,8 @@ f(1)
     ```py
     MyNone = None
     
    -# TODO: this should not be an error
    -# error: [invalid-type-form] "Variable of type `None` is not allowed in a type expression"
     def g(x: MyNone):
    -    # TODO: this should be `None`
    -    reveal_type(x)  # revealed: Unknown
    +    reveal_type(x)  # revealed: None
     
     g(None)
     ```
    diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
    index bee767b763..131d9e7630 100644
    --- a/crates/ty_python_semantic/src/types.rs
    +++ b/crates/ty_python_semantic/src/types.rs
    @@ -6600,6 +6600,7 @@ impl<'db> Type<'db> {
                 Type::Dynamic(_) => Ok(*self),
     
                 Type::NominalInstance(instance) => match instance.known_class(db) {
    +                Some(KnownClass::NoneType) => Ok(Type::none(db)),
                     Some(KnownClass::TypeVar) => Ok(todo_type!(
                         "Support for `typing.TypeVar` instances in type expressions"
                     )),
    
    From 47e41ac6b66d81e2aa8a5249985d255e03e52aaa Mon Sep 17 00:00:00 2001
    From: Dan Parizher <105245560+danparizher@users.noreply.github.com>
    Date: Tue, 4 Nov 2025 11:02:50 -0500
    Subject: [PATCH 175/188] [`refurb`] Fix false negative for underscores before
     sign in `Decimal` constructor (`FURB157`) (#21190)
    
    ## Summary
    
    Fixes FURB157 false negative where `Decimal("_-1")` was not flagged as
    verbose when underscores precede the sign character. This fixes #21186.
    
    ## Problem Analysis
    
    The `verbose-decimal-constructor` (FURB157) rule failed to detect
    verbose `Decimal` constructors when the sign character (`+` or `-`) was
    preceded by underscores. For example, `Decimal("_-1")` was not flagged,
    even though it can be simplified to `Decimal(-1)`.
    
    The bug occurred because the rule checked for the sign character at the
    start of the string before stripping leading underscores. According to
    Python's `Decimal` parser behavior (as documented in CPython's
    `_pydecimal.py`), underscores are removed before parsing the sign. The
    rule's logic didn't match this behavior, causing a false negative for
    cases like `"_-1"` where the underscore came before the sign.
    
    This was a regression introduced in version 0.14.3, as these cases were
    correctly flagged in version 0.14.2.
    
    ## Approach
    
    The fix updates the sign extraction logic to:
    1. Strip leading underscores first (matching Python's Decimal parser
    behavior)
    2. Extract the sign from the underscore-stripped string
    3. Preserve the string after the sign for normalization purposes
    
    This ensures that cases like `Decimal("_-1")`, `Decimal("_+1")`, and
    `Decimal("_-1_000")` are correctly detected and flagged. The
    normalization logic was also updated to use the string after the sign
    (without underscores) to avoid double signs in the replacement output.
    ---
     .../resources/test/fixtures/refurb/FURB157.py |  6 ++
     .../rules/verbose_decimal_constructor.rs      | 17 ++++--
     ...es__refurb__tests__FURB157_FURB157.py.snap | 59 +++++++++++++++++++
     3 files changed, 76 insertions(+), 6 deletions(-)
    
    diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py
    index d795fd1941..db49315f54 100644
    --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py
    +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py
    @@ -85,3 +85,9 @@ Decimal("1234_5678")    # Safe fix: preserves non-thousands separators
     Decimal("0001_2345")
     Decimal("000_1_2345")
     Decimal("000_000")
    +
    +# Test cases for underscores before sign
    +# https://github.com/astral-sh/ruff/issues/21186
    +Decimal("_-1")      # Should flag as verbose
    +Decimal("_+1")      # Should flag as verbose
    +Decimal("_-1_000")  # Should flag as verbose
    diff --git a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs
    index 50c98026d5..28779b021a 100644
    --- a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs
    +++ b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs
    @@ -93,16 +93,21 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal
                 // https://github.com/python/cpython/blob/ac556a2ad1213b8bb81372fe6fb762f5fcb076de/Lib/_pydecimal.py#L6060-L6077
                 // _after_ trimming whitespace from the string and removing all occurrences of "_".
                 let original_str = str_literal.to_str().trim_whitespace();
    +            // Strip leading underscores before extracting the sign, as Python's Decimal parser
    +            // removes underscores before parsing the sign.
    +            let sign_check_str = original_str.trim_start_matches('_');
                 // Extract the unary sign, if any.
    -            let (unary, original_str) = if let Some(trimmed) = original_str.strip_prefix('+') {
    +            let (unary, sign_check_str) = if let Some(trimmed) = sign_check_str.strip_prefix('+') {
                     ("+", trimmed)
    -            } else if let Some(trimmed) = original_str.strip_prefix('-') {
    +            } else if let Some(trimmed) = sign_check_str.strip_prefix('-') {
                     ("-", trimmed)
                 } else {
    -                ("", original_str)
    +                ("", sign_check_str)
                 };
    -            let mut rest = Cow::from(original_str);
    -            let has_digit_separators = memchr::memchr(b'_', rest.as_bytes()).is_some();
    +            // Save the string after the sign for normalization (before removing underscores)
    +            let str_after_sign_for_normalization = sign_check_str;
    +            let mut rest = Cow::from(sign_check_str);
    +            let has_digit_separators = memchr::memchr(b'_', original_str.as_bytes()).is_some();
                 if has_digit_separators {
                     rest = Cow::from(rest.replace('_', ""));
                 }
    @@ -123,7 +128,7 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal
     
                 // If the original string had digit separators, normalize them
                 let rest = if has_digit_separators {
    -                Cow::from(normalize_digit_separators(original_str))
    +                Cow::from(normalize_digit_separators(str_after_sign_for_normalization))
                 } else {
                     Cow::from(rest)
                 };
    diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap
    index 92e8057055..3f0a1c2cf6 100644
    --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap
    +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap
    @@ -669,6 +669,7 @@ help: Replace with `1_2345`
     85 + Decimal(1_2345)
     86 | Decimal("000_1_2345")
     87 | Decimal("000_000")
    +88 | 
     
     FURB157 [*] Verbose expression in `Decimal` constructor
       --> FURB157.py:86:9
    @@ -686,6 +687,8 @@ help: Replace with `1_2345`
        - Decimal("000_1_2345")
     86 + Decimal(1_2345)
     87 | Decimal("000_000")
    +88 | 
    +89 | # Test cases for underscores before sign
     
     FURB157 [*] Verbose expression in `Decimal` constructor
       --> FURB157.py:87:9
    @@ -694,6 +697,8 @@ FURB157 [*] Verbose expression in `Decimal` constructor
     86 | Decimal("000_1_2345")
     87 | Decimal("000_000")
        |         ^^^^^^^^^
    +88 |
    +89 | # Test cases for underscores before sign
        |
     help: Replace with `0`
     84 | # Separators _and_ leading zeros
    @@ -701,3 +706,57 @@ help: Replace with `0`
     86 | Decimal("000_1_2345")
        - Decimal("000_000")
     87 + Decimal(0)
    +88 | 
    +89 | # Test cases for underscores before sign
    +90 | # https://github.com/astral-sh/ruff/issues/21186
    +
    +FURB157 [*] Verbose expression in `Decimal` constructor
    +  --> FURB157.py:91:9
    +   |
    +89 | # Test cases for underscores before sign
    +90 | # https://github.com/astral-sh/ruff/issues/21186
    +91 | Decimal("_-1")      # Should flag as verbose
    +   |         ^^^^^
    +92 | Decimal("_+1")      # Should flag as verbose
    +93 | Decimal("_-1_000")  # Should flag as verbose
    +   |
    +help: Replace with `-1`
    +88 | 
    +89 | # Test cases for underscores before sign
    +90 | # https://github.com/astral-sh/ruff/issues/21186
    +   - Decimal("_-1")      # Should flag as verbose
    +91 + Decimal(-1)      # Should flag as verbose
    +92 | Decimal("_+1")      # Should flag as verbose
    +93 | Decimal("_-1_000")  # Should flag as verbose
    +
    +FURB157 [*] Verbose expression in `Decimal` constructor
    +  --> FURB157.py:92:9
    +   |
    +90 | # https://github.com/astral-sh/ruff/issues/21186
    +91 | Decimal("_-1")      # Should flag as verbose
    +92 | Decimal("_+1")      # Should flag as verbose
    +   |         ^^^^^
    +93 | Decimal("_-1_000")  # Should flag as verbose
    +   |
    +help: Replace with `+1`
    +89 | # Test cases for underscores before sign
    +90 | # https://github.com/astral-sh/ruff/issues/21186
    +91 | Decimal("_-1")      # Should flag as verbose
    +   - Decimal("_+1")      # Should flag as verbose
    +92 + Decimal(+1)      # Should flag as verbose
    +93 | Decimal("_-1_000")  # Should flag as verbose
    +
    +FURB157 [*] Verbose expression in `Decimal` constructor
    +  --> FURB157.py:93:9
    +   |
    +91 | Decimal("_-1")      # Should flag as verbose
    +92 | Decimal("_+1")      # Should flag as verbose
    +93 | Decimal("_-1_000")  # Should flag as verbose
    +   |         ^^^^^^^^^
    +   |
    +help: Replace with `-1_000`
    +90 | # https://github.com/astral-sh/ruff/issues/21186
    +91 | Decimal("_-1")      # Should flag as verbose
    +92 | Decimal("_+1")      # Should flag as verbose
    +   - Decimal("_-1_000")  # Should flag as verbose
    +93 + Decimal(-1_000)  # Should flag as verbose
    
    From f79044478c9999c58707f716d220767505bcf2d4 Mon Sep 17 00:00:00 2001
    From: David Peter 
    Date: Tue, 4 Nov 2025 18:36:36 +0100
    Subject: [PATCH 176/188] [ty] Fix playground crash when file name includes
     path separator (#21151)
    
    ---
     playground/ty/src/Editor/SecondaryPanel.tsx | 22 ++++++++++++++++++---
     1 file changed, 19 insertions(+), 3 deletions(-)
    
    diff --git a/playground/ty/src/Editor/SecondaryPanel.tsx b/playground/ty/src/Editor/SecondaryPanel.tsx
    index a73cb57961..6c8cd0189a 100644
    --- a/playground/ty/src/Editor/SecondaryPanel.tsx
    +++ b/playground/ty/src/Editor/SecondaryPanel.tsx
    @@ -103,11 +103,17 @@ function Content({
       }
     }
     
    +const SANDBOX_BASE_DIRECTORY = "/playground/";
    +
     function Run({ files, theme }: { files: ReadonlyFiles; theme: Theme }) {
       const [runOutput, setRunOutput] = useState | null>(null);
       const handleRun = () => {
         const output = (async () => {
    -      const pyodide = await loadPyodide();
    +      const pyodide = await loadPyodide({
    +        env: {
    +          HOME: SANDBOX_BASE_DIRECTORY,
    +        },
    +      });
     
           let combined_output = "";
     
    @@ -122,7 +128,17 @@ function Run({ files, theme }: { files: ReadonlyFiles; theme: Theme }) {
     
           let fileName = "main.py";
           for (const file of files.index) {
    -        pyodide.FS.writeFile(file.name, files.contents[file.id]);
    +        const last_separator = file.name.lastIndexOf("/");
    +
    +        if (last_separator !== -1) {
    +          const directory =
    +            SANDBOX_BASE_DIRECTORY + file.name.slice(0, last_separator);
    +          pyodide.FS.mkdirTree(directory);
    +        }
    +        pyodide.FS.writeFile(
    +          SANDBOX_BASE_DIRECTORY + file.name,
    +          files.contents[file.id],
    +        );
     
             if (file.id === files.selected) {
               fileName = file.name;
    @@ -133,7 +149,7 @@ function Run({ files, theme }: { files: ReadonlyFiles; theme: Theme }) {
           const globals = dict();
     
           try {
    -        // Patch up reveal types
    +        // Patch `reveal_type` to print runtime values
             pyodide.runPython(`
             import builtins
     
    
    From 7009d6026014532dc69a04bb135d1ffc717effd8 Mon Sep 17 00:00:00 2001
    From: Micha Reiser 
    Date: Wed, 5 Nov 2025 14:24:03 +0100
    Subject: [PATCH 177/188] [ty] Refactor `Range` to/from `TextRange` conversion
     as prep for notebook support (#21230)
    
    ---
     crates/ty_server/src/document/location.rs     |  55 ++-
     crates/ty_server/src/document/range.rs        | 359 ++++++++++++------
     .../ty_server/src/document/text_document.rs   |   4 +-
     .../ty_server/src/server/api/diagnostics.rs   |   9 +-
     .../src/server/api/requests/completion.rs     |  15 +-
     .../src/server/api/requests/doc_highlights.rs |  11 +-
     .../server/api/requests/document_symbols.rs   |  42 +-
     .../server/api/requests/goto_declaration.rs   |   8 +-
     .../server/api/requests/goto_definition.rs    |   8 +-
     .../server/api/requests/goto_references.rs    |   8 +-
     .../api/requests/goto_type_definition.rs      |   8 +-
     .../src/server/api/requests/hover.rs          |  22 +-
     .../src/server/api/requests/inlay_hints.rs    |   9 +-
     .../src/server/api/requests/prepare_rename.rs |   9 +-
     .../src/server/api/requests/rename.rs         |   8 +-
     .../server/api/requests/selection_range.rs    |  10 +-
     .../api/requests/semantic_tokens_range.rs     |  11 +-
     .../src/server/api/requests/signature_help.rs |   8 +-
     .../server/api/requests/workspace_symbols.rs  |  24 +-
     .../src/server/api/semantic_tokens.rs         |   8 +-
     crates/ty_server/src/server/api/symbols.rs    |  25 +-
     crates/ty_server/src/session.rs               |   5 +
     22 files changed, 386 insertions(+), 280 deletions(-)
    
    diff --git a/crates/ty_server/src/document/location.rs b/crates/ty_server/src/document/location.rs
    index d5924595b2..f02dc20d98 100644
    --- a/crates/ty_server/src/document/location.rs
    +++ b/crates/ty_server/src/document/location.rs
    @@ -1,12 +1,9 @@
     use crate::PositionEncoding;
     use crate::document::{FileRangeExt, ToRangeExt};
    -use crate::system::file_to_url;
     use lsp_types::Location;
     use ruff_db::files::FileRange;
    -use ruff_db::source::{line_index, source_text};
    -use ruff_text_size::Ranged;
     use ty_ide::{NavigationTarget, ReferenceTarget};
    -use ty_project::Db;
    +use ty_python_semantic::Db;
     
     pub(crate) trait ToLink {
         fn to_location(&self, db: &dyn Db, encoding: PositionEncoding) -> Option;
    @@ -21,7 +18,9 @@ pub(crate) trait ToLink {
     
     impl ToLink for NavigationTarget {
         fn to_location(&self, db: &dyn Db, encoding: PositionEncoding) -> Option {
    -        FileRange::new(self.file(), self.focus_range()).to_location(db, encoding)
    +        FileRange::new(self.file(), self.focus_range())
    +            .as_lsp_range(db, encoding)
    +            .to_location()
         }
     
         fn to_link(
    @@ -31,22 +30,24 @@ impl ToLink for NavigationTarget {
             encoding: PositionEncoding,
         ) -> Option {
             let file = self.file();
    -        let uri = file_to_url(db, file)?;
    -        let source = source_text(db, file);
    -        let index = line_index(db, file);
     
    -        let target_range = self.full_range().to_lsp_range(&source, &index, encoding);
    -        let selection_range = self.focus_range().to_lsp_range(&source, &index, encoding);
    +        // Get target_range and URI together to ensure they're consistent (same cell for notebooks)
    +        let target_location = self
    +            .full_range()
    +            .as_lsp_range(db, file, encoding)
    +            .to_location()?;
    +        let target_range = target_location.range;
     
    -        let src = src.map(|src| {
    -            let source = source_text(db, src.file());
    -            let index = line_index(db, src.file());
    +        // For selection_range, we can use as_local_range since we know it's in the same document/cell
    +        let selection_range = self
    +            .focus_range()
    +            .as_lsp_range(db, file, encoding)
    +            .to_local_range();
     
    -            src.range().to_lsp_range(&source, &index, encoding)
    -        });
    +        let src = src.map(|src| src.as_lsp_range(db, encoding).to_local_range());
     
             Some(lsp_types::LocationLink {
    -            target_uri: uri,
    +            target_uri: target_location.uri,
                 target_range,
                 target_selection_range: selection_range,
                 origin_selection_range: src,
    @@ -56,7 +57,7 @@ impl ToLink for NavigationTarget {
     
     impl ToLink for ReferenceTarget {
         fn to_location(&self, db: &dyn Db, encoding: PositionEncoding) -> Option {
    -        self.file_range().to_location(db, encoding)
    +        self.file_range().as_lsp_range(db, encoding).to_location()
         }
     
         fn to_link(
    @@ -65,22 +66,18 @@ impl ToLink for ReferenceTarget {
             src: Option,
             encoding: PositionEncoding,
         ) -> Option {
    -        let uri = file_to_url(db, self.file())?;
    -        let source = source_text(db, self.file());
    -        let index = line_index(db, self.file());
    -
    -        let target_range = self.range().to_lsp_range(&source, &index, encoding);
    +        // Get target_range and URI together to ensure they're consistent (same cell for notebooks)
    +        let target_location = self
    +            .range()
    +            .as_lsp_range(db, self.file(), encoding)
    +            .to_location()?;
    +        let target_range = target_location.range;
             let selection_range = target_range;
     
    -        let src = src.map(|src| {
    -            let source = source_text(db, src.file());
    -            let index = line_index(db, src.file());
    -
    -            src.range().to_lsp_range(&source, &index, encoding)
    -        });
    +        let src = src.map(|src| src.as_lsp_range(db, encoding).to_local_range());
     
             Some(lsp_types::LocationLink {
    -            target_uri: uri,
    +            target_uri: target_location.uri,
                 target_range,
                 target_selection_range: selection_range,
                 origin_selection_range: src,
    diff --git a/crates/ty_server/src/document/range.rs b/crates/ty_server/src/document/range.rs
    index 1d107e5a30..1e7f381ae5 100644
    --- a/crates/ty_server/src/document/range.rs
    +++ b/crates/ty_server/src/document/range.rs
    @@ -1,148 +1,288 @@
     use super::PositionEncoding;
    -use super::notebook;
     use crate::system::file_to_url;
    +use ty_python_semantic::Db;
     
     use lsp_types as types;
    -use lsp_types::Location;
    -
    -use ruff_db::files::FileRange;
    +use lsp_types::{Location, Position, Url};
    +use ruff_db::files::{File, FileRange};
     use ruff_db::source::{line_index, source_text};
    -use ruff_notebook::NotebookIndex;
     use ruff_source_file::LineIndex;
     use ruff_source_file::{OneIndexed, SourceLocation};
     use ruff_text_size::{Ranged, TextRange, TextSize};
    -use ty_python_semantic::Db;
     
    -#[expect(dead_code)]
    -pub(crate) struct NotebookRange {
    -    pub(crate) cell: notebook::CellId,
    -    pub(crate) range: types::Range,
    +/// Represents a range that has been prepared for LSP conversion but requires
    +/// a decision about how to use it - either as a local range within the same
    +/// document/cell, or as a location that can reference any document in the project.
    +#[derive(Clone)]
    +pub(crate) struct LspRange<'db> {
    +    file: File,
    +    range: TextRange,
    +    db: &'db dyn Db,
    +    encoding: PositionEncoding,
    +}
    +
    +impl std::fmt::Debug for LspRange<'_> {
    +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    +        f.debug_struct("LspRange")
    +            .field("range", &self.range)
    +            .field("file", &self.file)
    +            .field("encoding", &self.encoding)
    +            .finish_non_exhaustive()
    +    }
    +}
    +
    +impl LspRange<'_> {
    +    /// Convert to an LSP Range for use within the same document/cell.
    +    /// Returns only the LSP Range without any URI information.
    +    ///
    +    /// Use this when you already have a URI context and this range is guaranteed
    +    /// to be within the same document/cell:
    +    /// - Selection ranges within a `LocationLink` (where `target_uri` provides context)
    +    /// - Additional ranges in the same cell (e.g., `selection_range` when you already have `target_range`)
    +    ///
    +    /// Do NOT use this for standalone ranges - use `to_location()` instead to ensure
    +    /// the URI and range are consistent.
    +    pub(crate) fn to_local_range(&self) -> types::Range {
    +        self.to_uri_and_range().1
    +    }
    +
    +    /// Convert to a Location that can reference any document.
    +    /// Returns a Location with both URI and Range.
    +    ///
    +    /// Use this for:
    +    /// - Go-to-definition targets
    +    /// - References
    +    /// - Diagnostics related information
    +    /// - Any cross-file navigation
    +    pub(crate) fn to_location(&self) -> Option {
    +        let (uri, range) = self.to_uri_and_range();
    +        Some(Location { uri: uri?, range })
    +    }
    +
    +    pub(crate) fn to_uri_and_range(&self) -> (Option, lsp_types::Range) {
    +        let source = source_text(self.db, self.file);
    +        let index = line_index(self.db, self.file);
    +
    +        let uri = file_to_url(self.db, self.file);
    +        let range = text_range_to_lsp_range(self.range, &source, &index, self.encoding);
    +        (uri, range)
    +    }
    +}
    +
    +/// Represents a position that has been prepared for LSP conversion but requires
    +/// a decision about how to use it - either as a local position within the same
    +/// document/cell, or as a location with a single-point range that can reference
    +/// any document in the project.
    +#[derive(Clone)]
    +pub(crate) struct LspPosition<'db> {
    +    file: File,
    +    position: TextSize,
    +    db: &'db dyn Db,
    +    encoding: PositionEncoding,
    +}
    +
    +impl std::fmt::Debug for LspPosition<'_> {
    +    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    +        f.debug_struct("LspPosition")
    +            .field("position", &self.position)
    +            .field("file", &self.file)
    +            .field("encoding", &self.encoding)
    +            .finish_non_exhaustive()
    +    }
    +}
    +
    +impl LspPosition<'_> {
    +    /// Convert to an LSP Position for use within the same document/cell.
    +    /// Returns only the LSP Position without any URI information.
    +    ///
    +    /// Use this when you already have a URI context and this position is guaranteed
    +    /// to be within the same document/cell:
    +    /// - Inlay hints (where the document URI is already known)
    +    /// - Positions within the same cell as a parent range
    +    ///
    +    /// Do NOT use this for standalone positions that might need a URI - use
    +    /// `to_location()` instead to ensure the URI and position are consistent.
    +    pub(crate) fn to_local_position(&self) -> types::Position {
    +        self.to_location().1
    +    }
    +
    +    /// Convert to a Location with a single-point range that can reference any document.
    +    /// Returns a Location with both URI and a range where start == end.
    +    ///
    +    /// Use this for any cross-file navigation where you need both URI and position.
    +    pub(crate) fn to_location(&self) -> (Option, Position) {
    +        let source = source_text(self.db, self.file);
    +        let index = line_index(self.db, self.file);
    +
    +        let uri = file_to_url(self.db, self.file);
    +        let position = text_size_to_lsp_position(self.position, &source, &index, self.encoding);
    +        (uri, position)
    +    }
     }
     
     pub(crate) trait RangeExt {
    -    fn to_text_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding)
    -    -> TextRange;
    +    /// Convert an LSP Range to internal `TextRange`.
    +    fn to_text_range(
    +        &self,
    +        db: &dyn Db,
    +        file: File,
    +        url: &lsp_types::Url,
    +        encoding: PositionEncoding,
    +    ) -> TextRange;
    +}
    +
    +impl RangeExt for lsp_types::Range {
    +    fn to_text_range(
    +        &self,
    +        db: &dyn Db,
    +        file: File,
    +        url: &lsp_types::Url,
    +        encoding: PositionEncoding,
    +    ) -> TextRange {
    +        let start = self.start.to_text_size(db, file, url, encoding);
    +        let end = self.end.to_text_size(db, file, url, encoding);
    +
    +        TextRange::new(start, end)
    +    }
     }
     
     pub(crate) trait PositionExt {
    -    fn to_text_size(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> TextSize;
    +    /// Convert an LSP Position to internal `TextSize`.
    +    fn to_text_size(
    +        &self,
    +        db: &dyn Db,
    +        file: File,
    +        url: &lsp_types::Url,
    +        encoding: PositionEncoding,
    +    ) -> TextSize;
    +}
    +
    +impl PositionExt for lsp_types::Position {
    +    fn to_text_size(
    +        &self,
    +        db: &dyn Db,
    +        file: File,
    +        _url: &lsp_types::Url,
    +        encoding: PositionEncoding,
    +    ) -> TextSize {
    +        let source = source_text(db, file);
    +        let index = line_index(db, file);
    +
    +        lsp_position_to_text_size(*self, &source, &index, encoding)
    +    }
     }
     
     pub(crate) trait TextSizeExt {
    -    fn to_position(
    -        self,
    -        text: &str,
    -        index: &LineIndex,
    +    /// Converts this position to an `LspPosition`, which then requires an explicit
    +    /// decision about how to use it (as a local position or as a location).
    +    fn as_lsp_position<'db>(
    +        &self,
    +        db: &'db dyn Db,
    +        file: File,
             encoding: PositionEncoding,
    -    ) -> types::Position
    +    ) -> LspPosition<'db>
         where
             Self: Sized;
     }
     
     impl TextSizeExt for TextSize {
    -    fn to_position(
    -        self,
    -        text: &str,
    -        index: &LineIndex,
    +    fn as_lsp_position<'db>(
    +        &self,
    +        db: &'db dyn Db,
    +        file: File,
             encoding: PositionEncoding,
    -    ) -> types::Position {
    -        let source_location = index.source_location(self, text, encoding.into());
    -        source_location_to_position(&source_location)
    +    ) -> LspPosition<'db> {
    +        LspPosition {
    +            file,
    +            position: *self,
    +            db,
    +            encoding,
    +        }
         }
     }
     
     pub(crate) trait ToRangeExt {
    -    fn to_lsp_range(
    +    /// Converts this range to an `LspRange`, which then requires an explicit
    +    /// decision about how to use it (as a local range or as a location).
    +    fn as_lsp_range<'db>(
             &self,
    -        text: &str,
    -        index: &LineIndex,
    +        db: &'db dyn Db,
    +        file: File,
             encoding: PositionEncoding,
    -    ) -> types::Range;
    -
    -    #[expect(dead_code)]
    -    fn to_notebook_range(
    -        &self,
    -        text: &str,
    -        source_index: &LineIndex,
    -        notebook_index: &NotebookIndex,
    -        encoding: PositionEncoding,
    -    ) -> NotebookRange;
    +    ) -> LspRange<'db>;
     }
     
     fn u32_index_to_usize(index: u32) -> usize {
         usize::try_from(index).expect("u32 fits in usize")
     }
     
    -impl PositionExt for lsp_types::Position {
    -    fn to_text_size(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> TextSize {
    -        index.offset(
    -            SourceLocation {
    -                line: OneIndexed::from_zero_indexed(u32_index_to_usize(self.line)),
    -                character_offset: OneIndexed::from_zero_indexed(u32_index_to_usize(self.character)),
    -            },
    -            text,
    -            encoding.into(),
    -        )
    +fn text_size_to_lsp_position(
    +    offset: TextSize,
    +    text: &str,
    +    index: &LineIndex,
    +    encoding: PositionEncoding,
    +) -> types::Position {
    +    let source_location = index.source_location(offset, text, encoding.into());
    +    source_location_to_position(&source_location)
    +}
    +
    +fn text_range_to_lsp_range(
    +    range: TextRange,
    +    text: &str,
    +    index: &LineIndex,
    +    encoding: PositionEncoding,
    +) -> types::Range {
    +    types::Range {
    +        start: text_size_to_lsp_position(range.start(), text, index, encoding),
    +        end: text_size_to_lsp_position(range.end(), text, index, encoding),
         }
     }
     
    -impl RangeExt for lsp_types::Range {
    -    fn to_text_range(
    -        &self,
    -        text: &str,
    -        index: &LineIndex,
    -        encoding: PositionEncoding,
    -    ) -> TextRange {
    -        TextRange::new(
    -            self.start.to_text_size(text, index, encoding),
    -            self.end.to_text_size(text, index, encoding),
    -        )
    -    }
    +/// Helper function to convert an LSP Position to internal `TextSize`.
    +/// This is used internally by the `PositionExt` trait and other helpers.
    +fn lsp_position_to_text_size(
    +    position: lsp_types::Position,
    +    text: &str,
    +    index: &LineIndex,
    +    encoding: PositionEncoding,
    +) -> TextSize {
    +    index.offset(
    +        SourceLocation {
    +            line: OneIndexed::from_zero_indexed(u32_index_to_usize(position.line)),
    +            character_offset: OneIndexed::from_zero_indexed(u32_index_to_usize(position.character)),
    +        },
    +        text,
    +        encoding.into(),
    +    )
    +}
    +
    +/// Helper function to convert an LSP Range to internal `TextRange`.
    +/// This is used internally by the `RangeExt` trait and in special cases
    +/// where `db` and `file` are not available (e.g., when applying document changes).
    +pub(crate) fn lsp_range_to_text_range(
    +    range: lsp_types::Range,
    +    text: &str,
    +    index: &LineIndex,
    +    encoding: PositionEncoding,
    +) -> TextRange {
    +    TextRange::new(
    +        lsp_position_to_text_size(range.start, text, index, encoding),
    +        lsp_position_to_text_size(range.end, text, index, encoding),
    +    )
     }
     
     impl ToRangeExt for TextRange {
    -    fn to_lsp_range(
    +    fn as_lsp_range<'db>(
             &self,
    -        text: &str,
    -        index: &LineIndex,
    +        db: &'db dyn Db,
    +        file: File,
             encoding: PositionEncoding,
    -    ) -> types::Range {
    -        types::Range {
    -            start: self.start().to_position(text, index, encoding),
    -            end: self.end().to_position(text, index, encoding),
    -        }
    -    }
    -
    -    fn to_notebook_range(
    -        &self,
    -        text: &str,
    -        source_index: &LineIndex,
    -        notebook_index: &NotebookIndex,
    -        encoding: PositionEncoding,
    -    ) -> NotebookRange {
    -        let start = source_index.source_location(self.start(), text, encoding.into());
    -        let mut end = source_index.source_location(self.end(), text, encoding.into());
    -        let starting_cell = notebook_index.cell(start.line);
    -
    -        // weird edge case here - if the end of the range is where the newline after the cell got added (making it 'out of bounds')
    -        // we need to move it one character back (which should place it at the end of the last line).
    -        // we test this by checking if the ending offset is in a different (or nonexistent) cell compared to the cell of the starting offset.
    -        if notebook_index.cell(end.line) != starting_cell {
    -            end.line = end.line.saturating_sub(1);
    -            let offset = self.end().checked_sub(1.into()).unwrap_or_default();
    -            end.character_offset = source_index
    -                .source_location(offset, text, encoding.into())
    -                .character_offset;
    -        }
    -
    -        let start = source_location_to_position(¬ebook_index.translate_source_location(&start));
    -        let end = source_location_to_position(¬ebook_index.translate_source_location(&end));
    -
    -        NotebookRange {
    -            cell: starting_cell
    -                .map(OneIndexed::to_zero_indexed)
    -                .unwrap_or_default(),
    -            range: types::Range { start, end },
    +    ) -> LspRange<'db> {
    +        LspRange {
    +            file,
    +            range: *self,
    +            db,
    +            encoding,
             }
         }
     }
    @@ -156,17 +296,18 @@ fn source_location_to_position(location: &SourceLocation) -> types::Position {
     }
     
     pub(crate) trait FileRangeExt {
    -    fn to_location(&self, db: &dyn Db, encoding: PositionEncoding) -> Option;
    +    /// Converts this file range to an `LspRange`, which then requires an explicit
    +    /// decision about how to use it (as a local range or as a location).
    +    fn as_lsp_range<'db>(&self, db: &'db dyn Db, encoding: PositionEncoding) -> LspRange<'db>;
     }
     
     impl FileRangeExt for FileRange {
    -    fn to_location(&self, db: &dyn Db, encoding: PositionEncoding) -> Option {
    -        let file = self.file();
    -        let uri = file_to_url(db, file)?;
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
    -
    -        let range = self.range().to_lsp_range(&source, &line_index, encoding);
    -        Some(Location { uri, range })
    +    fn as_lsp_range<'db>(&self, db: &'db dyn Db, encoding: PositionEncoding) -> LspRange<'db> {
    +        LspRange {
    +            file: self.file(),
    +            range: self.range(),
    +            db,
    +            encoding,
    +        }
         }
     }
    diff --git a/crates/ty_server/src/document/text_document.rs b/crates/ty_server/src/document/text_document.rs
    index 9898dd670b..e6cd4c4e0b 100644
    --- a/crates/ty_server/src/document/text_document.rs
    +++ b/crates/ty_server/src/document/text_document.rs
    @@ -3,7 +3,7 @@ use ruff_source_file::LineIndex;
     
     use crate::PositionEncoding;
     
    -use super::RangeExt;
    +use super::range::lsp_range_to_text_range;
     
     pub(crate) type DocumentVersion = i32;
     
    @@ -114,7 +114,7 @@ impl TextDocument {
             } in changes
             {
                 if let Some(range) = range {
    -                let range = range.to_text_range(&new_contents, &active_index, encoding);
    +                let range = lsp_range_to_text_range(range, &new_contents, &active_index, encoding);
     
                     new_contents.replace_range(
                         usize::from(range.start())..usize::from(range.end()),
    diff --git a/crates/ty_server/src/server/api/diagnostics.rs b/crates/ty_server/src/server/api/diagnostics.rs
    index 7680dc1bad..adbb17dcdf 100644
    --- a/crates/ty_server/src/server/api/diagnostics.rs
    +++ b/crates/ty_server/src/server/api/diagnostics.rs
    @@ -9,7 +9,6 @@ use rustc_hash::FxHashMap;
     
     use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic};
     use ruff_db::files::FileRange;
    -use ruff_db::source::{line_index, source_text};
     use ruff_db::system::SystemPathBuf;
     use ty_project::{Db, ProjectDatabase};
     
    @@ -279,11 +278,9 @@ pub(super) fn to_lsp_diagnostic(
     ) -> Diagnostic {
         let range = if let Some(span) = diagnostic.primary_span() {
             let file = span.expect_ty_file();
    -        let index = line_index(db, file);
    -        let source = source_text(db, file);
     
             span.range()
    -            .map(|range| range.to_lsp_range(&source, &index, encoding))
    +            .map(|range| range.as_lsp_range(db, file, encoding).to_local_range())
                 .unwrap_or_default()
         } else {
             Range::default()
    @@ -365,7 +362,7 @@ fn annotation_to_related_information(
     
         let annotation_message = annotation.get_message()?;
         let range = FileRange::try_from(span).ok()?;
    -    let location = range.to_location(db, encoding)?;
    +    let location = range.as_lsp_range(db, encoding).to_location()?;
     
         Some(DiagnosticRelatedInformation {
             location,
    @@ -383,7 +380,7 @@ fn sub_diagnostic_to_related_information(
     
         let span = primary_annotation.get_span();
         let range = FileRange::try_from(span).ok()?;
    -    let location = range.to_location(db, encoding)?;
    +    let location = range.as_lsp_range(db, encoding).to_location()?;
     
         Some(DiagnosticRelatedInformation {
             location,
    diff --git a/crates/ty_server/src/server/api/requests/completion.rs b/crates/ty_server/src/server/api/requests/completion.rs
    index bf712c5efb..6473661939 100644
    --- a/crates/ty_server/src/server/api/requests/completion.rs
    +++ b/crates/ty_server/src/server/api/requests/completion.rs
    @@ -6,7 +6,6 @@ use lsp_types::{
         CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionList,
         CompletionParams, CompletionResponse, Documentation, TextEdit, Url,
     };
    -use ruff_db::source::{line_index, source_text};
     use ruff_source_file::OneIndexed;
     use ruff_text_size::Ranged;
     use ty_ide::{CompletionKind, CompletionSettings, completion};
    @@ -49,11 +48,10 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
                 return Ok(None);
             };
     
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
             let offset = params.text_document_position.position.to_text_size(
    -            &source,
    -            &line_index,
    +            db,
    +            file,
    +            snapshot.url(),
                 snapshot.encoding(),
             );
             let settings = CompletionSettings {
    @@ -73,9 +71,10 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
                     let kind = comp.kind(db).map(ty_kind_to_lsp_kind);
                     let type_display = comp.ty.map(|ty| ty.display(db).to_string());
                     let import_edit = comp.import.as_ref().map(|edit| {
    -                    let range =
    -                        edit.range()
    -                            .to_lsp_range(&source, &line_index, snapshot.encoding());
    +                    let range = edit
    +                        .range()
    +                        .as_lsp_range(db, file, snapshot.encoding())
    +                        .to_local_range();
                         TextEdit {
                             range,
                             new_text: edit.content().map(ToString::to_string).unwrap_or_default(),
    diff --git a/crates/ty_server/src/server/api/requests/doc_highlights.rs b/crates/ty_server/src/server/api/requests/doc_highlights.rs
    index b5b6d0d9ab..c96c3d4fef 100644
    --- a/crates/ty_server/src/server/api/requests/doc_highlights.rs
    +++ b/crates/ty_server/src/server/api/requests/doc_highlights.rs
    @@ -2,7 +2,6 @@ use std::borrow::Cow;
     
     use lsp_types::request::DocumentHighlightRequest;
     use lsp_types::{DocumentHighlight, DocumentHighlightKind, DocumentHighlightParams, Url};
    -use ruff_db::source::{line_index, source_text};
     use ty_ide::{ReferenceKind, document_highlights};
     use ty_project::ProjectDatabase;
     
    @@ -41,11 +40,10 @@ impl BackgroundDocumentRequestHandler for DocumentHighlightRequestHandler {
                 return Ok(None);
             };
     
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
             let offset = params.text_document_position_params.position.to_text_size(
    -            &source,
    -            &line_index,
    +            db,
    +            file,
    +            snapshot.url(),
                 snapshot.encoding(),
             );
     
    @@ -58,7 +56,8 @@ impl BackgroundDocumentRequestHandler for DocumentHighlightRequestHandler {
                 .map(|target| {
                     let range = target
                         .range()
    -                    .to_lsp_range(&source, &line_index, snapshot.encoding());
    +                    .as_lsp_range(db, file, snapshot.encoding())
    +                    .to_local_range();
     
                     let kind = match target.kind() {
                         ReferenceKind::Read => Some(DocumentHighlightKind::READ),
    diff --git a/crates/ty_server/src/server/api/requests/document_symbols.rs b/crates/ty_server/src/server/api/requests/document_symbols.rs
    index ea5ee312c6..980e0850ef 100644
    --- a/crates/ty_server/src/server/api/requests/document_symbols.rs
    +++ b/crates/ty_server/src/server/api/requests/document_symbols.rs
    @@ -2,10 +2,9 @@ use std::borrow::Cow;
     
     use lsp_types::request::DocumentSymbolRequest;
     use lsp_types::{DocumentSymbol, DocumentSymbolParams, SymbolInformation, Url};
    -use ruff_db::source::{line_index, source_text};
    -use ruff_source_file::LineIndex;
    +use ruff_db::files::File;
     use ty_ide::{HierarchicalSymbols, SymbolId, SymbolInfo, document_symbols};
    -use ty_project::ProjectDatabase;
    +use ty_project::{Db, ProjectDatabase};
     
     use crate::document::{PositionEncoding, ToRangeExt};
     use crate::server::api::symbols::{convert_symbol_kind, convert_to_lsp_symbol_information};
    @@ -30,7 +29,7 @@ impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler {
             db: &ProjectDatabase,
             snapshot: &DocumentSnapshot,
             _client: &Client,
    -        params: DocumentSymbolParams,
    +        _params: DocumentSymbolParams,
         ) -> crate::server::Result> {
             if snapshot
                 .workspace_settings()
    @@ -43,9 +42,6 @@ impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler {
                 return Ok(None);
             };
     
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
    -
             // Check if the client supports hierarchical document symbols
             let supports_hierarchical = snapshot
                 .resolved_client_capabilities()
    @@ -62,11 +58,11 @@ impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler {
                     .iter()
                     .map(|(id, symbol)| {
                         convert_to_lsp_document_symbol(
    +                        db,
    +                        file,
                             &symbols,
                             id,
                             symbol,
    -                        &source,
    -                        &line_index,
                             snapshot.encoding(),
                         )
                     })
    @@ -77,14 +73,8 @@ impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler {
                 // Return flattened symbols as SymbolInformation
                 let lsp_symbols: Vec = symbols
                     .iter()
    -                .map(|(_, symbol)| {
    -                    convert_to_lsp_symbol_information(
    -                        symbol,
    -                        ¶ms.text_document.uri,
    -                        &source,
    -                        &line_index,
    -                        snapshot.encoding(),
    -                    )
    +                .filter_map(|(_, symbol)| {
    +                    convert_to_lsp_symbol_information(db, file, symbol, snapshot.encoding())
                     })
                     .collect();
     
    @@ -96,11 +86,11 @@ impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler {
     impl RetriableRequestHandler for DocumentSymbolRequestHandler {}
     
     fn convert_to_lsp_document_symbol(
    +    db: &dyn Db,
    +    file: File,
         symbols: &HierarchicalSymbols,
         id: SymbolId,
         symbol: SymbolInfo<'_>,
    -    source: &str,
    -    line_index: &LineIndex,
         encoding: PositionEncoding,
     ) -> DocumentSymbol {
         let symbol_kind = convert_symbol_kind(symbol.kind);
    @@ -112,15 +102,19 @@ fn convert_to_lsp_document_symbol(
             tags: None,
             #[allow(deprecated)]
             deprecated: None,
    -        range: symbol.full_range.to_lsp_range(source, line_index, encoding),
    -        selection_range: symbol.name_range.to_lsp_range(source, line_index, encoding),
    +        range: symbol
    +            .full_range
    +            .as_lsp_range(db, file, encoding)
    +            .to_local_range(),
    +        selection_range: symbol
    +            .name_range
    +            .as_lsp_range(db, file, encoding)
    +            .to_local_range(),
             children: Some(
                 symbols
                     .children(id)
                     .map(|(child_id, child)| {
    -                    convert_to_lsp_document_symbol(
    -                        symbols, child_id, child, source, line_index, encoding,
    -                    )
    +                    convert_to_lsp_document_symbol(db, file, symbols, child_id, child, encoding)
                     })
                     .collect(),
             ),
    diff --git a/crates/ty_server/src/server/api/requests/goto_declaration.rs b/crates/ty_server/src/server/api/requests/goto_declaration.rs
    index 1c16a74bc5..2a8c931401 100644
    --- a/crates/ty_server/src/server/api/requests/goto_declaration.rs
    +++ b/crates/ty_server/src/server/api/requests/goto_declaration.rs
    @@ -2,7 +2,6 @@ use std::borrow::Cow;
     
     use lsp_types::request::{GotoDeclaration, GotoDeclarationParams};
     use lsp_types::{GotoDefinitionResponse, Url};
    -use ruff_db::source::{line_index, source_text};
     use ty_ide::goto_declaration;
     use ty_project::ProjectDatabase;
     
    @@ -41,11 +40,10 @@ impl BackgroundDocumentRequestHandler for GotoDeclarationRequestHandler {
                 return Ok(None);
             };
     
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
             let offset = params.text_document_position_params.position.to_text_size(
    -            &source,
    -            &line_index,
    +            db,
    +            file,
    +            snapshot.url(),
                 snapshot.encoding(),
             );
     
    diff --git a/crates/ty_server/src/server/api/requests/goto_definition.rs b/crates/ty_server/src/server/api/requests/goto_definition.rs
    index bc33411778..343f90a5c9 100644
    --- a/crates/ty_server/src/server/api/requests/goto_definition.rs
    +++ b/crates/ty_server/src/server/api/requests/goto_definition.rs
    @@ -2,7 +2,6 @@ use std::borrow::Cow;
     
     use lsp_types::request::GotoDefinition;
     use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse, Url};
    -use ruff_db::source::{line_index, source_text};
     use ty_ide::goto_definition;
     use ty_project::ProjectDatabase;
     
    @@ -41,11 +40,10 @@ impl BackgroundDocumentRequestHandler for GotoDefinitionRequestHandler {
                 return Ok(None);
             };
     
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
             let offset = params.text_document_position_params.position.to_text_size(
    -            &source,
    -            &line_index,
    +            db,
    +            file,
    +            snapshot.url(),
                 snapshot.encoding(),
             );
     
    diff --git a/crates/ty_server/src/server/api/requests/goto_references.rs b/crates/ty_server/src/server/api/requests/goto_references.rs
    index 3afaf28b14..6cdb8e21a4 100644
    --- a/crates/ty_server/src/server/api/requests/goto_references.rs
    +++ b/crates/ty_server/src/server/api/requests/goto_references.rs
    @@ -2,7 +2,6 @@ use std::borrow::Cow;
     
     use lsp_types::request::References;
     use lsp_types::{Location, ReferenceParams, Url};
    -use ruff_db::source::{line_index, source_text};
     use ty_ide::goto_references;
     use ty_project::ProjectDatabase;
     
    @@ -41,11 +40,10 @@ impl BackgroundDocumentRequestHandler for ReferencesRequestHandler {
                 return Ok(None);
             };
     
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
             let offset = params.text_document_position.position.to_text_size(
    -            &source,
    -            &line_index,
    +            db,
    +            file,
    +            snapshot.url(),
                 snapshot.encoding(),
             );
     
    diff --git a/crates/ty_server/src/server/api/requests/goto_type_definition.rs b/crates/ty_server/src/server/api/requests/goto_type_definition.rs
    index 379defa344..11564f50d7 100644
    --- a/crates/ty_server/src/server/api/requests/goto_type_definition.rs
    +++ b/crates/ty_server/src/server/api/requests/goto_type_definition.rs
    @@ -2,7 +2,6 @@ use std::borrow::Cow;
     
     use lsp_types::request::{GotoTypeDefinition, GotoTypeDefinitionParams};
     use lsp_types::{GotoDefinitionResponse, Url};
    -use ruff_db::source::{line_index, source_text};
     use ty_ide::goto_type_definition;
     use ty_project::ProjectDatabase;
     
    @@ -41,11 +40,10 @@ impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler {
                 return Ok(None);
             };
     
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
             let offset = params.text_document_position_params.position.to_text_size(
    -            &source,
    -            &line_index,
    +            db,
    +            file,
    +            snapshot.url(),
                 snapshot.encoding(),
             );
     
    diff --git a/crates/ty_server/src/server/api/requests/hover.rs b/crates/ty_server/src/server/api/requests/hover.rs
    index cc8f8e0dab..d051007003 100644
    --- a/crates/ty_server/src/server/api/requests/hover.rs
    +++ b/crates/ty_server/src/server/api/requests/hover.rs
    @@ -1,6 +1,6 @@
     use std::borrow::Cow;
     
    -use crate::document::{PositionExt, ToRangeExt};
    +use crate::document::{FileRangeExt, PositionExt};
     use crate::server::api::traits::{
         BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
     };
    @@ -8,8 +8,6 @@ use crate::session::DocumentSnapshot;
     use crate::session::client::Client;
     use lsp_types::request::HoverRequest;
     use lsp_types::{HoverContents, HoverParams, MarkupContent, Url};
    -use ruff_db::source::{line_index, source_text};
    -use ruff_text_size::Ranged;
     use ty_ide::{MarkupKind, hover};
     use ty_project::ProjectDatabase;
     
    @@ -41,11 +39,10 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler {
                 return Ok(None);
             };
     
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
             let offset = params.text_document_position_params.position.to_text_size(
    -            &source,
    -            &line_index,
    +            db,
    +            file,
    +            snapshot.url(),
                 snapshot.encoding(),
             );
     
    @@ -69,11 +66,12 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler {
                     kind: lsp_markup_kind,
                     value: contents,
                 }),
    -            range: Some(range_info.file_range().range().to_lsp_range(
    -                &source,
    -                &line_index,
    -                snapshot.encoding(),
    -            )),
    +            range: Some(
    +                range_info
    +                    .file_range()
    +                    .as_lsp_range(db, snapshot.encoding())
    +                    .to_local_range(),
    +            ),
             }))
         }
     }
    diff --git a/crates/ty_server/src/server/api/requests/inlay_hints.rs b/crates/ty_server/src/server/api/requests/inlay_hints.rs
    index 21eb1d09b6..ec445f9b1e 100644
    --- a/crates/ty_server/src/server/api/requests/inlay_hints.rs
    +++ b/crates/ty_server/src/server/api/requests/inlay_hints.rs
    @@ -8,7 +8,6 @@ use crate::session::DocumentSnapshot;
     use crate::session::client::Client;
     use lsp_types::request::InlayHintRequest;
     use lsp_types::{InlayHintParams, Url};
    -use ruff_db::source::{line_index, source_text};
     use ty_ide::{InlayHintKind, InlayHintLabel, inlay_hints};
     use ty_project::ProjectDatabase;
     
    @@ -40,12 +39,9 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler {
                 return Ok(None);
             };
     
    -        let index = line_index(db, file);
    -        let source = source_text(db, file);
    -
             let range = params
                 .range
    -            .to_text_range(&source, &index, snapshot.encoding());
    +            .to_text_range(db, file, snapshot.url(), snapshot.encoding());
     
             let inlay_hints = inlay_hints(db, file, range, workspace_settings.inlay_hints());
     
    @@ -54,7 +50,8 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler {
                 .map(|hint| lsp_types::InlayHint {
                     position: hint
                         .position
    -                    .to_position(&source, &index, snapshot.encoding()),
    +                    .as_lsp_position(db, file, snapshot.encoding())
    +                    .to_local_position(),
                     label: inlay_hint_label(&hint.label),
                     kind: Some(inlay_hint_kind(&hint.kind)),
                     tooltip: None,
    diff --git a/crates/ty_server/src/server/api/requests/prepare_rename.rs b/crates/ty_server/src/server/api/requests/prepare_rename.rs
    index a12541729d..2593122530 100644
    --- a/crates/ty_server/src/server/api/requests/prepare_rename.rs
    +++ b/crates/ty_server/src/server/api/requests/prepare_rename.rs
    @@ -2,7 +2,6 @@ use std::borrow::Cow;
     
     use lsp_types::request::PrepareRenameRequest;
     use lsp_types::{PrepareRenameResponse, TextDocumentPositionParams, Url};
    -use ruff_db::source::{line_index, source_text};
     use ty_ide::can_rename;
     use ty_project::ProjectDatabase;
     
    @@ -41,17 +40,17 @@ impl BackgroundDocumentRequestHandler for PrepareRenameRequestHandler {
                 return Ok(None);
             };
     
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
             let offset = params
                 .position
    -            .to_text_size(&source, &line_index, snapshot.encoding());
    +            .to_text_size(db, file, snapshot.url(), snapshot.encoding());
     
             let Some(range) = can_rename(db, file, offset) else {
                 return Ok(None);
             };
     
    -        let lsp_range = range.to_lsp_range(&source, &line_index, snapshot.encoding());
    +        let lsp_range = range
    +            .as_lsp_range(db, file, snapshot.encoding())
    +            .to_local_range();
     
             Ok(Some(PrepareRenameResponse::Range(lsp_range)))
         }
    diff --git a/crates/ty_server/src/server/api/requests/rename.rs b/crates/ty_server/src/server/api/requests/rename.rs
    index d434cb733e..efa3891ced 100644
    --- a/crates/ty_server/src/server/api/requests/rename.rs
    +++ b/crates/ty_server/src/server/api/requests/rename.rs
    @@ -3,7 +3,6 @@ use std::collections::HashMap;
     
     use lsp_types::request::Rename;
     use lsp_types::{RenameParams, TextEdit, Url, WorkspaceEdit};
    -use ruff_db::source::{line_index, source_text};
     use ty_ide::rename;
     use ty_project::ProjectDatabase;
     
    @@ -42,11 +41,10 @@ impl BackgroundDocumentRequestHandler for RenameRequestHandler {
                 return Ok(None);
             };
     
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
             let offset = params.text_document_position.position.to_text_size(
    -            &source,
    -            &line_index,
    +            db,
    +            file,
    +            snapshot.url(),
                 snapshot.encoding(),
             );
     
    diff --git a/crates/ty_server/src/server/api/requests/selection_range.rs b/crates/ty_server/src/server/api/requests/selection_range.rs
    index 516ea6aeda..77d9df4c25 100644
    --- a/crates/ty_server/src/server/api/requests/selection_range.rs
    +++ b/crates/ty_server/src/server/api/requests/selection_range.rs
    @@ -2,7 +2,6 @@ use std::borrow::Cow;
     
     use lsp_types::request::SelectionRangeRequest;
     use lsp_types::{SelectionRange as LspSelectionRange, SelectionRangeParams, Url};
    -use ruff_db::source::{line_index, source_text};
     use ty_ide::selection_range;
     use ty_project::ProjectDatabase;
     
    @@ -41,13 +40,10 @@ impl BackgroundDocumentRequestHandler for SelectionRangeRequestHandler {
                 return Ok(None);
             };
     
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
    -
             let mut results = Vec::new();
     
             for position in params.positions {
    -            let offset = position.to_text_size(&source, &line_index, snapshot.encoding());
    +            let offset = position.to_text_size(db, file, snapshot.url(), snapshot.encoding());
     
                 let ranges = selection_range(db, file, offset);
                 if !ranges.is_empty() {
    @@ -55,7 +51,9 @@ impl BackgroundDocumentRequestHandler for SelectionRangeRequestHandler {
                     let mut lsp_range = None;
                     for &range in &ranges {
                         lsp_range = Some(LspSelectionRange {
    -                        range: range.to_lsp_range(&source, &line_index, snapshot.encoding()),
    +                        range: range
    +                            .as_lsp_range(db, file, snapshot.encoding())
    +                            .to_local_range(),
                             parent: lsp_range.map(Box::new),
                         });
                     }
    diff --git a/crates/ty_server/src/server/api/requests/semantic_tokens_range.rs b/crates/ty_server/src/server/api/requests/semantic_tokens_range.rs
    index 03193b32a6..7daa116876 100644
    --- a/crates/ty_server/src/server/api/requests/semantic_tokens_range.rs
    +++ b/crates/ty_server/src/server/api/requests/semantic_tokens_range.rs
    @@ -8,7 +8,6 @@ use crate::server::api::traits::{
     use crate::session::DocumentSnapshot;
     use crate::session::client::Client;
     use lsp_types::{SemanticTokens, SemanticTokensRangeParams, SemanticTokensRangeResult, Url};
    -use ruff_db::source::{line_index, source_text};
     use ty_project::ProjectDatabase;
     
     pub(crate) struct SemanticTokensRangeRequestHandler;
    @@ -39,13 +38,11 @@ impl BackgroundDocumentRequestHandler for SemanticTokensRangeRequestHandler {
                 return Ok(None);
             };
     
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
    -
             // Convert LSP range to text offsets
    -        let requested_range = params
    -            .range
    -            .to_text_range(&source, &line_index, snapshot.encoding());
    +        let requested_range =
    +            params
    +                .range
    +                .to_text_range(db, file, snapshot.url(), snapshot.encoding());
     
             let lsp_tokens = generate_semantic_tokens(
                 db,
    diff --git a/crates/ty_server/src/server/api/requests/signature_help.rs b/crates/ty_server/src/server/api/requests/signature_help.rs
    index f9b20cccd9..99d60c398f 100644
    --- a/crates/ty_server/src/server/api/requests/signature_help.rs
    +++ b/crates/ty_server/src/server/api/requests/signature_help.rs
    @@ -11,7 +11,6 @@ use lsp_types::{
         Documentation, ParameterInformation, ParameterLabel, SignatureHelp, SignatureHelpParams,
         SignatureInformation, Url,
     };
    -use ruff_db::source::{line_index, source_text};
     use ty_ide::signature_help;
     use ty_project::ProjectDatabase;
     
    @@ -43,11 +42,10 @@ impl BackgroundDocumentRequestHandler for SignatureHelpRequestHandler {
                 return Ok(None);
             };
     
    -        let source = source_text(db, file);
    -        let line_index = line_index(db, file);
             let offset = params.text_document_position_params.position.to_text_size(
    -            &source,
    -            &line_index,
    +            db,
    +            file,
    +            snapshot.url(),
                 snapshot.encoding(),
             );
     
    diff --git a/crates/ty_server/src/server/api/requests/workspace_symbols.rs b/crates/ty_server/src/server/api/requests/workspace_symbols.rs
    index a964954546..252857e7e4 100644
    --- a/crates/ty_server/src/server/api/requests/workspace_symbols.rs
    +++ b/crates/ty_server/src/server/api/requests/workspace_symbols.rs
    @@ -8,8 +8,6 @@ use crate::server::api::traits::{
     };
     use crate::session::SessionSnapshot;
     use crate::session::client::Client;
    -use crate::system::file_to_url;
    -use ruff_db::source::{line_index, source_text};
     
     pub(crate) struct WorkspaceSymbolRequestHandler;
     
    @@ -41,23 +39,19 @@ impl BackgroundRequestHandler for WorkspaceSymbolRequestHandler {
                 for workspace_symbol_info in workspace_symbol_infos {
                     let WorkspaceSymbolInfo { symbol, file } = workspace_symbol_info;
     
    -                // Get file information for URL conversion
    -                let source = source_text(db, file);
    -                let line_index = line_index(db, file);
    -
    -                // Convert file to URL
    -                let Some(url) = file_to_url(db, file) else {
    -                    tracing::debug!("Failed to convert file to URL at {}", file.path(db));
    -                    continue;
    -                };
    -
                     // Get position encoding from session
                     let encoding = snapshot.position_encoding();
     
    -                let lsp_symbol =
    -                    convert_to_lsp_symbol_information(symbol, &url, &source, &line_index, encoding);
    +                let Some(symbol) = convert_to_lsp_symbol_information(db, file, symbol, encoding)
    +                else {
    +                    tracing::debug!(
    +                        "Failed to convert symbol '{}' to LSP symbol information",
    +                        file.path(db)
    +                    );
    +                    continue;
    +                };
     
    -                all_symbols.push(lsp_symbol);
    +                all_symbols.push(symbol);
                 }
             }
     
    diff --git a/crates/ty_server/src/server/api/semantic_tokens.rs b/crates/ty_server/src/server/api/semantic_tokens.rs
    index b168ef7877..ee9808b791 100644
    --- a/crates/ty_server/src/server/api/semantic_tokens.rs
    +++ b/crates/ty_server/src/server/api/semantic_tokens.rs
    @@ -1,5 +1,5 @@
     use lsp_types::SemanticToken;
    -use ruff_db::source::{line_index, source_text};
    +use ruff_db::source::source_text;
     use ruff_text_size::{Ranged, TextRange};
     use ty_ide::semantic_tokens;
     use ty_project::ProjectDatabase;
    @@ -16,7 +16,6 @@ pub(crate) fn generate_semantic_tokens(
         multiline_token_support: bool,
     ) -> Vec {
         let source = source_text(db, file);
    -    let line_index = line_index(db, file);
         let semantic_token_data = semantic_tokens(db, file, range);
     
         // Convert semantic tokens to LSP format
    @@ -25,7 +24,10 @@ pub(crate) fn generate_semantic_tokens(
         let mut prev_start = 0u32;
     
         for token in &*semantic_token_data {
    -        let lsp_range = token.range().to_lsp_range(&source, &line_index, encoding);
    +        let lsp_range = token
    +            .range()
    +            .as_lsp_range(db, file, encoding)
    +            .to_local_range();
             let line = lsp_range.start.line;
             let character = lsp_range.start.character;
     
    diff --git a/crates/ty_server/src/server/api/symbols.rs b/crates/ty_server/src/server/api/symbols.rs
    index 396f236e8d..fc6c1bc18c 100644
    --- a/crates/ty_server/src/server/api/symbols.rs
    +++ b/crates/ty_server/src/server/api/symbols.rs
    @@ -1,9 +1,9 @@
     //! Utility functions common to language server request handlers
     //! that return symbol information.
     
    -use lsp_types::{SymbolInformation, SymbolKind, Url};
    -use ruff_source_file::LineIndex;
    +use lsp_types::{SymbolInformation, SymbolKind};
     use ty_ide::SymbolInfo;
    +use ty_project::Db;
     
     use crate::document::{PositionEncoding, ToRangeExt};
     
    @@ -27,24 +27,25 @@ pub(crate) fn convert_symbol_kind(kind: ty_ide::SymbolKind) -> SymbolKind {
     
     /// Convert a `ty_ide` `SymbolInfo` to LSP `SymbolInformation`
     pub(crate) fn convert_to_lsp_symbol_information(
    +    db: &dyn Db,
    +    file: ruff_db::files::File,
         symbol: SymbolInfo<'_>,
    -    uri: &Url,
    -    source: &str,
    -    line_index: &LineIndex,
         encoding: PositionEncoding,
    -) -> SymbolInformation {
    +) -> Option {
         let symbol_kind = convert_symbol_kind(symbol.kind);
     
    -    SymbolInformation {
    +    let location = symbol
    +        .full_range
    +        .as_lsp_range(db, file, encoding)
    +        .to_location()?;
    +
    +    Some(SymbolInformation {
             name: symbol.name.into_owned(),
             kind: symbol_kind,
             tags: None,
             #[allow(deprecated)]
             deprecated: None,
    -        location: lsp_types::Location {
    -            uri: uri.clone(),
    -            range: symbol.full_range.to_lsp_range(source, line_index, encoding),
    -        },
    +        location,
             container_name: None,
    -    }
    +    })
     }
    diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs
    index c5daec77e3..9cc3553342 100644
    --- a/crates/ty_server/src/session.rs
    +++ b/crates/ty_server/src/session.rs
    @@ -1028,6 +1028,11 @@ impl DocumentSnapshot {
             &self.document
         }
     
    +    /// Returns the URL of the document.
    +    pub(crate) fn url(&self) -> &lsp_types::Url {
    +        self.document.url()
    +    }
    +
         pub(crate) fn notebook(&self) -> Option<&NotebookDocument> {
             self.notebook.as_deref()
         }
    
    From 7569b09bdd8dfa4587e44cfbed63fa71ca361261 Mon Sep 17 00:00:00 2001
    From: Micha Reiser 
    Date: Wed, 5 Nov 2025 14:40:07 +0100
    Subject: [PATCH 178/188] [ty] Add `ty_server::Db` trait (#21241)
    
    ---
     crates/ty/src/lib.rs                          |  4 +--
     crates/ty_project/src/lib.rs                  |  8 ++---
     crates/ty_server/src/db.rs                    | 33 +++++++++++++++++++
     crates/ty_server/src/document/location.rs     |  7 ++--
     crates/ty_server/src/document/range.rs        |  2 +-
     crates/ty_server/src/lib.rs                   |  2 ++
     .../ty_server/src/server/api/diagnostics.rs   |  3 +-
     .../notifications/did_change_watched_files.rs |  2 +-
     .../server/api/requests/document_symbols.rs   |  3 +-
     .../server/api/requests/execute_command.rs    |  2 +-
     .../api/requests/workspace_diagnostic.rs      | 13 +++++---
     crates/ty_server/src/server/api/symbols.rs    |  2 +-
     crates/ty_server/src/system.rs                | 25 +++++++-------
     13 files changed, 76 insertions(+), 30 deletions(-)
     create mode 100644 crates/ty_server/src/db.rs
    
    diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs
    index 2b28329f3f..9c667b0c82 100644
    --- a/crates/ty/src/lib.rs
    +++ b/crates/ty/src/lib.rs
    @@ -450,12 +450,12 @@ impl ty_project::ProgressReporter for IndicatifReporter {
             self.bar.set_draw_target(self.printer.progress_target());
         }
     
    -    fn report_checked_file(&self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]) {
    +    fn report_checked_file(&self, db: &ProjectDatabase, file: File, diagnostics: &[Diagnostic]) {
             self.collector.report_checked_file(db, file, diagnostics);
             self.bar.inc(1);
         }
     
    -    fn report_diagnostics(&mut self, db: &dyn Db, diagnostics: Vec) {
    +    fn report_diagnostics(&mut self, db: &ProjectDatabase, diagnostics: Vec) {
             self.collector.report_diagnostics(db, diagnostics);
         }
     }
    diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs
    index 4c7688d47f..fe034b0a07 100644
    --- a/crates/ty_project/src/lib.rs
    +++ b/crates/ty_project/src/lib.rs
    @@ -124,12 +124,12 @@ pub trait ProgressReporter: Send + Sync {
         fn set_files(&mut self, files: usize);
     
         /// Report the completion of checking a given file along with its diagnostics.
    -    fn report_checked_file(&self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]);
    +    fn report_checked_file(&self, db: &ProjectDatabase, file: File, diagnostics: &[Diagnostic]);
     
         /// Reports settings or IO related diagnostics. The diagnostics
         /// can belong to different files or no file at all.
         /// But it's never a file for which [`Self::report_checked_file`] gets called.
    -    fn report_diagnostics(&mut self, db: &dyn Db, diagnostics: Vec);
    +    fn report_diagnostics(&mut self, db: &ProjectDatabase, diagnostics: Vec);
     }
     
     /// Reporter that collects all diagnostics into a `Vec`.
    @@ -149,7 +149,7 @@ impl CollectReporter {
     
     impl ProgressReporter for CollectReporter {
         fn set_files(&mut self, _files: usize) {}
    -    fn report_checked_file(&self, _db: &dyn Db, _file: File, diagnostics: &[Diagnostic]) {
    +    fn report_checked_file(&self, _db: &ProjectDatabase, _file: File, diagnostics: &[Diagnostic]) {
             if diagnostics.is_empty() {
                 return;
             }
    @@ -160,7 +160,7 @@ impl ProgressReporter for CollectReporter {
                 .extend(diagnostics.iter().map(Clone::clone));
         }
     
    -    fn report_diagnostics(&mut self, _db: &dyn Db, diagnostics: Vec) {
    +    fn report_diagnostics(&mut self, _db: &ProjectDatabase, diagnostics: Vec) {
             self.0.get_mut().unwrap().extend(diagnostics);
         }
     }
    diff --git a/crates/ty_server/src/db.rs b/crates/ty_server/src/db.rs
    new file mode 100644
    index 0000000000..9ddc746cf1
    --- /dev/null
    +++ b/crates/ty_server/src/db.rs
    @@ -0,0 +1,33 @@
    +use crate::NotebookDocument;
    +use crate::session::index::Document;
    +use crate::system::LSPSystem;
    +use ruff_db::Db as _;
    +use ruff_db::files::{File, FilePath};
    +use ty_project::{Db as ProjectDb, ProjectDatabase};
    +
    +#[salsa::db]
    +pub(crate) trait Db: ProjectDb {
    +    /// Returns the LSP [`Document`] corresponding to `File` or
    +    /// `None` if the file isn't open in the editor.
    +    fn document(&self, file: File) -> Option<&Document>;
    +
    +    /// Returns the LSP [`NotebookDocument`] corresponding to `File` or
    +    /// `None` if the file isn't open in the editor or if it isn't a notebook.
    +    fn notebook_document(&self, file: File) -> Option<&NotebookDocument> {
    +        self.document(file)?.as_notebook()
    +    }
    +}
    +
    +#[salsa::db]
    +impl Db for ProjectDatabase {
    +    fn document(&self, file: File) -> Option<&Document> {
    +        self.system()
    +            .as_any()
    +            .downcast_ref::()
    +            .and_then(|system| match file.path(self) {
    +                FilePath::System(path) => system.system_path_to_document(path),
    +                FilePath::SystemVirtual(path) => system.system_virtual_path_to_document(path),
    +                FilePath::Vendored(_) => None,
    +            })
    +    }
    +}
    diff --git a/crates/ty_server/src/document/location.rs b/crates/ty_server/src/document/location.rs
    index f02dc20d98..91a064acd3 100644
    --- a/crates/ty_server/src/document/location.rs
    +++ b/crates/ty_server/src/document/location.rs
    @@ -1,9 +1,10 @@
    -use crate::PositionEncoding;
    -use crate::document::{FileRangeExt, ToRangeExt};
     use lsp_types::Location;
     use ruff_db::files::FileRange;
     use ty_ide::{NavigationTarget, ReferenceTarget};
    -use ty_python_semantic::Db;
    +
    +use crate::Db;
    +use crate::PositionEncoding;
    +use crate::document::{FileRangeExt, ToRangeExt};
     
     pub(crate) trait ToLink {
         fn to_location(&self, db: &dyn Db, encoding: PositionEncoding) -> Option;
    diff --git a/crates/ty_server/src/document/range.rs b/crates/ty_server/src/document/range.rs
    index 1e7f381ae5..6d3d3eb0d4 100644
    --- a/crates/ty_server/src/document/range.rs
    +++ b/crates/ty_server/src/document/range.rs
    @@ -1,6 +1,6 @@
     use super::PositionEncoding;
    +use crate::Db;
     use crate::system::file_to_url;
    -use ty_python_semantic::Db;
     
     use lsp_types as types;
     use lsp_types::{Location, Position, Url};
    diff --git a/crates/ty_server/src/lib.rs b/crates/ty_server/src/lib.rs
    index 374c8421cf..122f50d277 100644
    --- a/crates/ty_server/src/lib.rs
    +++ b/crates/ty_server/src/lib.rs
    @@ -4,6 +4,7 @@ use anyhow::Context;
     use lsp_server::Connection;
     use ruff_db::system::{OsSystem, SystemPathBuf};
     
    +use crate::db::Db;
     pub use crate::logging::{LogLevel, init_logging};
     pub use crate::server::{PartialWorkspaceProgress, PartialWorkspaceProgressParams, Server};
     pub use crate::session::{ClientOptions, DiagnosticMode};
    @@ -11,6 +12,7 @@ pub use document::{NotebookDocument, PositionEncoding, TextDocument};
     pub(crate) use session::Session;
     
     mod capabilities;
    +mod db;
     mod document;
     mod logging;
     mod server;
    diff --git a/crates/ty_server/src/server/api/diagnostics.rs b/crates/ty_server/src/server/api/diagnostics.rs
    index adbb17dcdf..54a0e79a2e 100644
    --- a/crates/ty_server/src/server/api/diagnostics.rs
    +++ b/crates/ty_server/src/server/api/diagnostics.rs
    @@ -10,8 +10,9 @@ use rustc_hash::FxHashMap;
     use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic};
     use ruff_db::files::FileRange;
     use ruff_db::system::SystemPathBuf;
    -use ty_project::{Db, ProjectDatabase};
    +use ty_project::{Db as _, ProjectDatabase};
     
    +use crate::Db;
     use crate::document::{FileRangeExt, ToRangeExt};
     use crate::session::DocumentSnapshot;
     use crate::session::client::Client;
    diff --git a/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs b/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs
    index ce55100dee..2d9c308f36 100644
    --- a/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs
    +++ b/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs
    @@ -8,7 +8,7 @@ use crate::system::AnySystemPath;
     use lsp_types as types;
     use lsp_types::{FileChangeType, notification as notif};
     use rustc_hash::FxHashMap;
    -use ty_project::Db;
    +use ty_project::Db as _;
     use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind};
     
     pub(crate) struct DidChangeWatchedFiles;
    diff --git a/crates/ty_server/src/server/api/requests/document_symbols.rs b/crates/ty_server/src/server/api/requests/document_symbols.rs
    index 980e0850ef..95edd391f4 100644
    --- a/crates/ty_server/src/server/api/requests/document_symbols.rs
    +++ b/crates/ty_server/src/server/api/requests/document_symbols.rs
    @@ -4,8 +4,9 @@ use lsp_types::request::DocumentSymbolRequest;
     use lsp_types::{DocumentSymbol, DocumentSymbolParams, SymbolInformation, Url};
     use ruff_db::files::File;
     use ty_ide::{HierarchicalSymbols, SymbolId, SymbolInfo, document_symbols};
    -use ty_project::{Db, ProjectDatabase};
    +use ty_project::ProjectDatabase;
     
    +use crate::Db;
     use crate::document::{PositionEncoding, ToRangeExt};
     use crate::server::api::symbols::{convert_symbol_kind, convert_to_lsp_symbol_information};
     use crate::server::api::traits::{
    diff --git a/crates/ty_server/src/server/api/requests/execute_command.rs b/crates/ty_server/src/server/api/requests/execute_command.rs
    index a51ece8598..8c0c0f9076 100644
    --- a/crates/ty_server/src/server/api/requests/execute_command.rs
    +++ b/crates/ty_server/src/server/api/requests/execute_command.rs
    @@ -9,7 +9,7 @@ use lsp_server::ErrorCode;
     use lsp_types::{self as types, request as req};
     use std::fmt::Write;
     use std::str::FromStr;
    -use ty_project::Db;
    +use ty_project::Db as _;
     
     pub(crate) struct ExecuteCommand;
     
    diff --git a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs
    index 2d37436116..87c7e4c77c 100644
    --- a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs
    +++ b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs
    @@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize};
     use std::collections::BTreeMap;
     use std::sync::Mutex;
     use std::time::{Duration, Instant};
    -use ty_project::{Db, ProgressReporter};
    +use ty_project::{ProgressReporter, ProjectDatabase};
     
     /// Handler for [Workspace diagnostics](workspace-diagnostics)
     ///
    @@ -230,7 +230,7 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> {
             state.report_progress(&self.work_done);
         }
     
    -    fn report_checked_file(&self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]) {
    +    fn report_checked_file(&self, db: &ProjectDatabase, file: File, diagnostics: &[Diagnostic]) {
             // Another thread might have panicked at this point because of a salsa cancellation which
             // poisoned the result. If the response is poisoned, just don't report and wait for our thread
             // to unwind with a salsa cancellation next.
    @@ -260,7 +260,7 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> {
             state.response.maybe_flush();
         }
     
    -    fn report_diagnostics(&mut self, db: &dyn Db, diagnostics: Vec) {
    +    fn report_diagnostics(&mut self, db: &ProjectDatabase, diagnostics: Vec) {
             let mut by_file: BTreeMap> = BTreeMap::new();
     
             for diagnostic in diagnostics {
    @@ -358,7 +358,12 @@ impl<'a> ResponseWriter<'a> {
             }
         }
     
    -    fn write_diagnostics_for_file(&mut self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]) {
    +    fn write_diagnostics_for_file(
    +        &mut self,
    +        db: &ProjectDatabase,
    +        file: File,
    +        diagnostics: &[Diagnostic],
    +    ) {
             let Some(url) = file_to_url(db, file) else {
                 tracing::debug!("Failed to convert file path to URL at {}", file.path(db));
                 return;
    diff --git a/crates/ty_server/src/server/api/symbols.rs b/crates/ty_server/src/server/api/symbols.rs
    index fc6c1bc18c..e823e32a98 100644
    --- a/crates/ty_server/src/server/api/symbols.rs
    +++ b/crates/ty_server/src/server/api/symbols.rs
    @@ -3,8 +3,8 @@
     
     use lsp_types::{SymbolInformation, SymbolKind};
     use ty_ide::SymbolInfo;
    -use ty_project::Db;
     
    +use crate::Db;
     use crate::document::{PositionEncoding, ToRangeExt};
     
     /// Convert `ty_ide` `SymbolKind` to LSP `SymbolKind`
    diff --git a/crates/ty_server/src/system.rs b/crates/ty_server/src/system.rs
    index 17b9bcbde6..ce93d9636b 100644
    --- a/crates/ty_server/src/system.rs
    +++ b/crates/ty_server/src/system.rs
    @@ -4,6 +4,7 @@ use std::fmt::Display;
     use std::panic::RefUnwindSafe;
     use std::sync::Arc;
     
    +use crate::Db;
     use crate::document::DocumentKey;
     use crate::session::index::{Document, Index};
     use lsp_types::Url;
    @@ -16,7 +17,6 @@ use ruff_db::system::{
     };
     use ruff_notebook::{Notebook, NotebookError};
     use ty_ide::cached_vendored_path;
    -use ty_python_semantic::Db;
     
     /// Returns a [`Url`] for the given [`File`].
     pub(crate) fn file_to_url(db: &dyn Db, file: File) -> Option {
    @@ -112,25 +112,28 @@ impl LSPSystem {
             self.index.as_ref().unwrap()
         }
     
    -    fn make_document_ref(&self, path: AnySystemPath) -> Option<&Document> {
    +    fn document(&self, path: AnySystemPath) -> Option<&Document> {
             let index = self.index();
             index.document(&DocumentKey::from(path)).ok()
         }
     
    -    fn system_path_to_document_ref(&self, path: &SystemPath) -> Option<&Document> {
    +    pub(crate) fn system_path_to_document(&self, path: &SystemPath) -> Option<&Document> {
             let any_path = AnySystemPath::System(path.to_path_buf());
    -        self.make_document_ref(any_path)
    +        self.document(any_path)
         }
     
    -    fn system_virtual_path_to_document_ref(&self, path: &SystemVirtualPath) -> Option<&Document> {
    +    pub(crate) fn system_virtual_path_to_document(
    +        &self,
    +        path: &SystemVirtualPath,
    +    ) -> Option<&Document> {
             let any_path = AnySystemPath::SystemVirtual(path.to_path_buf());
    -        self.make_document_ref(any_path)
    +        self.document(any_path)
         }
     }
     
     impl System for LSPSystem {
         fn path_metadata(&self, path: &SystemPath) -> Result {
    -        let document = self.system_path_to_document_ref(path);
    +        let document = self.system_path_to_document(path);
     
             if let Some(document) = document {
                 Ok(Metadata::new(
    @@ -152,7 +155,7 @@ impl System for LSPSystem {
         }
     
         fn read_to_string(&self, path: &SystemPath) -> Result {
    -        let document = self.system_path_to_document_ref(path);
    +        let document = self.system_path_to_document(path);
     
             match document {
                 Some(Document::Text(document)) => Ok(document.contents().to_string()),
    @@ -161,7 +164,7 @@ impl System for LSPSystem {
         }
     
         fn read_to_notebook(&self, path: &SystemPath) -> std::result::Result {
    -        let document = self.system_path_to_document_ref(path);
    +        let document = self.system_path_to_document(path);
     
             match document {
                 Some(Document::Text(document)) => Notebook::from_source_code(document.contents()),
    @@ -172,7 +175,7 @@ impl System for LSPSystem {
     
         fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result {
             let document = self
    -            .system_virtual_path_to_document_ref(path)
    +            .system_virtual_path_to_document(path)
                 .ok_or_else(|| virtual_path_not_found(path))?;
     
             if let Document::Text(document) = &document {
    @@ -187,7 +190,7 @@ impl System for LSPSystem {
             path: &SystemVirtualPath,
         ) -> std::result::Result {
             let document = self
    -            .system_virtual_path_to_document_ref(path)
    +            .system_virtual_path_to_document(path)
                 .ok_or_else(|| virtual_path_not_found(path))?;
     
             match document {
    
    From 5c69e00d1c73a2a1cb9e2da1a1f0b76d5ccf8548 Mon Sep 17 00:00:00 2001
    From: Ibraheem Ahmed 
    Date: Wed, 5 Nov 2025 10:03:19 -0500
    Subject: [PATCH 179/188] [ty] Simplify unions containing multiple type
     variables during inference (#21275)
    
    ## Summary
    
    Splitting this one out from https://github.com/astral-sh/ruff/pull/21210. This is also something that should be made obselete by the new constraint solver, but is easy enough to fix now.
    ---
     .../resources/mdtest/generics/pep695/functions.md      | 10 ++++++++++
     crates/ty_python_semantic/src/types/generics.rs        |  6 ++++--
     2 files changed, 14 insertions(+), 2 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 a5e62f6866..5db84cfd5a 100644
    --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md
    +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md
    @@ -474,6 +474,16 @@ def g(x: str):
         f(prefix=x, suffix=".tar.gz")
     ```
     
    +If the type variable is present multiple times in the union, we choose the correct union element to
    +infer against based on the argument type:
    +
    +```py
    +def h[T](x: list[T] | dict[T, T]) -> T | None: ...
    +def _(x: list[int], y: dict[int, int]):
    +    reveal_type(h(x))  # revealed: int | None
    +    reveal_type(h(y))  # revealed: int | None
    +```
    +
     ## Nested functions see typevars bound in outer function
     
     ```py
    diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs
    index 444c5badd6..992e664401 100644
    --- a/crates/ty_python_semantic/src/types/generics.rs
    +++ b/crates/ty_python_semantic/src/types/generics.rs
    @@ -1397,11 +1397,13 @@ impl<'db> SpecializationBuilder<'db> {
                 return Ok(());
             }
     
    -        // Remove the union elements that are not related to `formal`.
    +        // Remove the union elements from `actual` that are not related to `formal`, and vice
    +        // versa.
             //
             // For example, if `formal` is `list[T]` and `actual` is `list[int] | None`, we want to specialize `T`
    -        // to `int`.
    +        // to `int`, and so ignore the `None`.
             let actual = actual.filter_disjoint_elements(self.db, formal, self.inferable);
    +        let formal = formal.filter_disjoint_elements(self.db, actual, self.inferable);
     
             match (formal, actual) {
                 // TODO: We haven't implemented a full unification solver yet. If typevars appear in
    
    From cef6600cf3a7a22db214c6cb5d2393ede4209c37 Mon Sep 17 00:00:00 2001
    From: chiri 
    Date: Wed, 5 Nov 2025 20:07:33 +0300
    Subject: [PATCH 180/188] [`ruff`] Fix false positives on starred arguments
     (`RUF057`) (#21256)
    
    ## Summary
    
    Fixes https://github.com/astral-sh/ruff/issues/21209
    
    ## Test Plan
    
    `cargo nextest run ruf057`
    ---
     .../ruff_linter/resources/test/fixtures/ruff/RUF057.py   | 4 ++++
     .../src/rules/ruff/rules/unnecessary_round.rs            | 9 +++++++++
     ...uff_linter__rules__ruff__tests__RUF057_RUF057.py.snap | 5 +++++
     3 files changed, 18 insertions(+)
    
    diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF057.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF057.py
    index 2db91ac322..bb43b6d1d4 100644
    --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF057.py
    +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF057.py
    @@ -81,3 +81,7 @@ round(# a comment
     round(
         17 # a comment
     )
    +
    +# See: https://github.com/astral-sh/ruff/issues/21209
    +print(round(125, **{"ndigits": -2}))
    +print(round(125, *[-2]))
    \ No newline at end of file
    diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs
    index c7fe4687e8..e2ab51e1db 100644
    --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs
    +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs
    @@ -143,6 +143,15 @@ pub(super) fn rounded_and_ndigits<'a>(
             return None;
         }
     
    +    // *args
    +    if arguments.args.iter().any(Expr::is_starred_expr) {
    +        return None;
    +    }
    +    // **kwargs
    +    if arguments.keywords.iter().any(|kw| kw.arg.is_none()) {
    +        return None;
    +    }
    +
         let rounded = arguments.find_argument_value("number", 0)?;
         let ndigits = arguments.find_argument_value("ndigits", 1);
     
    diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF057_RUF057.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF057_RUF057.py.snap
    index abace6c8b4..8c536b67a8 100644
    --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF057_RUF057.py.snap
    +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF057_RUF057.py.snap
    @@ -253,6 +253,8 @@ RUF057 [*] Value being rounded is already an integer
     82 | |     17 # a comment
     83 | | )
        | |_^
    +84 |
    +85 |   # See: https://github.com/astral-sh/ruff/issues/21209
        |
     help: Remove unnecessary `round` call
     78 | round(# a comment
    @@ -262,4 +264,7 @@ help: Remove unnecessary `round` call
        -     17 # a comment
        - )
     81 + 17
    +82 | 
    +83 | # See: https://github.com/astral-sh/ruff/issues/21209
    +84 | print(round(125, **{"ndigits": -2}))
     note: This is an unsafe fix and may change runtime behavior
    
    From eda85f3c646ddb9f3dddf13315d653f51d187f64 Mon Sep 17 00:00:00 2001
    From: Douglas Creager 
    Date: Wed, 5 Nov 2025 12:31:53 -0500
    Subject: [PATCH 181/188] [ty] Constraining a typevar with itself (possibly via
     union or intersection) (#21273)
    
    This PR carries over some of the `has_relation_to` logic for comparing a
    typevar with itself. A typevar will specialize to the same type if it's
    mentioned multiple times, so it is always assignable to and a subtype of
    itself. (Note that typevars can only specialize to fully static types.)
    This is also true when the typevar appears in a union on the right-hand
    side, or in an intersection on the left-hand side. Similarly, a typevar
    is always disjoint from its negation, so when a negated typevar appears
    on the left-hand side, the constraint set is never satisfiable.
    
    (Eventually this will allow us to remove the corresponding clauses from
    `has_relation_to`, but that can't happen until more of #20093 lands.)
    ---
     .../mdtest/type_properties/constraints.md     | 169 +++++++++++++++++-
     crates/ty_python_semantic/src/types.rs        |  34 ++++
     .../ty_python_semantic/src/types/call/bind.rs |  20 +++
     .../src/types/constraints.rs                  |  55 +++++-
     .../ty_python_semantic/src/types/display.rs   |   3 +
     .../ty_extensions/ty_extensions.pyi           |   7 +
     6 files changed, 278 insertions(+), 10 deletions(-)
    
    diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md
    index 00a3e2837f..f677298c51 100644
    --- a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md
    +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md
    @@ -258,6 +258,50 @@ def _[T]() -> None:
         reveal_type(ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Unrelated, T, object))
     ```
     
    +Expanding on this, when intersecting two upper bounds constraints (`(T ≤ Base) ∧ (T ≤ Other)`), we
    +intersect the upper bounds. Any type that satisfies both `T ≤ Base` and `T ≤ Other` must necessarily
    +satisfy their intersection `T ≤ Base & Other`, and vice versa.
    +
    +```py
    +from typing import Never
    +from ty_extensions import Intersection, static_assert
    +
    +# This is not final, so it's possible for a subclass to inherit from both Base and Other.
    +class Other: ...
    +
    +def upper_bounds[T]():
    +    intersection_type = ConstraintSet.range(Never, T, Intersection[Base, Other])
    +    # revealed: ty_extensions.ConstraintSet[(T@upper_bounds ≤ Base & Other)]
    +    reveal_type(intersection_type)
    +
    +    intersection_constraint = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, T, Other)
    +    # revealed: ty_extensions.ConstraintSet[(T@upper_bounds ≤ Base & Other)]
    +    reveal_type(intersection_constraint)
    +
    +    # The two constraint sets are equivalent; each satisfies the other.
    +    static_assert(intersection_type.satisfies(intersection_constraint))
    +    static_assert(intersection_constraint.satisfies(intersection_type))
    +```
    +
    +For an intersection of two lower bounds constraints (`(Base ≤ T) ∧ (Other ≤ T)`), we union the lower
    +bounds. Any type that satisfies both `Base ≤ T` and `Other ≤ T` must necessarily satisfy their union
    +`Base | Other ≤ T`, and vice versa.
    +
    +```py
    +def lower_bounds[T]():
    +    union_type = ConstraintSet.range(Base | Other, T, object)
    +    # revealed: ty_extensions.ConstraintSet[(Base | Other ≤ T@lower_bounds)]
    +    reveal_type(union_type)
    +
    +    intersection_constraint = ConstraintSet.range(Base, T, object) & ConstraintSet.range(Other, T, object)
    +    # revealed: ty_extensions.ConstraintSet[(Base | Other ≤ T@lower_bounds)]
    +    reveal_type(intersection_constraint)
    +
    +    # The two constraint sets are equivalent; each satisfies the other.
    +    static_assert(union_type.satisfies(intersection_constraint))
    +    static_assert(intersection_constraint.satisfies(union_type))
    +```
    +
     ### Intersection of a range and a negated range
     
     The bounds of the range constraint provide a range of types that should be included; the bounds of
    @@ -335,7 +379,7 @@ def _[T]() -> None:
         reveal_type(~ConstraintSet.range(Sub, T, Super) & ~ConstraintSet.range(Sub, T, Super))
     ```
     
    -Otherwise, the union cannot be simplified.
    +Otherwise, the intersection cannot be simplified.
     
     ```py
     def _[T]() -> None:
    @@ -350,13 +394,14 @@ def _[T]() -> None:
     In particular, the following does not simplify, even though it seems like it could simplify to
     `¬(SubSub ≤ T@_ ≤ Super)`. The issue is that there are types that are within the bounds of
     `SubSub ≤ T@_ ≤ Super`, but which are not comparable to `Base` or `Sub`, and which therefore should
    -be included in the union. An example would be the type that contains all instances of `Super`,
    -`Base`, and `SubSub` (but _not_ including instances of `Sub`). (We don't have a way to spell that
    -type at the moment, but it is a valid type.) That type is not in `SubSub ≤ T ≤ Base`, since it
    -includes `Super`, which is outside the range. It's also not in `Sub ≤ T ≤ Super`, because it does
    -not include `Sub`. That means it should be in the union. (Remember that for negated range
    -constraints, the lower and upper bounds define the "hole" of types that are _not_ allowed.) Since
    -that type _is_ in `SubSub ≤ T ≤ Super`, it is not correct to simplify the union in this way.
    +be included in the intersection. An example would be the type that contains all instances of
    +`Super`, `Base`, and `SubSub` (but _not_ including instances of `Sub`). (We don't have a way to
    +spell that type at the moment, but it is a valid type.) That type is not in `SubSub ≤ T ≤ Base`,
    +since it includes `Super`, which is outside the range. It's also not in `Sub ≤ T ≤ Super`, because
    +it does not include `Sub`. That means it should be in the intersection. (Remember that for negated
    +range constraints, the lower and upper bounds define the "hole" of types that are _not_ allowed.)
    +Since that type _is_ in `SubSub ≤ T ≤ Super`, it is not correct to simplify the intersection in this
    +way.
     
     ```py
     def _[T]() -> None:
    @@ -441,6 +486,65 @@ def _[T]() -> None:
         reveal_type(ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super))
     ```
     
    +The union of two upper bound constraints (`(T ≤ Base) ∨ (T ≤ Other)`) is different than the single
    +range constraint involving the corresponding union type (`T ≤ Base | Other`). There are types (such
    +as `T = Base | Other`) that satisfy the union type, but not the union constraint. But every type
    +that satisfies the union constraint satisfies the union type.
    +
    +```py
    +from typing import Never
    +from ty_extensions import static_assert
    +
    +# This is not final, so it's possible for a subclass to inherit from both Base and Other.
    +class Other: ...
    +
    +def union[T]():
    +    union_type = ConstraintSet.range(Never, T, Base | Other)
    +    # revealed: ty_extensions.ConstraintSet[(T@union ≤ Base | Other)]
    +    reveal_type(union_type)
    +
    +    union_constraint = ConstraintSet.range(Never, T, Base) | ConstraintSet.range(Never, T, Other)
    +    # revealed: ty_extensions.ConstraintSet[(T@union ≤ Base) ∨ (T@union ≤ Other)]
    +    reveal_type(union_constraint)
    +
    +    # (T = Base | Other) satisfies (T ≤ Base | Other) but not (T ≤ Base ∨ T ≤ Other)
    +    specialization = ConstraintSet.range(Base | Other, T, Base | Other)
    +    # revealed: ty_extensions.ConstraintSet[(T@union = Base | Other)]
    +    reveal_type(specialization)
    +    static_assert(specialization.satisfies(union_type))
    +    static_assert(not specialization.satisfies(union_constraint))
    +
    +    # Every specialization that satisfies (T ≤ Base ∨ T ≤ Other) also satisfies
    +    # (T ≤ Base | Other)
    +    static_assert(union_constraint.satisfies(union_type))
    +```
    +
    +These relationships are reversed for unions involving lower bounds. `T = Base` is an example that
    +satisfies the union constraint (`(Base ≤ T) ∨ (Other ≤ T)`) but not the union type
    +(`Base | Other ≤ T`). And every type that satisfies the union type satisfies the union constraint.
    +
    +```py
    +def union[T]():
    +    union_type = ConstraintSet.range(Base | Other, T, object)
    +    # revealed: ty_extensions.ConstraintSet[(Base | Other ≤ T@union)]
    +    reveal_type(union_type)
    +
    +    union_constraint = ConstraintSet.range(Base, T, object) | ConstraintSet.range(Other, T, object)
    +    # revealed: ty_extensions.ConstraintSet[(Base ≤ T@union) ∨ (Other ≤ T@union)]
    +    reveal_type(union_constraint)
    +
    +    # (T = Base) satisfies (Base ≤ T ∨ Other ≤ T) but not (Base | Other ≤ T)
    +    specialization = ConstraintSet.range(Base, T, Base)
    +    # revealed: ty_extensions.ConstraintSet[(T@union = Base)]
    +    reveal_type(specialization)
    +    static_assert(not specialization.satisfies(union_type))
    +    static_assert(specialization.satisfies(union_constraint))
    +
    +    # Every specialization that satisfies (Base | Other ≤ T) also satisfies
    +    # (Base ≤ T ∨ Other ≤ T)
    +    static_assert(union_type.satisfies(union_constraint))
    +```
    +
     ### Union of a range and a negated range
     
     The bounds of the range constraint provide a range of types that should be included; the bounds of
    @@ -729,3 +833,52 @@ def f[T]():
         # revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)]
         reveal_type(ConstraintSet.range(Never, T, int | str))
     ```
    +
    +### Constraints on the same typevar
    +
    +Any particular specialization maps each typevar to one type. That means it's not useful to constrain
    +a typevar with itself as an upper or lower bound. No matter what type the typevar is specialized to,
    +that type is always a subtype of itself. (Remember that typevars are only specialized to fully
    +static types.)
    +
    +```py
    +from typing import Never
    +from ty_extensions import ConstraintSet
    +
    +def same_typevar[T]():
    +    # revealed: ty_extensions.ConstraintSet[always]
    +    reveal_type(ConstraintSet.range(Never, T, T))
    +    # revealed: ty_extensions.ConstraintSet[always]
    +    reveal_type(ConstraintSet.range(T, T, object))
    +    # revealed: ty_extensions.ConstraintSet[always]
    +    reveal_type(ConstraintSet.range(T, T, T))
    +```
    +
    +This is also true when the typevar appears in a union in the upper bound, or in an intersection in
    +the lower bound. (Note that this lines up with how we simplify the intersection of two constraints,
    +as shown above.)
    +
    +```py
    +from ty_extensions import Intersection
    +
    +def same_typevar[T]():
    +    # revealed: ty_extensions.ConstraintSet[always]
    +    reveal_type(ConstraintSet.range(Never, T, T | None))
    +    # revealed: ty_extensions.ConstraintSet[always]
    +    reveal_type(ConstraintSet.range(Intersection[T, None], T, object))
    +    # revealed: ty_extensions.ConstraintSet[always]
    +    reveal_type(ConstraintSet.range(Intersection[T, None], T, T | None))
    +```
    +
    +Similarly, if the lower bound is an intersection containing the _negation_ of the typevar, then the
    +constraint set can never be satisfied, since every type is disjoint with its negation.
    +
    +```py
    +from ty_extensions import Not
    +
    +def same_typevar[T]():
    +    # revealed: ty_extensions.ConstraintSet[never]
    +    reveal_type(ConstraintSet.range(Intersection[Not[T], None], T, object))
    +    # revealed: ty_extensions.ConstraintSet[never]
    +    reveal_type(ConstraintSet.range(Not[T], T, object))
    +```
    diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
    index 131d9e7630..59e1ef4030 100644
    --- a/crates/ty_python_semantic/src/types.rs
    +++ b/crates/ty_python_semantic/src/types.rs
    @@ -4197,6 +4197,14 @@ impl<'db> Type<'db> {
                     ))
                     .into()
                 }
    +            Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked))
    +                if name == "satisfies" =>
    +            {
    +                Place::bound(Type::KnownBoundMethod(
    +                    KnownBoundMethodType::ConstraintSetSatisfies(tracked),
    +                ))
    +                .into()
    +            }
                 Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked))
                     if name == "satisfied_by_all_typevars" =>
                 {
    @@ -6973,6 +6981,7 @@ impl<'db> Type<'db> {
                     | KnownBoundMethodType::ConstraintSetAlways
                     | KnownBoundMethodType::ConstraintSetNever
                     | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
    +                | KnownBoundMethodType::ConstraintSetSatisfies(_)
                     | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_)
                 )
                 | Type::DataclassDecorator(_)
    @@ -7126,6 +7135,7 @@ impl<'db> Type<'db> {
                     | KnownBoundMethodType::ConstraintSetAlways
                     | KnownBoundMethodType::ConstraintSetNever
                     | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
    +                | KnownBoundMethodType::ConstraintSetSatisfies(_)
                     | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
                 )
                 | Type::DataclassDecorator(_)
    @@ -10470,6 +10480,7 @@ pub enum KnownBoundMethodType<'db> {
         ConstraintSetAlways,
         ConstraintSetNever,
         ConstraintSetImpliesSubtypeOf(TrackedConstraintSet<'db>),
    +    ConstraintSetSatisfies(TrackedConstraintSet<'db>),
         ConstraintSetSatisfiedByAllTypeVars(TrackedConstraintSet<'db>),
     }
     
    @@ -10499,6 +10510,7 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size
             | KnownBoundMethodType::ConstraintSetAlways
             | KnownBoundMethodType::ConstraintSetNever
             | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
    +        | KnownBoundMethodType::ConstraintSetSatisfies(_)
             | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {}
         }
     }
    @@ -10568,6 +10580,10 @@ impl<'db> KnownBoundMethodType<'db> {
                     KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
                     KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_),
                 )
    +            | (
    +                KnownBoundMethodType::ConstraintSetSatisfies(_),
    +                KnownBoundMethodType::ConstraintSetSatisfies(_),
    +            )
                 | (
                     KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
                     KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
    @@ -10584,6 +10600,7 @@ impl<'db> KnownBoundMethodType<'db> {
                     | KnownBoundMethodType::ConstraintSetAlways
                     | KnownBoundMethodType::ConstraintSetNever
                     | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
    +                | KnownBoundMethodType::ConstraintSetSatisfies(_)
                     | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
                     KnownBoundMethodType::FunctionTypeDunderGet(_)
                     | KnownBoundMethodType::FunctionTypeDunderCall(_)
    @@ -10595,6 +10612,7 @@ impl<'db> KnownBoundMethodType<'db> {
                     | KnownBoundMethodType::ConstraintSetAlways
                     | KnownBoundMethodType::ConstraintSetNever
                     | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
    +                | KnownBoundMethodType::ConstraintSetSatisfies(_)
                     | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
                 ) => ConstraintSet::from(false),
             }
    @@ -10649,6 +10667,10 @@ impl<'db> KnownBoundMethodType<'db> {
                     KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(left_constraints),
                     KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(right_constraints),
                 )
    +            | (
    +                KnownBoundMethodType::ConstraintSetSatisfies(left_constraints),
    +                KnownBoundMethodType::ConstraintSetSatisfies(right_constraints),
    +            )
                 | (
                     KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(left_constraints),
                     KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(right_constraints),
    @@ -10667,6 +10689,7 @@ impl<'db> KnownBoundMethodType<'db> {
                     | KnownBoundMethodType::ConstraintSetAlways
                     | KnownBoundMethodType::ConstraintSetNever
                     | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
    +                | KnownBoundMethodType::ConstraintSetSatisfies(_)
                     | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
                     KnownBoundMethodType::FunctionTypeDunderGet(_)
                     | KnownBoundMethodType::FunctionTypeDunderCall(_)
    @@ -10678,6 +10701,7 @@ impl<'db> KnownBoundMethodType<'db> {
                     | KnownBoundMethodType::ConstraintSetAlways
                     | KnownBoundMethodType::ConstraintSetNever
                     | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
    +                | KnownBoundMethodType::ConstraintSetSatisfies(_)
                     | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
                 ) => ConstraintSet::from(false),
             }
    @@ -10703,6 +10727,7 @@ impl<'db> KnownBoundMethodType<'db> {
                 | KnownBoundMethodType::ConstraintSetAlways
                 | KnownBoundMethodType::ConstraintSetNever
                 | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
    +            | KnownBoundMethodType::ConstraintSetSatisfies(_)
                 | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => self,
             }
         }
    @@ -10720,6 +10745,7 @@ impl<'db> KnownBoundMethodType<'db> {
                 | KnownBoundMethodType::ConstraintSetAlways
                 | KnownBoundMethodType::ConstraintSetNever
                 | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
    +            | KnownBoundMethodType::ConstraintSetSatisfies(_)
                 | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {
                     KnownClass::ConstraintSet
                 }
    @@ -10862,6 +10888,14 @@ impl<'db> KnownBoundMethodType<'db> {
                     )))
                 }
     
    +            KnownBoundMethodType::ConstraintSetSatisfies(_) => {
    +                Either::Right(std::iter::once(Signature::new(
    +                    Parameters::new([Parameter::positional_only(Some(Name::new_static("other")))
    +                        .with_annotated_type(KnownClass::ConstraintSet.to_instance(db))]),
    +                    Some(KnownClass::ConstraintSet.to_instance(db)),
    +                )))
    +            }
    +
                 KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {
                     Either::Right(std::iter::once(Signature::new(
                         Parameters::new([Parameter::keyword_only(Name::new_static("inferable"))
    diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs
    index d2739fa696..60868cfc3f 100644
    --- a/crates/ty_python_semantic/src/types/call/bind.rs
    +++ b/crates/ty_python_semantic/src/types/call/bind.rs
    @@ -1176,6 +1176,26 @@ impl<'db> Bindings<'db> {
                             ));
                         }
     
    +                    Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetSatisfies(
    +                        tracked,
    +                    )) => {
    +                        let [Some(other)] = overload.parameter_types() else {
    +                            continue;
    +                        };
    +                        let Type::KnownInstance(KnownInstanceType::ConstraintSet(other)) = other
    +                        else {
    +                            continue;
    +                        };
    +
    +                        let result = tracked
    +                            .constraints(db)
    +                            .implies(db, || other.constraints(db));
    +                        let tracked = TrackedConstraintSet::new(db, result);
    +                        overload.set_return_type(Type::KnownInstance(
    +                            KnownInstanceType::ConstraintSet(tracked),
    +                        ));
    +                    }
    +
                         Type::KnownBoundMethod(
                             KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(tracked),
                         ) => {
    diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs
    index ee66cd85f3..c1e5ac2697 100644
    --- a/crates/ty_python_semantic/src/types/constraints.rs
    +++ b/crates/ty_python_semantic/src/types/constraints.rs
    @@ -320,6 +320,11 @@ impl<'db> ConstraintSet<'db> {
             self
         }
     
    +    /// Returns a constraint set encoding that this constraint set implies another.
    +    pub(crate) fn implies(self, db: &'db dyn Db, other: impl FnOnce() -> Self) -> Self {
    +        self.negate(db).or(db, other)
    +    }
    +
         pub(crate) fn iff(self, db: &'db dyn Db, other: Self) -> Self {
             ConstraintSet {
                 node: self.node.iff(db, other.node),
    @@ -381,12 +386,58 @@ impl<'db> ConstrainedTypeVar<'db> {
         fn new_node(
             db: &'db dyn Db,
             typevar: BoundTypeVarInstance<'db>,
    -        lower: Type<'db>,
    -        upper: Type<'db>,
    +        mut lower: Type<'db>,
    +        mut upper: Type<'db>,
         ) -> Node<'db> {
             debug_assert_eq!(lower, lower.bottom_materialization(db));
             debug_assert_eq!(upper, upper.top_materialization(db));
     
    +        // Two identical typevars must always solve to the same type, so it is not useful to have
    +        // an upper or lower bound that is the typevar being constrained.
    +        match lower {
    +            Type::TypeVar(lower_bound_typevar)
    +                if typevar.is_same_typevar_as(db, lower_bound_typevar) =>
    +            {
    +                lower = Type::Never;
    +            }
    +            Type::Intersection(intersection)
    +                if intersection.positive(db).iter().any(|element| {
    +                    element.as_typevar().is_some_and(|element_bound_typevar| {
    +                        typevar.is_same_typevar_as(db, element_bound_typevar)
    +                    })
    +                }) =>
    +            {
    +                lower = Type::Never;
    +            }
    +            Type::Intersection(intersection)
    +                if intersection.negative(db).iter().any(|element| {
    +                    element.as_typevar().is_some_and(|element_bound_typevar| {
    +                        typevar.is_same_typevar_as(db, element_bound_typevar)
    +                    })
    +                }) =>
    +            {
    +                return Node::AlwaysFalse;
    +            }
    +            _ => {}
    +        }
    +        match upper {
    +            Type::TypeVar(upper_bound_typevar)
    +                if typevar.is_same_typevar_as(db, upper_bound_typevar) =>
    +            {
    +                upper = Type::object();
    +            }
    +            Type::Union(union)
    +                if union.elements(db).iter().any(|element| {
    +                    element.as_typevar().is_some_and(|element_bound_typevar| {
    +                        typevar.is_same_typevar_as(db, element_bound_typevar)
    +                    })
    +                }) =>
    +            {
    +                upper = Type::object();
    +            }
    +            _ => {}
    +        }
    +
             // 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_subtype_of(db, upper) {
    diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs
    index 8500c142e8..42e2373134 100644
    --- a/crates/ty_python_semantic/src/types/display.rs
    +++ b/crates/ty_python_semantic/src/types/display.rs
    @@ -535,6 +535,9 @@ impl Display for DisplayRepresentation<'_> {
                 Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)) => {
                     f.write_str("bound method `ConstraintSet.implies_subtype_of`")
                 }
    +            Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetSatisfies(_)) => {
    +                f.write_str("bound method `ConstraintSet.satisfies`")
    +            }
                 Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(
                     _,
                 )) => f.write_str("bound method `ConstraintSet.satisfied_by_all_typevars`"),
    diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi
    index d23554f0ae..744bd5af37 100644
    --- a/crates/ty_vendored/ty_extensions/ty_extensions.pyi
    +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi
    @@ -67,6 +67,13 @@ class ConstraintSet:
             .. _subtype: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
             """
     
    +    def satisfies(self, other: Self) -> Self:
    +        """
    +        Returns whether this constraint set satisfies another — that is, whether
    +        every specialization that satisfies this constraint set also satisfies
    +        `other`.
    +        """
    +
         def satisfied_by_all_typevars(
             self, *, inferable: tuple[Any, ...] | None = None
         ) -> bool:
    
    From cddc0fedc24d6ca90601d81bacfaab418fe50a97 Mon Sep 17 00:00:00 2001
    From: Bhuminjay Soni 
    Date: Thu, 6 Nov 2025 00:43:28 +0530
    Subject: [PATCH 182/188] [syntax-error]: no binding for nonlocal  PLE0117 as a
     semantic syntax error (#21032)
    
    
    
    ## Summary
    
    
    
    This PR ports PLE0117 as a semantic syntax error.
    
    ## Test Plan
    
    
    Tests previously written
    
    ---------
    
    Signed-off-by: 11happy 
    Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
    Co-authored-by: Brent Westbrook 
    Co-authored-by: Alex Waygood 
    ---
     .../src/checkers/ast/analyze/statement.rs     |  3 ---
     crates/ruff_linter/src/checkers/ast/mod.rs    | 13 ++++++++++-
     .../pylint/rules/nonlocal_without_binding.rs  | 19 ---------------
     .../ruff_python_parser/src/semantic_errors.rs | 23 ++++++++++++++++++-
     crates/ruff_python_parser/tests/fixtures.rs   |  4 ++++
     .../src/semantic_index/builder.rs             |  6 +++++
     6 files changed, 44 insertions(+), 24 deletions(-)
    
    diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs
    index 7c0037e10d..2e7523891b 100644
    --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs
    +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs
    @@ -43,9 +43,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
                         pycodestyle::rules::ambiguous_variable_name(checker, name, name.range());
                     }
                 }
    -            if checker.is_rule_enabled(Rule::NonlocalWithoutBinding) {
    -                pylint::rules::nonlocal_without_binding(checker, nonlocal);
    -            }
                 if checker.is_rule_enabled(Rule::NonlocalAndGlobal) {
                     pylint::rules::nonlocal_and_global(checker, nonlocal);
                 }
    diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs
    index 2280dc33cb..f3315b3b47 100644
    --- a/crates/ruff_linter/src/checkers/ast/mod.rs
    +++ b/crates/ruff_linter/src/checkers/ast/mod.rs
    @@ -73,7 +73,8 @@ use crate::rules::pyflakes::rules::{
         UndefinedLocalWithNestedImportStarUsage, YieldOutsideFunction,
     };
     use crate::rules::pylint::rules::{
    -    AwaitOutsideAsync, LoadBeforeGlobalDeclaration, YieldFromInAsyncFunction,
    +    AwaitOutsideAsync, LoadBeforeGlobalDeclaration, NonlocalWithoutBinding,
    +    YieldFromInAsyncFunction,
     };
     use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
     use crate::settings::rule_table::RuleTable;
    @@ -641,6 +642,10 @@ impl SemanticSyntaxContext for Checker<'_> {
             self.semantic.global(name)
         }
     
    +    fn has_nonlocal_binding(&self, name: &str) -> bool {
    +        self.semantic.nonlocal(name).is_some()
    +    }
    +
         fn report_semantic_error(&self, error: SemanticSyntaxError) {
             match error.kind {
                 SemanticSyntaxErrorKind::LateFutureImport => {
    @@ -717,6 +722,12 @@ impl SemanticSyntaxContext for Checker<'_> {
                         self.report_diagnostic(pyflakes::rules::ContinueOutsideLoop, error.range);
                     }
                 }
    +            SemanticSyntaxErrorKind::NonlocalWithoutBinding(name) => {
    +                // PLE0117
    +                if self.is_rule_enabled(Rule::NonlocalWithoutBinding) {
    +                    self.report_diagnostic(NonlocalWithoutBinding { name }, error.range);
    +                }
    +            }
                 SemanticSyntaxErrorKind::ReboundComprehensionVariable
                 | SemanticSyntaxErrorKind::DuplicateTypeParameter
                 | SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
    diff --git a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs
    index 90f5e0dde5..f71902cb32 100644
    --- a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs
    +++ b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs
    @@ -1,9 +1,6 @@
     use ruff_macros::{ViolationMetadata, derive_message_formats};
    -use ruff_python_ast as ast;
    -use ruff_text_size::Ranged;
     
     use crate::Violation;
    -use crate::checkers::ast::Checker;
     
     /// ## What it does
     /// Checks for `nonlocal` names without bindings.
    @@ -46,19 +43,3 @@ impl Violation for NonlocalWithoutBinding {
             format!("Nonlocal name `{name}` found without binding")
         }
     }
    -
    -/// PLE0117
    -pub(crate) fn nonlocal_without_binding(checker: &Checker, nonlocal: &ast::StmtNonlocal) {
    -    if !checker.semantic().scope_id.is_global() {
    -        for name in &nonlocal.names {
    -            if checker.semantic().nonlocal(name).is_none() {
    -                checker.report_diagnostic(
    -                    NonlocalWithoutBinding {
    -                        name: name.to_string(),
    -                    },
    -                    name.range(),
    -                );
    -            }
    -        }
    -    }
    -}
    diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs
    index f35029d4b9..2775aa9065 100644
    --- a/crates/ruff_python_parser/src/semantic_errors.rs
    +++ b/crates/ruff_python_parser/src/semantic_errors.rs
    @@ -219,7 +219,7 @@ impl SemanticSyntaxChecker {
                         AwaitOutsideAsyncFunctionKind::AsyncWith,
                     );
                 }
    -            Stmt::Nonlocal(ast::StmtNonlocal { range, .. }) => {
    +            Stmt::Nonlocal(ast::StmtNonlocal { names, range, .. }) => {
                     // test_ok nonlocal_declaration_at_module_level
                     // def _():
                     //     nonlocal x
    @@ -234,6 +234,18 @@ impl SemanticSyntaxChecker {
                             *range,
                         );
                     }
    +
    +                if !ctx.in_module_scope() {
    +                    for name in names {
    +                        if !ctx.has_nonlocal_binding(name) {
    +                            Self::add_error(
    +                                ctx,
    +                                SemanticSyntaxErrorKind::NonlocalWithoutBinding(name.to_string()),
    +                                name.range,
    +                            );
    +                        }
    +                    }
    +                }
                 }
                 Stmt::Break(ast::StmtBreak { range, .. }) => {
                     if !ctx.in_loop_context() {
    @@ -1154,6 +1166,9 @@ impl Display for SemanticSyntaxError {
                 SemanticSyntaxErrorKind::DifferentMatchPatternBindings => {
                     write!(f, "alternative patterns bind different names")
                 }
    +            SemanticSyntaxErrorKind::NonlocalWithoutBinding(name) => {
    +                write!(f, "no binding for nonlocal `{name}` found")
    +            }
             }
         }
     }
    @@ -1554,6 +1569,9 @@ pub enum SemanticSyntaxErrorKind {
         ///         ...
         /// ```
         DifferentMatchPatternBindings,
    +
    +    /// Represents a nonlocal statement for a name that has no binding in an enclosing scope.
    +    NonlocalWithoutBinding(String),
     }
     
     #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]
    @@ -2004,6 +2022,9 @@ pub trait SemanticSyntaxContext {
         /// Return the [`TextRange`] at which a name is declared as `global` in the current scope.
         fn global(&self, name: &str) -> Option;
     
    +    /// Returns `true` if `name` has a binding in an enclosing scope.
    +    fn has_nonlocal_binding(&self, name: &str) -> bool;
    +
         /// Returns `true` if the visitor is currently in an async context, i.e. an async function.
         fn in_async_context(&self) -> bool;
     
    diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs
    index c646fe525b..2de49e6d68 100644
    --- a/crates/ruff_python_parser/tests/fixtures.rs
    +++ b/crates/ruff_python_parser/tests/fixtures.rs
    @@ -527,6 +527,10 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> {
             None
         }
     
    +    fn has_nonlocal_binding(&self, _name: &str) -> bool {
    +        true
    +    }
    +
         fn in_async_context(&self) -> bool {
             if let Some(scope) = self.scopes.iter().next_back() {
                 match scope {
    diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs
    index 5645fed7d4..6affc11424 100644
    --- a/crates/ty_python_semantic/src/semantic_index/builder.rs
    +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs
    @@ -2712,6 +2712,12 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> {
             None
         }
     
    +    // We handle the one syntax error that relies on this method (`NonlocalWithoutBinding`) directly
    +    // in `TypeInferenceBuilder::infer_nonlocal_statement`, so this just returns `true`.
    +    fn has_nonlocal_binding(&self, _name: &str) -> bool {
    +        true
    +    }
    +
         fn in_async_context(&self) -> bool {
             for scope_info in self.scope_stack.iter().rev() {
                 let scope = &self.scopes[scope_info.file_scope_id];
    
    From 76127e5fb538ec7642af00a4dc68230ab52cf050 Mon Sep 17 00:00:00 2001
    From: Micha Reiser 
    Date: Wed, 5 Nov 2025 23:15:01 +0100
    Subject: [PATCH 183/188] [ty] Update salsa (#21281)
    
    ---
     Cargo.lock                                   |  6 ++---
     Cargo.toml                                   |  2 +-
     crates/ty_python_semantic/src/types/infer.rs | 28 +++++++++-----------
     fuzz/Cargo.toml                              |  2 +-
     4 files changed, 17 insertions(+), 21 deletions(-)
    
    diff --git a/Cargo.lock b/Cargo.lock
    index 2da7d9ff0e..db433c9c1f 100644
    --- a/Cargo.lock
    +++ b/Cargo.lock
    @@ -3586,7 +3586,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
     [[package]]
     name = "salsa"
     version = "0.24.0"
    -source = "git+https://github.com/salsa-rs/salsa.git?rev=664750a6e588ed23a0d2d9105a02cb5993c8e178#664750a6e588ed23a0d2d9105a02cb5993c8e178"
    +source = "git+https://github.com/salsa-rs/salsa.git?rev=05a9af7f554b64b8aadc2eeb6f2caf73d0408d09#05a9af7f554b64b8aadc2eeb6f2caf73d0408d09"
     dependencies = [
      "boxcar",
      "compact_str",
    @@ -3610,12 +3610,12 @@ dependencies = [
     [[package]]
     name = "salsa-macro-rules"
     version = "0.24.0"
    -source = "git+https://github.com/salsa-rs/salsa.git?rev=664750a6e588ed23a0d2d9105a02cb5993c8e178#664750a6e588ed23a0d2d9105a02cb5993c8e178"
    +source = "git+https://github.com/salsa-rs/salsa.git?rev=05a9af7f554b64b8aadc2eeb6f2caf73d0408d09#05a9af7f554b64b8aadc2eeb6f2caf73d0408d09"
     
     [[package]]
     name = "salsa-macros"
     version = "0.24.0"
    -source = "git+https://github.com/salsa-rs/salsa.git?rev=664750a6e588ed23a0d2d9105a02cb5993c8e178#664750a6e588ed23a0d2d9105a02cb5993c8e178"
    +source = "git+https://github.com/salsa-rs/salsa.git?rev=05a9af7f554b64b8aadc2eeb6f2caf73d0408d09#05a9af7f554b64b8aadc2eeb6f2caf73d0408d09"
     dependencies = [
      "proc-macro2",
      "quote",
    diff --git a/Cargo.toml b/Cargo.toml
    index b2122cea97..cd9e05d6b8 100644
    --- a/Cargo.toml
    +++ b/Cargo.toml
    @@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
     rustc-hash = { version = "2.0.0" }
     rustc-stable-hash = { version = "0.1.2" }
     # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
    -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "664750a6e588ed23a0d2d9105a02cb5993c8e178", default-features = false, features = [
    +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "05a9af7f554b64b8aadc2eeb6f2caf73d0408d09", default-features = false, features = [
         "compact_str",
         "macros",
         "salsa_unstable",
    diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs
    index 78e91f5883..f8ccfc05ae 100644
    --- a/crates/ty_python_semantic/src/types/infer.rs
    +++ b/crates/ty_python_semantic/src/types/infer.rs
    @@ -114,17 +114,15 @@ pub(crate) fn infer_definition_types<'db>(
     fn definition_cycle_recover<'db>(
         db: &'db dyn Db,
         _id: salsa::Id,
    -    _last_provisional_value: &DefinitionInference<'db>,
    -    _value: &DefinitionInference<'db>,
    +    last_provisional_value: &DefinitionInference<'db>,
    +    value: DefinitionInference<'db>,
         count: u32,
         definition: Definition<'db>,
    -) -> salsa::CycleRecoveryAction> {
    -    if count == ITERATIONS_BEFORE_FALLBACK {
    -        salsa::CycleRecoveryAction::Fallback(DefinitionInference::cycle_fallback(
    -            definition.scope(db),
    -        ))
    +) -> DefinitionInference<'db> {
    +    if &value == last_provisional_value || count != ITERATIONS_BEFORE_FALLBACK {
    +        value
         } else {
    -        salsa::CycleRecoveryAction::Iterate
    +        DefinitionInference::cycle_fallback(definition.scope(db))
         }
     }
     
    @@ -230,17 +228,15 @@ pub(crate) fn infer_isolated_expression<'db>(
     fn expression_cycle_recover<'db>(
         db: &'db dyn Db,
         _id: salsa::Id,
    -    _last_provisional_value: &ExpressionInference<'db>,
    -    _value: &ExpressionInference<'db>,
    +    last_provisional_value: &ExpressionInference<'db>,
    +    value: ExpressionInference<'db>,
         count: u32,
         input: InferExpression<'db>,
    -) -> salsa::CycleRecoveryAction> {
    -    if count == ITERATIONS_BEFORE_FALLBACK {
    -        salsa::CycleRecoveryAction::Fallback(ExpressionInference::cycle_fallback(
    -            input.expression(db).scope(db),
    -        ))
    +) -> ExpressionInference<'db> {
    +    if &value == last_provisional_value || count != ITERATIONS_BEFORE_FALLBACK {
    +        value
         } else {
    -        salsa::CycleRecoveryAction::Iterate
    +        ExpressionInference::cycle_fallback(input.expression(db).scope(db))
         }
     }
     
    diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
    index 278267fc15..c4e40423d3 100644
    --- a/fuzz/Cargo.toml
    +++ b/fuzz/Cargo.toml
    @@ -30,7 +30,7 @@ ty_python_semantic = { path = "../crates/ty_python_semantic" }
     ty_vendored = { path = "../crates/ty_vendored" }
     
     libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false }
    -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "664750a6e588ed23a0d2d9105a02cb5993c8e178", default-features = false, features = [
    +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "05a9af7f554b64b8aadc2eeb6f2caf73d0408d09", default-features = false, features = [
         "compact_str",
         "macros",
         "salsa_unstable",
    
    From c6573b16ace72f7db86c9f6245bd0251a1e046bb Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?Lo=C3=AFc=20Riegel?=
     <96702577+LoicRiegel@users.noreply.github.com>
    Date: Thu, 6 Nov 2025 02:11:29 +0100
    Subject: [PATCH 184/188] docs: revise Ruff setup instructions for Zed editor
     (#20935)
    
    Co-authored-by: Micha Reiser 
    ---
     docs/editors/setup.md | 79 +++++++------------------------------------
     1 file changed, 13 insertions(+), 66 deletions(-)
    
    diff --git a/docs/editors/setup.md b/docs/editors/setup.md
    index 3d81935465..17652539c3 100644
    --- a/docs/editors/setup.md
    +++ b/docs/editors/setup.md
    @@ -422,29 +422,12 @@ bundle for TextMate.
     
     ## Zed
     
    -Ruff is available as an extension for the Zed editor. To install it:
    +Ruff support is now built into Zed (no separate extension required).
     
    -1. Open the command palette with `Cmd+Shift+P`
    -1. Search for "zed: extensions"
    -1. Search for "ruff" in the extensions list and click "Install"
    +By default, Zed uses Ruff for formatting and linting.
     
    -To configure Zed to use the Ruff language server for Python files, add the following
    -to your `settings.json` file:
    -
    -```json
    -{
    -  "languages": {
    -    "Python": {
    -      "language_servers": ["ruff"]
    -      // Or, if there are other language servers you want to use with Python
    -      // "language_servers": ["pyright", "ruff"]
    -    }
    -  }
    -}
    -```
    -
    -To configure the language server, you can provide the [server settings](settings.md)
    -under the [`lsp.ruff.initialization_options.settings`](https://zed.dev/docs/configuring-zed#lsp) key:
    +To set up editor-wide Ruff options, provide the [server settings](settings.md)
    +under the [`lsp.ruff.initialization_options.settings`](https://zed.dev/docs/configuring-zed#lsp) key of your `settings.json` file:
     
     ```json
     {
    @@ -452,7 +435,7 @@ under the [`lsp.ruff.initialization_options.settings`](https://zed.dev/docs/conf
         "ruff": {
           "initialization_options": {
             "settings": {
    -          // Ruff server settings goes here
    +          // Ruff server settings go here
               "lineLength": 80,
               "lint": {
                 "extendSelect": ["I"],
    @@ -464,22 +447,14 @@ under the [`lsp.ruff.initialization_options.settings`](https://zed.dev/docs/conf
     }
     ```
     
    -You can configure Ruff to format Python code on-save by registering the Ruff formatter
    -and enabling the [`format_on_save`](https://zed.dev/docs/configuring-zed#format-on-save) setting:
    +[`format_on_save`](https://zed.dev/docs/configuring-zed#format-on-save) is enabled by default.
    +You can disable it for Python by changing `format_on_save` in your `settings.json` file:
     
     ```json
     {
       "languages": {
         "Python": {
    -      "language_servers": ["ruff"],
    -      "format_on_save": "on",
    -      "formatter": [
    -        {
    -          "language_server": {
    -            "name": "ruff"
    -          }
    -        }
    -      ]
    +      "format_on_save": "off"
         }
       }
     }
    @@ -492,40 +467,12 @@ You can configure Ruff to fix lint violations and/or organize imports on-save by
     {
       "languages": {
         "Python": {
    -      "language_servers": ["ruff"],
    -      "format_on_save": "on",
    -      "formatter": [
    -        // Fix all auto-fixable lint violations
    -        { "code_action": "source.fixAll.ruff" },
    +      "code_actions_on_format": {
             // Organize imports
    -        { "code_action": "source.organizeImports.ruff" }
    -      ]
    -    }
    -  }
    -}
    -```
    -
    -Taken together, you can configure Ruff to format, fix, and organize imports on-save via the
    -following `settings.json`:
    -
    -!!! note
    -
    -    For this configuration, it is important to use the correct order of the code action and
    -    formatter language server settings. The code actions should be defined before the formatter to
    -    ensure that the formatter takes care of any remaining style issues after the code actions have
    -    been applied.
    -
    -```json
    -{
    -  "languages": {
    -    "Python": {
    -      "language_servers": ["ruff"],
    -      "format_on_save": "on",
    -      "formatter": [
    -        { "code_action": "source.fixAll.ruff" },
    -        { "code_action": "source.organizeImports.ruff" },
    -        { "language_server": { "name": "ruff" } }
    -      ]
    +        "source.organizeImports.ruff": true,
    +        // Fix all auto-fixable lint violations
    +        "source.fixAll.ruff": true
    +      }
         }
       }
     }
    
    From b5ff96595dd3f2b85b7178fd1527b6aba9344c2d Mon Sep 17 00:00:00 2001
    From: Matthew Mckee 
    Date: Thu, 6 Nov 2025 11:46:08 +0000
    Subject: [PATCH 185/188] [ty] Favour imported symbols over builtin symbols
     (#21285)
    
    
    
    ## Summary
    
    Raised by @AlexWaygood.
    
    We previously did not favour imported symbols, when we probably
    should've
    
    ## Test Plan
    
    Add test showing that we favour imported symbol even if it is
    alphabetically after other symbols that are builtin.
    ---
     .../completion-evaluation-tasks.csv           |  2 +-
     crates/ty_ide/src/completion.rs               | 22 ++++++++++++++++++-
     2 files changed, 22 insertions(+), 2 deletions(-)
    
    diff --git a/crates/ty_completion_eval/completion-evaluation-tasks.csv b/crates/ty_completion_eval/completion-evaluation-tasks.csv
    index 4bea881bf6..92d1a3f03d 100644
    --- a/crates/ty_completion_eval/completion-evaluation-tasks.csv
    +++ b/crates/ty_completion_eval/completion-evaluation-tasks.csv
    @@ -17,7 +17,7 @@ numpy-array,main.py,1,1
     object-attr-instance-methods,main.py,0,1
     object-attr-instance-methods,main.py,1,1
     raise-uses-base-exception,main.py,0,2
    -scope-existing-over-new-import,main.py,0,13
    +scope-existing-over-new-import,main.py,0,1
     scope-prioritize-closer,main.py,0,2
     scope-simple-long-identifier,main.py,0,1
     tstring-completions,main.py,0,1
    diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs
    index 5b84e199f0..ae856dd7e7 100644
    --- a/crates/ty_ide/src/completion.rs
    +++ b/crates/ty_ide/src/completion.rs
    @@ -883,9 +883,10 @@ fn is_in_definition_place(db: &dyn Db, tokens: &[Token], file: File) -> bool {
     /// This has the effect of putting all dunder attributes after "normal"
     /// attributes, and all single-underscore attributes after dunder attributes.
     fn compare_suggestions(c1: &Completion, c2: &Completion) -> Ordering {
    -    fn key<'a>(completion: &'a Completion) -> (bool, NameKind, bool, &'a Name) {
    +    fn key<'a>(completion: &'a Completion) -> (bool, bool, NameKind, bool, &'a Name) {
             (
                 completion.module_name.is_some(),
    +            completion.builtin,
                 NameKind::classify(&completion.name),
                 completion.is_type_check_only,
                 &completion.name,
    @@ -4196,6 +4197,25 @@ type 
             ");
         }
     
    +    #[test]
    +    fn favour_imported_over_builtin() {
    +        let snapshot =
    +            completion_test_builder("from typing import Protocol\nclass Foo(P: ...")
    +                .filter(|c| c.name.starts_with('P'))
    +                .build()
    +                .snapshot();
    +
    +        // Here we favour `Protocol` over the other completions
    +        // because `Protocol` has been imported, and the other completions are builtin.
    +        assert_snapshot!(snapshot, @r"
    +        Protocol
    +        PendingDeprecationWarning
    +        PermissionError
    +        ProcessLookupError
    +        PythonFinalizationError
    +        ");
    +    }
    +
         /// A way to create a simple single-file (named `main.py`) completion test
         /// builder.
         ///
    
    From 5517c9943a5a7d66b0ea75e95667831ceb46dd09 Mon Sep 17 00:00:00 2001
    From: Ben Beasley 
    Date: Thu, 6 Nov 2025 12:43:32 +0000
    Subject: [PATCH 186/188] Require ignore 0.4.24 in `Cargo.toml` (#21292)
    
    
    
    ## Summary
    
    
    Since 4c4ddc8c29e, ruff uses the `WalkBuilder::current_dir` API
    [introduced in `ignore` version
    0.4.24](https://diff.rs/ignore/0.4.23/0.4.24/src%2Fwalk.rs), so it
    should explicitly depend on this minimum version.
    
    See also https://github.com/astral-sh/ruff/pull/20979.
    
    ## Test Plan
    
    
    Source inspection verifies this version is necessary; no additional
    testing is required since `Cargo.lock` already has (at least) this
    version.
    ---
     Cargo.toml | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/Cargo.toml b/Cargo.toml
    index cd9e05d6b8..525366136c 100644
    --- a/Cargo.toml
    +++ b/Cargo.toml
    @@ -103,7 +103,7 @@ hashbrown = { version = "0.16.0", default-features = false, features = [
         "inline-more",
     ] }
     heck = "0.5.0"
    -ignore = { version = "0.4.22" }
    +ignore = { version = "0.4.24" }
     imara-diff = { version = "0.1.5" }
     imperative = { version = "1.0.4" }
     indexmap = { version = "2.6.0" }
    
    From f189aad6d2e835743d43228a6b5ff2e40b17a000 Mon Sep 17 00:00:00 2001
    From: Alex Waygood 
    Date: Thu, 6 Nov 2025 09:00:43 -0500
    Subject: [PATCH 187/188] [ty] Make special cases for `UnionType` slightly
     narrower (#21276)
    
    Fixes https://github.com/astral-sh/ty/issues/1478
    ---
     .../resources/mdtest/implicit_type_aliases.md | 90 +++++++++++++++++--
     crates/ty_python_semantic/src/types/call.rs   | 32 +++++--
     .../src/types/infer/builder.rs                | 75 +++++++++++-----
     3 files changed, 158 insertions(+), 39 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 9ce736e235..b557a730f7 100644
    --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md
    +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md
    @@ -135,14 +135,15 @@ def _(int_or_int: IntOrInt, list_of_int_or_list_of_int: ListOfIntOrListOfInt):
     None | None  # error: [unsupported-operator] "Operator `|` is unsupported between objects of type `None` and `None`"
     ```
     
    -When constructing something non-sensical like `int | 1`, we could ideally emit a diagnostic for the
    -expression itself, as it leads to a `TypeError` at runtime. No other type checker supports this, so
    -for now we only emit an error when it is used in a type expression:
    +When constructing something nonsensical like `int | 1`, we emit a diagnostic for the expression
    +itself, as it leads to a `TypeError` at runtime. The result of the expression is then inferred as
    +`Unknown`, so we permit it to be used in a type expression.
     
     ```py
    -IntOrOne = int | 1
    +IntOrOne = int | 1  # error: [unsupported-operator]
    +
    +reveal_type(IntOrOne)  # revealed: Unknown
     
    -# error: [invalid-type-form] "Variable of type `Literal[1]` is not allowed in a type expression"
     def _(int_or_one: IntOrOne):
         reveal_type(int_or_one)  # revealed: Unknown
     ```
    @@ -160,6 +161,77 @@ def f(SomeUnionType: UnionType):
     f(int | str)
     ```
     
    +## `|` operator between class objects and non-class objects
    +
    +Using the `|` operator between a class object and a non-class object does not create a `UnionType`
    +instance; it calls the relevant dunder as normal:
    +
    +```py
    +class Foo:
    +    def __or__(self, other) -> str:
    +        return "foo"
    +
    +reveal_type(Foo() | int)  # revealed: str
    +reveal_type(Foo() | list[int])  # revealed: str
    +
    +class Bar:
    +    def __ror__(self, other) -> str:
    +        return "bar"
    +
    +reveal_type(int | Bar())  # revealed: str
    +reveal_type(list[int] | Bar())  # revealed: str
    +
    +class Invalid:
    +    def __or__(self, other: "Invalid") -> str:
    +        return "Invalid"
    +
    +    def __ror__(self, other: "Invalid") -> str:
    +        return "Invalid"
    +
    +# error: [unsupported-operator]
    +reveal_type(int | Invalid())  # revealed: Unknown
    +# error: [unsupported-operator]
    +reveal_type(Invalid() | list[int])  # revealed: Unknown
    +```
    +
    +## Custom `__(r)or__` methods on metaclasses are only partially respected
    +
    +A drawback of our extensive special casing of `|` operations between class objects is that
    +`__(r)or__` methods on metaclasses are completely disregarded if two classes are `|`'d together. We
    +respect the metaclass dunder if a class is `|`'d with a non-class, however:
    +
    +```py
    +class Meta(type):
    +    def __or__(self, other) -> str:
    +        return "Meta"
    +
    +class Foo(metaclass=Meta): ...
    +class Bar(metaclass=Meta): ...
    +
    +X = Foo | Bar
    +
    +# In an ideal world, perhaps we would respect `Meta.__or__` here and reveal `str`?
    +# But we still need to record what the elements are, since (according to the typing spec)
    +# `X` is still a valid type alias
    +reveal_type(X)  # revealed: types.UnionType
    +
    +def f(obj: X):
    +    reveal_type(obj)  # revealed: Foo | Bar
    +
    +# We do respect the metaclass `__or__` if it's used between a class and a non-class, however:
    +
    +Y = Foo | 42
    +reveal_type(Y)  # revealed: str
    +
    +Z = Bar | 56
    +reveal_type(Z)  # revealed: str
    +
    +def g(
    +    arg1: Y,  # error: [invalid-type-form]
    +    arg2: Z,  # error: [invalid-type-form]
    +): ...
    +```
    +
     ## Generic types
     
     Implicit type aliases can also refer to generic types:
    @@ -191,7 +263,8 @@ From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/
     > type hint is acceptable in a type alias
     
     However, no other type checker seems to support stringified annotations in implicit type aliases. We
    -currently also do not support them:
    +currently also do not support them, and we detect places where these attempted unions cause runtime
    +errors:
     
     ```py
     AliasForStr = "str"
    @@ -200,9 +273,10 @@ AliasForStr = "str"
     def _(s: AliasForStr):
         reveal_type(s)  # revealed: Unknown
     
    -IntOrStr = int | "str"
    +IntOrStr = int | "str"  # error: [unsupported-operator]
    +
    +reveal_type(IntOrStr)  # revealed: Unknown
     
    -# error: [invalid-type-form] "Variable of type `Literal["str"]` is not allowed in a type expression"
     def _(int_or_str: IntOrStr):
         reveal_type(int_or_str)  # revealed: Unknown
     ```
    diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs
    index e2fb7dac96..084fdbcfbd 100644
    --- a/crates/ty_python_semantic/src/types/call.rs
    +++ b/crates/ty_python_semantic/src/types/call.rs
    @@ -1,8 +1,8 @@
     use super::context::InferContext;
     use super::{Signature, Type, TypeContext};
     use crate::Db;
    -use crate::types::PropertyInstanceType;
     use crate::types::call::bind::BindingError;
    +use crate::types::{MemberLookupPolicy, PropertyInstanceType};
     use ruff_python_ast as ast;
     
     mod arguments;
    @@ -16,6 +16,16 @@ impl<'db> Type<'db> {
             left_ty: Type<'db>,
             op: ast::Operator,
             right_ty: Type<'db>,
    +    ) -> Result, CallBinOpError> {
    +        Self::try_call_bin_op_with_policy(db, left_ty, op, right_ty, MemberLookupPolicy::default())
    +    }
    +
    +    pub(crate) fn try_call_bin_op_with_policy(
    +        db: &'db dyn Db,
    +        left_ty: Type<'db>,
    +        op: ast::Operator,
    +        right_ty: Type<'db>,
    +        policy: MemberLookupPolicy,
         ) -> Result, CallBinOpError> {
             // We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from
             // the Python spec [1] is:
    @@ -43,39 +53,43 @@ impl<'db> Type<'db> {
                     && rhs_reflected != left_class.member(db, reflected_dunder).place
                 {
                     return Ok(right_ty
    -                    .try_call_dunder(
    +                    .try_call_dunder_with_policy(
                             db,
                             reflected_dunder,
    -                        CallArguments::positional([left_ty]),
    +                        &mut CallArguments::positional([left_ty]),
                             TypeContext::default(),
    +                        policy,
                         )
                         .or_else(|_| {
    -                        left_ty.try_call_dunder(
    +                        left_ty.try_call_dunder_with_policy(
                                 db,
                                 op.dunder(),
    -                            CallArguments::positional([right_ty]),
    +                            &mut CallArguments::positional([right_ty]),
                                 TypeContext::default(),
    +                            policy,
                             )
                         })?);
                 }
             }
     
    -        let call_on_left_instance = left_ty.try_call_dunder(
    +        let call_on_left_instance = left_ty.try_call_dunder_with_policy(
                 db,
                 op.dunder(),
    -            CallArguments::positional([right_ty]),
    +            &mut CallArguments::positional([right_ty]),
                 TypeContext::default(),
    +            policy,
             );
     
             call_on_left_instance.or_else(|_| {
                 if left_ty == right_ty {
                     Err(CallBinOpError::NotSupported)
                 } else {
    -                Ok(right_ty.try_call_dunder(
    +                Ok(right_ty.try_call_dunder_with_policy(
                         db,
                         op.reflected_dunder(),
    -                    CallArguments::positional([left_ty]),
    +                    &mut CallArguments::positional([left_ty]),
                         TypeContext::default(),
    +                    policy,
                     )?)
                 }
             })
    diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
    index b7608bbfff..e72b4af8db 100644
    --- a/crates/ty_python_semantic/src/types/infer/builder.rs
    +++ b/crates/ty_python_semantic/src/types/infer/builder.rs
    @@ -8474,11 +8474,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                     | Type::GenericAlias(..)
                     | Type::SpecialForm(_)
                     | Type::KnownInstance(KnownInstanceType::UnionType(_)),
    -                _,
    -                ast::Operator::BitOr,
    -            )
    -            | (
    -                _,
                     Type::ClassLiteral(..)
                     | Type::SubclassOf(..)
                     | Type::GenericAlias(..)
    @@ -8486,30 +8481,66 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                     | Type::KnownInstance(KnownInstanceType::UnionType(_)),
                     ast::Operator::BitOr,
                 ) if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310 => {
    -                // For a value expression like `int | None`, the inferred type for `None` will be
    -                // a nominal instance of `NoneType`, so we need to convert it to a class literal
    -                // such that it can later be converted back to a nominal instance type when calling
    -                // `.in_type_expression` on the `UnionType` instance.
    -                let convert_none_type = |ty: Type<'db>| {
    -                    if ty.is_none(self.db()) {
    -                        KnownClass::NoneType.to_class_literal(self.db())
    -                    } else {
    -                        ty
    -                    }
    -                };
    -
                     if left_ty.is_equivalent_to(self.db(), right_ty) {
                         Some(left_ty)
                     } else {
                         Some(Type::KnownInstance(KnownInstanceType::UnionType(
    -                        UnionTypeInstance::new(
    -                            self.db(),
    -                            convert_none_type(left_ty),
    -                            convert_none_type(right_ty),
    -                        ),
    +                        UnionTypeInstance::new(self.db(), left_ty, right_ty),
                         )))
                     }
                 }
    +            (
    +                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 Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310
    +                && instance.has_known_class(self.db(), KnownClass::NoneType) =>
    +            {
    +                Some(Type::KnownInstance(KnownInstanceType::UnionType(
    +                    UnionTypeInstance::new(self.db(), left_ty, right_ty),
    +                )))
    +            }
    +
    +            // 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 Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310 => {
    +                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.
    
    From 132d10fb6fb30db17ebf894284e97cd2cc831e10 Mon Sep 17 00:00:00 2001
    From: Zanie Blue 
    Date: Thu, 6 Nov 2025 08:27:49 -0600
    Subject: [PATCH 188/188] [ty] Discover site-packages from the environment that
     ty is installed in (#21286)
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    
    
    ## Summary
    
    
    
    Closes https://github.com/astral-sh/ty/issues/989
    
    There are various situations where users expect the Python packages
    installed in the same environment as ty itself to be considered during
    type checking. A minimal example would look like:
    
    ```
    uv venv my-env
    uv pip install my-env ty httpx
    echo "import httpx" > foo.py
    ./my-env/bin/ty check foo.py
    ```
    
    or
    
    ```
    uv tool install ty --with httpx
    echo "import httpx" > foo.py
    ty check foo.py
    ```
    
    While these are a bit contrived, there are real-world situations where a
    user would expect a similar behavior to work. Notably, all of the other
    type checkers consider their own environment when determining search
    paths (though I'll admit that I have not verified when they choose not
    to do this).
    
    One common situation where users are encountering this today is with
    `uvx --with-requirements script.py ty check script.py` — which is
    currently our "best" recommendation for type checking a PEP 723 script,
    but it doesn't work.
    
    Of the options discussed in
    https://github.com/astral-sh/ty/issues/989#issuecomment-3307417985, I've
    chosen (2) as our criteria for including ty's environment in the search
    paths.
    
    - If no virtual environment is discovered, we will always include ty's
    environment.
    - If a `.venv` is discovered in the working directory, we will _prepend_
    ty's environment to the search paths. The dependencies in ty's
    environment (e.g., from `uvx --with`) will take precedence.
    - If a virtual environment is active, e.g., `VIRTUAL_ENV` (i.e.,
    including conda prefixes) is set, we will not include ty's environment.
    
    The reason we need to special case the `.venv` case is that we both
    
    1.  Recommend `uvx ty` today as a way to check your project
    2. Want to enable `uvx --with <...> ty`
    
    And I don't want (2) to break when you _happen_ to be in a project
    (i.e., if we only included ty's environment when _no_ environment is
    found) and don't want to remove support for (1).
    
    I think long-term, I want to make `uvx ` layer the environment on
    _top_ of the project environment (in uv), which would obviate the need
    for this change when you're using uv. However, that change is breaking
    and I think users will expect this behavior in contexts where they're
    not using uv, so I think we should handle it in ty regardless.
    
    I've opted not to include the environment if it's non-virtual (i.e., a
    system environment) for now. It seems better to start by being more
    restrictive. I left a comment in the code.
    
    ## Test Plan
    
    I did some manual testing with the initial commit, then subsequently
    added some unit tests.
    
    ```
    ❯ echo "import httpx" > example.py
    ❯ uvx --with httpx ty check example.py
    Installed 8 packages in 19ms
    error[unresolved-import]: Cannot resolve imported module `httpx`
     --> foo/example.py:1:8
      |
    1 | import httpx
      |        ^^^^^
      |
    info: Searched in the following paths during module resolution:
    info:   1. /Users/zb/workspace/ty/python (first-party code)
    info:   2. /Users/zb/workspace/ty (first-party code)
    info:   3. vendored://stdlib (stdlib typeshed stubs vendored by ty)
    info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
    info: rule `unresolved-import` is enabled by default
    
    Found 1 diagnostic
    ❯ uvx --from . --with httpx ty check example.py
    All checks passed!
    ```
    
    ```
    ❯ uv init --script foo.py
    Initialized script at `foo.py`
    ❯ uv add --script foo.py httpx
    warning: The Python request from `.python-version` resolved to Python 3.13.8, which is incompatible with the script's Python requirement: `>=3.14`
    Updated `foo.py`
    ❯ echo "import httpx" >> foo.py
    ❯ uvx --with-requirements foo.py ty check foo.py
    error[unresolved-import]: Cannot resolve imported module `httpx`
      --> foo.py:15:8
       |
    13 | if __name__ == "__main__":
    14 |     main()
    15 | import httpx
       |        ^^^^^
       |
    info: Searched in the following paths during module resolution:
    info:   1. /Users/zb/workspace/ty/python (first-party code)
    info:   2. /Users/zb/workspace/ty (first-party code)
    info:   3. vendored://stdlib (stdlib typeshed stubs vendored by ty)
    info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
    info: rule `unresolved-import` is enabled by default
    
    Found 1 diagnostic
    ❯ uvx --from . --with-requirements foo.py ty check foo.py
    All checks passed!
    ```
    
    Notice we do not include ty's environment if `VIRTUAL_ENV` is set
    
    ```
    ❯ VIRTUAL_ENV=.venv uvx --with httpx ty check foo/example.py
    error[unresolved-import]: Cannot resolve imported module `httpx`
     --> foo/example.py:1:8
      |
    1 | import httpx
      |        ^^^^^
      |
    info: Searched in the following paths during module resolution:
    info:   1. /Users/zb/workspace/ty/python (first-party code)
    info:   2. /Users/zb/workspace/ty (first-party code)
    info:   3. vendored://stdlib (stdlib typeshed stubs vendored by ty)
    info:   4. /Users/zb/workspace/ty/.venv/lib/python3.13/site-packages (site-packages)
    info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
    info: rule `unresolved-import` is enabled by default
    
    Found 1 diagnostic
    ```
    ---
     crates/ty/tests/cli/main.rs                   |  43 ++-
     crates/ty/tests/cli/python_environment.rs     | 274 +++++++++++++++++-
     crates/ty_project/src/metadata/options.rs     |  56 +++-
     .../ty_python_semantic/src/site_packages.rs   |  62 +++-
     4 files changed, 417 insertions(+), 18 deletions(-)
    
    diff --git a/crates/ty/tests/cli/main.rs b/crates/ty/tests/cli/main.rs
    index e911e300c0..446ba86611 100644
    --- a/crates/ty/tests/cli/main.rs
    +++ b/crates/ty/tests/cli/main.rs
    @@ -5,6 +5,7 @@ mod python_environment;
     mod rule_selection;
     
     use anyhow::Context as _;
    +use insta::Settings;
     use insta::internals::SettingsBindDropGuard;
     use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
     use std::{
    @@ -760,8 +761,10 @@ fn can_handle_large_binop_expressions() -> anyhow::Result<()> {
     
     pub(crate) struct CliTest {
         _temp_dir: TempDir,
    -    _settings_scope: SettingsBindDropGuard,
    +    settings: Settings,
    +    settings_scope: Option,
         project_dir: PathBuf,
    +    ty_binary_path: PathBuf,
     }
     
     impl CliTest {
    @@ -794,7 +797,9 @@ impl CliTest {
             Ok(Self {
                 project_dir,
                 _temp_dir: temp_dir,
    -            _settings_scope: settings_scope,
    +            settings,
    +            settings_scope: Some(settings_scope),
    +            ty_binary_path: get_cargo_bin("ty"),
             })
         }
     
    @@ -823,6 +828,30 @@ impl CliTest {
             Ok(())
         }
     
    +    /// Return [`Self`] with the ty binary copied to the specified path instead.
    +    pub(crate) fn with_ty_at(mut self, dest_path: impl AsRef) -> anyhow::Result {
    +        let dest_path = dest_path.as_ref();
    +        let dest_path = self.project_dir.join(dest_path);
    +
    +        Self::ensure_parent_directory(&dest_path)?;
    +        std::fs::copy(&self.ty_binary_path, &dest_path)
    +            .with_context(|| format!("Failed to copy ty binary to `{}`", dest_path.display()))?;
    +
    +        self.ty_binary_path = dest_path;
    +        Ok(self)
    +    }
    +
    +    /// Add a filter to the settings and rebind them.
    +    pub(crate) fn with_filter(mut self, pattern: &str, replacement: &str) -> Self {
    +        self.settings.add_filter(pattern, replacement);
    +        // Drop the old scope before binding a new one, otherwise the old scope is dropped _after_
    +        // binding and assigning the new one, restoring the settings to their state before the old
    +        // scope was bound.
    +        drop(self.settings_scope.take());
    +        self.settings_scope = Some(self.settings.bind_to_scope());
    +        self
    +    }
    +
         fn ensure_parent_directory(path: &Path) -> anyhow::Result<()> {
             if let Some(parent) = path.parent() {
                 std::fs::create_dir_all(parent)
    @@ -868,7 +897,7 @@ impl CliTest {
         }
     
         pub(crate) fn command(&self) -> Command {
    -        let mut command = Command::new(get_cargo_bin("ty"));
    +        let mut command = Command::new(&self.ty_binary_path);
             command.current_dir(&self.project_dir).arg("check");
     
             // Unset all environment variables because they can affect test behavior.
    @@ -881,3 +910,11 @@ impl CliTest {
     fn tempdir_filter(path: &Path) -> String {
         format!(r"{}\\?/?", regex::escape(path.to_str().unwrap()))
     }
    +
    +fn site_packages_filter(python_version: &str) -> String {
    +    if cfg!(windows) {
    +        "Lib/site-packages".to_string()
    +    } else {
    +        format!("lib/python{}/site-packages", regex::escape(python_version))
    +    }
    +}
    diff --git a/crates/ty/tests/cli/python_environment.rs b/crates/ty/tests/cli/python_environment.rs
    index 04fa8be88f..de6d99aa9a 100644
    --- a/crates/ty/tests/cli/python_environment.rs
    +++ b/crates/ty/tests/cli/python_environment.rs
    @@ -1,7 +1,7 @@
     use insta_cmd::assert_cmd_snapshot;
     use ruff_python_ast::PythonVersion;
     
    -use crate::CliTest;
    +use crate::{CliTest, site_packages_filter};
     
     /// Specifying an option on the CLI should take precedence over the same setting in the
     /// project's configuration. Here, this is tested for the Python version.
    @@ -1654,6 +1654,278 @@ home = ./
         Ok(())
     }
     
    +/// ty should include site packages from its own environment when no other environment is found.
    +#[test]
    +fn ty_environment_is_only_environment() -> anyhow::Result<()> {
    +    let ty_venv_site_packages = if cfg!(windows) {
    +        "ty-venv/Lib/site-packages"
    +    } else {
    +        "ty-venv/lib/python3.13/site-packages"
    +    };
    +
    +    let ty_executable_path = if cfg!(windows) {
    +        "ty-venv/Scripts/ty.exe"
    +    } else {
    +        "ty-venv/bin/ty"
    +    };
    +
    +    let ty_package_path = format!("{ty_venv_site_packages}/ty_package/__init__.py");
    +
    +    let case = CliTest::with_files([
    +        (ty_package_path.as_str(), "class TyEnvClass: ..."),
    +        (
    +            "ty-venv/pyvenv.cfg",
    +            r"
    +            home = ./
    +            version = 3.13
    +            ",
    +        ),
    +        (
    +            "test.py",
    +            r"
    +            from ty_package import TyEnvClass
    +            ",
    +        ),
    +    ])?;
    +
    +    let case = case.with_ty_at(ty_executable_path)?;
    +    assert_cmd_snapshot!(case.command(), @r###"
    +    success: true
    +    exit_code: 0
    +    ----- stdout -----
    +    All checks passed!
    +
    +    ----- stderr -----
    +    "###);
    +
    +    Ok(())
    +}
    +
    +/// ty should include site packages from both its own environment and a local `.venv`. The packages
    +/// from ty's environment should take precedence.
    +#[test]
    +fn ty_environment_and_discovered_venv() -> anyhow::Result<()> {
    +    let ty_venv_site_packages = if cfg!(windows) {
    +        "ty-venv/Lib/site-packages"
    +    } else {
    +        "ty-venv/lib/python3.13/site-packages"
    +    };
    +
    +    let ty_executable_path = if cfg!(windows) {
    +        "ty-venv/Scripts/ty.exe"
    +    } else {
    +        "ty-venv/bin/ty"
    +    };
    +
    +    let local_venv_site_packages = if cfg!(windows) {
    +        ".venv/Lib/site-packages"
    +    } else {
    +        ".venv/lib/python3.13/site-packages"
    +    };
    +
    +    let ty_unique_package = format!("{ty_venv_site_packages}/ty_package/__init__.py");
    +    let local_unique_package = format!("{local_venv_site_packages}/local_package/__init__.py");
    +    let ty_conflicting_package = format!("{ty_venv_site_packages}/shared_package/__init__.py");
    +    let local_conflicting_package =
    +        format!("{local_venv_site_packages}/shared_package/__init__.py");
    +
    +    let case = CliTest::with_files([
    +        (ty_unique_package.as_str(), "class TyEnvClass: ..."),
    +        (local_unique_package.as_str(), "class LocalClass: ..."),
    +        (ty_conflicting_package.as_str(), "class FromTyEnv: ..."),
    +        (
    +            local_conflicting_package.as_str(),
    +            "class FromLocalVenv: ...",
    +        ),
    +        (
    +            "ty-venv/pyvenv.cfg",
    +            r"
    +            home = ./
    +            version = 3.13
    +            ",
    +        ),
    +        (
    +            ".venv/pyvenv.cfg",
    +            r"
    +            home = ./
    +            version = 3.13
    +            ",
    +        ),
    +        (
    +            "test.py",
    +            r"
    +            # Should resolve from ty's environment
    +            from ty_package import TyEnvClass
    +            # Should resolve from local .venv
    +            from local_package import LocalClass
    +            # Should resolve from ty's environment (takes precedence)
    +            from shared_package import FromTyEnv
    +            # Should NOT resolve (shadowed by ty's environment version)
    +            from shared_package import FromLocalVenv
    +            ",
    +        ),
    +    ])?
    +    .with_ty_at(ty_executable_path)?;
    +
    +    assert_cmd_snapshot!(case.command(), @r###"
    +    success: false
    +    exit_code: 1
    +    ----- stdout -----
    +    error[unresolved-import]: Module `shared_package` has no member `FromLocalVenv`
    +     --> test.py:9:28
    +      |
    +    7 | from shared_package import FromTyEnv
    +    8 | # Should NOT resolve (shadowed by ty's environment version)
    +    9 | from shared_package import FromLocalVenv
    +      |                            ^^^^^^^^^^^^^
    +      |
    +    info: rule `unresolved-import` is enabled by default
    +
    +    Found 1 diagnostic
    +
    +    ----- stderr -----
    +    "###);
    +
    +    Ok(())
    +}
    +
    +/// When `VIRTUAL_ENV` is set, ty should *not* discover its own environment's site-packages.
    +#[test]
    +fn ty_environment_and_active_environment() -> anyhow::Result<()> {
    +    let ty_venv_site_packages = if cfg!(windows) {
    +        "ty-venv/Lib/site-packages"
    +    } else {
    +        "ty-venv/lib/python3.13/site-packages"
    +    };
    +
    +    let ty_executable_path = if cfg!(windows) {
    +        "ty-venv/Scripts/ty.exe"
    +    } else {
    +        "ty-venv/bin/ty"
    +    };
    +
    +    let active_venv_site_packages = if cfg!(windows) {
    +        "active-venv/Lib/site-packages"
    +    } else {
    +        "active-venv/lib/python3.13/site-packages"
    +    };
    +
    +    let ty_package_path = format!("{ty_venv_site_packages}/ty_package/__init__.py");
    +    let active_package_path = format!("{active_venv_site_packages}/active_package/__init__.py");
    +
    +    let case = CliTest::with_files([
    +        (ty_package_path.as_str(), "class TyEnvClass: ..."),
    +        (
    +            "ty-venv/pyvenv.cfg",
    +            r"
    +            home = ./
    +            version = 3.13
    +            ",
    +        ),
    +        (active_package_path.as_str(), "class ActiveClass: ..."),
    +        (
    +            "active-venv/pyvenv.cfg",
    +            r"
    +            home = ./
    +            version = 3.13
    +            ",
    +        ),
    +        (
    +            "test.py",
    +            r"
    +            from ty_package import TyEnvClass
    +            from active_package import ActiveClass
    +            ",
    +        ),
    +    ])?
    +    .with_ty_at(ty_executable_path)?
    +    .with_filter(&site_packages_filter("3.13"), "");
    +
    +    assert_cmd_snapshot!(
    +        case.command()
    +            .env("VIRTUAL_ENV", case.root().join("active-venv")),
    +        @r"
    +    success: false
    +    exit_code: 1
    +    ----- stdout -----
    +    error[unresolved-import]: Cannot resolve imported module `ty_package`
    +     --> test.py:2:6
    +      |
    +    2 | from ty_package import TyEnvClass
    +      |      ^^^^^^^^^^
    +    3 | from active_package import ActiveClass
    +      |
    +    info: Searched in the following paths during module resolution:
    +    info:   1. / (first-party code)
    +    info:   2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
    +    info:   3. /active-venv/ (site-packages)
    +    info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
    +    info: rule `unresolved-import` is enabled by default
    +
    +    Found 1 diagnostic
    +
    +    ----- stderr -----
    +    "
    +    );
    +
    +    Ok(())
    +}
    +
    +/// When ty is installed in a system environment rather than a virtual environment, it should
    +/// not include the environment's site-packages in its search path.
    +#[test]
    +fn ty_environment_is_system_not_virtual() -> anyhow::Result<()> {
    +    let ty_system_site_packages = if cfg!(windows) {
    +        "system-python/Lib/site-packages"
    +    } else {
    +        "system-python/lib/python3.13/site-packages"
    +    };
    +
    +    let ty_executable_path = if cfg!(windows) {
    +        "system-python/Scripts/ty.exe"
    +    } else {
    +        "system-python/bin/ty"
    +    };
    +
    +    let ty_package_path = format!("{ty_system_site_packages}/system_package/__init__.py");
    +
    +    let case = CliTest::with_files([
    +        // Package in system Python installation (should NOT be discovered)
    +        (ty_package_path.as_str(), "class SystemClass: ..."),
    +        // Note: NO pyvenv.cfg - this is a system installation, not a venv
    +        (
    +            "test.py",
    +            r"
    +            from system_package import SystemClass
    +            ",
    +        ),
    +    ])?
    +    .with_ty_at(ty_executable_path)?;
    +
    +    assert_cmd_snapshot!(case.command(), @r###"
    +    success: false
    +    exit_code: 1
    +    ----- stdout -----
    +    error[unresolved-import]: Cannot resolve imported module `system_package`
    +     --> test.py:2:6
    +      |
    +    2 | from system_package import SystemClass
    +      |      ^^^^^^^^^^^^^^
    +      |
    +    info: Searched in the following paths during module resolution:
    +    info:   1. / (first-party code)
    +    info:   2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
    +    info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
    +    info: rule `unresolved-import` is enabled by default
    +
    +    Found 1 diagnostic
    +
    +    ----- stderr -----
    +    "###);
    +
    +    Ok(())
    +}
    +
     #[test]
     fn src_root_deprecation_warning() -> anyhow::Result<()> {
         let case = CliTest::with_files([
    diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs
    index 1e498f6bf8..d83270be90 100644
    --- a/crates/ty_project/src/metadata/options.rs
    +++ b/crates/ty_project/src/metadata/options.rs
    @@ -164,14 +164,24 @@ impl Options {
                     .context("Failed to discover local Python environment")?
             };
     
    -        let site_packages_paths = if let Some(python_environment) = python_environment.as_ref() {
    +        let self_site_packages = self_environment_search_paths(
                 python_environment
    -                .site_packages_paths(system)
    -                .context("Failed to discover the site-packages directory")?
    +                .as_ref()
    +                .map(ty_python_semantic::PythonEnvironment::origin)
    +                .cloned(),
    +            system,
    +        )
    +        .unwrap_or_default();
    +
    +        let site_packages_paths = if let Some(python_environment) = python_environment.as_ref() {
    +            self_site_packages.concatenate(
    +                python_environment
    +                    .site_packages_paths(system)
    +                    .context("Failed to discover the site-packages directory")?,
    +            )
             } else {
                 tracing::debug!("No virtual environment found");
    -
    -            SitePackagesPaths::default()
    +            self_site_packages
             };
     
             let real_stdlib_path = python_environment.as_ref().and_then(|python_environment| {
    @@ -461,6 +471,42 @@ impl Options {
         }
     }
     
    +/// Return the site-packages from the environment ty is installed in, as derived from ty's
    +/// executable.
    +///
    +/// If there's an existing environment with an origin that does not allow including site-packages
    +/// from ty's environment, discovery of ty's environment is skipped and [`None`] is returned.
    +///
    +/// Since ty may be executed from an arbitrary non-Python location, errors during discovery of ty's
    +/// environment are not raised, instead [`None`] is returned.
    +fn self_environment_search_paths(
    +    existing_origin: Option,
    +    system: &dyn System,
    +) -> Option {
    +    if existing_origin.is_some_and(|origin| !origin.allows_concatenation_with_self_environment()) {
    +        return None;
    +    }
    +
    +    let Ok(exe_path) = std::env::current_exe() else {
    +        return None;
    +    };
    +    let ty_path = SystemPath::from_std_path(exe_path.as_path())?;
    +
    +    let environment = PythonEnvironment::new(ty_path, SysPrefixPathOrigin::SelfEnvironment, system)
    +        .inspect_err(|err| tracing::debug!("Failed to discover ty's environment: {err}"))
    +        .ok()?;
    +
    +    let search_paths = environment
    +        .site_packages_paths(system)
    +        .inspect_err(|err| {
    +            tracing::debug!("Failed to discover site-packages in ty's environment: {err}");
    +        })
    +        .ok();
    +
    +    tracing::debug!("Using site-packages from ty's environment");
    +    search_paths
    +}
    +
     #[derive(
         Debug,
         Default,
    diff --git a/crates/ty_python_semantic/src/site_packages.rs b/crates/ty_python_semantic/src/site_packages.rs
    index c162dfc70a..8418db6124 100644
    --- a/crates/ty_python_semantic/src/site_packages.rs
    +++ b/crates/ty_python_semantic/src/site_packages.rs
    @@ -62,6 +62,15 @@ impl SitePackagesPaths {
             self.0.extend(other.0);
         }
     
    +    /// Concatenate two instances of [`SitePackagesPaths`].
    +    #[must_use]
    +    pub fn concatenate(mut self, other: Self) -> Self {
    +        for path in other {
    +            self.0.insert(path);
    +        }
    +        self
    +    }
    +
         /// Tries to detect the version from the layout of the `site-packages` directory.
         pub fn python_version_from_layout(&self) -> Option {
             if cfg!(windows) {
    @@ -252,6 +261,13 @@ impl PythonEnvironment {
                 Self::System(env) => env.real_stdlib_directory(system),
             }
         }
    +
    +    pub fn origin(&self) -> &SysPrefixPathOrigin {
    +        match self {
    +            Self::Virtual(env) => &env.root_path.origin,
    +            Self::System(env) => &env.root_path.origin,
    +        }
    +    }
     }
     
     /// Enumeration of the subdirectories of `sys.prefix` that could contain a
    @@ -1393,15 +1409,15 @@ impl SysPrefixPath {
         ) -> SitePackagesDiscoveryResult {
             let sys_prefix = if !origin.must_point_directly_to_sys_prefix()
                 && system.is_file(unvalidated_path)
    -            && unvalidated_path
    -                .file_name()
    -                .is_some_and(|name| name.starts_with("python"))
    -        {
    -            // It looks like they passed us a path to a Python executable, e.g. `.venv/bin/python3`.
    -            // Try to figure out the `sys.prefix` value from the Python executable.
    +            && unvalidated_path.file_name().is_some_and(|name| {
    +                name.starts_with("python")
    +                    || name.eq_ignore_ascii_case(&format!("ty{}", std::env::consts::EXE_SUFFIX))
    +            }) {
    +            // It looks like they passed us a path to an executable, e.g. `.venv/bin/python3`. Try
    +            // to figure out the `sys.prefix` value from the Python executable.
                 let sys_prefix = if cfg!(windows) {
    -                // On Windows, the relative path to the Python executable from `sys.prefix`
    -                // is different depending on whether it's a virtual environment or a system installation.
    +                // On Windows, the relative path to the executable from `sys.prefix` is different
    +                // depending on whether it's a virtual environment or a system installation.
                     // System installations have their executable at `/python.exe`,
                     // whereas virtual environments have their executable at `/Scripts/python.exe`.
                     unvalidated_path.parent().and_then(|parent| {
    @@ -1586,6 +1602,8 @@ pub enum SysPrefixPathOrigin {
         /// A `.venv` directory was found in the current working directory,
         /// and the `sys.prefix` path is the path to that virtual environment.
         LocalVenv,
    +    /// The `sys.prefix` path came from the environment ty is installed in.
    +    SelfEnvironment,
     }
     
     impl SysPrefixPathOrigin {
    @@ -1599,6 +1617,13 @@ impl SysPrefixPathOrigin {
                 | Self::Editor
                 | Self::DerivedFromPyvenvCfg
                 | Self::CondaPrefixVar => false,
    +            // It's not strictly true that the self environment must be virtual, e.g., ty could be
    +            // installed in a system Python environment and users may expect us to respect
    +            // dependencies installed alongside it. However, we're intentionally excluding support
    +            // for this to start. Note a change here has downstream implications, i.e., we probably
    +            // don't want the packages in a system environment to take precedence over those in a
    +            // virtual environment and would need to reverse the ordering in that case.
    +            Self::SelfEnvironment => true,
             }
         }
     
    @@ -1608,13 +1633,31 @@ impl SysPrefixPathOrigin {
         /// the `sys.prefix` directory, e.g. the `--python` CLI flag.
         pub(crate) const fn must_point_directly_to_sys_prefix(&self) -> bool {
             match self {
    -            Self::PythonCliFlag | Self::ConfigFileSetting(..) | Self::Editor => false,
    +            Self::PythonCliFlag
    +            | Self::ConfigFileSetting(..)
    +            | Self::Editor
    +            | Self::SelfEnvironment => false,
                 Self::VirtualEnvVar
                 | Self::CondaPrefixVar
                 | Self::DerivedFromPyvenvCfg
                 | Self::LocalVenv => true,
             }
         }
    +
    +    /// Whether paths with this origin should allow combination with paths with a
    +    /// [`SysPrefixPathOrigin::SelfEnvironment`] origin.
    +    pub const fn allows_concatenation_with_self_environment(&self) -> bool {
    +        match self {
    +            Self::SelfEnvironment
    +            | Self::CondaPrefixVar
    +            | Self::VirtualEnvVar
    +            | Self::Editor
    +            | Self::DerivedFromPyvenvCfg
    +            | Self::ConfigFileSetting(..)
    +            | Self::PythonCliFlag => false,
    +            Self::LocalVenv => true,
    +        }
    +    }
     }
     
     impl std::fmt::Display for SysPrefixPathOrigin {
    @@ -1627,6 +1670,7 @@ impl std::fmt::Display for SysPrefixPathOrigin {
                 Self::DerivedFromPyvenvCfg => f.write_str("derived `sys.prefix` path"),
                 Self::LocalVenv => f.write_str("local virtual environment"),
                 Self::Editor => f.write_str("selected interpreter in your editor"),
    +            Self::SelfEnvironment => f.write_str("ty environment"),
             }
         }
     }