From 2e84dc0c371a49079bc8553f76dfb6ab70c8cefa Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:50:40 +0000 Subject: [PATCH] Fix infinite recursion in example generation for circular type references Add cycle detection to prevent infinite recursion when generating examples for types with circular references in both Python and TypeScript generators. Python changes: - Add RecursionGuard utility class to track visited types and enforce depth limits - Thread recursion_guard parameter through SnippetWriter and all snippet generators - Return None/undefined when cycles are detected (filtered by existing code) TypeScript changes: - Add RecursionGuard utility class with similar functionality - Update GeneratedTypeReferenceExampleImpl to check for cycles before recursing - Thread recursion_guard through all buildExample implementations - Return undefined for cyclic values (filtered by existing code) This fixes infinite recursion bugs in both Python and TypeScript SDK generation when types have circular references. Co-Authored-By: William McAdams --- .../abc/abstract_type_snippet_generator.py | 7 ++- .../alias_generator.py | 8 ++- .../enum_generator.py | 7 ++- .../pydantic_model_object_generator.py | 8 ++- ...e_declaration_snippet_generator_builder.py | 43 ++++++++-------- .../typeddicts/typeddict_object_generator.py | 9 ++-- .../undiscriminated_union_generator.py | 8 ++- .../generators/pydantic_model/typeddict.py | 11 ++-- .../fern_python/snippet/recursion_guard.py | 50 +++++++++++++++++++ .../src/fern_python/snippet/snippet_writer.py | 30 +++++++++++ .../type_declaration_snippet_generator.py | 25 ++++++---- .../src/AbstractGeneratedType.ts | 3 +- .../src/alias/GeneratedAliasTypeImpl.ts | 2 +- .../alias/GeneratedBrandedStringAliasImpl.ts | 2 +- .../src/enum/GeneratedEnumTypeImpl.ts | 2 +- .../src/object/GeneratedObjectTypeImpl.ts | 2 +- .../GeneratedUndiscriminatedUnionTypeImpl.ts | 2 +- .../src/union/GeneratedUnionTypeImpl.ts | 2 +- .../src/GeneratedTypeReferenceExampleImpl.ts | 43 ++++++++++------ .../src/RecursionGuard.ts | 36 +++++++++++++ .../src/index.ts | 1 + .../base-context/type/BaseGeneratedType.ts | 3 +- 22 files changed, 235 insertions(+), 69 deletions(-) create mode 100644 generators/python/src/fern_python/snippet/recursion_guard.py create mode 100644 generators/typescript/model/type-reference-example-generator/src/RecursionGuard.ts diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/abc/abstract_type_snippet_generator.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/abc/abstract_type_snippet_generator.py index f7dc1a833bab..d1831684a2bf 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/abc/abstract_type_snippet_generator.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/abc/abstract_type_snippet_generator.py @@ -1,9 +1,12 @@ from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, TYPE_CHECKING from fern_python.codegen import AST from fern_python.snippet import SnippetWriter +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + class AbstractTypeSnippetGenerator(ABC): def __init__( @@ -13,4 +16,4 @@ def __init__( self.snippet_writer = snippet_writer @abstractmethod - def generate_snippet(self) -> Optional[AST.Expression]: ... + def generate_snippet(self, recursion_guard: Optional["RecursionGuard"] = None) -> Optional[AST.Expression]: ... diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/alias_generator.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/alias_generator.py index 5df7068f4cd3..ee14cf9bdd43 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/alias_generator.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/alias_generator.py @@ -1,5 +1,5 @@ from abc import ABC -from typing import Optional +from typing import Optional, TYPE_CHECKING from ...context.pydantic_generator_context import PydanticGeneratorContext from ..custom_config import PydanticModelCustomConfig @@ -12,6 +12,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + class AbstractAliasGenerator(AbstractTypeGenerator, ABC): def __init__( @@ -52,9 +55,10 @@ def __init__( self.as_request = as_request self.example = example - def generate_snippet(self) -> Optional[AST.Expression]: + def generate_snippet(self, recursion_guard: Optional["RecursionGuard"] = None) -> Optional[AST.Expression]: return self.snippet_writer.get_snippet_for_example_type_reference( example_type_reference=self.example.value, use_typeddict_request=self.use_typeddict_request, as_request=self.as_request, + recursion_guard=recursion_guard, ) diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/enum_generator.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/enum_generator.py index bdf5991b17fb..9afa6fe11323 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/enum_generator.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/enum_generator.py @@ -1,4 +1,4 @@ -from typing import Optional, Union +from typing import Optional, Union, TYPE_CHECKING from ...context.pydantic_generator_context import PydanticGeneratorContext from ..custom_config import PydanticModelCustomConfig @@ -12,6 +12,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + # Note enums are the same for both pydantic models and typeddicts os the generator is not multiplexed class EnumGenerator(AbstractTypeGenerator): @@ -166,7 +169,7 @@ def __init__( self.name = name self.example = example.value if isinstance(example, ir_types.ExampleEnumType) else example - def generate_snippet(self) -> AST.Expression: + def generate_snippet(self, recursion_guard: Optional["RecursionGuard"] = None) -> AST.Expression: class_reference = self.snippet_writer.get_class_reference_for_declared_type_name( name=self.name, as_request=False, diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/pydantic_models/pydantic_model_object_generator.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/pydantic_models/pydantic_model_object_generator.py index f8717f96b8f8..3598ca3b0ce6 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/pydantic_models/pydantic_model_object_generator.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/pydantic_models/pydantic_model_object_generator.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, TYPE_CHECKING from ....context.pydantic_generator_context import PydanticGeneratorContext from ...custom_config import PydanticModelCustomConfig @@ -13,6 +13,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + class PydanticModelObjectGenerator(AbstractObjectGenerator): def __init__( @@ -97,7 +100,7 @@ def __init__( example=example, ) - def generate_snippet(self) -> AST.Expression: + def generate_snippet(self, recursion_guard: Optional["RecursionGuard"] = None) -> AST.Expression: return AST.Expression( AST.ClassInstantiation( class_=self.snippet_writer.get_class_reference_for_declared_type_name( @@ -110,6 +113,7 @@ def generate_snippet(self) -> AST.Expression: use_typeddict_request=False, as_request=False, in_typeddict=False, + recursion_guard=recursion_guard, ), ), ) diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/type_declaration_snippet_generator_builder.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/type_declaration_snippet_generator_builder.py index e91d63a9ed72..da34cda26f3b 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/type_declaration_snippet_generator_builder.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/type_declaration_snippet_generator_builder.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, TYPE_CHECKING from ...context.pydantic_generator_context import PydanticGeneratorContext from .enum_generator import EnumSnippetGenerator @@ -27,6 +27,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + class TypeDeclarationSnippetGeneratorBuilder: def __init__( @@ -41,50 +44,50 @@ def get_generator( self, ) -> TypeDeclarationSnippetGenerator: return TypeDeclarationSnippetGenerator( - alias=lambda example: self._get_alias_snippet_generator(example), - enum=lambda name, example: EnumSnippetGenerator( + alias=lambda example, recursion_guard=None: self._get_alias_snippet_generator(example, recursion_guard), + enum=lambda name, example, recursion_guard=None: EnumSnippetGenerator( snippet_writer=self._snippet_writer, name=name, example=example, use_str_enums=self._context.use_str_enums, - ).generate_snippet(), - object=lambda name, example: self._get_object_snippet_generator(name, example), - discriminated_union=lambda name, example: self._get_discriminated_union_snippet_generator(name, example), - undiscriminated_union=lambda name, example: self._get_undiscriminated_union_snippet_generator( - name, example + ).generate_snippet(recursion_guard), + object=lambda name, example, recursion_guard=None: self._get_object_snippet_generator(name, example, recursion_guard), + discriminated_union=lambda name, example, recursion_guard=None: self._get_discriminated_union_snippet_generator(name, example, recursion_guard), + undiscriminated_union=lambda name, example, recursion_guard=None: self._get_undiscriminated_union_snippet_generator( + name, example, recursion_guard ), ) - def _get_alias_snippet_generator(self, example: ir_types.ExampleAliasType) -> Optional[AST.Expression]: + def _get_alias_snippet_generator(self, example: ir_types.ExampleAliasType, recursion_guard: Optional["RecursionGuard"] = None) -> Optional[AST.Expression]: if self._context.use_typeddict_requests: return TypedDictAliasSnippetGenerator( snippet_writer=self._snippet_writer, example=example, - ).generate_snippet() + ).generate_snippet(recursion_guard) return PydanticModelAliasSnippetGenerator( snippet_writer=self._snippet_writer, example=example, - ).generate_snippet() + ).generate_snippet(recursion_guard) def _get_object_snippet_generator( - self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleObjectType + self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleObjectType, recursion_guard: Optional["RecursionGuard"] = None ) -> AST.Expression: if self._context.use_typeddict_requests: return TypeddictObjectSnippetGenerator( name=name, snippet_writer=self._snippet_writer, example=example, - ).generate_snippet() + ).generate_snippet(recursion_guard) return PydanticModelObjectSnippetGenerator( name=name, snippet_writer=self._snippet_writer, example=example, - ).generate_snippet() + ).generate_snippet(recursion_guard) def _get_discriminated_union_snippet_generator( - self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleUnionType + self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleUnionType, recursion_guard: Optional["RecursionGuard"] = None ) -> AST.Expression: if self._context.use_typeddict_requests: return TypeddictDiscriminatedUnionSnippetGenerator( @@ -92,27 +95,27 @@ def _get_discriminated_union_snippet_generator( snippet_writer=self._snippet_writer, example=example, union_naming_version=self._context.union_naming_version, - ).generate_snippet() + ).generate_snippet(recursion_guard) return PydanticModelDiscriminatedUnionSnippetGenerator( name=name, snippet_writer=self._snippet_writer, example=example, union_naming_version=self._context.union_naming_version, - ).generate_snippet() + ).generate_snippet(recursion_guard) def _get_undiscriminated_union_snippet_generator( - self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleUndiscriminatedUnionType + self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleUndiscriminatedUnionType, recursion_guard: Optional["RecursionGuard"] = None ) -> Optional[AST.Expression]: if self._context.use_typeddict_requests: return TypeddictUndiscriminatedUnionSnippetGenerator( name=name, snippet_writer=self._snippet_writer, example=example, - ).generate_snippet() + ).generate_snippet(recursion_guard) return PydanticModelUndiscriminatedUnionSnippetGenerator( name=name, snippet_writer=self._snippet_writer, example=example, - ).generate_snippet() + ).generate_snippet(recursion_guard) diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/typeddicts/typeddict_object_generator.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/typeddicts/typeddict_object_generator.py index 9fd383cb7f70..faccb28ead43 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/typeddicts/typeddict_object_generator.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/typeddicts/typeddict_object_generator.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, TYPE_CHECKING from ....context.pydantic_generator_context import PydanticGeneratorContext from ...custom_config import PydanticModelCustomConfig @@ -13,6 +13,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + class TypeddictObjectGenerator(AbstractObjectGenerator): def __init__( @@ -72,5 +75,5 @@ def __init__( example=example, ) - def generate_snippet(self) -> AST.Expression: - return FernTypedDict.type_to_snippet(example=self.example, snippet_writer=self.snippet_writer) + def generate_snippet(self, recursion_guard: Optional["RecursionGuard"] = None) -> AST.Expression: + return FernTypedDict.type_to_snippet(example=self.example, snippet_writer=self.snippet_writer, recursion_guard=recursion_guard) diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/undiscriminated_union_generator.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/undiscriminated_union_generator.py index 6ed13b0d7ff9..aa3f7e88a91a 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/undiscriminated_union_generator.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/undiscriminated_union_generator.py @@ -1,6 +1,6 @@ from abc import ABC from dataclasses import dataclass -from typing import List, Optional +from typing import List, Optional, TYPE_CHECKING from ...context.pydantic_generator_context import PydanticGeneratorContext from ..custom_config import PydanticModelCustomConfig @@ -13,6 +13,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + @dataclass(frozen=True) class CycleAwareMemberType: @@ -71,9 +74,10 @@ def __init__( self.as_request = as_request self.use_typeddict_request = use_typeddict_request - def generate_snippet(self) -> Optional[AST.Expression]: + def generate_snippet(self, recursion_guard: Optional["RecursionGuard"] = None) -> Optional[AST.Expression]: return self.snippet_writer.get_snippet_for_example_type_reference( example_type_reference=self.example.single_union_type, use_typeddict_request=self.use_typeddict_request, as_request=self.as_request, + recursion_guard=recursion_guard, ) diff --git a/generators/python/src/fern_python/generators/pydantic_model/typeddict.py b/generators/python/src/fern_python/generators/pydantic_model/typeddict.py index 2565176f103a..214086aed2aa 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/typeddict.py +++ b/generators/python/src/fern_python/generators/pydantic_model/typeddict.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from types import TracebackType -from typing import Dict, List, Optional, Sequence, Type +from typing import Dict, List, Optional, Sequence, Type, TYPE_CHECKING from ..context.pydantic_generator_context import PydanticGeneratorContext from fern_python.codegen import AST, SourceFile @@ -16,6 +16,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + TYPING_EXTENSIONS_MODULE = AST.Module.external( module_path=("typing_extensions",), dependency=TYPING_EXTENSIONS_DEPENDENCY, @@ -203,7 +206,7 @@ def wrap_string_as_example(cls, string: str) -> ir_types.ExampleTypeReference: @classmethod def snippet_from_properties( - cls, example_properties: List[SimpleObjectProperty], snippet_writer: SnippetWriter + cls, example_properties: List[SimpleObjectProperty], snippet_writer: SnippetWriter, recursion_guard: Optional["RecursionGuard"] = None ) -> AST.Expression: example_dict_pairs: List[ir_types.ExampleKeyValuePair] = [] for property in example_properties: @@ -219,7 +222,7 @@ def snippet_from_properties( ) ) return snippet_writer._get_snippet_for_map( - example_dict_pairs, use_typeddict_request=True, as_request=True, in_typeddict=True + example_dict_pairs, use_typeddict_request=True, as_request=True, in_typeddict=True, recursion_guard=recursion_guard ) @classmethod @@ -228,6 +231,7 @@ def type_to_snippet( example: ir_types.ExampleObjectType, snippet_writer: SnippetWriter, additional_properties: List[SimpleObjectProperty] = [], + recursion_guard: Optional["RecursionGuard"] = None, ) -> AST.Expression: example_properties = [ SimpleObjectProperty( @@ -240,6 +244,7 @@ def type_to_snippet( return cls.snippet_from_properties( example_properties=example_properties, snippet_writer=snippet_writer, + recursion_guard=recursion_guard, ) @classmethod diff --git a/generators/python/src/fern_python/snippet/recursion_guard.py b/generators/python/src/fern_python/snippet/recursion_guard.py new file mode 100644 index 000000000000..41c9de12b5ee --- /dev/null +++ b/generators/python/src/fern_python/snippet/recursion_guard.py @@ -0,0 +1,50 @@ +from typing import Optional, Set +import fern.ir.resources as ir_types + + +class RecursionGuard: + """ + Guards against infinite recursion when generating examples for types with circular references. + Tracks visited types on the current recursion path and enforces a maximum depth limit. + """ + + def __init__(self, max_depth: int = 5): + self._visited: Set[str] = set() + self._depth: int = 0 + self._max_depth: int = max_depth + + def _get_type_key(self, type_name: ir_types.DeclaredTypeName) -> str: + """Generate a unique key for a type based on its package path and name.""" + fern_filepath = ".".join(type_name.fern_filepath.package_path.parts) if type_name.fern_filepath.package_path.parts else "" + return f"{fern_filepath}:{type_name.name.original_name}" + + def can_recurse(self, type_name: ir_types.DeclaredTypeName) -> bool: + """ + Check if we can safely recurse into the given type. + Returns False if the type is already on the recursion stack or if max depth is exceeded. + """ + if self._depth >= self._max_depth: + return False + + type_key = self._get_type_key(type_name) + return type_key not in self._visited + + def enter(self, type_name: ir_types.DeclaredTypeName) -> "RecursionGuard": + """ + Enter a new recursion level for the given type. + Returns a new RecursionGuard with the type added to the visited set. + """ + new_guard = RecursionGuard(max_depth=self._max_depth) + new_guard._visited = self._visited.copy() + new_guard._visited.add(self._get_type_key(type_name)) + new_guard._depth = self._depth + 1 + return new_guard + + def with_depth_increment(self) -> "RecursionGuard": + """ + Increment depth without adding to visited set (for containers like lists/maps). + """ + new_guard = RecursionGuard(max_depth=self._max_depth) + new_guard._visited = self._visited.copy() + new_guard._depth = self._depth + 1 + return new_guard diff --git a/generators/python/src/fern_python/snippet/snippet_writer.py b/generators/python/src/fern_python/snippet/snippet_writer.py index 009a6edfb5d1..715399bbba9f 100644 --- a/generators/python/src/fern_python/snippet/snippet_writer.py +++ b/generators/python/src/fern_python/snippet/snippet_writer.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional from .type_declaration_snippet_generator import TypeDeclarationSnippetGenerator +from .recursion_guard import RecursionGuard from fern_python.codegen import AST from fern_python.generators.context.pydantic_generator_context import PydanticGeneratorContext @@ -22,13 +23,20 @@ def get_snippet_for_example_type_shape( self, name: ir_types.DeclaredTypeName, example_type_shape: ir_types.ExampleTypeShape, + recursion_guard: Optional[RecursionGuard] = None, ) -> Optional[AST.Expression]: if self._type_declaration_snippet_generator is None: return None + guard = recursion_guard or RecursionGuard() + + if not guard.can_recurse(name): + return None + return self._type_declaration_snippet_generator.generate_snippet( name=name, example=example_type_shape, + recursion_guard=guard.enter(name), ) def get_class_reference_for_declared_type_name( @@ -66,8 +74,10 @@ def get_snippet_for_example_type_reference( as_request: bool, in_typeddict: bool = False, force_include_literals: bool = False, + recursion_guard: Optional[RecursionGuard] = None, ) -> Optional[AST.Expression]: unwrapped_reference = self._context.unwrap_example_type_reference(example_type_reference) + guard = recursion_guard or RecursionGuard() return unwrapped_reference.shape.visit( primitive=lambda primitive: self._get_snippet_for_primitive( @@ -79,6 +89,7 @@ def get_snippet_for_example_type_reference( as_request=as_request, in_typeddict=in_typeddict, force_include_literals=force_include_literals, + recursion_guard=guard, ), unknown=lambda unknown: self._get_snippet_for_unknown( unknown=unknown, @@ -86,6 +97,7 @@ def get_snippet_for_example_type_reference( named=lambda named: self.get_snippet_for_example_type_shape( name=named.type_name, example_type_shape=named.shape, + recursion_guard=guard, ), ) @@ -96,8 +108,10 @@ def get_snippet_for_object_properties( in_typeddict: bool, use_typeddict_request: bool, as_request: bool, + recursion_guard: Optional[RecursionGuard] = None, ) -> List[AST.Expression]: args: List[AST.Expression] = [] + guard = recursion_guard or RecursionGuard() for property in example.properties: value = property.value.shape.visit( primitive=lambda primitive: self._get_snippet_for_primitive( @@ -108,6 +122,7 @@ def get_snippet_for_object_properties( use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ), unknown=lambda unknown: self._get_snippet_for_unknown( unknown=unknown, @@ -115,6 +130,7 @@ def get_snippet_for_object_properties( named=lambda named: self.get_snippet_for_example_type_shape( name=named.type_name, example_type_shape=named.shape, + recursion_guard=guard, ), ) if value is not None: @@ -221,7 +237,9 @@ def _get_snippet_for_container( use_typeddict_request: bool, as_request: bool, force_include_literals: bool = False, + recursion_guard: Optional[RecursionGuard] = None, ) -> Optional[AST.Expression]: + guard = recursion_guard or RecursionGuard() return container.visit( list_=lambda list: self._get_snippet_for_list_or_set( example_type_references=list.list_, @@ -229,6 +247,7 @@ def _get_snippet_for_container( in_typeddict=in_typeddict, use_typeddict_request=use_typeddict_request, as_request=as_request, + recursion_guard=guard, ), set_=lambda set: self._get_snippet_for_list_or_set( example_type_references=set.set_, @@ -236,12 +255,14 @@ def _get_snippet_for_container( in_typeddict=in_typeddict, use_typeddict_request=use_typeddict_request, as_request=as_request, + recursion_guard=guard, ), optional=lambda optional: self.get_snippet_for_example_type_reference( example_type_reference=optional.optional, use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ) if optional.optional is not None else None, @@ -250,6 +271,7 @@ def _get_snippet_for_container( use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ) if nullable.nullable is not None else None, @@ -258,6 +280,7 @@ def _get_snippet_for_container( use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ), literal=lambda lit: self._get_snippet_for_primitive(lit.literal) if in_typeddict or force_include_literals @@ -284,8 +307,10 @@ def _get_snippet_for_list_or_set( in_typeddict: bool, use_typeddict_request: bool, as_request: bool, + recursion_guard: Optional[RecursionGuard] = None, ) -> Optional[AST.Expression]: values: List[AST.Expression] = [] + guard = recursion_guard or RecursionGuard() # We use lists for sets if the inner type is non-primitive because Pydantic models aren't hashable contents_are_primitive = False for example_type_reference in example_type_references: @@ -300,6 +325,7 @@ def _get_snippet_for_list_or_set( use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ) if expression is not None: values.append(expression) @@ -313,21 +339,25 @@ def _get_snippet_for_map( in_typeddict: bool, use_typeddict_request: bool, as_request: bool, + recursion_guard: Optional[RecursionGuard] = None, ) -> AST.Expression: keys: List[AST.Expression] = [] values: List[AST.Expression] = [] + guard = recursion_guard or RecursionGuard() for pair in pairs: key = self.get_snippet_for_example_type_reference( example_type_reference=pair.key, use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ) value = self.get_snippet_for_example_type_reference( example_type_reference=pair.value, use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ) if key is not None and value is not None: keys.append(key) diff --git a/generators/python/src/fern_python/snippet/type_declaration_snippet_generator.py b/generators/python/src/fern_python/snippet/type_declaration_snippet_generator.py index d762d26a3cf8..af9d66dae638 100644 --- a/generators/python/src/fern_python/snippet/type_declaration_snippet_generator.py +++ b/generators/python/src/fern_python/snippet/type_declaration_snippet_generator.py @@ -4,12 +4,16 @@ import fern.ir.resources as ir_types -AliasSnippetGenerator = Callable[[ir_types.ExampleAliasType], Optional[AST.Expression]] -EnumSnippetGenerator = Callable[[ir_types.DeclaredTypeName, ir_types.ExampleEnumType], AST.Expression] -ObjectSnippetGenerator = Callable[[ir_types.DeclaredTypeName, ir_types.ExampleObjectType], AST.Expression] -DiscriminatedUnionGenerator = Callable[[ir_types.DeclaredTypeName, ir_types.ExampleUnionType], AST.Expression] +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from .recursion_guard import RecursionGuard + +AliasSnippetGenerator = Callable[[ir_types.ExampleAliasType, Optional["RecursionGuard"]], Optional[AST.Expression]] +EnumSnippetGenerator = Callable[[ir_types.DeclaredTypeName, ir_types.ExampleEnumType, Optional["RecursionGuard"]], AST.Expression] +ObjectSnippetGenerator = Callable[[ir_types.DeclaredTypeName, ir_types.ExampleObjectType, Optional["RecursionGuard"]], AST.Expression] +DiscriminatedUnionGenerator = Callable[[ir_types.DeclaredTypeName, ir_types.ExampleUnionType, Optional["RecursionGuard"]], AST.Expression] UndiscriminatedUnionGenerator = Callable[ - [ir_types.DeclaredTypeName, ir_types.ExampleUndiscriminatedUnionType], Optional[AST.Expression] + [ir_types.DeclaredTypeName, ir_types.ExampleUndiscriminatedUnionType, Optional["RecursionGuard"]], Optional[AST.Expression] ] @@ -32,11 +36,12 @@ def generate_snippet( self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleTypeShape, + recursion_guard: Optional["RecursionGuard"] = None, ) -> Optional[AST.Expression]: return example.visit( - alias=lambda alias: self._generate_alias(alias), - enum=lambda enum: self._generate_enum(name, enum), - object=lambda object_: self._generate_object(name, object_), - union=lambda union: self._generate_discriminated_union(name, union), - undiscriminated_union=lambda union: self._generate_undiscriminated_union(name, union), + alias=lambda alias: self._generate_alias(alias, recursion_guard), + enum=lambda enum: self._generate_enum(name, enum, recursion_guard), + object=lambda object_: self._generate_object(name, object_, recursion_guard), + union=lambda union: self._generate_discriminated_union(name, union, recursion_guard), + undiscriminated_union=lambda union: self._generate_undiscriminated_union(name, union, recursion_guard), ) diff --git a/generators/typescript/model/type-generator/src/AbstractGeneratedType.ts b/generators/typescript/model/type-generator/src/AbstractGeneratedType.ts index 3e9b55ca8ef1..5b114b9a75b0 100644 --- a/generators/typescript/model/type-generator/src/AbstractGeneratedType.ts +++ b/generators/typescript/model/type-generator/src/AbstractGeneratedType.ts @@ -1,6 +1,7 @@ import { ExampleType, ExampleTypeShape, FernFilepath } from "@fern-fern/ir-sdk/api"; import { GetReferenceOpts, getTextOfTsNode, Reference } from "@fern-typescript/commons"; import { BaseContext, BaseGeneratedType } from "@fern-typescript/contexts"; +import { RecursionGuard } from "@fern-typescript/type-reference-example-generator"; import { ModuleDeclarationStructure, StatementStructures, ts, WriterFunction } from "ts-morph"; export declare namespace AbstractGeneratedType { @@ -108,5 +109,5 @@ export abstract class AbstractGeneratedType responseTypeNode: ts.TypeNode | undefined; }; public abstract generateModule(context: Context): ModuleDeclarationStructure | undefined; - public abstract buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression; + public abstract buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression; } diff --git a/generators/typescript/model/type-generator/src/alias/GeneratedAliasTypeImpl.ts b/generators/typescript/model/type-generator/src/alias/GeneratedAliasTypeImpl.ts index a38294a489c2..9a74155facfc 100644 --- a/generators/typescript/model/type-generator/src/alias/GeneratedAliasTypeImpl.ts +++ b/generators/typescript/model/type-generator/src/alias/GeneratedAliasTypeImpl.ts @@ -73,7 +73,7 @@ export class GeneratedAliasTypeImpl }); } - public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression { + public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression { if (example.type !== "alias") { throw new Error("Example is not for an alias"); } diff --git a/generators/typescript/model/type-generator/src/alias/GeneratedBrandedStringAliasImpl.ts b/generators/typescript/model/type-generator/src/alias/GeneratedBrandedStringAliasImpl.ts index 2edfb3a717ee..7dc6f1e19523 100644 --- a/generators/typescript/model/type-generator/src/alias/GeneratedBrandedStringAliasImpl.ts +++ b/generators/typescript/model/type-generator/src/alias/GeneratedBrandedStringAliasImpl.ts @@ -54,7 +54,7 @@ export class GeneratedBrandedStringAliasImpl return this.getReferenceToSelf(context).getExpression(opts); } - public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression { + public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression { if (example.type !== "alias") { throw new Error("Example is not for an alias"); } diff --git a/generators/typescript/model/type-generator/src/enum/GeneratedEnumTypeImpl.ts b/generators/typescript/model/type-generator/src/enum/GeneratedEnumTypeImpl.ts index 8684c8e7ed9b..888be83b63e3 100644 --- a/generators/typescript/model/type-generator/src/enum/GeneratedEnumTypeImpl.ts +++ b/generators/typescript/model/type-generator/src/enum/GeneratedEnumTypeImpl.ts @@ -275,7 +275,7 @@ export class GeneratedEnumTypeImpl return enumModule; } - public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression { + public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression { if (example.type !== "enum") { throw new Error("Example is not for an enum"); } diff --git a/generators/typescript/model/type-generator/src/object/GeneratedObjectTypeImpl.ts b/generators/typescript/model/type-generator/src/object/GeneratedObjectTypeImpl.ts index d1558d0a1065..0cf9e0f3918d 100644 --- a/generators/typescript/model/type-generator/src/object/GeneratedObjectTypeImpl.ts +++ b/generators/typescript/model/type-generator/src/object/GeneratedObjectTypeImpl.ts @@ -253,7 +253,7 @@ export class GeneratedObjectTypeImpl } } - public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression { + public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression { if (example.type !== "object") { throw new Error("Example is not for an object"); } diff --git a/generators/typescript/model/type-generator/src/undiscriminated-union/GeneratedUndiscriminatedUnionTypeImpl.ts b/generators/typescript/model/type-generator/src/undiscriminated-union/GeneratedUndiscriminatedUnionTypeImpl.ts index 8e07940fdabc..8e74b84bfb33 100644 --- a/generators/typescript/model/type-generator/src/undiscriminated-union/GeneratedUndiscriminatedUnionTypeImpl.ts +++ b/generators/typescript/model/type-generator/src/undiscriminated-union/GeneratedUndiscriminatedUnionTypeImpl.ts @@ -175,7 +175,7 @@ export class GeneratedUndiscriminatedUnionTypeImpl return this.getTypeReferenceNode(context, member).typeNode; } - public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression { + public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression { if (example.type !== "undiscriminatedUnion") { throw new Error("Example is not for an undiscriminated union"); } diff --git a/generators/typescript/model/type-generator/src/union/GeneratedUnionTypeImpl.ts b/generators/typescript/model/type-generator/src/union/GeneratedUnionTypeImpl.ts index c105dbeb810c..994bf26feb00 100644 --- a/generators/typescript/model/type-generator/src/union/GeneratedUnionTypeImpl.ts +++ b/generators/typescript/model/type-generator/src/union/GeneratedUnionTypeImpl.ts @@ -119,7 +119,7 @@ export class GeneratedUnionTypeImpl } } - public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression { + public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression { if (example.type !== "union") { throw new Error("Example is not for an union"); } diff --git a/generators/typescript/model/type-reference-example-generator/src/GeneratedTypeReferenceExampleImpl.ts b/generators/typescript/model/type-reference-example-generator/src/GeneratedTypeReferenceExampleImpl.ts index 2d9476e4f16b..419699af96c6 100644 --- a/generators/typescript/model/type-reference-example-generator/src/GeneratedTypeReferenceExampleImpl.ts +++ b/generators/typescript/model/type-reference-example-generator/src/GeneratedTypeReferenceExampleImpl.ts @@ -10,6 +10,7 @@ import { import { GetReferenceOpts, isExpressionUndefined } from "@fern-typescript/commons"; import { BaseContext, GeneratedTypeReferenceExample } from "@fern-typescript/contexts"; import { ts } from "ts-morph"; +import { RecursionGuard, RecursionGuardImpl } from "./RecursionGuard"; export declare namespace GeneratedTypeReferenceExampleImpl { export interface Init { @@ -31,18 +32,21 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference } public build(context: BaseContext, opts: GetReferenceOpts): ts.Expression { - return this.buildExample({ example: this.example, context, opts }); + return this.buildExample({ example: this.example, context, opts, recursionGuard: new RecursionGuardImpl() }); } private buildExample({ example, context, - opts + opts, + recursionGuard }: { example: ExampleTypeReference; context: BaseContext; opts: GetReferenceOpts; + recursionGuard?: RecursionGuard; }): ts.Expression { + const guard = recursionGuard ?? new RecursionGuardImpl(); return ExampleTypeReferenceShape._visit(example.shape, { primitive: (primitiveExample) => ExamplePrimitive._visit(primitiveExample, { @@ -91,7 +95,7 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference list: (exampleItems) => ts.factory.createArrayLiteralExpression( exampleItems.list - .map((exampleItem) => this.buildExample({ example: exampleItem, context, opts })) + .map((exampleItem) => this.buildExample({ example: exampleItem, context, opts, recursionGuard: guard.withDepthIncrement() })) .filter((expr) => !isExpressionUndefined(expr)) ), set: (exampleItems) => { @@ -100,7 +104,7 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference ts.factory.createArrayLiteralExpression( exampleItems.set .map((exampleItem) => - this.buildExample({ example: exampleItem, context, opts }) + this.buildExample({ example: exampleItem, context, opts, recursionGuard: guard.withDepthIncrement() }) ) .filter((expr) => !isExpressionUndefined(expr)) ) @@ -108,7 +112,7 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference } else { return ts.factory.createArrayLiteralExpression( exampleItems.set - .map((exampleItem) => this.buildExample({ example: exampleItem, context, opts })) + .map((exampleItem) => this.buildExample({ example: exampleItem, context, opts, recursionGuard: guard.withDepthIncrement() })) .filter((expr) => !isExpressionUndefined(expr)) ); } @@ -117,8 +121,8 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference ts.factory.createObjectLiteralExpression( examplePairs.map .map<[ts.PropertyName, ts.Expression]>((examplePair) => [ - this.getExampleAsPropertyName({ example: examplePair.key, context, opts }), - this.buildExample({ example: examplePair.value, context, opts }) + this.getExampleAsPropertyName({ example: examplePair.key, context, opts, recursionGuard: guard }), + this.buildExample({ example: examplePair.value, context, opts, recursionGuard: guard.withDepthIncrement() }) ]) .filter(([, value]) => !isExpressionUndefined(value)) .map(([key, value]) => @@ -128,11 +132,11 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference ), nullable: (exampleItem) => exampleItem.nullable != null - ? this.buildExample({ example: exampleItem.nullable, context, opts }) + ? this.buildExample({ example: exampleItem.nullable, context, opts, recursionGuard: guard }) : ts.factory.createIdentifier("null"), optional: (exampleItem) => exampleItem.optional != null - ? this.buildExample({ example: exampleItem.optional, context, opts }) + ? this.buildExample({ example: exampleItem.optional, context, opts, recursionGuard: guard }) : ts.factory.createIdentifier("undefined"), literal: (exampleItem) => exampleItem != null @@ -142,7 +146,8 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference shape: ExampleTypeReferenceShape.primitive(exampleItem.literal) }, context, - opts + opts, + recursionGuard: guard }) : ts.factory.createIdentifier("undefined"), _other: () => { @@ -151,7 +156,10 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference }); }, named: ({ typeName, shape: example }) => { - return context.type.getGeneratedType(typeName).buildExample(example, context, opts); + if (!guard.canRecurse(typeName.typeId)) { + return ts.factory.createIdentifier("undefined"); + } + return context.type.getGeneratedType(typeName).buildExample(example, context, opts, guard.enter(typeName.typeId)); }, unknown: (value) => { const parsed = ts.parseJsonText( @@ -208,12 +216,15 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference private getExampleAsPropertyName({ example, context, - opts + opts, + recursionGuard }: { example: ExampleTypeReference; context: BaseContext; opts: GetReferenceOpts; + recursionGuard?: RecursionGuard; }): ts.PropertyName { + const guard = recursionGuard ?? new RecursionGuardImpl(); return ExampleTypeReferenceShape._visit(example.shape, { primitive: (primitiveExample) => ExamplePrimitive._visit(primitiveExample, { @@ -246,7 +257,8 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference jsonExample: example.jsonExample }, context, - opts + opts, + recursionGuard: guard }); } throw new Error("Cannot convert container to property name"); @@ -267,12 +279,13 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference ); } case "alias": - return this.getExampleAsPropertyName({ example: example.value, context, opts }); + return this.getExampleAsPropertyName({ example: example.value, context, opts, recursionGuard: guard }); case "undiscriminatedUnion": return this.getExampleAsPropertyName({ example: example.singleUnionType, context, - opts + opts, + recursionGuard: guard }); default: assertNever(example); diff --git a/generators/typescript/model/type-reference-example-generator/src/RecursionGuard.ts b/generators/typescript/model/type-reference-example-generator/src/RecursionGuard.ts new file mode 100644 index 000000000000..c4ca7cd6a752 --- /dev/null +++ b/generators/typescript/model/type-reference-example-generator/src/RecursionGuard.ts @@ -0,0 +1,36 @@ +import { TypeId } from "@fern-fern/ir-sdk/api"; + +export interface RecursionGuard { + canRecurse(typeId: TypeId): boolean; + enter(typeId: TypeId): RecursionGuard; + withDepthIncrement(): RecursionGuard; +} + +export class RecursionGuardImpl implements RecursionGuard { + private visited: Set; + private depth: number; + private maxDepth: number; + + constructor(maxDepth: number = 5, visited: Set = new Set(), depth: number = 0) { + this.maxDepth = maxDepth; + this.visited = visited; + this.depth = depth; + } + + public canRecurse(typeId: TypeId): boolean { + if (this.depth >= this.maxDepth) { + return false; + } + return !this.visited.has(typeId); + } + + public enter(typeId: TypeId): RecursionGuard { + const newVisited = new Set(this.visited); + newVisited.add(typeId); + return new RecursionGuardImpl(this.maxDepth, newVisited, this.depth + 1); + } + + public withDepthIncrement(): RecursionGuard { + return new RecursionGuardImpl(this.maxDepth, new Set(this.visited), this.depth + 1); + } +} diff --git a/generators/typescript/model/type-reference-example-generator/src/index.ts b/generators/typescript/model/type-reference-example-generator/src/index.ts index f81d6188bac2..168ce114ebfe 100644 --- a/generators/typescript/model/type-reference-example-generator/src/index.ts +++ b/generators/typescript/model/type-reference-example-generator/src/index.ts @@ -1 +1,2 @@ export { TypeReferenceExampleGenerator } from "./TypeReferenceExampleGenerator"; +export { RecursionGuard, RecursionGuardImpl } from "./RecursionGuard"; diff --git a/generators/typescript/utils/contexts/src/base-context/type/BaseGeneratedType.ts b/generators/typescript/utils/contexts/src/base-context/type/BaseGeneratedType.ts index 3ac1d03bbaf5..8bbd1dc64b82 100644 --- a/generators/typescript/utils/contexts/src/base-context/type/BaseGeneratedType.ts +++ b/generators/typescript/utils/contexts/src/base-context/type/BaseGeneratedType.ts @@ -6,11 +6,12 @@ import { GeneratedFile } from "../../commons"; import { GeneratedModule } from "../../commons/GeneratedModule"; import { GeneratedStatements } from "../../commons/GeneratedStatements"; import { GeneratedUnionInlineMemberNode } from "../../commons/GeneratedUnionInlineMemberNode"; +import { RecursionGuard } from "@fern-typescript/type-reference-example-generator"; export interface BaseGeneratedType extends GeneratedFile, GeneratedStatements, GeneratedModule, GeneratedUnionInlineMemberNode { - buildExample: (example: ExampleTypeShape, context: Context, opts: GetReferenceOpts) => ts.Expression; + buildExample: (example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard) => ts.Expression; }