diff --git a/internal/execute/blackbox_panic_test.go b/internal/execute/blackbox_panic_test.go new file mode 100644 index 0000000000..19623f3dd6 --- /dev/null +++ b/internal/execute/blackbox_panic_test.go @@ -0,0 +1,32 @@ +package execute_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/execute" +) + +// This test intentionally reproduces a panic in source-map emission during declaration emit. +// It mirrors the blackbox invocation that crashes when declaration maps are enabled. +// The test is expected to FAIL until the underlying bug is fixed. +func TestDeclarationMap_DestructuredParam_BlackboxPanic(t *testing.T) { + t.Parallel() + + files := FileMap{ + "/home/src/workspaces/project/tsconfig.json": `{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "strict": true + } + }`, + "/home/src/workspaces/project/index.ts": `export const fn = ({ a, b }: { a: string; b: number }) => { console.log(a, b); };`, + } + + sys := newTestSys(files, "") + // Intentionally do not recover: we want the panic to crash the test to reproduce the bug. + _ = execute.CommandLine(sys, []string{}, true) +} + + diff --git a/internal/printer/printer.go b/internal/printer/printer.go index abb3829c5f..3b31f7c962 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -19,6 +19,7 @@ package printer import ( "fmt" + "os" "slices" "strings" @@ -5429,11 +5430,11 @@ func (p *Printer) setSourceMapSource(source sourcemap.Source) { } func (p *Printer) emitPos(pos int) { - if p.sourceMapsDisabled || p.sourceMapSource == nil || p.sourceMapGenerator == nil || p.sourceMapSourceIsJson || ast.PositionIsSynthesized(pos) { + if p.sourceMapsDisabled || p.sourceMapSource == nil || p.sourceMapGenerator == nil || p.sourceMapSourceIsJson || ast.PositionIsSynthesized(pos) { return } - - sourceLine, sourceCharacter := scanner.GetLineAndCharacterOfPosition(p.sourceMapSource, pos) + + sourceLine, sourceCharacter := scanner.GetLineAndCharacterOfPosition(p.sourceMapSource, pos) if err := p.sourceMapGenerator.AddSourceMapping( p.writer.GetLine(), p.writer.GetColumn(), @@ -5499,12 +5500,19 @@ func (p *Printer) emitSourceMapsBeforeNode(node *ast.Node) *sourceMapState { emitFlags := p.emitContext.EmitFlags(node) loc := p.emitContext.SourceMapRange(node) - - if !ast.IsNotEmittedStatement(node) && - emitFlags&EFNoLeadingSourceMap == 0 && - !ast.PositionIsSynthesized(loc.Pos()) { - p.emitSourcePos(p.sourceMapSource, scanner.SkipTrivia(p.currentSourceFile.Text(), loc.Pos())) // !!! support SourceMapRange from Strada? - } + // Prefer mapping against the original parse tree source file if available. + mapNode := p.emitContext.MostOriginal(node) + mapSourceFile := ast.GetSourceFileOfNode(mapNode) + mapSource := core.IfElse[sourcemap.Source](mapSourceFile != nil, mapSourceFile, p.sourceMapSource) + + if !ast.IsNotEmittedStatement(node) && + emitFlags&EFNoLeadingSourceMap == 0 && + !ast.PositionIsSynthesized(loc.Pos()) { + text := mapSource.Text() + pos := loc.Pos() + + p.emitSourcePos(mapSource, scanner.SkipTrivia(text, pos)) // !!! support SourceMapRange from Strada? + } if emitFlags&EFNoNestedSourceMaps != 0 { p.sourceMapsDisabled = true @@ -5522,6 +5530,9 @@ func (p *Printer) emitSourceMapsAfterNode(node *ast.Node, previousState *sourceM emitFlags := previousState.emitFlags loc := previousState.sourceMapRange + mapNode := p.emitContext.MostOriginal(node) + mapSourceFile := ast.GetSourceFileOfNode(mapNode) + mapSource := core.IfElse[sourcemap.Source](mapSourceFile != nil, mapSourceFile, p.sourceMapSource) if emitFlags&EFNoNestedSourceMaps != 0 { p.sourceMapsDisabled = false @@ -5530,7 +5541,7 @@ func (p *Printer) emitSourceMapsAfterNode(node *ast.Node, previousState *sourceM if !ast.IsNotEmittedStatement(node) && emitFlags&EFNoTrailingSourceMap == 0 && !ast.PositionIsSynthesized(loc.End()) { - p.emitSourcePos(p.sourceMapSource, loc.End()) // !!! support SourceMapRange from Strada? + p.emitSourcePos(mapSource, loc.End()) // !!! support SourceMapRange from Strada? } } @@ -5541,12 +5552,15 @@ func (p *Printer) emitSourceMapsBeforeToken(token ast.Kind, pos int, contextNode emitFlags := p.emitContext.EmitFlags(contextNode) loc, hasLoc := p.emitContext.TokenSourceMapRange(contextNode, token) + mapNode := p.emitContext.MostOriginal(contextNode) + mapSourceFile := ast.GetSourceFileOfNode(mapNode) + mapSource := core.IfElse[sourcemap.Source](mapSourceFile != nil, mapSourceFile, p.sourceMapSource) if emitFlags&EFNoTokenLeadingSourceMaps == 0 { if hasLoc { pos = loc.Pos() } if pos >= 0 { - p.emitSourcePos(p.sourceMapSource, pos) // !!! support SourceMapRange from Strada? + p.emitSourcePos(mapSource, pos) // !!! support SourceMapRange from Strada? } } @@ -5563,12 +5577,15 @@ func (p *Printer) emitSourceMapsAfterToken(token ast.Kind, pos int, contextNode emitFlags := previousState.emitFlags loc := previousState.sourceMapRange hasLoc := previousState.hasTokenSourceMapRange + mapNode := p.emitContext.MostOriginal(contextNode) + mapSourceFile := ast.GetSourceFileOfNode(mapNode) + mapSource := core.IfElse[sourcemap.Source](mapSourceFile != nil, mapSourceFile, p.sourceMapSource) if emitFlags&EFNoTokenTrailingSourceMaps == 0 { if hasLoc { pos = loc.End() } if pos >= 0 { - p.emitSourcePos(p.sourceMapSource, pos) // !!! support SourceMapRange from Strada? + p.emitSourcePos(mapSource, pos) // !!! support SourceMapRange from Strada? } } } diff --git a/internal/printer/sourcemap_mismatch_panic_test.go b/internal/printer/sourcemap_mismatch_panic_test.go new file mode 100644 index 0000000000..9bf287af24 --- /dev/null +++ b/internal/printer/sourcemap_mismatch_panic_test.go @@ -0,0 +1,66 @@ +package printer + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/parser" + "github.com/microsoft/typescript-go/internal/sourcemap" + "github.com/microsoft/typescript-go/internal/tspath" +) + +// This test intentionally reproduces the panic seen when the source map range +// refers to a different file (or position space) than the file used for text lookups. +// It constructs a small source file and forces a SourceMapRange with a huge Pos, +// then triggers printing with source maps enabled. The buggy implementation slices +// text[start:pos] with pos > len(text), panicking. +func TestSourceMapMismatchPanics(t *testing.T) { + t.Parallel() + + // Small source text (to be the current file) + sourceText := "export const x = 1;\n" + sf := parser.ParseSourceFile(ast.SourceFileParseOptions{ + FileName: "/home/src/workspaces/project/index.ts", + Path: "/home/src/workspaces/project/index.ts", + JSDocParsingMode: ast.JSDocParsingModeParseAll, + }, sourceText, core.ScriptKindTS) + + // Choose a node to print (the first statement) + if len(sf.Statements.Nodes) == 0 { + t.Fatalf("expected at least one statement") + } + stmt := sf.Statements.Nodes[0] + + // Create a second, much longer file to act as the "original" mapping source + longPrefix := make([]byte, 2500) + for i := range longPrefix { + longPrefix[i] = ' ' + } + longText := string(longPrefix) + "export const y = 2;\n" + sf2 := parser.ParseSourceFile(ast.SourceFileParseOptions{ + FileName: "/home/src/workspaces/project/long.ts", + Path: "/home/src/workspaces/project/long.ts", + JSDocParsingMode: ast.JSDocParsingModeParseAll, + }, longText, core.ScriptKindTS) + if len(sf2.Statements.Nodes) == 0 { + t.Fatalf("expected at least one statement in long file") + } + stmt2 := sf2.Statements.Nodes[0] + + // Build a printer with source maps enabled + emitCtx := NewEmitContext() + p := NewPrinter(PrinterOptions{SourceMap: true}, PrintHandlers{}, emitCtx) + writer := NewTextWriter("\n") + gen := sourcemap.NewGenerator("index.js", "", "/home/src/workspaces/project", tspath.ComparePathsOptions{}) + + // Map the current node to the original node from the longer file. + // This simulates transformation attaching original positions from a different file tree. + emitCtx.SetOriginal(stmt, stmt2) + emitCtx.SetSourceMapRange(stmt, stmt2.Loc) + + // This should not panic if source selection is correct (mapping against original file). + p.Write(stmt, sf, writer, gen) +} + + diff --git a/internal/printer/utilities.go b/internal/printer/utilities.go index 9eac7f1b89..fcbc644546 100644 --- a/internal/printer/utilities.go +++ b/internal/printer/utilities.go @@ -14,6 +14,8 @@ import ( "github.com/microsoft/typescript-go/internal/tspath" ) +// + type getLiteralTextFlags int const ( diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index fdad013ef1..9cf18c0c2a 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -2116,11 +2116,11 @@ func SkipTriviaEx(text string, pos int, options *SkipTriviaOptions) int { options = &SkipTriviaOptions{} } - textLen := len(text) + textLen := len(text) canConsumeStar := false // Keep in sync with couldStartTrivia for { - ch, size := utf8.DecodeRuneInString(text[pos:]) + ch, size := utf8.DecodeRuneInString(text[pos:]) switch ch { case '\r': if pos+1 < textLen && text[pos+1] == '\n' { @@ -2429,10 +2429,11 @@ func GetLineStarts(sourceFile ast.SourceFileLike) []core.TextPos { } func GetLineAndCharacterOfPosition(sourceFile ast.SourceFileLike, pos int) (line int, character int) { - lineMap := GetLineStarts(sourceFile) - line = ComputeLineOfPosition(lineMap, pos) - character = utf8.RuneCountInString(sourceFile.Text()[lineMap[line]:pos]) - return + lineMap := GetLineStarts(sourceFile) + line = ComputeLineOfPosition(lineMap, pos) + start := int(lineMap[line]) + character = utf8.RuneCountInString(sourceFile.Text()[start:pos]) + return } func GetEndLinePosition(sourceFile *ast.SourceFile, line int) int { diff --git a/internal/scanner/utilities.go b/internal/scanner/utilities.go index 06ba7cf666..c947eee07a 100644 --- a/internal/scanner/utilities.go +++ b/internal/scanner/utilities.go @@ -1,13 +1,15 @@ package scanner import ( - "strings" - "unicode/utf8" + "strings" + "unicode/utf8" - "github.com/microsoft/typescript-go/internal/ast" - "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/core" ) +// + func tokenIsIdentifierOrKeyword(token ast.Kind) bool { return token >= ast.KindIdentifier }