@@ -1208,36 +1208,67 @@ function convertChildAsPattern(child: ts.Node | undefined | null, parent: LazyNo
12081208// SHAPES only handles the mechanical pattern. Subclasses with custom
12091209// constructor logic (range mutation, modifier-derived flags, conditional
12101210// branching) stay hand-written. The factory + table live alongside.
1211- type ShapeSlotConvert = 'convertChild' | 'convertChildren' | 'convertChildAsPattern' ;
1211+ type ShapeSlotConvert =
1212+ | 'convertChild'
1213+ | 'convertChildren'
1214+ | 'convertChildAsPattern'
1215+ | ( ( tsValue : any , parent : LazyNode ) => any ) ;
12121216interface ShapeSlotDef {
12131217 tsField : string ;
1218+ // How to convert the TS value. Defaults to 'convertChild'. Function
1219+ // option lets a slot route through a custom converter (e.g.
1220+ // `convertTypeAnnotation` for synthetic-wrapper-bearing type slots).
12141221 via ?: ShapeSlotConvert ;
1215- }
1222+ // Value when the TS field is null/undefined. Default 'null' — matches
1223+ // typescript-estree for value slots (`init`, `expression`, etc.).
1224+ // Type-position slots (`returnType`, `typeAnnotation`, `typeArguments`)
1225+ // use 'undefined' to match eager's distinction.
1226+ whenAbsent ?: 'null' | 'undefined' ;
1227+ }
1228+ // Sentinel: cache value "not yet computed". Lets factory-built getters
1229+ // memoise null AND undefined results without re-running on subsequent
1230+ // reads. Hand-written subclasses use the `??=` pattern which conflates
1231+ // the two; the factory needs to honour eager's null-vs-undefined
1232+ // distinction (e.g. ReturnStatement.argument is null when bare; type
1233+ // annotations are undefined when absent).
1234+ const SHAPE_UNSET = Symbol ( 'shape-unset' ) ;
12161235interface ShapeDef {
12171236 type : string ;
12181237 slots : Record < string , ShapeSlotDef > ;
1219- // Optional callback that derives readonly fields from the TS node
1220- // (e.g. computing `delegate` from `asteriskToken`, `const`/`in`/`out`
1221- // from modifier flags). Applied in the constructor after super().
1238+ // Static field defaults — values that don't depend on the TS node
1239+ // (e.g. UnaryExpression's `operator: 'void'`, ObjectPattern's
1240+ // `decorators: EMPTY_ARRAY`). Per-instance assignment matches how
1241+ // `readonly x = '...'` class fields compile.
1242+ defaults ?: Record < string , unknown > ;
1243+ // Per-instance callback that derives readonly fields from the TS
1244+ // node (e.g. computing `delegate` from `asteriskToken`,
1245+ // `const`/`in`/`out` from modifier flags). Applied after super().
12221246 consts ?: ( tsNode : any ) => Record < string , unknown > ;
12231247}
12241248const SHAPE_CLASSES = new Map < ts . SyntaxKind , new ( tsNode : ts . Node , parent : LazyNode | null ) => LazyNode > ( ) ;
12251249function defineShape ( tsKind : ts . SyntaxKind , def : ShapeDef ) : void {
1250+ const slotKeys = Object . keys ( def . slots ) . map ( g => '_' + g ) ;
12261251 const cls = class extends LazyNode {
12271252 readonly type = def . type ;
12281253 constructor ( tsNode : ts . Node , parent : LazyNode | null ) {
12291254 super ( tsNode , parent ) ;
1255+ // Init each cache key to UNSET so getters can distinguish
1256+ // "not yet read" from "computed and got null/undefined".
1257+ for ( const k of slotKeys ) ( this as any ) [ k ] = SHAPE_UNSET ;
1258+ if ( def . defaults ) Object . assign ( this , def . defaults ) ;
12301259 if ( def . consts ) Object . assign ( this , def . consts ( tsNode ) ) ;
12311260 }
12321261 } ;
12331262 for ( const [ getter , slot ] of Object . entries ( def . slots ) ) {
12341263 const cacheKey = '_' + getter ;
1264+ const absent = slot . whenAbsent === 'undefined' ? undefined : null ;
1265+ const via = slot . via ?? 'convertChild' ;
12351266 Object . defineProperty ( cls . prototype , getter , {
12361267 get ( this : any ) {
1237- if ( this [ cacheKey ] !== undefined ) return this [ cacheKey ] ;
1268+ if ( this [ cacheKey ] !== SHAPE_UNSET ) return this [ cacheKey ] ;
12381269 const tsValue = this . _ts [ slot . tsField ] ;
1239- if ( tsValue == null ) return this [ cacheKey ] = null ;
1240- const via = slot . via ?? 'convertChild' ;
1270+ if ( tsValue == null ) return this [ cacheKey ] = absent ;
1271+ if ( typeof via === 'function' ) return this [ cacheKey ] = via ( tsValue , this ) ;
12411272 if ( via === 'convertChildren' ) return this [ cacheKey ] = convertChildren ( tsValue , this ) ;
12421273 if ( via === 'convertChildAsPattern' ) return this [ cacheKey ] = convertChildAsPattern ( tsValue , this ) ;
12431274 return this [ cacheKey ] = convertChild ( tsValue , this ) ;
@@ -1435,6 +1466,50 @@ defineShape(SK.YieldExpression, {
14351466 consts : ( tn : ts . YieldExpression ) => ( { delegate : ! ! tn . asteriskToken } ) ,
14361467 slots : { argument : { tsField : 'expression' } } ,
14371468} ) ;
1469+ // Pure mechanical with `defaults`:
1470+ defineShape ( SK . AsExpression , {
1471+ type : 'TSAsExpression' ,
1472+ slots : {
1473+ expression : { tsField : 'expression' } ,
1474+ typeAnnotation : { tsField : 'type' } ,
1475+ } ,
1476+ } ) ;
1477+ defineShape ( SK . ExpressionStatement , {
1478+ type : 'ExpressionStatement' ,
1479+ defaults : { directive : undefined } ,
1480+ slots : { expression : { tsField : 'expression' } } ,
1481+ } ) ;
1482+ defineShape ( SK . TypeQuery , {
1483+ type : 'TSTypeQuery' ,
1484+ defaults : { typeArguments : undefined } ,
1485+ slots : { exprName : { tsField : 'exprName' } } ,
1486+ } ) ;
1487+ defineShape ( SK . NamespaceImport , {
1488+ type : 'ImportNamespaceSpecifier' ,
1489+ slots : { local : { tsField : 'name' } } ,
1490+ } ) ;
1491+ defineShape ( SK . ObjectBindingPattern , {
1492+ type : 'ObjectPattern' ,
1493+ defaults : { decorators : EMPTY_ARRAY , optional : false , typeAnnotation : undefined } ,
1494+ slots : { properties : { tsField : 'elements' , via : 'convertChildren' } } ,
1495+ } ) ;
1496+ // Unary expressions — TS has separate kinds for void/typeof/delete; ESTree
1497+ // folds them into UnaryExpression with the operator literal baked in.
1498+ defineShape ( SK . VoidExpression , {
1499+ type : 'UnaryExpression' ,
1500+ defaults : { operator : 'void' , prefix : true } ,
1501+ slots : { argument : { tsField : 'expression' } } ,
1502+ } ) ;
1503+ defineShape ( SK . DeleteExpression , {
1504+ type : 'UnaryExpression' ,
1505+ defaults : { operator : 'delete' , prefix : true } ,
1506+ slots : { argument : { tsField : 'expression' } } ,
1507+ } ) ;
1508+ defineShape ( SK . TypeOfExpression , {
1509+ type : 'UnaryExpression' ,
1510+ defaults : { operator : 'typeof' , prefix : true } ,
1511+ slots : { argument : { tsField : 'expression' } } ,
1512+ } ) ;
14381513
14391514function convertChildInner ( child : ts . Node , parent : LazyNode ) : LazyNode | null {
14401515 const ShapeCls = SHAPE_CLASSES . get ( child . kind ) ;
@@ -1448,16 +1523,12 @@ function convertChildInner(child: ts.Node, parent: LazyNode): LazyNode | null {
14481523 return new VariableDeclarationNode ( child as ts . VariableStatement , parent ) ;
14491524 case SK . VariableDeclaration :
14501525 return new VariableDeclaratorNode ( child as ts . VariableDeclaration , parent ) ;
1451- case SK . AsExpression :
1452- return new TSAsExpressionNode ( child , parent ) ;
14531526 case SK . TypeReference :
14541527 return new TSTypeReferenceNode ( child , parent ) ;
14551528 case SK . NumericLiteral :
14561529 return new LiteralNode ( child as ts . NumericLiteral , parent ) ;
14571530 case SK . StringLiteral :
14581531 return new LiteralNode ( child as ts . StringLiteral , parent ) ;
1459- case SK . ExpressionStatement :
1460- return new ExpressionStatementNode ( child , parent ) ;
14611532 case SK . Block :
14621533 return new BlockStatementNode ( child , parent ) ;
14631534 case SK . BinaryExpression : {
@@ -1510,8 +1581,6 @@ function convertChildInner(child: ts.Node, parent: LazyNode): LazyNode | null {
15101581 return new ImportDeclarationNode ( child as ts . ImportDeclaration , parent ) ;
15111582 case SK . ImportSpecifier :
15121583 return new ImportSpecifierNode ( child as ts . ImportSpecifier , parent ) ;
1513- case SK . NamespaceImport :
1514- return new ImportNamespaceSpecifierNode ( child , parent ) ;
15151584 case SK . ImportClause :
15161585 return new ImportDefaultSpecifierNode ( child as ts . ImportClause , parent ) ;
15171586 case SK . InterfaceDeclaration :
@@ -1522,8 +1591,6 @@ function convertChildInner(child: ts.Node, parent: LazyNode): LazyNode | null {
15221591 return new TSMethodSignatureNode ( child as ts . MethodSignature , parent ) ;
15231592 case SK . FunctionType :
15241593 return new TSFunctionTypeNode ( child , parent ) ;
1525- case SK . TypeQuery :
1526- return new TSTypeQueryNode ( child , parent ) ;
15271594 case SK . TypeOperator :
15281595 return new TSTypeOperatorNode ( child as ts . TypeOperatorNode , parent ) ;
15291596 case SK . LiteralType :
@@ -1563,8 +1630,6 @@ function convertChildInner(child: ts.Node, parent: LazyNode): LazyNode | null {
15631630 return new UnaryLikeExpressionNode ( child as ts . PrefixUnaryExpression , parent , true ) ;
15641631 case SK . PostfixUnaryExpression :
15651632 return new UnaryLikeExpressionNode ( child as ts . PostfixUnaryExpression , parent , false ) ;
1566- case SK . TypeOfExpression :
1567- return new TypeofExpressionNode ( child , parent ) ;
15681633 case SK . NamedTupleMember :
15691634 return convertNamedTupleMember ( child as ts . NamedTupleMember , parent ) ;
15701635 case SK . NewExpression :
@@ -1655,8 +1720,6 @@ function convertChildInner(child: ts.Node, parent: LazyNode): LazyNode | null {
16551720 }
16561721 case SK . ArrayBindingPattern :
16571722 return new ArrayPatternNode ( child , parent ) ;
1658- case SK . ObjectBindingPattern :
1659- return new ObjectPatternNode ( child , parent ) ;
16601723 case SK . BindingElement : {
16611724 // In ArrayBindingPattern, BindingElement resolves to the inner
16621725 // name directly (or wrapped in RestElement if `...`). Only
@@ -1724,10 +1787,6 @@ function convertChildInner(child: ts.Node, parent: LazyNode): LazyNode | null {
17241787 return null ; // handled inline by ClassNode
17251788 case SK . VariableDeclarationList :
17261789 return new VariableDeclarationListAsNode ( child as ts . VariableDeclarationList , parent ) ;
1727- case SK . VoidExpression :
1728- return new VoidExpressionNode ( child , parent ) ;
1729- case SK . DeleteExpression :
1730- return new DeleteExpressionNode ( child , parent ) ;
17311790 case SK . JsxElement :
17321791 case SK . JsxSelfClosingElement :
17331792 return new JSXElementNode ( child , parent ) ;
@@ -2112,20 +2171,6 @@ class VariableDeclaratorNode extends LazyNode {
21122171 }
21132172}
21142173
2115- class TSAsExpressionNode extends LazyNode {
2116- readonly type = 'TSAsExpression' as const ;
2117- private _expression ?: LazyNode | null ;
2118- private _typeAnnotation ?: LazyNode | null ;
2119-
2120- get expression ( ) {
2121- return this . _expression ??= convertChild ( ( this . _ts as ts . AsExpression ) . expression , this ) ;
2122- }
2123-
2124- get typeAnnotation ( ) {
2125- return this . _typeAnnotation ??= convertChild ( ( this . _ts as ts . AsExpression ) . type , this ) ;
2126- }
2127- }
2128-
21292174class TSTypeReferenceNode extends LazyNode {
21302175 readonly type = 'TSTypeReference' as const ;
21312176 private _typeName ?: LazyNode | null ;
@@ -2169,15 +2214,6 @@ class TypeKeywordNode extends LazyNode {
21692214 }
21702215}
21712216
2172- class ExpressionStatementNode extends LazyNode {
2173- readonly type = 'ExpressionStatement' as const ;
2174- directive : string | undefined = undefined ;
2175- private _expression ?: LazyNode | null ;
2176- get expression ( ) {
2177- return this . _expression ??= convertChild ( ( this . _ts as ts . ExpressionStatement ) . expression , this ) ;
2178- }
2179- }
2180-
21812217class BlockStatementNode extends LazyNode {
21822218 readonly type = 'BlockStatement' as const ;
21832219 private _body ?: ( LazyNode | null ) [ ] ;
@@ -2201,15 +2237,6 @@ class BlockStatementNode extends LazyNode {
22012237
22022238// Type-position nodes — direct 1:1 with typescript-estree's cases.
22032239
2204- class TSTypeQueryNode extends LazyNode {
2205- readonly type = 'TSTypeQuery' as const ;
2206- readonly typeArguments = undefined ;
2207- private _exprName ?: LazyNode | null ;
2208- get exprName ( ) {
2209- return this . _exprName ??= convertChild ( ( this . _ts as ts . TypeQueryNode ) . exprName , this ) ;
2210- }
2211- }
2212-
22132240class TSTypeOperatorNode extends LazyNode {
22142241 readonly type = 'TSTypeOperator' as const ;
22152242 readonly operator : 'keyof' | 'unique' | 'readonly' ;
@@ -2316,26 +2343,6 @@ class TSImportTypeNode extends LazyNode {
23162343 }
23172344}
23182345
2319- class VoidExpressionNode extends LazyNode {
2320- readonly type = 'UnaryExpression' as const ;
2321- readonly operator = 'void' as const ;
2322- readonly prefix = true as const ;
2323- private _argument ?: LazyNode | null ;
2324- get argument ( ) {
2325- return this . _argument ??= convertChild ( ( this . _ts as ts . VoidExpression ) . expression , this ) ;
2326- }
2327- }
2328-
2329- class DeleteExpressionNode extends LazyNode {
2330- readonly type = 'UnaryExpression' as const ;
2331- readonly operator = 'delete' as const ;
2332- readonly prefix = true as const ;
2333- private _argument ?: LazyNode | null ;
2334- get argument ( ) {
2335- return this . _argument ??= convertChild ( ( this . _ts as ts . DeleteExpression ) . expression , this ) ;
2336- }
2337- }
2338-
23392346// VariableDeclarationList appears in for-loop initializers (`for (let i = 0;...)`).
23402347// typescript-estree converts it to a VariableDeclaration with no `declare`.
23412348class VariableDeclarationListAsNode extends LazyNode {
@@ -2697,17 +2704,6 @@ class ArrayPatternNode extends LazyNode {
26972704 }
26982705}
26992706
2700- class ObjectPatternNode extends LazyNode {
2701- readonly type = 'ObjectPattern' as const ;
2702- readonly decorators : never [ ] = EMPTY_ARRAY ;
2703- readonly optional = false ;
2704- readonly typeAnnotation = undefined ;
2705- private _properties ?: ( LazyNode | null ) [ ] ;
2706- get properties ( ) {
2707- return this . _properties ??= convertChildren ( ( this . _ts as ts . ObjectBindingPattern ) . elements , this ) ;
2708- }
2709- }
2710-
27112707// Used when `[a = 1] = ...` and `{ b: c = 2 } = ...` — wraps the inner
27122708// pattern with a default value. typescript-estree's range covers from
27132709// the binding NAME (not the BindingElement's outer start, which would
@@ -3498,16 +3494,6 @@ class UnaryLikeExpressionNode extends LazyNode {
34983494 }
34993495}
35003496
3501- class TypeofExpressionNode extends LazyNode {
3502- readonly type = 'UnaryExpression' as const ;
3503- readonly operator = 'typeof' as const ;
3504- readonly prefix = true as const ;
3505- private _argument ?: LazyNode | null ;
3506- get argument ( ) {
3507- return this . _argument ??= convertChild ( ( this . _ts as ts . TypeOfExpression ) . expression , this ) ;
3508- }
3509- }
3510-
35113497// Export forms — typescript-estree picks ExportNamedDeclaration vs
35123498// ExportAllDeclaration vs ExportDefaultDeclaration vs TSExportAssignment
35133499// based on the structure. Mirror.
@@ -3973,15 +3959,6 @@ class ImportSpecifierNode extends LazyNode {
39733959 }
39743960}
39753961
3976- class ImportNamespaceSpecifierNode extends LazyNode {
3977- readonly type = 'ImportNamespaceSpecifier' as const ;
3978- private _local ?: LazyNode | null ;
3979-
3980- get local ( ) {
3981- return this . _local ??= convertChild ( ( this . _ts as ts . NamespaceImport ) . name , this ) ;
3982- }
3983- }
3984-
39853962// ImportClause maps to ImportDefaultSpecifier in ESTree (when it has a name).
39863963class ImportDefaultSpecifierNode extends LazyNode {
39873964 readonly type = 'ImportDefaultSpecifier' as const ;
0 commit comments