@@ -1441,6 +1441,39 @@ function extractDeriveCallback(
14411441 return undefined ;
14421442}
14431443
1444+ /**
1445+ * Resolve capture name collisions with the original input parameter name.
1446+ * If a capture has the same name as originalInputParamName, rename it (e.g., multiplier -> multiplier_1).
1447+ * Returns a mapping from original capture names to their potentially renamed versions.
1448+ */
1449+ function resolveDeriveCaptureNameCollisions (
1450+ originalInputParamName : string ,
1451+ captureTree : Map < string , CaptureTreeNode > ,
1452+ ) : Map < string , string > {
1453+ const captureNameMap = new Map < string , string > ( ) ;
1454+ const usedNames = new Set < string > ( [ originalInputParamName ] ) ;
1455+
1456+ for ( const [ captureName ] of captureTree ) {
1457+ if ( captureName === originalInputParamName ) {
1458+ // Collision detected - rename the capture
1459+ let renamed = `${ captureName } _1` ;
1460+ let suffix = 1 ;
1461+ while ( usedNames . has ( renamed ) || captureTree . has ( renamed ) ) {
1462+ suffix ++ ;
1463+ renamed = `${ captureName } _${ suffix } ` ;
1464+ }
1465+ captureNameMap . set ( captureName , renamed ) ;
1466+ usedNames . add ( renamed ) ;
1467+ } else {
1468+ // No collision - use original name
1469+ captureNameMap . set ( captureName , captureName ) ;
1470+ usedNames . add ( captureName ) ;
1471+ }
1472+ }
1473+
1474+ return captureNameMap ;
1475+ }
1476+
14441477/**
14451478 * Build the merged input object containing both the original input and captures.
14461479 * Example: {value, multiplier} where value is the original input and multiplier is a capture.
@@ -1452,6 +1485,7 @@ function buildDeriveInputObject(
14521485 originalInput : ts . Expression ,
14531486 originalInputParamName : string ,
14541487 captureTree : Map < string , CaptureTreeNode > ,
1488+ captureNameMap : Map < string , string > ,
14551489 factory : ts . NodeFactory ,
14561490 hadZeroParameters : boolean ,
14571491) : ts . ObjectLiteralExpression {
@@ -1478,15 +1512,90 @@ function buildDeriveInputObject(
14781512 }
14791513 }
14801514
1481- // Add captures
1482- properties . push ( ...buildCapturePropertyAssignments ( captureTree , factory ) ) ;
1515+ // Add captures with potentially renamed property names
1516+ for ( const [ originalName , node ] of captureTree ) {
1517+ const propertyName = captureNameMap . get ( originalName ) ?? originalName ;
1518+ properties . push (
1519+ factory . createPropertyAssignment (
1520+ createPropertyName ( propertyName , factory ) ,
1521+ buildHierarchicalParamsValue ( node , originalName , factory ) ,
1522+ ) ,
1523+ ) ;
1524+ }
14831525
14841526 return factory . createObjectLiteralExpression (
14851527 properties ,
14861528 properties . length > 1 ,
14871529 ) ;
14881530}
14891531
1532+ /**
1533+ * Rewrite the callback body to use renamed capture identifiers.
1534+ * For example, if `multiplier` was renamed to `multiplier_1`, replace all
1535+ * references to the captured `multiplier` with `multiplier_1`.
1536+ */
1537+ function rewriteCaptureReferences (
1538+ body : ts . ConciseBody ,
1539+ captureNameMap : Map < string , string > ,
1540+ factory : ts . NodeFactory ,
1541+ ) : ts . ConciseBody {
1542+ // Build a reverse map: original capture name -> list of renamed names that should be substituted
1543+ const substitutions = new Map < string , string > ( ) ;
1544+ for ( const [ originalName , renamedName ] of captureNameMap ) {
1545+ if ( originalName !== renamedName ) {
1546+ substitutions . set ( originalName , renamedName ) ;
1547+ }
1548+ }
1549+
1550+ if ( substitutions . size === 0 ) {
1551+ return body ; // No substitutions needed
1552+ }
1553+
1554+ const visitor = ( node : ts . Node , parent ?: ts . Node ) : ts . Node => {
1555+ // Handle shorthand property assignments specially
1556+ // { multiplier } needs to become { multiplier: multiplier_1 } if multiplier is renamed
1557+ if ( ts . isShorthandPropertyAssignment ( node ) ) {
1558+ const substituteName = substitutions . get ( node . name . text ) ;
1559+ if ( substituteName ) {
1560+ // Expand shorthand into full property assignment
1561+ return factory . createPropertyAssignment (
1562+ node . name , // Property name stays the same
1563+ factory . createIdentifier ( substituteName ) , // Value uses renamed identifier
1564+ ) ;
1565+ }
1566+ // No substitution needed, keep as shorthand
1567+ return node ;
1568+ }
1569+
1570+ // Don't substitute identifiers that are property names
1571+ if ( ts . isIdentifier ( node ) ) {
1572+ // Skip if this identifier is the property name in a property access (e.g., '.get' in 'obj.get')
1573+ if (
1574+ parent && ts . isPropertyAccessExpression ( parent ) && parent . name === node
1575+ ) {
1576+ return node ;
1577+ }
1578+
1579+ // Skip if this identifier is a property name in an object literal (e.g., 'foo' in '{ foo: value }')
1580+ if ( parent && ts . isPropertyAssignment ( parent ) && parent . name === node ) {
1581+ return node ;
1582+ }
1583+
1584+ const substituteName = substitutions . get ( node . text ) ;
1585+ if ( substituteName ) {
1586+ return factory . createIdentifier ( substituteName ) ;
1587+ }
1588+ }
1589+
1590+ return ts . visitEachChild ( node , ( child ) => visitor ( child , node ) , undefined ) ;
1591+ } ;
1592+
1593+ return ts . visitNode (
1594+ body ,
1595+ ( node ) => visitor ( node , undefined ) ,
1596+ ) as ts . ConciseBody ;
1597+ }
1598+
14901599/**
14911600 * Create the derive callback with parameter aliasing to preserve user's parameter name.
14921601 * Example: ({value: v, multiplier}) => v * multiplier
@@ -1499,6 +1608,7 @@ function createDeriveCallback(
14991608 transformedBody : ts . ConciseBody ,
15001609 originalInputParamName : string ,
15011610 captureTree : Map < string , CaptureTreeNode > ,
1611+ captureNameMap : Map < string , string > ,
15021612 context : TransformationContext ,
15031613 hadZeroParameters : boolean ,
15041614) : ts . ArrowFunction | ts . FunctionExpression {
@@ -1590,14 +1700,19 @@ function createDeriveCallback(
15901700 ) ,
15911701 ) ;
15921702
1593- // Add bindings for captures
1703+ // Add bindings for captures using the potentially renamed property names
15941704 const createBindingIdentifier = ( name : string ) : ts . Identifier => {
15951705 return reserveIdentifier ( name , usedBindingNames , factory ) ;
15961706 } ;
15971707
1708+ // Create binding elements using the renamed capture names
1709+ const renamedCaptureNames = Array . from ( captureTree . keys ( ) ) . map (
1710+ ( originalName ) => captureNameMap . get ( originalName ) ?? originalName ,
1711+ ) ;
1712+
15981713 bindingElements . push (
15991714 ...createBindingElementsFromNames (
1600- captureTree . keys ( ) ,
1715+ renamedCaptureNames ,
16011716 factory ,
16021717 createBindingIdentifier ,
16031718 ) ,
@@ -1613,6 +1728,13 @@ function createDeriveCallback(
16131728 undefined ,
16141729 ) ;
16151730
1731+ // Rewrite the body to use renamed capture identifiers
1732+ const rewrittenBody = rewriteCaptureReferences (
1733+ transformedBody ,
1734+ captureNameMap ,
1735+ factory ,
1736+ ) ;
1737+
16161738 // Create the new callback
16171739 if ( ts . isArrowFunction ( callback ) ) {
16181740 return factory . createArrowFunction (
@@ -1621,7 +1743,7 @@ function createDeriveCallback(
16211743 [ parameter ] ,
16221744 undefined , // No return type - rely on inference
16231745 callback . equalsGreaterThanToken ,
1624- transformedBody ,
1746+ rewrittenBody ,
16251747 ) ;
16261748 } else {
16271749 return factory . createFunctionExpression (
@@ -1631,7 +1753,7 @@ function createDeriveCallback(
16311753 callback . typeParameters ,
16321754 [ parameter ] ,
16331755 undefined , // No return type - rely on inference
1634- transformedBody as ts . Block ,
1756+ rewrittenBody as ts . Block ,
16351757 ) ;
16361758 }
16371759}
@@ -1646,6 +1768,7 @@ function buildDeriveInputSchema(
16461768 originalInputParamName : string ,
16471769 originalInput : ts . Expression ,
16481770 captureTree : Map < string , CaptureTreeNode > ,
1771+ captureNameMap : Map < string , string > ,
16491772 context : TransformationContext ,
16501773 hadZeroParameters : boolean ,
16511774) : ts . TypeNode {
@@ -1677,11 +1800,40 @@ function buildDeriveInputSchema(
16771800 ) ;
16781801 }
16791802
1680- // Add type elements for captures
1681- typeElements . push (
1682- ...buildTypeElementsFromCaptureTree ( captureTree , context ) ,
1803+ // Add type elements for captures using the existing helper
1804+ const captureTypeElements = buildTypeElementsFromCaptureTree (
1805+ captureTree ,
1806+ context ,
16831807 ) ;
16841808
1809+ // Rename the property signatures if there are collisions
1810+ for ( const typeElement of captureTypeElements ) {
1811+ if (
1812+ ts . isPropertySignature ( typeElement ) && ts . isIdentifier ( typeElement . name )
1813+ ) {
1814+ const originalName = typeElement . name . text ;
1815+ const renamedName = captureNameMap . get ( originalName ) ?? originalName ;
1816+
1817+ if ( renamedName !== originalName ) {
1818+ // Create a new property signature with the renamed identifier
1819+ typeElements . push (
1820+ factory . createPropertySignature (
1821+ typeElement . modifiers ,
1822+ factory . createIdentifier ( renamedName ) ,
1823+ typeElement . questionToken ,
1824+ typeElement . type ,
1825+ ) ,
1826+ ) ;
1827+ } else {
1828+ // No renaming needed
1829+ typeElements . push ( typeElement ) ;
1830+ }
1831+ } else {
1832+ // Not a simple property signature, keep as-is
1833+ typeElements . push ( typeElement ) ;
1834+ }
1835+ }
1836+
16851837 // Create object type literal
16861838 return factory . createTypeLiteralNode ( typeElements ) ;
16871839}
@@ -1757,11 +1909,18 @@ function transformDeriveCall(
17571909 // In this case, we don't need to preserve the input - just use captures
17581910 const hadZeroParameters = callback . parameters . length === 0 ;
17591911
1912+ // Resolve capture name collisions with the original input parameter name
1913+ const captureNameMap = resolveDeriveCaptureNameCollisions (
1914+ originalInputParamName ,
1915+ captureTree ,
1916+ ) ;
1917+
17601918 // Build merged input object
17611919 const mergedInput = buildDeriveInputObject (
17621920 originalInput ,
17631921 originalInputParamName ,
17641922 captureTree ,
1923+ captureNameMap ,
17651924 factory ,
17661925 hadZeroParameters ,
17671926 ) ;
@@ -1772,6 +1931,7 @@ function transformDeriveCall(
17721931 transformedBody ,
17731932 originalInputParamName ,
17741933 captureTree ,
1934+ captureNameMap ,
17751935 context ,
17761936 hadZeroParameters ,
17771937 ) ;
@@ -1782,6 +1942,7 @@ function transformDeriveCall(
17821942 originalInputParamName ,
17831943 originalInput ,
17841944 captureTree ,
1945+ captureNameMap ,
17851946 context ,
17861947 hadZeroParameters ,
17871948 ) ;
@@ -1790,19 +1951,26 @@ function transformDeriveCall(
17901951 // SchemaInjectionTransformer will use this to generate the result schema
17911952 const signature = context . checker . getSignatureFromDeclaration ( callback ) ;
17921953 let resultTypeNode : ts . TypeNode | undefined ;
1954+ let resultType : ts . Type | undefined ;
17931955
17941956 if ( callback . type ) {
1795- // Explicit return type annotation - use it
1957+ // Explicit return type annotation - use it directly (no need to register in typeRegistry)
17961958 resultTypeNode = callback . type ;
17971959 } else if ( signature ) {
17981960 // Infer from callback signature
1799- const returnType = signature . getReturnType ( ) ;
1961+ resultType = signature . getReturnType ( ) ;
18001962 resultTypeNode = context . checker . typeToTypeNode (
1801- returnType ,
1963+ resultType ,
18021964 context . sourceFile ,
18031965 ts . NodeBuilderFlags . NoTruncation |
18041966 ts . NodeBuilderFlags . UseStructuralFallback ,
18051967 ) ;
1968+
1969+ // Register the result Type in typeRegistry for the synthetic TypeNode
1970+ // This fixes schema generation for shorthand properties referencing captured variables
1971+ if ( resultTypeNode && context . options . typeRegistry ) {
1972+ context . options . typeRegistry . set ( resultTypeNode , resultType ) ;
1973+ }
18061974 }
18071975
18081976 // Build the derive call expression
0 commit comments