Repository navigation
Conversation
|
Install the latest version of pnpm add https://pkg.svelte.dev/esrap/c/a4d5642a56f4f4e57fa195e2f002c00ecce7f073Open in Note This PR is from a fork. A maintainer must approve approve each commit before it can be built and installed. |
🦋 Changeset detectedLatest commit: a4d5642 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
This was referenced Sep 24, 2026
leonidaz
force-pushed
the
feat/tokens
branch
from
September 24, 2026 13:22
ff4ea86 to
b0448af
Compare
The root visitor maps every node's start and end, which covers tokens that open a node. Tokens inside a node have no node boundary to anchor them: the brackets of a computed key, the parentheses of parameter lists, calls and control-flow heads, the braces of import and export lists, static blocks, switch and enum bodies, template interpolations, and every keyword written after a node's first token (`function` after `async`, `else`, `finally`, `while` in `do … while`, `default` after `export`, `type` after `import`, class and member modifiers, `extends`, `implements`, `in`, `of`, `as`, `satisfies`). A new `tokens` option takes the parser's token list (Acorn's `onToken`, Babel's `tokens`, ESLint-style `ast.tokens`). The tokens of interest are indexed once, sorted if the parser emitted them out of order, and each one is found by a bounded binary search from a position the AST does know. Without tokens they stay unmapped. Closing delimiters that end a node (`)` of a call, `]` of an array or computed member, `}` of an object, switch, enum or interface body) are mapped from the node's end without tokens, as BlockStatement already does. Printed output is unchanged; 61 sample maps gain segments. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
leonidaz
force-pushed
the
feat/tokens
branch
from
September 24, 2026 13:26
b0448af to
f28b511
Compare
A decorated class's decorators are printed before `export`, so the root visitor cannot map the export node's start where it is written. Emit that start once the decorators are out: the export node begins at `export` with both Acorn and typescript-estree, whichever side of it the decorators were written on. Instead of clearing the declaration's decorators and location while it is printed, remember that its decorators were printed. The root visitor then maps its start when that start is the `class` keyword (typescript-estree, decorators written before `export`) and skips it when it is a decorator (Acorn, see sveltejs/acorn-typescript#117), and the keyword lookup can locate `class`, `abstract` and the rest from the tokens because the node keeps its location. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Deferred from the first version of this branch until sveltejs#212 gave the catch clause its own visitor. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
sveltejs#219 prints `import type {} from` as one string; route its keyword and braces through the lookups like the non-empty form. Also mention keywords in the `tokens` option's documentation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit f573c055666c07224f7666c2fb4a157d63d510de)
This was referenced Sep 25, 2026
Member
|
Closing since #235 was merged |
This was referenced Sep 27, 2026
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.
Replaces #183, rebuilt on top of #198 and #199. Follows the rule from #198: positions come from the AST or from the parser, never from arithmetic.
Problem
Since #198 every node's start and end is mapped, so a token that opens a node (
[of a computedProperty,{of an object,if,class) is covered. Tokens inside a node have no node boundary to anchor them and stay unmapped:]of a computed key, and both brackets of a computed class member ([ Symbol.iterator ]())(and)of parameter lists, calls, andif/for/while/switch/do … whileheads{and}of import and export specifier lists and import attributes, of static blocks, switch and enum bodies${and}in template literals and template-literal typesfunctionafterasyncordeclare,else,finally,whileindo … while,defaultafterexport,typeafterimport/export,classafterabstract, the second and later modifiers of a class member,extends,implements,in,of,as,satisfies,enum,interface,namespaceTypeScript reports on ranges that start or end on these: TS2416 / TS2464 / TS1170 on
[ Symbol.iterator ](brackets included), TS1040 onasync, TS1029 / TS1030 on a modifier, TS1103 onawaitinfor await, and return-type inlay hints right after the parameter list's), which with prettier's default trailing commas sits alone on its line. The repro from #183 (comment) on 2.3.11 maps the class member to[ Symbol.iteratorand drops the inlay hint. This is also the exact fix for #190:for /* foo */ await /* bar */ (…)mapsawaitto the token instead of toforplus four characters.Change (
minor)tokensoption onts()/tsx(): the parser's token list, in any of the common shapes (AcornonTokenwithtype.label/type.keyword, Babeltokens, ESLint-style{ type: 'Punctuator' | 'Keyword' | 'Identifier', value }). Its presence is the switch; there is no other flag.src/languages/ts/tokens.jspulls the delimiters and the keywords listed above out of the token list on first use, sorts them if the parser emitted them out of order (acorn-typescript does when it backtracks), and looks one up by a bounded binary search from a position the AST does know: the(after the callee's end, the]after the key's end, the)before the body's start, theelseafter the consequent's end, the firstfunctionafter the node's start (or after its last decorator). Never leaving the node being printed keeps nested tokens apart. Withouttokensevery lookup returnsundefinedand the token is written unmapped, exactly as today.Closers at a node's end (
)of a call,]of an array, tuple or computed member,}of an object, switch, enum, mapped type or interface body) are mapped fromnode.loc.endwithout tokens, the wayBlockStatementalready maps its}. Those nodes end with that character by grammar.new Foomay have no parentheses, soNewExpressionuses the token lookup instead.In the printer this is one-line changes of the form
plus
write_params, which replaces nine copies of "write(, print the parameters, write)". Printed output is byte-identical with and without tokens; 62 sample maps gain segments and no code snapshot changes.Coverage on the samples from the #183 thread (86 delimiters; 107 keyword positions):
main(2.3.13)What is left with tokens: the
in/instanceofoperators, which are out of scope here.One estimate removed on the way
handle_var_declarationwrites the leading keywords with a single call:token()maps the start fromnode.loc.start, which is exact because the node begins with the keyword, and the end atstart + text.length. For a plainconstthat is exact. Fordeclare constthe text is"declare const", so the end lands at start + 13, which is only right when there is exactly one space between the two words;declare const xordeclare /* c */ const xputs that end segment in the wrong place. The same kind of estimate #192 removed elsewhere.Now:
declarekeeps its exact start and end. The kind after it comes from the tokens, so with tokensconstmaps to its real position wherever it sits, and without tokens it is unmapped. That is the only place where the no-tokens map differs frommain(thets-declaresample loses that one end segment). Plainconst/let/var, including inforheads, are unchanged without tokens.Decorated declarations (after #227)
Follows up #227 (comment). Two things, in the second commit:
exportof a decorated class is mapped without tokens. The export node starts atexportwith both Acorn and typescript-estree, whichever side of it the decorators were written on, so once the decorators are printed the visitor emits that start itself. The root visitor skips it for an export whose declaration has decorators, so nothing maps the@column toexportany more (@dec export class,export @dec class, and thedefaultvariants are tested).class,abstract,declareand the member modifiers after decorators come from the tokens.visit_without_decoratorsno longer clears the declaration'slocanddecorators; it records that the decorators were printed, andblock_decoratorsand the root visitor consult that. The node keeps its location, sowrite_keywordcan locateclassafter the last decorator. The root visitor maps the node's start when that start is theclasskeyword and skips it when it is a decorator. The two parsers differ here: for@dec export class K {}typescript-estree ranges the class fromclass(12-22), Acorn from the decorator (0-22, before its parent export node 5-22). Filed as Class range starts at decorators written beforeexport, ahead of the parent export node (typescript-estree starts it atclass) acorn-typescript#117. With Acorn theclassposition is therefore only known from the tokens, which is the case Rich described in that comment.Tests
sourcemap-keywords.test.js: for each construct, every delimiter or keyword of a kind in the output maps to the matching source token and the output is identical with and without tokens (brackets: 18 cases, parentheses: 27, braces: 11, keywords: 32, each with comments between the tokens; decorated exports and members: 8), plus: outer parentheses chosen forif ((x)), internal tokens unmapped without tokens, closers mapped without tokens, tokens without locations, empty tokens, out-of-order tokens, ESLint-style tokens.Validation:
pnpm test— 518 tests passedpnpm lint— passedpnpm check— the two pre-existing TS2580Buffererrors insrc/index.jsonmain, nothing newCommits: the squashed feature, merges of
main(mine after #212 / #214 / #215 / #227, then Rich's after #217–#223 and #233), the decorated-declaration follow-up, thecatchparentheses, and the braces of an empty type-only import (import type {} from, which #219 prints as one string).🤖 Generated with Claude Code