package incremental import ( "fmt" "maps" "reflect" "slices" "strings" "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/tsoptions" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" ) func snapshotToBuildInfo(snapshot *snapshot, program *compiler.Program, buildInfoFileName string) *BuildInfo { buildInfo := &BuildInfo{ Version: core.Version(), } to := &toBuildInfo{ snapshot: snapshot, program: program, buildInfo: buildInfo, buildInfoDirectory: tspath.GetDirectoryPath(buildInfoFileName), comparePathsOptions: tspath.ComparePathsOptions{ CurrentDirectory: program.GetCurrentDirectory(), UseCaseSensitiveFileNames: program.UseCaseSensitiveFileNames(), }, fileNameToFileId: make(map[string]BuildInfoFileId), fileNamesToFileIdListId: make(map[string]BuildInfoFileIdListId), roots: make(map[*ast.SourceFile]tspath.Path), } if snapshot.options.IsIncremental() { to.collectRootFiles() to.setFileInfoAndEmitSignatures() to.setRootOfIncrementalProgram() to.setCompilerOptions() to.setReferencedMap() to.setChangeFileSet() to.setSemanticDiagnostics() to.setEmitDiagnostics() to.setAffectedFilesPendingEmit() if snapshot.latestChangedDtsFile != "" { buildInfo.LatestChangedDtsFile = to.relativeToBuildInfo(snapshot.latestChangedDtsFile) } } else { to.setRootOfNonIncrementalProgram() } buildInfo.Errors = snapshot.hasErrors.IsTrue() buildInfo.SemanticErrors = snapshot.hasSemanticErrors buildInfo.CheckPending = snapshot.checkPending return buildInfo } type toBuildInfo struct { snapshot *snapshot program *compiler.Program buildInfo *BuildInfo buildInfoDirectory string comparePathsOptions tspath.ComparePathsOptions fileNameToFileId map[string]BuildInfoFileId fileNamesToFileIdListId map[string]BuildInfoFileIdListId roots map[*ast.SourceFile]tspath.Path } func (t *toBuildInfo) relativeToBuildInfo(path string) string { return tspath.EnsurePathIsNonModuleName(tspath.GetRelativePathFromDirectory(t.buildInfoDirectory, path, t.comparePathsOptions)) } func (t *toBuildInfo) toFileId(path tspath.Path) BuildInfoFileId { fileId := t.fileNameToFileId[string(path)] if fileId == 0 { if libFile := t.program.GetDefaultLibFile(path); libFile != nil && !libFile.Replaced { t.buildInfo.FileNames = append(t.buildInfo.FileNames, libFile.Name) } else { t.buildInfo.FileNames = append(t.buildInfo.FileNames, t.relativeToBuildInfo(string(path))) } fileId = BuildInfoFileId(len(t.buildInfo.FileNames)) t.fileNameToFileId[string(path)] = fileId } return fileId } func (t *toBuildInfo) toFileIdListId(set *collections.Set[tspath.Path]) BuildInfoFileIdListId { fileIds := core.Map(slices.Collect(maps.Keys(set.Keys())), t.toFileId) slices.Sort(fileIds) key := strings.Join(core.Map(fileIds, func(id BuildInfoFileId) string { return fmt.Sprintf("%d", id) }), ",") fileIdListId := t.fileNamesToFileIdListId[key] if fileIdListId == 0 { t.buildInfo.FileIdsList = append(t.buildInfo.FileIdsList, fileIds) fileIdListId = BuildInfoFileIdListId(len(t.buildInfo.FileIdsList)) t.fileNamesToFileIdListId[key] = fileIdListId } return fileIdListId } func (t *toBuildInfo) toRelativeToBuildInfoCompilerOptionValue(option *tsoptions.CommandLineOption, v any) any { if option.Kind == "list" { if option.Elements().IsFilePath { if arr, ok := v.([]string); ok { return core.Map(arr, t.relativeToBuildInfo) } } } else if option.IsFilePath { if str, ok := v.(string); ok && str != "" { return t.relativeToBuildInfo(v.(string)) } } return v } func (t *toBuildInfo) toBuildInfoDiagnosticsFromFileNameDiagnostics(diagnostics []*buildInfoDiagnosticWithFileName) []*BuildInfoDiagnostic { return core.Map(diagnostics, func(d *buildInfoDiagnosticWithFileName) *BuildInfoDiagnostic { var file BuildInfoFileId if d.file != "" { file = t.toFileId(d.file) } return &BuildInfoDiagnostic{ File: file, NoFile: d.noFile, Pos: d.pos, End: d.end, Code: d.code, Category: d.category, Message: d.message, MessageChain: t.toBuildInfoDiagnosticsFromFileNameDiagnostics(d.messageChain), RelatedInformation: t.toBuildInfoDiagnosticsFromFileNameDiagnostics(d.relatedInformation), ReportsUnnecessary: d.reportsUnnecessary, ReportsDeprecated: d.reportsDeprecated, SkippedOnNoEmit: d.skippedOnNoEmit, } }) } func (t *toBuildInfo) toBuildInfoDiagnosticsFromDiagnostics(filePath tspath.Path, diagnostics []*ast.Diagnostic) []*BuildInfoDiagnostic { return core.Map(diagnostics, func(d *ast.Diagnostic) *BuildInfoDiagnostic { var file BuildInfoFileId noFile := false if d.File() == nil { noFile = true } else if d.File().Path() != filePath { file = t.toFileId(d.File().Path()) } return &BuildInfoDiagnostic{ File: file, NoFile: noFile, Pos: d.Loc().Pos(), End: d.Loc().End(), Code: d.Code(), Category: d.Category(), Message: d.Message(), MessageChain: t.toBuildInfoDiagnosticsFromDiagnostics(filePath, d.MessageChain()), RelatedInformation: t.toBuildInfoDiagnosticsFromDiagnostics(filePath, d.RelatedInformation()), ReportsUnnecessary: d.ReportsUnnecessary(), ReportsDeprecated: d.ReportsDeprecated(), SkippedOnNoEmit: d.SkippedOnNoEmit(), } }) } func (t *toBuildInfo) toBuildInfoDiagnosticsOfFile(filePath tspath.Path, diags *diagnosticsOrBuildInfoDiagnosticsWithFileName) *BuildInfoDiagnosticsOfFile { if len(diags.diagnostics) > 0 { return &BuildInfoDiagnosticsOfFile{ FileId: t.toFileId(filePath), Diagnostics: t.toBuildInfoDiagnosticsFromDiagnostics(filePath, diags.diagnostics), } } if len(diags.buildInfoDiagnostics) > 0 { return &BuildInfoDiagnosticsOfFile{ FileId: t.toFileId(filePath), Diagnostics: t.toBuildInfoDiagnosticsFromFileNameDiagnostics(diags.buildInfoDiagnostics), } } return nil } func (t *toBuildInfo) collectRootFiles() { for _, fileName := range t.program.CommandLine().FileNames() { var file *ast.SourceFile if redirect := t.program.GetParseFileRedirect(fileName); redirect != "" { file = t.program.GetSourceFile(redirect) } else { file = t.program.GetSourceFile(fileName) } if file != nil { t.roots[file] = tspath.ToPath(fileName, t.comparePathsOptions.CurrentDirectory, t.comparePathsOptions.UseCaseSensitiveFileNames) } } } func (t *toBuildInfo) setFileInfoAndEmitSignatures() { t.buildInfo.FileInfos = core.MapNonNil(t.program.GetSourceFiles(), func(file *ast.SourceFile) *BuildInfoFileInfo { info, _ := t.snapshot.fileInfos.Load(file.Path()) fileId := t.toFileId(file.Path()) // tryAddRoot(key, fileId); if t.buildInfo.FileNames[fileId-1] != t.relativeToBuildInfo(string(file.Path())) { if libFile := t.program.GetDefaultLibFile(file.Path()); libFile == nil || libFile.Replaced || t.buildInfo.FileNames[fileId-1] != libFile.Name { panic(fmt.Sprintf("File name at index %d does not match expected relative path or libName: %s != %s", fileId-1, t.buildInfo.FileNames[fileId-1], t.relativeToBuildInfo(string(file.Path())))) } } if int(fileId) != len(t.buildInfo.FileNames) { // Duplicate - for now ignore return nil } if t.snapshot.options.Composite.IsTrue() { if !ast.IsJsonSourceFile(file) && t.program.SourceFileMayBeEmitted(file, false) { if emitSignature, loaded := t.snapshot.emitSignatures.Load(file.Path()); !loaded { t.buildInfo.EmitSignatures = append(t.buildInfo.EmitSignatures, &BuildInfoEmitSignature{ FileId: fileId, }) } else if emitSignature.signature != info.signature { incrementalEmitSignature := &BuildInfoEmitSignature{ FileId: fileId, } if emitSignature.signature != "" { incrementalEmitSignature.Signature = emitSignature.signature } else if emitSignature.signatureWithDifferentOptions[0] == info.signature { incrementalEmitSignature.DiffersOnlyInDtsMap = true } else { incrementalEmitSignature.Signature = emitSignature.signatureWithDifferentOptions[0] incrementalEmitSignature.DiffersInOptions = true } t.buildInfo.EmitSignatures = append(t.buildInfo.EmitSignatures, incrementalEmitSignature) } } } return newBuildInfoFileInfo(info) }) if t.buildInfo.FileInfos == nil { t.buildInfo.FileInfos = []*BuildInfoFileInfo{} } } func (t *toBuildInfo) setRootOfIncrementalProgram() { keys := slices.Collect(maps.Keys(t.roots)) slices.SortFunc(keys, func(a, b *ast.SourceFile) int { return int(t.toFileId(a.Path())) - int(t.toFileId(b.Path())) }) for _, file := range keys { root := t.toFileId(t.roots[file]) resolved := t.toFileId(file.Path()) if t.buildInfo.Root == nil { // First fileId as is t.buildInfo.Root = append(t.buildInfo.Root, &BuildInfoRoot{Start: resolved}) } else { last := t.buildInfo.Root[len(t.buildInfo.Root)-1] if last.End == resolved-1 { // If its [..., last = [start, end = fileId - 1]], update last to [start, fileId] last.End = resolved } else if last.End == 0 && last.Start == resolved-1 { // If its [..., last = start = fileId - 1 ], update last to [start, fileId] last.End = resolved } else { t.buildInfo.Root = append(t.buildInfo.Root, &BuildInfoRoot{Start: resolved}) } } if root != resolved { t.buildInfo.ResolvedRoot = append(t.buildInfo.ResolvedRoot, &BuildInfoResolvedRoot{ Resolved: resolved, Root: root, }) } } } func (t *toBuildInfo) setCompilerOptions() { tsoptions.ForEachCompilerOptionValue( t.snapshot.options, func(option *tsoptions.CommandLineOption) bool { return option.AffectsBuildInfo }, func(option *tsoptions.CommandLineOption, value reflect.Value, i int) bool { if value.IsZero() { return false } // Make it relative to buildInfo directory if file path if t.buildInfo.Options == nil { t.buildInfo.Options = &collections.OrderedMap[string, any]{} } t.buildInfo.Options.Set(option.Name, t.toRelativeToBuildInfoCompilerOptionValue(option, value.Interface())) return false }, ) } func (t *toBuildInfo) setReferencedMap() { keys := t.snapshot.referencedMap.getPathsWithReferences() slices.Sort(keys) t.buildInfo.ReferencedMap = core.Map(keys, func(filePath tspath.Path) *BuildInfoReferenceMapEntry { references, _ := t.snapshot.referencedMap.getReferences(filePath) return &BuildInfoReferenceMapEntry{ FileId: t.toFileId(filePath), FileIdListId: t.toFileIdListId(references), } }) } func (t *toBuildInfo) setChangeFileSet() { files := slices.Collect(t.snapshot.changedFilesSet.Keys()) slices.Sort(files) t.buildInfo.ChangeFileSet = core.Map(files, t.toFileId) } func (t *toBuildInfo) setSemanticDiagnostics() { for _, file := range t.program.GetSourceFiles() { value, ok := t.snapshot.semanticDiagnosticsPerFile.Load(file.Path()) if !ok { if !t.snapshot.changedFilesSet.Has(file.Path()) { t.buildInfo.SemanticDiagnosticsPerFile = append(t.buildInfo.SemanticDiagnosticsPerFile, &BuildInfoSemanticDiagnostic{ FileId: t.toFileId(file.Path()), }) } } else { diagnostics := t.toBuildInfoDiagnosticsOfFile(file.Path(), value) if diagnostics != nil { t.buildInfo.SemanticDiagnosticsPerFile = append(t.buildInfo.SemanticDiagnosticsPerFile, &BuildInfoSemanticDiagnostic{ Diagnostics: diagnostics, }) } } } } func (t *toBuildInfo) setEmitDiagnostics() { files := slices.Collect(t.snapshot.emitDiagnosticsPerFile.Keys()) slices.Sort(files) t.buildInfo.EmitDiagnosticsPerFile = core.Map(files, func(filePath tspath.Path) *BuildInfoDiagnosticsOfFile { value, _ := t.snapshot.emitDiagnosticsPerFile.Load(filePath) return t.toBuildInfoDiagnosticsOfFile(filePath, value) }) } func (t *toBuildInfo) setAffectedFilesPendingEmit() { files := slices.Collect(t.snapshot.affectedFilesPendingEmit.Keys()) slices.Sort(files) fullEmitKind := GetFileEmitKind(t.snapshot.options) for _, filePath := range files { file := t.program.GetSourceFileByPath(filePath) if file == nil || !t.program.SourceFileMayBeEmitted(file, false) { continue } pendingEmit, _ := t.snapshot.affectedFilesPendingEmit.Load(filePath) t.buildInfo.AffectedFilesPendingEmit = append(t.buildInfo.AffectedFilesPendingEmit, &BuildInfoFilePendingEmit{ FileId: t.toFileId(filePath), EmitKind: core.IfElse(pendingEmit == fullEmitKind, 0, pendingEmit), }) } } func (t *toBuildInfo) setRootOfNonIncrementalProgram() { t.buildInfo.Root = core.Map(t.program.CommandLine().FileNames(), func(fileName string) *BuildInfoRoot { return &BuildInfoRoot{ NonIncremental: t.relativeToBuildInfo(string(tspath.ToPath(fileName, t.comparePathsOptions.CurrentDirectory, t.comparePathsOptions.UseCaseSensitiveFileNames))), } }) }