compat-eslint: KnownEstreeType union β compile-time gate against phantom types#90
Merged
Merged
Conversation
β¦ gate
Introduces a `KnownEstreeType` string-literal union listing every
ESTree type lazy-estree is allowed to produce (~135 entries: core
ESTree + JSX + TS-prefixed types + 13 TS keyword types). Used to
constrain three places:
- `LazyNode.type` (abstract): every concrete subclass's `type`
literal must be in the union or compilation fails.
- `defineShape({ type, ... })`: same for factory-built shapes.
- `TypeKeywordNode` constructor parameter narrowed from `string`
to a 13-entry `TypeKeyword` subset so the dynamic dispatch
can't accept a non-keyword name.
When the gate was added it caught one missing entry (`AccessorProperty`
on PropertyDefinitionNode's union) on first build β exactly the bug
class this gate exists to prevent. Added it to the union; build clean.
Closes the bug class from PR #88 at the type level: the phantom
`'TSJsxAttributes'` regression that started this whole refactor
would now be a compile error, not a silent silent-replay-stale bug
that needs a parity test to catch. Runtime phantom-types invariant
(in lazy-estree.test) still backs this up for GenericTSNode's
deliberate dynamic-output escape valve, which has to use `as` to
satisfy the constraint and is exhausted by the parity sweep.
Adding a new ESTree shape now requires editing `KnownEstreeType`
first β explicit declaration, single source of truth.
All compat-eslint suites still pass: 152/152 predicate coverage,
24/24 scope-compat, full bottom-up parity sweep + phantom-types
invariant. Self-lint clean across 6 packages.
Addresses PR #90 review. Phantom-types invariant (lazy-estree.test): the hand-coded set of ~80 typescript-estree TS-* types is now derived from `@typescript-eslint/typescript-estree`'s exported `AST_NODE_TYPES` enum. When upstream adds a new type, the test auto-picks it up with no maintenance β and conversely, if our KnownEstreeType ever drifts to include a non-existent name, fixture runs catch it (any produced 'TS<KindName>' not in the enum is flagged as phantom). Drops 80 lines of hand-listed strings from the test file. Also: comment on TypeKeywordNode said "14 near-identical declarations" β there are 13 keyword types. Off by one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The bug class fixed by PR #88 β phantom ESTree types like
'TSJsxAttributes'slipping through hand-written subclasses β was caught by runtime tests. PRs #88 and #89 reduced the surface but the test gate remained the only line of defence: any future subclass could still declarereadonly type = 'X' as constfor an X that doesn't exist in typescript-estree's spec, and it'd compile fine.This PR closes that hole at the type level. Every concrete
LazyNodesubclass'stypeliteral must now be a member ofKnownEstreeTypeβ a string-literal union enumerating the ~135 ESTree types lazy-estree is allowed to produce. Adding a new shape requires editing the union first (explicit, reviewable). Phantom types are now a compile error, not a silent runtime bug.The gate caught one already
When the constraint was added on the existing codebase, TypeScript immediately flagged
AccessorPropertyonPropertyDefinitionNode'stypeunion β exactly the kind of "obscure type literal not in spec" that the gate exists to catch. (Turned out to be a real ESTree type; added it to the union.) Validates the design: had this gate existed before PR #88, the original'TSJsxAttributes'regression would have failed atpnpm buildinstead of needing a runtime parity test to surface.What's constrained
KnownEstreeTypeis built from typescript-estree's published node-type list:Identifier,BlockStatement, β¦)JSXAttribute,JSXOpeningElement, β¦)TSAsExpression,TSTypeReference, β¦)TSAnyKeyword,TSStringKeyword, β¦)GenericTSNodeis the deliberate phantom-output safety valve. Itstypeis dynamic ('TS' + ts.SyntaxKind[kind]) and castsas KnownEstreeTypeβ most produced types ARE in the union (TSEnumDeclaration,TSImportType, etc.), but some (TSJsxAttributes,TSEndOfFileToken) are not. The runtime phantom-types invariant inlazy-estree.test(already in master) asserts these never reach a position rules can observe β they exist only as transient walk-up objects before being shadowed by a real subclass. The compile-time gate + runtime invariant together cover the whole drift surface.Bug class status after this PR
typeliteraldefineShapedeclares phantomtypeTypeKeywordNodeconstructed with non-keywordGenericTSNodeproduces phantom transientlyWhat's NOT in this PR
GenericTSNode-style dynamic types must be enumerated, AND every synthetic-creating hand-written subclass declares its synthetics in a registry that the navigation tables auto-derive from. Separate branch.this" + "range mutation hook" capabilities, but the audit showed the marginal class is increasingly complex; pivoted this branch to the higher-leverage architectural gate. Future migrations land as separate small PRs as needed.Test plan
pnpm buildclean (compile-time gate active)KnownEstreeTypeis a TS-only construct, erased at compile time