diff --git a/crates/red_knot_project/tests/check.rs b/crates/red_knot_project/tests/check.rs index 711cec75f2..5e94a96000 100644 --- a/crates/red_knot_project/tests/check.rs +++ b/crates/red_knot_project/tests/check.rs @@ -283,4 +283,9 @@ const KNOWN_FAILURES: &[(&str, bool, bool)] = &[ // related to circular references in f-string annotations (invalid syntax) ("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_15.py", true, true), ("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_14.py", false, true), + // related to circular references in stub type annotations (salsa cycle panic): + ("crates/ruff_linter/resources/test/fixtures/pycodestyle/E501_4.py", false, true), + ("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_0.py", false, true), + ("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_12.py", false, true), + ("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_14.py", false, true), ]; diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md b/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md index e3b418716e..8218952448 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md +++ b/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md @@ -15,3 +15,30 @@ class Bar(Foo[Bar]): ... reveal_type(Bar) # revealed: Literal[Bar] reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]] ``` + +## Access to attributes declarated in stubs + +Unlike regular Python modules, stub files often omit the right-hand side in declarations, including +in class scope. However, from the perspective of the type checker, we have to treat them as bindings +too. That is, `symbol: type` is the same as `symbol: type = ...`. + +One implication of this is that we'll always treat symbols in class scope as safe to be accessed +from the class object itself. We'll never infer a "pure instance attribute" from a stub. + +`b.pyi`: + +```pyi +from typing import ClassVar + +class C: + class_or_instance_var: int +``` + +```py +from typing import ClassVar, Literal + +from b import C + +# No error here, since we treat `class_or_instance_var` as bound on the class. +reveal_type(C.class_or_instance_var) # revealed: int +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/locals.md b/crates/red_knot_python_semantic/resources/mdtest/stubs/locals.md new file mode 100644 index 0000000000..9852f683ac --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/stubs/locals.md @@ -0,0 +1,17 @@ +# Declarations in stubs + +Unlike regular Python modules, stub files often declare module-global variables without initializing +them. If these symbols are then used in the same stub, applying regular logic would lead to an +undefined variable access error. + +However, from the perspective of the type checker, we should treat something like `symbol: type` the +same as `symbol: type = ...`. In other words, assume these are bindings too. + +```pyi +from typing import Literal + +CONSTANT: Literal[42] + +# No error here, even though the variable is not initialized. +uses_constant: int = CONSTANT +``` diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index 84c2197749..74e3494a6d 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -353,7 +353,7 @@ impl<'db> SemanticIndexBuilder<'db> { #[allow(unsafe_code)] // SAFETY: `definition_node` is guaranteed to be a child of `self.module` let kind = unsafe { definition_node.into_owned(self.module.clone()) }; - let category = kind.category(); + let category = kind.category(self.file.is_stub(self.db.upcast())); let is_reexported = kind.is_reexported(); let definition = Definition::new( self.db, diff --git a/crates/red_knot_python_semantic/src/semantic_index/definition.rs b/crates/red_knot_python_semantic/src/semantic_index/definition.rs index 19e49f244e..d41c3ccdb8 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/definition.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/definition.rs @@ -515,7 +515,7 @@ impl DefinitionKind<'_> { } } - pub(crate) fn category(&self) -> DefinitionCategory { + pub(crate) fn category(&self, in_stub: bool) -> DefinitionCategory { match self { // functions, classes, and imports always bind, and we consider them declarations DefinitionKind::Function(_) @@ -543,9 +543,10 @@ impl DefinitionKind<'_> { DefinitionCategory::Binding } } - // annotated assignment is always a declaration, only a binding if there is a RHS + // Annotated assignment is always a declaration. It is also a binding if there is a RHS + // or if we are in a stub file. Unfortunately, it is common for stubs to omit even an `...` value placeholder. DefinitionKind::AnnotatedAssignment(ann_assign) => { - if ann_assign.value.is_some() { + if in_stub || ann_assign.value.is_some() { DefinitionCategory::DeclarationAndBinding } else { DefinitionCategory::Declaration diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 632458aa94..224086ec44 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -119,7 +119,7 @@ fn infer_definition_types_cycle_recovery<'db>( ) -> TypeInference<'db> { tracing::trace!("infer_definition_types_cycle_recovery"); let mut inference = TypeInference::empty(input.scope(db)); - let category = input.kind(db).category(); + let category = input.kind(db).category(input.file(db).is_stub(db.upcast())); if category.is_declaration() { inference .declarations @@ -903,7 +903,10 @@ impl<'db> TypeInferenceBuilder<'db> { } fn add_binding(&mut self, node: AnyNodeRef, binding: Definition<'db>, ty: Type<'db>) { - debug_assert!(binding.kind(self.db()).category().is_binding()); + debug_assert!(binding + .kind(self.db()) + .category(self.context.in_stub()) + .is_binding()); let use_def = self.index.use_def_map(binding.file_scope(self.db())); let declarations = use_def.declarations_at_binding(binding); let mut bound_ty = ty; @@ -938,7 +941,10 @@ impl<'db> TypeInferenceBuilder<'db> { declaration: Definition<'db>, ty: TypeAndQualifiers<'db>, ) { - debug_assert!(declaration.kind(self.db()).category().is_declaration()); + debug_assert!(declaration + .kind(self.db()) + .category(self.context.in_stub()) + .is_declaration()); let use_def = self.index.use_def_map(declaration.file_scope(self.db())); let prior_bindings = use_def.bindings_at_declaration(declaration); // unbound_ty is Never because for this check we don't care about unbound @@ -968,8 +974,14 @@ impl<'db> TypeInferenceBuilder<'db> { definition: Definition<'db>, declared_and_inferred_ty: &DeclaredAndInferredType<'db>, ) { - debug_assert!(definition.kind(self.db()).category().is_binding()); - debug_assert!(definition.kind(self.db()).category().is_declaration()); + debug_assert!(definition + .kind(self.db()) + .category(self.context.in_stub()) + .is_binding()); + debug_assert!(definition + .kind(self.db()) + .category(self.context.in_stub()) + .is_declaration()); let (declared_ty, inferred_ty) = match *declared_and_inferred_ty { DeclaredAndInferredType::AreTheSame(ty) => (ty.into(), ty), @@ -2226,7 +2238,15 @@ impl<'db> TypeInferenceBuilder<'db> { }, ); } else { - self.add_declaration(assignment.into(), definition, declared_ty); + if self.in_stub() { + self.add_declaration_with_binding( + assignment.into(), + definition, + &DeclaredAndInferredType::AreTheSame(declared_ty.inner_type()), + ); + } else { + self.add_declaration(assignment.into(), definition, declared_ty); + } } self.infer_expression(target); diff --git a/crates/ruff_benchmark/benches/red_knot.rs b/crates/ruff_benchmark/benches/red_knot.rs index beed5bd96c..11ec12577c 100644 --- a/crates/ruff_benchmark/benches/red_knot.rs +++ b/crates/ruff_benchmark/benches/red_knot.rs @@ -96,13 +96,6 @@ static EXPECTED_DIAGNOSTICS: &[KeyDiagnosticFields] = &[ Cow::Borrowed("Unused blanket `type: ignore` directive"), Severity::Warning, ), - ( - DiagnosticId::lint("invalid-attribute-access"), - Some("/src/tomllib/__init__.py"), - Some(270..296), - Cow::Borrowed("Cannot assign to instance attribute `__module__` from the class object `Literal[TOMLDecodeError]`"), - Severity::Error, - ), ]; fn tomllib_path(file: &TestFile) -> SystemPathBuf {