package compiler import ( "math" "sync" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/module" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tsoptions" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" ) type parseTask struct { normalizedFilePath string path tspath.Path file *ast.SourceFile libFile *LibFile redirectedParseTask *parseTask subTasks []*parseTask loaded bool isForAutomaticTypeDirective bool includeReason *fileIncludeReason metadata ast.SourceFileMetaData resolutionsInFile module.ModeAwareCache[*module.ResolvedModule] resolutionsTrace []string typeResolutionsInFile module.ModeAwareCache[*module.ResolvedTypeReferenceDirective] typeResolutionsTrace []string resolutionDiagnostics []*ast.Diagnostic importHelpersImportSpecifier *ast.Node jsxRuntimeImportSpecifier *jsxRuntimeImportSpecifier increaseDepth bool elideOnDepth bool // Track if this file is from an external library (node_modules) // This mirrors the TypeScript currentNodeModulesDepth > 0 check fromExternalLibrary bool loadedTask *parseTask allIncludeReasons []*fileIncludeReason } func (t *parseTask) FileName() string { return t.normalizedFilePath } func (t *parseTask) Path() tspath.Path { return t.path } func (t *parseTask) isRoot() bool { // Intentionally not checking t.includeReason != nil to ensure we can catch cases for missing include reason return !t.isForAutomaticTypeDirective && (t.includeReason.kind == fileIncludeKindRootFile || t.includeReason.kind == fileIncludeKindLibFile) } func (t *parseTask) load(loader *fileLoader) { t.loaded = true t.path = loader.toPath(t.normalizedFilePath) if t.isForAutomaticTypeDirective { t.loadAutomaticTypeDirectives(loader) return } redirect := loader.projectReferenceFileMapper.getParseFileRedirect(t) if redirect != "" { t.redirect(loader, redirect) return } loader.totalFileCount.Add(1) if t.libFile != nil { loader.libFileCount.Add(1) } t.metadata = loader.loadSourceFileMetaData(t.normalizedFilePath) file := loader.parseSourceFile(t) if file == nil { return } t.file = file t.subTasks = make([]*parseTask, 0, len(file.ReferencedFiles)+len(file.Imports())+len(file.ModuleAugmentations)) for index, ref := range file.ReferencedFiles { resolvedPath := loader.resolveTripleslashPathReference(ref.FileName, file.FileName(), index) t.addSubTask(resolvedPath, nil) } compilerOptions := loader.opts.Config.CompilerOptions() loader.resolveTypeReferenceDirectives(t) if compilerOptions.NoLib != core.TSTrue { for index, lib := range file.LibReferenceDirectives { includeReason := &fileIncludeReason{ kind: fileIncludeKindLibReferenceDirective, data: &referencedFileData{ file: t.path, index: index, }, } if name, ok := tsoptions.GetLibFileName(lib.FileName); ok { libFile := loader.pathForLibFile(name) t.addSubTask(resolvedRef{ fileName: libFile.path, includeReason: includeReason, }, libFile) } else { loader.includeProcessor.addProcessingDiagnostic(&processingDiagnostic{ kind: processingDiagnosticKindUnknownReference, data: includeReason, }) } } } loader.resolveImportsAndModuleAugmentations(t) } func (t *parseTask) redirect(loader *fileLoader, fileName string) { t.redirectedParseTask = &parseTask{ normalizedFilePath: tspath.NormalizePath(fileName), libFile: t.libFile, fromExternalLibrary: t.fromExternalLibrary, includeReason: t.includeReason, } // increaseDepth and elideOnDepth are not copied to redirects, otherwise their depth would be double counted. t.subTasks = []*parseTask{t.redirectedParseTask} } func (t *parseTask) loadAutomaticTypeDirectives(loader *fileLoader) { toParseTypeRefs, typeResolutionsInFile, typeResolutionsTrace := loader.resolveAutomaticTypeDirectives(t.normalizedFilePath) t.typeResolutionsInFile = typeResolutionsInFile t.typeResolutionsTrace = typeResolutionsTrace for _, typeResolution := range toParseTypeRefs { t.addSubTask(typeResolution, nil) } } type resolvedRef struct { fileName string increaseDepth bool elideOnDepth bool isFromExternalLibrary bool includeReason *fileIncludeReason } func (t *parseTask) addSubTask(ref resolvedRef, libFile *LibFile) { normalizedFilePath := tspath.NormalizePath(ref.fileName) subTask := &parseTask{ normalizedFilePath: normalizedFilePath, libFile: libFile, increaseDepth: ref.increaseDepth, elideOnDepth: ref.elideOnDepth, fromExternalLibrary: ref.isFromExternalLibrary, includeReason: ref.includeReason, } t.subTasks = append(t.subTasks, subTask) } type filesParser struct { wg core.WorkGroup tasksByFileName collections.SyncMap[string, *queuedParseTask] maxDepth int } type queuedParseTask struct { task *parseTask mu sync.Mutex lowestDepth int fromExternalLibrary bool } func (w *filesParser) parse(loader *fileLoader, tasks []*parseTask) { w.start(loader, tasks, 0, false) w.wg.RunAndWait() } func (w *filesParser) start(loader *fileLoader, tasks []*parseTask, depth int, isFromExternalLibrary bool) { for i, task := range tasks { taskIsFromExternalLibrary := isFromExternalLibrary || task.fromExternalLibrary newTask := &queuedParseTask{task: task, lowestDepth: math.MaxInt} loadedTask, loaded := w.tasksByFileName.LoadOrStore(task.FileName(), newTask) task = loadedTask.task if loaded { tasks[i].loadedTask = task // Add in the loaded task's external-ness. taskIsFromExternalLibrary = taskIsFromExternalLibrary || task.fromExternalLibrary } w.wg.Queue(func() { loadedTask.mu.Lock() defer loadedTask.mu.Unlock() startSubtasks := false currentDepth := depth if task.increaseDepth { currentDepth++ } if currentDepth < loadedTask.lowestDepth { // If we're seeing this task at a lower depth than before, // reprocess its subtasks to ensure they are loaded. loadedTask.lowestDepth = currentDepth startSubtasks = true } if !task.isRoot() && taskIsFromExternalLibrary && !loadedTask.fromExternalLibrary { // If we're seeing this task now as an external library, // reprocess its subtasks to ensure they are also marked as external. loadedTask.fromExternalLibrary = true startSubtasks = true } if task.elideOnDepth && currentDepth > w.maxDepth { return } if !task.loaded { task.load(loader) } if startSubtasks { w.start(loader, task.subTasks, loadedTask.lowestDepth, loadedTask.fromExternalLibrary) } }) } } func (w *filesParser) collect(loader *fileLoader, tasks []*parseTask, iterate func(*parseTask)) { // Mark all tasks we saw as external after the fact. w.tasksByFileName.Range(func(key string, value *queuedParseTask) bool { if value.fromExternalLibrary { value.task.fromExternalLibrary = true } return true }) w.collectWorker(loader, tasks, iterate, collections.Set[*parseTask]{}) } func (w *filesParser) collectWorker(loader *fileLoader, tasks []*parseTask, iterate func(*parseTask), seen collections.Set[*parseTask]) { for _, task := range tasks { // Exclude automatic type directive tasks from include reason processing, // as these are internal implementation details and should not contribute // to the reasons for including files. if task.redirectedParseTask == nil && !task.isForAutomaticTypeDirective { includeReason := task.includeReason if task.loadedTask != nil { task = task.loadedTask } w.addIncludeReason(loader, task, includeReason) } // ensure we only walk each task once if !task.loaded || !seen.AddIfAbsent(task) { continue } for _, trace := range task.typeResolutionsTrace { loader.opts.Host.Trace(trace) } for _, trace := range task.resolutionsTrace { loader.opts.Host.Trace(trace) } if subTasks := task.subTasks; len(subTasks) > 0 { w.collectWorker(loader, subTasks, iterate, seen) } iterate(task) } } func (w *filesParser) addIncludeReason(loader *fileLoader, task *parseTask, reason *fileIncludeReason) { if task.redirectedParseTask != nil { w.addIncludeReason(loader, task.redirectedParseTask, reason) } else if task.loaded { if existing, ok := loader.includeProcessor.fileIncludeReasons[task.path]; ok { loader.includeProcessor.fileIncludeReasons[task.path] = append(existing, reason) } else { loader.includeProcessor.fileIncludeReasons[task.path] = []*fileIncludeReason{reason} } } }