718 lines
22 KiB
Go
718 lines
22 KiB
Go
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}
|
|
}
|