package project import ( "fmt" "strings" "sync" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/project/ata" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/project/logging" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tsoptions" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" ) const ( inferredProjectName = "/dev/null/inferred" // lowercase so toPath is a no-op regardless of settings hr = "-----------------------------------------------" ) //go:generate go tool golang.org/x/tools/cmd/stringer -type=Kind -trimprefix=Kind -output=project_stringer_generated.go //go:generate go tool mvdan.cc/gofumpt -w project_stringer_generated.go type Kind int const ( KindInferred Kind = iota KindConfigured ) type ProgramUpdateKind int const ( ProgramUpdateKindNone ProgramUpdateKind = iota ProgramUpdateKindCloned ProgramUpdateKindSameFileNames ProgramUpdateKindNewFiles ) type PendingReload int const ( PendingReloadNone PendingReload = iota PendingReloadFileNames PendingReloadFull ) // Project represents a TypeScript project. // If changing struct fields, also update the Clone method. type Project struct { Kind Kind currentDirectory string configFileName string configFilePath tspath.Path dirty bool dirtyFilePath tspath.Path host *compilerHost CommandLine *tsoptions.ParsedCommandLine commandLineWithTypingsFiles *tsoptions.ParsedCommandLine commandLineWithTypingsFilesOnce sync.Once Program *compiler.Program // The kind of update that was performed on the program last time it was updated. ProgramUpdateKind ProgramUpdateKind // The ID of the snapshot that created the program stored in this project. ProgramLastUpdate uint64 programFilesWatch *WatchedFiles[patternsAndIgnored] failedLookupsWatch *WatchedFiles[map[tspath.Path]string] affectingLocationsWatch *WatchedFiles[map[tspath.Path]string] typingsWatch *WatchedFiles[patternsAndIgnored] checkerPool *checkerPool // installedTypingsInfo is the value of `project.ComputeTypingsInfo()` that was // used during the most recently completed typings installation. installedTypingsInfo *ata.TypingsInfo // typingsFiles are the root files added by the typings installer. typingsFiles []string } func NewConfiguredProject( configFileName string, configFilePath tspath.Path, builder *projectCollectionBuilder, logger *logging.LogTree, ) *Project { return NewProject(configFileName, KindConfigured, tspath.GetDirectoryPath(configFileName), builder, logger) } func NewInferredProject( currentDirectory string, compilerOptions *core.CompilerOptions, rootFileNames []string, builder *projectCollectionBuilder, logger *logging.LogTree, ) *Project { p := NewProject(inferredProjectName, KindInferred, currentDirectory, builder, logger) if compilerOptions == nil { compilerOptions = &core.CompilerOptions{ AllowJs: core.TSTrue, Module: core.ModuleKindESNext, ModuleResolution: core.ModuleResolutionKindBundler, Target: core.ScriptTargetES2022, Jsx: core.JsxEmitReactJSX, AllowImportingTsExtensions: core.TSTrue, StrictNullChecks: core.TSTrue, StrictFunctionTypes: core.TSTrue, SourceMap: core.TSTrue, ESModuleInterop: core.TSTrue, AllowNonTsExtensions: core.TSTrue, ResolveJsonModule: core.TSTrue, } } p.CommandLine = tsoptions.NewParsedCommandLine( compilerOptions, rootFileNames, tspath.ComparePathsOptions{ UseCaseSensitiveFileNames: builder.fs.fs.UseCaseSensitiveFileNames(), CurrentDirectory: currentDirectory, }, ) return p } func NewProject( configFileName string, kind Kind, currentDirectory string, builder *projectCollectionBuilder, logger *logging.LogTree, ) *Project { if logger != nil { logger.Log(fmt.Sprintf("Creating %sProject: %s, currentDirectory: %s", kind.String(), configFileName, currentDirectory)) } project := &Project{ configFileName: configFileName, Kind: kind, currentDirectory: currentDirectory, dirty: true, } project.configFilePath = tspath.ToPath(configFileName, currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()) if builder.sessionOptions.WatchEnabled { project.programFilesWatch = NewWatchedFiles( "non-root program files for "+configFileName, lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete, core.Identity, ) project.failedLookupsWatch = NewWatchedFiles( "failed lookups for "+configFileName, lsproto.WatchKindCreate, createResolutionLookupGlobMapper(builder.sessionOptions.CurrentDirectory, builder.sessionOptions.DefaultLibraryPath, project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()), ) project.affectingLocationsWatch = NewWatchedFiles( "affecting locations for "+configFileName, lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete, createResolutionLookupGlobMapper(builder.sessionOptions.CurrentDirectory, builder.sessionOptions.DefaultLibraryPath, project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()), ) if builder.sessionOptions.TypingsLocation != "" { project.typingsWatch = NewWatchedFiles( "typings installer files", lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete, core.Identity, ) } } return project } func (p *Project) Name() string { return p.configFileName } // ConfigFileName panics if Kind() is not KindConfigured. func (p *Project) ConfigFileName() string { if p.Kind != KindConfigured { panic("ConfigFileName called on non-configured project") } return p.configFileName } // ConfigFilePath panics if Kind() is not KindConfigured. func (p *Project) ConfigFilePath() tspath.Path { if p.Kind != KindConfigured { panic("ConfigFilePath called on non-configured project") } return p.configFilePath } func (p *Project) GetProgram() *compiler.Program { return p.Program } func (p *Project) containsFile(path tspath.Path) bool { return p.Program != nil && p.Program.GetSourceFileByPath(path) != nil } func (p *Project) IsSourceFromProjectReference(path tspath.Path) bool { return p.Program != nil && p.Program.IsSourceFromProjectReference(path) } func (p *Project) Clone() *Project { return &Project{ Kind: p.Kind, currentDirectory: p.currentDirectory, configFileName: p.configFileName, configFilePath: p.configFilePath, dirty: p.dirty, dirtyFilePath: p.dirtyFilePath, host: p.host, CommandLine: p.CommandLine, commandLineWithTypingsFiles: p.commandLineWithTypingsFiles, Program: p.Program, ProgramUpdateKind: ProgramUpdateKindNone, ProgramLastUpdate: p.ProgramLastUpdate, programFilesWatch: p.programFilesWatch, failedLookupsWatch: p.failedLookupsWatch, affectingLocationsWatch: p.affectingLocationsWatch, typingsWatch: p.typingsWatch, checkerPool: p.checkerPool, installedTypingsInfo: p.installedTypingsInfo, typingsFiles: p.typingsFiles, } } // getCommandLineWithTypingsFiles returns the command line augmented with typing files if ATA is enabled. func (p *Project) getCommandLineWithTypingsFiles() *tsoptions.ParsedCommandLine { if len(p.typingsFiles) == 0 { return p.CommandLine } // Check if ATA is enabled for this project typeAcquisition := p.GetTypeAcquisition() if typeAcquisition == nil || !typeAcquisition.Enable.IsTrue() { return p.CommandLine } p.commandLineWithTypingsFilesOnce.Do(func() { if p.commandLineWithTypingsFiles == nil { // Create an augmented command line that includes typing files originalRootNames := p.CommandLine.FileNames() newRootNames := make([]string, 0, len(originalRootNames)+len(p.typingsFiles)) newRootNames = append(newRootNames, originalRootNames...) newRootNames = append(newRootNames, p.typingsFiles...) // Create a new ParsedCommandLine with the augmented root file names p.commandLineWithTypingsFiles = tsoptions.NewParsedCommandLine( p.CommandLine.CompilerOptions(), newRootNames, tspath.ComparePathsOptions{ UseCaseSensitiveFileNames: p.host.FS().UseCaseSensitiveFileNames(), CurrentDirectory: p.currentDirectory, }, ) } }) return p.commandLineWithTypingsFiles } type CreateProgramResult struct { Program *compiler.Program UpdateKind ProgramUpdateKind CheckerPool *checkerPool } func (p *Project) CreateProgram() CreateProgramResult { updateKind := ProgramUpdateKindNewFiles var programCloned bool var checkerPool *checkerPool var newProgram *compiler.Program // Create the command line, potentially augmented with typing files commandLine := p.getCommandLineWithTypingsFiles() if p.dirtyFilePath != "" && p.Program != nil && p.Program.CommandLine() == commandLine { newProgram, programCloned = p.Program.UpdateProgram(p.dirtyFilePath, p.host) if programCloned { updateKind = ProgramUpdateKindCloned for _, file := range newProgram.GetSourceFiles() { if file.Path() != p.dirtyFilePath { // UpdateProgram only called host.GetSourceFile for the dirty file. // Increment ref count for all other files. p.host.builder.parseCache.Ref(file) } } } } else { var typingsLocation string if p.GetTypeAcquisition().Enable.IsTrue() { typingsLocation = p.host.sessionOptions.TypingsLocation } newProgram = compiler.NewProgram( compiler.ProgramOptions{ Host: p.host, Config: commandLine, UseSourceOfProjectReference: true, TypingsLocation: typingsLocation, JSDocParsingMode: ast.JSDocParsingModeParseAll, CreateCheckerPool: func(program *compiler.Program) compiler.CheckerPool { checkerPool = newCheckerPool(4, program, p.log) return checkerPool }, }, ) } if !programCloned && p.Program != nil && p.Program.HasSameFileNames(newProgram) { updateKind = ProgramUpdateKindSameFileNames } newProgram.BindSourceFiles() return CreateProgramResult{ Program: newProgram, UpdateKind: updateKind, CheckerPool: checkerPool, } } func (p *Project) CloneWatchers(workspaceDir string, libDir string) (programFilesWatch *WatchedFiles[patternsAndIgnored], failedLookupsWatch *WatchedFiles[map[tspath.Path]string], affectingLocationsWatch *WatchedFiles[map[tspath.Path]string]) { failedLookups := make(map[tspath.Path]string) affectingLocations := make(map[tspath.Path]string) programFiles := getNonRootFileGlobs(workspaceDir, libDir, p.Program.GetSourceFiles(), p.CommandLine.FileNamesByPath(), tspath.ComparePathsOptions{ UseCaseSensitiveFileNames: p.host.FS().UseCaseSensitiveFileNames(), CurrentDirectory: p.currentDirectory, }) extractLookups(p.toPath, failedLookups, affectingLocations, p.Program.GetResolvedModules()) extractLookups(p.toPath, failedLookups, affectingLocations, p.Program.GetResolvedTypeReferenceDirectives()) programFilesWatch = p.programFilesWatch.Clone(programFiles) failedLookupsWatch = p.failedLookupsWatch.Clone(failedLookups) affectingLocationsWatch = p.affectingLocationsWatch.Clone(affectingLocations) return programFilesWatch, failedLookupsWatch, affectingLocationsWatch } func (p *Project) log(msg string) { // !!! } func (p *Project) toPath(fileName string) tspath.Path { return tspath.ToPath(fileName, p.currentDirectory, p.host.FS().UseCaseSensitiveFileNames()) } func (p *Project) print(writeFileNames bool, writeFileExplanation bool, builder *strings.Builder) string { builder.WriteString(fmt.Sprintf("\nProject '%s'\n", p.Name())) if p.Program == nil { builder.WriteString("\tFiles (0) NoProgram\n") } else { sourceFiles := p.Program.GetSourceFiles() builder.WriteString(fmt.Sprintf("\tFiles (%d)\n", len(sourceFiles))) if writeFileNames { for _, sourceFile := range sourceFiles { builder.WriteString("\t\t" + sourceFile.FileName() + "\n") } // !!! // if writeFileExplanation {} } } builder.WriteString(hr) return builder.String() } // GetTypeAcquisition returns the type acquisition settings for this project. func (p *Project) GetTypeAcquisition() *core.TypeAcquisition { if p.Kind == KindInferred { // For inferred projects, use default settings return &core.TypeAcquisition{ Enable: core.TSTrue, Include: nil, Exclude: nil, DisableFilenameBasedTypeAcquisition: core.TSFalse, } } if p.CommandLine != nil { return p.CommandLine.TypeAcquisition() } return nil } // GetUnresolvedImports extracts unresolved imports from this project's program. func (p *Project) GetUnresolvedImports() *collections.Set[string] { if p.Program == nil { return nil } return p.Program.GetUnresolvedImports() } // ShouldTriggerATA determines if ATA should be triggered for this project. func (p *Project) ShouldTriggerATA(snapshotID uint64) bool { if p.Program == nil || p.CommandLine == nil { return false } typeAcquisition := p.GetTypeAcquisition() if typeAcquisition == nil || !typeAcquisition.Enable.IsTrue() { return false } if p.installedTypingsInfo == nil || p.ProgramLastUpdate == snapshotID && p.ProgramUpdateKind == ProgramUpdateKindNewFiles { return true } return !p.installedTypingsInfo.Equals(p.ComputeTypingsInfo()) } func (p *Project) ComputeTypingsInfo() ata.TypingsInfo { return ata.TypingsInfo{ CompilerOptions: p.CommandLine.CompilerOptions(), TypeAcquisition: p.GetTypeAcquisition(), UnresolvedImports: p.GetUnresolvedImports(), } }