vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View File

@@ -0,0 +1,425 @@
# Glob Matching Algorithm Specification
This document is a formal algorithmic specification of the file-path glob matching
logic. An implementation conforming to this specification must produce identical
results for all inputs. All subroutine errors are propagated as errors of the
calling routine unless stated otherwise.
---
## 1. Definitions
**Path** — A normalized, `/`-separated absolute file path (e.g., `/project/src/index.ts`).
**Path component** — A single segment between `/` delimiters (e.g., `src`, `index.ts`). The leading `/` produces the root component, which is the empty string `""`.
**Spec** — A user-provided glob string (e.g., `src/**/*.ts`), before compilation.
**Base path** — The absolute directory against which relative specs are resolved.
**Usage mode** — One of three modes that alter matching semantics:
- **Files** — Matches complete file paths.
- **Directories** — Matches directory prefixes for traversal pruning.
- **Exclude** — Matches paths to be excluded.
**Component kind** — One of:
- **Literal** — Contains no `*` or `?` characters.
- **Wildcard** — Contains at least one `*` or `?` character.
- **DoubleAsterisk** — The exact string `**`.
**Character** — A single Unicode scalar value (codepoint). See Section 9 for the precise character-boundary requirements that apply during segment matching.
**Segment kind** — One of:
- **SegLiteral** — An exact literal substring.
- **SegStar** — Matches zero or more characters excluding `/`.
- **SegQuestion** — Matches exactly one character excluding `/`.
**Pattern** — A compiled spec consisting of a component list, a usage mode, and a case-sensitivity flag.
---
## 2. Helper Predicates
**IS_HIDDEN_PATH**(_component_)
> 1. If the length of _component_ is 0, return **false**.
> 2. If the first character of _component_ is `"."`, return **true**.
> 3. Return **false**.
**IS_PACKAGE_FOLDER**(_component_)
> 1. If _component_ equals `"node_modules"` (case-insensitive), return **true**.
> 2. If _component_ equals `"bower_components"` (case-insensitive), return **true**.
> 3. If _component_ equals `"jspm_packages"` (case-insensitive), return **true**.
> 4. Return **false**.
**ENSURE_TRAILING_SLASH**(_s_)
> 1. If the length of _s_ is 0, return _s_.
> 2. If the last character of _s_ is `"/"`, return _s_.
> 3. Return _s_ concatenated with `"/"`.
**STRINGS_EQUAL**(_a_, _b_, _caseSensitive_)
> 1. If _caseSensitive_ is **true**, return whether _a_ and _b_ are byte-for-byte identical.
> 2. Return whether _a_ and _b_ are equal under Unicode case folding.
**IS_IMPLICIT_GLOB**(_component_)
> 1. If _component_ contains any of the characters `"."`, `"*"`, or `"?"`, return **false**.
> 2. Return **true**.
---
## 3. Spec Normalization
**NORMALIZE_SPEC**(_spec_, _basePath_)
> 1. Let _components_ be the result of resolving _spec_ against _basePath_ into an ordered list of normalized path components. The first element is the absolute root prefix (e.g., `"/home"`). The resolution uses `/` as the path separator, resolves `.` and `..` segments, and collapses consecutive separators.
> 2. If the last character of _components_\[0\] is `"/"`, remove it.
> 3. Return _components_.
---
## 4. Segment Parsing
**PARSE_SEGMENTS**(_string_)
> 1. Let _segments_ be an empty list.
> 2. Let _start_ be 0.
> 3. For each index _i_ from 0 to the length of _string_ 1:
> 1. If _string_\[_i_\] is `"*"` or `"?"`, then:
> 1. If _i_ > _start_, append a **SegLiteral** segment with value _string_\[_start_.._i_\] to _segments_.
> 2. If _string_\[_i_\] is `"*"`, append a **SegStar** segment to _segments_.
> 3. Otherwise, append a **SegQuestion** segment to _segments_.
> 4. Set _start_ to _i_ + 1.
> 4. If _start_ < length of _string_, append a **SegLiteral** segment with value _string_\[_start_..\] to _segments_.
> 5. Return _segments_.
---
## 5. Pattern Compilation
**COMPILE_PATTERN**(_spec_, _basePath_, _usage_, _caseSensitive_)
> 1. Let _components_ be the result of NORMALIZE_SPEC(_spec_, _basePath_).
> 2. If the last element of _components_ is `"**"` and _usage_ is not **Exclude**, return **failure**. (The pattern compiles to nothing.)
> 3. If IS_IMPLICIT_GLOB(last element of _components_) is **true** (note: this check is applied to the _normalized_ component, not the raw spec string), then:
> 1. Append `"**"` to _components_.
> 2. Append `"*"` to _components_.
> 4. Let _compiledComponents_ be an empty list.
> 5. For each _part_ in _components_:
> 1. If _part_ is `"**"`, append a **DoubleAsterisk** component to _compiledComponents_.
> 2. Otherwise, if _part_ contains no `"*"` or `"?"` characters, append a **Literal** component with value _part_ to _compiledComponents_.
> 3. Otherwise, append a **Wildcard** component with segments PARSE_SEGMENTS(_part_) to _compiledComponents_.
> 6. Return a pattern with component list _compiledComponents_, usage mode _usage_, and case-sensitivity flag _caseSensitive_.
---
## 6. Path Component Extraction
**NEXT_PATH_COMPONENT**(_path_, _offset_)
> 1. If _offset_ ≥ length of _path_, return (**none**, _offset_, **false**).
> 2. If _offset_ is 0 and _path_\[0\] is `"/"`, return (`""`, 1, **true**).
> 3. While _offset_ < length of _path_ and _path_\[_offset_\] is `"/"`, increment _offset_.
> 4. If _offset_ ≥ length of _path_, return (**none**, _offset_, **false**).
> 5. Let _start_ be _offset_.
> 6. While _offset_ < length of _path_ and _path_\[_offset_\] is not `"/"`, increment _offset_.
> 7. Return (_path_\[_start_.._offset_\], _offset_, **true**).
---
## 7. Full-Path Matching
**MATCH_PATH**(_pattern_, _path_)
> 1. Return the result of MATCH_PATH_INNER(_pattern_, _path_, 0, 0, **false**).
**MATCH_PATH_PREFIX**(_pattern_, _path_)
> 1. Return the result of MATCH_PATH_INNER(_pattern_, _path_, 0, 0, **true**).
**MATCH_PATH_INNER**(_pattern_, _path_, _pathOffset_, _compIdx_, _prefixOnly_)
> 1. Let _components_ be the component list of _pattern_.
> 2. Let _usage_ be the usage mode of _pattern_.
> 3. Let _caseSensitive_ be the case-sensitivity flag of _pattern_.
> 4. Loop:
> 1. Let (_part_, _nextOffset_, _ok_) be the result of NEXT_PATH_COMPONENT(_path_, _pathOffset_).
> 2. If _ok_ is **false**, then:
> 1. If _prefixOnly_ is **true**, return **true**.
> 2. Return the result of PATTERN_SATISFIED(_components_, _compIdx_).
> 3. If _compIdx_ ≥ length of _components_, then:
> 1. If _usage_ is **Exclude** and _prefixOnly_ is **false**, return **true**.
> 2. Return **false**.
> 4. Let _comp_ be _components_\[_compIdx_\].
> 5. If the kind of _comp_ is **DoubleAsterisk**, then:
> 1. Let _skipResult_ be the result of MATCH_PATH_INNER(_pattern_, _path_, _pathOffset_, _compIdx_ + 1, _prefixOnly_).
> 2. If _skipResult_ is **true**, return **true**.
> 3. If _usage_ is not **Exclude**, then:
> 1. If IS_HIDDEN_PATH(_part_) is **true**, return **false**.
> 2. If IS_PACKAGE_FOLDER(_part_) is **true**, return **false**.
> 4. Set _pathOffset_ to _nextOffset_.
> 5. Continue the loop.
> 6. If the kind of _comp_ is **Literal**, then:
> 1. If STRINGS_EQUAL(_comp_.value, _part_, _caseSensitive_) is **false**, return **false**.
> 7. If the kind of _comp_ is **Wildcard**, then:
> 1. If _usage_ is not **Exclude** and IS_PACKAGE_FOLDER(_part_) is **true**, return **false**.
> 2. If the result of MATCH_WILDCARD(_pattern_, _comp_.segments, _part_) is **false**, return **false**.
> 8. Set _pathOffset_ to _nextOffset_.
> 9. Increment _compIdx_.
**PATTERN_SATISFIED**(_components_, _compIdx_)
> 1. For each index _i_ from _compIdx_ to length of _components_ 1:
> 1. If the kind of _components_\[_i_\] is not **DoubleAsterisk**, return **false**.
> 2. Return **true**.
---
## 8. Wildcard Component Matching
**MATCH_WILDCARD**(_pattern_, _segments_, _string_)
> 1. Let _usage_ be the usage mode of _pattern_.
> 2. Let _caseSensitive_ be the case-sensitivity flag of _pattern_.
> 3. If _usage_ is not **Exclude**, then:
> 1. If the length of _segments_ > 0, then:
> 1. Let _firstKind_ be the kind of _segments_\[0\].
> 2. If (_firstKind_ is **SegStar** or _firstKind_ is **SegQuestion**) and IS_HIDDEN_PATH(_string_) is **true**, return **false**.
> 4. Let _matched_ be the result of MATCH_SEGMENTS(_segments_, _string_, _caseSensitive_).
> 5. If _matched_ is **false**, return **false**.
> 6. Let _accepted_ be the result of SHOULD_ACCEPT_MIN_JS(_pattern_, _segments_, _string_).
> 7. Return _accepted_.
---
## 9. Segment Matching
In this section, all string positions refer to **character** (codepoint) boundaries.
Implementations must advance by full codepoints, not by encoding units (e.g., not
by individual bytes in UTF-8, nor by individual code units in UTF-16). "Increment
_sIdx_" means advance _sIdx_ past the next character (one codepoint). Likewise,
"length of _s_" is the number of characters, and _s_\[_sIdx_\] is the character
at position _sIdx_.
The original TypeScript implementation uses ECMAScript regexes without the `u` flag,
which operate on UTF-16 code units; a conforming implementation may match on
codepoints instead, as the difference is only observable for supplementary-plane
characters (U+10000 and above) in filenames.
**MATCH_SEGMENTS**(_segments_, _s_, _caseSensitive_)
> 1. Let _segIdx_ be 0.
> 2. Let _sIdx_ be 0.
> 3. Let _starSegIdx_ be 1.
> 4. Let _starSIdx_ be 0.
> 5. While _sIdx_ < length of _s_:
> 1. If _segIdx_ < length of _segments_, then:
> 1. Let _seg_ be _segments_\[_segIdx_\].
> 2. If the kind of _seg_ is **SegLiteral**, then:
> 1. Let _lit_ be the value of _seg_.
> 2. If _sIdx_ + length of _lit_ ≤ length of _s_ and STRINGS_EQUAL(_lit_, _s_\[_sIdx_.._sIdx_+len(_lit_)\], _caseSensitive_) is **true**, then:
> 1. Set _sIdx_ to _sIdx_ + length of _lit_.
> 2. Increment _segIdx_.
> 3. Continue the loop.
> 3. If the kind of _seg_ is **SegQuestion**, then:
> 1. If _s_\[_sIdx_\] is not `"/"`, then:
> 1. Increment _sIdx_.
> 2. Increment _segIdx_.
> 3. Continue the loop.
> 4. If the kind of _seg_ is **SegStar**, then:
> 1. Set _starSegIdx_ to _segIdx_.
> 2. Set _starSIdx_ to _sIdx_.
> 3. Increment _segIdx_.
> 4. Continue the loop.
> 2. If _starSegIdx_ ≥ 0 and _starSIdx_ < length of _s_ and _s_\[_starSIdx_\] is not `"/"`, then:
> 1. Increment _starSIdx_.
> 2. Set _sIdx_ to _starSIdx_.
> 3. Set _segIdx_ to _starSegIdx_ + 1.
> 4. Continue the loop.
> 3. Return **false**.
> 6. While _segIdx_ < length of _segments_ and the kind of _segments_\[_segIdx_\] is **SegStar**:
> 1. Increment _segIdx_.
> 7. Return _segIdx_ ≥ length of _segments_.
---
## 10. `.min.js` Default Exclusion
**SHOULD_ACCEPT_MIN_JS**(_pattern_, _segments_, _filename_)
> 1. Let _usage_ be the usage mode of _pattern_.
> 2. If _usage_ is not **Files**, return **true**.
> 3. If the result of HAS_MIN_JS_SUFFIX(_filename_, _pattern_.caseSensitive) is **false**, return **true**.
> 4. If the result of PATTERN_MENTIONS_MIN_SUFFIX(_segments_, _pattern_.caseSensitive) is **true**, return **true**.
> 5. Return **false**.
**HAS_MIN_JS_SUFFIX**(_filename_, _caseSensitive_)
> 1. Let _suffix_ be `".min.js"`.
> 2. If length of _filename_ < length of _suffix_, return **false**.
> 3. Let _tail_ be the last 7 characters of _filename_.
> 4. If _caseSensitive_ is **true**, return whether _tail_ equals `".min.js"`.
> 5. Return whether _tail_ equals `".min.js"` under Unicode case folding.
**PATTERN_MENTIONS_MIN_SUFFIX**(_segments_, _caseSensitive_)
> 1. For each _seg_ in _segments_:
> 1. If the kind of _seg_ is not **SegLiteral**, continue.
> 2. Let _lit_ be the value of _seg_.
> 3. If _caseSensitive_ is **false**, let _lit_ be the lowercase form of _lit_.
> 4. If _lit_ contains the substring `".min.js"` or `".min."`, return **true**.
> 2. Return **false**.
---
## 11. Composite Matchers
**MATCH_FILE**(_path_, _includePatterns_, _excludePatterns_, _hadIncludes_)
> 1. For each _pattern_ in _excludePatterns_:
> 1. If the result of MATCH_PATH(_pattern_, _path_) is **true**, return (0, **false**).
> 2. If length of _includePatterns_ is 0, then:
> 1. If _hadIncludes_ is **true**, return (0, **false**).
> 2. Return (0, **true**).
> 3. For each index _i_ from 0 to length of _includePatterns_ 1:
> 1. If the result of MATCH_PATH(_includePatterns_\[_i_\], _path_) is **true**, return (_i_, **true**).
> 4. Return (0, **false**).
**MATCH_DIRECTORY**(_path_, _includePatterns_, _excludePatterns_, _hadIncludes_)
> 1. For each _pattern_ in _excludePatterns_:
> 1. If the result of MATCH_PATH(_pattern_, _path_) is **true**, return **false**.
> 2. If length of _includePatterns_ is 0, then:
> 1. If _hadIncludes_ is **true**, return **false**.
> 2. Return **true**.
> 3. For each _pattern_ in _includePatterns_:
> 1. If the result of MATCH_PATH_PREFIX(_pattern_, _path_) is **true**, return **true**.
> 4. Return **false**.
**MATCH_SPEC**(_patterns_, _path_)
> 1. For each _pattern_ in _patterns_:
> 1. If the result of MATCH_PATH(_pattern_, _path_) is **true**, return **true**.
> 2. Return **false**.
**MATCH_SPEC_INDEX**(_patterns_, _path_)
> 1. For each index _i_ from 0 to length of _patterns_ 1:
> 1. If the result of MATCH_PATH(_patterns_\[_i_\], _path_) is **true**, return _i_.
> 2. Return 1.
---
## 12. Pattern Set Compilation
**COMPILE_PATTERNS**(_specs_, _basePath_, _usage_, _caseSensitive_)
> 1. Let _patterns_ be an empty list.
> 2. For each _spec_ in _specs_:
> 1. Let _result_ be the result of COMPILE_PATTERN(_spec_, _basePath_, _usage_, _caseSensitive_).
> 2. If _result_ is not **failure**, append _result_ to _patterns_.
> 3. Return _patterns_.
**COMPILE_FILE_MATCHER**(_includeSpecs_, _excludeSpecs_, _basePath_, _caseSensitive_)
> 1. Let _includePatterns_ be the result of COMPILE_PATTERNS(_includeSpecs_, _basePath_, **Files**, _caseSensitive_).
> 2. Let _excludePatterns_ be the result of COMPILE_PATTERNS(_excludeSpecs_, _basePath_, **Exclude**, _caseSensitive_).
> 3. Let _hadIncludes_ be whether length of _includeSpecs_ > 0.
> 4. Return (_includePatterns_, _excludePatterns_, _hadIncludes_).
**COMPILE_DIRECTORY_MATCHER**(_includeSpecs_, _excludeSpecs_, _basePath_, _caseSensitive_)
> 1. Let _includePatterns_ be the result of COMPILE_PATTERNS(_includeSpecs_, _basePath_, **Directories**, _caseSensitive_).
> 2. Let _excludePatterns_ be the result of COMPILE_PATTERNS(_excludeSpecs_, _basePath_, **Exclude**, _caseSensitive_).
> 3. Let _hadIncludes_ be whether length of _includeSpecs_ > 0.
> 4. Return (_includePatterns_, _excludePatterns_, _hadIncludes_).
---
## 13. Base Path Computation
**GET_BASE_PATHS**(_rootPath_, _includeSpecs_, _caseSensitive_)
> 1. Let _basePaths_ be a list containing _rootPath_.
> 2. If _includeSpecs_ is empty, return _basePaths_.
> 3. Let _includeBasePaths_ be an empty list.
> 4. For each _spec_ in _includeSpecs_:
> 1. Let _absolute_ be the result of resolving _spec_ to an absolute normalized path against _rootPath_.
> 2. Let _basePath_ be GET_INCLUDE_BASE_PATH(_absolute_).
> 3. Append _basePath_ to _includeBasePaths_.
> 5. Sort _includeBasePaths_ using a string comparator that is case-insensitive if _caseSensitive_ is **false**.
> 6. For each _candidate_ in _includeBasePaths_:
> 1. If no element of _basePaths_ is a path-prefix of _candidate_ (respecting _caseSensitive_), append _candidate_ to _basePaths_.
> 7. Return _basePaths_.
**GET_INCLUDE_BASE_PATH**(_absoluteSpec_)
> 1. Let _wildcardOffset_ be the index of the first `"*"` or `"?"` character in _absoluteSpec_.
> 2. If _wildcardOffset_ < 0, then:
> 1. If _absoluteSpec_ has a file extension (contains `"."`), return the parent directory of _absoluteSpec_.
> 2. Return _absoluteSpec_.
> 3. Return the substring of _absoluteSpec_ up to and including the last `"/"` before _wildcardOffset_.
---
## 14. Directory Traversal
**READ_DIRECTORY**(_host_, _currentDir_, _path_, _extensions_, _excludeSpecs_, _includeSpecs_, _caseSensitive_, _depth_)
The _host_ must provide the following operations:
- **Realpath**(_path_) — Resolves symlinks and returns the canonical absolute path.
- **GetAccessibleEntries**(_path_) — Returns the sorted lists of files and subdirectories in the directory at _path_.
> 1. Let _path_ be the normalized form of _path_.
> 2. Let _currentDir_ be the normalized form of _currentDir_.
> 3. Let _absolutePath_ be the concatenation of _currentDir_, `"/"`, and _path_ (normalized).
> 4. Let (_fileIncludes_, _fileExcludes_, _fileHadIncludes_) be the result of COMPILE_FILE_MATCHER(_includeSpecs_, _excludeSpecs_, _absolutePath_, _caseSensitive_).
> 5. Let (_dirIncludes_, _dirExcludes_, _dirHadIncludes_) be the result of COMPILE_DIRECTORY_MATCHER(_includeSpecs_, _excludeSpecs_, _absolutePath_, _caseSensitive_).
> 6. Let _resultBuckets_ be a list of empty lists, with length equal to max(length of _fileIncludes_, 1).
> 7. Let _visited_ be an empty set of strings.
> 8. Let _basePaths_ be the result of GET_BASE_PATHS(_path_, _includeSpecs_, _caseSensitive_).
> 9. For each _basePath_ in _basePaths_:
> 1. Let _baseAbsolute_ be the concatenation of _currentDir_, `"/"`, and _basePath_ (normalized).
> 2. Perform VISIT(_host_, _basePath_, _baseAbsolute_, _depth_, _extensions_, _fileIncludes_, _fileExcludes_, _fileHadIncludes_, _dirIncludes_, _dirExcludes_, _dirHadIncludes_, _caseSensitive_, _visited_, _resultBuckets_).
> 10. Return the concatenation of all lists in _resultBuckets_, in order.
**VISIT**(_host_, _path_, _absolutePath_, _depth_, _extensions_, _fileIncludes_, _fileExcludes_, _fileHadIncludes_, _dirIncludes_, _dirExcludes_, _dirHadIncludes_, _caseSensitive_, _visited_, _resultBuckets_)
> 1. Let _realPath_ be the result of _host_.Realpath(_absolutePath_).
> 2. Let _canonicalPath_ be the canonical form of _realPath_ under the file system's case-sensitivity rules.
> 3. If _visited_ contains _canonicalPath_, return.
> 4. Add _canonicalPath_ to _visited_.
> 5. Let _entries_ be the result of _host_.GetAccessibleEntries(_absolutePath_).
> 6. Let _absPrefix_ be ENSURE_TRAILING_SLASH(_absolutePath_).
> 7. Let _pathPrefix_ be ENSURE_TRAILING_SLASH(_path_).
> 8. For each _file_ in _entries_.files:
> 1. If _extensions_ is non-empty and the file extension of _file_ is not in _extensions_, continue.
> 2. Let _absFile_ be _absPrefix_ concatenated with _file_.
> 3. Let (_index_, _matched_) be the result of MATCH_FILE(_absFile_, _fileIncludes_, _fileExcludes_, _fileHadIncludes_).
> 4. If _matched_ is **true**, append _pathPrefix_ concatenated with _file_ to _resultBuckets_\[_index_\].
> 9. If _depth_ is finite (i.e., not the sentinel value representing unlimited depth), then:
> 1. Decrement _depth_.
> 2. If _depth_ is 0, return.
> 10. For each _dir_ in _entries_.directories:
> 1. Let _absDir_ be _absPrefix_ concatenated with _dir_.
> 2. If the result of MATCH_DIRECTORY(_absDir_, _dirIncludes_, _dirExcludes_, _dirHadIncludes_) is **false**, continue.
> 3. Perform VISIT(_host_, _pathPrefix_ concatenated with _dir_, _absDir_, _depth_, _extensions_, _fileIncludes_, _fileExcludes_, _fileHadIncludes_, _dirIncludes_, _dirExcludes_, _dirHadIncludes_, _caseSensitive_, _visited_, _resultBuckets_).
---
## 15. Invariants
The following properties hold for all conforming implementations:
1. COMPILE_PATTERN returns **failure** for any include or directory spec whose last component is `"**"`.
2. When the pattern is exhausted but path components remain, exclude patterns return **true** and all other patterns return **false**.
3. MATCH_SEGMENTS is guaranteed **O(n·m)** where _n_ is the string length and _m_ is the segment count.
4. The `.min.js` default exclusion applies only under **Files** usage mode and only to wildcard components.
5. Symlink cycles are detected through real-path canonicalization in VISIT and cause the directory to be skipped.
6. Excludes are always evaluated before includes in MATCH_FILE and MATCH_DIRECTORY.
7. For include patterns, wildcard components reject package folders; literal components do not.
8. For include patterns, `**` does not descend into hidden paths or package folders.
9. For include patterns, a wildcard component whose first segment is **SegStar** or **SegQuestion** does not match hidden path components.

