Skip to content

Add beta CLI --tsgo backend (native-preview shim)#102

Closed
johnsoncodehk wants to merge 10 commits into
masterfrom
feat/cli-tsgo
Closed

Add beta CLI --tsgo backend (native-preview shim)#102
johnsoncodehk wants to merge 10 commits into
masterfrom
feat/cli-tsgo

Conversation

@johnsoncodehk

@johnsoncodehk johnsoncodehk commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add --tsgo to @tsslint/cli using @typescript/native-preview (ts-go) as an optional type backend for plain --project runs, with a TypeScript API shim (tsgo-backend, facade, JS symbol resolver).
  • Reuse one tsgo child process across --project entries (tsgo-api-pool); --tsgo-fast opt-in eager-prepares all files at setup.
  • Selector-driven prefetch: derive per-file PrefetchPlan from ESLint rule listeners and batch-warm types/symbols/contextual types before the sync rule loop.
  • Checker memo layer covering both project.checker.* methods AND Type/Symbol object methods (getProperties, getCallSignatures, getSymbol, getTarget, getTypes, etc.) — the latter was the key bottleneck (21,951 raw IPC calls → 444 via memo).
  • Per-file Map caches (cleared after each lint) instead of WeakMap, since lint is sequential.
  • TSSLINT_TIME_TSGO=1 RPC profiling with full apiRequest instrumentation.

Performance (darwin arm64, Node 24, 3-run median)

Scenario Strada --tsgo Ratio
medium (~37 files, ts-eslint) ~1.6–3s ~1.8–2s ~1×
full (~59 files, 8 tsconfigs) ~1.9s ~2.2s ~1.15×

Profile (TSSLINT_TIME_TSGO=1, compat-eslint): 6,080 total IPC / 512ms (was 34,023 / 3,670ms); 55,173 memo hits.

Dogfood parity

pnpm run lint:tsgo vs Strada: 84 passed with known gap on shim sources / tsgo checker divergence.

Test plan

  • pnpm run build
  • node packages/cli/test/tsgo-backend.test.js
  • pnpm run lint:tsgo (compat-eslint tsconfig)
  • node packages/poc-tsgo/bench.js --scenario=full --runs=3
  • Profile: TSSLINT_TIME_TSGO=1 packages/cli/bin/tsslint.js --force --project packages/compat-eslint/tsconfig.json --tsgo
  • CI test workflow on Linux

johnsoncodehk and others added 10 commits June 26, 2026 19:17
Expose ts-go as an optional type backend for plain --project runs, with auto fast-path on multi-file projects, poc-tsgo benchmarks, and lazy-estree TSImportType.options parity so dogfood CI stays green.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use tsgo-load hasNativePreview() in the workflow check and add root devDependency so optional peer installs reliably on Linux CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
The rule disagrees across Strada vs tsgo checkers on interop assertions in shim sources; split tsslint.config so shim files keep consistent-type-imports only.

Co-authored-by: Cursor <cursoragent@cursor.com>
Patch Type predicates per prototype and instance, resolve aliasTypeArguments handles lazily, and fixup nested type arrays so union traversal no longer throws.

Co-authored-by: Cursor <cursoragent@cursor.com>
Remove the proto getter that looped with getAliasTypeArguments(), drop handle caches on fixup, and resolve type.symbol eagerly so no-unnecessary-type-assertion no longer stack-overflows.

Co-authored-by: Cursor <cursoragent@cursor.com>
Reuse one tsgo subprocess across projects, add TSSLINT_TIME_TSGO RPC
profiling, and derive per-file prefetch plans from ESLint rule selectors.

Co-authored-by: Cursor <cursoragent@cursor.com>
Route computeGetTypeAtLocation through memo-wrapped checker methods,
batch-resolve import/export symbols and contextual types during prepare,
and warm index-infos on member-access prefetch.

Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve JS-bind misses via getSymbolAtPosition(batch), warm call/new
types with getTypeAtLocation(batch), batch getTypeOfSymbol for tsgo
symbols, memo getWidenedType, and drop eager getIndexInfosOfType prefetch.

Co-authored-by: Cursor <cursoragent@cursor.com>
…p caches

Fix the hidden 34k raw-IPC bottleneck: patchTsgoTypeCheckerMethods was
routing type.getProperties() / getCallSignatures() / getBaseTypes() etc.
through the raw project.checker (no memo), causing 21,951 uncacheable
getPropertiesOfType IPC calls alone. Route through the wrapped checker
so they hit the existing propertiesOfTypeCache / signaturesOfTypeCache.

Also memoize Type/Symbol object methods (getSymbol, getTarget, getTypes,
getMembers, getExports) that issue apiRequest on first access, and
switch all checker memo tables from WeakMap to per-file Map (cleared
in releaseFile) since lint is sequential per file.

Full bench: --tsgo 5.7s → 2.2s (1.15× Strada, was 3.0×).

Co-authored-by: Cursor <cursoragent@cursor.com>
…p no-sig types

- Add shouldPrefetchSymbol() to skip property names, declaration names,
  labels, and other identifiers the scope manager's _classifyIdentifier
  never queries via getSymbolAtLocation. Reduces getSymbolsAtPositions
  batch from ~10k to ~900 positions (90% reduction), 217ms → 22ms.
- Increase BATCH_CHUNK to 2048 (one chunk per file for typical sizes).
- Add TypeFlags early-return for getCallSignatures/getConstructSignatures
  on primitive/literal types (skip IPC for types that can't have sigs).
- Memoize getShorthandAssignmentValueSymbol via shorthandValueSymbolCache.
- Improve JS resolver with memberOf/resolveJsNodeSymbol for qualified-name
  and property-access member resolution.
- Result: --tsgo full bench ~2.1s vs Strada ~2.0s (1.05×, was 1.17×).

Co-authored-by: Cursor <cursoragent@cursor.com>
@johnsoncodehk

Copy link
Copy Markdown
Owner Author

Performance Update — --tsgo now 1.05× Strada

Latest optimization: symbol prefetch filtering

Key insight: The prefetch was batch-resolving symbols for ALL identifiers in each file (~10,000+ positions across 25 files), but the scope manager's _classifyIdentifier only queries getSymbolAtLocation on ~900 of those — the rest are property names, declaration names, labels, etc. that are never looked up.

Fix: Added shouldPrefetchSymbol() that mirrors _classifyIdentifier's skip logic, filtering out:

  • Property access names (obj.xx is never a reference)
  • Qualified name rights (A.BB is never a reference)
  • Declaration name slots (property/method/enum/class/function names)
  • Import specifier names, labels, meta properties

Results

Metric Before After Change
getSymbolsAtPositions 217ms 22ms −90%
Total RPC time 502ms 351ms −30%
--tsgo full bench (median) 2,444ms 2,105ms −339ms
--tsgo / Strada ratio 1.17× 1.05×

Also included:

  • TypeFlags early-return for getCallSignatures/getConstructSignatures on primitive/literal types
  • Memoized getShorthandAssignmentValueSymbol
  • Improved JS resolver with resolveJsNodeSymbol for qualified-name/property-access member resolution

@johnsoncodehk

Copy link
Copy Markdown
Owner Author

Pivoting to an in-process NAPI approach via a separate typescript-native-bridge package (a typescript-shaped drop-in alias). The IPC shim strategy is abandoned in favor of the alias-based integration, so closing this PR.

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.

1 participant