Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5f0c199
Move vfs matching to package
jakebailey Dec 17, 2025
e169936
Big test
jakebailey Dec 17, 2025
89940b4
it works
jakebailey Dec 17, 2025
e08673a
Start splitting apart
jakebailey Dec 17, 2025
97046fe
fmt
jakebailey Dec 17, 2025
c8ead74
perf optimizations
jakebailey Dec 17, 2025
c1c0947
fix benchmarks
jakebailey Dec 17, 2025
e16c972
perf optimizations
jakebailey Dec 17, 2025
e1a8f97
Even fewer regex
jakebailey Dec 17, 2025
8d9ecfd
move to old school testing
jakebailey Dec 17, 2025
f3e63f2
Make seperable
jakebailey Dec 17, 2025
297c82b
fmt
jakebailey Dec 17, 2025
57c83ff
Remove from getWildcardDirectoryFromSpec
jakebailey Dec 17, 2025
9ddea2e
bench
jakebailey Dec 17, 2025
bab63db
Eliminate another
jakebailey Dec 17, 2025
83931d1
Drop bad comments
jakebailey Dec 17, 2025
ccd944c
Make my life easier
jakebailey Dec 17, 2025
c21638a
more perf
jakebailey Dec 17, 2025
f37c9bf
Simplify
jakebailey Dec 17, 2025
314bc82
Proper enum
jakebailey Dec 17, 2025
6919ad3
No hardcode
jakebailey Dec 17, 2025
834fbd8
Big simplify pass
jakebailey Dec 18, 2025
604196e
More simplficiations
jakebailey Dec 18, 2025
ef638bf
More simplficiations
jakebailey Dec 18, 2025
c06760b
More simplficiations
jakebailey Dec 18, 2025
0801191
Fix range
jakebailey Dec 18, 2025
e511530
rando fixups
jakebailey Dec 18, 2025
8d06bcd
Unused method
jakebailey Dec 18, 2025
816b3b2
Remove leftover nil checks
jakebailey Dec 18, 2025
1168650
more testing
jakebailey Dec 18, 2025
e51f138
More cleanup, tests
jakebailey Dec 18, 2025
68e49cf
big oops
jakebailey Dec 18, 2025
1d8a075
Fix confusing min.js behavior
jakebailey Dec 18, 2025
316e46d
Pesky pesky min.js
jakebailey Dec 18, 2025
ee3f2e4
why can't I type
jakebailey Dec 18, 2025
5f971f6
More perf
jakebailey Dec 18, 2025
efd6350
GetNormalizedPathComponents specialize
jakebailey Dec 18, 2025
323caa4
More perf optimizations
jakebailey Dec 18, 2025
041843f
Random exported funcs
jakebailey Dec 18, 2025
9a6cbbb
Stop using pointer for depth
jakebailey Dec 19, 2025
e20b040
globPattern as a value type
jakebailey Dec 19, 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
38 changes: 13 additions & 25 deletions internal/ls/autoimports.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"strings"

"github.com/dlclark/regexp2"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/binder"
Expand All @@ -24,7 +23,7 @@ import (
"github.com/microsoft/typescript-go/internal/packagejson"
"github.com/microsoft/typescript-go/internal/stringutil"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
)

