Skip to content

Fix panic in SkipTriviaEx when printing type predicates with declaration maps enabled #1093

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions internal/scanner/issue1092_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package scanner

import (
"testing"

"github.com/microsoft/typescript-go/internal/core"
)

// TestTypePredicate_Issue1092 tests the specific scenario that caused the panic in issue #1092
// This test ensures that type predicates with declaration maps enabled do not cause panics
func TestTypePredicate_Issue1092(t *testing.T) {
// The original problematic text that caused the slice bounds out of range panic
// This simulates the content from a declaration map that references a position beyond text bounds

shortText := "import { foo } from './export';\nexport const x = foo();"
textLen := len(shortText) // Should be 55 characters

// The original issue had pos=158 (or 167) but text length was 55/58
problematicPos := 158

// This should not panic - before the fix, this would cause:
// panic: runtime error: slice bounds out of range [158:55]
result := SkipTriviaEx(shortText, problematicPos, nil)

// The function should return the original position when it's beyond text bounds
if result != problematicPos {
t.Errorf("Expected position %d, got %d", problematicPos, result)
}

// Also test the GetLineAndCharacterOfPosition function that had a similar issue
sourceFile := &mockSourceFile{
text: shortText,
lineMap: []core.TextPos{0, 27}, // Approximate line starts
}

// This should also not panic
line, char := GetLineAndCharacterOfPosition(sourceFile, problematicPos)

// We should get valid results (not negative numbers)
if line < 0 {
t.Errorf("Got negative line number: %d", line)
}
if char < 0 {
t.Errorf("Got negative character number: %d", char)
}

t.Logf("Successfully handled position %d beyond text length %d", problematicPos, textLen)
t.Logf("Computed line: %d, character: %d", line, char)
}
13 changes: 12 additions & 1 deletion internal/scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2094,6 +2094,9 @@ func SkipTriviaEx(text string, pos int, options *SkipTriviaOptions) int {
}

textLen := len(text)
if pos >= textLen {
return pos
}
canConsumeStar := false
// Keep in sync with couldStartTrivia
for {
Expand Down Expand Up @@ -2327,7 +2330,15 @@ 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])
textLen := len(sourceFile.Text())
startPos := int(lineMap[line])
if pos > textLen {
pos = textLen
}
if startPos > textLen {
startPos = textLen
}
character = utf8.RuneCountInString(sourceFile.Text()[startPos:pos])
return
}

Expand Down
80 changes: 80 additions & 0 deletions internal/scanner/scanner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package scanner

import (
"testing"

"github.com/microsoft/typescript-go/internal/core"
)

func TestSkipTriviaEx_BoundsCheck(t *testing.T) {
// Test with trivia (spaces and tabs)
text := " \t hello"

// Test normal functionality - should skip leading whitespace
result := SkipTriviaEx(text, 0, nil)
if result != 4 { // Should skip 2 spaces and 1 tab to position 4 ('h')
t.Errorf("Expected position 4, got %d", result)
}

textLen := len(text) // 8 characters

// Test position at end of text
result = SkipTriviaEx(text, textLen, nil)
if result != textLen {
t.Errorf("Expected position %d, got %d", textLen, result)
}

// Test position beyond text bounds (should not panic)
result = SkipTriviaEx(text, textLen+10, nil)
if result != textLen+10 {
t.Errorf("Expected position %d, got %d", textLen+10, result)
}

// Test position way beyond text bounds (reproduces the original issue)
result = SkipTriviaEx(text, 158, nil)
if result != 158 {
t.Errorf("Expected position 158, got %d", result)
}
}

func TestGetLineAndCharacterOfPosition_BoundsCheck(t *testing.T) {
// Create a mock source file with a small text
sourceFile := &mockSourceFile{
text: "hello\nworld",
lineMap: []core.TextPos{0, 6}, // Line 0 starts at 0, line 1 starts at 6
}

textLen := len(sourceFile.text) // 11 characters

// Test position within bounds
line, char := GetLineAndCharacterOfPosition(sourceFile, 7)
if line != 1 || char != 1 {
t.Errorf("Expected line 1, char 1, got line %d, char %d", line, char)
}

// Test position beyond text bounds (should not panic)
line, char = GetLineAndCharacterOfPosition(sourceFile, textLen+10)
if line < 0 {
t.Errorf("Expected valid line, got %d", line)
}

// Test the exact scenario from the bug report
line, char = GetLineAndCharacterOfPosition(sourceFile, 158)
if line < 0 {
t.Errorf("Expected valid line, got %d", line)
}
}

// Mock source file for testing
type mockSourceFile struct {
text string
lineMap []core.TextPos
}

func (m *mockSourceFile) Text() string {
return m.text
}

func (m *mockSourceFile) LineMap() []core.TextPos {
return m.lineMap
}
Loading