vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View File

@@ -0,0 +1,393 @@
package incremental
import (
"context"
"maps"
"slices"
"sync"
"sync/atomic"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tspath"
)
type dtsMayChange map[tspath.Path]FileEmitKind
func (c dtsMayChange) addFileToAffectedFilesPendingEmit(filePath tspath.Path, emitKind FileEmitKind) {
c[filePath] = emitKind
}
type updatedSignature struct {
mu sync.Mutex
signature string
kind SignatureUpdateKind
}
type affectedFilesHandler struct {
ctx context.Context
program *Program
hasAllFilesExcludingDefaultLibraryFile atomic.Bool
updatedSignatures collections.SyncMap[tspath.Path, *updatedSignature]
dtsMayChange []dtsMayChange
filesToRemoveDiagnostics collections.SyncSet[tspath.Path]
cleanedDiagnosticsOfLibFiles sync.Once
seenFileAndReferences collections.SyncMap[tspath.Path, bool]
}
func (h *affectedFilesHandler) getDtsMayChange(affectedFilePath tspath.Path, affectedFileEmitKind FileEmitKind) dtsMayChange {
result := dtsMayChange(map[tspath.Path]FileEmitKind{affectedFilePath: affectedFileEmitKind})
h.dtsMayChange = append(h.dtsMayChange, result)
return result
}
func (h *affectedFilesHandler) isChangedSignature(path tspath.Path) bool {
newSignature, _ := h.updatedSignatures.Load(path)
// This method is called after updating signatures of that path, so signature is present in updatedSignatures
// And is already calculated, so no need to lock and unlock mutex on the entry
oldInfo, _ := h.program.snapshot.fileInfos.Load(path)
return newSignature.signature != oldInfo.signature
}
func (h *affectedFilesHandler) removeSemanticDiagnosticsOf(path tspath.Path) {
h.filesToRemoveDiagnostics.Add(path)
}
func (h *affectedFilesHandler) removeDiagnosticsOfLibraryFiles() {
h.cleanedDiagnosticsOfLibFiles.Do(func() {
for _, file := range h.program.GetSourceFiles() {
if h.program.program.IsSourceFileDefaultLibrary(file.Path()) && !h.program.program.SkipTypeChecking(file, true) {
h.removeSemanticDiagnosticsOf(file.Path())
}
}
})
}
func (h *affectedFilesHandler) computeDtsSignature(file *ast.SourceFile) string {
var signature string
h.program.program.Emit(h.ctx, compiler.EmitOptions{
TargetSourceFile: file,
EmitOnly: compiler.EmitOnlyForcedDts,
WriteFile: func(fileName string, text string, data *compiler.WriteFileData) error {
if !tspath.IsDeclarationFileName(fileName) {
panic("File extension for signature expected to be dts, got : " + fileName)
}
signature = h.program.snapshot.computeSignatureWithDiagnostics(file, text, data)
return nil
},
})
return signature
}
func (h *affectedFilesHandler) updateShapeSignature(file *ast.SourceFile, useFileVersionAsSignature bool) bool {
update := &updatedSignature{}
update.mu.Lock()
defer update.mu.Unlock()
// If we have cached the result for this file, that means hence forth we should assume file shape is uptodate
if existing, ok := h.updatedSignatures.LoadOrStore(file.Path(), update); ok {
// Ensure calculations for existing ones are complete before using the value
existing.mu.Lock()
defer existing.mu.Unlock()
return false
}
info, _ := h.program.snapshot.fileInfos.Load(file.Path())
prevSignature := info.signature
if !file.IsDeclarationFile && !useFileVersionAsSignature {
update.signature = h.computeDtsSignature(file)
}
// Default is to use file version as signature
if update.signature == "" {
update.signature = info.version
update.kind = SignatureUpdateKindUsedVersion
}
return update.signature != prevSignature
}
func (h *affectedFilesHandler) getFilesAffectedBy(path tspath.Path) []*ast.SourceFile {
file := h.program.program.GetSourceFileByPath(path)
if file == nil {
return nil
}
if !h.updateShapeSignature(file, false) {
return []*ast.SourceFile{file}
}
if info, _ := h.program.snapshot.fileInfos.Load(file.Path()); info.affectsGlobalScope {
h.hasAllFilesExcludingDefaultLibraryFile.Store(true)
h.program.snapshot.getAllFilesExcludingDefaultLibraryFile(h.program.program, file)
}
if h.program.snapshot.options.IsolatedModules.IsTrue() {
return []*ast.SourceFile{file}
}
// Now we need to if each file in the referencedBy list has a shape change as well.
// Because if so, its own referencedBy files need to be saved as well to make the
// emitting result consistent with files on disk.
seenFileNamesMap := h.forEachFileReferencedBy(
file,
func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool) {
// If the current file is not nil and has a shape change, we need to queue it for processing
if currentFile != nil && h.updateShapeSignature(currentFile, false) {
return true, false
}
return false, false
},
)
// Return array of values that needs emit
return core.Filter(slices.Collect(maps.Values(seenFileNamesMap)), func(file *ast.SourceFile) bool {
return file != nil
})
}
func (h *affectedFilesHandler) forEachFileReferencedBy(file *ast.SourceFile, fn func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool)) map[tspath.Path]*ast.SourceFile {
// Now we need to if each file in the referencedBy list has a shape change as well.
// Because if so, its own referencedBy files need to be saved as well to make the
// emitting result consistent with files on disk.
seenFileNamesMap := map[tspath.Path]*ast.SourceFile{}
// Start with the paths this file was referenced by
seenFileNamesMap[file.Path()] = file
queue := slices.Collect(h.program.snapshot.referencedMap.getReferencedBy(file.Path()))
for len(queue) > 0 {
currentPath := queue[len(queue)-1]
queue = queue[:len(queue)-1]
if _, ok := seenFileNamesMap[currentPath]; !ok {
currentFile := h.program.program.GetSourceFileByPath(currentPath)
seenFileNamesMap[currentPath] = currentFile
queueForFile, fastReturn := fn(currentFile, currentPath)
if fastReturn {
return seenFileNamesMap
}
if queueForFile {
for ref := range h.program.snapshot.referencedMap.getReferencedBy(currentFile.Path()) {
queue = append(queue, ref)
}
}
}
}
return seenFileNamesMap
}
// Handles semantic diagnostics and dts emit for affectedFile and files, that are referencing modules that export entities from affected file
// This is because even though js emit doesnt change, dts emit / type used can change resulting in need for dts emit and js change
func (h *affectedFilesHandler) handleDtsMayChangeOfAffectedFile(dtsMayChange dtsMayChange, affectedFile *ast.SourceFile) {
h.removeSemanticDiagnosticsOf(affectedFile.Path())
// If affected files is everything except default library, then nothing more to do
if h.hasAllFilesExcludingDefaultLibraryFile.Load() {
h.removeDiagnosticsOfLibraryFiles()
// When a change affects the global scope, all files are considered to be affected without updating their signature
// That means when affected file is handled, its signature can be out of date
// To avoid this, ensure that we update the signature for any affected file in this scenario.
h.updateShapeSignature(affectedFile, false)
return
}
if h.program.snapshot.options.AssumeChangesOnlyAffectDirectDependencies.IsTrue() {
return
}
// Iterate on referencing modules that export entities from affected file and delete diagnostics and add pending emit
// If there was change in signature (dts output) for the changed file,
// then only we need to handle pending file emit
if !h.program.snapshot.changedFilesSet.Has(affectedFile.Path()) ||
!h.isChangedSignature(affectedFile.Path()) {
return
}
// At this point affectedFile is actually one of the changed files
// that has some change in its .d.ts signature.
// Since isolated modules dont change js files, files affected by change in signature is itself
// But we need to cleanup semantic diagnostics and queue dts emit for affected files
if h.program.snapshot.options.IsolatedModules.IsTrue() {
h.forEachFileReferencedBy(
affectedFile,
func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool) {
if h.handleDtsMayChangeOfGlobalScope(dtsMayChange, currentPath /*invalidateJsFiles*/, false) {
return false, true
}
h.handleDtsMayChangeOf(dtsMayChange, currentPath /*invalidateJsFiles*/, false)
if h.isChangedSignature(currentPath) {
return true, false
}
return false, false
},
)
}
invalidateJsFiles := false
var typeChecker *checker.Checker
var done func()
// If exported const enum, we need to ensure that js files are emitted as well since the const enum value changed
if affectedFile.Symbol != nil {
for _, exported := range affectedFile.Symbol.Exports {
if exported.Flags&ast.SymbolFlagsConstEnum != 0 {
invalidateJsFiles = true
break
}
if typeChecker == nil {
typeChecker, done = h.program.program.GetTypeCheckerForFileExclusive(h.ctx, affectedFile)
}
aliased := checker.SkipAlias(exported, typeChecker)
if aliased == exported {
continue
}
if (aliased.Flags & ast.SymbolFlagsConstEnum) != 0 {
if slices.ContainsFunc(aliased.Declarations, func(d *ast.Node) bool {
return ast.GetSourceFileOfNode(d) == affectedFile
}) {
invalidateJsFiles = true
break
}
}
}
}
if done != nil {
done()
}
// Go through files that reference affected file and handle dts emit and semantic diagnostics for them and their references
for fileReferencingChangedFile := range h.program.snapshot.referencedMap.getReferencedBy(affectedFile.Path()) {
if h.handleDtsMayChangeOfGlobalScope(dtsMayChange, fileReferencingChangedFile, invalidateJsFiles) {
return
}
// Since references of changed file = affected files - we would have already handled d.ts emit and semantic diagnostics
// for those files. Now we need to handle files referencing those affected files to ensure correctness.
for fileReferencingAffectedFile := range h.program.snapshot.referencedMap.getReferencedBy(fileReferencingChangedFile) {
if h.handleDtsMayChangeOfFileAndReferences(dtsMayChange, fileReferencingAffectedFile, invalidateJsFiles) {
return
}
}
}
}
func (h *affectedFilesHandler) handleDtsMayChangeOfFileAndReferences(dtsMayChange dtsMayChange, filePath tspath.Path, invalidateJsFiles bool) bool {
if existing, loaded := h.seenFileAndReferences.LoadOrStore(filePath, invalidateJsFiles); loaded && (existing || !invalidateJsFiles) {
return false
} else if loaded && invalidateJsFiles {
h.seenFileAndReferences.Store(filePath, true)
}
if h.handleDtsMayChangeOfGlobalScope(dtsMayChange, filePath, invalidateJsFiles) {
return true
}
h.handleDtsMayChangeOf(dtsMayChange, filePath, invalidateJsFiles)
// Remove the diagnostics of files that import this file and
// any files that are referenced by it (directly or indirectly)
for referencingFilePath := range h.program.snapshot.referencedMap.getReferencedBy(filePath) {
if h.handleDtsMayChangeOfFileAndReferences(dtsMayChange, referencingFilePath, invalidateJsFiles) {
return true
}
}
return false
}
func (h *affectedFilesHandler) handleDtsMayChangeOfGlobalScope(dtsMayChange dtsMayChange, filePath tspath.Path, invalidateJsFiles bool) bool {
if info, ok := h.program.snapshot.fileInfos.Load(filePath); !ok || !info.affectsGlobalScope {
return false
}
// Every file needs to be handled
for _, file := range h.program.snapshot.getAllFilesExcludingDefaultLibraryFile(h.program.program, nil) {
h.handleDtsMayChangeOf(dtsMayChange, file.Path(), invalidateJsFiles)
}
h.removeDiagnosticsOfLibraryFiles()
return true
}
// Handle the dts may change, so they need to be added to pending emit if dts emit is enabled,
// Also we need to make sure signature is updated for these files
func (h *affectedFilesHandler) handleDtsMayChangeOf(dtsMayChange dtsMayChange, path tspath.Path, invalidateJsFiles bool) {
if h.program.snapshot.changedFilesSet.Has(path) {
return
}
file := h.program.program.GetSourceFileByPath(path)
if file == nil {
return
}
h.removeSemanticDiagnosticsOf(path)
// Even though the js emit doesnt change and we are already handling dts emit and semantic diagnostics
// we need to update the signature to reflect correctness of the signature(which is output d.ts emit) of this file
// This ensures that we dont later during incremental builds considering wrong signature.
// Eg where this also is needed to ensure that .tsbuildinfo generated by incremental build should be same as if it was first fresh build
// But we avoid expensive full shape computation, as using file version as shape is enough for correctness.
h.updateShapeSignature(file, true)
// If not dts emit, nothing more to do
if invalidateJsFiles {
dtsMayChange.addFileToAffectedFilesPendingEmit(path, GetFileEmitKind(h.program.snapshot.options))
} else if h.program.snapshot.options.GetEmitDeclarations() {
dtsMayChange.addFileToAffectedFilesPendingEmit(path, core.IfElse(h.program.snapshot.options.DeclarationMap.IsTrue(), FileEmitKindAllDts, FileEmitKindDts))
}
}
func (h *affectedFilesHandler) updateSnapshot() {
if h.ctx.Err() != nil {
return
}
h.updatedSignatures.Range(func(filePath tspath.Path, update *updatedSignature) bool {
if info, ok := h.program.snapshot.fileInfos.Load(filePath); ok {
info.signature = update.signature
if h.program.testingData != nil {
h.program.testingData.UpdatedSignatureKinds[filePath] = update.kind
}
}
return true
})
h.filesToRemoveDiagnostics.Range(func(file tspath.Path) bool {
h.program.snapshot.semanticDiagnosticsPerFile.Delete(file)
return true
})
for _, change := range h.dtsMayChange {
for filePath, emitKind := range change {
h.program.snapshot.addFileToAffectedFilesPendingEmit(filePath, emitKind)
}
}
h.program.snapshot.changedFilesSet = collections.SyncSet[tspath.Path]{}
h.program.snapshot.buildInfoEmitPending.Store(true)
}
func collectAllAffectedFiles(ctx context.Context, program *Program) {
if program.snapshot.changedFilesSet.Size() == 0 {
return
}
handler := affectedFilesHandler{ctx: ctx, program: program}
wg := core.NewWorkGroup(handler.program.program.SingleThreaded())
var result collections.SyncSet[*ast.SourceFile]
program.snapshot.changedFilesSet.Range(func(file tspath.Path) bool {
wg.Queue(func() {
for _, affectedFile := range handler.getFilesAffectedBy(file) {
result.Add(affectedFile)
}
})
return true
})
wg.RunAndWait()
if ctx.Err() != nil {
return
}
// For all the affected files, get all the files that would need to change their dts or js files,
// update their diagnostics
wg = core.NewWorkGroup(program.program.SingleThreaded())
emitKind := GetFileEmitKind(program.snapshot.options)
result.Range(func(file *ast.SourceFile) bool {
// remove the cached semantic diagnostics and handle dts emit and js emit if needed
dtsMayChange := handler.getDtsMayChange(file.Path(), emitKind)
wg.Queue(func() {
handler.handleDtsMayChangeOfAffectedFile(dtsMayChange, file)
})
return true
})
wg.RunAndWait()
// Update the snapshot with the new state
handler.updateSnapshot()
}