View File

@@ -0,0 +1,233 @@
package vfsmatch
import (
"testing"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/cachedvfs"
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
)
// Benchmark test cases using the same hosts as the unit tests
func BenchmarkReadDirectory(b *testing.B) {
benchCases := []struct {
name string
host func() vfs.FS
path string
extensions []string
excludes []string
includes []string
}{
{
name: "LiteralIncludes",
host: caseInsensitiveHost,
path: "/dev",
extensions: []string{".ts", ".tsx", ".d.ts"},
includes: []string{"a.ts", "b.ts"},
},
{
name: "WildcardIncludes",
host: caseInsensitiveHost,
path: "/dev",
extensions: []string{".ts", ".tsx", ".d.ts"},
includes: []string{"z/*.ts", "x/*.ts"},
},
{
name: "RecursiveWildcard",
host: caseInsensitiveHost,
path: "/dev",
extensions: []string{".ts", ".tsx", ".d.ts"},
includes: []string{"**/a.ts"},
},
{
name: "RecursiveWithExcludes",
host: caseInsensitiveHost,
path: "/dev",
extensions: []string{".ts", ".tsx", ".d.ts"},
excludes: []string{"**/b.ts"},
includes: []string{"**/*.ts"},
},
{
name: "ComplexPattern",
host: caseInsensitiveHost,
path: "/dev",
extensions: []string{".ts", ".tsx", ".d.ts"},
excludes: []string{"*.ts", "z/??z.ts", "*/b.ts"},
includes: []string{"a.ts", "b.ts", "z/a.ts", "z/abz.ts", "z/aba.ts", "x/b.ts"},
},
{
name: "DottedFolders",
host: dottedFoldersHost,
path: "/dev",
extensions: []string{".ts", ".tsx", ".d.ts"},
includes: []string{"**/.*/*"},
},
{
name: "CommonPackageFolders",
host: commonFoldersHost,
path: "/dev",
extensions: []string{".ts", ".tsx", ".d.ts"},
includes: []string{"**/a.ts"},
},
{
name: "NoIncludes",
host: caseInsensitiveHost,
path: "/dev",
extensions: []string{".ts", ".tsx", ".d.ts"},
},
{
name: "MultipleRecursive",
host: caseInsensitiveHost,
path: "/dev",
extensions: []string{".ts", ".tsx", ".d.ts"},
includes: []string{"**/x/**/*"},
},
{
name: "LargeFileSystem",
host: largeFileSystemHost,
path: "/project",
extensions: []string{".ts", ".tsx", ".d.ts"},
includes: []string{"src/**/*.ts"},
excludes: []string{"**/node_modules/**", "**/*.test.ts"},
},
{
name: "LargeAllFiles",
host: largeFileSystemHost,
path: "/project",
extensions: []string{".ts", ".tsx", ".js"},
excludes: []string{"**/node_modules/**"},
includes: []string{"**/*"},
},
}
for _, bc := range benchCases {
b.Run(bc.name, func(b *testing.B) {
host := cachedvfs.From(bc.host())
b.ReportAllocs()
for b.Loop() {
matchFiles(bc.path, bc.extensions, bc.excludes, bc.includes, host.UseCaseSensitiveFileNames(), "/", UnlimitedDepth, host)
}
})
}
}
// largeFileSystemHost creates a more realistic file system with many files
func largeFileSystemHost() vfs.FS {
files := make(map[string]string)
// Create a realistic project structure
dirs := []string{
"/project/src",
"/project/src/components",
"/project/src/utils",
"/project/src/services",
"/project/src/models",
"/project/src/hooks",
"/project/test",
"/project/node_modules/react",
"/project/node_modules/typescript",
"/project/node_modules/@types/node",
}
// Add files to each directory
for _, dir := range dirs {
for j := range 20 {
files[dir+"/file"+string(rune('a'+j))+".ts"] = ""
files[dir+"/file"+string(rune('a'+j))+".test.ts"] = ""
}
}
// Add some dotted directories
files["/project/src/.hidden/secret.ts"] = ""
files["/project/.config/settings.ts"] = ""
return vfstest.FromMap(files, false)
}
// BenchmarkPatternCompilation benchmarks the pattern compilation step
func BenchmarkPatternCompilation(b *testing.B) {
patterns := []struct {
name string
spec string
}{
{"Literal", "src/file.ts"},
{"SingleWildcard", "src/*.ts"},
{"QuestionMark", "src/?.ts"},
{"DoubleAsterisk", "**/file.ts"},
{"Complex", "src/**/components/*.tsx"},
{"DottedPattern", "**/.*/*"},
}
for _, p := range patterns {
b.Run(p.name, func(b *testing.B) {
for b.Loop() {
_, _ = compileGlobPattern(p.spec, "/project", UsageFiles, true)
}
})
}
}
// BenchmarkPatternMatching benchmarks pattern matching against paths
func BenchmarkPatternMatching(b *testing.B) {
testCases := []struct {
name string
spec string
paths []string
}{
{
name: "LiteralMatch",
spec: "src/file.ts",
paths: []string{
"/project/src/file.ts",
"/project/src/other.ts",
"/project/lib/file.ts",
},
},
{
name: "WildcardMatch",
spec: "src/*.ts",
paths: []string{
"/project/src/file.ts",
"/project/src/component.ts",
"/project/src/deep/file.ts",
"/project/lib/file.ts",
},
},
{
name: "RecursiveMatch",
spec: "**/file.ts",
paths: []string{
"/project/file.ts",
"/project/src/file.ts",
"/project/src/deep/nested/file.ts",
"/project/src/other.ts",
},
},
{
name: "ComplexMatch",
spec: "src/**/components/*.tsx",
paths: []string{
"/project/src/components/Button.tsx",
"/project/src/features/auth/components/Login.tsx",
"/project/src/components/Button.ts",
"/project/lib/components/Button.tsx",
},
},
}
for _, tc := range testCases {
pattern, ok := compileGlobPattern(tc.spec, "/project", UsageFiles, true)
if !ok {
continue
}
b.Run(tc.name, func(b *testing.B) {
for b.Loop() {
for _, path := range tc.paths {
pattern.matches(path)
}
}
})
}
}

View File

@@ -0,0 +1,26 @@
// Code generated by "stringer -type=Usage -trimprefix=Usage -output=stringer_generated.go"; DO NOT EDIT.
package vfsmatch
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[UsageFiles-0]
_ = x[UsageDirectories-1]
_ = x[UsageExclude-2]
}
const _Usage_name = "FilesDirectoriesExclude"
var _Usage_index = [...]uint8{0, 5, 16, 23}
func (i Usage) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_Usage_index)-1 {
return "Usage(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Usage_name[_Usage_index[idx]:_Usage_index[idx+1]]
}

View 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}
}

File diff suppressed because it is too large Load Diff