Repository navigation
Report TS1003 for JSX tag and attribute names starting with # - #65431
Ryan Cavanaugh (RyanCavanaugh) with Copilot wants to merge 7 commits into
Conversation
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>
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
# handling#
|
Copilot you said:
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>
Simplified in 5f999e9. |
There was a problem hiding this comment.
🟡 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

JSX names starting with
#parse without a syntax error and emit invalid JS (React.createElement(#a, null)).<#a>only gets a checker TS2304, whichnoChecksuppresses; attribute and namespaced names get no error at all.The root cause:
tokenIsIdentifierOrKeywordalso matchesKindPrivateIdentifier. As a result,ScanJsxIdentifierturned#ainto an ordinary identifier, and the parser created anIdentifierwith text#a.<a.#g />already errored, but only becauseparseRightSideOfDotchecks for private identifiers before identifier names.Changes
Scanner:
ScanJsxIdentifierkeepsKindPrivateIdentifierinstead of converting it to an identifier. It still appends JSX identifier parts, so#a-bstays one token.Parser: the new
parseJsxIdentifierNamereplaces the four JSX name call sites (tag name, attribute name, and both sides of a namespaced name). On a private identifier it: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
getTokenAtPositionrescanned the gap, the regular scanner split#a-binto#a,-,b. The trailing identifier then hit thedid not expect … in its triviapanic, e.g. on hover inside<div #n-o={1} />.scanNavigationTokennow rescans such a private identifier withScanJsxIdentifier, matching how the parser tokenized it.<#a-b.c />).<a.#b-c />, where the parser read#bwith the regular scanner.Notes for review
#name was (React.createElement(, null),{ "": true }).tokenIsIdentifierOrKeywordglobally: class members like#foodepend on it.#aIdentifier: it cascades into checker errors and still emits invalid JS.#a-bwith dashes:-bthen produces cascading parse errors.Tests
jsxNamesStartingWithHash.tsxcovers the issue cases plus namespaced and hyphenated variants, forjsx: preserve, react.TestGetTokenAtPositionJsxNameStartingWithHashintsc/internal/astnav/tokens_test.gopanicked before the astnav change.#are accepted without a syntax error #65396