Skip to content

fix(core): map whole calls and parenthesized expressions so their diagnostics report (#231) - #262

Merged
leonidaz merged 2 commits into
mainfrom
fix/call-paren-span-mapping-231
Sep 24, 2026
Merged

leonidaz merged 2 commits into
mainfrom
fix/call-paren-span-mapping-231

Conversation

@leonidaz

@leonidaz leonidaz commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #231

Problem

tsrx-tsc exited 0 for this file, which TypeScript rejects with five errors:

declare const plain: {};
declare function createBase(): {};
declare function createVoid(): void;
export function run() {
	const items = [...createBase()]; // TS2488
	createBase()(); // TS2349
	(plain)(); // TS2349
	if (createVoid()) { // TS1345
	}
	const frozen = createBase() as const; // TS1355
	return [items, frozen];
}

TypeScript reports each of these on a whole expression: the spread argument, the callee, the if condition, the operand of as const. Volar drops a diagnostic unless a mapping covers its end. The walker in segments.js mapped neither a CallExpression nor a ParenthesizedExpression end to end (the latter only with metadata.forceMapping, which nothing in this repository sets), so nothing reached the closing ).

Fix

The walker now adds a verification-only whole-span mapping on each call and parenthesized expression, guarded on both ends having a source-map entry, as #222 does.

A mapping on every call is not safe. The transformed AST also contains calls and parentheses the compiler generates with source locations copied onto them. Errors in those would be reported over the author's code. Trying that version on the target test suites turned up:

  • Vue's defineVaporComponent(function App() { ... }) wrapper, which carries the component's location. That was 144 of the 490 mappings the trial added, and it fails maps Vue function components to the generated function in Volar TSX.
  • (@if (props.on) { <b>on</b> }), which keeps its source parentheses around a generated props.on ? App__static1 : null (or <Show when={props.on}> on Solid).

So the mapping only goes on a node the author wrote. visit_source_ast, the existing walk over the source AST that collects CSS and script regions, now also records each call and parenthesized expression by type and span, along with the span of its callee or wrapped expression. The mapping walker gives a transformed node the mapping only when both match. With that check, every mapping the fix adds across the target, tsrx-utils, typescript-plugin and language-server test suites prints the same text as its source (ignoring whitespace), with one exception: an authored props.rows.map(...) whose callback the compiler rewrites into an IIFE (spread-ref normalization). That call is the author's, and the {...} around it already mapped over the same generated callback, so the new mapping only narrows where an error there lands.

The superclass case from #222 now only handles array literals, because call and parenthesized superclasses get this mapping from their own walker case. Without that change they would be mapped twice.

Attribute values

The JSXAttribute visitor in visit_source_ast never called context.next(), apparently since it was renamed from Attribute, so the walk skipped attribute values. It now descends. That keeps the mapping for a call in an attribute value. In <div onClick={() => { if (createVoid()) {} }} />, the compiler prints the arrow body over several lines, so on main the attribute's own mapping reports TS1345 two columns late.

Descending also changes what the editor gets inside attribute values. Style blocks now get their CSS region, <script> bodies their TypeScript region, and scoped class names their CSS hover, as they already do elsewhere in a template. This matches the compiled output, because the compiler already scopes those elements: icon={<span class="a" />} prints class="a tsrx-…". The changeset covers this too.

esrap

This doesn't depend on esrap. I linked in esrap 2.3.2 (the lockfile version), upstream main (e924445), sveltejs/esrap#213, sveltejs/esrap#214, and #212 through #215 merged together, and ran 27 call and parenthesis shapes against each. Every build gave the same result: 21 of 27 diagnostics dropped without a walker change, all 27 reported on the author's expression with it. The issue description has the details. The esrap-dependent superclass case is #230, which needs sveltejs/esrap#214.

