Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9e5555e

Browse files
[codex] Simplify type-value normalization (#454)
## Summary - make `Value.get_type_value()` context-aware so type-value computation can reuse normal simplification rules - simplify `IntersectionValue.get_type_value()` via `intersect_multi()` and `MultiValuedValue.get_type_value()` via `unite_values()` - model predicate type-values as `type[object]` and add regression tests for `hasattr()`-guarded constructor calls ## Why Taxonomy hit confusing constructor overloads after `hasattr()` narrowing because `type(tag)` preserved predicate-derived fallback class branches instead of simplifying back to the real class object. ## Root cause `get_type_value()` was context-free and `IntersectionValue.get_type_value()` just wrapped member type-values in a new intersection without normalizing them. That left callable fallback members in place long enough for constructor signature collection to turn them into spurious overloads like `object.__init__`. ## Testing - `UV_CACHE_DIR=/tmp/uv-cache uv run --python 3.14 --extra tests pytest pycroscope/test_value.py pycroscope/test_constructors.py`
1 parent 94250aa commit 9e5555e

7 files changed

Lines changed: 76 additions & 34 deletions

File tree

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Simplify `type()` lookups through narrowed intersections, so `hasattr()`-guarded class-object calls preserve the real constructor signature instead of picking up confusing fallback overloads like `object.__init__`.
56
- Fix assignability involving weak list literals in nested invariant containers, so values like `defaultdict(lambda: ([], []))` can satisfy annotated tuple-valued `dict` entries instead of failing on the inner empty-list literals.
67
- Improve nested tuple mismatch diagnostics so assignments involving tuple-valued generics now report which tuple element failed instead of the vague message `tuple is not assignable to tuple`.
78
- Add an opt-in `--find-unused-call-patterns` check, plus `enforce_no_unused_call_patterns`, to report parameters whose accepted call shapes are only partially exercised, including always-omitted defaults, one-sided boolean flags, and unused union members.

pycroscope/implementation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2212,7 +2212,7 @@ def _subclasses_impl(ctx: CallContext) -> Value:
22122212

22132213

22142214
def _type_impl(ctx: CallContext) -> Value:
2215-
return ctx.vars["object"].get_type_value()
2215+
return ctx.vars["object"].get_type_value(ctx.visitor)
22162216

22172217

22182218
def _assert_is_impl(ctx: CallContext) -> ImplReturn:

pycroscope/name_check_visitor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13675,7 +13675,7 @@ def _get_dunder(
1367513675
elif isinstance(callee_val, KnownValueWithTypeVars):
1367613676
fallback_lookup_val = callee_val
1367713677
else:
13678-
fallback_lookup_val = callee_val.get_type_value()
13678+
fallback_lookup_val = callee_val.get_type_value(self)
1367913679
if isinstance(synthetic_lookup_val, TypedValue) and isinstance(
1368013680
synthetic_lookup_val.typ, str
1368113681
):

pycroscope/test_constructors.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
11
# static analysis: ignore
22

3+
from .error_code import ErrorCode
34
from .test_name_check_visitor import TestNameCheckVisitorBase
45
from .test_node_visitor import assert_passes
56

67

78
class TestConstructors(TestNameCheckVisitorBase):
9+
def test_hasattr_constructor_does_not_add_object_overload(self):
10+
errors = self._run_str(
11+
"""
12+
class C:
13+
def __init__(self, x: int) -> None:
14+
self.x = x
15+
16+
def f(c: C) -> None:
17+
if hasattr(c, "name"):
18+
type(c)(name=1)
19+
""",
20+
expect_failure=False,
21+
fail_after_first=False,
22+
)
23+
assert len(errors) == 1
24+
error = errors[0]
25+
assert error["code"] == ErrorCode.incompatible_call
26+
assert "object.__init__" not in error["message"]
27+
assert "C.__init__" in error["message"]
28+
829
@assert_passes()
930
def test_metaclass_call(self):
1031
from typing import Type

pycroscope/test_value.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,22 @@ def test_predicate_intersection() -> None:
964964
assert_can_assign(pred, inters)
965965

966966

967+
def test_predicate_get_type_value_uses_subclass_object() -> None:
968+
pred = value.PredicateValue(MinLen(1))
969+
assert pred.get_type_value(CTX) == SubclassValue(TypedValue(object))
970+
971+
972+
def test_intersection_get_type_value_simplifies() -> None:
973+
pred = value.PredicateValue(MinLen(1))
974+
inters = IntersectionValue((TypedValue(str), pred))
975+
assert inters.get_type_value(CTX) == KnownValue(str)
976+
977+
978+
def test_union_get_type_value_uses_unite_values() -> None:
979+
union = MultiValuedValue([KnownValue(True), KnownValue(False)])
980+
assert union.get_type_value(CTX) == KnownValue(bool)
981+
982+
967983
def test_overlapping_value_intersection_simplifies() -> None:
968984
overlapping_int = OverlappingValue(TypedValue(int))
969985

pycroscope/type_object.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3983,13 +3983,13 @@ def _protocol_classmethod_receiver_value(
39833983
receiver_type = _receiver_type_value(receiver_value, ctx)
39843984
if receiver_type is not None:
39853985
return SubclassValue.make(receiver_type)
3986-
return SubclassValue.make(receiver_value.get_type_value())
3986+
return SubclassValue.make(receiver_value.get_type_value(ctx))
39873987
receiver_key = _class_key_from_value(receiver_value)
39883988
if receiver_key is not None:
39893989
class_object = _class_object_value_for_key(receiver_key, ctx)
39903990
if class_object is not None:
39913991
return class_object
3992-
return SubclassValue.make(receiver_value.get_type_value())
3992+
return SubclassValue.make(receiver_value.get_type_value(ctx))
39933993

39943994

39953995
def _merge_protocol_receiver_typevars(

pycroscope/value.py

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ def get_fallback_value(self) -> Optional["Value"]:
643643
"""
644644
return None
645645

646-
def get_type_value(self) -> "Value":
646+
def get_type_value(self, ctx: "CanAssignContext") -> "Value":
647647
"""Return the type of this object as used for dunder lookups."""
648648
return self
649649

@@ -975,8 +975,8 @@ def __str__(self) -> str:
975975
def get_fallback_value(self) -> Value:
976976
return self.runtime_value
977977

978-
def get_type_value(self) -> Value:
979-
return self.runtime_value.get_type_value()
978+
def get_type_value(self, ctx: CanAssignContext) -> Value:
979+
return self.runtime_value.get_type_value(ctx)
980980

981981
def substitute_typevars(self, typevars: TypeVarMap) -> "PartialValue":
982982
return PartialValue(
@@ -1018,8 +1018,8 @@ def __str__(self) -> str:
10181018
def get_fallback_value(self) -> Value:
10191019
return self.runtime_value
10201020

1021-
def get_type_value(self) -> Value:
1022-
return self.runtime_value.get_type_value()
1021+
def get_type_value(self, ctx: CanAssignContext) -> Value:
1022+
return self.runtime_value.get_type_value(ctx)
10231023

10241024
def substitute_typevars(self, typevars: TypeVarMap) -> "PartialCallValue":
10251025
return PartialCallValue(
@@ -1057,8 +1057,8 @@ def __str__(self) -> str:
10571057
def get_fallback_value(self) -> Value:
10581058
return TypedValue(super)
10591059

1060-
def get_type_value(self) -> Value:
1061-
return self.get_fallback_value().get_type_value()
1060+
def get_type_value(self, ctx: CanAssignContext) -> Value:
1061+
return self.get_fallback_value().get_type_value(ctx)
10621062

10631063
def substitute_typevars(self, typevars: TypeVarMap) -> "SuperValue":
10641064
return SuperValue(
@@ -1564,8 +1564,8 @@ def is_type(self, typ: type) -> bool:
15641564
def get_type(self) -> type | None:
15651565
return self.get_value().get_type()
15661566

1567-
def get_type_value(self) -> Value:
1568-
return self.get_value().get_type_value()
1567+
def get_type_value(self, ctx: CanAssignContext) -> Value:
1568+
return self.get_value().get_type_value(ctx)
15691569

15701570
def can_overlap(
15711571
self, other: Value, ctx: CanAssignContext, mode: OverlapMode
@@ -1631,7 +1631,7 @@ def get_type_object(
16311631
) -> "pycroscope.type_object.TypeObject":
16321632
return ctx.make_type_object(type(self.val))
16331633

1634-
def get_type_value(self) -> Value:
1634+
def get_type_value(self, ctx: CanAssignContext) -> Value:
16351635
return KnownValue(type(self.val))
16361636

16371637
def can_overlap(
@@ -1813,7 +1813,7 @@ def is_type(self, typ: type) -> bool:
18131813
def get_type(self) -> type:
18141814
return type(self.get_method())
18151815

1816-
def get_type_value(self) -> Value:
1816+
def get_type_value(self, ctx: CanAssignContext) -> Value:
18171817
return KnownValue(type(self.get_method()))
18181818

18191819
def can_overlap(
@@ -2043,7 +2043,7 @@ def get_type(self) -> type | None:
20432043
return None
20442044
return self.typ
20452045

2046-
def get_type_value(self) -> Value:
2046+
def get_type_value(self, ctx: CanAssignContext) -> Value:
20472047
if isinstance(self.typ, str):
20482048
return AnyValue(AnySource.inference)
20492049
return KnownValue(self.typ)
@@ -2102,8 +2102,8 @@ def can_overlap(
21022102
return CanAssignError(f"NewTypes {self} and {other} cannot overlap")
21032103
return super().can_overlap(other, ctx, mode)
21042104

2105-
def get_type_value(self) -> Value:
2106-
return self.value.get_type_value()
2105+
def get_type_value(self, ctx: CanAssignContext) -> Value:
2106+
return self.value.get_type_value(ctx)
21072107

21082108
def get_fallback_value(self) -> Value:
21092109
return self.value
@@ -2596,7 +2596,7 @@ def walk_values(self) -> Iterable["Value"]:
25962596
yield self
25972597
yield from self.class_type.walk_values()
25982598

2599-
def get_type_value(self) -> Value:
2599+
def get_type_value(self, ctx: CanAssignContext) -> Value:
26002600
if isinstance(self.class_type.typ, type):
26012601
return KnownValue(type(self.class_type.typ))
26022602
return TypedValue(type)
@@ -2782,7 +2782,7 @@ def get_type(self) -> type | None:
27822782
else:
27832783
return None
27842784

2785-
def get_type_value(self) -> Value:
2785+
def get_type_value(self, ctx: CanAssignContext) -> Value:
27862786
typ = self.get_type()
27872787
if typ is not None:
27882788
return KnownValue(typ)
@@ -2837,8 +2837,10 @@ def walk_values(self) -> Iterable[Value]:
28372837
for val in self.vals:
28382838
yield from val.walk_values()
28392839

2840-
def get_type_value(self) -> Value:
2841-
return IntersectionValue(tuple(val.get_type_value() for val in self.vals))
2840+
def get_type_value(self, ctx: CanAssignContext) -> Value:
2841+
return pycroscope.relations.intersect_multi(
2842+
[val.get_type_value(ctx) for val in self.vals], ctx
2843+
)
28422844

28432845
def __str__(self) -> str:
28442846
return " & ".join(str(val) for val in self.vals)
@@ -2860,8 +2862,8 @@ def walk_values(self) -> Iterable[Value]:
28602862
def get_fallback_value(self) -> Value:
28612863
return TypedValue(object)
28622864

2863-
def get_type_value(self) -> Value:
2864-
return self.get_fallback_value().get_type_value()
2865+
def get_type_value(self, ctx: CanAssignContext) -> Value:
2866+
return self.get_fallback_value().get_type_value(ctx)
28652867

28662868
def __str__(self) -> str:
28672869
return f"Overlapping[{self.type}]"
@@ -2902,10 +2904,10 @@ def can_overlap(
29022904
errors.append(error)
29032905
return CanAssignError("Cannot overlap with Union", errors)
29042906

2905-
def get_type_value(self) -> Value:
2907+
def get_type_value(self, ctx: CanAssignContext) -> Value:
29062908
if not self.vals:
29072909
return self
2908-
return MultiValuedValue([val.get_type_value() for val in self.vals])
2910+
return unite_values(*[val.get_type_value(ctx) for val in self.vals])
29092911

29102912
def decompose(self) -> Iterable[Value]:
29112913
return self.vals
@@ -3061,8 +3063,8 @@ def get_fallback_value(self) -> Value:
30613063
return unite_values(*self.typevar_param.constraints)
30623064
return AnyValue(AnySource.inference) # TODO: should be object
30633065

3064-
def get_type_value(self) -> Value:
3065-
return self.get_fallback_value().get_type_value()
3066+
def get_type_value(self, ctx: CanAssignContext) -> Value:
3067+
return self.get_fallback_value().get_type_value(ctx)
30663068

30673069
def __str__(self) -> str:
30683070
return str(self.typevar_param)
@@ -3106,8 +3108,8 @@ def can_overlap(
31063108
def get_fallback_value(self) -> Value:
31073109
return AnyValue(AnySource.inference)
31083110

3109-
def get_type_value(self) -> Value:
3110-
return self.get_fallback_value().get_type_value()
3111+
def get_type_value(self, ctx: CanAssignContext) -> Value:
3112+
return self.get_fallback_value().get_type_value(ctx)
31113113

31123114
def __str__(self) -> str:
31133115
return str(self.typevar)
@@ -3724,8 +3726,8 @@ def is_type(self, typ: type) -> bool:
37243726
def get_type(self) -> type | None:
37253727
return self.value.get_type()
37263728

3727-
def get_type_value(self) -> Value:
3728-
return self.value.get_type_value()
3729+
def get_type_value(self, ctx: CanAssignContext) -> Value:
3730+
return self.value.get_type_value(ctx)
37293731

37303732
def substitute_typevars(self, typevars: TypeVarMap) -> Value:
37313733
metadata = tuple(val.substitute_typevars(typevars) for val in self.metadata)
@@ -3881,8 +3883,10 @@ def __str__(self) -> str:
38813883
def substitute_typevars(self, typevars: TypeVarMap) -> Value:
38823884
return PredicateValue(self.predicate.substitute_typevars(typevars))
38833885

3884-
def get_type_value(self) -> Value:
3885-
return KnownValue(object)
3886+
def get_type_value(self, ctx: CanAssignContext) -> Value:
3887+
# A predicate only preserves that the runtime class is some subclass of
3888+
# `object`, not an exact class object.
3889+
return SubclassValue(TypedValue(object))
38863890

38873891
def walk_values(self) -> Iterable[Value]:
38883892
yield self

0 commit comments

Comments
 (0)