|
9 | 9 |
|
10 | 10 | import collections.abc
|
11 | 11 | import enum
|
| 12 | +import struct |
| 13 | +import sys |
12 | 14 | from collections.abc import Iterable, Iterator, Sequence
|
13 | 15 | from dataclasses import dataclass, replace
|
14 | 16 | from types import FunctionType
|
|
19 | 21 | import pycroscope
|
20 | 22 | from pycroscope.analysis_lib import Sentinel
|
21 | 23 | from pycroscope.find_unused import used
|
22 |
| -from pycroscope.safe import safe_equals, safe_isinstance |
| 24 | +from pycroscope.safe import safe_equals, safe_isinstance, safe_issubclass |
23 | 25 | from pycroscope.typevar import resolve_bounds_map
|
24 | 26 | from pycroscope.value import (
|
25 | 27 | NO_RETURN_VALUE,
|
@@ -1374,10 +1376,96 @@ def _intersect_typed(
|
1374 | 1376 | ) -> TypeOrIrreducible:
|
1375 | 1377 | if isinstance(left, TypedDictValue) and isinstance(right, TypedDictValue):
|
1376 | 1378 | return _intersect_typeddict(left, right, ctx)
|
1377 |
| - # TODO: Consider more options |
| 1379 | + |
| 1380 | + # If either type is final, the intersection reduces to Never unless the final class |
| 1381 | + # is a subclass of the other class. In that case, we treat the intersection as irreducible. |
| 1382 | + # Note that we would have already reduced intersections where the *types* are subtypes; |
| 1383 | + # the irreducible case can happen in certain situations involving generics. |
| 1384 | + left_tobj = left.get_type_object(ctx) |
| 1385 | + right_tobj = right.get_type_object(ctx) |
| 1386 | + if left_tobj.is_final and not left_tobj.is_assignable_to_type_object(right_tobj): |
| 1387 | + return NO_RETURN_VALUE |
| 1388 | + if right_tobj.is_final and not right_tobj.is_assignable_to_type_object(left_tobj): |
| 1389 | + return NO_RETURN_VALUE |
| 1390 | + |
| 1391 | + # If both types are nominal and are real (non-synthetic) types, we can mirror CPython's logic |
| 1392 | + # to check whether a child class can exist at runtime. |
| 1393 | + if ( |
| 1394 | + not left_tobj.is_protocol |
| 1395 | + and not right_tobj.is_protocol |
| 1396 | + and isinstance(left_tobj.typ, type) |
| 1397 | + and isinstance(right_tobj.typ, type) |
| 1398 | + and not _can_nominal_types_intersect(left_tobj.typ, right_tobj.typ) |
| 1399 | + ): |
| 1400 | + return NO_RETURN_VALUE |
| 1401 | + |
| 1402 | + # TODO: Consider more options for reducing the intersection: |
| 1403 | + # - Certain cases involving incompatible metaclasses can return to Never |
| 1404 | + # - Possibly some cases with attributes of incompatible types (possibly debatable) |
| 1405 | + # - Cases involving SequenceValue, DictIncompleteValue, and other concrete subclasses of |
| 1406 | + # TypedValue |
1378 | 1407 | return Irreducible
|
1379 | 1408 |
|
1380 | 1409 |
|
| 1410 | +def _can_nominal_types_intersect(t1: type[object], t2: type[object]) -> bool: |
| 1411 | + sb1 = _solid_base(t1) |
| 1412 | + sb2 = _solid_base(t2) |
| 1413 | + return safe_issubclass(sb1, sb2) or safe_issubclass(sb2, sb1) |
| 1414 | + |
| 1415 | + |
| 1416 | +SIZEOF_PYOBJECT = struct.calcsize("P") |
| 1417 | + |
| 1418 | + |
| 1419 | +def _shape_differs(t1: type[object], t2: type[object]) -> bool: |
| 1420 | + """Check whether two types differ in shape. |
| 1421 | +
|
| 1422 | + Mirrors the shape_differs() function in typeobject.c in CPython.""" |
| 1423 | + if sys.version_info >= (3, 12): |
| 1424 | + return ( |
| 1425 | + t1.__basicsize__ != t2.__basicsize__ or t1.__itemsize__ != t2.__itemsize__ |
| 1426 | + ) |
| 1427 | + else: |
| 1428 | + # CPython had more complicated logic before 3.12: |
| 1429 | + # https://github.com/python/cpython/blob/f3c6f882cddc8dc30320d2e73edf019e201394fc/Objects/typeobject.c#L2224 |
| 1430 | + # We attempt to mirror it here well enough to support the most common cases. |
| 1431 | + if t1.__itemsize__ or t2.__itemsize__: |
| 1432 | + return ( |
| 1433 | + t1.__basicsize__ != t2.__basicsize__ |
| 1434 | + or t1.__itemsize__ != t2.__itemsize__ |
| 1435 | + ) |
| 1436 | + t_size = t1.__basicsize__ |
| 1437 | + if ( |
| 1438 | + # TODO: better understanding of attributes of type objects |
| 1439 | + not t2.__weakrefoffset__ # static analysis: ignore[value_always_true] |
| 1440 | + and t1.__weakrefoffset__ + SIZEOF_PYOBJECT == t_size |
| 1441 | + ): |
| 1442 | + t_size -= SIZEOF_PYOBJECT |
| 1443 | + if ( |
| 1444 | + not t2.__dictoffset__ # static analysis: ignore[value_always_true] |
| 1445 | + and t1.__dictoffset__ + SIZEOF_PYOBJECT == t_size |
| 1446 | + ): |
| 1447 | + t_size -= SIZEOF_PYOBJECT |
| 1448 | + if ( |
| 1449 | + not t2.__weakrefoffset__ # static analysis: ignore[value_always_true] |
| 1450 | + and t2.__weakrefoffset__ == t_size |
| 1451 | + ): |
| 1452 | + t_size -= SIZEOF_PYOBJECT |
| 1453 | + return t_size != t2.__basicsize__ |
| 1454 | + |
| 1455 | + |
| 1456 | +def _solid_base(typ: type[object]) -> type[object]: |
| 1457 | + """Return the "solid base" of a type, mirroring the logic in CPython's typeobject.c. |
| 1458 | +
|
| 1459 | + A "solid base" is a base that determines the shape of the type.""" |
| 1460 | + if typ is object: |
| 1461 | + return object |
| 1462 | + base = typ.__base__ |
| 1463 | + assert base is not None, f"Type {typ} has no base" |
| 1464 | + if _shape_differs(typ, base): |
| 1465 | + return typ |
| 1466 | + return _solid_base(base) |
| 1467 | + |
| 1468 | + |
1381 | 1469 | def _intersect_maybe_mutable(
|
1382 | 1470 | left: Value,
|
1383 | 1471 | left_readonly: bool,
|
|
0 commit comments