Tests

  • Shared source-mapping tests (React, Preact, Solid, Vue, Hono): exactly one whole-span mapping, matching the generated text, for a spread argument, an inner and outer call callee, (plain) and (plain)(), a void if condition, an as const operand, a multi-line call with a trailing comma, an optional-chain callee, a logical expression in parentheses, nested parentheses, a call in a component body, and a call in an attribute value. No mapping over (@if (...) { ... }). A style block, a <script> body and scoped class names inside attribute values get their CSS region, script region and CSS hovers.
  • tsc.test.js: tsrx-tsc reports the five errors from the issue at the columns tsc reports them.
  • With origin/main's segments.js, 85 of the 91 new cases fail, including the CLI test. The 6 that pass are the @if case, which guards against a mapping on every call and parenthesis.
  • Ten attribute-value shapes (calls, parentheses and void tests inside arrows, nested JSX, component props, a spread with ref) all report at tsc's position. On main, the TS1345 case above is two columns late.
  • tsrx-tsc against tsc on 27 shapes: 6/27 on main, 27/27 with the fix on Preact, React and Vue (three template-body cases differ from tsc only because the .ts comparison source isn't the same text). Another 14 shapes (binary, logical, conditional, await, unary, satisfies, array, object, template and new callees) already matched and still do.
  • pnpm test (118 files, 5372 tests), every pnpm typecheck step (the Hono typecheck:tests step run through node ../typescript-plugin/src/tsc.js, because a fresh worktree has no tsrx-tsc bin), pnpm format:check and pnpm changeset:check pass.

🤖 Generated with Claude Code


Note

Medium Risk
Changes core source-map-to-Volar mapping logic that drives editor and CLI diagnostic positions; behavior is scoped with authored-node guards and covered by broad tests.

Overview
Fixes missing .tsrx diagnostics when TypeScript flags an entire call or parenthesized expression (e.g. TS2488 on [...createBase()], TS2349 on createBase()() / (plain)(), TS1345 on if (createVoid()), TS1355 on createBase() as const). Volar drops errors unless a source map covers the diagnostic end, and the mapping walker previously left those closing spans unmapped.

The Volar mapping walker in segments.js now adds verification-only, whole-span mappings for CallExpression and ParenthesizedExpression, but only for author-written nodes: collect_authored_span_nodes records calls/parens from the source AST (type, span, and callee/inner expression), and push_authored_span_mapping applies the mapping on the transformed tree only when that fingerprint matches—avoiding false positives on compiler-generated wrappers (e.g. defineVaporComponent, parentheses around compiled @if).

Superclass handling is adjusted so call/parenthesized extends expressions rely on the new path; the extra whole-span push remains only for ArrayExpression superclasses to prevent duplicate mappings.

Shared source-mapping tests and a tsrx-tsc integration test assert one correct mapping per shape and CLI output at the expected columns; the changeset documents the patch for @tsrx/core.

Reviewed by Cursor Bugbot for commit 26acd38. Bugbot is set up for automated code reviews on this repo. Configure here.

…gnostics report (#231)

TypeScript reports TS2488 on a spread argument, TS2349 on a callee, TS1345
on a `void` condition and TS1355 on an `as const` operand over the whole
expression. The mapping walker covered neither a call nor a parenthesized
expression end to end, so Volar dropped these diagnostics.

The walker now adds a verification-only whole-span mapping on each call
and parenthesized expression the author wrote: one whose type and span
match a node of the source AST, with the same callee or wrapped
expression. Calls and parentheses the compiler generates with copied
locations (Vue's defineVaporComponent wrapper, the parentheses around a
compiled @if) get none. The superclass case from #222 now only covers
array literals, since calls and parentheses map themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
tsrx-website Ready Ready Preview Sep 24, 2026 2:53pm UTC
tsrx-website-mcp Ready Ready Preview Sep 24, 2026 2:53pm UTC

Request Review

)

Record the author's calls and parenthesized expressions in
visit_source_ast instead of a second walk over the source AST.

That walk's JSXAttribute visitor never called next(), so it skipped
attribute values. It now descends, which keeps the whole-call mapping
for a call inside an attribute value (the compiler reprints an arrow
body there over several lines, so the attribute's own mapping reported
TS1345 two columns late) and also gives style blocks, script bodies and
scoped class names inside attribute values their editor regions and
CSS hovers, matching the compiled output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@leonidaz
leonidaz merged commit bc68cb9 into main Sep 24, 2026
11 checks passed
@leonidaz
leonidaz deleted the fix/call-paren-span-mapping-231 branch September 24, 2026 15:49
@github-actions github-actions Bot mentioned this pull request Sep 24, 2026

This branch was successfully deployed

2 active deployments
Preview – tsrx-website — 0a82c82b Deployed Sep 24, 2026 by vercel[bot]
Preview – tsrx-website-mcp — 0a82c82b Deployed Sep 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: tsrx-tsc drops diagnostics reported on a whole call or parenthesized expression

1 participant