Skip to content

Commit 2131195

Browse files
committed
refactor(compat-eslint): factory gains defaults / via-callback / whenAbsent
Phase 1 of the A3 follow-up: extend defineShape's capabilities so more hand-written subclasses become declarative. Three new ShapeDef options: defaults: Record<string, unknown> Per-instance static field assignment (matches `readonly x = '...'` class-field semantics). Unlocks shapes whose ESTree side has fixed-value fields like UnaryExpression's `operator: 'void'`, ObjectPattern's `decorators: EMPTY_ARRAY`. ShapeSlotDef.via: function Custom converter callback for slots that go through helper functions (`convertTypeAnnotation`, `convertTypeArguments`, `convertJSXTagName`, etc.) instead of the stock convertChild family. Unlocks type-position slots that wrap in synthetic intermediates. ShapeSlotDef.whenAbsent: 'null' | 'undefined' Per-slot absent-value choice (default 'null'). Type-position slots like `returnType` / `typeAnnotation` use 'undefined' to match typescript-estree's distinction. Backed by SHAPE_UNSET sentinel so the cache distinguishes "not yet computed" from "computed and got null/undefined" — the existing `??=` pattern in hand-written getters conflates the two. 8 new mechanical-with-defaults migrations using the new capabilities: AsExpression → TSAsExpression ExpressionStatement (with directive default) TypeQuery → TSTypeQuery (with typeArguments default) NamespaceImport → ImportNamespaceSpecifier ObjectBindingPattern → ObjectPattern (with decorators / optional / typeAnnotation defaults) VoidExpression → UnaryExpression(void) DeleteExpression → UnaryExpression(delete) TypeOfExpression → UnaryExpression(typeof) Total: 38 SHAPES entries (up from 30), 115 hand-written subclasses (down from 123). Net -23 lines on top of merged baseline. All compat-eslint suites pass.
1 parent 54ec512 commit 2131195

1 file changed

Lines changed: 83 additions & 106 deletions

File tree

packages/compat-eslint/lib/lazy-estree.ts

Lines changed: 83 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -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);
12121216
interface 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');
12161235
interface 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
}
12241248
const SHAPE_CLASSES = new Map<ts.SyntaxKind, new(tsNode: ts.Node, parent: LazyNode | null) => LazyNode>();
12251249
function 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

14391514
function 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-
21292174
class 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-
21812217
class 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-
22132240
class 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`.
23412348
class 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).
39863963
class ImportDefaultSpecifierNode extends LazyNode {
39873964
readonly type = 'ImportDefaultSpecifier' as const;

0 commit comments

Comments
 (0)