From 8f9b31542f71ccf8f7893f173a809af1295e5880 Mon Sep 17 00:00:00 2001 From: JemmyIndex Date: Thu, 5 Nov 2026 17:09:26 +0800 Subject: [PATCH 1/2] Report JS diagnostics at missing nodes without skipping trivia `jsErrorAtRange` unconditionally skipped leading trivia when creating the diagnostic span. For a missing node the location is empty (pos == end), so skipping the following whitespace moved the start past the end: the TS8010 on `var R: ;` in a .js file was reported at column 8 instead of 7, and `--pretty` crashed in strings.Repeat with a negative count because the span length was -1. Route node-anchored JS diagnostics through getErrorSpanForNode, which only skips trivia for present nodes. This matches the classic compiler, where createDiagnosticForNodeInSourceFile goes through getErrorSpanForNode and skips trivia only when the node is not missing. The three call sites that pass a node list (type parameters, parameters' modifiers, type arguments) keep the range helper, mirroring createDiagnosticForNodeArray, which skips trivia unconditionally. Fixes #65078 --- tsc/internal/parser/parser.go | 47 ++++++++++++++++------------- tsc/internal/parser/parser_test.go | 48 ++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/tsc/internal/parser/parser.go b/tsc/internal/parser/parser.go index db1bb03defb8c..4cd2aac15dc0e 100644 --- a/tsc/internal/parser/parser.go +++ b/tsc/internal/parser/parser.go @@ -6825,6 +6825,13 @@ func (p *Parser) parseResolutionMode(mode string, pos int, end int) (resolutionK return resolutionKind } +func (p *Parser) jsErrorAtNode(node *ast.Node, message *diagnostics.Message, args ...any) { + // getErrorSpanForNode does not skip trivia for missing nodes, so a diagnostic + // anchored at an empty location reports the position where the node would have + // been instead of the following token (see https://github.com/microsoft/TypeScript/issues/65078). + p.jsDiagnostics = append(p.jsDiagnostics, ast.NewDiagnostic(nil, getErrorSpanForNode(p.sourceText, node), message, args...)) +} + func (p *Parser) jsErrorAtRange(loc core.TextRange, message *diagnostics.Message, args ...any) { p.jsDiagnostics = append(p.jsDiagnostics, ast.NewDiagnostic(nil, core.NewTextRange(scanner.SkipTrivia(p.sourceText, loc.Pos()), loc.End()), message, args...)) } @@ -6838,7 +6845,7 @@ func (p *Parser) checkJSDecoratorSyntax(node *ast.Node) { if ast.CanHaveIllegalDecorators(node) { for _, modifier := range modifiers { if ast.IsDecorator(modifier) { - p.jsErrorAtRange(modifier.Loc, diagnostics.Decorators_are_not_valid_here) + p.jsErrorAtNode(modifier, diagnostics.Decorators_are_not_valid_here) break } } @@ -6853,7 +6860,7 @@ func (p *Parser) checkJSDecoratorSyntax(node *ast.Node) { }) if decoratorIndex > exportIndex && defaultIndex >= 0 && decoratorIndex < defaultIndex { // Decorator between `export` and `default` - p.jsErrorAtRange(modifiers[decoratorIndex].Loc, diagnostics.Decorators_are_not_valid_here) + p.jsErrorAtNode(modifiers[decoratorIndex], diagnostics.Decorators_are_not_valid_here) } else if decoratorIndex < exportIndex { // Find a trailing decorator after the export keyword trailingDecoratorIndex := -1 @@ -6890,56 +6897,56 @@ func (p *Parser) checkJSSyntax(node *ast.Node) *ast.Node { switch node.Kind { case ast.KindParameter, ast.KindPropertyDeclaration, ast.KindMethodDeclaration: if token := node.QuestionToken(); token != nil && token.Flags&ast.NodeFlagsReparsed == 0 && ast.IsQuestionToken(token) { - p.jsErrorAtRange(token.Loc, diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, "?") + p.jsErrorAtNode(token, diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, "?") } fallthrough case ast.KindMethodSignature, ast.KindConstructor, ast.KindGetAccessor, ast.KindSetAccessor, ast.KindFunctionExpression, ast.KindFunctionDeclaration, ast.KindArrowFunction, ast.KindVariableDeclaration, ast.KindIndexSignature: if ast.IsFunctionLike(node) && node.Body() == nil { - p.jsErrorAtRange(node.Loc, diagnostics.Signature_declarations_can_only_be_used_in_TypeScript_files) + p.jsErrorAtNode(node, diagnostics.Signature_declarations_can_only_be_used_in_TypeScript_files) } else if t := node.Type(); t != nil && t.Flags&ast.NodeFlagsReparsed == 0 { - p.jsErrorAtRange(t.Loc, diagnostics.Type_annotations_can_only_be_used_in_TypeScript_files) + p.jsErrorAtNode(t, diagnostics.Type_annotations_can_only_be_used_in_TypeScript_files) } case ast.KindImportDeclaration: if clause := node.ImportClause(); clause != nil && clause.IsTypeOnly() { - p.jsErrorAtRange(node.Loc, diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, "import type") + p.jsErrorAtNode(node, diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, "import type") } case ast.KindExportDeclaration: if node.IsTypeOnly() { - p.jsErrorAtRange(node.Loc, diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, "export type") + p.jsErrorAtNode(node, diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, "export type") } case ast.KindImportSpecifier: if node.IsTypeOnly() { - p.jsErrorAtRange(node.Loc, diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, "import...type") + p.jsErrorAtNode(node, diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, "import...type") } case ast.KindExportSpecifier: if node.IsTypeOnly() { - p.jsErrorAtRange(node.Loc, diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, "export...type") + p.jsErrorAtNode(node, diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, "export...type") } case ast.KindImportEqualsDeclaration: - p.jsErrorAtRange(node.Loc, diagnostics.X_import_can_only_be_used_in_TypeScript_files) + p.jsErrorAtNode(node, diagnostics.X_import_can_only_be_used_in_TypeScript_files) case ast.KindExportAssignment: if node.AsExportAssignment().IsExportEquals { - p.jsErrorAtRange(node.Loc, diagnostics.X_export_can_only_be_used_in_TypeScript_files) + p.jsErrorAtNode(node, diagnostics.X_export_can_only_be_used_in_TypeScript_files) } case ast.KindHeritageClause: if node.AsHeritageClause().Token == ast.KindImplementsKeyword { - p.jsErrorAtRange(node.Loc, diagnostics.X_implements_clauses_can_only_be_used_in_TypeScript_files) + p.jsErrorAtNode(node, diagnostics.X_implements_clauses_can_only_be_used_in_TypeScript_files) } case ast.KindInterfaceDeclaration: - p.jsErrorAtRange(node.Name().Loc, diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, "interface") + p.jsErrorAtNode(node.Name(), diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, "interface") case ast.KindModuleDeclaration: - p.jsErrorAtRange(node.Name().Loc, diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, scanner.TokenToString(node.AsModuleDeclaration().Keyword)) + p.jsErrorAtNode(node.Name(), diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, scanner.TokenToString(node.AsModuleDeclaration().Keyword)) case ast.KindTypeAliasDeclaration: - p.jsErrorAtRange(node.Name().Loc, diagnostics.Type_aliases_can_only_be_used_in_TypeScript_files) + p.jsErrorAtNode(node.Name(), diagnostics.Type_aliases_can_only_be_used_in_TypeScript_files) case ast.KindEnumDeclaration: - p.jsErrorAtRange(node.Name().Loc, diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, "enum") + p.jsErrorAtNode(node.Name(), diagnostics.X_0_declarations_can_only_be_used_in_TypeScript_files, "enum") case ast.KindNonNullExpression: - p.jsErrorAtRange(node.Loc, diagnostics.Non_null_assertions_can_only_be_used_in_TypeScript_files) + p.jsErrorAtNode(node, diagnostics.Non_null_assertions_can_only_be_used_in_TypeScript_files) case ast.KindAsExpression: - p.jsErrorAtRange(node.Type().Loc, diagnostics.Type_assertion_expressions_can_only_be_used_in_TypeScript_files) + p.jsErrorAtNode(node.Type(), diagnostics.Type_assertion_expressions_can_only_be_used_in_TypeScript_files) case ast.KindSatisfiesExpression: - p.jsErrorAtRange(node.Type().Loc, diagnostics.Type_satisfaction_expressions_can_only_be_used_in_TypeScript_files) + p.jsErrorAtNode(node.Type(), diagnostics.Type_satisfaction_expressions_can_only_be_used_in_TypeScript_files) } // Check decorator placement in JS files p.checkJSDecoratorSyntax(node) @@ -6954,7 +6961,7 @@ func (p *Parser) checkJSSyntax(node *ast.Node) *ast.Node { case ast.KindVariableStatement, ast.KindPropertyDeclaration: for _, modifier := range node.ModifierNodes() { if modifier.Flags&ast.NodeFlagsReparsed == 0 && modifier.Kind != ast.KindDecorator && ast.ModifierToFlag(modifier.Kind)&ast.ModifierFlagsJavaScript == 0 { - p.jsErrorAtRange(modifier.Loc, diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, scanner.TokenToString(modifier.Kind)) + p.jsErrorAtNode(modifier, diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, scanner.TokenToString(modifier.Kind)) } } case ast.KindParameter: diff --git a/tsc/internal/parser/parser_test.go b/tsc/internal/parser/parser_test.go index a1b7bef809623..dff743850ef3d 100644 --- a/tsc/internal/parser/parser_test.go +++ b/tsc/internal/parser/parser_test.go @@ -663,3 +663,51 @@ func TestCommonJSModuleIndicator(t *testing.T) { }) } } + +// https://github.com/microsoft/TypeScript/issues/65078 +// jsErrorAtNode must use getErrorSpanForNode so that diagnostics anchored at a +// missing (empty) node do not skip trivia: skipping moved the start past the +// end, producing an inverted span that reported the wrong column and crashed +// the --pretty writer with a negative strings.Repeat count. +func TestJSDiagnosticsForMissingNodesDoNotSkipTrivia(t *testing.T) { + t.Parallel() + tests := []struct { + name string + source string + code int32 + wantPos int + wantEnd int + }{ + { + name: "missing type annotation (var R: ;)", + source: "var R: ;", + code: diagnostics.Type_annotations_can_only_be_used_in_TypeScript_files.Code(), + wantPos: 6, // the missing type occupies [6, 6] + wantEnd: 6, + }, + { + name: "present type annotation still skips leading trivia", + source: "var R: number ;", + code: diagnostics.Type_annotations_can_only_be_used_in_TypeScript_files.Code(), + wantPos: 10, // 'number' + wantEnd: 16, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + opts := ast.SourceFileParseOptions{FileName: "/a.js", PathKey: tspath.CaseSensitive.PathKey("/a.js")} + file := parser.ParseSourceFile(opts, test.source, core.ScriptKindJS) + var found *ast.Diagnostic + for _, d := range file.JSDiagnostics() { + if d.Code() == test.code { + found = d + break + } + } + assert.Assert(t, found != nil, "expected diagnostic with code %d", test.code) + assert.Equal(t, found.Loc().Pos(), test.wantPos) + assert.Equal(t, found.Loc().End(), test.wantEnd) + }) + } +} From 33b13c6c4eeeea60afce97cb8d01f2ccbff5b6ea Mon Sep 17 00:00:00 2001 From: JemmyIndex Date: Fri, 6 Nov 2026 10:56:45 +0800 Subject: [PATCH 2/2] test: cover missing JS diagnostic spans through CLI --- tsc/internal/execute/tsctests/tsc_test.go | 22 +++++++++ tsc/internal/parser/parser_test.go | 48 ------------------- .../missing-type-annotation-pretty-false.js | 35 ++++++++++++++ .../missing-type-annotation-pretty-true.js | 46 ++++++++++++++++++ ...tation-with-leading-trivia-pretty-false.js | 34 +++++++++++++ ...otation-with-leading-trivia-pretty-true.js | 41 ++++++++++++++++ 6 files changed, 178 insertions(+), 48 deletions(-) create mode 100644 tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/missing-type-annotation-pretty-false.js create mode 100644 tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/missing-type-annotation-pretty-true.js create mode 100644 tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/present-type-annotation-with-leading-trivia-pretty-false.js create mode 100644 tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/present-type-annotation-with-leading-trivia-pretty-true.js diff --git a/tsc/internal/execute/tsctests/tsc_test.go b/tsc/internal/execute/tsctests/tsc_test.go index ee429a826f94a..681425333a8f2 100644 --- a/tsc/internal/execute/tsctests/tsc_test.go +++ b/tsc/internal/execute/tsctests/tsc_test.go @@ -55,6 +55,28 @@ func TestOutputSanitizerVersionPadding(t *testing.T) { } } +// https://github.com/microsoft/TypeScript/issues/65078 +func TestTscJSDiagnosticsForMissingNodes(t *testing.T) { + t.Parallel() + for _, test := range []struct { + name string + source string + }{ + {name: "missing type annotation", source: "var R: ;"}, + {name: "present type annotation with leading trivia", source: "var R: number ;"}, + } { + for _, pretty := range []string{"true", "false"} { + (&tscInput{ + subScenario: test.name + " pretty " + pretty, + files: FileMap{ + "/home/src/workspaces/project/a.js": test.source, + }, + commandLineArgs: []string{"a.js", "--allowJs", "--noEmit", "--pretty", pretty}, + }).run(t, "jsDiagnosticsForMissingNodes") + } + } +} + func TestTscCommandline(t *testing.T) { t.Parallel() colorTest := func(subScenario string, env map[string]string, outputIsTTY bool) *tscInput { diff --git a/tsc/internal/parser/parser_test.go b/tsc/internal/parser/parser_test.go index dff743850ef3d..a1b7bef809623 100644 --- a/tsc/internal/parser/parser_test.go +++ b/tsc/internal/parser/parser_test.go @@ -663,51 +663,3 @@ func TestCommonJSModuleIndicator(t *testing.T) { }) } } - -// https://github.com/microsoft/TypeScript/issues/65078 -// jsErrorAtNode must use getErrorSpanForNode so that diagnostics anchored at a -// missing (empty) node do not skip trivia: skipping moved the start past the -// end, producing an inverted span that reported the wrong column and crashed -// the --pretty writer with a negative strings.Repeat count. -func TestJSDiagnosticsForMissingNodesDoNotSkipTrivia(t *testing.T) { - t.Parallel() - tests := []struct { - name string - source string - code int32 - wantPos int - wantEnd int - }{ - { - name: "missing type annotation (var R: ;)", - source: "var R: ;", - code: diagnostics.Type_annotations_can_only_be_used_in_TypeScript_files.Code(), - wantPos: 6, // the missing type occupies [6, 6] - wantEnd: 6, - }, - { - name: "present type annotation still skips leading trivia", - source: "var R: number ;", - code: diagnostics.Type_annotations_can_only_be_used_in_TypeScript_files.Code(), - wantPos: 10, // 'number' - wantEnd: 16, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - opts := ast.SourceFileParseOptions{FileName: "/a.js", PathKey: tspath.CaseSensitive.PathKey("/a.js")} - file := parser.ParseSourceFile(opts, test.source, core.ScriptKindJS) - var found *ast.Diagnostic - for _, d := range file.JSDiagnostics() { - if d.Code() == test.code { - found = d - break - } - } - assert.Assert(t, found != nil, "expected diagnostic with code %d", test.code) - assert.Equal(t, found.Loc().Pos(), test.wantPos) - assert.Equal(t, found.Loc().End(), test.wantEnd) - }) - } -} diff --git a/tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/missing-type-annotation-pretty-false.js b/tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/missing-type-annotation-pretty-false.js new file mode 100644 index 0000000000000..a19a12b1f3cea --- /dev/null +++ b/tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/missing-type-annotation-pretty-false.js @@ -0,0 +1,35 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/a.js] *new* +var R: ; + +tsgo a.js --allowJs --noEmit --pretty false +ExitStatus:: DiagnosticsPresent_OutputsGenerated +Output:: +a.js(1,7): error TS8010: Type annotations can only be used in TypeScript files. +a.js(1,8): error TS1110: Type expected. +//// [/home/src/tslibs/TS/Lib/lib.es2026.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/tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/missing-type-annotation-pretty-true.js b/tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/missing-type-annotation-pretty-true.js new file mode 100644 index 0000000000000..682cef8657acf --- /dev/null +++ b/tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/missing-type-annotation-pretty-true.js @@ -0,0 +1,46 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/a.js] *new* +var R: ; + +tsgo a.js --allowJs --noEmit --pretty true +ExitStatus:: DiagnosticsPresent_OutputsGenerated +Output:: +a.js:1:7 - error TS8010: Type annotations can only be used in TypeScript files. + +1 var R: ; +   ~ + +a.js:1:8 - error TS1110: Type expected. + +1 var R: ; +   ~ + + +Found 2 errors in the same file, starting at: a.js:1 + +//// [/home/src/tslibs/TS/Lib/lib.es2026.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/tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/present-type-annotation-with-leading-trivia-pretty-false.js b/tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/present-type-annotation-with-leading-trivia-pretty-false.js new file mode 100644 index 0000000000000..0e6118fc4c2e8 --- /dev/null +++ b/tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/present-type-annotation-with-leading-trivia-pretty-false.js @@ -0,0 +1,34 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/a.js] *new* +var R: number ; + +tsgo a.js --allowJs --noEmit --pretty false +ExitStatus:: DiagnosticsPresent_OutputsGenerated +Output:: +a.js(1,11): error TS8010: Type annotations can only be used in TypeScript files. +//// [/home/src/tslibs/TS/Lib/lib.es2026.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/tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/present-type-annotation-with-leading-trivia-pretty-true.js b/tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/present-type-annotation-with-leading-trivia-pretty-true.js new file mode 100644 index 0000000000000..6cc60b8b6c37a --- /dev/null +++ b/tsc/testdata/baselines/reference/tsc/jsDiagnosticsForMissingNodes/present-type-annotation-with-leading-trivia-pretty-true.js @@ -0,0 +1,41 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/a.js] *new* +var R: number ; + +tsgo a.js --allowJs --noEmit --pretty true +ExitStatus:: DiagnosticsPresent_OutputsGenerated +Output:: +a.js:1:11 - error TS8010: Type annotations can only be used in TypeScript files. + +1 var R: number ; +   ~~~~~~ + + +Found 1 error in a.js:1 + +//// [/home/src/tslibs/TS/Lib/lib.es2026.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; }; +