From 6ec89f4334d69103276a00df8331d36c688c4942 Mon Sep 17 00:00:00 2001 From: swapnil Date: Thu, 15 Oct 2026 00:28:01 -0700 Subject: [PATCH 1/2] parser(ts): a plain JSX tag is an element row, never a call `
` produced no expression row in the TypeScript parser, so nothing in the IR said a handler or a ref callback sat on a plain tag: an attribute's value existed only as an unattached embedded expression. The JavaScript parser records the same tag as an element. An intrinsic tag (`
`, ``, ``) is now a JSX_INTRINSIC_ELEMENT row with no child and no call site, rooted wherever it sits (a return, a ternary branch, another element's children, a function inside an attribute). It is never added to the call nodes, so nothing can resolve it into JSX.IntrinsicElements. Component tags keep their JSX_ELEMENT / JSX_SELF_CLOSING rows and JSX_COMPONENT_CALL sites. --- .../expressions/TsExpressionKind.ts | 14 ++++- .../extractors/ts-expression-extractor.ts | 12 +++-- .../extractors/ts-expression-walker.ts | 10 ++-- parser/src/schema/typescript/schema.json | 1 + parser/src/test/typescript-tests.ts | 54 +++++++++++++++++-- 5 files changed, 77 insertions(+), 14 deletions(-) diff --git a/parser/src/enums/typescript/expressions/TsExpressionKind.ts b/parser/src/enums/typescript/expressions/TsExpressionKind.ts index 6d42c2088..ebb5b03d2 100644 --- a/parser/src/enums/typescript/expressions/TsExpressionKind.ts +++ b/parser/src/enums/typescript/expressions/TsExpressionKind.ts @@ -33,8 +33,10 @@ * A JSX element whose tag names a component IS a call to it, with the whole * props object as argument 0: JSX_ELEMENT / JSX_SELF_CLOSING, 1:1 with a call * site, with the tag as its METHOD_NAME child. Attribute values and children are - * rooted as their own JSX_EMBEDDED_EXPRESSION trees. An intrinsic element emits - * no row. + * rooted as their own JSX_EMBEDDED_EXPRESSION trees. An intrinsic element + * (`
`, ``, ``) is JSX_INTRINSIC_ELEMENT: a row with no + * children and no call site, so it exists only as the element its attributes + * belong to. * * Schema §4.14 c0, §4.15.1. */ @@ -99,6 +101,14 @@ export enum TsExpressionKind { JSX_ELEMENT = 'JSX_ELEMENT', /** `` naming a component. 1:1 with a call site. */ JSX_SELF_CLOSING = 'JSX_SELF_CLOSING', + /** + * `
`, ``, `` -- a tag that names no + * declaration. NOT a call: no call site, no METHOD_NAME child, so nothing can + * resolve it into `JSX.IntrinsicElements`. The row exists so that a plain + * tag's attributes (an event handler, a `ref` callback) have an element to + * belong to, as the JavaScript parser's JSX_INTRINSIC_ELEMENT does. + */ + JSX_INTRINSIC_ELEMENT = 'JSX_INTRINSIC_ELEMENT', /** `delete a`, `typeof a`, `void a` — `operatorString` says which. */ DELETE_TYPEOF_VOID = 'DELETE_TYPEOF_VOID', /** `a, b` — the comma operator. */ diff --git a/parser/src/parsers/typescript/extractors/ts-expression-extractor.ts b/parser/src/parsers/typescript/extractors/ts-expression-extractor.ts index f4183b369..5c86cf7c6 100644 --- a/parser/src/parsers/typescript/extractors/ts-expression-extractor.ts +++ b/parser/src/parsers/typescript/extractors/ts-expression-extractor.ts @@ -47,7 +47,9 @@ import { isVueTemplateTag } from '@/utils/vue-sfc'; * * A JSX element naming a component is a call (§4.15.1): a JSX_ELEMENT or * JSX_SELF_CLOSING row with its tag as the METHOD_NAME child, and a - * JSX_COMPONENT_CALL site. `JSX_ATTRIBUTE_VALUE` and `JSX_CHILD` stay reserved: + * JSX_COMPONENT_CALL site. An intrinsic element (`
`) is a + * JSX_INTRINSIC_ELEMENT row with no child and no call site. + * `JSX_ATTRIBUTE_VALUE` and `JSX_CHILD` stay reserved: * attribute values and children are rooted as their own trees by the walker, and * the gate asserts the emptiness so that switching them on shows up as a failure. */ @@ -453,10 +455,12 @@ export function expressionKindOf(node: ts.Node): TsExpressionKind | undefined { case ts.SyntaxKind.JsxElement: case ts.SyntaxKind.JsxSelfClosingElement: { // `` IS a call to Badge. `
` is an intrinsic element that no - // project declaration can bind, so it produces no row -- a call site that - // can only ever resolve into `JSX.IntrinsicElements` would be noise. + // project declaration can bind: a call site that can only ever resolve + // into `JSX.IntrinsicElements` would be noise, so it is its own kind, + // which is never a call (see `emit`) and carries no tag child. The row is + // what a plain tag's attributes -- `onClick={h}`, `ref={r}` -- belong to. if (jsxTagReference(node) === undefined) { - return undefined; + return TsExpressionKind.JSX_INTRINSIC_ELEMENT; } return ts.isJsxElement(node) ? TsExpressionKind.JSX_ELEMENT : TsExpressionKind.JSX_SELF_CLOSING; } diff --git a/parser/src/parsers/typescript/extractors/ts-expression-walker.ts b/parser/src/parsers/typescript/extractors/ts-expression-walker.ts index 1b82d1018..5fda834bf 100644 --- a/parser/src/parsers/typescript/extractors/ts-expression-walker.ts +++ b/parser/src/parsers/typescript/extractors/ts-expression-walker.ts @@ -2,7 +2,6 @@ import * as ts from 'typescript'; import { ExpressionOwner, - jsxTagReference, TsExpressionExtractor, } from '@/parsers/typescript/extractors/ts-expression-extractor'; import { TsExpressionOwnerKind, TsRootContext } from '@/enums/typescript/expressions'; @@ -513,11 +512,12 @@ export class TsExpressionWalker { // attribute values, children, and spreads at once, wherever the JSX sits. // // A component ELEMENT is a call in its own right: `` calls Badge. - // One that is already a row -- `cond ? : `, `x && ` -- was - // reached by the worklist as an operand; any other is rooted here, which - // is what reaches an element nested in another element's children. + // An intrinsic one (`
`) is a JSX_INTRINSIC_ELEMENT row and no call, + // the element its attributes belong to. One that is already a row -- + // `cond ? : `, `x &&
` -- was reached by the worklist as an + // operand; any other is rooted here, which is what reaches an element + // nested in another element's children. if ((ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) - && jsxTagReference(child) !== undefined && !this.options.extractor.rowByNode.has(nodeId(child, this.sf))) { this.root(child, TsRootContext.JSX_EMBEDDED_EXPRESSION); return; diff --git a/parser/src/schema/typescript/schema.json b/parser/src/schema/typescript/schema.json index 590c68a26..b91264822 100644 --- a/parser/src/schema/typescript/schema.json +++ b/parser/src/schema/typescript/schema.json @@ -829,6 +829,7 @@ "FUNCTION_EXPRESSION", "IDENTIFIER_REFERENCE", "JSX_ELEMENT", + "JSX_INTRINSIC_ELEMENT", "JSX_SELF_CLOSING", "LITERAL", "NEW_EXPRESSION", diff --git a/parser/src/test/typescript-tests.ts b/parser/src/test/typescript-tests.ts index fd295003d..d10428fd9 100644 --- a/parser/src/test/typescript-tests.ts +++ b/parser/src/test/typescript-tests.ts @@ -1414,7 +1414,9 @@ function irCompleteness(): number { * The component element is a call too: `` is `Badge({...})`, a * JSX_COMPONENT_CALL site, wherever it sits -- a child, a ternary branch, the * body of a `.map` arrow. The CONTROL is the intrinsic element: `
` and - * `` name no declaration and must produce no call site at all. + * `` name no declaration and must produce no call site at all. It is + * still an element, a childless JSX_INTRINSIC_ELEMENT row, so that + * `', + ' {flag && }', + ' ', '
', ' );', '}', @@ -1493,6 +1500,15 @@ async function jsxBraceExpressionsWalked(): Promise { } } let expected = 0; + // Expression rows by position, so an element's row can be asserted. + const exprRows = relation(outputDir, 'all-typescript-expressions.csv') + .filter((r) => (modulePath.get(r.tsModuleLinkHash ?? '') ?? '').endsWith('panel.tsx')); + const exprKindsAt = new Map(); + for (const r of exprRows) { + const key = `panel.tsx:${r.startLine}:${r.startColumn}`; + exprKindsAt.set(key, [...(exprKindsAt.get(key) ?? []), r.kind ?? '']); + } + const parents = new Set(exprRows.map((r) => r.parentExpressionHash ?? '')); let components = 0; let intrinsics = 0; const walk = (node: ts.Node): void => { @@ -1507,12 +1523,43 @@ async function jsxBraceExpressionsWalked(): Promise { failures.push(`no JSX_COMPONENT_CALL at ${key} for \`${node.getText(sf).slice(0, 44)}\`` + ` (found ${kindAt.get(key) ?? 'nothing'})`); } + // The control: a component keeps its call-shaped row, never the intrinsic kind. + const want = ts.isJsxElement(node) ? 'JSX_ELEMENT' : 'JSX_SELF_CLOSING'; + const kinds = exprKindsAt.get(key) ?? []; + if (!kinds.includes(want) || kinds.includes('JSX_INTRINSIC_ELEMENT')) { + failures.push(`component at ${key} has expression kind(s) [${kinds.join(', ')}], ` + + `expected ${want}`); + } } else { intrinsics += 1; if (emitted.has(key)) { failures.push(`intrinsic \`<${tag.getText(sf)}>\` at ${key} became a call site ` + `(${kindAt.get(key)}) -- it names no declaration`); } + // ...but it IS an element: one JSX_INTRINSIC_ELEMENT row, with no child + // (no tag reference for anything to resolve), so its attributes have an + // element to belong to, as in the JavaScript parser. + const rows = exprRows.filter((r) => `panel.tsx:${r.startLine}:${r.startColumn}` === key + && r.kind === 'JSX_INTRINSIC_ELEMENT'); + if (rows.length !== 1) { + failures.push(`intrinsic \`<${tag.getText(sf)}>\` at ${key} has ${rows.length} ` + + 'JSX_INTRINSIC_ELEMENT row(s), expected 1'); + } else if (parents.has(rows[0]!.tsExpressionUniqueHash ?? '')) { + failures.push(`intrinsic \`<${tag.getText(sf)}>\` at ${key} has a child row -- ` + + 'its tag references nothing, so nothing hangs off it'); + } + // An attribute's value is still its own rooted tree. + const attributes = ts.isJsxElement(node) ? node.openingElement.attributes : node.attributes; + for (const attribute of attributes.properties) { + if (ts.isJsxAttribute(attribute) && attribute.initializer + && ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression) { + const v = sf.getLineAndCharacterOfPosition(attribute.initializer.expression.getStart(sf)); + const vkey = `panel.tsx:${v.line + 1}:${v.character + 1}`; + if ((exprKindsAt.get(vkey) ?? []).length === 0) { + failures.push(`attribute \`${attribute.getText(sf)}\` at ${vkey} has no value row`); + } + } + } } } if (ts.isCallExpression(node) || ts.isNewExpression(node)) { @@ -1545,7 +1592,8 @@ async function jsxBraceExpressionsWalked(): Promise { console.log(` ${expected} call/new node(s) in JSX attributes, children, spreads and a ` + `nested map; ${expected - missingCalls} emitted; ${components} component element(s) as ` + - `calls, ${intrinsics} intrinsic element(s) as none; ${RESERVED_TSX_VALUES.size} reserved ` + + `calls, ${intrinsics} intrinsic element(s) as JSX_INTRINSIC_ELEMENT rows and no call; ` + + `${RESERVED_TSX_VALUES.size} reserved ` + 'value(s) still empty'); for (const f of failures.slice(0, 10)) console.log(` ${f}`); fs.rmSync(root, { recursive: true, force: true }); @@ -4721,7 +4769,7 @@ const CHECKS: Check[] = [ { name: 'tsc-adjudicated resolution', proves: 'every resolved call target equals getResolvedSignature', run: tscAdjudicatedResolution }, { name: 'type-only isolation', proves: 'no type-only construct reaches the call graph', run: typeOnlyIsolation }, { name: 'TSX reserved but empty', proves: 'the JSX attribute and child edge roles carry no rows', run: tsxReservedButEmpty }, - { name: 'JSX brace expressions are walked', proves: 'a call inside a JSX brace is an ordinary call site, a component element is a JSX_COMPONENT_CALL, an intrinsic element is none', run: jsxBraceExpressionsWalked }, + { name: 'JSX brace expressions are walked', proves: 'a call inside a JSX brace is an ordinary call site, a component element is a JSX_COMPONENT_CALL, an intrinsic element is a childless JSX_INTRINSIC_ELEMENT row and no call', run: jsxBraceExpressionsWalked }, { name: 'destructuring records its source', proves: 'a bound name carries the property or index it binds, so a renamed or positional binding is recoverable', run: destructuringRecordsItsSource }, { name: 'no emitted value can split a row', proves: 'no value contains a character a consumer treats as a line break, so a row cannot tear', run: noValueCanSplitARow }, { name: 'signatures link their return type', proves: 'a call through any callable shape reaches a result type, so a chain does not stop at it', run: signaturesLinkTheirReturnType }, From 47dacac63f27d1faa028791e27b41ed87e53b35f Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:38:02 -0700 Subject: [PATCH 2/2] parser(ts): a plain JSX element's row names its tag A JSX_INTRINSIC_ELEMENT row said a plain tag was there but not which one: `
`, `
` and `