324 lines
11 KiB
Go
324 lines
11 KiB
Go
package incremental
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"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/diagnostics"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
|
|
"github.com/zeebo/xxh3"
|
|
)
|
|
|
|
type fileInfo struct {
|
|
version string
|
|
signature string
|
|
affectsGlobalScope bool
|
|
impliedNodeFormat core.ResolutionMode
|
|
}
|
|
|
|
func (f *fileInfo) Version() string { return f.version }
|
|
func (f *fileInfo) Signature() string { return f.signature }
|
|
func (f *fileInfo) AffectsGlobalScope() bool { return f.affectsGlobalScope }
|
|
func (f *fileInfo) ImpliedNodeFormat() core.ResolutionMode { return f.impliedNodeFormat }
|
|
|
|
func ComputeHash(text string, hashWithText bool) string {
|
|
hashBytes := xxh3.Hash128([]byte(text)).Bytes()
|
|
hash := hex.EncodeToString(hashBytes[:])
|
|
if hashWithText {
|
|
hash += "-" + text
|
|
}
|
|
return hash
|
|
}
|
|
|
|
type FileEmitKind uint32
|
|
|
|
const (
|
|
FileEmitKindNone FileEmitKind = 0
|
|
FileEmitKindJs FileEmitKind = 1 << 0 // emit js file
|
|
FileEmitKindJsMap FileEmitKind = 1 << 1 // emit js.map file
|
|
FileEmitKindJsInlineMap FileEmitKind = 1 << 2 // emit inline source map in js file
|
|
FileEmitKindDtsErrors FileEmitKind = 1 << 3 // emit dts errors
|
|
FileEmitKindDtsEmit FileEmitKind = 1 << 4 // emit d.ts file
|
|
FileEmitKindDtsMap FileEmitKind = 1 << 5 // emit d.ts.map file
|
|
|
|
FileEmitKindDts = FileEmitKindDtsErrors | FileEmitKindDtsEmit
|
|
FileEmitKindAllJs = FileEmitKindJs | FileEmitKindJsMap | FileEmitKindJsInlineMap
|
|
FileEmitKindAllDtsEmit = FileEmitKindDtsEmit | FileEmitKindDtsMap
|
|
FileEmitKindAllDts = FileEmitKindDts | FileEmitKindDtsMap
|
|
FileEmitKindAll = FileEmitKindAllJs | FileEmitKindAllDts
|
|
)
|
|
|
|
func GetFileEmitKind(options *core.CompilerOptions) FileEmitKind {
|
|
result := FileEmitKindJs
|
|
if options.SourceMap.IsTrue() {
|
|
result |= FileEmitKindJsMap
|
|
}
|
|
if options.InlineSourceMap.IsTrue() {
|
|
result |= FileEmitKindJsInlineMap
|
|
}
|
|
if options.GetEmitDeclarations() {
|
|
result |= FileEmitKindDts
|
|
}
|
|
if options.DeclarationMap.IsTrue() {
|
|
result |= FileEmitKindDtsMap
|
|
}
|
|
if options.EmitDeclarationOnly.IsTrue() {
|
|
result &= FileEmitKindAllDts
|
|
}
|
|
return result
|
|
}
|
|
|
|
func getPendingEmitKindWithOptions(options *core.CompilerOptions, oldOptions *core.CompilerOptions) FileEmitKind {
|
|
oldEmitKind := GetFileEmitKind(oldOptions)
|
|
newEmitKind := GetFileEmitKind(options)
|
|
return getPendingEmitKind(newEmitKind, oldEmitKind)
|
|
}
|
|
|
|
func getPendingEmitKind(emitKind FileEmitKind, oldEmitKind FileEmitKind) FileEmitKind {
|
|
if oldEmitKind == emitKind {
|
|
return FileEmitKindNone
|
|
}
|
|
if oldEmitKind == 0 || emitKind == 0 {
|
|
return emitKind
|
|
}
|
|
diff := oldEmitKind ^ emitKind
|
|
result := FileEmitKindNone
|
|
// If there is diff in Js emit, pending emit is js emit flags
|
|
if (diff & FileEmitKindAllJs) != 0 {
|
|
result |= emitKind & FileEmitKindAllJs
|
|
}
|
|
// If dts errors pending, add dts errors flag
|
|
if (diff & FileEmitKindDtsErrors) != 0 {
|
|
result |= emitKind & FileEmitKindAllDts
|
|
}
|
|
// If there is diff in Dts emit, pending emit is dts emit flags
|
|
if (diff & FileEmitKindAllDtsEmit) != 0 {
|
|
result |= emitKind & FileEmitKindAllDtsEmit
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Signature (Hash of d.ts emitted), is string if it was emitted using same d.ts.map option as what compilerOptions indicate,
|
|
// otherwise tuple of string
|
|
type emitSignature struct {
|
|
signature string
|
|
signatureWithDifferentOptions []string
|
|
}
|
|
|
|
// Covert to Emit signature based on oldOptions and EmitSignature format
|
|
// If d.ts map options differ then swap the format, otherwise use as is
|
|
func (e *emitSignature) getNewEmitSignature(oldOptions *core.CompilerOptions, newOptions *core.CompilerOptions) *emitSignature {
|
|
if oldOptions.DeclarationMap.IsTrue() == newOptions.DeclarationMap.IsTrue() {
|
|
return e
|
|
}
|
|
if e.signatureWithDifferentOptions == nil {
|
|
return &emitSignature{
|
|
signatureWithDifferentOptions: []string{e.signature},
|
|
}
|
|
} else {
|
|
return &emitSignature{
|
|
signature: e.signatureWithDifferentOptions[0],
|
|
}
|
|
}
|
|
}
|
|
|
|
type buildInfoDiagnosticWithFileName struct {
|
|
// filename if it is for a File thats other than its stored for
|
|
file tspath.Path
|
|
noFile bool
|
|
pos int
|
|
end int
|
|
code int32
|
|
category diagnostics.Category
|
|
message string
|
|
messageChain []*buildInfoDiagnosticWithFileName
|
|
relatedInformation []*buildInfoDiagnosticWithFileName
|
|
reportsUnnecessary bool
|
|
reportsDeprecated bool
|
|
skippedOnNoEmit bool
|
|
}
|
|
|
|
type diagnosticsOrBuildInfoDiagnosticsWithFileName struct {
|
|
diagnostics []*ast.Diagnostic
|
|
buildInfoDiagnostics []*buildInfoDiagnosticWithFileName
|
|
}
|
|
|
|
func (b *buildInfoDiagnosticWithFileName) toDiagnostic(p *compiler.Program, file *ast.SourceFile) *ast.Diagnostic {
|
|
var fileForDiagnostic *ast.SourceFile
|
|
if b.file != "" {
|
|
fileForDiagnostic = p.GetSourceFileByPath(b.file)
|
|
} else if !b.noFile {
|
|
fileForDiagnostic = file
|
|
}
|
|
var messageChain []*ast.Diagnostic
|
|
for _, msg := range b.messageChain {
|
|
messageChain = append(messageChain, msg.toDiagnostic(p, fileForDiagnostic))
|
|
}
|
|
var relatedInformation []*ast.Diagnostic
|
|
for _, info := range b.relatedInformation {
|
|
relatedInformation = append(relatedInformation, info.toDiagnostic(p, fileForDiagnostic))
|
|
}
|
|
return ast.NewDiagnosticWith(
|
|
fileForDiagnostic,
|
|
core.NewTextRange(b.pos, b.end),
|
|
b.code,
|
|
b.category,
|
|
b.message,
|
|
messageChain,
|
|
relatedInformation,
|
|
b.reportsUnnecessary,
|
|
b.reportsDeprecated,
|
|
b.skippedOnNoEmit,
|
|
)
|
|
}
|
|
|
|
func (d *diagnosticsOrBuildInfoDiagnosticsWithFileName) getDiagnostics(p *compiler.Program, file *ast.SourceFile) []*ast.Diagnostic {
|
|
if d.diagnostics != nil {
|
|
return d.diagnostics
|
|
}
|
|
// Convert and cache the diagnostics
|
|
d.diagnostics = core.Map(d.buildInfoDiagnostics, func(diag *buildInfoDiagnosticWithFileName) *ast.Diagnostic {
|
|
return diag.toDiagnostic(p, file)
|
|
})
|
|
return d.diagnostics
|
|
}
|
|
|
|
type snapshot struct {
|
|
// These are the fields that get serialized
|
|
|
|
// Information of the file eg. its version, signature etc
|
|
fileInfos collections.SyncMap[tspath.Path, *fileInfo]
|
|
options *core.CompilerOptions
|
|
// Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled
|
|
referencedMap referenceMap
|
|
// Cache of semantic diagnostics for files with their Path being the key
|
|
semanticDiagnosticsPerFile collections.SyncMap[tspath.Path, *diagnosticsOrBuildInfoDiagnosticsWithFileName]
|
|
// Cache of dts emit diagnostics for files with their Path being the key
|
|
emitDiagnosticsPerFile collections.SyncMap[tspath.Path, *diagnosticsOrBuildInfoDiagnosticsWithFileName]
|
|
// The map has key by source file's path that has been changed
|
|
changedFilesSet collections.SyncSet[tspath.Path]
|
|
// Files pending to be emitted
|
|
affectedFilesPendingEmit collections.SyncMap[tspath.Path, FileEmitKind]
|
|
// Name of the file whose dts was the latest to change
|
|
latestChangedDtsFile string
|
|
// Hash of d.ts emitted for the file, use to track when emit of d.ts changes
|
|
emitSignatures collections.SyncMap[tspath.Path, *emitSignature]
|
|
// Recorded if program had errors that need to be reported even with --noCheck
|
|
hasErrors core.Tristate
|
|
// Recorded if program had semantic errors only for non incremental build
|
|
hasSemanticErrors bool
|
|
// If semantic diagnostic check is pending
|
|
checkPending bool
|
|
|
|
// Additional fields that are not serialized but needed to track state
|
|
|
|
// true if build info emit is pending
|
|
buildInfoEmitPending atomic.Bool
|
|
hasErrorsFromOldState core.Tristate
|
|
hasSemanticErrorsFromOldState bool
|
|
allFilesExcludingDefaultLibraryFileOnce sync.Once
|
|
// Cache of all files excluding default library file for the current program
|
|
allFilesExcludingDefaultLibraryFile []*ast.SourceFile
|
|
hasChangedDtsFile bool
|
|
hasEmitDiagnostics bool
|
|
|
|
// Used with testing to add text of hash for better comparison
|
|
hashWithText bool
|
|
}
|
|
|
|
func (s *snapshot) addFileToChangeSet(filePath tspath.Path) {
|
|
s.changedFilesSet.Add(filePath)
|
|
s.buildInfoEmitPending.Store(true)
|
|
}
|
|
|
|
func (s *snapshot) addFileToAffectedFilesPendingEmit(filePath tspath.Path, emitKind FileEmitKind) {
|
|
existingKind, _ := s.affectedFilesPendingEmit.Load(filePath)
|
|
s.affectedFilesPendingEmit.Store(filePath, existingKind|emitKind)
|
|
if emitKind&FileEmitKindDtsErrors != 0 {
|
|
s.emitDiagnosticsPerFile.Delete(filePath)
|
|
}
|
|
s.buildInfoEmitPending.Store(true)
|
|
}
|
|
|
|
func (s *snapshot) getAllFilesExcludingDefaultLibraryFile(program *compiler.Program, firstSourceFile *ast.SourceFile) []*ast.SourceFile {
|
|
s.allFilesExcludingDefaultLibraryFileOnce.Do(func() {
|
|
files := program.GetSourceFiles()
|
|
s.allFilesExcludingDefaultLibraryFile = make([]*ast.SourceFile, 0, len(files))
|
|
addSourceFile := func(file *ast.SourceFile) {
|
|
if !program.IsSourceFileDefaultLibrary(file.Path()) {
|
|
s.allFilesExcludingDefaultLibraryFile = append(s.allFilesExcludingDefaultLibraryFile, file)
|
|
}
|
|
}
|
|
if firstSourceFile != nil {
|
|
addSourceFile(firstSourceFile)
|
|
}
|
|
for _, file := range files {
|
|
if file != firstSourceFile {
|
|
addSourceFile(file)
|
|
}
|
|
}
|
|
})
|
|
return s.allFilesExcludingDefaultLibraryFile
|
|
}
|
|
|
|
func getTextHandlingSourceMapForSignature(text string, data *compiler.WriteFileData) string {
|
|
if data.SourceMapUrlPos != -1 {
|
|
return text[:data.SourceMapUrlPos]
|
|
}
|
|
return text
|
|
}
|
|
|
|
func (s *snapshot) computeSignatureWithDiagnostics(file *ast.SourceFile, text string, data *compiler.WriteFileData) string {
|
|
var builder strings.Builder
|
|
builder.WriteString(getTextHandlingSourceMapForSignature(text, data))
|
|
for _, diag := range data.Diagnostics {
|
|
diagnosticToStringBuilder(diag, file, &builder)
|
|
}
|
|
return s.computeHash(builder.String())
|
|
}
|
|
|
|
func diagnosticToStringBuilder(diagnostic *ast.Diagnostic, file *ast.SourceFile, builder *strings.Builder) {
|
|
if diagnostic == nil {
|
|
return
|
|
}
|
|
builder.WriteString("\n")
|
|
if diagnostic.File() != file {
|
|
builder.WriteString(tspath.EnsurePathIsNonModuleName(tspath.GetRelativePathFromDirectory(
|
|
tspath.GetDirectoryPath(string(file.Path())),
|
|
string(diagnostic.File().Path()),
|
|
tspath.ComparePathsOptions{},
|
|
)))
|
|
}
|
|
if diagnostic.File() != nil {
|
|
builder.WriteString(fmt.Sprintf("(%d,%d): ", diagnostic.Pos(), diagnostic.Len()))
|
|
}
|
|
builder.WriteString(diagnostic.Category().Name())
|
|
builder.WriteString(fmt.Sprintf("%d: ", diagnostic.Code()))
|
|
builder.WriteString(diagnostic.Message())
|
|
for _, chain := range diagnostic.MessageChain() {
|
|
diagnosticToStringBuilder(chain, file, builder)
|
|
}
|
|
for _, info := range diagnostic.RelatedInformation() {
|
|
diagnosticToStringBuilder(info, file, builder)
|
|
}
|
|
}
|
|
|
|
func (s *snapshot) computeHash(text string) string {
|
|
return ComputeHash(text, s.hashWithText)
|
|
}
|
|
|
|
func (s *snapshot) canUseIncrementalState() bool {
|
|
if !s.options.IsIncremental() && s.options.Build.IsTrue() {
|
|
// If not incremental build (with tsc -b), we don't need to track state except diagnostics per file so we can use it
|
|
return false
|
|
}
|
|
return true
|
|
}
|