internal/fourslash/statebaseline.go (460 lines of code) (raw):
package fourslash
import (
"fmt"
"io"
"iter"
"maps"
"slices"
"strings"
"testing"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/project"
"github.com/microsoft/typescript-go/internal/testutil/fsbaselineutil"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs/iovfs"
"gotest.tools/v3/assert"
)
type stateBaseline struct {
baseline strings.Builder
fsDiffer *fsbaselineutil.FSDiffer
isInitialized bool
serializedProjects map[string]projectInfo
serializedOpenFiles map[string]*openFileInfo
serializedConfigFileRegistry *project.ConfigFileRegistry
}
func newStateBaseline(fsFromMap iovfs.FsWithSys) *stateBaseline {
stateBaseline := &stateBaseline{
fsDiffer: &fsbaselineutil.FSDiffer{
FS: fsFromMap,
WrittenFiles: &collections.SyncSet[string]{},
},
}
fmt.Fprintf(&stateBaseline.baseline, "UseCaseSensitiveFileNames: %v\n", fsFromMap.UseCaseSensitiveFileNames())
stateBaseline.fsDiffer.BaselineFSwithDiff(&stateBaseline.baseline)
return stateBaseline
}
type requestOrMessage struct {
Method lsproto.Method `json:"method"`
Params any `json:"params,omitzero"`
}
func (f *FourslashTest) baselineRequestOrNotification(t *testing.T, method lsproto.Method, params any) {
t.Helper()
if !f.testData.isStateBaseliningEnabled() {
return
}
res, _ := json.Marshal(requestOrMessage{method, params}, jsontext.WithIndent(" "))
f.stateBaseline.baseline.WriteString("\n" + string(res) + "\n")
f.stateBaseline.isInitialized = true
}
func (f *FourslashTest) baselineProjectsAfterNotification(t *testing.T, fileName string) {
t.Helper()
if !f.testData.isStateBaseliningEnabled() {
return
}
// Do hover so we have snapshot to check things on!!
_, _, resultOk := sendRequestWorker(t, f, lsproto.TextDocumentHoverInfo, &lsproto.HoverParams{
TextDocument: lsproto.TextDocumentIdentifier{
Uri: lsconv.FileNameToDocumentURI(fileName),
},
Position: lsproto.Position{
Line: uint32(0),
Character: uint32(0),
},
})
assert.Assert(t, resultOk)
f.baselineState(t)
}
func (f *FourslashTest) baselineState(t *testing.T) {
t.Helper()
if !f.testData.isStateBaseliningEnabled() {
return
}
serialized := f.serializedState(t)
if serialized != "" {
f.stateBaseline.baseline.WriteString("\n")
f.stateBaseline.baseline.WriteString(serialized)
}
}
func (f *FourslashTest) serializedState(t *testing.T) string {
t.Helper()
var builder strings.Builder
f.stateBaseline.fsDiffer.BaselineFSwithDiff(&builder)
if strings.TrimSpace(builder.String()) == "" {
builder.Reset()
}
f.printStateDiff(t, &builder)
return builder.String()
}
type projectInfo = *compiler.Program
type openFileInfo struct {
defaultProjectName string
allProjects []string
}
type diffTableOptions struct {
indent string
sortKeys bool
}
type diffTable struct {
diff collections.OrderedMap[string, string]
options diffTableOptions
}
func (d *diffTable) add(key, value string) {
d.diff.Set(key, value)
}
func (d *diffTable) print(w io.Writer, header string) {
count := d.diff.Size()
if count == 0 {
return
}
if header != "" {
fmt.Fprintf(w, "%s%s\n", d.options.indent, header)
}
diffKeys := make([]string, 0, count)
keyWidth := 0
indent := d.options.indent + " "
for key := range d.diff.Keys() {
keyWidth = max(keyWidth, len(key))
diffKeys = append(diffKeys, key)
}
if d.options.sortKeys {
slices.Sort(diffKeys)
}
for _, key := range diffKeys {
value := d.diff.GetOrZero(key)
fmt.Fprintf(w, "%s%-*s %s\n", indent, keyWidth+1, key, value)
}
}
type diffTableWriter struct {
hasChange bool
header string
diffs map[string]func(io.Writer)
}
func newDiffTableWriter(header string) *diffTableWriter {
return &diffTableWriter{header: header, diffs: make(map[string]func(io.Writer))}
}
func (d *diffTableWriter) setHasChange() {
d.hasChange = true
}
func (d *diffTableWriter) add(key string, fn func(io.Writer)) {
d.diffs[key] = fn
}
func (d *diffTableWriter) print(w io.Writer) {
if d.hasChange {
fmt.Fprintf(w, "%s::\n", d.header)
keys := slices.Collect(maps.Keys(d.diffs))
slices.Sort(keys)
for _, key := range keys {
d.diffs[key](w)
}
}
}
func areIterSeqEqual(a, b iter.Seq[tspath.Path]) bool {
aSlice := slices.Collect(a)
bSlice := slices.Collect(b)
slices.Sort(aSlice)
slices.Sort(bSlice)
return slices.Equal(aSlice, bSlice)
}
func printSlicesWithDiffTable(w io.Writer, header string, newSlice []string, getOldSlice func() []string, options diffTableOptions, topChange string, isDefault func(entry string) bool) {
var oldSlice []string
if topChange == "*modified*" {
oldSlice = getOldSlice()
}
table := diffTable{options: options}
for _, entry := range newSlice {
entryChange := ""
if isDefault != nil && isDefault(entry) {
entryChange = "(default) "
}
if topChange == "*modified*" && !slices.Contains(oldSlice, entry) {
entryChange = "*new*"
}
table.add(entry, entryChange)
}
if topChange == "*modified*" {
for _, entry := range oldSlice {
if !slices.Contains(newSlice, entry) {
table.add(entry, "*deleted*")
}
}
}
table.print(w, header)
}
func sliceFromIterSeqPath(seq iter.Seq[tspath.Path]) []string {
var result []string
for path := range seq {
result = append(result, string(path))
}
slices.Sort(result)
return result
}
func printPathIterSeqWithDiffTable(w io.Writer, header string, newIterSeq iter.Seq[tspath.Path], getOldIterSeq func() iter.Seq[tspath.Path], options diffTableOptions, topChange string) {
printSlicesWithDiffTable(
w,
header,
sliceFromIterSeqPath(newIterSeq),
func() []string { return sliceFromIterSeqPath(getOldIterSeq()) },
options,
topChange,
nil,
)
}
func (f *FourslashTest) printStateDiff(t *testing.T, w io.Writer) {
if !f.stateBaseline.isInitialized {
return
}
session := f.server.Session()
snapshot, release := session.Snapshot()
defer release()
f.printProjectsDiff(t, snapshot, w)
f.printOpenFilesDiff(t, snapshot, w)
f.printConfigFileRegistryDiff(t, snapshot, w)
}
func (f *FourslashTest) printProjectsDiff(t *testing.T, snapshot *project.Snapshot, w io.Writer) {
t.Helper()
currentProjects := make(map[string]projectInfo)
options := diffTableOptions{indent: " "}
projectsDiffTable := newDiffTableWriter("Projects")
for _, project := range snapshot.ProjectCollection.Projects() {
program := project.GetProgram()
var oldProgram *compiler.Program
currentProjects[project.Name()] = program
projectChange := ""
if existing, ok := f.stateBaseline.serializedProjects[project.Name()]; ok {
oldProgram = existing
if oldProgram != program {
projectChange = "*modified*"
projectsDiffTable.setHasChange()
} else {
projectChange = ""
}
} else {
projectChange = "*new*"
projectsDiffTable.setHasChange()
}
projectsDiffTable.add(project.Name(), func(w io.Writer) {
fmt.Fprintf(w, " [%s] %s\n", project.Name(), projectChange)
subDiff := diffTable{options: options}
if program != nil {
for _, file := range program.GetSourceFiles() {
fileDiff := ""
// No need to write "*new*" for files as its obvious
fileName := file.FileName()
if projectChange == "*modified*" {
if oldProgram == nil {
if !isLibFile(fileName) {
fileDiff = "*new*"
}
} else if oldFile := oldProgram.GetSourceFileByPath(file.Path()); oldFile == nil {
fileDiff = "*new*"
} else if oldFile != file {
fileDiff = "*modified*"
}
}
if fileDiff != "" || !isLibFile(fileName) {
subDiff.add(fileName, fileDiff)
}
}
}
if oldProgram != program && oldProgram != nil {
for _, file := range oldProgram.GetSourceFiles() {
if program == nil || program.GetSourceFileByPath(file.Path()) == nil {
subDiff.add(file.FileName(), "*deleted*")
}
}
}
subDiff.print(w, "")
})
}
for projectName, info := range f.stateBaseline.serializedProjects {
if _, found := currentProjects[projectName]; !found {
projectsDiffTable.setHasChange()
projectsDiffTable.add(projectName, func(w io.Writer) {
fmt.Fprintf(w, " [%s] *deleted*\n", projectName)
subDiff := diffTable{options: options}
if info != nil {
for _, file := range info.GetSourceFiles() {
if fileName := file.FileName(); !isLibFile(fileName) {
subDiff.add(fileName, "")
}
}
}
subDiff.print(w, "")
})
}
}
f.stateBaseline.serializedProjects = currentProjects
projectsDiffTable.print(w)
}
func (f *FourslashTest) printOpenFilesDiff(t *testing.T, snapshot *project.Snapshot, w io.Writer) {
t.Helper()
currentOpenFiles := make(map[string]*openFileInfo)
filesDiffTable := newDiffTableWriter("Open Files")
options := diffTableOptions{indent: " ", sortKeys: true}
for fileName := range f.openFiles {
path := tspath.ToPath(fileName, "/", f.vfs.UseCaseSensitiveFileNames())
defaultProject := snapshot.ProjectCollection.GetDefaultProject(fileName, path)
newFileInfo := &openFileInfo{}
if defaultProject != nil {
newFileInfo.defaultProjectName = defaultProject.Name()
}
for _, project := range snapshot.ProjectCollection.Projects() {
if program := project.GetProgram(); program != nil && program.GetSourceFileByPath(path) != nil {
newFileInfo.allProjects = append(newFileInfo.allProjects, project.Name())
}
}
slices.Sort(newFileInfo.allProjects)
currentOpenFiles[fileName] = newFileInfo
openFileChange := ""
var oldFileInfo *openFileInfo
if existing, ok := f.stateBaseline.serializedOpenFiles[fileName]; ok {
oldFileInfo = existing
if existing.defaultProjectName != newFileInfo.defaultProjectName || !slices.Equal(existing.allProjects, newFileInfo.allProjects) {
openFileChange = "*modified*"
filesDiffTable.setHasChange()
} else {
openFileChange = ""
}
} else {
openFileChange = "*new*"
filesDiffTable.setHasChange()
}
filesDiffTable.add(fileName, func(w io.Writer) {
fmt.Fprintf(w, " [%s] %s\n", fileName, openFileChange)
printSlicesWithDiffTable(
w,
"",
newFileInfo.allProjects,
func() []string { return oldFileInfo.allProjects },
options,
openFileChange,
func(projectName string) bool { return projectName == newFileInfo.defaultProjectName },
)
})
}
for fileName := range f.stateBaseline.serializedOpenFiles {
if _, found := currentOpenFiles[fileName]; !found {
filesDiffTable.setHasChange()
filesDiffTable.add(fileName, func(w io.Writer) {
fmt.Fprintf(w, " [%s] *closed*\n", fileName)
})
}
}
f.stateBaseline.serializedOpenFiles = currentOpenFiles
filesDiffTable.print(w)
}
func (f *FourslashTest) printConfigFileRegistryDiff(t *testing.T, snapshot *project.Snapshot, w io.Writer) {
t.Helper()
configFileRegistry := snapshot.ProjectCollection.ConfigFileRegistry()
configDiffsTable := newDiffTableWriter("Config")
configFileNamesDiffsTable := newDiffTableWriter("Config File Names")
if f.stateBaseline.serializedConfigFileRegistry == configFileRegistry {
return
}
options := diffTableOptions{indent: " ", sortKeys: true}
configFileRegistry.ForEachTestConfigEntry(func(path tspath.Path, entry *project.TestConfigEntry) {
configChange := ""
oldEntry := f.stateBaseline.serializedConfigFileRegistry.GetTestConfigEntry(path)
if oldEntry == nil {
configChange = "*new*"
configDiffsTable.setHasChange()
} else if oldEntry != entry {
if !areIterSeqEqual(oldEntry.RetainingProjects, entry.RetainingProjects) ||
!areIterSeqEqual(oldEntry.RetainingOpenFiles, entry.RetainingOpenFiles) ||
!areIterSeqEqual(oldEntry.RetainingConfigs, entry.RetainingConfigs) {
configChange = "*modified*"
configDiffsTable.setHasChange()
}
}
configDiffsTable.add(string(path), func(w io.Writer) {
fmt.Fprintf(w, " [%s] %s\n", entry.FileName, configChange)
// Print the details of the config entry
var retainingProjectsModified string
var retainingOpenFilesModified string
var retainingConfigsModified string
if configChange == "*modified*" {
if !areIterSeqEqual(entry.RetainingProjects, oldEntry.RetainingProjects) {
retainingProjectsModified = " *modified*"
}
if !areIterSeqEqual(entry.RetainingOpenFiles, oldEntry.RetainingOpenFiles) {
retainingOpenFilesModified = " *modified*"
}
if !areIterSeqEqual(entry.RetainingConfigs, oldEntry.RetainingConfigs) {
retainingConfigsModified = " *modified*"
}
}
printPathIterSeqWithDiffTable(w, "RetainingProjects:"+retainingProjectsModified, entry.RetainingProjects, func() iter.Seq[tspath.Path] { return oldEntry.RetainingProjects }, options, configChange)
printPathIterSeqWithDiffTable(w, "RetainingOpenFiles:"+retainingOpenFilesModified, entry.RetainingOpenFiles, func() iter.Seq[tspath.Path] { return oldEntry.RetainingOpenFiles }, options, configChange)
printPathIterSeqWithDiffTable(w, "RetainingConfigs:"+retainingConfigsModified, entry.RetainingConfigs, func() iter.Seq[tspath.Path] { return oldEntry.RetainingConfigs }, options, configChange)
})
})
configFileRegistry.ForEachTestConfigFileNamesEntry(func(path tspath.Path, entry *project.TestConfigFileNamesEntry) {
configFileNamesChange := ""
oldEntry := f.stateBaseline.serializedConfigFileRegistry.GetTestConfigFileNamesEntry(path)
if oldEntry == nil {
configFileNamesChange = "*new*"
configFileNamesDiffsTable.setHasChange()
} else if oldEntry.NearestConfigFileName != entry.NearestConfigFileName ||
!maps.Equal(oldEntry.Ancestors, entry.Ancestors) {
configFileNamesChange = "*modified*"
configFileNamesDiffsTable.setHasChange()
}
configFileNamesDiffsTable.add(string(path), func(w io.Writer) {
fmt.Fprintf(w, " [%s] %s\n", path, configFileNamesChange)
var nearestConfigFileNameModified string
var ancestorDiffModified string
if configFileNamesChange == "*modified*" {
if oldEntry.NearestConfigFileName != entry.NearestConfigFileName {
nearestConfigFileNameModified = " *modified*"
}
if !maps.Equal(oldEntry.Ancestors, entry.Ancestors) {
ancestorDiffModified = " *modified*"
}
}
fmt.Fprintf(w, " NearestConfigFileName: %s%s\n", entry.NearestConfigFileName, nearestConfigFileNameModified)
ancestorDiff := diffTable{options: options}
for config, ancestorOfConfig := range entry.Ancestors {
ancestorChange := ""
if configFileNamesChange == "*modified*" {
if oldConfigFileName, ok := oldEntry.Ancestors[config]; ok {
if oldConfigFileName != ancestorOfConfig {
ancestorChange = "*modified*"
}
} else {
ancestorChange = "*new*"
}
}
ancestorDiff.add(config, fmt.Sprintf("%s %s", ancestorOfConfig, ancestorChange))
}
if configFileNamesChange == "*modified*" {
for ancestorPath, oldConfigFileName := range oldEntry.Ancestors {
if _, ok := entry.Ancestors[ancestorPath]; !ok {
ancestorDiff.add(ancestorPath, oldConfigFileName+" *deleted*")
}
}
}
ancestorDiff.print(w, "Ancestors:"+ancestorDiffModified)
})
})
f.stateBaseline.serializedConfigFileRegistry.ForEachTestConfigEntry(func(path tspath.Path, entry *project.TestConfigEntry) {
if configFileRegistry.GetTestConfigEntry(path) == nil {
configDiffsTable.setHasChange()
configDiffsTable.add(string(path), func(w io.Writer) {
fmt.Fprintf(w, " [%s] *deleted*\n", entry.FileName)
})
}
})
f.stateBaseline.serializedConfigFileRegistry.ForEachTestConfigFileNamesEntry(func(path tspath.Path, entry *project.TestConfigFileNamesEntry) {
if configFileRegistry.GetTestConfigFileNamesEntry(path) == nil {
configFileNamesDiffsTable.setHasChange()
configFileNamesDiffsTable.add(string(path), func(w io.Writer) {
fmt.Fprintf(w, " [%s] *deleted*\n", path)
})
}
})
f.stateBaseline.serializedConfigFileRegistry = configFileRegistry
configDiffsTable.print(w)
configFileNamesDiffsTable.print(w)
}