2025-10-15 10:12:44 +03:00

585 lines
20 KiB
Go

package incremental
import (
"fmt"
"iter"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnostics"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tsoptions"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
)
type (
BuildInfoFileId int
BuildInfoFileIdListId int
)
// buildInfoRoot is
// - for incremental program buildinfo
// - start and end of FileId for consecutive fileIds to be included as root
// - start - single fileId that is root
//
// - for non incremental program buildinfo
// - string that is the root file name
type BuildInfoRoot struct {
Start BuildInfoFileId
End BuildInfoFileId
NonIncremental string // Root of a non incremental program
}
func (b *BuildInfoRoot) MarshalJSON() ([]byte, error) {
if b.Start != 0 {
if b.End != 0 {
return json.Marshal([2]BuildInfoFileId{b.Start, b.End})
} else {
return json.Marshal(b.Start)
}
} else {
return json.Marshal(b.NonIncremental)
}
}
func (b *BuildInfoRoot) UnmarshalJSON(data []byte) error {
var startAndEnd *[2]int
if err := json.Unmarshal(data, &startAndEnd); err != nil {
var start int
if err := json.Unmarshal(data, &start); err != nil {
var name string
if err := json.Unmarshal(data, &name); err != nil {
return fmt.Errorf("invalid BuildInfoRoot: %s", data)
}
*b = BuildInfoRoot{
NonIncremental: name,
}
return nil
}
*b = BuildInfoRoot{
Start: BuildInfoFileId(start),
}
return nil
}
*b = BuildInfoRoot{
Start: BuildInfoFileId(startAndEnd[0]),
End: BuildInfoFileId(startAndEnd[1]),
}
return nil
}
type buildInfoFileInfoNoSignature struct {
Version string `json:"version,omitzero"`
NoSignature bool `json:"noSignature,omitzero"`
AffectsGlobalScope bool `json:"affectsGlobalScope,omitzero"`
ImpliedNodeFormat core.ResolutionMode `json:"impliedNodeFormat,omitzero"`
}
// Signature is
// - undefined if FileInfo.version === FileInfo.signature
// - string actual signature
type buildInfoFileInfoWithSignature struct {
Version string `json:"version,omitzero"`
Signature string `json:"signature,omitzero"`
AffectsGlobalScope bool `json:"affectsGlobalScope,omitzero"`
ImpliedNodeFormat core.ResolutionMode `json:"impliedNodeFormat,omitzero"`
}
type BuildInfoFileInfo struct {
signature string
noSignature *buildInfoFileInfoNoSignature
fileInfo *buildInfoFileInfoWithSignature
}
func newBuildInfoFileInfo(fileInfo *fileInfo) *BuildInfoFileInfo {
if fileInfo.version == fileInfo.signature {
if !fileInfo.affectsGlobalScope && fileInfo.impliedNodeFormat == core.ResolutionModeCommonJS {
return &BuildInfoFileInfo{signature: fileInfo.signature}
}
} else if fileInfo.signature == "" {
return &BuildInfoFileInfo{noSignature: &buildInfoFileInfoNoSignature{
Version: fileInfo.version,
NoSignature: true,
AffectsGlobalScope: fileInfo.affectsGlobalScope,
ImpliedNodeFormat: fileInfo.impliedNodeFormat,
}}
}
return &BuildInfoFileInfo{fileInfo: &buildInfoFileInfoWithSignature{
Version: fileInfo.version,
Signature: core.IfElse(fileInfo.signature == fileInfo.version, "", fileInfo.signature),
AffectsGlobalScope: fileInfo.affectsGlobalScope,
ImpliedNodeFormat: fileInfo.impliedNodeFormat,
}}
}
func (b *BuildInfoFileInfo) GetFileInfo() *fileInfo {
if b == nil {
return nil
}
if b.signature != "" {
return &fileInfo{
version: b.signature,
signature: b.signature,
impliedNodeFormat: core.ResolutionModeCommonJS,
}
}
if b.noSignature != nil {
return &fileInfo{
version: b.noSignature.Version,
affectsGlobalScope: b.noSignature.AffectsGlobalScope,
impliedNodeFormat: b.noSignature.ImpliedNodeFormat,
}
}
return &fileInfo{
version: b.fileInfo.Version,
signature: core.IfElse(b.fileInfo.Signature == "", b.fileInfo.Version, b.fileInfo.Signature),
affectsGlobalScope: b.fileInfo.AffectsGlobalScope,
impliedNodeFormat: b.fileInfo.ImpliedNodeFormat,
}
}
func (b *BuildInfoFileInfo) HasSignature() bool {
return b.signature != ""
}
func (b *BuildInfoFileInfo) MarshalJSON() ([]byte, error) {
if b.signature != "" {
return json.Marshal(b.signature)
}
if b.noSignature != nil {
return json.Marshal(b.noSignature)
}
return json.Marshal(b.fileInfo)
}
func (b *BuildInfoFileInfo) UnmarshalJSON(data []byte) error {
var vSignature string
if err := json.Unmarshal(data, &vSignature); err != nil {
var noSignature buildInfoFileInfoNoSignature
if err := json.Unmarshal(data, &noSignature); err != nil || !noSignature.NoSignature {
var fileInfo buildInfoFileInfoWithSignature
if err := json.Unmarshal(data, &fileInfo); err != nil {
return fmt.Errorf("invalid BuildInfoFileInfo: %s", data)
}
*b = BuildInfoFileInfo{fileInfo: &fileInfo}
return nil
}
*b = BuildInfoFileInfo{noSignature: &noSignature}
return nil
}
*b = BuildInfoFileInfo{signature: vSignature}
return nil
}
type BuildInfoReferenceMapEntry struct {
FileId BuildInfoFileId
FileIdListId BuildInfoFileIdListId
}
func (b *BuildInfoReferenceMapEntry) MarshalJSON() ([]byte, error) {
return json.Marshal([2]int{int(b.FileId), int(b.FileIdListId)})
}
func (b *BuildInfoReferenceMapEntry) UnmarshalJSON(data []byte) error {
var v *[2]int
if err := json.Unmarshal(data, &v); err != nil {
return err
}
*b = BuildInfoReferenceMapEntry{
FileId: BuildInfoFileId(v[0]),
FileIdListId: BuildInfoFileIdListId(v[1]),
}
return nil
}
type BuildInfoDiagnostic struct {
// BuildInfoFileId if it is for a File thats other than its stored for
File BuildInfoFileId `json:"file,omitzero"`
NoFile bool `json:"noFile,omitzero"`
Pos int `json:"pos,omitzero"`
End int `json:"end,omitzero"`
Code int32 `json:"code,omitzero"`
Category diagnostics.Category `json:"category,omitzero"`
Message string `json:"message,omitzero"`
MessageChain []*BuildInfoDiagnostic `json:"messageChain,omitzero"`
RelatedInformation []*BuildInfoDiagnostic `json:"relatedInformation,omitzero"`
ReportsUnnecessary bool `json:"reportsUnnecessary,omitzero"`
ReportsDeprecated bool `json:"reportsDeprecated,omitzero"`
SkippedOnNoEmit bool `json:"skippedOnNoEmit,omitzero"`
}
type BuildInfoDiagnosticsOfFile struct {
FileId BuildInfoFileId
Diagnostics []*BuildInfoDiagnostic
}
func (b *BuildInfoDiagnosticsOfFile) MarshalJSON() ([]byte, error) {
fileIdAndDiagnostics := make([]any, 0, 2)
fileIdAndDiagnostics = append(fileIdAndDiagnostics, b.FileId)
fileIdAndDiagnostics = append(fileIdAndDiagnostics, b.Diagnostics)
return json.Marshal(fileIdAndDiagnostics)
}
func (b *BuildInfoDiagnosticsOfFile) UnmarshalJSON(data []byte) error {
var fileIdAndDiagnostics []jsontext.Value
if err := json.Unmarshal(data, &fileIdAndDiagnostics); err != nil {
return fmt.Errorf("invalid BuildInfoDiagnosticsOfFile: %s", data)
}
if len(fileIdAndDiagnostics) != 2 {
return fmt.Errorf("invalid BuildInfoDiagnosticsOfFile: expected 2 elements, got %d", len(fileIdAndDiagnostics))
}
var fileId BuildInfoFileId
if err := json.Unmarshal(fileIdAndDiagnostics[0], &fileId); err != nil {
return fmt.Errorf("invalid fileId in BuildInfoDiagnosticsOfFile: %w", err)
}
var diagnostics []*BuildInfoDiagnostic
if err := json.Unmarshal(fileIdAndDiagnostics[1], &diagnostics); err != nil {
return fmt.Errorf("invalid diagnostics in BuildInfoDiagnosticsOfFile: %w", err)
}
*b = BuildInfoDiagnosticsOfFile{
FileId: fileId,
Diagnostics: diagnostics,
}
return nil
}
type BuildInfoSemanticDiagnostic struct {
FileId BuildInfoFileId // File is not in changedSet and still doesnt have cached diagnostics
Diagnostics *BuildInfoDiagnosticsOfFile // Diagnostics for file
}
func (b *BuildInfoSemanticDiagnostic) MarshalJSON() ([]byte, error) {
if b.FileId != 0 {
return json.Marshal(b.FileId)
}
return json.Marshal(b.Diagnostics)
}
func (b *BuildInfoSemanticDiagnostic) UnmarshalJSON(data []byte) error {
var fileId BuildInfoFileId
if err := json.Unmarshal(data, &fileId); err != nil {
var diagnostics BuildInfoDiagnosticsOfFile
if err := json.Unmarshal(data, &diagnostics); err != nil {
return fmt.Errorf("invalid BuildInfoSemanticDiagnostic: %s", data)
}
*b = BuildInfoSemanticDiagnostic{
Diagnostics: &diagnostics,
}
return nil
}
*b = BuildInfoSemanticDiagnostic{
FileId: fileId,
}
return nil
}
// fileId if pending emit is same as what compilerOptions suggest
// [fileId] if pending emit is only dts file emit
// [fileId, emitKind] if any other type emit is pending
type BuildInfoFilePendingEmit struct {
FileId BuildInfoFileId
EmitKind FileEmitKind
}
func (b *BuildInfoFilePendingEmit) MarshalJSON() ([]byte, error) {
if b.EmitKind == 0 {
return json.Marshal(b.FileId)
}
if b.EmitKind == FileEmitKindDts {
fileListIds := []BuildInfoFileId{b.FileId}
return json.Marshal(fileListIds)
}
fileAndEmitKind := []int{int(b.FileId), int(b.EmitKind)}
return json.Marshal(fileAndEmitKind)
}
func (b *BuildInfoFilePendingEmit) UnmarshalJSON(data []byte) error {
var fileId BuildInfoFileId
if err := json.Unmarshal(data, &fileId); err != nil {
var intTuple []int
if err := json.Unmarshal(data, &intTuple); err != nil || len(intTuple) == 0 {
return fmt.Errorf("invalid BuildInfoFilePendingEmit: %s", data)
}
switch len(intTuple) {
case 1:
*b = BuildInfoFilePendingEmit{
FileId: BuildInfoFileId(intTuple[0]),
EmitKind: FileEmitKindDts,
}
return nil
case 2:
*b = BuildInfoFilePendingEmit{
FileId: BuildInfoFileId(intTuple[0]),
EmitKind: FileEmitKind(intTuple[1]),
}
return nil
default:
return fmt.Errorf("invalid BuildInfoFilePendingEmit: expected 1 or 2 integers, got %d", len(intTuple))
}
}
*b = BuildInfoFilePendingEmit{
FileId: fileId,
}
return nil
}
// [fileId, signature] if different from file's signature
// fileId if file wasnt emitted
type BuildInfoEmitSignature struct {
FileId BuildInfoFileId
Signature string // Signature if it is different from file's Signature
DiffersOnlyInDtsMap bool // true if signature is different only in dtsMap value
DiffersInOptions bool // true if signature is different in options used to emit file
}
func (b *BuildInfoEmitSignature) noEmitSignature() bool {
return b.Signature == "" && !b.DiffersOnlyInDtsMap && !b.DiffersInOptions
}
func (b *BuildInfoEmitSignature) toEmitSignature(path tspath.Path, emitSignatures *collections.SyncMap[tspath.Path, *emitSignature]) *emitSignature {
var signature string
var signatureWithDifferentOptions []string
if b.DiffersOnlyInDtsMap {
signatureWithDifferentOptions = make([]string, 0, 1)
info, _ := emitSignatures.Load(path)
signatureWithDifferentOptions = append(signatureWithDifferentOptions, info.signature)
} else if b.DiffersInOptions {
signatureWithDifferentOptions = make([]string, 0, 1)
signatureWithDifferentOptions = append(signatureWithDifferentOptions, b.Signature)
} else {
signature = b.Signature
}
return &emitSignature{
signature: signature,
signatureWithDifferentOptions: signatureWithDifferentOptions,
}
}
func (b *BuildInfoEmitSignature) MarshalJSON() ([]byte, error) {
if b.noEmitSignature() {
return json.Marshal(b.FileId)
}
fileIdAndSignature := make([]any, 2)
fileIdAndSignature[0] = b.FileId
var signature any
if b.DiffersOnlyInDtsMap {
signature = []string{}
} else if b.DiffersInOptions {
signature = []string{b.Signature}
} else {
signature = b.Signature
}
fileIdAndSignature[1] = signature
return json.Marshal(fileIdAndSignature)
}
func (b *BuildInfoEmitSignature) UnmarshalJSON(data []byte) error {
var fileId BuildInfoFileId
if err := json.Unmarshal(data, &fileId); err != nil {
var fileIdAndSignature []any
if err := json.Unmarshal(data, &fileIdAndSignature); err != nil {
return fmt.Errorf("invalid BuildInfoEmitSignature: %s", data)
}
if len(fileIdAndSignature) != 2 {
return fmt.Errorf("invalid BuildInfoEmitSignature: expected 2 elements, got %d", len(fileIdAndSignature))
}
var fileId BuildInfoFileId
if id, ok := fileIdAndSignature[0].(float64); !ok {
return fmt.Errorf("invalid fileId in BuildInfoEmitSignature: expected float64, got %T", fileIdAndSignature[0])
} else {
fileId = BuildInfoFileId(id)
}
var signature string
var differsOnlyInDtsMap, differsInOptions bool
if signatureV, ok := fileIdAndSignature[1].(string); !ok {
if signatureList, ok := fileIdAndSignature[1].([]any); !ok {
return fmt.Errorf("invalid signature in BuildInfoEmitSignature: expected string or []string, got %T", fileIdAndSignature[1])
} else {
switch len(signatureList) {
case 0:
differsOnlyInDtsMap = true
case 1:
if sig, ok := signatureList[0].(string); !ok {
return fmt.Errorf("invalid signature in BuildInfoEmitSignature: expected string, got %T", signatureList[0])
} else {
signature = sig
differsInOptions = true
}
default:
return fmt.Errorf("invalid signature in BuildInfoEmitSignature: expected string or []string with 0 or 1 element, got %d elements", len(signatureList))
}
}
} else {
signature = signatureV
}
*b = BuildInfoEmitSignature{
FileId: fileId,
Signature: signature,
DiffersOnlyInDtsMap: differsOnlyInDtsMap,
DiffersInOptions: differsInOptions,
}
return nil
}
*b = BuildInfoEmitSignature{
FileId: fileId,
}
return nil
}
type BuildInfoResolvedRoot struct {
Resolved BuildInfoFileId
Root BuildInfoFileId
}
func (b *BuildInfoResolvedRoot) MarshalJSON() ([]byte, error) {
return json.Marshal([2]BuildInfoFileId{b.Resolved, b.Root})
}
func (b *BuildInfoResolvedRoot) UnmarshalJSON(data []byte) error {
var resolvedAndRoot [2]int
if err := json.Unmarshal(data, &resolvedAndRoot); err != nil {
return fmt.Errorf("invalid BuildInfoResolvedRoot: %s", data)
}
*b = BuildInfoResolvedRoot{
Resolved: BuildInfoFileId(resolvedAndRoot[0]),
Root: BuildInfoFileId(resolvedAndRoot[1]),
}
return nil
}
type BuildInfo struct {
Version string `json:"version,omitzero"`
// Common between incremental and tsc -b buildinfo for non incremental programs
Errors bool `json:"errors,omitzero"`
CheckPending bool `json:"checkPending,omitzero"`
Root []*BuildInfoRoot `json:"root,omitzero"`
// IncrementalProgram info
FileNames []string `json:"fileNames,omitzero"`
FileInfos []*BuildInfoFileInfo `json:"fileInfos,omitzero"`
FileIdsList [][]BuildInfoFileId `json:"fileIdsList,omitzero"`
Options *collections.OrderedMap[string, any] `json:"options,omitzero"`
ReferencedMap []*BuildInfoReferenceMapEntry `json:"referencedMap,omitzero"`
SemanticDiagnosticsPerFile []*BuildInfoSemanticDiagnostic `json:"semanticDiagnosticsPerFile,omitzero"`
EmitDiagnosticsPerFile []*BuildInfoDiagnosticsOfFile `json:"emitDiagnosticsPerFile,omitzero"`
ChangeFileSet []BuildInfoFileId `json:"changeFileSet,omitzero"`
AffectedFilesPendingEmit []*BuildInfoFilePendingEmit `json:"affectedFilesPendingEmit,omitzero"`
LatestChangedDtsFile string `json:"latestChangedDtsFile,omitzero"` // Because this is only output file in the program, we dont need fileId to deduplicate name
EmitSignatures []*BuildInfoEmitSignature `json:"emitSignatures,omitzero"`
ResolvedRoot []*BuildInfoResolvedRoot `json:"resolvedRoot,omitzero"`
// NonIncrementalProgram info
SemanticErrors bool `json:"semanticErrors,omitzero"`
}
func (b *BuildInfo) IsValidVersion() bool {
return b.Version == core.Version()
}
func (b *BuildInfo) IsIncremental() bool {
return b != nil && len(b.FileNames) != 0
}
func (b *BuildInfo) fileName(fileId BuildInfoFileId) string {
return b.FileNames[fileId-1]
}
func (b *BuildInfo) fileInfo(fileId BuildInfoFileId) *BuildInfoFileInfo {
return b.FileInfos[fileId-1]
}
func (b *BuildInfo) GetCompilerOptions(buildInfoDirectory string) *core.CompilerOptions {
options := &core.CompilerOptions{}
for option, value := range b.Options.Entries() {
if buildInfoDirectory != "" {
result, ok := tsoptions.ConvertOptionToAbsolutePath(option, value, tsoptions.CommandLineCompilerOptionsMap, buildInfoDirectory)
if ok {
tsoptions.ParseCompilerOptions(option, result, options)
continue
}
}
tsoptions.ParseCompilerOptions(option, value, options)
}
return options
}
func (b *BuildInfo) IsEmitPending(resolved *tsoptions.ParsedCommandLine, buildInfoDirectory string) bool {
// Some of the emit files like source map or dts etc are not yet done
if !resolved.CompilerOptions().NoEmit.IsTrue() || resolved.CompilerOptions().GetEmitDeclarations() {
pendingEmit := getPendingEmitKindWithOptions(resolved.CompilerOptions(), b.GetCompilerOptions(buildInfoDirectory))
if resolved.CompilerOptions().NoEmit.IsTrue() {
pendingEmit &= FileEmitKindDtsErrors
}
return pendingEmit != 0
}
return false
}
func (b *BuildInfo) GetBuildInfoRootInfoReader(buildInfoDirectory string, comparePathOptions tspath.ComparePathsOptions) *BuildInfoRootInfoReader {
resolvedRootFileInfos := make(map[tspath.Path]*BuildInfoFileInfo, len(b.FileNames))
// Roots of the File
rootToResolved := collections.NewOrderedMapWithSizeHint[tspath.Path, tspath.Path](len(b.FileNames))
resolvedToRoot := make(map[tspath.Path]tspath.Path, len(b.ResolvedRoot))
toPath := func(fileName string) tspath.Path {
return tspath.ToPath(fileName, buildInfoDirectory, comparePathOptions.UseCaseSensitiveFileNames)
}
// Create map from resolvedRoot to Root
for _, resolved := range b.ResolvedRoot {
resolvedToRoot[toPath(b.fileName(resolved.Resolved))] = toPath(b.fileName(resolved.Root))
}
addRoot := func(resolvedRoot string, fileInfo *BuildInfoFileInfo) {
resolvedRootPath := toPath(resolvedRoot)
if rootPath, ok := resolvedToRoot[resolvedRootPath]; ok {
rootToResolved.Set(rootPath, resolvedRootPath)
} else {
rootToResolved.Set(resolvedRootPath, resolvedRootPath)
}
if fileInfo != nil {
resolvedRootFileInfos[resolvedRootPath] = fileInfo
}
}
for _, root := range b.Root {
if root.NonIncremental != "" {
addRoot(root.NonIncremental, nil)
} else if root.End == 0 {
addRoot(b.fileName(root.Start), b.fileInfo(root.Start))
} else {
for i := root.Start; i <= root.End; i++ {
addRoot(b.fileName(i), b.fileInfo(i))
}
}
}
return &BuildInfoRootInfoReader{
resolvedRootFileInfos: resolvedRootFileInfos,
rootToResolved: rootToResolved,
}
}
type BuildInfoRootInfoReader struct {
resolvedRootFileInfos map[tspath.Path]*BuildInfoFileInfo
rootToResolved *collections.OrderedMap[tspath.Path, tspath.Path]
}
func (b *BuildInfoRootInfoReader) GetBuildInfoFileInfo(inputFilePath tspath.Path) (*BuildInfoFileInfo, tspath.Path) {
if info, ok := b.resolvedRootFileInfos[inputFilePath]; ok {
return info, inputFilePath
}
if resolved, ok := b.rootToResolved.Get(inputFilePath); ok {
return b.resolvedRootFileInfos[resolved], resolved
}
return nil, ""
}
func (b *BuildInfoRootInfoReader) Roots() iter.Seq[tspath.Path] {
return b.rootToResolved.Keys()
}