JS-1506 Support TypeScript 7 with a Go-based JS/TS analyzer#6458
JS-1506 Support TypeScript 7 with a Go-based JS/TS analyzer#6458vdiez wants to merge 58 commits into
Conversation
Implementation Details — Build & CI SetupCommits after initial implementationThe initial implementation commit required several follow-up fixes to get the build working end-to-end: 1.
|
| Tool | Version | Purpose |
|---|---|---|
| Go | 1.26+ | Cross-compile tsgolint sonar-server binary |
| protoc | 28.3 | Generate Go proto stubs from analyzer.proto |
| protoc-gen-go | latest | protoc plugin for Go message types |
| protoc-gen-go-grpc | latest | protoc plugin for Go gRPC service stubs |
CI Workflow Changes (.github/workflows/build.yml)
The Build SonarJS on Linux job now has 3 extra steps before the Maven build:
# 1. Checkout WITH submodules (was: plain checkout)
- uses: actions/checkout@v6
with:
submodules: true # clones tsgolint submodule
# 2. Go 1.26 via mise (separate from shared &mise anchor to avoid polluting other jobs)
- uses: jdx/mise-action@v3.6.1
with:
mise_toml: |
[tools]
go = "1.26"
# 3. Install protoc + Go proto generators
- run: |
curl -sSLo /tmp/protoc.zip "https://github.com/protocolbuffers/protobuf/releases/download/v28.3/protoc-28.3-linux-x86_64.zip"
sudo unzip -o /tmp/protoc.zip -d /usr/local bin/protoc
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestMaven Build Steps (sonar-plugin/sonar-javascript-plugin/pom.xml)
During generate-resources phase, these exec-maven-plugin executions run in order:
| # | Step | What it does |
|---|---|---|
| 1 | tsgolint-submodule-init |
git submodule update --init tsgolint (safety net for local builds) |
| 2 | tsgolint-copy-overlay |
Copies bridge/src/main/go/sonar-server/*.go → tsgolint/cmd/sonar-server/ |
| 3 | tsgolint-install-proto-gen |
go install protoc-gen-go@latest (idempotent) |
| 4 | tsgolint-install-proto-gen-grpc |
go install protoc-gen-go-grpc@latest (idempotent) |
| 5 | tsgolint-proto-gen-go |
protoc --go_out --go-grpc_out generates tsgolint/cmd/sonar-server/grpc/*.pb.go |
| 6 | tsgolint-go-get-grpc |
go get google.golang.org/grpc@latest google.golang.org/protobuf@latest |
| 7-12 | build-tsgolint-{platform} |
go build ./cmd/sonar-server for 6 platforms (linux-x64, linux-arm64, linux-x64-musl, win-x64, darwin-arm64, darwin-x64) |
| 13-18 | compress-tsgolint-{platform} |
xz -k -f compresses each binary |
All Go commands use GOWORK=off and CGO_ENABLED=0 for static cross-compilation.
Proto Compilation for Java (sonar-plugin/bridge/pom.xml)
Two protobuf-maven-plugin executions:
| # | Execution | Proto source | Output |
|---|---|---|---|
| 1 | Generate Protobuf Java Sources |
packages/jsts/src/parsers/ (estree) |
target/generated-sources/ (protobuf package) |
| 2 | Generate gRPC Analyzer Java Sources |
bridge/src/main/proto/ (analyzer) |
target/generated-sources/ (grpc package) |
Execution 2 has <clearOutputDirectory>false</clearOutputDirectory> to avoid wiping execution 1's output. It also uses protoc-gen-grpc-java plugin for gRPC service stubs.
Shade Plugin (sonar-plugin/sonar-javascript-plugin/pom.xml)
Each platform-specific shade execution (win-x64, linux-x64, etc.) includes an IncludeResourceTransformer for the tsgolint binary:
<resource>tsgolint/{platform}/tsgolint.xz</resource>
<file>${project.build.directory}/tsgolint/{platform}/tsgolint.xz</file>The multi execution includes all 6 platforms.
Note: typescript-go nested submodule is NOT neededtsgolint has a Dependency chain with
What the submodule + The |
0a80b64 to
e3ce7b0
Compare
Session: Wire up tsgolint linter for ruling parity (2026-03-04)What was doneCommit
Gotchas discovered
Remaining issue: ruling test still shows 1 differenceThe ruling test on Next steps
|
|
Implemented all requested fixes and validated through the real fast ITS path (WebSensor -> tsgolint binary over gRPC), without relying on the smoke test. What was fixed
Validation run Used the faster profile because no Go code changed:
Executed:
Result: Pushed commit:
|
|
Follow-up proposal: externalize tsgolint build orchestration from SonarJS Maven into tsgolint-owned tasks Given that Suggested next steps
Expected benefits
|
f3f3941 to
dba86da
Compare
|
dba86da to
ef9768c
Compare
README Freshness Check❌ The rules README is out of date. A fix PR has been created: #6824 Please review and merge it into your branch. |
|
ef9768c to
ef4b6f2
Compare
|
Implemented and pushed in 5f30300. What was done
Validation
Final assessment
Rules now offloaded to
What is still open
Remaining SonarJS rules that still use
So the practical status is:
|
7debf2f to
182f9d7
Compare
Placeholder commit for tsgolint integration branch. Offload 7 pure-external typescript-eslint rules to tsgolint (Go-based linter) running alongside the existing Node.js bridge, orchestrated by Java via gRPC. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the complete scaffolding to offload 7 pure-external typescript-eslint rules (S4123, S2933, S4157, S4325, S6565, S6583, S6671) to tsgolint, a Go-based TypeScript linter, running alongside the existing Node.js bridge via gRPC. Key changes: - analyzer.proto: generic AnalyzerService with server-side streaming - Java gRPC client: AnalyzerGrpcServerImpl manages process lifecycle, ManagedChannel, and streaming response handling - TsgolintBundle: binary extraction following EmbeddedNode pattern - TsgolintIssueConverter: proto Issue -> BridgeServer.Issue mapping - JsTsChecks: rule partitioning (bridge vs tsgolint) - WebSensor: starts tsgolint alongside bridge, runs parallel analysis - Maven: Go cross-compilation (6 platforms), xz compression, shade bundling - Go server scaffold: cmd/sonar-server with gRPC bootstrap - Integration test with 7 TS files triggering each rule Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add tsgolint (oxc-project/tsgolint) as git submodule - Move Go server overlay files to sonar-plugin/bridge/src/main/go/ (copied into submodule at build time since we can't push to upstream) - Fix proto go_package to match actual module path (typescript-eslint) - Fix protobuf-maven-plugin clearOutputDirectory conflict - Add javax.annotation (annotations-api) for gRPC generated code - Add GOWORK=off to all Go build steps (tsgolint go.work needs local deps) - Add git submodule init, proto gen, go get, overlay copy build steps - Exclude tsgolint submodule from license header checks - Fix tsConfigPaths access (use JsTsContext accessor) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build process modifies submodule working tree (go get, proto gen, overlay copy). These are build artifacts, not source changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Linux build: checkout with submodules so tsgolint is available - Add Go 1.26 via mise (separate step, doesn't pollute shared anchor) - Install protoc v28.3 from GitHub releases - Install protoc-gen-go and protoc-gen-go-grpc in CI and POM - POM go install steps ensure local builds also work Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add mkdir -p before copying Go overlay files into tsgolint submodule (the target directory doesn't exist in the upstream repo) - Download protoc binary via maven-dependency-plugin instead of requiring it on PATH — removes curl/unzip from CI workflow - Lift protobuf.version and os-maven-plugin to parent pom so both bridge and sonar-javascript-plugin share the same config - Add Go setup to Windows build job - Remove redundant protoc + go proto generator install from CI (Maven exec steps already handle this) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Maven dependency:copy doesn't preserve execute permissions. Wrap protoc call in bash -c with chmod +x to fix permission denied error on Linux CI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Correction to my 2026-05-20 status note: the parity-harness gap is now only \S4123\. \S4325\ no longer belongs on that list after commit \58bfdb0b6\ (\JS-1746 JS-1743 close S4325 parity gaps\). That change added violating-fixture coverage to the \S4325\ parity corpus, aligned the Go diagnostic shape with Node for the remaining mismatches, and both \ |
| run := s.analyze(analysis.ctx, input) | ||
| if err := grpcCallCancelled(stream.Context()); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| for _, filePath := range run.orderedFiles { | ||
| if err := stream.Send(&pb.AnalyzeProjectStreamResponse{ | ||
| Message: &pb.AnalyzeProjectStreamResponse_FileResult{ | ||
| FileResult: &pb.FileResultMessage{ | ||
| FilePath: filePath, | ||
| Result: run.results[filePath], | ||
| }, | ||
| }, | ||
| }); err != nil { | ||
| return err |
There was a problem hiding this comment.
⚠️ Bug: AnalyzeProject streams results even when analysis was cancelled
In AnalyzeProject (streaming), after s.analyze() returns, the code unconditionally iterates run.orderedFiles and sends all file results via stream.Send (lines 62-73), and only afterwards checks run.cancelled (line 75) to send the cancellation marker. This means that even when analysis was cancelled partway through, all accumulated partial results are sent to the client before the cancellation signal. The client has no way to know which results are complete and which are partial/incomplete due to cancellation.
In contrast, AnalyzeProjectUnary (line 110-113) returns all results including partial ones without any cancellation indication in the response body, so the client silently receives incomplete data.
Consider checking run.cancelled before sending file results, or at minimum documenting that partial results are intentionally sent before the cancellation marker.
Skip sending file results when analysis was cancelled, only send the cancellation marker:
run := s.analyze(analysis.ctx, input)
if err := grpcCallCancelled(stream.Context()); err != nil {
return err
}
if !run.cancelled {
for _, filePath := range run.orderedFiles {
if err := stream.Send(&pb.AnalyzeProjectStreamResponse{
Message: &pb.AnalyzeProjectStreamResponse_FileResult{
FileResult: &pb.FileResultMessage{
FilePath: filePath,
Result: run.results[filePath],
},
},
}); err != nil {
return err
}
}
}
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| void saveIssue(JsTsContext<?> context, JsTsChecks checks, InputFile inputFile, Issue issue) { | ||
| this.checks = checks; | ||
| this.file = inputFile; | ||
| saveIssue(context, issue); | ||
| } |
There was a problem hiding this comment.
💡 Quality: AnalysisProcessor.saveIssue mutates shared instance fields
The new saveIssue(JsTsContext, JsTsChecks, InputFile, Issue) overload at lines 379-383 sets the instance fields this.checks and this.file before delegating to the private saveIssue(context, issue). While the two analysis paths (bridge and jsts-go) currently execute sequentially in analyzeFiles() (line 360-362 in WebSensor.java), this pattern is fragile: it silently relies on call-site ordering for correctness, and any future parallelism would introduce a race condition on these shared mutable fields.
Consider passing checks and inputFile through to the methods that need them rather than storing them as instance state.
Was this helpful? React with 👍 / 👎
| - checkerPool = newCheckerPool(4, program, nil) | ||
| + checkerPool = newCheckerPool(runtime.GOMAXPROCS(0), program, nil) |
There was a problem hiding this comment.
⚠️ Edge Case: Checker pool sized to GOMAXPROCS may over-allocate on large machines
Patch 0001 changes the TypeScript checker pool size from a hardcoded 4 to runtime.GOMAXPROCS(0). On machines with many cores (e.g., 32-128 in CI or cloud), this creates one checker per logical CPU. Each checker holds a full copy of the type-checking state, so this can significantly increase memory usage for large TypeScript projects without proportional benefit — the linter itself is hardcoded to workers=1 (serial execution in run_linter_on_file.go:125), so only one checker is used at a time during linting.
Consider capping the pool size (e.g., min(runtime.GOMAXPROCS(0), 8)) or aligning it with the actual degree of parallelism in the analysis pipeline.
Cap checker pool at 8 to avoid excessive memory use on high-core machines:
- checkerPool = newCheckerPool(runtime.GOMAXPROCS(0), program, nil)
+ n := runtime.GOMAXPROCS(0)
+ if n > 8 { n = 8 }
+ checkerPool = newCheckerPool(n, program, nil)
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| var rulesRunnableWithoutProgram = map[string]struct{}{ | ||
| "consistent-type-assertions": {}, | ||
| "no-async-promise-executor": {}, | ||
| } |
There was a problem hiding this comment.
⚠️ Bug: AST-only rules missing from rulesRunnableWithoutProgram map
16 of the newly added rules are purely AST-based (they never call ResolvesToGlobalValue, GetTypeAtLocation, or any other type-checker API), yet none of them are registered in rulesRunnableWithoutProgram. This means they will be skipped when no typed program is available (e.g. disableTypeChecking or orphan/AST-only fallback lanes), even though they could run perfectly fine.
Affected rules: no-debugger, no-ternary, no-object-as-default-parameter, adjacent-overload-signatures, no-confusing-non-null-assertion, no-duplicate-enum-values, no-misused-new, no-non-null-assertion, no-explicit-any, no-inferrable-types, no-unnecessary-type-constraint, prefer-as-const, no-negation-in-equality-check, no-this-assignment, no-typeof-undefined, no-instanceof-builtins.
Rules that correctly remain excluded (they use ResolvesToGlobalValue or type-checker): radix, no-setter-return, no-new-func, valid-typeof, no-array-method-this-argument.
Add all 16 AST-only rules to rulesRunnableWithoutProgram so they run even when type information is unavailable.:
var rulesRunnableWithoutProgram = map[string]struct{}{
"consistent-type-assertions": {},
"no-async-promise-executor": {},
"no-debugger": {},
"no-ternary": {},
"no-object-as-default-parameter": {},
"adjacent-overload-signatures": {},
"no-confusing-non-null-assertion": {},
"no-duplicate-enum-values": {},
"no-misused-new": {},
"no-non-null-assertion": {},
"no-explicit-any": {},
"no-inferrable-types": {},
"no-unnecessary-type-constraint": {},
"prefer-as-const": {},
"no-negation-in-equality-check": {},
"no-this-assignment": {},
"no-typeof-undefined": {},
"no-instanceof-builtins": {},
}
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| func runDirectRuleOnCode( | ||
| t *testing.T, | ||
| availableRule rule.Rule, | ||
| options any, | ||
| fileName string, | ||
| code string, | ||
| tsconfigName string, | ||
| tsconfigContent string, | ||
| ) []rule.RuleDiagnostic { | ||
| t.Helper() | ||
|
|
||
| rootDir := fixtures.GetRootDir() | ||
| filePath := tspath.ResolvePath(rootDir, fileName) | ||
| overlays := map[string]string{filePath: code} | ||
| if tsconfigContent != "" { |
There was a problem hiding this comment.
💡 Quality: Duplicate test helper: runDirectRuleOnCode ≈ runRuleOnCode
direct_rule_test_helpers_test.go defines runDirectRuleOnCode which is functionally identical to runRuleOnCode in rule_test_helpers_test.go. Both create overlays, build an overlay VFS over an identically-initialised cached FS (cachedvfs.From(bundled.WrapFS(osvfs.FS()))), create a program, and run the linter with the same parameters. The only differences are variable names (availableRule vs ruleToRun, rule vs rulepkg) and a separate cached FS variable. Tests like no_setter_return_test.go use runRuleOnCode while adjacent_overload_signatures_test.go uses runDirectRuleOnCode, with no semantic distinction. Consider consolidating into a single helper to reduce maintenance burden.
Was this helpful? React with 👍 / 👎
| case '\n': | ||
| return "", pos + 1 | ||
| default: | ||
| if !lineBreaksError && source[pos] == '\n' { | ||
| return "\n", pos + 1 | ||
| } | ||
| return source[pos : pos+1], pos + 1 |
There was a problem hiding this comment.
💡 Quality: Dead code in decodeEscapeSequence default branch
In decodeEscapeSequence, the case ' ' at line 463 already handles the newline character and returns ("", pos + 1). The condition source[pos] == ' ' in the default branch (line 466) can therefore never be true, since the switch would have matched the explicit case first. This is dead code that adds confusion about the intended behavior for newline handling in template literals vs quoted strings.
Was this helpful? React with 👍 / 👎
| func SplitRegexLiteral(text string) (string, string, bool) { | ||
| if len(text) < 2 || text[0] != '/' { | ||
| return "", "", false | ||
| } | ||
|
|
||
| escaped := false | ||
| closingSlash := -1 | ||
| for index := 1; index < len(text); index++ { | ||
| switch { | ||
| case escaped: | ||
| escaped = false | ||
| case text[index] == '\\': | ||
| escaped = true | ||
| case text[index] == '/': | ||
| closingSlash = index |
There was a problem hiding this comment.
💡 Quality: Duplicate regex parsing infrastructure across two packages
The codebase now has two independent regex parsing implementations: regex_batch_helpers (1080 lines, returns an AST with typed nodes) and regexbatch (731 lines, returns a flat parsed structure). Both implement pattern parsing, escape handling, character classes, quantifiers, and groups with slightly different APIs and completeness levels. For example, regex_batch_helpers.SplitRegexLiteral does not track character classes when finding the closing slash (works correctly because it finds the last unescaped slash, but is fragile), while regexbatch.splitRegexLiteral properly tracks inCharacterClass state. Having two parsers increases maintenance burden and risk of divergent behavior. Consider consolidating into a single regex parser with different output adapters.
Was this helpful? React with 👍 / 👎
|
All hard type-service rules are now routed to
So the blocking type-service routing work is in place, but these four rules still have known parity gaps outside the current acceptance corpus. |
| case ast.KindForInStatement, ast.KindForOfStatement: | ||
| forOf := node.AsForInOrOfStatement() |
There was a problem hiding this comment.
⚠️ Bug: for-in/for-of treated as never completing normally without break
In statementCanCompleteNormally, ForInStatement and ForOfStatement return hasBreakStatement(forOf.Statement). This means if the loop body always returns/throws and has no break, the function concludes the for-of cannot complete normally. However, a for-of/for-in over an empty iterable always completes normally regardless of body contents.
Example that would be missed:
function example(items: string[]): number {
for (const item of items) {
return item.length;
}
// implicit return when items is empty — should be flagged
}With the current logic, hasImplicitReturn returns false (because the for-of is considered to never complete), so the inconsistency is not reported. This causes false negatives.
for-in/for-of loops can always complete normally (empty iterable), so unconditionally return true.:
case ast.KindForInStatement, ast.KindForOfStatement:
return true
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
CI failed: The migration to the new Go-based JS/TS analyzer is causing critical runtime crashes (SIGSEGV) in the analysis engine, leading to widespread test failures and gRPC connection errors.OverviewMultiple CI jobs failed across integration test modules due to segmentation faults in the Failuresjsts-go Runtime Segmentation Fault (confidence: high)
gRPC Connection Loss (confidence: high)
Git SCM Branch Detection Failure (confidence: low)
Summary
Code Review
|
| Auto-apply | Compact | Unblock |
|
|
|
Was this helpful? React with 👍 / 👎 | Gitar
|




Scope
This PR now tracks JS-1506: support TypeScript 7 with a Go-based JS/TS analyzer, while keeping Node.js as the primary analyzer for the rest of SonarJS.
The branch has moved well beyond the original
JS-1379/tsgolintgRPC PoC. It should now be read as the integration branch for the currentjsts-gosidecar and the Go-side runtime needed to approach Nodeanalyze-projectparity for the migrated rule slice.Current Branch State
server-go/sonar-server.AnalyzeProjectgRPC contract frommaster, includingAnalyzeProject,AnalyzeProjectUnary,CancelAnalysis, andLease.WebSensoras the orchestrator: Node.js still runs the main analysis, and only selected JS/TS rules are routed tojsts-go.tsgolintsubmodule/runtime dependency. The live upstream inputs areserver-go/typescript-go,server-go/patches/typescript-go,server-go/shim, and Sonar-owned code inserver-go/sonar-server.Routed Rules
JSTS_GO_RULEScurrently routes these 16 Sonar keys to the Go sidecar:S131switch-exhaustiveness-checkS2870no-array-deleteS2933prefer-readonlyS4123await-thenableS4137consistent-type-assertionsS4157no-unnecessary-type-argumentsS4325no-unnecessary-type-assertionS6544no-misused-promisesS6551no-base-to-stringS6557prefer-string-starts-ends-withS6565prefer-return-this-typeS6571no-redundant-type-constituentsS6582prefer-optional-chainS6583no-mixed-enumsS6606prefer-nullish-coalescingS6671prefer-promise-reject-errorsNotes:
S131stays Sonar-owned on the Go side by combiningswitch-exhaustiveness-checkwith Go-side defaults and issue relocation.S6544stays composite on the Go side by pairingno-misused-promiseswith the Go companionno-async-promise-executor.S2870stays Sonar-owned on the Go side asno-array-delete.Analyze-Project And Runtime Work In This Branch
pathMap,VirtualFiles,bundles, andrulesWorkdir.canAccessFileSystem=false.files.tsconfig.json,package.json,deno.json, anddeno.jsonc, plus dependency/module/ECMAScript signals used by migrated rules.disableTypeCheckingrouting through the AST-only lane for the migrated subset that supports it.globals/environmentshandling for scope-sensitive migrated rules.skipNodeModuleLookupOutsideBaseDirsupport in Go program creation.FsEventsinvalidation for cached configured and orphan programs.packages/parity/corpus, withnpm run jsts-go-parity:diff-all, currently covering all 16 routed Go rules.Build Notes
skip-nodejsfrommaster.-Pskip-jsts-goto skip Go build and compress steps when they are not under test.jsts-gounderserver-go.Remaining Gaps And Non-Goals
analyze-projectoutput surface.JS-1741remains the CI / packaging / cross-platform stabilization ticket.JS-1743now owns further parity-corpus expansion plus the remaining Node-vs-Go diagnostic-shape gaps on the shared rule set.JS-1744covers benchmarking.JS-1746is now primarily the typedunicornslice (S7727,S7728,S7729,S7755,S7778,S7785).Summary by Gitar
unused-import,inconsistent-return,no-implicit-dependencies, and several regex-specific rules (unused-named-groups,regex-complexity,slow-regex, etc.).packages/parity/corpusto validate the newly implemented Go rules.This will update automatically on new commits.