Skip to content

Rewrite matchFiles without regexp #1483

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

Draft
wants to merge 55 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
1fc3c95
Don't use a regex in IsImplicitGlob
jakebailey Jun 6, 2025
948c0f5
Attempt one
jakebailey Jul 30, 2025
9a2bd4b
lint
jakebailey Jul 30, 2025
9b87477
More
jakebailey Jul 30, 2025
84211d2
unexport
jakebailey Jul 30, 2025
323344e
Move code
jakebailey Jul 30, 2025
113391c
More removal
jakebailey Jul 30, 2025
22b1daa
Benchmark
jakebailey Jul 30, 2025
93b2598
Lint
jakebailey Jul 30, 2025
415dae2
Optimize matchFilesNew: add path caching and efficient string concate…
jakebailey Jul 30, 2025
99d747c
Optimize path normalization, glob matching, and result collection for…
jakebailey Jul 30, 2025
d738488
Remove more regexp2
jakebailey Jul 30, 2025
d415699
Rename
jakebailey Jul 30, 2025
2e09780
Undo
jakebailey Jul 30, 2025
066ba11
Rename funcs
jakebailey Jul 30, 2025
2c8cbf8
Fix test
jakebailey Jul 30, 2025
d0c7409
Fix
jakebailey Jul 30, 2025
b497c2e
lint
jakebailey Jul 30, 2025
104acbb
tests
jakebailey Jul 30, 2025
ccdf5b6
Move code to new package so compilation is not slow
jakebailey Jul 30, 2025
234566f
Unexport more
jakebailey Jul 30, 2025
8493b7c
Missed a func
jakebailey Jul 30, 2025
61cb696
Fix glob pattern matching for explicit .min.js includes
jakebailey Jul 30, 2025
0c77771
Unexport
jakebailey Jul 30, 2025
3554586
Split new old again
jakebailey Jul 30, 2025
ffe5017
Testing
jakebailey Jul 30, 2025
1821416
Add differential testing and fix matchesExcludeNew for extensionless …
jakebailey Jul 30, 2025
8445b00
consolidate tests
jakebailey Jul 30, 2025
45c06d2
Rando tests
jakebailey Jul 30, 2025
6e089a8
Split benchmarks out into separate file
jakebailey Jul 30, 2025
e838322
Clearer
jakebailey Jul 31, 2025
8763fe3
Check
jakebailey Jul 31, 2025
a480ac4
Merge branch 'main' into jabaile/eliminate-regexp2-copilot-2
jakebailey Jul 31, 2025
3085416
reorder tests
jakebailey Jul 31, 2025
9edfd47
Update tests, but they are wrong
jakebailey Jul 31, 2025
e6174f2
Delete unused
jakebailey Jul 31, 2025
d882555
note surprising but correct result
jakebailey Jul 31, 2025
f419cd1
Strada did work this way, oh no
jakebailey Jul 31, 2025
4a1312e
wip
jakebailey Jul 31, 2025
dcb3cdc
it's doing something
jakebailey Jul 31, 2025
d77cf49
more tests unfortunately
jakebailey Jul 31, 2025
42edfff
hmm
jakebailey Jul 31, 2025
8b061bc
Fix lint
jakebailey Jul 31, 2025
5dd4298
Daniel's suggestions
jakebailey Jul 31, 2025
0290fa1
Move code needed for both out of old
jakebailey Jul 31, 2025
9bd9941
Rando copilot suggestions
jakebailey Jul 31, 2025
a4afd76
Use SplitSeq where possible
jakebailey Aug 1, 2025
bf9fdc4
Modernize
jakebailey Aug 1, 2025
faa832e
cleanup
jakebailey Aug 1, 2025
cbb7570
Merge branch 'main' into jabaile/eliminate-regexp2-copilot-2
jakebailey Aug 5, 2025
34998b5
more tests showing differences
jakebailey Aug 6, 2025
46ab1bd
Use any so we can do symlink tests soon
jakebailey Aug 6, 2025
c8c3097
This is getting funky
jakebailey Aug 6, 2025
5954e9a
baseline
jakebailey Aug 6, 2025
6028c94
lint
jakebailey Aug 8, 2025
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
3 changes: 2 additions & 1 deletion internal/project/discovertypings.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/microsoft/typescript-go/internal/semver"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
)

