From 2cc0a9cf163bc2587eac63f8d73a861432f161dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 19:41:47 +0000 Subject: [PATCH 1/4] Initial plan From d97449ce6245ef51724e9e348b6d49e2930ccf28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 20:11:21 +0000 Subject: [PATCH 2/4] Fix source-map emit panic for unclosed blocks by clamping position Clamp the position in getLineAndCharacter to the text length to prevent a slice bounds out of range panic when the parser produces positions beyond the source text (e.g., for unclosed blocks). Fixes #1281 Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/4965050f-5ceb-40df-bcd2-6c97877ebf70 Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- internal/printer/utilities.go | 5 ++++ .../sourceMapUnclosedBlock.errors.txt | 8 ++++++ .../compiler/sourceMapUnclosedBlock.js | 9 +++++++ .../compiler/sourceMapUnclosedBlock.js.map | 3 +++ .../sourceMapUnclosedBlock.sourcemap.txt | 27 +++++++++++++++++++ .../compiler/sourceMapUnclosedBlock.symbols | 4 +++ .../compiler/sourceMapUnclosedBlock.types | 4 +++ .../cases/compiler/sourceMapUnclosedBlock.ts | 2 ++ 8 files changed, 62 insertions(+) create mode 100644 testdata/baselines/reference/compiler/sourceMapUnclosedBlock.errors.txt create mode 100644 testdata/baselines/reference/compiler/sourceMapUnclosedBlock.js create mode 100644 testdata/baselines/reference/compiler/sourceMapUnclosedBlock.js.map create mode 100644 testdata/baselines/reference/compiler/sourceMapUnclosedBlock.sourcemap.txt create mode 100644 testdata/baselines/reference/compiler/sourceMapUnclosedBlock.symbols create mode 100644 testdata/baselines/reference/compiler/sourceMapUnclosedBlock.types create mode 100644 testdata/tests/cases/compiler/sourceMapUnclosedBlock.ts diff --git a/internal/printer/utilities.go b/internal/printer/utilities.go index cc3839cba5a..0fe3821f84a 100644 --- a/internal/printer/utilities.go +++ b/internal/printer/utilities.go @@ -909,6 +909,11 @@ func newLineCharacterCache(source sourcemap.Source) *lineCharacterCache { // getLineAndCharacter returns the 0-based line number and UTF-16 code unit // offset from the start of that line for the given byte position. func (c *lineCharacterCache) getLineAndCharacter(pos int) (line int, character core.UTF16Offset) { + // Clamp pos to the text length to avoid panics for positions beyond + // the source text (e.g. synthesized end positions for unclosed blocks). + if pos > len(c.text) { + pos = len(c.text) + } line = scanner.ComputeLineOfPosition(c.lineMap, pos) if c.hasCached && line == c.cachedLine && pos >= c.cachedPos { // Incremental: only count UTF-16 code units from the last cached position. diff --git a/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.errors.txt b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.errors.txt new file mode 100644 index 00000000000..1c60f767b68 --- /dev/null +++ b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.errors.txt @@ -0,0 +1,8 @@ +sourceMapUnclosedBlock.ts(1,2): error TS1005: '}' expected. + + +==== sourceMapUnclosedBlock.ts (1 errors) ==== + { + +!!! error TS1005: '}' expected. +!!! related TS1007 sourceMapUnclosedBlock.ts:1:1: The parser expected to find a '}' to match the '{' token here. \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.js b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.js new file mode 100644 index 00000000000..7cc5e6aef7a --- /dev/null +++ b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.js @@ -0,0 +1,9 @@ +//// [tests/cases/compiler/sourceMapUnclosedBlock.ts] //// + +//// [sourceMapUnclosedBlock.ts] +{ + +//// [sourceMapUnclosedBlock.js] +"use strict"; +{ } +//# sourceMappingURL=sourceMapUnclosedBlock.js.map \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.js.map b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.js.map new file mode 100644 index 00000000000..8d25dcffdaf --- /dev/null +++ b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.js.map @@ -0,0 +1,3 @@ +//// [sourceMapUnclosedBlock.js.map] +{"version":3,"file":"sourceMapUnclosedBlock.js","sourceRoot":"","sources":["sourceMapUnclosedBlock.ts"],"names":[],"mappings":";AAAA,CAAC,CAAA,CAAA"} +//// https://sokra.github.io/source-map-visualization#base64,InVzZSBzdHJpY3QiOw0KeyB9DQovLyMgc291cmNlTWFwcGluZ1VSTD1zb3VyY2VNYXBVbmNsb3NlZEJsb2NrLmpzLm1hcA==,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic291cmNlTWFwVW5jbG9zZWRCbG9jay5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInNvdXJjZU1hcFVuY2xvc2VkQmxvY2sudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLENBQUMsQ0FBQSxDQUFBIn0=,ew== diff --git a/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.sourcemap.txt b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.sourcemap.txt new file mode 100644 index 00000000000..6f2ac77800f --- /dev/null +++ b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.sourcemap.txt @@ -0,0 +1,27 @@ +=================================================================== +JsFile: sourceMapUnclosedBlock.js +mapUrl: sourceMapUnclosedBlock.js.map +sourceRoot: +sources: sourceMapUnclosedBlock.ts +=================================================================== +------------------------------------------------------------------- +emittedFile:sourceMapUnclosedBlock.js +sourceFile:sourceMapUnclosedBlock.ts +------------------------------------------------------------------- +>>>"use strict"; +>>>{ } +1 > +2 >^ +3 > ^ +4 > ^ +5 > ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-> +1 > +2 >{ +3 > +4 > +1 >Emitted(2, 1) Source(1, 1) + SourceIndex(0) +2 >Emitted(2, 2) Source(1, 2) + SourceIndex(0) +3 >Emitted(2, 3) Source(1, 2) + SourceIndex(0) +4 >Emitted(2, 4) Source(1, 2) + SourceIndex(0) +--- +>>>//# sourceMappingURL=sourceMapUnclosedBlock.js.map \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.symbols b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.symbols new file mode 100644 index 00000000000..bedf7c17d81 --- /dev/null +++ b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.symbols @@ -0,0 +1,4 @@ +//// [tests/cases/compiler/sourceMapUnclosedBlock.ts] //// + +=== sourceMapUnclosedBlock.ts === +{ diff --git a/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.types b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.types new file mode 100644 index 00000000000..bedf7c17d81 --- /dev/null +++ b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.types @@ -0,0 +1,4 @@ +//// [tests/cases/compiler/sourceMapUnclosedBlock.ts] //// + +=== sourceMapUnclosedBlock.ts === +{ diff --git a/testdata/tests/cases/compiler/sourceMapUnclosedBlock.ts b/testdata/tests/cases/compiler/sourceMapUnclosedBlock.ts new file mode 100644 index 00000000000..a80b38b0dd2 --- /dev/null +++ b/testdata/tests/cases/compiler/sourceMapUnclosedBlock.ts @@ -0,0 +1,2 @@ +// @sourceMap: true +{ \ No newline at end of file From 251729467660319dffeebe5461ac06bf523cf6af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 20:48:13 +0000 Subject: [PATCH 3/4] Move out-of-bounds guard from getLineAndCharacter to emitPos Instead of clamping in getLineAndCharacter, skip emitting source mappings in emitPos when the position exceeds the source text length. This is consistent with emitPos's existing guards (synthesized positions, disabled source maps, etc.) and avoids masking bugs elsewhere. For error-recovery nodes like unclosed blocks, the parser produces end positions beyond the source text. TypeScript's equivalent code uses arithmetic (position - lineStarts[lineNumber]) which handles this implicitly, but Go's string-slicing approach for UTF-16 computation would panic. Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/cfe89aba-474b-4ce6-a914-fd1fddc59c71 Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- internal/printer/printer.go | 4 ++++ internal/printer/utilities.go | 5 ----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/printer/printer.go b/internal/printer/printer.go index cab32e25892..6d3731f9ce1 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -5773,6 +5773,10 @@ func (p *Printer) emitPos(pos int) { return } + if pos > len(p.sourceMapSource.Text()) { + return + } + sourceLine, sourceCharacter := p.sourceMapLineCharCache.getLineAndCharacter(pos) if err := p.sourceMapGenerator.AddSourceMapping( p.writer.GetLine(), diff --git a/internal/printer/utilities.go b/internal/printer/utilities.go index 0fe3821f84a..cc3839cba5a 100644 --- a/internal/printer/utilities.go +++ b/internal/printer/utilities.go @@ -909,11 +909,6 @@ func newLineCharacterCache(source sourcemap.Source) *lineCharacterCache { // getLineAndCharacter returns the 0-based line number and UTF-16 code unit // offset from the start of that line for the given byte position. func (c *lineCharacterCache) getLineAndCharacter(pos int) (line int, character core.UTF16Offset) { - // Clamp pos to the text length to avoid panics for positions beyond - // the source text (e.g. synthesized end positions for unclosed blocks). - if pos > len(c.text) { - pos = len(c.text) - } line = scanner.ComputeLineOfPosition(c.lineMap, pos) if c.hasCached && line == c.cachedLine && pos >= c.cachedPos { // Incremental: only count UTF-16 code units from the last cached position. From 8add68bcf9cff85a9474346c759b8bf12a86e0aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 21:14:55 +0000 Subject: [PATCH 4/4] Handle past-EOF positions in getLineAndCharacter instead of guarding emitPos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root cause: writeTokenText returns pos + len(tokenString), which goes past EOF for error-recovery tokens (e.g., missing closing braces). This is expected behavior — TypeScript has the identical flow. TypeScript handles it implicitly because computeLineAndCharacterOfPosition uses arithmetic (position - lineStarts[lineNumber]). The Go port's getLineAndCharacter uses string slicing for UTF-16 computation, which panics on out-of-bounds. Fix getLineAndCharacter to compute UTF-16 character offset up to EOF, then add the remaining byte offset arithmetically. This matches TypeScript's behavior exactly: the source map entry is emitted (not skipped) with a character position past the line end. Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/671ae780-244e-4f81-819a-9b54bb2981a0 Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- internal/printer/printer.go | 4 ---- internal/printer/utilities.go | 16 ++++++++++++---- .../compiler/sourceMapUnclosedBlock.js.map | 4 ++-- .../sourceMapUnclosedBlock.sourcemap.txt | 2 +- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/internal/printer/printer.go b/internal/printer/printer.go index 6d3731f9ce1..cab32e25892 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -5773,10 +5773,6 @@ func (p *Printer) emitPos(pos int) { return } - if pos > len(p.sourceMapSource.Text()) { - return - } - sourceLine, sourceCharacter := p.sourceMapLineCharCache.getLineAndCharacter(pos) if err := p.sourceMapGenerator.AddSourceMapping( p.writer.GetLine(), diff --git a/internal/printer/utilities.go b/internal/printer/utilities.go index cc3839cba5a..35d0137fa19 100644 --- a/internal/printer/utilities.go +++ b/internal/printer/utilities.go @@ -910,15 +910,23 @@ func newLineCharacterCache(source sourcemap.Source) *lineCharacterCache { // offset from the start of that line for the given byte position. func (c *lineCharacterCache) getLineAndCharacter(pos int) (line int, character core.UTF16Offset) { line = scanner.ComputeLineOfPosition(c.lineMap, pos) - if c.hasCached && line == c.cachedLine && pos >= c.cachedPos { + lineStart := int(c.lineMap[line]) + // When pos is beyond the source text (e.g., for error-recovery tokens like + // missing closing braces), we can't slice past the text end. Compute the + // UTF-16 length up to EOF and add the remaining byte offset arithmetically, + // matching TypeScript's computeLineAndCharacterOfPosition which uses + // arithmetic (position - lineStarts[lineNumber]) and handles this implicitly. + endPos := min(pos, len(c.text)) + if c.hasCached && line == c.cachedLine && endPos >= c.cachedPos { // Incremental: only count UTF-16 code units from the last cached position. - character = c.cachedChar + core.UTF16Len(c.text[c.cachedPos:pos]) + character = c.cachedChar + core.UTF16Len(c.text[c.cachedPos:endPos]) } else { // Full computation from line start. - character = core.UTF16Len(c.text[c.lineMap[line]:pos]) + character = core.UTF16Len(c.text[lineStart:endPos]) } + character += core.UTF16Offset(pos - endPos) c.cachedLine = line - c.cachedPos = pos + c.cachedPos = endPos c.cachedChar = character c.hasCached = true return line, character diff --git a/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.js.map b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.js.map index 8d25dcffdaf..3f1dddc6359 100644 --- a/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.js.map +++ b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.js.map @@ -1,3 +1,3 @@ //// [sourceMapUnclosedBlock.js.map] -{"version":3,"file":"sourceMapUnclosedBlock.js","sourceRoot":"","sources":["sourceMapUnclosedBlock.ts"],"names":[],"mappings":";AAAA,CAAC,CAAA,CAAA"} -//// https://sokra.github.io/source-map-visualization#base64,InVzZSBzdHJpY3QiOw0KeyB9DQovLyMgc291cmNlTWFwcGluZ1VSTD1zb3VyY2VNYXBVbmNsb3NlZEJsb2NrLmpzLm1hcA==,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic291cmNlTWFwVW5jbG9zZWRCbG9jay5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInNvdXJjZU1hcFVuY2xvc2VkQmxvY2sudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLENBQUMsQ0FBQSxDQUFBIn0=,ew== +{"version":3,"file":"sourceMapUnclosedBlock.js","sourceRoot":"","sources":["sourceMapUnclosedBlock.ts"],"names":[],"mappings":";AAAA,CAAC,CAAA,CAAC"} +//// https://sokra.github.io/source-map-visualization#base64,InVzZSBzdHJpY3QiOw0KeyB9DQovLyMgc291cmNlTWFwcGluZ1VSTD1zb3VyY2VNYXBVbmNsb3NlZEJsb2NrLmpzLm1hcA==,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic291cmNlTWFwVW5jbG9zZWRCbG9jay5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInNvdXJjZU1hcFVuY2xvc2VkQmxvY2sudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLENBQUMsQ0FBQSxDQUFDIn0=,ew== diff --git a/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.sourcemap.txt b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.sourcemap.txt index 6f2ac77800f..8483eec9c79 100644 --- a/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.sourcemap.txt +++ b/testdata/baselines/reference/compiler/sourceMapUnclosedBlock.sourcemap.txt @@ -22,6 +22,6 @@ sourceFile:sourceMapUnclosedBlock.ts 1 >Emitted(2, 1) Source(1, 1) + SourceIndex(0) 2 >Emitted(2, 2) Source(1, 2) + SourceIndex(0) 3 >Emitted(2, 3) Source(1, 2) + SourceIndex(0) -4 >Emitted(2, 4) Source(1, 2) + SourceIndex(0) +4 >Emitted(2, 4) Source(1, 3) + SourceIndex(0) --- >>>//# sourceMappingURL=sourceMapUnclosedBlock.js.map \ No newline at end of file