Skip to content

Report TS1003 for JSX tag and attribute names starting with # - #65431

Open
Ryan Cavanaugh (RyanCavanaugh) with Copilot wants to merge 7 commits into
mainfrom
copilot/fix-jsx-identifier-issue
Open

Ryan Cavanaugh (RyanCavanaugh) with Copilot wants to merge 7 commits into
mainfrom
copilot/fix-jsx-identifier-issue

Conversation

Copilot AI commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

JSX names starting with # parse without a syntax error and emit invalid JS (React.createElement(#a, null)). <#a> only gets a checker TS2304, which noCheck suppresses; attribute and namespaced names get no error at all.

The root cause: tokenIsIdentifierOrKeyword also matches KindPrivateIdentifier. As a result, ScanJsxIdentifier turned #a into an ordinary identifier, and the parser created an Identifier with text #a.

<#a></#a>;    // now TS1003: Identifier expected.
<div #b />;   // now TS1003
<div #c:d />; // now TS1003
<a:#e />;     // now TS1003

<a.#g /> already errored, but only because parseRightSideOfDot checks for private identifiers before identifier names.

Changes

  • Scanner: ScanJsxIdentifier keeps KindPrivateIdentifier instead of converting it to an identifier. It still appends JSX identifier parts, so #a-b stays one token.

  • Parser: the new parseJsxIdentifierName replaces the four JSX name call sites (tag name, attribute name, and both sides of a namespaced name). On a private identifier it:

    • reports TS1003 on the token;
    • consumes the token;
    • returns a missing identifier.

    This follows the <a.#g /> path. The missing name means no TS2304 follows, and emit no longer contains #.

  • astnav: the skipped token isn't covered by any node. When getTokenAtPosition rescanned the gap, the regular scanner split #a-b into #a, -, b. The trailing identifier then hit the did not expect … in its trivia panic, e.g. on hover inside <div #n-o={1} />.

    • scanNavigationToken now rescans such a private identifier with ScanJsxIdentifier, matching how the parser tokenized it.
    • This applies inside JSX opening, closing and self-closing elements, attributes and namespaced names, and in the leftmost part of a dotted tag name (<#a-b.c />).
    • It is skipped for <a.#b-c />, where the parser read #b with the regular scanner.

Notes for review

  • Emit for these error cases now contains an empty name where the # name was (React.createElement(, null), { "": true }).
  • Alternatives rejected:
    • Narrowing tokenIsIdentifierOrKeyword globally: class members like #foo depend on it.
    • Keeping the #a Identifier: it cascades into checker errors and still emits invalid JS.
    • Not extending #a-b with dashes: -b then produces cascading parse errors.

Tests

  • jsxNamesStartingWithHash.tsx covers the issue cases plus namespaced and hyphenated variants, for jsx: preserve, react.
  • TestGetTokenAtPositionJsxNameStartingWithHash in tsc/internal/astnav/tokens_test.go panicked before the astnav change.

Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
@typescript-automation typescript-automation Bot added For Uncommitted Bug PR for untriaged, rejected, closed or missing bug and removed For Milestone Bug PRs that fix a bug with a specific milestone labels Nov 10, 2026
Copilot AI and others added 4 commits November 10, 2026 18:04
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix JSX tag and attribute names starting with # handling Report TS1003 for JSX tag and attribute names starting with # Nov 10, 2026
@RyanCavanaugh

Copy link
Copy Markdown
Member

Copilot you said:

Not extending #a-b with dashes: -b then produces cascading parse errors.

No one cares, this is not a likely thing to write and cascading parse errors are not a violated invariant. Can you simplify this PR given this instruction?

Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>

Copilot AI commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Copilot you said:

Not extending #a-b with dashes: -b then produces cascading parse errors.
...

Simplified in 5f999e9. ScanJsxIdentifier now leaves a private identifier untouched instead of extending it with dashes, so the skipped token is exactly #a, which the regular scanner rescans identically. That made the astnav rescan workaround and its test unnecessary, so both are reverted. The PR is now just the 1-line scanner guard, parseJsxIdentifierName in the parser, and the compiler test. I dropped the hyphenated cases from the test.

@RyanCavanaugh
Ryan Cavanaugh (RyanCavanaugh) marked this pull request as ready for review November 10, 2026 19:43
Copilot AI balanced review requested due to automatic review settings November 10, 2026 19:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The scanner truncates hyphenated private JSX attribute names, causing cascading parse errors rather than recovering at the attribute value.

1 open issue
What changed in this PR

This PR addresses #65396 by reporting syntax errors for JSX tag and attribute names starting with # in the native TypeScript compiler.

Changes:

  • Prevents private identifiers from being converted into ordinary JSX identifiers.
  • Adds parser recovery that reports TS1003 and produces missing identifiers.
  • Adds regression cases and diagnostic, emit, type, and symbol baselines for React and preserved JSX.
File Description
tsc/​testdata/​tests/​cases/​compiler/​jsxNamesStartingWithHash.tsx Adds invalid JSX name regression cases.
tsc/​testdata/​baselines/​reference/​compiler/​jsxNamesStartingWithHash(jsx=react).types Records React-mode inferred types.
tsc/​testdata/​baselines/​reference/​compiler/​jsxNamesStartingWithHash(jsx=react).symbols Records React-mode symbol bindings.
tsc/​testdata/​baselines/​reference/​compiler/​jsxNamesStartingWithHash(jsx=react).js Records React emit after error recovery.
tsc/​testdata/​baselines/​reference/​compiler/​jsxNamesStartingWithHash(jsx=react).errors.txt Records React-mode syntax diagnostics.
tsc/​testdata/​baselines/​reference/​compiler/​jsxNamesStartingWithHash(jsx=preserve).types Records preserve-mode inferred types.
tsc/​testdata/​baselines/​reference/​compiler/​jsxNamesStartingWithHash(jsx=preserve).symbols Records preserve-mode symbol bindings.
tsc/​testdata/​baselines/​reference/​compiler/​jsxNamesStartingWithHash(jsx=preserve).js Records preserved JSX after error recovery.
tsc/​testdata/​baselines/​reference/​compiler/​jsxNamesStartingWithHash(jsx=preserve).errors.txt Records preserve-mode syntax diagnostics.
tsc/​internal/​scanner/​scanner.go Keeps private identifiers from becoming ordinary JSX identifiers.
tsc/​internal/​parser/​parser.go Rejects private identifiers at JSX name parsing sites.

Review effort: Balanced

Comment thread tsc/internal/scanner/scanner.go Resolved

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JSX tag and attribute names starting with # are accepted without a syntax error

3 participants