From 9013718a4621dbc7ec219774c6abb473dafe28cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 07:03:38 +0000 Subject: [PATCH 1/4] Initial plan From 256b95984177ed7262f9798fae73d8e2415e6b9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 07:18:49 +0000 Subject: [PATCH 2/4] Fix diagnostic CLI exit code Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- cmd/tsgo/main.go | 10 +++++++++- cmd/tsgo/main_test.go | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 cmd/tsgo/main_test.go diff --git a/cmd/tsgo/main.go b/cmd/tsgo/main.go index 8d6816fa3d4..d3c044a4457 100644 --- a/cmd/tsgo/main.go +++ b/cmd/tsgo/main.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/execute" + "github.com/microsoft/typescript-go/internal/execute/tsc" ) func main() { @@ -28,5 +29,12 @@ func runMain() int { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() result := execute.CommandLine(ctx, newSystem(), args, nil) - return int(result.Status) + return exitCode(result.Status) +} + +func exitCode(status tsc.ExitStatus) int { + if status == tsc.ExitStatusDiagnosticsPresent_OutputsSkipped { + return 2 + } + return int(status) } diff --git a/cmd/tsgo/main_test.go b/cmd/tsgo/main_test.go new file mode 100644 index 00000000000..5e7b972b3ed --- /dev/null +++ b/cmd/tsgo/main_test.go @@ -0,0 +1,19 @@ +package main + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/execute/tsc" + "gotest.tools/v3/assert" +) + +func TestExitCode(t *testing.T) { + t.Parallel() + + assert.Equal(t, exitCode(tsc.ExitStatusSuccess), 0) + assert.Equal(t, exitCode(tsc.ExitStatusDiagnosticsPresent_OutputsSkipped), 2) + assert.Equal(t, exitCode(tsc.ExitStatusDiagnosticsPresent_OutputsGenerated), 2) + assert.Equal(t, exitCode(tsc.ExitStatusInvalidProject_OutputsSkipped), 3) + assert.Equal(t, exitCode(tsc.ExitStatusProjectReferenceCycle_OutputsSkipped), 4) + assert.Equal(t, exitCode(tsc.ExitStatusNotImplemented), 5) +} From c156501c5cfc91fbf1856915ae9220581599d5eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 07:21:24 +0000 Subject: [PATCH 3/4] Make diagnostic exit mapping explicit Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- cmd/tsgo/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/tsgo/main.go b/cmd/tsgo/main.go index d3c044a4457..43d8a78074c 100644 --- a/cmd/tsgo/main.go +++ b/cmd/tsgo/main.go @@ -33,7 +33,8 @@ func runMain() int { } func exitCode(status tsc.ExitStatus) int { - if status == tsc.ExitStatusDiagnosticsPresent_OutputsSkipped { + switch status { + case tsc.ExitStatusDiagnosticsPresent_OutputsSkipped, tsc.ExitStatusDiagnosticsPresent_OutputsGenerated: return 2 } return int(status) From cc8ea6993713f4b7d90e4d327ac9e486421d480f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:55:12 +0000 Subject: [PATCH 4/4] Restore noEmit exit code via handleNoEmitOptions port Port tsc's handleNoEmitOptions in Program.Emit: a whole-program noEmit is not skipped (only a single-file emit is), so a program with diagnostics reports DiagnosticsPresent_OutputsGenerated, matching tsc and the process exit code 2 that older tsgo releases produced. Revert the earlier cmd/tsgo exit-code mapping hack and its test; add a tsc command-line regression test instead. Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- cmd/tsgo/main.go | 11 +---- cmd/tsgo/main_test.go | 19 --------- internal/compiler/program.go | 5 +++ internal/execute/tsctests/tsc_test.go | 5 +++ .../tsc/commandLine/noEmit-with-type-error.js | 41 +++++++++++++++++++ .../generateTrace-generates-types-file.js | 4 +- ...e-with-multiple-files-and-complex-types.js | 8 +--- .../reference/tsc/noEmit/dts-errors.js | 8 ++-- .../reference/tsc/noEmit/semantic-errors.js | 8 ++-- .../reference/tsc/noEmit/syntax-errors.js | 8 ++-- 10 files changed, 67 insertions(+), 50 deletions(-) delete mode 100644 cmd/tsgo/main_test.go create mode 100644 testdata/baselines/reference/tsc/commandLine/noEmit-with-type-error.js diff --git a/cmd/tsgo/main.go b/cmd/tsgo/main.go index 43d8a78074c..8d6816fa3d4 100644 --- a/cmd/tsgo/main.go +++ b/cmd/tsgo/main.go @@ -8,7 +8,6 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/execute" - "github.com/microsoft/typescript-go/internal/execute/tsc" ) func main() { @@ -29,13 +28,5 @@ func runMain() int { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() result := execute.CommandLine(ctx, newSystem(), args, nil) - return exitCode(result.Status) -} - -func exitCode(status tsc.ExitStatus) int { - switch status { - case tsc.ExitStatusDiagnosticsPresent_OutputsSkipped, tsc.ExitStatusDiagnosticsPresent_OutputsGenerated: - return 2 - } - return int(status) + return int(result.Status) } diff --git a/cmd/tsgo/main_test.go b/cmd/tsgo/main_test.go deleted file mode 100644 index 5e7b972b3ed..00000000000 --- a/cmd/tsgo/main_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "testing" - - "github.com/microsoft/typescript-go/internal/execute/tsc" - "gotest.tools/v3/assert" -) - -func TestExitCode(t *testing.T) { - t.Parallel() - - assert.Equal(t, exitCode(tsc.ExitStatusSuccess), 0) - assert.Equal(t, exitCode(tsc.ExitStatusDiagnosticsPresent_OutputsSkipped), 2) - assert.Equal(t, exitCode(tsc.ExitStatusDiagnosticsPresent_OutputsGenerated), 2) - assert.Equal(t, exitCode(tsc.ExitStatusInvalidProject_OutputsSkipped), 3) - assert.Equal(t, exitCode(tsc.ExitStatusProjectReferenceCycle_OutputsSkipped), 4) - assert.Equal(t, exitCode(tsc.ExitStatusNotImplemented), 5) -} diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 35093fab681..a4bc6fca222 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -1631,6 +1631,11 @@ func (p *Program) Emit(ctx context.Context, options EmitOptions) *EmitResult { } if options.EmitOnly != EmitOnlyForcedDts { + if p.Options().NoEmit.IsTrue() { + // Mirror handleNoEmitOptions: a single-file noEmit is skipped, but a whole-program + // noEmit is not, so a program with diagnostics reports DiagnosticsPresent_OutputsGenerated. + return &EmitResult{EmitSkipped: options.TargetSourceFile != nil} + } result := HandleNoEmitOnError( ctx, p, diff --git a/internal/execute/tsctests/tsc_test.go b/internal/execute/tsctests/tsc_test.go index 46128bbb759..693184fcce0 100644 --- a/internal/execute/tsctests/tsc_test.go +++ b/internal/execute/tsctests/tsc_test.go @@ -118,6 +118,11 @@ func TestTscCommandline(t *testing.T) { files: FileMap{"/home/src/workspaces/project/first.ts": `export const Key = Symbol()`}, commandLineArgs: []string{"--lib", "es6 ", "first.ts"}, }, + { + subScenario: "noEmit with type error", + files: FileMap{"/home/src/workspaces/project/index.ts": `x = 5;`}, + commandLineArgs: []string{"--noEmit", "index.ts"}, + }, { subScenario: "option diagnostics are suppressed when there are syntactic errors", files: FileMap{"/home/src/workspaces/project/a.ts": `const x: = 1;`}, diff --git a/testdata/baselines/reference/tsc/commandLine/noEmit-with-type-error.js b/testdata/baselines/reference/tsc/commandLine/noEmit-with-type-error.js new file mode 100644 index 00000000000..fbe6848a262 --- /dev/null +++ b/testdata/baselines/reference/tsc/commandLine/noEmit-with-type-error.js @@ -0,0 +1,41 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +x = 5; + +tsgo --noEmit index.ts +ExitStatus:: DiagnosticsPresent_OutputsGenerated +Output:: +index.ts:1:1 - error TS2304: Cannot find name 'x'. + +1 x = 5; +  ~ + + +Found 1 error in index.ts:1 + +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; + diff --git a/testdata/baselines/reference/tsc/generateTrace/generateTrace-generates-types-file.js b/testdata/baselines/reference/tsc/generateTrace/generateTrace-generates-types-file.js index 7905bb21b6b..ac1c0aa7a47 100644 --- a/testdata/baselines/reference/tsc/generateTrace/generateTrace-generates-types-file.js +++ b/testdata/baselines/reference/tsc/generateTrace/generateTrace-generates-types-file.js @@ -77,9 +77,7 @@ declare const console: { log(msg: any): void; }; {"pid":1,"tid":2,"ph":"E","cat":"check","ts":18,"name":"checkSourceFile","args":{"checkerId":0,"path":"/home/src/workspaces/project/a.ts"}}, {"pid":1,"tid":1,"ph":"E","cat":"check","ts":19,"name":"checkSourceFiles"}, {"pid":1,"tid":1,"ph":"B","cat":"emit","ts":20,"name":"emit"}, -{"pid":1,"tid":354130385,"ph":"B","cat":"emit","ts":21,"name":"emit","args":{"path":"/home/src/workspaces/project/a.ts"}}, -{"pid":1,"tid":354130385,"ph":"E","cat":"emit","ts":22,"name":"emit","args":{"path":"/home/src/workspaces/project/a.ts"}}, -{"pid":1,"tid":1,"ph":"E","cat":"emit","ts":23,"name":"emit"} +{"pid":1,"tid":1,"ph":"E","cat":"emit","ts":21,"name":"emit"} ] //// [/home/src/workspaces/project/trace/types_0.json] *new* diff --git a/testdata/baselines/reference/tsc/generateTrace/generateTrace-with-multiple-files-and-complex-types.js b/testdata/baselines/reference/tsc/generateTrace/generateTrace-with-multiple-files-and-complex-types.js index d981e92cb2f..befa8be5368 100644 --- a/testdata/baselines/reference/tsc/generateTrace/generateTrace-with-multiple-files-and-complex-types.js +++ b/testdata/baselines/reference/tsc/generateTrace/generateTrace-with-multiple-files-and-complex-types.js @@ -20,7 +20,7 @@ export interface Container { export type Nullable = T | null | undefined; tsgo --generateTrace /home/src/workspaces/project/trace --singleThreaded -ExitStatus:: DiagnosticsPresent_OutputsSkipped +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: main.ts:2:74 - error TS2322: Type '(fn: (x: number) => U) => Container' is not assignable to type '(fn: (x: U) => U) => Container'. Types of parameters 'fn' and 'fn' are incompatible. @@ -104,11 +104,7 @@ declare const console: { log(msg: any): void; }; {"pid":1,"tid":2,"ph":"E","cat":"check","ts":24,"name":"checkSourceFile","args":{"checkerId":0,"path":"/home/src/workspaces/project/main.ts"}}, {"pid":1,"tid":1,"ph":"E","cat":"check","ts":25,"name":"checkSourceFiles"}, {"pid":1,"tid":1,"ph":"B","cat":"emit","ts":26,"name":"emit"}, -{"pid":1,"tid":100209852,"ph":"B","cat":"emit","ts":27,"name":"emit","args":{"path":"/home/src/workspaces/project/main.ts"}}, -{"pid":1,"tid":100209852,"ph":"E","cat":"emit","ts":28,"name":"emit","args":{"path":"/home/src/workspaces/project/main.ts"}}, -{"pid":1,"tid":553096334,"ph":"B","cat":"emit","ts":29,"name":"emit","args":{"path":"/home/src/workspaces/project/types.ts"}}, -{"pid":1,"tid":553096334,"ph":"E","cat":"emit","ts":30,"name":"emit","args":{"path":"/home/src/workspaces/project/types.ts"}}, -{"pid":1,"tid":1,"ph":"E","cat":"emit","ts":31,"name":"emit"} +{"pid":1,"tid":1,"ph":"E","cat":"emit","ts":27,"name":"emit"} ] //// [/home/src/workspaces/project/trace/types_0.json] *new* diff --git a/testdata/baselines/reference/tsc/noEmit/dts-errors.js b/testdata/baselines/reference/tsc/noEmit/dts-errors.js index 67254f6a2b2..299fdcb8dac 100644 --- a/testdata/baselines/reference/tsc/noEmit/dts-errors.js +++ b/testdata/baselines/reference/tsc/noEmit/dts-errors.js @@ -12,7 +12,7 @@ const a = class { private p = 10; }; } tsgo --noEmit -ExitStatus:: DiagnosticsPresent_OutputsSkipped +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: a.ts:1:7 - error TS4094: Property 'p' of exported anonymous class type may not be private or protected. @@ -55,7 +55,7 @@ declare const console: { log(msg: any): void; }; Edit [0]:: no change tsgo --noEmit -ExitStatus:: DiagnosticsPresent_OutputsSkipped +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: a.ts:1:7 - error TS4094: Property 'p' of exported anonymous class type may not be private or protected. @@ -118,7 +118,7 @@ Edit [5]:: Introduce error const a = class { private p = 10; }; tsgo --noEmit -ExitStatus:: DiagnosticsPresent_OutputsSkipped +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: a.ts:1:7 - error TS4094: Property 'p' of exported anonymous class type may not be private or protected. @@ -164,7 +164,7 @@ const a = class { Edit [7]:: no change tsgo --noEmit -ExitStatus:: DiagnosticsPresent_OutputsSkipped +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: a.ts:1:7 - error TS4094: Property 'p' of exported anonymous class type may not be private or protected. diff --git a/testdata/baselines/reference/tsc/noEmit/semantic-errors.js b/testdata/baselines/reference/tsc/noEmit/semantic-errors.js index 96ff47848c8..78f6e09630f 100644 --- a/testdata/baselines/reference/tsc/noEmit/semantic-errors.js +++ b/testdata/baselines/reference/tsc/noEmit/semantic-errors.js @@ -12,7 +12,7 @@ const a: number = "hello" } tsgo --noEmit -ExitStatus:: DiagnosticsPresent_OutputsSkipped +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: a.ts:1:7 - error TS2322: Type 'string' is not assignable to type 'number'. @@ -51,7 +51,7 @@ declare const console: { log(msg: any): void; }; Edit [0]:: no change tsgo --noEmit -ExitStatus:: DiagnosticsPresent_OutputsSkipped +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: a.ts:1:7 - error TS2322: Type 'string' is not assignable to type 'number'. @@ -107,7 +107,7 @@ Edit [5]:: Introduce error const a: number = "hello" tsgo --noEmit -ExitStatus:: DiagnosticsPresent_OutputsSkipped +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: a.ts:1:7 - error TS2322: Type 'string' is not assignable to type 'number'. @@ -140,7 +140,7 @@ Found 1 error in a.ts:1 Edit [7]:: no change tsgo --noEmit -ExitStatus:: DiagnosticsPresent_OutputsSkipped +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: a.ts:1:7 - error TS2322: Type 'string' is not assignable to type 'number'. diff --git a/testdata/baselines/reference/tsc/noEmit/syntax-errors.js b/testdata/baselines/reference/tsc/noEmit/syntax-errors.js index 5f1f5d6e7c4..8e28f48d7b3 100644 --- a/testdata/baselines/reference/tsc/noEmit/syntax-errors.js +++ b/testdata/baselines/reference/tsc/noEmit/syntax-errors.js @@ -12,7 +12,7 @@ const a = "hello } tsgo --noEmit -ExitStatus:: DiagnosticsPresent_OutputsSkipped +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: a.ts:1:17 - error TS1002: Unterminated string literal. @@ -51,7 +51,7 @@ declare const console: { log(msg: any): void; }; Edit [0]:: no change tsgo --noEmit -ExitStatus:: DiagnosticsPresent_OutputsSkipped +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: a.ts:1:17 - error TS1002: Unterminated string literal. @@ -107,7 +107,7 @@ Edit [5]:: Introduce error const a = "hello tsgo --noEmit -ExitStatus:: DiagnosticsPresent_OutputsSkipped +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: a.ts:1:17 - error TS1002: Unterminated string literal. @@ -143,7 +143,7 @@ const a = "hello; Edit [7]:: no change tsgo --noEmit -ExitStatus:: DiagnosticsPresent_OutputsSkipped +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: a.ts:1:17 - error TS1002: Unterminated string literal.