From 67764bf0ddad9a28b60b85215b59ab3908656a51 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Thu, 2 Apr 2026 10:37:37 +0200 Subject: [PATCH] Fix self-referential supertype in Python type mapping for namedtuple pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pattern `class Pair(namedtuple('Pair', ...))` causes ty-types to emit two classLiteral descriptors with the same FQN but different type_ids. The classLiteral handler used _create_class_type() which deduplicates by FQN, collapsing both into the same JavaType.Class object. When the outer class sets its supertype to the inner namedtuple type, it ends up pointing to itself — a self-loop that causes StackOverflowError in Java's TypeUtils.isAssignableTo(). Fix by creating a fresh JavaType.Class per classLiteral type_id instead of deduplicating by FQN. The _type_id_cache in _resolve_type() already handles deduplication by type_id, and ty-types type_ids are the correct source of truth for type identity. --- .../src/rewrite/python/type_mapping.py | 14 ++++-- .../tests/python/test_type_attribution.py | 48 +++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) 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"