Files
kjol/tools/tsgo/internal/project/configfileregistrybuilder.go
2026-07-09 16:50:43 -04:00

734 lines
26 KiB
Go

package project
import (
"fmt"
"maps"
"slices"
"strings"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/project/dirty"
"github.com/microsoft/typescript-go/internal/project/logging"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
)
var (
_ tsoptions.ParseConfigHost = (*configFileRegistryBuilder)(nil)
_ tsoptions.ExtendedConfigCache = (*configFileRegistryBuilder)(nil)
)
// configFileRegistryBuilder tracks changes made on top of a previous
// configFileRegistry, producing a new clone with `finalize()` after
// all changes have been made.
type configFileRegistryBuilder struct {
hasRelativePatternCapability bool
fs *sourceFS
isOpenFile func(tspath.Path) bool
extendedConfigCache *ExtendedConfigCache
snapshotID uint64
sessionOptions *SessionOptions
customConfigFileName string
base *ConfigFileRegistry
configs *dirty.SyncMap[tspath.Path, *configFileEntry]
configFileNames *dirty.Map[tspath.Path, *configFileNames]
customConfigFileNameChanged bool
}
func newConfigFileRegistryBuilder(
hasRelativePatternCapability bool,
fs *snapshotFSBuilder,
oldConfigFileRegistry *ConfigFileRegistry,
extendedConfigCache *ExtendedConfigCache,
snapshotID uint64,
sessionOptions *SessionOptions,
customConfigFileName string,
logger *logging.LogTree,
) *configFileRegistryBuilder {
return &configFileRegistryBuilder{
hasRelativePatternCapability: hasRelativePatternCapability,
fs: newSourceFS(false, fs, fs.toPath),
isOpenFile: fs.isOpenFile,
base: oldConfigFileRegistry,
sessionOptions: sessionOptions,
extendedConfigCache: extendedConfigCache,
snapshotID: snapshotID,
customConfigFileName: customConfigFileName,
customConfigFileNameChanged: customConfigFileName != oldConfigFileRegistry.customConfigFileName,
configs: dirty.NewSyncMap(oldConfigFileRegistry.configs),
configFileNames: dirty.NewMap(oldConfigFileRegistry.configFileNames),
}
}
// Finalize creates a new configFileRegistry based on the changes made in the builder.
// If no changes were made, it returns the original base registry.
func (c *configFileRegistryBuilder) Finalize() *ConfigFileRegistry {
var changed bool
newRegistry := c.base
ensureCloned := func() {
if !changed {
newRegistry = newRegistry.clone()
changed = true
}
}
if configs, changedConfigs := c.configs.Finalize(); changedConfigs {
ensureCloned()
newRegistry.configs = configs
}
if configFileNames, changedNames := c.configFileNames.Finalize(); changedNames {
ensureCloned()
newRegistry.configFileNames = configFileNames
}
if c.customConfigFileNameChanged {
ensureCloned()
newRegistry.customConfigFileName = c.customConfigFileName
}
return newRegistry
}
func (c *configFileRegistryBuilder) findOrAcquireConfigForFile(
configFileName string,
configFilePath tspath.Path,
filePath tspath.Path,
loadKind projectLoadKind,
logger *logging.LogTree,
) *tsoptions.ParsedCommandLine {
switch loadKind {
case projectLoadKindFind:
if entry, ok := c.configs.Load(configFilePath); ok {
return entry.Value().commandLine
}
return nil
case projectLoadKindCreate:
return c.acquireConfigForFile(configFileName, configFilePath, filePath, logger)
default:
panic(fmt.Sprintf("unknown project load kind: %d", loadKind))
}
}
// reloadIfNeeded updates the command line of the config file entry based on its
// pending reload state. This function should only be called from within the
// Change() method of a dirty map entry.
func (c *configFileRegistryBuilder) reloadIfNeeded(entry *configFileEntry, fileName string, path tspath.Path, logger *logging.LogTree) {
switch entry.pendingReload {
case PendingReloadFileNames:
logger.Log("Reloading file names for config: " + fileName)
entry.commandLine = entry.commandLine.ReloadFileNamesOfParsedCommandLine(c.fs)
case PendingReloadFull:
logger.Log("Loading config file: " + fileName)
oldCommandLine := entry.commandLine
entry.commandLine, _ = tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, nil /*optionsRaw*/, c, c)
c.updateExtendingConfigs(path, entry.commandLine, oldCommandLine)
c.updateRootFilesWatch(fileName, entry)
logger.Log("Finished loading config file")
default:
return
}
entry.pendingReload = PendingReloadNone
}
func (c *configFileRegistryBuilder) updateExtendingConfigs(extendingConfigPath tspath.Path, newCommandLine *tsoptions.ParsedCommandLine, oldCommandLine *tsoptions.ParsedCommandLine) {
var newExtendedConfigPaths collections.Set[tspath.Path]
if newCommandLine != nil {
for _, extendedConfig := range newCommandLine.ExtendedSourceFiles() {
extendedConfigPath := c.fs.toPath(extendedConfig)
newExtendedConfigPaths.Add(extendedConfigPath)
entry, loaded := c.configs.LoadOrStore(extendedConfigPath, newExtendedConfigFileEntry(extendedConfig, extendingConfigPath))
if loaded {
entry.ChangeIf(
func(config *configFileEntry) bool {
_, alreadyRetaining := config.retainingConfigs[extendingConfigPath]
return !alreadyRetaining
},
func(config *configFileEntry) {
if config.retainingConfigs == nil {
config.retainingConfigs = make(map[tspath.Path]struct{})
}
config.retainingConfigs[extendingConfigPath] = struct{}{}
},
)
}
}
}
if oldCommandLine != nil {
for _, extendedConfig := range oldCommandLine.ExtendedSourceFiles() {
extendedConfigPath := c.fs.toPath(extendedConfig)
if newExtendedConfigPaths.Has(extendedConfigPath) {
continue
}
if entry, ok := c.configs.Load(extendedConfigPath); ok {
entry.ChangeIf(
func(config *configFileEntry) bool {
_, exists := config.retainingConfigs[extendingConfigPath]
return exists
},
func(config *configFileEntry) {
delete(config.retainingConfigs, extendingConfigPath)
},
)
}
}
}
}
func (c *configFileRegistryBuilder) updateRootFilesWatch(fileName string, entry *configFileEntry) {
if entry.rootFilesWatch == nil {
return
}
var ignored map[string]struct{}
var globs []string
var externalDirectories []string
var includeWorkspace bool
var includeTsconfigDir bool
tsconfigDir := tspath.GetDirectoryPath(fileName)
wildcardDirectories := entry.commandLine.WildcardDirectories()
comparePathsOptions := tspath.ComparePathsOptions{
CurrentDirectory: c.sessionOptions.CurrentDirectory,
UseCaseSensitiveFileNames: c.FS().UseCaseSensitiveFileNames(),
}
for dir := range wildcardDirectories {
if tspath.ContainsPath(c.sessionOptions.CurrentDirectory, dir, comparePathsOptions) {
includeWorkspace = true
} else if tspath.ContainsPath(tsconfigDir, dir, comparePathsOptions) {
includeTsconfigDir = true
} else {
externalDirectories = append(externalDirectories, dir)
}
}
for _, fileName := range entry.commandLine.LiteralFileNames() {
if tspath.ContainsPath(c.sessionOptions.CurrentDirectory, fileName, comparePathsOptions) {
includeWorkspace = true
} else if tspath.ContainsPath(tsconfigDir, fileName, comparePathsOptions) {
includeTsconfigDir = true
} else {
externalDirectories = append(externalDirectories, tspath.GetDirectoryPath(fileName))
}
}
if includeWorkspace {
globs = append(globs, getRecursiveGlobPattern(c.sessionOptions.CurrentDirectory))
}
if includeTsconfigDir {
globs = append(globs, getRecursiveGlobPattern(tsconfigDir))
}
for _, fileName := range entry.commandLine.ExtendedSourceFiles() {
if includeWorkspace && tspath.ContainsPath(c.sessionOptions.CurrentDirectory, fileName, comparePathsOptions) {
continue
}
globs = append(globs, fileName)
}
if len(externalDirectories) > 0 {
commonParents, ignoredExternalDirs := tspath.GetCommonParents(externalDirectories, minWatchLocationDepth, getPathComponentsForWatching, comparePathsOptions)
for _, parent := range commonParents {
globs = append(globs, getRecursiveGlobPattern(parent))
}
ignored = ignoredExternalDirs
}
slices.Sort(globs)
entry.rootFilesWatch = entry.rootFilesWatch.Clone(PatternsAndIgnored{
patternsInsideWorkspace: globs,
ignored: ignored,
})
}
// acquireConfigForProject loads a config file entry from the cache, or parses it if not already
// cached, then adds the project (if provided) to `retainingProjects` to keep it alive
// in the cache. Each `acquireConfigForProject` call that passes a `project` should be accompanied
// by an eventual `releaseConfigForProject` call with the same project.
func (c *configFileRegistryBuilder) acquireConfigForProject(fileName string, path tspath.Path, project *Project, logger *logging.LogTree) *tsoptions.ParsedCommandLine {
entry, _ := c.configs.LoadOrStore(path, newConfigFileEntry(c.hasRelativePatternCapability, fileName))
var needsRetainProject bool
entry.ChangeIf(
func(config *configFileEntry) bool {
_, alreadyRetaining := config.retainingProjects[project.configFilePath]
needsRetainProject = !alreadyRetaining
return needsRetainProject || config.pendingReload != PendingReloadNone
},
func(config *configFileEntry) {
if needsRetainProject {
if config.retainingProjects == nil {
config.retainingProjects = make(map[tspath.Path]struct{})
}
config.retainingProjects[project.configFilePath] = struct{}{}
}
c.reloadIfNeeded(config, fileName, path, logger)
},
)
return entry.Value().commandLine
}
// acquireConfigForFile loads a config file entry from the cache, or parses it if not already
// cached, then adds the open file to `retainingOpenFiles` to keep it alive in the cache.
// Each `acquireConfigForFile` call that passes an `openFilePath`
// should be accompanied by an eventual `releaseConfigForOpenFile` call with the same open file.
func (c *configFileRegistryBuilder) acquireConfigForFile(configFileName string, configFilePath tspath.Path, filePath tspath.Path, logger *logging.LogTree) *tsoptions.ParsedCommandLine {
entry, _ := c.configs.LoadOrStore(configFilePath, newConfigFileEntry(c.hasRelativePatternCapability, configFileName))
var needsRetainOpenFile bool
entry.ChangeIf(
func(config *configFileEntry) bool {
if c.isOpenFile(filePath) {
_, alreadyRetaining := config.retainingOpenFiles[filePath]
needsRetainOpenFile = !alreadyRetaining
}
return needsRetainOpenFile || config.pendingReload != PendingReloadNone
},
func(config *configFileEntry) {
if needsRetainOpenFile {
if config.retainingOpenFiles == nil {
config.retainingOpenFiles = make(map[tspath.Path]struct{})
}
config.retainingOpenFiles[filePath] = struct{}{}
}
c.reloadIfNeeded(config, configFileName, configFilePath, logger)
},
)
return entry.Value().commandLine
}
// releaseConfigForProject removes the project from the config entry. Once no projects
// or files are associated with the config entry, it will be removed on the next call to `cleanup`.
func (c *configFileRegistryBuilder) releaseConfigForProject(configFilePath tspath.Path, projectPath tspath.Path) {
if entry, ok := c.configs.Load(configFilePath); ok {
entry.ChangeIf(
func(config *configFileEntry) bool {
_, exists := config.retainingProjects[projectPath]
return exists
},
func(config *configFileEntry) {
delete(config.retainingProjects, projectPath)
},
)
}
}
// didCloseFile removes the open file from the config entry. Once no projects
// or files are associated with the config entry, it will be removed on the next call to `cleanup`.
func (c *configFileRegistryBuilder) didCloseFile(path tspath.Path) {
if tspath.IsDynamicFileName(string(path)) {
return
}
c.configFileNames.Delete(path)
c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry]) bool {
entry.ChangeIf(
func(config *configFileEntry) bool {
_, ok := config.retainingOpenFiles[path]
return ok
},
func(config *configFileEntry) {
delete(config.retainingOpenFiles, path)
},
)
return true
})
}
type changeFileResult struct {
affectedProjects map[tspath.Path]struct{}
affectedFiles map[tspath.Path]struct{}
}
func (r changeFileResult) IsEmpty() bool {
return len(r.affectedProjects) == 0 && len(r.affectedFiles) == 0
}
func (c *configFileRegistryBuilder) DidChangeCustomConfigFileName(logger *logging.LogTree) bool {
if !c.customConfigFileNameChanged {
return false
}
c.configFileNames.Clear()
return true
}
func (c *configFileRegistryBuilder) invalidateCache(logger *logging.LogTree) changeFileResult {
var affectedProjects map[tspath.Path]struct{}
var affectedFiles map[tspath.Path]struct{}
logger.Log("Too many files changed; marking all configs for reload")
c.configFileNames.Range(func(entry *dirty.MapEntry[tspath.Path, *configFileNames]) bool {
if affectedFiles == nil {
affectedFiles = make(map[tspath.Path]struct{})
}
affectedFiles[entry.Key()] = struct{}{}
return true
})
c.configFileNames.Clear()
c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry]) bool {
entry.Change(func(entry *configFileEntry) {
affectedProjects = core.CopyMapInto(affectedProjects, entry.retainingProjects)
if entry.pendingReload != PendingReloadFull {
text, ok := c.FS().ReadFile(entry.fileName)
if !ok || entry.commandLine == nil || text != entry.commandLine.ConfigFile.SourceFile.Text() {
entry.pendingReload = PendingReloadFull
} else {
entry.pendingReload = PendingReloadFileNames
}
}
})
return true
})
return changeFileResult{
affectedProjects: affectedProjects,
affectedFiles: affectedFiles,
}
}
func (c *configFileRegistryBuilder) isConfigBaseName(baseName string) bool {
return baseName == "tsconfig.json" || baseName == "jsconfig.json" ||
(c.customConfigFileName != "" && baseName == c.customConfigFileName)
}
func (c *configFileRegistryBuilder) DidChangeFiles(summary FileChangeSummary, logger *logging.LogTree) changeFileResult {
var affectedProjects map[tspath.Path]struct{}
var affectedFiles map[tspath.Path]struct{}
var shouldInvalidateCache bool
logger.Log("Summarizing file changes")
hasExcessiveChanges := summary.HasExcessiveWatchEvents() && summary.IncludesWatchChangeOutsideNodeModules
createdFiles := make(map[tspath.Path]string, summary.Created.Len())
deletedFiles := make(map[tspath.Path]string, summary.Deleted.Len())
createdOrDeletedConfigFiles := make(map[tspath.Path]struct{})
createdOrChangedOrDeletedFiles := make(map[tspath.Path]struct{}, summary.Changed.Len()+summary.Deleted.Len())
for uri := range summary.Changed.Keys() {
if tspath.ContainsIgnoredPath(string(uri)) {
continue
}
fileName := uri.FileName()
path := c.fs.toPath(fileName)
baseName := tspath.GetBaseFileName(string(path))
if c.isConfigBaseName(baseName) {
createdOrDeletedConfigFiles[path] = struct{}{}
}
createdOrChangedOrDeletedFiles[path] = struct{}{}
}
for uri := range summary.Deleted.Keys() {
if tspath.ContainsIgnoredPath(string(uri)) {
continue
}
fileName := uri.FileName()
path := c.fs.toPath(fileName)
deletedFiles[path] = fileName
baseName := tspath.GetBaseFileName(string(path))
if c.isConfigBaseName(baseName) {
createdOrDeletedConfigFiles[path] = struct{}{}
}
createdOrChangedOrDeletedFiles[path] = struct{}{}
}
for uri := range summary.Created.Keys() {
if tspath.ContainsIgnoredPath(string(uri)) {
continue
}
fileName := uri.FileName()
path := c.fs.toPath(fileName)
createdFiles[path] = fileName
baseName := tspath.GetBaseFileName(string(path))
if c.isConfigBaseName(baseName) {
createdOrDeletedConfigFiles[path] = struct{}{}
}
createdOrChangedOrDeletedFiles[path] = struct{}{}
}
// Handle closed files - this ranges over config entries and could be combined
// with the file change handling, but a separate loop is simpler and a snapshot
// change with both closing and watch changes seems rare.
for uri := range summary.Closed.Keys() {
fileName := uri.FileName()
path := c.fs.toPath(fileName)
c.didCloseFile(path)
}
// Handle changes to stored config files
logger.Log("Checking if any changed files are config files")
for path := range createdOrChangedOrDeletedFiles {
if entry, ok := c.configs.Load(path); ok {
if hasExcessiveChanges {
return c.invalidateCache(logger)
}
affectedProjects = core.CopyMapInto(affectedProjects, c.handleConfigChange(entry, logger))
for extendingConfigPath := range entry.Value().retainingConfigs {
if extendingConfigEntry, ok := c.configs.Load(extendingConfigPath); ok {
affectedProjects = core.CopyMapInto(affectedProjects, c.handleConfigChange(extendingConfigEntry, logger))
}
}
// This was a config file, so assume it's not also a root file
delete(createdFiles, path)
}
}
// Handle created/deleted files named "tsconfig.json" or "jsconfig.json"
for path := range createdOrDeletedConfigFiles {
if hasExcessiveChanges {
return c.invalidateCache(logger)
}
directoryPath := path.GetDirectoryPath()
c.configFileNames.Range(func(entry *dirty.MapEntry[tspath.Path, *configFileNames]) bool {
if directoryPath.ContainsPath(entry.Key()) {
if affectedFiles == nil {
affectedFiles = make(map[tspath.Path]struct{})
}
affectedFiles[entry.Key()] = struct{}{}
entry.Delete()
}
return true
})
}
// Handle deletions of wildcard-included root files
for path, fileName := range deletedFiles {
c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry]) bool {
entry.ChangeIf(
func(config *configFileEntry) bool {
if config.pendingReload != PendingReloadNone || config.commandLine == nil {
return false
}
if _, ok := config.commandLine.FileNamesByPath()[path]; ok {
// If the file is included in FileNames() but not matched by literal "files", it must be
// included via wildcard, which means a reload of filenames will remove it from the list.
// (Files explicitly specified in "files" are always included in the ParsedCommandLine,
// triggering a missing root file error during program construction.)
return config.commandLine.GetMatchedFileSpec(fileName) == ""
}
return false
},
func(config *configFileEntry) {
config.pendingReload = PendingReloadFileNames
if affectedProjects == nil {
affectedProjects = make(map[tspath.Path]struct{})
}
maps.Copy(affectedProjects, config.retainingProjects)
logger.Logf("Root files for config %s changed", entry.Key())
shouldInvalidateCache = hasExcessiveChanges
},
)
return !shouldInvalidateCache
})
if shouldInvalidateCache {
return c.invalidateCache(logger)
}
}
// Handle possible root file creation
if len(createdFiles) > 0 {
c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry]) bool {
entry.ChangeIf(
func(config *configFileEntry) bool {
if config.commandLine == nil || config.rootFilesWatch == nil || config.pendingReload != PendingReloadNone {
return false
}
logger.Logf("Checking if any of %d created files match root files for config %s", len(createdFiles), entry.Key())
for path, fileName := range createdFiles {
if config.commandLine.PossiblyMatchesFileName(fileName) {
return true
}
if config.commandLine.PossiblyMatchesDirectoryName(path) && c.fs.DirectoryExists(fileName) {
// If we got a creation event for a directory, it's probably a symlink. We don't need to
// test realpath here; this is enough confidence to trigger a filename reload.
return true
}
}
return false
},
func(config *configFileEntry) {
config.pendingReload = PendingReloadFileNames
if affectedProjects == nil {
affectedProjects = make(map[tspath.Path]struct{})
}
maps.Copy(affectedProjects, config.retainingProjects)
logger.Logf("Root files for config %s changed", entry.Key())
shouldInvalidateCache = hasExcessiveChanges
},
)
return !shouldInvalidateCache
})
if shouldInvalidateCache {
return c.invalidateCache(logger)
}
}
return changeFileResult{
affectedProjects: affectedProjects,
affectedFiles: affectedFiles,
}
}
func (c *configFileRegistryBuilder) handleConfigChange(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry], logger *logging.LogTree) map[tspath.Path]struct{} {
var affectedProjects map[tspath.Path]struct{}
changed := entry.ChangeIf(
func(config *configFileEntry) bool { return config.pendingReload != PendingReloadFull },
func(config *configFileEntry) { config.pendingReload = PendingReloadFull },
)
if changed {
logger.Logf("Config file %s changed", entry.Key())
affectedProjects = maps.Clone(entry.Value().retainingProjects)
}
return affectedProjects
}
func (c *configFileRegistryBuilder) computeConfigFileName(fileName string, skipSearchInDirectoryOfFile bool, logger *logging.LogTree) string {
searchPath := tspath.GetDirectoryPath(fileName)
// Prefer custom config file if provided; search ancestors with correct skip behavior.
if c.customConfigFileName != "" {
skip := skipSearchInDirectoryOfFile
if result, _ := tspath.ForEachAncestorDirectory(searchPath, func(directory string) (result string, stop bool) {
if !skip {
customPath := tspath.CombinePaths(directory, c.customConfigFileName)
if c.FS().FileExists(customPath) {
return customPath, true
}
}
if strings.HasSuffix(directory, "/node_modules") {
return "", true
}
skip = false
return "", false
}); result != "" {
logger.Logf("computeConfigFileName:: File: %s:: Result: %s", fileName, result)
return result
}
}
// When searching for ancestor of a config file, determine which config types to skip
// in the starting directory. This matches TSServer's forEachConfigFileLocation behavior:
// - For ancestor of tsconfig.json: skip tsconfig.json but still check jsconfig.json
// - For ancestor of jsconfig.json: skip both tsconfig.json and jsconfig.json
skipTsconfig := skipSearchInDirectoryOfFile
skipJsconfig := skipSearchInDirectoryOfFile && !strings.HasSuffix(fileName, "/tsconfig.json")
result, _ := tspath.ForEachAncestorDirectory(searchPath, func(directory string) (result string, stop bool) {
if !skipTsconfig {
tsconfigPath := tspath.CombinePaths(directory, "tsconfig.json")
if c.FS().FileExists(tsconfigPath) {
return tsconfigPath, true
}
}
if !skipJsconfig {
jsconfigPath := tspath.CombinePaths(directory, "jsconfig.json")
if c.FS().FileExists(jsconfigPath) {
return jsconfigPath, true
}
}
if strings.HasSuffix(directory, "/node_modules") {
return "", true
}
skipTsconfig = false
skipJsconfig = false
return "", false
})
logger.Logf("computeConfigFileName:: File: %s:: Result: %s", fileName, result)
return result
}
func (c *configFileRegistryBuilder) getConfigFileNameForFile(fileName string, path tspath.Path, logger *logging.LogTree) string {
if tspath.IsDynamicFileName(fileName) {
return ""
}
if entry, ok := c.configFileNames.Get(path); ok {
return entry.Value().nearestConfigFileName
}
configName := c.computeConfigFileName(fileName, false, logger)
if c.isOpenFile(path) {
c.configFileNames.Add(path, &configFileNames{
nearestConfigFileName: configName,
})
}
return configName
}
func (c *configFileRegistryBuilder) forEachConfigFileNameFor(path tspath.Path, cb func(configFileName string)) {
if tspath.IsDynamicFileName(string(path)) {
return
}
if entry, ok := c.configFileNames.Get(path); ok {
configFileName := entry.Value().nearestConfigFileName
for configFileName != "" {
cb(configFileName)
if ancestorConfigName, found := entry.Value().ancestors[configFileName]; found {
configFileName = ancestorConfigName
} else {
return
}
}
}
}
func (c *configFileRegistryBuilder) getAncestorConfigFileName(fileName string, path tspath.Path, configFileName string, logger *logging.LogTree) string {
if tspath.IsDynamicFileName(fileName) {
return ""
}
entry, ok := c.configFileNames.Get(path)
if !ok {
return ""
}
if ancestorConfigName, found := entry.Value().ancestors[configFileName]; found {
return ancestorConfigName
}
// Look for config in parent folders of config file
result := c.computeConfigFileName(configFileName, true, logger)
if c.isOpenFile(path) {
entry.Change(func(value *configFileNames) {
if value.ancestors == nil {
value.ancestors = make(map[string]string)
}
value.ancestors[configFileName] = result
})
}
return result
}
// FS implements tsoptions.ParseConfigHost.
func (c *configFileRegistryBuilder) FS() vfs.FS {
return c.fs
}
// GetCurrentDirectory implements tsoptions.ParseConfigHost.
func (c *configFileRegistryBuilder) GetCurrentDirectory() string {
return c.sessionOptions.CurrentDirectory
}
// GetExtendedConfig implements tsoptions.ExtendedConfigCache.
func (c *configFileRegistryBuilder) GetExtendedConfig(fileName string, path tspath.Path, resolutionStack []tspath.Path, host tsoptions.ParseConfigHost) *tsoptions.ExtendedConfigCacheEntry {
var content string
fh := c.fs.GetFileByPath(fileName, path)
if fh != nil {
content = fh.Content()
}
return c.extendedConfigCache.LoadAndAcquire(path, c.snapshotID, ExtendedConfigParseArgs{
FileName: fileName,
Content: content,
FS: c.fs.source,
ResolutionStack: resolutionStack,
Host: host,
Cache: c,
}).ExtendedConfigCacheEntry
}
func (c *configFileRegistryBuilder) Cleanup() {
c.configs.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *configFileEntry]) bool {
entry.DeleteIf(func(value *configFileEntry) bool {
return len(value.retainingProjects) == 0 && len(value.retainingOpenFiles) == 0 && len(value.retainingConfigs) == 0
})
return true
})
}