Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions rewrite-python/rewrite/src/rewrite/python/type_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,15 @@ def _descriptor_to_java_type(self, descriptor: Dict[str, Any]) -> Optional[JavaT
fqn = f"{module_name}.{class_name}"
else:
fqn = class_name
class_type = self._create_class_type(fqn)

# Create a fresh JavaType.Class per type_id rather than deduplicating
# by FQN. ty-types can emit multiple classLiterals with the same FQN
# (e.g., class Pair(namedtuple('Pair', ...))) and collapsing them
# would cause self-referential supertypes.
class_type = JavaType.Class()
class_type._flags_bit_map = 0
class_type._fully_qualified_name = fqn
class_type._kind = JavaType.FullyQualified.Kind.Class

# Infer Kind from supertypes before resolving them
supertypes = descriptor.get('supertypes', [])
Expand All @@ -468,12 +476,12 @@ def _descriptor_to_java_type(self, descriptor: Dict[str, Any]) -> Optional[JavaT
break

# Populate supertypes: first → _supertype, rest → _interfaces
if supertypes and getattr(class_type, '_supertype', None) is None:
if supertypes:
super_type = self._resolve_type(supertypes[0])
if isinstance(super_type, JavaType.FullyQualified):
class_type._supertype = super_type

if len(supertypes) > 1 and getattr(class_type, '_interfaces', None) is None:
if len(supertypes) > 1:
interfaces = []
for st_id in supertypes[1:]:
iface = self._resolve_type(st_id)
Expand Down
48 changes: 48 additions & 0 deletions rewrite-python/rewrite/tests/python/test_type_attribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -1877,3 +1877,51 @@ def test_declaration_declaring_type_with_ty_types(self):
assert result._declaring_type._fully_qualified_name != "<unknown>"
finally:
_cleanup_mapping(mapping, tmpdir, client)


class TestCyclicTypeResolution:
"""Tests that FQN-deduplicated types don't produce self-referential supertypes.

Python's namedtuple pattern ``class Pair(namedtuple('Pair', ...))`` creates
two types with the same FQN. The type mapping deduplicates by FQN, which can
cause the single JavaType.Class object to have itself as its own supertype.
"""

def test_namedtuple_subclass_no_self_supertype(self):
"""A class extending a namedtuple with the same name must not have
itself as its own supertype.

This is the pattern from importlib_metadata._collections.Pair that
caused StackOverflowError in TypeUtils.isAssignableTo() on Moderne.
"""
source = 'x = 1'
mapping = PythonTypeMapping(source)

# Simulate what ty-types produces for:
# class Pair(collections.namedtuple('Pair', 'name value')): ...
# Two classLiteral descriptors with the same FQN but different type_ids.
# type_id 1: the namedtuple-generated Pair (no supertypes of its own here)
# type_id 2: the class Pair, whose supertype is type_id 1
mapping._type_registry[1] = {
'kind': 'classLiteral',
'className': 'Pair',
'moduleName': 'mymodule',
'supertypes': [],
}
mapping._type_registry[2] = {
'kind': 'classLiteral',
'className': 'Pair',
'moduleName': 'mymodule',
'supertypes': [1],
}

type_pair = mapping._resolve_type(2)

assert type_pair is not None
assert isinstance(type_pair, JavaType.FullyQualified)
assert type_pair.fully_qualified_name == 'mymodule.Pair'

# The supertype must NOT be itself
supertype = getattr(type_pair, '_supertype', None)
assert supertype is not type_pair, \
"Pair has itself as its own supertype — would cause StackOverflowError in Java"