vendor tsgo
This commit is contained in:
717
tools/tsgo/internal/vfs/vfsmatch/vfsmatch.go
Normal file
717
tools/tsgo/internal/vfs/vfsmatch/vfsmatch.go
Normal file
@@ -0,0 +1,717 @@
|
||||
package vfsmatch
|
||||
|
||||
import (
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
)
|
||||
|
||||
//go:generate go tool golang.org/x/tools/cmd/stringer -type=Usage -trimprefix=Usage -output=stringer_generated.go
|
||||
//go:generate npx dprint fmt stringer_generated.go
|
||||
|
||||
// This file implements the glob matching algorithm specified in MATCHING_ALGORITHM.md.
|
||||
|
||||
type Usage int8
|
||||
|
||||
const (
|
||||
UsageFiles Usage = iota
|
||||
UsageDirectories
|
||||
UsageExclude
|
||||
)
|
||||
|
||||
// UnlimitedDepth can be passed as the depth argument to indicate there is no depth limit.
|
||||
const UnlimitedDepth = math.MaxInt
|
||||
|
||||
func ReadDirectory(host vfs.FS, currentDir string, path string, extensions []string, excludes []string, includes []string, depth int) []string {
|
||||
return matchFiles(path, extensions, excludes, includes, host.UseCaseSensitiveFileNames(), currentDir, depth, host)
|
||||
}
|
||||
|
||||
// IsImplicitGlob checks if a path component is implicitly a glob.
|
||||
// An "includes" path "foo" is implicitly a glob "foo/** /*" (without the space) if its last component has no extension,
|
||||
// and does not contain any glob characters itself.
|
||||
func IsImplicitGlob(lastPathComponent string) bool {
|
||||
return !strings.ContainsAny(lastPathComponent, ".*?")
|
||||
}
|
||||
|
||||
var wildcardCharCodes = []rune{'*', '?'}
|
||||
|
||||
func getIncludeBasePath(absolute string) string {
|
||||
wildcardOffset := strings.IndexAny(absolute, string(wildcardCharCodes))
|
||||
if wildcardOffset < 0 {
|
||||
// No "*" or "?" in the path
|
||||
if !tspath.HasExtension(absolute) {
|
||||
return absolute
|
||||
} else {
|
||||
return tspath.RemoveTrailingDirectorySeparator(tspath.GetDirectoryPath(absolute))
|
||||
}
|
||||
}
|
||||
return absolute[:max(strings.LastIndex(absolute[:wildcardOffset], string(tspath.DirectorySeparator)), 0)]
|
||||
}
|
||||
|
||||
// getBasePaths computes the unique non-wildcard base paths amongst the provided include patterns.
|
||||
func getBasePaths(path string, includes []string, useCaseSensitiveFileNames bool) []string {
|
||||
// Storage for our results in the form of literal paths (e.g. the paths as written by the user).
|
||||
basePaths := []string{path}
|
||||
|
||||
if len(includes) > 0 {
|
||||
comparePathsOptions := tspath.ComparePathsOptions{CurrentDirectory: path, UseCaseSensitiveFileNames: useCaseSensitiveFileNames}
|
||||
stringComparer := comparePathsOptions.GetComparer()
|
||||
|
||||
// Storage for literal base paths amongst the include patterns.
|
||||
includeBasePaths := []string{}
|
||||
for _, include := range includes {
|
||||
// We also need to check the relative paths by converting them to absolute and normalizing
|
||||
// in case they escape the base path (e.g "..\somedirectory")
|
||||
var absolute string
|
||||
if tspath.IsRootedDiskPath(include) {
|
||||
absolute = include
|
||||
} else {
|
||||
absolute = tspath.NormalizePath(tspath.CombinePaths(path, include))
|
||||
}
|
||||
// Append the literal and canonical candidate base paths.
|
||||
includeBasePaths = append(includeBasePaths, getIncludeBasePath(absolute))
|
||||
}
|
||||
|
||||
// Sort the offsets array using either the literal or canonical path representations.
|
||||
slices.SortStableFunc(includeBasePaths, stringComparer)
|
||||
|
||||
// Iterate over each include base path and include unique base paths that are not a
|
||||
// subpath of an existing base path
|
||||
for _, includeBasePath := range includeBasePaths {
|
||||
if core.Every(basePaths, func(basepath string) bool {
|
||||
return !tspath.ContainsPath(basepath, includeBasePath, comparePathsOptions)
|
||||
}) {
|
||||
basePaths = append(basePaths, includeBasePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return basePaths
|
||||
}
|
||||
|
||||
// globPattern is a compiled glob pattern for matching file paths without regex.
|
||||
type globPattern struct {
|
||||
components []component // path segments to match (e.g., ["src", "**", "*.ts"])
|
||||
isExclude bool // exclude patterns have different matching rules
|
||||
caseSensitive bool
|
||||
excludeMinJs bool // for "files" patterns, exclude .min.js by default
|
||||
}
|
||||
|
||||
// component is a single path segment in a glob pattern.
|
||||
// Examples: "src" (literal), "*" (wildcard), "*.ts" (wildcard), "**" (recursive)
|
||||
type component struct {
|
||||
kind componentKind
|
||||
literal string // for kindLiteral: the exact string to match
|
||||
segments []segment // for kindWildcard: parsed wildcard pattern
|
||||
// Include patterns with wildcards skip common package folders (node_modules, etc.)
|
||||
skipPackageFolders bool
|
||||
}
|
||||
|
||||
type componentKind int
|
||||
|
||||
const (
|
||||
kindLiteral componentKind = iota // exact match (e.g., "src")
|
||||
kindWildcard // contains * or ? (e.g., "*.ts")
|
||||
kindDoubleAsterisk // ** matches zero or more directories
|
||||
)
|
||||
|
||||
// segment is a piece of a wildcard component.
|
||||
// Example: "*.ts" becomes [segStar, segLiteral(".ts")]
|
||||
type segment struct {
|
||||
kind segmentKind
|
||||
literal string // only for segLiteral
|
||||
}
|
||||
|
||||
type segmentKind int
|
||||
|
||||
const (
|
||||
segLiteral segmentKind = iota // exact text
|
||||
segStar // * matches any chars except /
|
||||
segQuestion // ? matches single char except /
|
||||
)
|
||||
|
||||
// compileGlobPattern compiles a glob spec (e.g., "src/**/*.ts") into a pattern.
|
||||
// Returns (pattern, false) if the pattern would match nothing.
|
||||
func compileGlobPattern(spec string, basePath string, usage Usage, caseSensitive bool) (globPattern, bool) {
|
||||
parts := tspath.GetNormalizedPathComponents(spec, basePath)
|
||||
|
||||
// "src/**" without a filename matches nothing (for include patterns)
|
||||
if usage != UsageExclude && core.LastOrNil(parts) == "**" {
|
||||
return globPattern{}, false
|
||||
}
|
||||
|
||||
// Normalize root: "/home/" -> "/home"
|
||||
parts[0] = tspath.RemoveTrailingDirectorySeparator(parts[0])
|
||||
|
||||
// Directories implicitly match all files: "src" -> "src/**/*"
|
||||
if IsImplicitGlob(core.LastOrNil(parts)) {
|
||||
parts = append(parts, "**", "*")
|
||||
}
|
||||
|
||||
p := globPattern{
|
||||
isExclude: usage == UsageExclude,
|
||||
caseSensitive: caseSensitive,
|
||||
excludeMinJs: usage == UsageFiles,
|
||||
// Avoid slice growth during compilation.
|
||||
components: make([]component, 0, len(parts)),
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
p.components = append(p.components, parseComponent(part, usage != UsageExclude))
|
||||
}
|
||||
return p, true
|
||||
}
|
||||
|
||||
// parseComponent converts a path segment string into a component.
|
||||
func parseComponent(s string, isInclude bool) component {
|
||||
if s == "**" {
|
||||
return component{kind: kindDoubleAsterisk}
|
||||
}
|
||||
if !strings.ContainsAny(s, "*?") {
|
||||
return component{kind: kindLiteral, literal: s}
|
||||
}
|
||||
return component{
|
||||
kind: kindWildcard,
|
||||
segments: parseSegments(s),
|
||||
skipPackageFolders: isInclude,
|
||||
}
|
||||
}
|
||||
|
||||
// parseSegments breaks "*.ts" into [segStar, segLiteral(".ts")]
|
||||
func parseSegments(s string) []segment {
|
||||
// Preallocate based on wildcard count: each wildcard contributes 1 segment,
|
||||
// and each wildcard can split literals into at most one extra literal segment.
|
||||
wildcards := 0
|
||||
for i := range len(s) {
|
||||
if s[i] == '*' || s[i] == '?' {
|
||||
wildcards++
|
||||
}
|
||||
}
|
||||
result := make([]segment, 0, 2*wildcards+1)
|
||||
start := 0
|
||||
for i := range len(s) {
|
||||
switch s[i] {
|
||||
case '*', '?':
|
||||
if i > start {
|
||||
result = append(result, segment{kind: segLiteral, literal: s[start:i]})
|
||||
}
|
||||
if s[i] == '*' {
|
||||
result = append(result, segment{kind: segStar})
|
||||
} else {
|
||||
result = append(result, segment{kind: segQuestion})
|
||||
}
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if start < len(s) {
|
||||
result = append(result, segment{kind: segLiteral, literal: s[start:]})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// matches returns true if path matches this pattern.
|
||||
func (p *globPattern) matches(path string) bool {
|
||||
return p.matchPathParts(path, "", 0, 0, false)
|
||||
}
|
||||
|
||||
// matchesParts returns true if prefix+suffix matches this pattern.
|
||||
// This avoids allocating a combined string for common call sites where prefix ends with '/'.
|
||||
func (p *globPattern) matchesParts(prefix, suffix string) bool {
|
||||
return p.matchPathParts(prefix, suffix, 0, 0, false)
|
||||
}
|
||||
|
||||
// matchesPrefixParts returns true if files under prefix+suffix could match.
|
||||
func (p *globPattern) matchesPrefixParts(prefix, suffix string) bool {
|
||||
return p.matchPathParts(prefix, suffix, 0, 0, true)
|
||||
}
|
||||
|
||||
// matchPathParts is like matchPath, but operates on a virtual path formed by prefix+suffix.
|
||||
// Offsets are in the combined string.
|
||||
func (p *globPattern) matchPathParts(prefix, suffix string, pathOffset, compIdx int, prefixOnly bool) bool {
|
||||
for {
|
||||
pathPart, nextOffset, ok := nextPathPartParts(prefix, suffix, pathOffset)
|
||||
if !ok {
|
||||
if prefixOnly {
|
||||
return true
|
||||
}
|
||||
return p.patternSatisfied(compIdx)
|
||||
}
|
||||
|
||||
if compIdx >= len(p.components) {
|
||||
return p.isExclude && !prefixOnly
|
||||
}
|
||||
|
||||
comp := p.components[compIdx]
|
||||
switch comp.kind {
|
||||
case kindDoubleAsterisk:
|
||||
if p.matchPathParts(prefix, suffix, pathOffset, compIdx+1, prefixOnly) {
|
||||
return true
|
||||
}
|
||||
if !p.isExclude && (isHiddenPath(pathPart) || isPackageFolder(pathPart)) {
|
||||
return false
|
||||
}
|
||||
pathOffset = nextOffset
|
||||
continue
|
||||
case kindLiteral:
|
||||
if comp.skipPackageFolders && isPackageFolder(pathPart) {
|
||||
panic("unreachable: literal components never have skipPackageFolders")
|
||||
}
|
||||
if !p.stringsEqual(comp.literal, pathPart) {
|
||||
return false
|
||||
}
|
||||
case kindWildcard:
|
||||
if comp.skipPackageFolders && isPackageFolder(pathPart) {
|
||||
return false
|
||||
}
|
||||
if !p.matchWildcard(comp.segments, pathPart) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
pathOffset = nextOffset
|
||||
compIdx++
|
||||
}
|
||||
}
|
||||
|
||||
// patternSatisfied checks if remaining pattern components can match empty input.
|
||||
func (p *globPattern) patternSatisfied(compIdx int) bool {
|
||||
// A pattern is satisfied when remaining components can match empty input.
|
||||
// For both include and exclude patterns, only trailing "**" components may match nothing.
|
||||
for _, c := range p.components[compIdx:] {
|
||||
if c.kind != kindDoubleAsterisk {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// nextPathPart extracts the next path component from path starting at offset.
|
||||
func nextPathPartSingle(s string, offset int) (part string, nextOffset int, ok bool) {
|
||||
if offset >= len(s) {
|
||||
return "", offset, false
|
||||
}
|
||||
if offset == 0 && len(s) > 0 && s[0] == '/' {
|
||||
return "", 1, true
|
||||
}
|
||||
for offset < len(s) && s[offset] == '/' {
|
||||
offset++
|
||||
}
|
||||
if offset >= len(s) {
|
||||
return "", offset, false
|
||||
}
|
||||
rest := s[offset:]
|
||||
if idx := strings.IndexByte(rest, '/'); idx >= 0 {
|
||||
return rest[:idx], offset + idx, true
|
||||
}
|
||||
return rest, len(s), true
|
||||
}
|
||||
|
||||
func nextPathPartParts(prefix, suffix string, offset int) (part string, nextOffset int, ok bool) {
|
||||
// Fast paths: keep the hot single-string scan tight.
|
||||
if len(suffix) == 0 {
|
||||
return nextPathPartSingle(prefix, offset)
|
||||
}
|
||||
if len(prefix) == 0 {
|
||||
return nextPathPartSingle(suffix, offset)
|
||||
}
|
||||
|
||||
// For matchFilesNoRegex call sites, prefix is a directory path ending in '/',
|
||||
// and suffix is a single entry name (no '/'). That makes this significantly
|
||||
// simpler than a general-purpose "virtual concatenation" scanner.
|
||||
|
||||
totalLen := len(prefix) + len(suffix)
|
||||
if offset >= totalLen {
|
||||
return "", offset, false
|
||||
}
|
||||
|
||||
// Handle leading slash (root of absolute path)
|
||||
if offset == 0 && prefix[0] == '/' {
|
||||
return "", 1, true
|
||||
}
|
||||
|
||||
// Scan within prefix.
|
||||
if offset < len(prefix) {
|
||||
for offset < len(prefix) && prefix[offset] == '/' {
|
||||
offset++
|
||||
}
|
||||
if offset < len(prefix) {
|
||||
rest := prefix[offset:]
|
||||
idx := strings.IndexByte(rest, '/')
|
||||
// idx is guaranteed >= 0 for the call sites we care about because prefix ends in '/'.
|
||||
return rest[:idx], offset + idx, true
|
||||
}
|
||||
// Fall through into suffix region.
|
||||
}
|
||||
|
||||
// Scan suffix: it's a single component.
|
||||
sOff := offset - len(prefix)
|
||||
if sOff >= len(suffix) {
|
||||
return "", offset, false
|
||||
}
|
||||
return suffix[sOff:], totalLen, true
|
||||
}
|
||||
|
||||
// matchWildcard matches a path component against wildcard segments.
|
||||
func (p *globPattern) matchWildcard(segs []segment, s string) bool {
|
||||
// Include patterns: wildcards at start cannot match hidden files
|
||||
if !p.isExclude && len(segs) > 0 && isHiddenPath(s) && (segs[0].kind == segStar || segs[0].kind == segQuestion) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Fast path: single * followed by literal suffix (e.g., "*.ts")
|
||||
if len(segs) == 2 && segs[0].kind == segStar && segs[1].kind == segLiteral {
|
||||
suffix := segs[1].literal
|
||||
if len(s) < len(suffix) || !p.stringsEqual(suffix, s[len(s)-len(suffix):]) {
|
||||
return false
|
||||
}
|
||||
return p.shouldIncludeMinJs(s, segs)
|
||||
}
|
||||
|
||||
return p.matchSegments(segs, s) && p.shouldIncludeMinJs(s, segs)
|
||||
}
|
||||
|
||||
// matchSegments matches segments against string s using an iterative algorithm.
|
||||
// This avoids exponential backtracking by tracking only the last star position.
|
||||
// The algorithm is O(n*m) where n is the string length and m is pattern length.
|
||||
func (p *globPattern) matchSegments(segs []segment, s string) bool {
|
||||
segIdx, sIdx := 0, 0
|
||||
starSegIdx, starSIdx := -1, 0
|
||||
|
||||
for sIdx < len(s) {
|
||||
if segIdx < len(segs) {
|
||||
seg := segs[segIdx]
|
||||
switch seg.kind {
|
||||
case segLiteral:
|
||||
end := sIdx + len(seg.literal)
|
||||
if end <= len(s) && p.stringsEqual(seg.literal, s[sIdx:end]) {
|
||||
sIdx = end
|
||||
segIdx++
|
||||
continue
|
||||
}
|
||||
case segQuestion:
|
||||
if s[sIdx] != '/' {
|
||||
_, size := utf8.DecodeRuneInString(s[sIdx:])
|
||||
sIdx += size
|
||||
segIdx++
|
||||
continue
|
||||
}
|
||||
case segStar:
|
||||
// Record star position for backtracking, then try matching zero chars.
|
||||
starSegIdx = segIdx
|
||||
starSIdx = sIdx
|
||||
segIdx++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Current segment didn't match. Backtrack to last star if possible.
|
||||
if starSegIdx >= 0 && starSIdx < len(s) && s[starSIdx] != '/' {
|
||||
// Star consumes one more character (rune), retry from segment after star.
|
||||
_, size := utf8.DecodeRuneInString(s[starSIdx:])
|
||||
starSIdx += size
|
||||
sIdx = starSIdx
|
||||
segIdx = starSegIdx + 1
|
||||
continue
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Consume any trailing stars.
|
||||
for segIdx < len(segs) && segs[segIdx].kind == segStar {
|
||||
segIdx++
|
||||
}
|
||||
return segIdx >= len(segs)
|
||||
}
|
||||
|
||||
func (p *globPattern) shouldIncludeMinJs(filename string, segs []segment) bool {
|
||||
if !p.excludeMinJs {
|
||||
return true
|
||||
}
|
||||
|
||||
// Preserve legacy behavior:
|
||||
// - When matching is case-sensitive, only the exact ".min.js" suffix is excluded by default.
|
||||
// - When matching is case-insensitive, any casing variant is excluded by default.
|
||||
if !p.hasMinJsSuffix(filename) {
|
||||
return true
|
||||
}
|
||||
// Allow when the user's pattern explicitly references the .min. suffix.
|
||||
if p.patternMentionsMinSuffix(segs) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *globPattern) hasMinJsSuffix(filename string) bool {
|
||||
if p.caseSensitive {
|
||||
return strings.HasSuffix(filename, ".min.js")
|
||||
}
|
||||
const minJs = ".min.js"
|
||||
if len(filename) < len(minJs) {
|
||||
return false
|
||||
}
|
||||
// Avoid allocating via strings.ToLower; compare suffix case-insensitively.
|
||||
return strings.EqualFold(filename[len(filename)-len(minJs):], minJs)
|
||||
}
|
||||
|
||||
func (p *globPattern) patternMentionsMinSuffix(segs []segment) bool {
|
||||
for _, seg := range segs {
|
||||
if seg.kind != segLiteral {
|
||||
continue
|
||||
}
|
||||
lit := seg.literal
|
||||
if !p.caseSensitive {
|
||||
lit = strings.ToLower(lit)
|
||||
}
|
||||
if strings.Contains(lit, ".min.js") || strings.Contains(lit, ".min.") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// stringsEqual compares strings with appropriate case sensitivity.
|
||||
func (p *globPattern) stringsEqual(a, b string) bool {
|
||||
if p.caseSensitive {
|
||||
return a == b
|
||||
}
|
||||
return strings.EqualFold(a, b)
|
||||
}
|
||||
|
||||
// isHiddenPath checks if a path component is hidden (starts with dot).
|
||||
func isHiddenPath(name string) bool {
|
||||
return len(name) > 0 && name[0] == '.'
|
||||
}
|
||||
|
||||
// isPackageFolder checks if name is a common package folder (node_modules, etc.)
|
||||
func isPackageFolder(name string) bool {
|
||||
switch len(name) {
|
||||
case len("node_modules"):
|
||||
return strings.EqualFold(name, "node_modules")
|
||||
case len("jspm_packages"):
|
||||
return strings.EqualFold(name, "jspm_packages")
|
||||
case len("bower_components"):
|
||||
return strings.EqualFold(name, "bower_components")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ensureTrailingSlash(s string) string {
|
||||
if len(s) > 0 && s[len(s)-1] != '/' {
|
||||
return s + "/"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// globMatcher combines include and exclude patterns for file matching.
|
||||
type globMatcher struct {
|
||||
includes []globPattern
|
||||
excludes []globPattern
|
||||
hadIncludes bool // true if include specs were provided (even if none compiled)
|
||||
}
|
||||
|
||||
func newGlobMatcher(includeSpecs, excludeSpecs []string, basePath string, caseSensitive bool, usage Usage) *globMatcher {
|
||||
m := &globMatcher{
|
||||
hadIncludes: len(includeSpecs) > 0,
|
||||
includes: make([]globPattern, 0, len(includeSpecs)),
|
||||
excludes: make([]globPattern, 0, len(excludeSpecs)),
|
||||
}
|
||||
|
||||
for _, spec := range includeSpecs {
|
||||
if p, ok := compileGlobPattern(spec, basePath, usage, caseSensitive); ok {
|
||||
m.includes = append(m.includes, p)
|
||||
}
|
||||
}
|
||||
for _, spec := range excludeSpecs {
|
||||
if p, ok := compileGlobPattern(spec, basePath, UsageExclude, caseSensitive); ok {
|
||||
m.excludes = append(m.excludes, p)
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// matchesFileParts checks if prefix+suffix matches against the glob patterns.
|
||||
// Returns the index of the matching include pattern and true if matched, or (0, false) if not.
|
||||
func (m *globMatcher) matchesFileParts(prefix, suffix string) (int, bool) {
|
||||
for i := range m.excludes {
|
||||
if m.excludes[i].matchesParts(prefix, suffix) {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
if len(m.includes) == 0 {
|
||||
if m.hadIncludes {
|
||||
return 0, false
|
||||
}
|
||||
return 0, true
|
||||
}
|
||||
for i := range m.includes {
|
||||
if m.includes[i].matchesParts(prefix, suffix) {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// matchesDirectoryParts checks if files under the directory prefix+suffix could match any pattern.
|
||||
func (m *globMatcher) matchesDirectoryParts(prefix, suffix string) bool {
|
||||
for i := range m.excludes {
|
||||
if m.excludes[i].matchesParts(prefix, suffix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if len(m.includes) == 0 {
|
||||
return !m.hadIncludes
|
||||
}
|
||||
for i := range m.includes {
|
||||
if m.includes[i].matchesPrefixParts(prefix, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// globVisitor traverses directories matching files against glob patterns.
|
||||
type globVisitor struct {
|
||||
host vfs.FS
|
||||
fileMatcher *globMatcher
|
||||
directoryMatcher *globMatcher
|
||||
extensions []string
|
||||
useCaseSensitiveFileNames bool
|
||||
visited collections.Set[string]
|
||||
results [][]string
|
||||
}
|
||||
|
||||
// visit walks a directory tree, collecting files that match the glob patterns.
|
||||
// resolvedRealPath, when non-empty, is the already-resolved real path for this
|
||||
// directory (computed incrementally from the parent). When empty, Realpath is
|
||||
// called to resolve symlinks.
|
||||
func (v *globVisitor) visit(path, absolutePath string, depth int, resolvedRealPath string) {
|
||||
// Detect symlink cycles
|
||||
var realPath string
|
||||
if resolvedRealPath != "" {
|
||||
realPath = resolvedRealPath
|
||||
} else {
|
||||
realPath = v.host.Realpath(absolutePath)
|
||||
}
|
||||
canonicalPath := tspath.GetCanonicalFileName(realPath, v.useCaseSensitiveFileNames)
|
||||
if v.visited.Has(canonicalPath) {
|
||||
return
|
||||
}
|
||||
v.visited.Add(canonicalPath)
|
||||
|
||||
entries := v.host.GetAccessibleEntries(absolutePath)
|
||||
|
||||
pathPrefix := ensureTrailingSlash(path)
|
||||
absPrefix := ensureTrailingSlash(absolutePath)
|
||||
|
||||
for _, file := range entries.Files {
|
||||
if len(v.extensions) > 0 && !tspath.FileExtensionIsOneOf(file, v.extensions) {
|
||||
continue
|
||||
}
|
||||
if idx, ok := v.fileMatcher.matchesFileParts(absPrefix, file); ok {
|
||||
v.results[idx] = append(v.results[idx], pathPrefix+file)
|
||||
}
|
||||
}
|
||||
|
||||
if depth != UnlimitedDepth {
|
||||
depth--
|
||||
if depth == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, dir := range entries.Directories {
|
||||
if !v.directoryMatcher.matchesDirectoryParts(absPrefix, dir) {
|
||||
continue
|
||||
}
|
||||
absDir := absPrefix + dir
|
||||
var childRealPath string
|
||||
if entries.Symlinks != nil {
|
||||
if _, isSymlink := entries.Symlinks[dir]; !isSymlink {
|
||||
// Non-symlink directory: compute realpath incrementally.
|
||||
childRealPath = tspath.CombinePaths(realPath, dir)
|
||||
}
|
||||
// else: symlink directory; leave childRealPath empty to force Realpath call.
|
||||
}
|
||||
// If Symlinks is nil, the FS doesn't track symlinks;
|
||||
// leave childRealPath empty to call Realpath (preserving old behavior).
|
||||
v.visit(pathPrefix+dir, absDir, depth, childRealPath)
|
||||
}
|
||||
}
|
||||
|
||||
func matchFiles(path string, extensions, excludes, includes []string, useCaseSensitiveFileNames bool, currentDirectory string, depth int, host vfs.FS) []string {
|
||||
path = tspath.NormalizePath(path)
|
||||
currentDirectory = tspath.NormalizePath(currentDirectory)
|
||||
absolutePath := tspath.CombinePaths(currentDirectory, path)
|
||||
|
||||
fileMatcher := newGlobMatcher(includes, excludes, absolutePath, useCaseSensitiveFileNames, UsageFiles)
|
||||
directoryMatcher := newGlobMatcher(includes, excludes, absolutePath, useCaseSensitiveFileNames, UsageDirectories)
|
||||
|
||||
v := globVisitor{
|
||||
host: host,
|
||||
fileMatcher: fileMatcher,
|
||||
directoryMatcher: directoryMatcher,
|
||||
extensions: extensions,
|
||||
useCaseSensitiveFileNames: useCaseSensitiveFileNames,
|
||||
results: make([][]string, max(len(fileMatcher.includes), 1)),
|
||||
}
|
||||
|
||||
for _, basePath := range getBasePaths(path, includes, useCaseSensitiveFileNames) {
|
||||
v.visit(basePath, tspath.CombinePaths(currentDirectory, basePath), depth, "")
|
||||
}
|
||||
|
||||
// Fast path: a single include bucket (or no includes) doesn't need flattening.
|
||||
if len(v.results) == 1 {
|
||||
return v.results[0]
|
||||
}
|
||||
return core.Flatten(v.results)
|
||||
}
|
||||
|
||||
// SpecMatcher wraps multiple glob patterns for matching paths.
|
||||
type SpecMatcher struct {
|
||||
patterns []globPattern
|
||||
}
|
||||
|
||||
// MatchString returns true if any pattern matches the path.
|
||||
func (m *SpecMatcher) MatchString(path string) bool {
|
||||
for i := range m.patterns {
|
||||
if m.patterns[i].matches(path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MatchIndex returns the index of the first matching pattern, or -1.
|
||||
func (m *SpecMatcher) MatchIndex(path string) int {
|
||||
for i := range m.patterns {
|
||||
if m.patterns[i].matches(path) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// NewSpecMatcher creates a matcher for one or more glob specs.
|
||||
// It returns a matcher that can test if paths match any of the patterns.
|
||||
func NewSpecMatcher(specs []string, basePath string, usage Usage, useCaseSensitiveFileNames bool) *SpecMatcher {
|
||||
if len(specs) == 0 {
|
||||
return nil
|
||||
}
|
||||
patterns := make([]globPattern, 0, len(specs))
|
||||
for _, spec := range specs {
|
||||
if p, ok := compileGlobPattern(spec, basePath, usage, useCaseSensitiveFileNames); ok {
|
||||
patterns = append(patterns, p)
|
||||
}
|
||||
}
|
||||
if len(patterns) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &SpecMatcher{patterns: patterns}
|
||||
}
|
||||
Reference in New Issue
Block a user