View File

@@ -0,0 +1,630 @@
package incremental
import (
"fmt"
"iter"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
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"`
MessageKey diagnostics.Key `json:"messageKey,omitzero"`
MessageArgs []string `json:"messageArgs,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"`
RepopulateInfo *BuildInfoRepopulateInfo `json:"repopulateInfo,omitzero"`
}
type BuildInfoRepopulateInfo struct {
Kind ast.RepopulateDiagnosticKind `json:"kind"`
ModuleReference string `json:"moduleReference,omitzero"`
Mode core.ResolutionMode `json:"mode,omitzero"`
PackageName string `json:"packageName,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 []json.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"`
PackageJsons []string `json:"packageJsons,omitzero"`
MissingPackageJsons []string `json:"missingPackageJsons,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 IsBuildInfoFileNameDefaultLibrary(fileName string) bool {
return !tspath.PathIsRelative(fileName) && !tspath.PathIsAbsolute(fileName)
}
func (b *BuildInfo) fileName(fileId BuildInfoFileId) string {
if fileId < 1 || int(fileId) > len(b.FileNames) {
return ""
}
return b.FileNames[fileId-1]
}
func (b *BuildInfo) fileInfo(fileId BuildInfoFileId) *BuildInfoFileInfo {
if fileId < 1 || int(fileId) > len(b.FileInfos) {
return nil
}
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) GetPackageJsons(buildInfoDirectory string) iter.Seq[string] {
return getNormalizedPaths(b.PackageJsons, buildInfoDirectory)
}
func (b *BuildInfo) GetMissingPackageJsons(buildInfoDirectory string) iter.Seq[string] {
return getNormalizedPaths(b.MissingPackageJsons, buildInfoDirectory)
}
func getNormalizedPaths(paths []string, buildInfoDirectory string) iter.Seq[string] {
return func(yield func(string) bool) {
for _, path := range paths {
if !yield(tspath.GetNormalizedAbsolutePath(path, buildInfoDirectory)) {
return
}
}
}
}
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 {
resolvedRoot := b.fileName(resolved.Resolved)
root := b.fileName(resolved.Root)
if resolvedRoot != "" && root != "" {
resolvedToRoot[toPath(resolvedRoot)] = toPath(root)
}
}
addRoot := func(resolvedRoot string, fileInfo *BuildInfoFileInfo) {
if resolvedRoot == "" {
return
}
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()
}

View File

@@ -0,0 +1,198 @@
package incremental
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
func buildInfoToSnapshot(buildInfo *BuildInfo, config *tsoptions.ParsedCommandLine, host compiler.CompilerHost) *snapshot {
to := &toSnapshot{
buildInfo: buildInfo,
buildInfoDirectory: tspath.GetDirectoryPath(tspath.GetNormalizedAbsolutePath(config.GetBuildInfoFileName(), config.GetCurrentDirectory())),
filePaths: make([]tspath.Path, 0, len(buildInfo.FileNames)),
filePathSet: make([]*collections.Set[tspath.Path], 0, len(buildInfo.FileIdsList)),
}
to.filePaths = core.Map(buildInfo.FileNames, func(fileName string) tspath.Path {
if IsBuildInfoFileNameDefaultLibrary(fileName) {
return tspath.ToPath(tspath.CombinePaths(host.DefaultLibraryPath(), fileName), host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames())
}
return tspath.ToPath(fileName, to.buildInfoDirectory, config.UseCaseSensitiveFileNames())
})
to.filePathSet = core.Map(buildInfo.FileIdsList, func(fileIdList []BuildInfoFileId) *collections.Set[tspath.Path] {
fileSet := collections.NewSetWithSizeHint[tspath.Path](len(fileIdList))
for _, fileId := range fileIdList {
fileSet.Add(to.toFilePath(fileId))
}
return fileSet
})
to.setCompilerOptions()
to.setFileInfoAndEmitSignatures()
to.setReferencedMap()
to.setChangeFileSet()
to.setSemanticDiagnostics()
to.setEmitDiagnostics()
to.setAffectedFilesPendingEmit()
if buildInfo.LatestChangedDtsFile != "" {
to.snapshot.latestChangedDtsFile = to.toAbsolutePath(buildInfo.LatestChangedDtsFile)
}
to.snapshot.hasErrors = core.IfElse(buildInfo.Errors, core.TSTrue, core.TSFalse)
to.snapshot.hasSemanticErrors = buildInfo.SemanticErrors
to.snapshot.checkPending = buildInfo.CheckPending
to.setPackageJsons()
return &to.snapshot
}
type toSnapshot struct {
buildInfo *BuildInfo
buildInfoDirectory string
snapshot snapshot
filePaths []tspath.Path
filePathSet []*collections.Set[tspath.Path]
}
func (t *toSnapshot) toAbsolutePath(path string) string {
return tspath.GetNormalizedAbsolutePath(path, t.buildInfoDirectory)
}
func (t *toSnapshot) toFilePath(fileId BuildInfoFileId) tspath.Path {
return t.filePaths[fileId-1]
}
func (t *toSnapshot) toFilePathSet(fileIdListId BuildInfoFileIdListId) *collections.Set[tspath.Path] {
return t.filePathSet[fileIdListId-1]
}
func (t *toSnapshot) toBuildInfoDiagnosticsWithFileName(diagnostics []*BuildInfoDiagnostic) []*buildInfoDiagnosticWithFileName {
return core.Map(diagnostics, func(d *BuildInfoDiagnostic) *buildInfoDiagnosticWithFileName {
var file tspath.Path
if d.File != 0 {
file = t.toFilePath(d.File)
}
return &buildInfoDiagnosticWithFileName{
file: file,
noFile: d.NoFile,
pos: d.Pos,
end: d.End,
code: d.Code,
category: d.Category,
messageKey: d.MessageKey,
messageArgs: d.MessageArgs,
messageChain: t.toBuildInfoDiagnosticsWithFileName(d.MessageChain),
relatedInformation: t.toBuildInfoDiagnosticsWithFileName(d.RelatedInformation),
reportsUnnecessary: d.ReportsUnnecessary,
reportsDeprecated: d.ReportsDeprecated,
skippedOnNoEmit: d.SkippedOnNoEmit,
repopulateInfo: fromBuildInfoRepopulateInfo(d.RepopulateInfo),
}
})
}
func (t *toSnapshot) toDiagnosticsOrBuildInfoDiagnosticsWithFileName(dig *BuildInfoDiagnosticsOfFile) *DiagnosticsOrBuildInfoDiagnosticsWithFileName {
return &DiagnosticsOrBuildInfoDiagnosticsWithFileName{
buildInfoDiagnostics: t.toBuildInfoDiagnosticsWithFileName(dig.Diagnostics),
}
}
func fromBuildInfoRepopulateInfo(info *BuildInfoRepopulateInfo) *ast.RepopulateDiagnosticInfo {
if info == nil {
return nil
}
return &ast.RepopulateDiagnosticInfo{
Kind: info.Kind,
ModuleReference: info.ModuleReference,
Mode: info.Mode,
PackageName: info.PackageName,
}
}
func (t *toSnapshot) setCompilerOptions() {
t.snapshot.options = t.buildInfo.GetCompilerOptions(t.buildInfoDirectory)
}
func (t *toSnapshot) setFileInfoAndEmitSignatures() {
isComposite := t.snapshot.options.Composite.IsTrue()
for index, buildInfoFileInfo := range t.buildInfo.FileInfos {
path := t.toFilePath(BuildInfoFileId(index + 1))
info := buildInfoFileInfo.GetFileInfo()
t.snapshot.fileInfos.Store(path, info)
// Add default emit signature as file's signature
if info.signature != "" && isComposite {
t.snapshot.emitSignatures.Store(path, &emitSignature{signature: info.signature})
}
}
// Fix up emit signatures
for _, value := range t.buildInfo.EmitSignatures {
if value.noEmitSignature() {
t.snapshot.emitSignatures.Delete(t.toFilePath(value.FileId))
} else {
path := t.toFilePath(value.FileId)
t.snapshot.emitSignatures.Store(path, value.toEmitSignature(path, &t.snapshot.emitSignatures))
}
}
}
func (t *toSnapshot) setReferencedMap() {
for _, entry := range t.buildInfo.ReferencedMap {
t.snapshot.referencedMap.storeReferences(t.toFilePath(entry.FileId), t.toFilePathSet(entry.FileIdListId))
}
}
func (t *toSnapshot) setChangeFileSet() {
for _, fileId := range t.buildInfo.ChangeFileSet {
filePath := t.toFilePath(fileId)
t.snapshot.changedFilesSet.Add(filePath)
}
}
func (t *toSnapshot) setSemanticDiagnostics() {
t.snapshot.fileInfos.Range(func(path tspath.Path, info *FileInfo) bool {
// Initialize to have no diagnostics if its not changed file
if !t.snapshot.changedFilesSet.Has(path) {
t.snapshot.semanticDiagnosticsPerFile.Store(path, &DiagnosticsOrBuildInfoDiagnosticsWithFileName{})
}
return true
})
for _, diagnostic := range t.buildInfo.SemanticDiagnosticsPerFile {
if diagnostic.FileId != 0 {
filePath := t.toFilePath(diagnostic.FileId)
t.snapshot.semanticDiagnosticsPerFile.Delete(filePath) // does not have cached diagnostics
} else {
filePath := t.toFilePath(diagnostic.Diagnostics.FileId)
t.snapshot.semanticDiagnosticsPerFile.Store(filePath, t.toDiagnosticsOrBuildInfoDiagnosticsWithFileName(diagnostic.Diagnostics))
}
}
}
func (t *toSnapshot) setEmitDiagnostics() {
for _, diagnostic := range t.buildInfo.EmitDiagnosticsPerFile {
filePath := t.toFilePath(diagnostic.FileId)
t.snapshot.emitDiagnosticsPerFile.Store(filePath, t.toDiagnosticsOrBuildInfoDiagnosticsWithFileName(diagnostic))
}
}
func (t *toSnapshot) setAffectedFilesPendingEmit() {
if len(t.buildInfo.AffectedFilesPendingEmit) == 0 {
return
}
ownOptionsEmitKind := GetFileEmitKind(t.snapshot.options)
for _, pendingEmit := range t.buildInfo.AffectedFilesPendingEmit {
t.snapshot.affectedFilesPendingEmit.Store(t.toFilePath(pendingEmit.FileId), core.IfElse(pendingEmit.EmitKind == 0, ownOptionsEmitKind, pendingEmit.EmitKind))
}
}
func (t *toSnapshot) setPackageJsons() {
if t.buildInfo.PackageJsons != nil {
t.snapshot.packageJsons = core.Map(t.buildInfo.PackageJsons, t.toAbsolutePath)
} else {
t.snapshot.packageJsons = make([]string, 0)
}
if t.buildInfo.MissingPackageJsons != nil {
t.snapshot.missingPackageJsons = core.Map(t.buildInfo.MissingPackageJsons, t.toAbsolutePath)
} else {
t.snapshot.missingPackageJsons = make([]string, 0)
}
}

View File

@@ -0,0 +1,343 @@
package incremental
import (
"context"
"sync/atomic"
"time"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tspath"
)
type emitUpdate struct {
pendingKind FileEmitKind
result *compiler.EmitResult
dtsErrorsFromCache bool
}
type emitFilesHandler struct {
ctx context.Context
program *Program
isForDtsErrors bool
signatures collections.SyncMap[tspath.Path, string]
emitSignatures collections.SyncMap[tspath.Path, *emitSignature]
latestChangedDtsFiles collections.SyncMap[tspath.Path, string]
deletedPendingKinds collections.Set[tspath.Path]
emitUpdates collections.SyncMap[tspath.Path, *emitUpdate]
hasEmitDiagnostics atomic.Bool
}
// Determining what all is pending to be emitted based on previous options or previous file emit flags
func (h *emitFilesHandler) getPendingEmitKindForEmitOptions(emitKind FileEmitKind, options compiler.EmitOptions) FileEmitKind {
pendingKind := getPendingEmitKind(emitKind, 0)
if options.EmitOnly == compiler.EmitOnlyDts {
pendingKind &= FileEmitKindAllDts
}
if h.isForDtsErrors {
pendingKind &= FileEmitKindDtsErrors
}
return pendingKind
}
// Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete
// The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host
// in that order would be used to write the files
func (h *emitFilesHandler) emitAllAffectedFiles(options compiler.EmitOptions) *compiler.EmitResult {
// Emit all affected files
if h.program.snapshot.canUseIncrementalState() {
results := h.emitFilesIncremental(options)
if h.isForDtsErrors {
if options.TargetSourceFile != nil {
// Result from cache
diagnostics, _ := h.program.snapshot.emitDiagnosticsPerFile.Load(options.TargetSourceFile.Path())
result := &compiler.EmitResult{
EmitSkipped: true,
Diagnostics: diagnostics.getDiagnostics(h.program.program, options.TargetSourceFile),
}
h.updateHasEmitDiagnostics(result)
return result
}
for _, result := range results {
h.updateHasEmitDiagnostics(result)
}
return compiler.CombineEmitResults(results)
} else {
// Combine results and update buildInfo
result := compiler.CombineEmitResults(results)
h.updateHasEmitDiagnostics(result)
h.emitBuildInfo(options, result)
return result
}
} else if !h.isForDtsErrors {
result := h.program.program.Emit(h.ctx, h.getEmitOptions(options))
h.updateHasEmitDiagnostics(result)
h.updateSnapshot()
h.emitBuildInfo(options, result)
return result
} else {
result := &compiler.EmitResult{
EmitSkipped: true,
Diagnostics: h.program.program.GetDeclarationDiagnostics(h.ctx, options.TargetSourceFile),
}
if len(result.Diagnostics) != 0 {
h.updateHasEmitDiagnostics(result)
h.program.snapshot.hasEmitDiagnostics = true
}
return result
}
}
func (h *emitFilesHandler) updateHasEmitDiagnostics(result *compiler.EmitResult) {
if result != nil && len(result.Diagnostics) != 0 {
h.hasEmitDiagnostics.Store(true)
}
}
func (h *emitFilesHandler) emitBuildInfo(options compiler.EmitOptions, result *compiler.EmitResult) {
buildInfoResult := h.program.emitBuildInfo(h.ctx, options)
if buildInfoResult != nil {
result.Diagnostics = append(result.Diagnostics, buildInfoResult.Diagnostics...)
result.EmittedFiles = append(result.EmittedFiles, buildInfoResult.EmittedFiles...)
}
}
func (h *emitFilesHandler) emitFilesIncremental(options compiler.EmitOptions) []*compiler.EmitResult {
// Get all affected files
collectAllAffectedFiles(h.ctx, h.program)
if h.ctx.Err() != nil {
return nil
}
wg := core.NewWorkGroup(h.program.program.SingleThreaded())
h.program.snapshot.affectedFilesPendingEmit.Range(func(path tspath.Path, emitKind FileEmitKind) bool {
affectedFile := h.program.program.GetSourceFileByPath(path)
if affectedFile == nil || !h.program.program.SourceFileMayBeEmitted(affectedFile, false) {
h.deletedPendingKinds.Add(path)
return true
}
pendingKind := h.getPendingEmitKindForEmitOptions(emitKind, options)
if pendingKind != 0 {
wg.Queue(func() {
// Determine if we can do partial emit
var emitOnly compiler.EmitOnly
if (pendingKind & FileEmitKindAllJs) != 0 {
emitOnly = compiler.EmitOnlyJs
}
if (pendingKind & FileEmitKindAllDts) != 0 {
if emitOnly == compiler.EmitOnlyJs {
emitOnly = compiler.EmitAll
} else {
emitOnly = compiler.EmitOnlyDts
}
}
var result *compiler.EmitResult
if !h.isForDtsErrors {
result = h.program.program.Emit(h.ctx, h.getEmitOptions(compiler.EmitOptions{
TargetSourceFile: affectedFile,
EmitOnly: emitOnly,
WriteFile: options.WriteFile,
}))
} else {
result = &compiler.EmitResult{
EmitSkipped: true,
Diagnostics: h.program.program.GetDeclarationDiagnostics(h.ctx, affectedFile),
}
}
h.updateHasEmitDiagnostics(result)
// Update the pendingEmit for the file
h.emitUpdates.Store(path, &emitUpdate{pendingKind: getPendingEmitKind(emitKind, pendingKind), result: result})
})
}
return true
})
wg.RunAndWait()
if h.ctx.Err() != nil {
return nil
}
// Get updated errors that were not included in affected files emit
h.program.snapshot.emitDiagnosticsPerFile.Range(func(path tspath.Path, diagnostics *DiagnosticsOrBuildInfoDiagnosticsWithFileName) bool {
if _, ok := h.emitUpdates.Load(path); !ok {
affectedFile := h.program.program.GetSourceFileByPath(path)
if affectedFile == nil || !h.program.program.SourceFileMayBeEmitted(affectedFile, false) {
h.deletedPendingKinds.Add(path)
return true
}
pendingKind, _ := h.program.snapshot.affectedFilesPendingEmit.Load(path)
h.emitUpdates.Store(path, &emitUpdate{
pendingKind: pendingKind,
result: &compiler.EmitResult{
EmitSkipped: true,
Diagnostics: diagnostics.getDiagnostics(h.program.program, affectedFile),
},
dtsErrorsFromCache: true,
})
}
return true
})
return h.updateSnapshot()
}
func (h *emitFilesHandler) getEmitOptions(options compiler.EmitOptions) compiler.EmitOptions {
if !h.program.snapshot.options.GetEmitDeclarations() {
return options
}
canUseIncrementalState := h.program.snapshot.canUseIncrementalState()
return compiler.EmitOptions{
TargetSourceFile: options.TargetSourceFile,
EmitOnly: options.EmitOnly,
WriteFile: func(fileName string, text string, data *compiler.WriteFileData) error {
var differsOnlyInMap bool
if tspath.IsDeclarationFileName(fileName) {
if canUseIncrementalState {
var emitSignature string
info, _ := h.program.snapshot.fileInfos.Load(options.TargetSourceFile.Path())
if info.signature == info.version {
signature := h.program.snapshot.computeSignatureWithDiagnostics(options.TargetSourceFile, text, data)
// With d.ts diagnostics they are also part of the signature so emitSignature will be different from it since its just hash of d.ts
if len(data.Diagnostics) == 0 {
emitSignature = signature
}
if signature != info.version { // Update it
h.signatures.Store(options.TargetSourceFile.Path(), signature)
}
}
// Store d.ts emit hash so later can be compared to check if d.ts has changed.
// Currently we do this only for composite projects since these are the only projects that can be referenced by other projects
// and would need their d.ts change time in --build mode
if h.skipDtsOutputOfComposite(options.TargetSourceFile, fileName, text, data, emitSignature, &differsOnlyInMap) {
return nil
}
}
}
var aTime time.Time
if differsOnlyInMap {
aTime = h.program.host.GetMTime(fileName)
}
var err error
if options.WriteFile != nil {
err = options.WriteFile(fileName, text, data)
} else {
err = h.program.program.Host().FS().WriteFile(fileName, text)
}
if err == nil && differsOnlyInMap {
// Revert the time to original one
err = h.program.host.SetMTime(fileName, aTime)
}
return err
},
}
}
// Compare to existing computed signature and store it or handle the changes in d.ts map option from before
// returning undefined means that, we dont need to emit this d.ts file since its contents didnt change
func (h *emitFilesHandler) skipDtsOutputOfComposite(file *ast.SourceFile, outputFileName string, text string, data *compiler.WriteFileData, newSignature string, differsOnlyInMap *bool) bool {
if !h.program.snapshot.options.Composite.IsTrue() {
return false
}
var oldSignature string
oldSignatureFormat, ok := h.program.snapshot.emitSignatures.Load(file.Path())
if ok {
if oldSignatureFormat.signature != "" {
oldSignature = oldSignatureFormat.signature
} else {
oldSignature = oldSignatureFormat.signatureWithDifferentOptions[0]
}
}
if newSignature == "" {
newSignature = h.program.snapshot.computeHash(getTextHandlingSourceMapForSignature(text, data))
}
// Dont write dts files if they didn't change
if newSignature == oldSignature {
// If the signature was encoded as string the dts map options match so nothing to do
if oldSignatureFormat != nil && oldSignatureFormat.signature == oldSignature {
data.SkippedDtsWrite = true
return true
} else {
// Mark as differsOnlyInMap so that we can reverse the timestamp with --build so that
// the downstream projects dont detect this as change in d.ts file
*differsOnlyInMap = h.program.Options().Build.IsTrue()
}
} else {
h.latestChangedDtsFiles.Store(file.Path(), outputFileName)
}
h.emitSignatures.Store(file.Path(), &emitSignature{signature: newSignature})
return false
}
func (h *emitFilesHandler) updateSnapshot() []*compiler.EmitResult {
if h.program.snapshot.canUseIncrementalState() {
h.signatures.Range(func(file tspath.Path, signature string) bool {
info, _ := h.program.snapshot.fileInfos.Load(file)
info.signature = signature
if h.program.testingData != nil {
h.program.testingData.UpdatedSignatureKinds[file] = SignatureUpdateKindStoredAtEmit
}
h.program.snapshot.buildInfoEmitPending.Store(true)
return true
})
h.emitSignatures.Range(func(file tspath.Path, signature *emitSignature) bool {
h.program.snapshot.emitSignatures.Store(file, signature)
h.program.snapshot.buildInfoEmitPending.Store(true)
return true
})
for file := range h.deletedPendingKinds.Keys() {
h.program.snapshot.affectedFilesPendingEmit.Delete(file)
h.program.snapshot.buildInfoEmitPending.Store(true)
}
// Always use correct order when to collect the result
var results []*compiler.EmitResult
for _, file := range h.program.GetSourceFiles() {
if latestChangedDtsFile, ok := h.latestChangedDtsFiles.Load(file.Path()); ok {
h.program.snapshot.latestChangedDtsFile = latestChangedDtsFile
h.program.snapshot.buildInfoEmitPending.Store(true)
h.program.snapshot.hasChangedDtsFile = true
}
if update, ok := h.emitUpdates.Load(file.Path()); ok {
if !update.dtsErrorsFromCache {
if update.pendingKind == 0 {
h.program.snapshot.affectedFilesPendingEmit.Delete(file.Path())
} else {
h.program.snapshot.affectedFilesPendingEmit.Store(file.Path(), update.pendingKind)
}
h.program.snapshot.buildInfoEmitPending.Store(true)
}
if update.result != nil {
results = append(results, update.result)
if len(update.result.Diagnostics) != 0 {
h.program.snapshot.emitDiagnosticsPerFile.Store(file.Path(), &DiagnosticsOrBuildInfoDiagnosticsWithFileName{diagnostics: update.result.Diagnostics})
}
}
}
}
return results
} else if h.hasEmitDiagnostics.Load() {
h.program.snapshot.hasEmitDiagnostics = true
}
return nil
}
func emitFiles(ctx context.Context, program *Program, options compiler.EmitOptions, isForDtsErrors bool) *compiler.EmitResult {
emitHandler := &emitFilesHandler{ctx: ctx, program: program, isForDtsErrors: isForDtsErrors}
// Single file emit - do direct from program
if !isForDtsErrors && options.TargetSourceFile != nil {
result := program.program.Emit(ctx, emitHandler.getEmitOptions(options))
emitHandler.updateHasEmitDiagnostics(result)
if ctx.Err() != nil {
return nil
}
emitHandler.updateSnapshot()
return result
}
// Emit only affected files if using builder for emit
return emitHandler.emitAllAffectedFiles(options)
}

View File

@@ -0,0 +1,45 @@
package incremental
import (
"time"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/vfs"
)
type Host interface {
FS() vfs.FS
GetMTime(fileName string) time.Time
SetMTime(fileName string, mTime time.Time) error
}
type host struct {
host compiler.CompilerHost
}
var _ Host = (*host)(nil)
func (h *host) FS() vfs.FS {
return h.host.FS()
}
func (h *host) GetMTime(fileName string) time.Time {
return GetMTime(h.host, fileName)
}
func (h *host) SetMTime(fileName string, mTime time.Time) error {
return h.host.FS().Chtimes(fileName, time.Time{}, mTime)
}
func CreateHost(compilerHost compiler.CompilerHost) Host {
return &host{host: compilerHost}
}
func GetMTime(host compiler.CompilerHost, fileName string) time.Time {
stat := host.FS().Stat(fileName)
var mTime time.Time
if stat != nil {
mTime = stat.ModTime()
}
return mTime
}

View File

@@ -0,0 +1,56 @@
package incremental
import (
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/tsoptions"
)
type BuildInfoReader interface {
ReadBuildInfo(config *tsoptions.ParsedCommandLine) *BuildInfo
}
var _ BuildInfoReader = (*buildInfoReader)(nil)
type buildInfoReader struct {
host compiler.CompilerHost
}
func (r *buildInfoReader) ReadBuildInfo(config *tsoptions.ParsedCommandLine) *BuildInfo {
buildInfoFileName := config.GetBuildInfoFileName()
if buildInfoFileName == "" {
return nil
}
// Read build info file
data, ok := r.host.FS().ReadFile(buildInfoFileName)
if !ok {
return nil
}
var buildInfo BuildInfo
err := json.Unmarshal([]byte(data), &buildInfo)
if err != nil {
return nil
}
return &buildInfo
}
func NewBuildInfoReader(
host compiler.CompilerHost,
) BuildInfoReader {
return &buildInfoReader{host: host}
}
func ReadBuildInfoProgram(config *tsoptions.ParsedCommandLine, reader BuildInfoReader, host compiler.CompilerHost) *Program {
// Read buildInfo file
buildInfo := reader.ReadBuildInfo(config)
if buildInfo == nil || !buildInfo.IsValidVersion() || !buildInfo.IsIncremental() {
return nil
}
// Convert to information that can be used to create incremental program
incrementalProgram := &Program{
snapshot: buildInfoToSnapshot(buildInfo, config, host),
}
return incrementalProgram
}

View File

@@ -0,0 +1,453 @@
package incremental
import (
"context"
"fmt"
"slices"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/outputpaths"
"github.com/microsoft/typescript-go/internal/packagejson"
"github.com/microsoft/typescript-go/internal/tracing"
"github.com/microsoft/typescript-go/internal/tspath"
)
type SignatureUpdateKind byte
const (
SignatureUpdateKindComputedDts SignatureUpdateKind = iota
SignatureUpdateKindStoredAtEmit
SignatureUpdateKindUsedVersion
)
type Program struct {
snapshot *snapshot
program *compiler.Program
host Host
// Testing data
testingData *TestingData
}
var _ compiler.ProgramLike = (*Program)(nil)
func NewProgram(program *compiler.Program, oldProgram *Program, host Host, testing bool) *Program {
incrementalProgram := &Program{
snapshot: programToSnapshot(program, oldProgram, testing),
program: program,
host: host,
}
if testing {
incrementalProgram.testingData = &TestingData{}
incrementalProgram.testingData.SemanticDiagnosticsPerFile = &incrementalProgram.snapshot.semanticDiagnosticsPerFile
if oldProgram != nil {
incrementalProgram.testingData.OldProgramSemanticDiagnosticsPerFile = &oldProgram.snapshot.semanticDiagnosticsPerFile
} else {
incrementalProgram.testingData.OldProgramSemanticDiagnosticsPerFile = &collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName]{}
}
incrementalProgram.testingData.UpdatedSignatureKinds = make(map[tspath.Path]SignatureUpdateKind)
}
return incrementalProgram
}
type TestingData struct {
SemanticDiagnosticsPerFile *collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName]
OldProgramSemanticDiagnosticsPerFile *collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName]
UpdatedSignatureKinds map[tspath.Path]SignatureUpdateKind
}
func (p *Program) GetTestingData() *TestingData {
return p.testingData
}
func (p *Program) panicIfNoProgram(method string) {
if p.program == nil {
panic(method + ": should not be called without program")
}
}
func (p *Program) GetProgram() *compiler.Program {
p.panicIfNoProgram("GetProgram")
return p.program
}
func (p *Program) HasChangedDtsFile() bool {
return p.snapshot.hasChangedDtsFile
}
// Options implements compiler.AnyProgram interface.
func (p *Program) Options() *core.CompilerOptions {
return p.snapshot.options
}
// CommonSourceDirectory implements compiler.AnyProgram interface.
func (p *Program) CommonSourceDirectory() string {
p.panicIfNoProgram("CommonSourceDirectory")
return p.program.CommonSourceDirectory()
}
// Program implements compiler.AnyProgram interface.
func (p *Program) Program() *compiler.Program {
p.panicIfNoProgram("Program")
return p.program
}
// IsSourceFileDefaultLibrary implements compiler.AnyProgram interface.
func (p *Program) IsSourceFileDefaultLibrary(path tspath.Path) bool {
p.panicIfNoProgram("IsSourceFileDefaultLibrary")
return p.program.IsSourceFileDefaultLibrary(path)
}
// GetSourceFiles implements compiler.AnyProgram interface.
func (p *Program) GetSourceFiles() []*ast.SourceFile {
p.panicIfNoProgram("GetSourceFiles")
return p.program.GetSourceFiles()
}
// GetSourceFile implements compiler.AnyProgram interface.
func (p *Program) GetSourceFile(path string) *ast.SourceFile {
p.panicIfNoProgram("GetSourceFile")
return p.program.GetSourceFile(path)
}
// GetConfigFileParsingDiagnostics implements compiler.AnyProgram interface.
func (p *Program) GetConfigFileParsingDiagnostics() []*ast.Diagnostic {
p.panicIfNoProgram("GetConfigFileParsingDiagnostics")
return p.program.GetConfigFileParsingDiagnostics()
}
// GetSyntacticDiagnostics implements compiler.AnyProgram interface.
func (p *Program) GetSyntacticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic {
p.panicIfNoProgram("GetSyntacticDiagnostics")
return p.program.GetSyntacticDiagnostics(ctx, file)
}
// GetBindDiagnostics implements compiler.AnyProgram interface.
func (p *Program) GetBindDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic {
p.panicIfNoProgram("GetBindDiagnostics")
return p.program.GetBindDiagnostics(ctx, file)
}
func (p *Program) GetProgramDiagnostics() []*ast.Diagnostic {
p.panicIfNoProgram("GetProgramDiagnostics")
return p.program.GetProgramDiagnostics()
}
func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic {
p.panicIfNoProgram("GetGlobalDiagnostics")
return p.program.GetGlobalDiagnostics(ctx)
}
// GetSemanticDiagnostics implements compiler.AnyProgram interface.
func (p *Program) GetSemanticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic {
p.panicIfNoProgram("GetSemanticDiagnostics")
if p.snapshot.options.NoCheck.IsTrue() {
return nil
}
// Ensure all the diagnsotics are cached
p.collectSemanticDiagnosticsOfAffectedFiles(ctx, file)
if ctx.Err() != nil {
return nil
}
// Return result from cache
if file != nil {
return p.getSemanticDiagnosticsOfFile(file)
}
var diagnostics []*ast.Diagnostic
for _, file := range p.program.GetSourceFiles() {
diagnostics = append(diagnostics, p.getSemanticDiagnosticsOfFile(file)...)
}
return diagnostics
}
func (p *Program) getSemanticDiagnosticsOfFile(file *ast.SourceFile) []*ast.Diagnostic {
cachedDiagnostics, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path())
if !ok {
panic("After handling all the affected files, there shouldnt be more changes")
}
return slices.Concat(
compiler.FilterNoEmitSemanticDiagnostics(cachedDiagnostics.getDiagnostics(p.program, file), p.snapshot.options),
p.program.GetIncludeProcessorDiagnostics(file),
)
}
// GetDeclarationDiagnostics implements compiler.AnyProgram interface.
func (p *Program) GetDeclarationDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic {
p.panicIfNoProgram("GetDeclarationDiagnostics")
result := emitFiles(ctx, p, compiler.EmitOptions{
TargetSourceFile: file,
}, true)
if result != nil {
return result.Diagnostics
}
return nil
}
// GetSuggestionDiagnostics implements compiler.AnyProgram interface.
func (p *Program) GetSuggestionDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic {
p.panicIfNoProgram("GetSuggestionDiagnostics")
return p.program.GetSuggestionDiagnostics(ctx, file) // TODO: incremental suggestion diagnostics (only relevant in editor incremental builder?)
}
// GetModeForUsageLocation implements compiler.AnyProgram interface.
func (p *Program) Emit(ctx context.Context, options compiler.EmitOptions) *compiler.EmitResult {
p.panicIfNoProgram("Emit")
var result *compiler.EmitResult
if p.snapshot.options.NoEmit.IsTrue() {
result = &compiler.EmitResult{EmitSkipped: true}
} else {
result = compiler.HandleNoEmitOnError(ctx, p, options.TargetSourceFile)
if ctx.Err() != nil {
return nil
}
}
if result != nil {
if options.TargetSourceFile != nil {
return result
}
// Emit buildInfo and combine result
buildInfoResult := p.emitBuildInfo(ctx, options)
if buildInfoResult != nil {
result.Diagnostics = append(result.Diagnostics, buildInfoResult.Diagnostics...)
result.EmittedFiles = append(result.EmittedFiles, buildInfoResult.EmittedFiles...)
}
return result
}
return emitFiles(ctx, p, options, false)
}
// Handle affected files and cache the semantic diagnostics for all of them or the file asked for
func (p *Program) collectSemanticDiagnosticsOfAffectedFiles(ctx context.Context, file *ast.SourceFile) {
if p.snapshot.canUseIncrementalState() {
// Get all affected files
collectAllAffectedFiles(ctx, p)
if ctx.Err() != nil {
return
}
if p.snapshot.semanticDiagnosticsPerFile.Size() == len(p.program.GetSourceFiles()) {
// If we have all the files,
return
}
}
var affectedFiles []*ast.SourceFile
if file != nil {
_, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path())
if ok {
return
}
affectedFiles = []*ast.SourceFile{file}
} else {
for _, file := range p.program.GetSourceFiles() {
if _, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()); !ok {
affectedFiles = append(affectedFiles, file)
}
}
}
// Get their diagnostics and cache them
diagnosticsPerFile := p.program.GetSemanticDiagnosticsWithoutNoEmitFiltering(ctx, affectedFiles)
// commit changes if no err
if ctx.Err() != nil {
return
}
// Commit changes to snapshot
for file, diagnostics := range diagnosticsPerFile {
p.snapshot.semanticDiagnosticsPerFile.Store(file.Path(), &DiagnosticsOrBuildInfoDiagnosticsWithFileName{diagnostics: diagnostics})
}
if p.snapshot.semanticDiagnosticsPerFile.Size() == len(p.program.GetSourceFiles()) && p.snapshot.checkPending && !p.snapshot.options.NoCheck.IsTrue() {
p.snapshot.checkPending = false
}
p.snapshot.buildInfoEmitPending.Store(true)
}
func (p *Program) emitBuildInfo(ctx context.Context, options compiler.EmitOptions) *compiler.EmitResult {
if tr := p.program.Tracing(); tr != nil {
defer tr.Push(tracing.PhaseEmit, "emitBuildInfo", nil, true)()
}
buildInfoFileName := outputpaths.GetBuildInfoFileName(p.snapshot.options, tspath.ComparePathsOptions{
CurrentDirectory: p.program.GetCurrentDirectory(),
UseCaseSensitiveFileNames: p.program.UseCaseSensitiveFileNames(),
})
if buildInfoFileName == "" || p.program.IsEmitBlocked(buildInfoFileName) {
return nil
}
if p.snapshot.hasErrors == core.TSUnknown {
p.ensureHasErrorsForState(ctx, p.program)
if p.snapshot.hasErrors != p.snapshot.hasErrorsFromOldState || p.snapshot.hasSemanticErrors != p.snapshot.hasSemanticErrorsFromOldState {
p.snapshot.buildInfoEmitPending.Store(true)
}
}
if p.snapshot.packageJsons == nil {
p.ensurePackageJsonsForState()
if !slices.Equal(p.snapshot.packageJsons, p.snapshot.packageJsonsFromOldState) ||
!slices.Equal(p.snapshot.missingPackageJsons, p.snapshot.missingPackageJsonsFromOldState) {
p.snapshot.buildInfoEmitPending.Store(true)
}
}
if !p.snapshot.buildInfoEmitPending.Load() {
return nil
}
if ctx.Err() != nil {
return nil
}
buildInfo := snapshotToBuildInfo(p.snapshot, p.program, buildInfoFileName)
text, err := json.Marshal(buildInfo)
if err != nil {
panic(fmt.Sprintf("Failed to marshal build info: %v", err))
}
if options.WriteFile != nil {
err = options.WriteFile(buildInfoFileName, string(text), &compiler.WriteFileData{
BuildInfo: buildInfo,
})
} else {
err = p.program.Host().FS().WriteFile(buildInfoFileName, string(text))
}
if err != nil {
return &compiler.EmitResult{
EmitSkipped: true,
Diagnostics: []*ast.Diagnostic{
ast.NewCompilerDiagnostic(diagnostics.Could_not_write_file_0_Colon_1, buildInfoFileName, err.Error()),
},
}
}
p.snapshot.buildInfoEmitPending.Store(false)
return &compiler.EmitResult{
EmitSkipped: false,
EmittedFiles: []string{buildInfoFileName},
}
}
func (p *Program) ensureHasErrorsForState(ctx context.Context, program *compiler.Program) {
var hasIncludeProcessingDiagnostics func() bool
var hasEmitDiagnostics bool
if p.snapshot.canUseIncrementalState() {
if slices.ContainsFunc(program.GetSourceFiles(), func(file *ast.SourceFile) bool {
if _, ok := p.snapshot.emitDiagnosticsPerFile.Load(file.Path()); ok {
// emit diagnostics will be encoded in buildInfo;
return true
}
if hasIncludeProcessingDiagnostics == nil && len(p.program.GetIncludeProcessorDiagnostics(file)) > 0 {
hasIncludeProcessingDiagnostics = func() bool { return true }
}
return false
}) {
hasEmitDiagnostics = true
}
if hasIncludeProcessingDiagnostics == nil {
hasIncludeProcessingDiagnostics = func() bool { return false }
}
} else {
hasEmitDiagnostics = p.snapshot.hasEmitDiagnostics
hasIncludeProcessingDiagnostics = func() bool {
return slices.ContainsFunc(program.GetSourceFiles(), func(file *ast.SourceFile) bool {
return len(p.program.GetIncludeProcessorDiagnostics(file)) > 0
})
}
}
if hasEmitDiagnostics {
// Record this for only non incremental build info
p.snapshot.hasErrors = core.IfElse(p.snapshot.options.IsIncremental(), core.TSFalse, core.TSTrue)
// Dont need to encode semantic errors state since the emit diagnostics are encoded
p.snapshot.hasSemanticErrors = false
return
}
if hasIncludeProcessingDiagnostics() ||
len(program.GetConfigFileParsingDiagnostics()) > 0 ||
len(program.GetSyntacticDiagnostics(ctx, nil)) > 0 ||
len(program.GetProgramDiagnostics()) > 0 ||
len(program.GetGlobalDiagnostics(ctx)) > 0 {
p.snapshot.hasErrors = core.TSTrue
// Dont need to encode semantic errors state since the syntax and program diagnostics are encoded as present
p.snapshot.hasSemanticErrors = false
return
}
p.snapshot.hasErrors = core.TSFalse
// Check semantic and emit diagnostics first as we dont need to ask program about it
if slices.ContainsFunc(p.program.GetSourceFiles(), func(file *ast.SourceFile) bool {
semanticDiagnostics, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path())
if !ok {
// Missing semantic diagnostics in cache will be encoded in incremental buildInfo
return p.snapshot.options.IsIncremental()
}
if len(semanticDiagnostics.diagnostics) > 0 || len(semanticDiagnostics.buildInfoDiagnostics) > 0 {
// cached semantic diagnostics will be encoded in buildInfo
return true
}
return false
}) {
// Because semantic diagnostics are recorded in buildInfo, we dont need to encode hasErrors in incremental buildInfo
// But encode as errors in non incremental buildInfo
p.snapshot.hasSemanticErrors = !p.snapshot.options.IsIncremental()
}
}
func (p *Program) ensurePackageJsonsForState() {
config := tspath.GetDirectoryPath(p.program.CommandLine().ConfigName())
if config != "" {
p.program.PackageJsonCacheEntries(func(key tspath.Path, value *packagejson.InfoCacheEntry) bool {
if value == nil {
return true
}
packageJson := tspath.CombinePaths(value.PackageDirectory, "package.json")
if value.Exists() || value.DirectoryExists {
packageJson = p.program.Host().FS().Realpath(packageJson)
}
if value.Exists() {
p.snapshot.packageJsons = append(p.snapshot.packageJsons, packageJson)
} else if strings.Contains(packageJson, "/node_modules/") {
p.snapshot.missingPackageJsons = append(p.snapshot.missingPackageJsons, packageJson)
}
return true
})
}
p.snapshot.packageJsons = normalizePackageJsons(p.snapshot.packageJsons)
p.snapshot.missingPackageJsons = normalizePackageJsons(p.snapshot.missingPackageJsons)
}
func normalizePackageJsons(packageJsons []string) []string {
if packageJsons == nil {
return make([]string, 0)
}
slices.Sort(packageJsons)
return core.Deduplicate(packageJsons)
}
func (p *Program) PackageJsonLookupPaths() []string {
config := tspath.GetDirectoryPath(p.program.CommandLine().ConfigName())
if config == "" {
return nil
}
var packageJsons []string
p.program.PackageJsonCacheEntries(func(key tspath.Path, value *packagejson.InfoCacheEntry) bool {
if value == nil {
return true
}
packageJson := tspath.CombinePaths(value.PackageDirectory, "package.json")
if value.Exists() || value.DirectoryExists {
packageJson = p.program.Host().FS().Realpath(packageJson)
}
packageJsons = append(packageJsons, packageJson)
return true
})
slices.Sort(packageJsons)
return core.Deduplicate(packageJsons)
}

View File

@@ -0,0 +1,403 @@
package incremental
import (
"context"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/binder"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
func programToSnapshot(program *compiler.Program, oldProgram *Program, hashWithText bool) *snapshot {
if oldProgram != nil && oldProgram.program == program {
return oldProgram.snapshot
}
snapshot := &snapshot{
options: program.Options(),
hashWithText: hashWithText,
checkPending: program.Options().NoCheck.IsTrue(),
}
to := &toProgramSnapshot{
program: program,
oldProgram: oldProgram,
snapshot: snapshot,
}
if to.snapshot.canUseIncrementalState() {
to.reuseFromOldProgram()
to.computeProgramFileChanges()
to.handleFileDelete()
to.handlePendingEmit()
to.handlePendingCheck()
}
return snapshot
}
type toProgramSnapshot struct {
program *compiler.Program
oldProgram *Program
snapshot *snapshot
globalFileRemoved bool
}
func (t *toProgramSnapshot) reuseFromOldProgram() {
if t.oldProgram != nil {
if t.snapshot.options.Composite.IsTrue() {
t.snapshot.latestChangedDtsFile = t.oldProgram.snapshot.latestChangedDtsFile
}
// Copy old snapshot's changed files set
t.oldProgram.snapshot.changedFilesSet.Range(func(key tspath.Path) bool {
t.snapshot.changedFilesSet.Add(key)
return true
})
t.oldProgram.snapshot.affectedFilesPendingEmit.Range(func(key tspath.Path, emitKind FileEmitKind) bool {
t.snapshot.affectedFilesPendingEmit.Store(key, emitKind)
return true
})
t.snapshot.buildInfoEmitPending.Store(t.oldProgram.snapshot.buildInfoEmitPending.Load())
t.snapshot.hasErrorsFromOldState = t.oldProgram.snapshot.hasErrors
t.snapshot.hasSemanticErrorsFromOldState = t.oldProgram.snapshot.hasSemanticErrors
t.snapshot.packageJsonsFromOldState = t.oldProgram.snapshot.packageJsons
t.snapshot.missingPackageJsonsFromOldState = t.oldProgram.snapshot.missingPackageJsons
} else {
t.snapshot.buildInfoEmitPending.Store(t.snapshot.options.IsIncremental())
}
}
func (t *toProgramSnapshot) computeProgramFileChanges() {
canCopySemanticDiagnostics := t.oldProgram != nil &&
!tsoptions.CompilerOptionsAffectSemanticDiagnostics(t.oldProgram.snapshot.options, t.program.Options())
// We can only reuse emit signatures (i.e. .d.ts signatures) if the .d.ts file is unchanged,
// which will eg be depedent on change in options like declarationDir and outDir options are unchanged.
// We need to look in oldState.compilerOptions, rather than oldCompilerOptions (i.e.we need to disregard useOldState) because
// oldCompilerOptions can be undefined if there was change in say module from None to some other option
// which would make useOldState as false since we can now use reference maps that are needed to track what to emit, what to check etc
// but that option change does not affect d.ts file name so emitSignatures should still be reused.
canCopyEmitSignatures := t.snapshot.options.Composite.IsTrue() &&
t.oldProgram != nil &&
!tsoptions.CompilerOptionsAffectDeclarationPath(t.oldProgram.snapshot.options, t.program.Options())
copyDeclarationFileDiagnostics := canCopySemanticDiagnostics &&
t.snapshot.options.SkipLibCheck.IsTrue() == t.oldProgram.snapshot.options.SkipLibCheck.IsTrue()
copyLibFileDiagnostics := copyDeclarationFileDiagnostics &&
t.snapshot.options.SkipDefaultLibCheck.IsTrue() == t.oldProgram.snapshot.options.SkipDefaultLibCheck.IsTrue()
files := t.program.GetSourceFiles()
wg := core.NewWorkGroup(t.program.SingleThreaded())
for _, file := range files {
wg.Queue(func() {
version := t.snapshot.computeHash(file.Text())
impliedNodeFormat := t.program.GetSourceFileMetaData(file.Path()).ImpliedNodeFormat
affectsGlobalScope := fileAffectsGlobalScope(file)
var signature string
newReferences := getReferencedFiles(t.program, file)
if newReferences != nil {
t.snapshot.referencedMap.storeReferences(file.Path(), newReferences)
}
if t.oldProgram != nil {
if oldFileInfo, ok := t.oldProgram.snapshot.fileInfos.Load(file.Path()); ok {
signature = oldFileInfo.signature
if oldFileInfo.version != version || oldFileInfo.affectsGlobalScope != affectsGlobalScope || oldFileInfo.impliedNodeFormat != impliedNodeFormat {
t.snapshot.addFileToChangeSet(file.Path())
} else if oldReferences, _ := t.oldProgram.snapshot.referencedMap.getReferences(file.Path()); !newReferences.Equals(oldReferences) {
// Referenced files changed
t.snapshot.addFileToChangeSet(file.Path())
} else if newReferences != nil {
for refPath := range newReferences.Keys() {
if t.program.GetSourceFileByPath(refPath) == nil {
if _, ok := t.oldProgram.snapshot.fileInfos.Load(refPath); ok {
// Referenced file was deleted in the new program
t.snapshot.addFileToChangeSet(file.Path())
break
}
}
}
}
} else {
t.snapshot.addFileToChangeSet(file.Path())
}
if !t.snapshot.changedFilesSet.Has(file.Path()) {
if emitDiagnostics, ok := t.oldProgram.snapshot.emitDiagnosticsPerFile.Load(file.Path()); ok {
t.snapshot.emitDiagnosticsPerFile.Store(file.Path(), repopulateDiagnosticsOfFile(emitDiagnostics, t.program, file))
}
if canCopySemanticDiagnostics {
if (!file.IsDeclarationFile || copyDeclarationFileDiagnostics) &&
(!t.program.IsSourceFileDefaultLibrary(file.Path()) || copyLibFileDiagnostics) {
// Unchanged file copy diagnostics
if diagnostics, ok := t.oldProgram.snapshot.semanticDiagnosticsPerFile.Load(file.Path()); ok {
t.snapshot.semanticDiagnosticsPerFile.Store(file.Path(), repopulateDiagnosticsOfFile(diagnostics, t.program, file))
}
}
}
}
if canCopyEmitSignatures {
if oldEmitSignature, ok := t.oldProgram.snapshot.emitSignatures.Load(file.Path()); ok {
t.snapshot.emitSignatures.Store(file.Path(), oldEmitSignature.getNewEmitSignature(t.oldProgram.snapshot.options, t.snapshot.options))
}
}
} else {
t.snapshot.addFileToAffectedFilesPendingEmit(file.Path(), GetFileEmitKind(t.snapshot.options))
signature = version
}
t.snapshot.fileInfos.Store(file.Path(), &FileInfo{
version: version,
signature: signature,
affectsGlobalScope: affectsGlobalScope,
impliedNodeFormat: impliedNodeFormat,
})
})
}
wg.RunAndWait()
}
func (t *toProgramSnapshot) handleFileDelete() {
if t.oldProgram != nil {
// If the global file is removed, add all files as changed
t.oldProgram.snapshot.fileInfos.Range(func(filePath tspath.Path, oldInfo *FileInfo) bool {
if _, ok := t.snapshot.fileInfos.Load(filePath); !ok {
if oldInfo.affectsGlobalScope {
for _, file := range t.snapshot.getAllFilesExcludingDefaultLibraryFile(t.program, nil) {
t.snapshot.addFileToChangeSet(file.Path())
}
t.globalFileRemoved = true
} else {
t.snapshot.buildInfoEmitPending.Store(true)
}
return false
}
return true
})
}
}
func (t *toProgramSnapshot) handlePendingEmit() {
if t.oldProgram != nil && !t.globalFileRemoved {
// If options affect emit, then we need to do complete emit per compiler options
// otherwise only the js or dts that needs to emitted because its different from previously emitted options
var pendingEmitKind FileEmitKind
if tsoptions.CompilerOptionsAffectEmit(t.oldProgram.snapshot.options, t.snapshot.options) {
pendingEmitKind = GetFileEmitKind(t.snapshot.options)
} else {
pendingEmitKind = getPendingEmitKindWithOptions(t.snapshot.options, t.oldProgram.snapshot.options)
}
if pendingEmitKind != FileEmitKindNone {
// Add all files to affectedFilesPendingEmit since emit changed
for _, file := range t.program.GetSourceFiles() {
// Add to affectedFilesPending emit only if not changed since any changed file will do full emit
if !t.snapshot.changedFilesSet.Has(file.Path()) {
t.snapshot.addFileToAffectedFilesPendingEmit(file.Path(), pendingEmitKind)
}
}
t.snapshot.buildInfoEmitPending.Store(true)
}
}
}
func (t *toProgramSnapshot) handlePendingCheck() {
if t.oldProgram != nil &&
t.snapshot.semanticDiagnosticsPerFile.Size() != len(t.program.GetSourceFiles()) &&
t.oldProgram.snapshot.checkPending != t.snapshot.checkPending {
t.snapshot.buildInfoEmitPending.Store(true)
}
}
func fileAffectsGlobalScope(file *ast.SourceFile) bool {
binder.BindSourceFile(file)
// if file contains anything that augments to global scope we need to build them as if
// they are global files as well as module
if core.Some(file.ModuleAugmentations, func(augmentation *ast.ModuleName) bool {
return ast.IsGlobalScopeAugmentation(augmentation.Parent)
}) {
return true
}
if ast.IsExternalOrCommonJSModule(file) || ast.IsJsonSourceFile(file) {
return false
}
// For script files that contains only ambient external modules, although they are not actually external module files,
// they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore,
// there are no point to rebuild all script files if these special files have changed. However, if any statement
// in the file is not ambient external module, we treat it as a regular script file.
return file.Statements != nil &&
file.Statements.Nodes != nil &&
core.Some(file.Statements.Nodes, func(stmt *ast.Node) bool {
return !ast.IsModuleWithStringLiteralName(stmt)
})
}
func addReferencedFilesFromSymbol(file *ast.SourceFile, referencedFiles *collections.Set[tspath.Path], symbol *ast.Symbol) {
if symbol == nil {
return
}
for _, declaration := range symbol.Declarations {
fileOfDecl := ast.GetSourceFileOfNode(declaration)
if fileOfDecl == nil {
continue
}
if file != fileOfDecl {
referencedFiles.Add(fileOfDecl.Path())
}
}
}
// Get the module source file and all augmenting files from the import name node from file
func addReferencedFilesFromImportLiteral(file *ast.SourceFile, referencedFiles *collections.Set[tspath.Path], checker *checker.Checker, importName *ast.LiteralLikeNode) {
symbol := checker.GetSymbolAtLocation(importName)
addReferencedFilesFromSymbol(file, referencedFiles, symbol)
}
// Gets the path to reference file from file name, it could be resolvedPath if present otherwise path
func addReferencedFileFromFileName(program *compiler.Program, fileName string, referencedFiles *collections.Set[tspath.Path], sourceFileDirectory string) {
if redirect := program.GetParseFileRedirect(fileName); redirect != "" {
referencedFiles.Add(tspath.ToPath(redirect, program.GetCurrentDirectory(), program.UseCaseSensitiveFileNames()))
} else {
referencedFiles.Add(tspath.ToPath(fileName, sourceFileDirectory, program.UseCaseSensitiveFileNames()))
}
}
// Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true
func getReferencedFiles(program *compiler.Program, file *ast.SourceFile) *collections.Set[tspath.Path] {
referencedFiles := collections.Set[tspath.Path]{}
// We need to use a set here since the code can contain the same import twice,
// but that will only be one dependency.
// To avoid invernal conversion, the key of the referencedFiles map must be of type Path
checker, done := program.GetTypeCheckerForFileExclusive(context.TODO(), file)
defer done()
for _, importName := range file.Imports() {
addReferencedFilesFromImportLiteral(file, &referencedFiles, checker, importName)
}
sourceFileDirectory := tspath.GetDirectoryPath(file.FileName())
// Handle triple slash references
for _, referencedFile := range file.ReferencedFiles {
addReferencedFileFromFileName(program, referencedFile.FileName, &referencedFiles, sourceFileDirectory)
}
// Handle type reference directives
if typeRefsInFile, ok := program.GetResolvedTypeReferenceDirectives()[file.Path()]; ok {
for _, typeRef := range typeRefsInFile {
if typeRef.ResolvedFileName != "" {
addReferencedFileFromFileName(program, typeRef.ResolvedFileName, &referencedFiles, sourceFileDirectory)
}
}
}
// Add module augmentation as references
for _, moduleName := range file.ModuleAugmentations {
if !ast.IsStringLiteral(moduleName) {
continue
}
addReferencedFilesFromImportLiteral(file, &referencedFiles, checker, moduleName)
}
// From ambient modules
for _, ambientModule := range checker.GetAmbientModules() {
addReferencedFilesFromSymbol(file, &referencedFiles, ambientModule)
}
return core.IfElse(referencedFiles.Len() > 0, &referencedFiles, nil)
}
// repopulateDiagnosticsOfFile repopulates diagnostic chains that depend on program state.
// When diagnostics are copied from a previous build, their message chains may reference
// stale program state (e.g., resolved module alternate results, package.json scope).
// This function recomputes those chains using the current program's state.
func repopulateDiagnosticsOfFile(diags *DiagnosticsOrBuildInfoDiagnosticsWithFileName, p *compiler.Program, file *ast.SourceFile) *DiagnosticsOrBuildInfoDiagnosticsWithFileName {
if diags.diagnostics != nil {
repopulated := repopulateDiagnosticsList(diags.diagnostics, p, file)
if repopulated == nil {
return diags
}
return &DiagnosticsOrBuildInfoDiagnosticsWithFileName{diagnostics: repopulated}
}
// buildInfoDiagnostics will be repopulated via toDiagnostic's repopulateInfo handling
return diags
}
// repopulateDiagnosticsList repopulates diagnostic chains in a list of diagnostics.
// Returns nil if no diagnostics needed repopulation (i.e., no changes were made).
func repopulateDiagnosticsList(diags []*ast.Diagnostic, p *compiler.Program, file *ast.SourceFile) []*ast.Diagnostic {
changed := false
result := make([]*ast.Diagnostic, len(diags))
for i, d := range diags {
repopulated := repopulateDiagnosticMessageChain(d.MessageChain(), p, file)
if repopulated != nil {
clone := d.Clone()
clone.SetMessageChain(repopulated)
result[i] = clone
changed = true
} else {
result[i] = d
}
}
if !changed {
return nil
}
return result
}
// repopulateDiagnosticMessageChain repopulates chains that have repopulate info.
// Returns nil if no changes were made.
func repopulateDiagnosticMessageChain(chain []*ast.Diagnostic, p *compiler.Program, file *ast.SourceFile) []*ast.Diagnostic {
if len(chain) == 0 {
return nil
}
changed := false
result := make([]*ast.Diagnostic, len(chain))
for i, c := range chain {
if c.RepopulateInfo() != nil {
// Convert to buildInfoDiagnosticWithFileName and repopulate
b := &buildInfoDiagnosticWithFileName{
pos: c.Pos(),
end: c.End(),
code: c.Code(),
category: c.Category(),
messageKey: c.MessageKey(),
messageArgs: c.MessageArgs(),
repopulateInfo: c.RepopulateInfo(),
}
// Recursively handle nested chains
for _, nested := range c.MessageChain() {
b.messageChain = append(b.messageChain, astDiagToBuildInfoDiag(nested))
}
result[i] = repopulateDiagnosticChain(b, p, file)
changed = true
} else {
// Check nested chains
nested := repopulateDiagnosticMessageChain(c.MessageChain(), p, file)
if nested != nil {
clone := c.Clone()
clone.SetMessageChain(nested)
result[i] = clone
changed = true
} else {
result[i] = c
}
}
}
if !changed {
return nil
}
return result
}
func astDiagToBuildInfoDiag(d *ast.Diagnostic) *buildInfoDiagnosticWithFileName {
b := &buildInfoDiagnosticWithFileName{
pos: d.Pos(),
end: d.End(),
code: d.Code(),
category: d.Category(),
messageKey: d.MessageKey(),
messageArgs: d.MessageArgs(),
repopulateInfo: d.RepopulateInfo(),
}
for _, nested := range d.MessageChain() {
b.messageChain = append(b.messageChain, astDiagToBuildInfoDiag(nested))
}
return b
}

View File

@@ -0,0 +1,52 @@
package incremental
import (
"iter"
"maps"
"slices"
"sync"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/tspath"
)
type referenceMap struct {
references collections.SyncMap[tspath.Path, *collections.Set[tspath.Path]]
referencedBy map[tspath.Path]*collections.Set[tspath.Path]
referenceBy sync.Once
}
func (r *referenceMap) storeReferences(path tspath.Path, refs *collections.Set[tspath.Path]) {
r.references.Store(path, refs)
}
func (r *referenceMap) getReferences(path tspath.Path) (*collections.Set[tspath.Path], bool) {
refs, ok := r.references.Load(path)
return refs, ok
}
func (r *referenceMap) getPathsWithReferences() []tspath.Path {
return slices.Collect(r.references.Keys())
}
func (r *referenceMap) getReferencedBy(path tspath.Path) iter.Seq[tspath.Path] {
r.referenceBy.Do(func() {
r.referencedBy = make(map[tspath.Path]*collections.Set[tspath.Path])
r.references.Range(func(key tspath.Path, value *collections.Set[tspath.Path]) bool {
for ref := range value.Keys() {
set, ok := r.referencedBy[ref]
if !ok {
set = &collections.Set[tspath.Path]{}
r.referencedBy[ref] = set
}
set.Add(key)
}
return true
})
})
refs, ok := r.referencedBy[path]
if ok {
return maps.Keys(refs.Keys())
}
return func(yield func(tspath.Path) bool) {}
}

View File

@@ -0,0 +1,440 @@
package incremental
import (
"encoding/hex"
"fmt"
"strings"
"sync"
"sync/atomic"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/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.HashString128(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
messageKey diagnostics.Key
messageArgs []string
messageChain []*buildInfoDiagnosticWithFileName
relatedInformation []*buildInfoDiagnosticWithFileName
reportsUnnecessary bool
reportsDeprecated bool
skippedOnNoEmit bool
repopulateInfo *ast.RepopulateDiagnosticInfo
}
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
}
if b.repopulateInfo != nil {
return repopulateDiagnosticChain(b, p, fileForDiagnostic)
}
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.NewDiagnosticFromSerialized(
fileForDiagnostic,
core.NewTextRange(b.pos, b.end),
b.code,
b.category,
b.messageKey,
b.messageArgs,
messageChain,
relatedInformation,
b.reportsUnnecessary,
b.reportsDeprecated,
b.skippedOnNoEmit,
)
}
// repopulateDiagnosticChain recomputes a diagnostic chain entry that depends on
// program state which may have changed between incremental builds.
func repopulateDiagnosticChain(b *buildInfoDiagnosticWithFileName, p *compiler.Program, file *ast.SourceFile) *ast.Diagnostic {
info := b.repopulateInfo
switch info.Kind {
case ast.RepopulateModeMismatch:
return repopulateModeMismatchChain(b, p, file)
case ast.RepopulateModuleNotFound:
return repopulateModuleNotFoundChain(b, p, file, info)
default:
// Fall back to using the stored (possibly stale) data
return b.toDiagnosticWithoutRepopulate(p, file)
}
}
func (b *buildInfoDiagnosticWithFileName) toDiagnosticWithoutRepopulate(p *compiler.Program, file *ast.SourceFile) *ast.Diagnostic {
var messageChain []*ast.Diagnostic
for _, msg := range b.messageChain {
messageChain = append(messageChain, msg.toDiagnostic(p, file))
}
var relatedInformation []*ast.Diagnostic
for _, info := range b.relatedInformation {
relatedInformation = append(relatedInformation, info.toDiagnostic(p, file))
}
return ast.NewDiagnosticFromSerialized(
file,
core.NewTextRange(b.pos, b.end),
b.code,
b.category,
b.messageKey,
b.messageArgs,
messageChain,
relatedInformation,
b.reportsUnnecessary,
b.reportsDeprecated,
b.skippedOnNoEmit,
)
}
func repopulateModeMismatchChain(b *buildInfoDiagnosticWithFileName, p *compiler.Program, file *ast.SourceFile) *ast.Diagnostic {
if file == nil {
return b.toDiagnosticWithoutRepopulate(p, file)
}
details := checker.CreateModeMismatchDetails(p, file)
var nextChain []*ast.Diagnostic
for _, msg := range b.messageChain {
nextChain = append(nextChain, msg.toDiagnostic(p, file))
}
return ast.NewDiagnosticFromSerialized(
file,
core.NewTextRange(b.pos, b.end),
details.Message.Code(),
details.Message.Category(),
details.Message.Key(),
diagnostics.StringifyArgs(details.Args),
nextChain,
nil,
false,
false,
false,
)
}
func repopulateModuleNotFoundChain(b *buildInfoDiagnosticWithFileName, p *compiler.Program, file *ast.SourceFile, info *ast.RepopulateDiagnosticInfo) *ast.Diagnostic {
if file == nil {
return b.toDiagnosticWithoutRepopulate(p, file)
}
packageName := info.PackageName
if packageName == "" {
packageName = info.ModuleReference
}
details := checker.CreateModuleNotFoundChain(p, file, info.ModuleReference, info.Mode, packageName)
var nextChain []*ast.Diagnostic
for _, msg := range b.messageChain {
nextChain = append(nextChain, msg.toDiagnostic(p, file))
}
return ast.NewDiagnosticFromSerialized(
file,
core.NewTextRange(b.pos, b.end),
details.Message.Code(),
details.Message.Category(),
details.Message.Key(),
diagnostics.StringifyArgs(details.Args),
nextChain,
nil,
false,
false,
false,
)
}
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
// Looked up package.json files from
packageJsons []string
missingPackageJsons []string
// 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
packageJsonsFromOldState []string
missingPackageJsonsFromOldState []string
// 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(string(diagnostic.MessageKey()))
builder.WriteString("\n")
for _, arg := range diagnostic.MessageArgs() {
builder.WriteString(arg)
builder.WriteString("\n")
}
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
}

View File

@@ -0,0 +1,381 @@
package incremental
import (
"fmt"
"maps"
"reflect"
"slices"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/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
to.setPackageJsons()
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,
MessageKey: d.messageKey,
MessageArgs: d.messageArgs,
MessageChain: t.toBuildInfoDiagnosticsFromFileNameDiagnostics(d.messageChain),
RelatedInformation: t.toBuildInfoDiagnosticsFromFileNameDiagnostics(d.relatedInformation),
ReportsUnnecessary: d.reportsUnnecessary,
ReportsDeprecated: d.reportsDeprecated,
SkippedOnNoEmit: d.skippedOnNoEmit,
RepopulateInfo: toBuildInfoRepopulateInfo(d.repopulateInfo),
}
})
}
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(),
MessageKey: d.MessageKey(),
MessageArgs: d.MessageArgs(),
MessageChain: t.toBuildInfoDiagnosticsFromDiagnostics(filePath, d.MessageChain()),
RelatedInformation: t.toBuildInfoDiagnosticsFromDiagnostics(filePath, d.RelatedInformation()),
ReportsUnnecessary: d.ReportsUnnecessary(),
ReportsDeprecated: d.ReportsDeprecated(),
SkippedOnNoEmit: d.SkippedOnNoEmit(),
RepopulateInfo: toBuildInfoRepopulateInfo(d.RepopulateInfo()),
}
})
}
func toBuildInfoRepopulateInfo(info *ast.RepopulateDiagnosticInfo) *BuildInfoRepopulateInfo {
if info == nil {
return nil
}
return &BuildInfoRepopulateInfo{
Kind: info.Kind,
ModuleReference: info.ModuleReference,
Mode: info.Mode,
PackageName: info.PackageName,
}
}
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.Map(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 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)
})
}
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))),
}
})
}
func (t *toBuildInfo) setPackageJsons() {
if len(t.snapshot.packageJsons) > 0 {
t.buildInfo.PackageJsons = core.Map(t.snapshot.packageJsons, t.relativeToBuildInfo)
}
if len(t.snapshot.missingPackageJsons) > 0 {
t.buildInfo.MissingPackageJsons = core.Map(t.snapshot.missingPackageJsons, t.relativeToBuildInfo)
}
}