type CachedTyping struct {
Expand Down Expand Up @@ -220,7 +221,7 @@ func addTypingNamesAndGetFilesToWatch(
} else {
// And #2. Depth = 3 because scoped packages look like `node_modules/@foo/bar/package.json`
depth := 3
for _, manifestPath := range vfs.ReadDirectory(fs, projectRootPath, packagesFolderPath, []string{tspath.ExtensionJson}, nil, nil, &depth) {
for _, manifestPath := range vfsmatch.ReadDirectory(fs, projectRootPath, packagesFolderPath, []string{tspath.ExtensionJson}, nil, nil, &depth) {
if tspath.GetBaseFileName(manifestPath) != manifestName {
continue
}
Expand Down
51 changes: 8 additions & 43 deletions internal/tsoptions/tsconfigparsing.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package tsoptions

import (
"fmt"
"reflect"
"regexp"
"slices"
"strings"

"github.com/dlclark/regexp2"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
Expand All @@ -18,6 +16,7 @@ import (
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
)

type extendsResult struct {
Expand Down Expand Up @@ -99,36 +98,11 @@ type configFileSpecs struct {
}

func (c *configFileSpecs) matchesExclude(fileName string, comparePathsOptions tspath.ComparePathsOptions) bool {
if len(c.validatedExcludeSpecs) == 0 {
return false
}
excludePattern := vfs.GetRegularExpressionForWildcard(c.validatedExcludeSpecs, comparePathsOptions.CurrentDirectory, "exclude")
excludeRegex := vfs.GetRegexFromPattern(excludePattern, comparePathsOptions.UseCaseSensitiveFileNames)
if match, err := excludeRegex.MatchString(fileName); err == nil && match {
return true
}
if !tspath.HasExtension(fileName) {
if match, err := excludeRegex.MatchString(tspath.EnsureTrailingDirectorySeparator(fileName)); err == nil && match {
return true
}
}
return false
return vfsmatch.MatchesExclude(fileName, c.validatedExcludeSpecs, comparePathsOptions.CurrentDirectory, comparePathsOptions.UseCaseSensitiveFileNames)
}

func (c *configFileSpecs) matchesInclude(fileName string, comparePathsOptions tspath.ComparePathsOptions) bool {
if len(c.validatedIncludeSpecs) == 0 {
return false
}
for _, spec := range c.validatedIncludeSpecs {
includePattern := vfs.GetPatternFromSpec(spec, comparePathsOptions.CurrentDirectory, "files")
if includePattern != "" {
includeRegex := vfs.GetRegexFromPattern(includePattern, comparePathsOptions.UseCaseSensitiveFileNames)
if match, err := includeRegex.MatchString(fileName); err == nil && match {
return true
}
}
}
return false
return vfsmatch.MatchesInclude(fileName, c.validatedIncludeSpecs, comparePathsOptions.CurrentDirectory, comparePathsOptions.UseCaseSensitiveFileNames)
}

type FileExtensionInfo struct {
Expand Down Expand Up @@ -1571,24 +1545,15 @@ func getFileNamesFromConfigSpecs(
literalFileMap.Set(keyMappper(fileName), file)
}

var jsonOnlyIncludeRegexes []*regexp2.Regexp
var jsonOnlyIncludeSpecs []string
if len(validatedIncludeSpecs) > 0 {
files := vfs.ReadDirectory(host, basePath, basePath, core.Flatten(supportedExtensionsWithJsonIfResolveJsonModule), validatedExcludeSpecs, validatedIncludeSpecs, nil)
files := vfsmatch.ReadDirectory(host, basePath, basePath, core.Flatten(supportedExtensionsWithJsonIfResolveJsonModule), validatedExcludeSpecs, validatedIncludeSpecs, nil)
for _, file := range files {
if tspath.FileExtensionIs(file, tspath.ExtensionJson) {
if jsonOnlyIncludeRegexes == nil {
includes := core.Filter(validatedIncludeSpecs, func(include string) bool { return strings.HasSuffix(include, tspath.ExtensionJson) })
includeFilePatterns := core.Map(vfs.GetRegularExpressionsForWildcards(includes, basePath, "files"), func(pattern string) string { return fmt.Sprintf("^%s$", pattern) })
if includeFilePatterns != nil {
jsonOnlyIncludeRegexes = core.Map(includeFilePatterns, func(pattern string) *regexp2.Regexp {
return vfs.GetRegexFromPattern(pattern, host.UseCaseSensitiveFileNames())
})
} else {
jsonOnlyIncludeRegexes = nil
}
if jsonOnlyIncludeSpecs == nil {
jsonOnlyIncludeSpecs = core.Filter(validatedIncludeSpecs, func(include string) bool { return strings.HasSuffix(include, tspath.ExtensionJson) })
}
includeIndex := core.FindIndex(jsonOnlyIncludeRegexes, func(re *regexp2.Regexp) bool { return core.Must(re.MatchString(file)) })
if includeIndex != -1 {
if vfsmatch.MatchesIncludeWithJsonOnly(file, jsonOnlyIncludeSpecs, basePath, host.UseCaseSensitiveFileNames()) {
key := keyMappper(file)
if !literalFileMap.Has(key) && !wildCardJsonFileMap.Has(key) {
wildCardJsonFileMap.Set(key, file)
Expand Down
79 changes: 46 additions & 33 deletions internal/tsoptions/wildcarddirectories.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package tsoptions

import (
"regexp"
"strings"

"github.com/dlclark/regexp2"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
)

func getWildcardDirectories(include []string, exclude []string, comparePathsOptions tspath.ComparePathsOptions) map[string]bool {
Expand All @@ -27,24 +25,14 @@ func getWildcardDirectories(include []string, exclude []string, comparePathsOpti
return nil
}

rawExcludeRegex := vfs.GetRegularExpressionForWildcard(exclude, comparePathsOptions.CurrentDirectory, "exclude")
var excludeRegex *regexp.Regexp
if rawExcludeRegex != "" {
options := ""
if !comparePathsOptions.UseCaseSensitiveFileNames {
options = "(?i)"
}
excludeRegex = regexp.MustCompile(options + rawExcludeRegex)
}

wildcardDirectories := make(map[string]bool)
wildCardKeyToPath := make(map[string]string)

var recursiveKeys []string

for _, file := range include {
spec := tspath.NormalizeSlashes(tspath.CombinePaths(comparePathsOptions.CurrentDirectory, file))
if excludeRegex != nil && excludeRegex.MatchString(spec) {
if vfsmatch.MatchesExclude(spec, exclude, comparePathsOptions.CurrentDirectory, comparePathsOptions.UseCaseSensitiveFileNames) {
continue
}

Expand Down Expand Up @@ -99,9 +87,6 @@ func toCanonicalKey(path string, useCaseSensitiveFileNames bool) string {
return strings.ToLower(path)
}

// wildcardDirectoryPattern matches paths with wildcard characters
var wildcardDirectoryPattern = regexp2.MustCompile(`^[^*?]*(?=\/[^/]*[*?])`, 0)

// wildcardDirectoryMatch represents the result of a wildcard directory match
type wildcardDirectoryMatch struct {
Key string
Expand All @@ -110,27 +95,55 @@ type wildcardDirectoryMatch struct {
}

func getWildcardDirectoryFromSpec(spec string, useCaseSensitiveFileNames bool) *wildcardDirectoryMatch {
match, _ := wildcardDirectoryPattern.FindStringMatch(spec)
if match != nil {
// We check this with a few `Index` calls because it's more efficient than complex regex
questionWildcardIndex := strings.Index(spec, "?")
starWildcardIndex := strings.Index(spec, "*")
lastDirectorySeparatorIndex := strings.LastIndexByte(spec, tspath.DirectorySeparator)

// Determine if this should be watched recursively
recursive := (questionWildcardIndex != -1 && questionWildcardIndex < lastDirectorySeparatorIndex) ||
(starWildcardIndex != -1 && starWildcardIndex < lastDirectorySeparatorIndex)

return &wildcardDirectoryMatch{
Key: toCanonicalKey(match.String(), useCaseSensitiveFileNames),
Path: match.String(),
Recursive: recursive,
// Find the first occurrence of wildcards (* or ?)
questionWildcardIndex := strings.IndexByte(spec, '?')
starWildcardIndex := strings.IndexByte(spec, '*')

// Find the earliest wildcard
var firstWildcardIndex int
if questionWildcardIndex == -1 || starWildcardIndex == -1 {
firstWildcardIndex = max(questionWildcardIndex, starWildcardIndex)
} else {
firstWildcardIndex = min(questionWildcardIndex, starWildcardIndex)
}

if firstWildcardIndex != -1 {
// Find the last directory separator before the first wildcard
prefixBeforeWildcard := spec[:firstWildcardIndex]
lastSepIndex := strings.LastIndexByte(prefixBeforeWildcard, tspath.DirectorySeparator)

if lastSepIndex != -1 {
// Check if there are wildcards in the segment after this directory separator
segmentStart := lastSepIndex + 1
segmentEnd := strings.IndexByte(spec[segmentStart:], tspath.DirectorySeparator)
if segmentEnd == -1 {
segmentEnd = len(spec)
} else {
segmentEnd += segmentStart
}

segment := spec[segmentStart:segmentEnd]
if strings.ContainsAny(segment, "*?") {
path := spec[:lastSepIndex]

lastDirectorySeparatorIndex := strings.LastIndexByte(spec, tspath.DirectorySeparator)

// Determine if this should be watched recursively
lastWildcardIndex := max(questionWildcardIndex, starWildcardIndex)
recursive := 0 <= lastWildcardIndex && lastWildcardIndex < lastDirectorySeparatorIndex

return &wildcardDirectoryMatch{
Key: toCanonicalKey(path, useCaseSensitiveFileNames),
Path: path,
Recursive: recursive,
}
}
}
}

if lastSepIndex := strings.LastIndexByte(spec, tspath.DirectorySeparator); lastSepIndex != -1 {
lastSegment := spec[lastSepIndex+1:]
if vfs.IsImplicitGlob(lastSegment) {
if vfsmatch.IsImplicitGlob(lastSegment) {
path := tspath.RemoveTrailingDirectorySeparator(spec)
return &wildcardDirectoryMatch{
Key: toCanonicalKey(path, useCaseSensitiveFileNames),
Expand Down
32 changes: 19 additions & 13 deletions internal/tspath/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,19 @@ func GetPathComponents(path string, currentDirectory string) []string {

func pathComponents(path string, rootLength int) []string {
root := path[:rootLength]
rest := strings.Split(path[rootLength:], "/")
if len(rest) > 0 && rest[len(rest)-1] == "" {
rest = rest[:len(rest)-1]
restStr := path[rootLength:]
var rest []string
start := 0
for i := 0; i <= len(restStr); i++ {
if i == len(restStr) || restStr[i] == '/' {
if i > start {
seg := restStr[start:i]
if seg != "" {
rest = append(rest, seg)
}
}
start = i + 1
}
}
return append([]string{root}, rest...)
}
Expand Down Expand Up @@ -279,21 +289,17 @@ func reducePathComponents(components []string) []string {
if len(components) == 0 {
return []string{}
}
reduced := []string{components[0]}
reduced := make([]string, 0, len(components))
reduced = append(reduced, components[0])
for i := 1; i < len(components); i++ {
component := components[i]
if component == "" {
continue
}
if component == "." {
if component == "" || component == "." {
continue
}
if component == ".." {
if len(reduced) > 1 {
if reduced[len(reduced)-1] != ".." {
reduced = reduced[:len(reduced)-1]
continue
}
if len(reduced) > 1 && reduced[len(reduced)-1] != ".." {
reduced = reduced[:len(reduced)-1]
continue
} else if reduced[0] != "" {
continue
}
Expand Down
Loading
Loading