diff --git a/rewrite-python/rewrite/src/rewrite/python/type_mapping.py b/rewrite-python/rewrite/src/rewrite/python/type_mapping.py index 55e99ccf0b..949f400aa5 100644 --- a/rewrite-python/rewrite/src/rewrite/python/type_mapping.py +++ b/rewrite-python/rewrite/src/rewrite/python/type_mapping.py @@ -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', []) @@ -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) diff --git a/rewrite-python/rewrite/tests/python/test_type_attribution.py b/rewrite-python/rewrite/tests/python/test_type_attribution.py index 4299476be9..a37b984237 100644 --- a/rewrite-python/rewrite/tests/python/test_type_attribution.py +++ b/rewrite-python/rewrite/tests/python/test_type_attribution.py @@ -1877,3 +1877,51 @@ def test_declaration_declaring_type_with_ty_types(self): assert result._declaring_type._fully_qualified_name != "" 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"