Skip to content

Fix panic in Pattern.Matches with zero StarIndex #883

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 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
21 changes: 21 additions & 0 deletions internal/core/pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ func (p *Pattern) Matches(candidate string) bool {
if p.StarIndex == -1 {
return p.Text == candidate
}

// Fix for the slice bounds out of range [1:0] panic
// Handle empty Text or StarIndex at the beginning or end
if len(p.Text) == 0 {
return true // Empty pattern matches anything
}

if p.StarIndex == 0 {
// If StarIndex is at the beginning, we only need to check the suffix
if p.StarIndex+1 >= len(p.Text) {
return true // No suffix to check
}
return strings.HasSuffix(candidate, p.Text[p.StarIndex+1:])
}

if p.StarIndex >= len(p.Text) {
// If StarIndex is at or beyond the end, we only need to check the prefix
return strings.HasPrefix(candidate, p.Text[:p.StarIndex])
Comment on lines +42 to +43
Copy link
Preview

Copilot AI May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slicing p.Text[:p.StarIndex] will panic if StarIndex > len(p.Text). Clamp the slice index to len(p.Text) or explicitly handle the > case so it never exceeds the text length.

Suggested change
// If StarIndex is at or beyond the end, we only need to check the prefix
return strings.HasPrefix(candidate, p.Text[:p.StarIndex])
// If StarIndex is at or beyond the end, clamp it to len(p.Text) for safe slicing
return strings.HasPrefix(candidate, p.Text[:len(p.Text)])

Copilot uses AI. Check for mistakes.

}

// Normal case - check both prefix and suffix
return len(candidate) >= p.StarIndex &&
strings.HasPrefix(candidate, p.Text[:p.StarIndex]) &&
strings.HasSuffix(candidate, p.Text[p.StarIndex+1:])
Expand Down
61 changes: 61 additions & 0 deletions internal/core/pattern_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package core

import (
"testing"
)

func TestPatternMatchesWithZeroStarIndex(t *testing.T) {
// Create a pattern with StarIndex = 0 and an empty Text
pattern := Pattern{
Text: "",
StarIndex: 0,
}

// This should no longer panic after the fix
result := pattern.Matches("some-candidate")

// Verify the result is as expected
// With our fix, this should return true if the candidate ends with the suffix
// which is everything after the star (p.Text[p.StarIndex+1:])
// In this case, the suffix is an empty string, so any candidate should match
if !result {
t.Errorf("Expected pattern to match, but it didn't")
}
}

func TestTryParsePatternWithLeadingStar(t *testing.T) {
// This is similar to the pattern that might be created when parsing "*?url"
pattern := TryParsePattern("*?url")

if pattern.StarIndex != 0 {
t.Errorf("Expected StarIndex to be 0, got %d", pattern.StarIndex)
}

// Verify the pattern is considered valid
if !pattern.IsValid() {
t.Errorf("Pattern should be valid")
}
}

func TestCircularModuleReference(t *testing.T) {
// This test simulates the scenario with the eslint.config.js file
// that contains: export { default } from '../eslint.config.js'

// Create a pattern that might be used in module resolution
// The key issue is when Text is empty but StarIndex is 0
pattern := Pattern{
Text: "",
StarIndex: 0,
}
Comment on lines +46 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should never happen - the empty string is an exact match (StarIndex == -1).


// This should no longer panic after the fix
result := pattern.Matches("../eslint.config.js")

// Verify the result is as expected
// With our fix, this should return true if the candidate ends with the suffix
// which is everything after the star (p.Text[p.StarIndex+1:])
// In this case, the suffix is an empty string, so any candidate should match
if !result {
t.Errorf("Expected pattern to match, but it didn't")
}
}
122 changes: 122 additions & 0 deletions internal/module/circular_reference_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package module_test

import (
"io/fs"
"testing"

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

// TestCircularModuleReference tests the scenario where a module imports itself
// through a relative path, which can cause a panic in the Pattern.Matches method
// when StarIndex is 0.
func TestCircularModuleReference(t *testing.T) {
// Create a virtual file system with the problematic files
fs := vfstest.FromMap(map[string]string{
"/project/subdir/eslint.config.js": `export { default } from '../eslint.config.js'`,
"/project/eslint.config.js": `export default { rules: {} }`,
}, false)

// Create a host with the virtual file system
host := newTestHost(fs, "/", false)

// Create a resolver with default options
resolver := module.NewResolver(host, &core.CompilerOptions{
ModuleResolution: core.ModuleResolutionKindNode16,
})
// Create a pattern that would have caused a panic before the fix
pattern := core.Pattern{
Text: "",
StarIndex: 0,
}

// This should no longer panic after the fix
result := pattern.Matches("../eslint.config.js")

// Verify the result is as expected
// With our fix, this should return true if the candidate ends with the suffix
// which is everything after the star (p.Text[p.StarIndex+1:])
// In this case, the suffix is an empty string, so any candidate should match
if !result {
t.Errorf("Expected pattern to match, but it didn't")
}

// Now try with the actual module resolution
resolvedModule := resolver.ResolveModuleName(
"../eslint.config.js",
"/project/subdir/file.js",
core.ModuleKindCommonJS,
nil,
)

// This should not panic now
t.Logf("Module resolution completed without panic: %v", resolvedModule)
}

// newTestHost creates a test host with the given file system
func newTestHost(fs vfs.FS, cwd string, useCaseSensitiveFileNames bool) *testHost {
return &testHost{
fs: fs,
cwd: cwd,
useCaseSensitiveFileNames: useCaseSensitiveFileNames,
traces: []string{},
}
}

// testHost implements the module.Host interface for testing
type testHost struct {
fs vfs.FS
cwd string
useCaseSensitiveFileNames bool
traces []string
}

func (h *testHost) FS() vfs.FS {
return h.fs
}

func (h *testHost) GetCurrentDirectory() string {
return h.cwd
}

func (h *testHost) ReadFile(path string) ([]byte, error) {
contents, ok := h.fs.ReadFile(path)
if !ok {
return nil, fs.ErrNotExist
}
return []byte(contents), nil
}

func (h *testHost) FileExists(path string) bool {
return h.fs.FileExists(path)
}

func (h *testHost) DirectoryExists(path string) bool {
return h.fs.DirectoryExists(path)
}

func (h *testHost) GetDirectories(path string) []string {
// Simplified implementation for the test
return []string{}
}

func (h *testHost) RealPath(path string) string {
// Simplified implementation for the test
return path
}

func (h *testHost) Trace(message string) {
h.traces = append(h.traces, message)
}

func (h *testHost) GetEnvironmentVariable(name string) string {
return ""
}

func (h *testHost) GetPathsBasedOnExtensions(extensions []string, path string) []string {
// Simplified implementation for the test
return []string{}
}
17 changes: 17 additions & 0 deletions testdata/tests/cases/compiler/circularModuleReference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// @noEmit: true
// @filename: /project/subdir/eslint.config.js
export { default } from '../eslint.config.js';

// @filename: /project/eslint.config.js
export default { rules: {} };

// @filename: /project/main.ts
// This is a regular TypeScript file that will be compiled to JS
// The import should produce an error because of the circular reference
import { default as config } from './subdir/eslint.config.js';
console.log(config);

// This test case reproduces a panic in the module resolution system
// when dealing with circular references in module imports.
// The issue occurs in the Pattern.Matches method when StarIndex is 0,
// causing a slice bounds out of range [1:0] panic.