type SymbolExportInfo struct {
Expand Down Expand Up @@ -1384,15 +1383,15 @@ func forEachExternalModuleToImportFrom(
// useAutoImportProvider bool,
cb func(module *ast.Symbol, moduleFile *ast.SourceFile, checker *checker.Checker, isFromPackageJson bool),
) {
var excludePatterns []*regexp2.Regexp
var excludeMatcher vfsmatch.SpecMatcher
if preferences.AutoImportFileExcludePatterns != nil {
excludePatterns = getIsExcludedPatterns(preferences, program.UseCaseSensitiveFileNames())
excludeMatcher = getIsExcludedMatcher(preferences, program.UseCaseSensitiveFileNames())
}

forEachExternalModule(
ch,
program.GetSourceFiles(),
excludePatterns,
excludeMatcher,
func(module *ast.Symbol, file *ast.SourceFile) {
cb(module, file, ch, false)
},
Expand All @@ -1414,35 +1413,26 @@ func forEachExternalModuleToImportFrom(
// }
}

func getIsExcludedPatterns(preferences *lsutil.UserPreferences, useCaseSensitiveFileNames bool) []*regexp2.Regexp {
func getIsExcludedMatcher(preferences *lsutil.UserPreferences, useCaseSensitiveFileNames bool) vfsmatch.SpecMatcher {
if preferences.AutoImportFileExcludePatterns == nil {
return nil
}
var patterns []*regexp2.Regexp
for _, spec := range preferences.AutoImportFileExcludePatterns {
pattern := vfs.GetSubPatternFromSpec(spec, "", vfs.UsageExclude, vfs.WildcardMatcher{})
if pattern != "" {
if re := vfs.GetRegexFromPattern(pattern, useCaseSensitiveFileNames); re != nil {
patterns = append(patterns, re)
}
}
}
return patterns
return vfsmatch.NewSpecMatcher(preferences.AutoImportFileExcludePatterns, "", vfsmatch.UsageExclude, useCaseSensitiveFileNames)
}

func forEachExternalModule(
ch *checker.Checker,
allSourceFiles []*ast.SourceFile,
excludePatterns []*regexp2.Regexp,
excludeMatcher vfsmatch.SpecMatcher,
cb func(moduleSymbol *ast.Symbol, sourceFile *ast.SourceFile),
) {
var isExcluded func(*ast.SourceFile) bool = func(_ *ast.SourceFile) bool { return false }
if excludePatterns != nil {
isExcluded = getIsExcluded(excludePatterns)
if excludeMatcher != nil {
isExcluded = getIsExcluded(excludeMatcher)
}

for _, ambient := range ch.GetAmbientModules() {
if !strings.Contains(ambient.Name, "*") && !(excludePatterns != nil && core.Every(ambient.Declarations, func(d *ast.Node) bool {
if !strings.Contains(ambient.Name, "*") && !(excludeMatcher != nil && core.Every(ambient.Declarations, func(d *ast.Node) bool {
return isExcluded(ast.GetSourceFileOfNode(d))
})) {
cb(ambient, nil /*sourceFile*/)
Expand All @@ -1455,15 +1445,13 @@ func forEachExternalModule(
}
}

func getIsExcluded(excludePatterns []*regexp2.Regexp) func(sourceFile *ast.SourceFile) bool {
func getIsExcluded(excludeMatcher vfsmatch.SpecMatcher) func(sourceFile *ast.SourceFile) bool {
// !!! SymlinkCache
// const realpathsWithSymlinks = host.getSymlinkCache?.().getSymlinkedDirectoriesByRealpath();
return func(sourceFile *ast.SourceFile) bool {
fileName := sourceFile.FileName()
for _, p := range excludePatterns {
if matched, _ := p.MatchString(fileName); matched {
return true
}
if excludeMatcher.MatchString(fileName) {
return true
}
// !! SymlinkCache
// if (realpathsWithSymlinks?.size && pathContainsNodeModules(fileName)) {
Expand Down
3 changes: 2 additions & 1 deletion internal/project/ata/discovertypings.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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"
)

func isTypingUpToDate(cachedTyping *CachedTyping, availableTypingVersions map[string]string) bool {
Expand Down Expand Up @@ -222,7 +223,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
3 changes: 2 additions & 1 deletion internal/tsoptions/parsedcommandline.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/microsoft/typescript-go/internal/outputpaths"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
)

const (
Expand Down Expand Up @@ -326,7 +327,7 @@ func (p *ParsedCommandLine) PossiblyMatchesFileName(fileName string) bool {
}

for _, include := range p.ConfigFile.configFileSpecs.validatedIncludeSpecs {
if !strings.ContainsAny(include, "*?") && !vfs.IsImplicitGlob(include) {
if !strings.ContainsAny(include, "*?") && !vfsmatch.IsImplicitGlob(include) {
includePath := tspath.ToPath(include, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())
if includePath == path {
return true
Expand Down
64 changes: 26 additions & 38 deletions internal/tsoptions/tsconfigparsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ package tsoptions

import (
"cmp"
"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 @@ -20,6 +17,7 @@ import (
"github.com/microsoft/typescript-go/internal/parser"
"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 @@ -106,13 +104,15 @@ func (c *configFileSpecs) matchesExclude(fileName string, comparePathsOptions ts
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 {
excludeMatcher := vfsmatch.NewSpecMatcher(c.validatedExcludeSpecs, comparePathsOptions.CurrentDirectory, vfsmatch.UsageExclude, comparePathsOptions.UseCaseSensitiveFileNames)
if excludeMatcher == nil {
return false
}
if excludeMatcher.MatchString(fileName) {
return true
}
if !tspath.HasExtension(fileName) {
if match, err := excludeRegex.MatchString(tspath.EnsureTrailingDirectorySeparator(fileName)); err == nil && match {
if excludeMatcher.MatchString(tspath.EnsureTrailingDirectorySeparator(fileName)) {
return true
}
}
Expand All @@ -124,12 +124,9 @@ func (c *configFileSpecs) getMatchedIncludeSpec(fileName string, comparePathsOpt
return ""
}
for index, 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 c.validatedIncludeSpecsBeforeSubstitution[index]
}
includeMatcher := vfsmatch.NewSingleSpecMatcher(spec, comparePathsOptions.CurrentDirectory, vfsmatch.UsageFiles, comparePathsOptions.UseCaseSensitiveFileNames)
if includeMatcher != nil && includeMatcher.MatchString(fileName) {
return c.validatedIncludeSpecsBeforeSubstitution[index]
}
}
return ""
Expand Down Expand Up @@ -1386,7 +1383,7 @@ func validateSpecs(specs any, disallowTrailingRecursion bool, jsonSourceFile *as

func specToDiagnostic(spec string, disallowTrailingRecursion bool) *diagnostics.Message {
if disallowTrailingRecursion {
if ok, _ := regexp.MatchString(invalidTrailingRecursionPattern, spec); ok {
if invalidTrailingRecursion(spec) {
return diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0
}
} else if invalidDotDotAfterRecursiveWildcard(spec) {
Expand All @@ -1395,6 +1392,13 @@ func specToDiagnostic(spec string, disallowTrailingRecursion bool) *diagnostics.
return nil
}

func invalidTrailingRecursion(spec string) bool {
// Matches **, /**, **/, and /**/, but not a**b.
// Strip optional trailing slash, then check if it ends with /** or is just **
s := strings.TrimSuffix(spec, "/")
return s == "**" || strings.HasSuffix(s, "/**")
}

func invalidDotDotAfterRecursiveWildcard(s string) bool {
// We used to use the regex /(^|\/)\*\*\/(.*\/)?\.\.($|\/)/ to check for this case, but
// in v8, that has polynomial performance because the recursive wildcard match - **/ -
Expand All @@ -1419,18 +1423,6 @@ func invalidDotDotAfterRecursiveWildcard(s string) bool {
return lastDotIndex > wildcardIndex
}

// Tests for a path that ends in a recursive directory wildcard.
//
// Matches **, \**, **\, and \**\, but not a**b.
// NOTE: used \ in place of / above to avoid issues with multiline comments.
//
// Breakdown:
//
// (^|\/) # matches either the beginning of the string or a directory separator.
// \*\* # matches the recursive directory wildcard "**".
// \/?$ # matches an optional trailing directory separator at the end of the string.
const invalidTrailingRecursionPattern = `(?:^|\/)\*\*\/?$`

func GetTsConfigPropArrayElementValue(tsConfigSourceFile *ast.SourceFile, propKey string, elementValue string) *ast.StringLiteral {
callback := GetCallbackForFindingPropertyAssignmentByValue(elementValue)
return ForEachTsConfigPropArray(tsConfigSourceFile, propKey, func(property *ast.PropertyAssignment) *ast.StringLiteral {
Expand Down Expand Up @@ -1661,23 +1653,19 @@ func getFileNamesFromConfigSpecs(
literalFileMap.Set(keyMappper(fileName), file)
}

var jsonOnlyIncludeRegexes []*regexp2.Regexp
var jsonOnlyIncludeMatchers vfsmatch.SpecMatchers
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, vfsmatch.UnlimitedDepth)
for _, file := range files {
if tspath.FileExtensionIs(file, tspath.ExtensionJson) {
if jsonOnlyIncludeRegexes == nil {
if jsonOnlyIncludeMatchers == 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
}
jsonOnlyIncludeMatchers = vfsmatch.NewSpecMatchers(includes, basePath, vfsmatch.UsageFiles, host.UseCaseSensitiveFileNames())
}
var includeIndex int = -1
if jsonOnlyIncludeMatchers != nil {
includeIndex = jsonOnlyIncludeMatchers.MatchIndex(file)
}
includeIndex := core.FindIndex(jsonOnlyIncludeRegexes, func(re *regexp2.Regexp) bool { return core.Must(re.MatchString(file)) })
if includeIndex != -1 {
key := keyMappper(file)
if !literalFileMap.Has(key) && !wildCardJsonFileMap.Has(key) {
Expand Down
57 changes: 23 additions & 34 deletions internal/tsoptions/wildcarddirectories.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ package tsoptions
import (
"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 @@ -26,15 +25,7 @@ func getWildcardDirectories(include []string, exclude []string, comparePathsOpti
return nil
}

rawExcludeRegex := vfs.GetRegularExpressionForWildcard(exclude, comparePathsOptions.CurrentDirectory, "exclude")
var excludeRegex *regexp2.Regexp
if rawExcludeRegex != "" {
flags := regexp2.ECMAScript
if !comparePathsOptions.UseCaseSensitiveFileNames {
flags |= regexp2.IgnoreCase
}
excludeRegex = regexp2.MustCompile(rawExcludeRegex, regexp2.RegexOptions(flags))
}
excludeMatcher := vfsmatch.NewSpecMatcher(exclude, comparePathsOptions.CurrentDirectory, vfsmatch.UsageExclude, comparePathsOptions.UseCaseSensitiveFileNames)

wildcardDirectories := make(map[string]bool)
wildCardKeyToPath := make(map[string]string)
Expand All @@ -43,10 +34,8 @@ func getWildcardDirectories(include []string, exclude []string, comparePathsOpti

for _, file := range include {
spec := tspath.NormalizeSlashes(tspath.CombinePaths(comparePathsOptions.CurrentDirectory, file))
if excludeRegex != nil {
if matched, _ := excludeRegex.MatchString(spec); matched {
continue
}
if excludeMatcher != nil && excludeMatcher.MatchString(spec) {
continue
}

match := getWildcardDirectoryFromSpec(spec, comparePathsOptions.UseCaseSensitiveFileNames)
Expand Down Expand Up @@ -100,9 +89,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 @@ -111,27 +97,30 @@ 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 a wildcard character
firstWildcard := strings.IndexAny(spec, "*?")
if firstWildcard != -1 {
// Find the last directory separator before the wildcard
lastSepBeforeWildcard := strings.LastIndexByte(spec[:firstWildcard], tspath.DirectorySeparator)
if lastSepBeforeWildcard != -1 {
path := spec[:lastSepBeforeWildcard]
lastDirectorySeparatorIndex := strings.LastIndexByte(spec, tspath.DirectorySeparator)

// Determine if this should be watched recursively:
// recursive if the wildcard appears in a directory segment (not just the final file segment)
recursive := firstWildcard < 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
Loading