Skip to content

Commit

Permalink
add special case for implicit instance attrribute declaration via par…
Browse files Browse the repository at this point in the history
…am assignment
  • Loading branch information
mishamsk committed Feb 12, 2025
1 parent 6e34f74 commit ecd0261
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 5 deletions.
15 changes: 12 additions & 3 deletions crates/red_knot_python_semantic/resources/mdtest/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,20 @@ class C:
if flag:
self.possibly_undeclared_unbound: str = "possibly set in __init__"

param = param if param is None else param + 42
self.inferred_from_redefined_param = param

def other_method(self, param: str) -> None:
self.inferred_from_param_not_in_init = param

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

# TODO: should be `int | None`
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
reveal_type(c_instance.inferred_from_param) # revealed: int | None

reveal_type(c_instance.declared_only) # revealed: bytes

Expand All @@ -41,13 +46,17 @@ reveal_type(c_instance.declared_and_bound) # revealed: bool
# mypy and pyright do not show an error here.
reveal_type(c_instance.possibly_undeclared_unbound) # revealed: str

reveal_type(c_instance.inferred_from_redefined_param) # revealed: Unknown | None | int

reveal_type(c_instance.inferred_from_param_not_in_init) # revealed: Unknown | str

# This assignment is fine, as we infer `Unknown | Literal[1, "a"]` for `inferred_from_value`.
c_instance.inferred_from_value = "value set on instance"

# This assignment is also fine:
c_instance.inferred_from_param = None

# TODO: this should be an error (incompatible types in assignment)
# error: [invalid-assignment] "Object of type `Literal["incompatible"]` is not assignable to attribute `inferred_from_param` of type `int | None`"
c_instance.inferred_from_param = "incompatible"

# TODO: we already show an error here but the message might be improved?
Expand Down
44 changes: 42 additions & 2 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ pub(crate) use self::signatures::Signature;
pub use self::subclass_of::SubclassOfType;
use crate::module_name::ModuleName;
use crate::module_resolver::{file_to_module, resolve_module, KnownModule};
use crate::semantic_index::ast_ids::HasScopedExpressionId;
use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId};
use crate::semantic_index::attribute_assignment::AttributeAssignment;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::definition::{Definition, DefinitionKind};
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{self as symbol, ScopeId, ScopedSymbolId};
use crate::semantic_index::{
Expand Down Expand Up @@ -4240,6 +4240,46 @@ impl<'db> Class<'db> {
//
// self.name = <value>

// Check for a special case - unannotated assignments in `__init__` method
// that assign a method param with declared type. E.g.:
// ```python
// class A:
// def __init__(self, name: str):
// self.name = name
// ```
// In this case we infer attribute type as if it had been declared with
// the type of the value assigned to it, without union with Unknown.
let value_expr_node = value.node_ref(db).node();

if let ast::Expr::Name(name_expr) = value_expr_node {
let expr_scope_id = value.scope(db);

let use_def_map = use_def_map(db, expr_scope_id);

// Check that the last (and implicitly only) reachable binding of the name
// is in the function definition (parameter declaration).
let bindings =
use_def_map.bindings_at_use(name_expr.scoped_use_id(db, expr_scope_id));

if bindings.last().is_some_and(|binding| {
binding.binding.is_some_and(|definition| {
matches!(definition.kind(db), DefinitionKind::Parameter(_))
&& definition.category(db).is_declaration()
})
}) {
if let Some(ast::StmtFunctionDef { name, .. }) =
expr_scope_id.node(db).as_function()
{
if name.as_str() == "__init__" {
let annotation_ty = infer_expression_type(db, *value);

// TODO: check if there are conflicting declarations
return Symbol::bound(annotation_ty);
}
};
}
}

let inferred_ty = infer_expression_type(db, *value);

union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
Expand Down

0 comments on commit ecd0261

Please sign in to comment.