178 lines
5.1 KiB
Go
178 lines
5.1 KiB
Go
package fsbaselineutil
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"maps"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/microsoft/typescript-go/internal/collections"
|
|
"github.com/microsoft/typescript-go/internal/vfs/iovfs"
|
|
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
|
)
|
|
|
|
type DiffEntry struct {
|
|
Content string
|
|
MTime time.Time
|
|
IsWritten bool
|
|
SymlinkTarget string
|
|
}
|
|
|
|
type Snapshot struct {
|
|
Snap map[string]*DiffEntry
|
|
DefaultLibs *collections.SyncSet[string]
|
|
}
|
|
|
|
type FSDiffer struct {
|
|
FS iovfs.FsWithSys
|
|
DefaultLibs func() *collections.SyncSet[string]
|
|
WrittenFiles *collections.SyncSet[string]
|
|
|
|
serializedDiff *Snapshot
|
|
}
|
|
|
|
func (d *FSDiffer) MapFs() *vfstest.MapFS {
|
|
return d.FS.FSys().(*vfstest.MapFS)
|
|
}
|
|
|
|
func (d *FSDiffer) SerializedDiff() *Snapshot {
|
|
return d.serializedDiff
|
|
}
|
|
|
|
func (d *FSDiffer) BaselineFSwithDiff(baseline io.Writer) {
|
|
// todo: baselines the entire fs, possibly doesn't correctly diff all cases of emitted files, since emit isn't fully implemented and doesn't always emit the same way as strada
|
|
snap := map[string]*DiffEntry{}
|
|
|
|
diffs := map[string]string{}
|
|
|
|
for path, file := range d.MapFs().Entries() {
|
|
if file.Mode&fs.ModeSymlink != 0 {
|
|
target, ok := d.MapFs().GetTargetOfSymlink(path)
|
|
if !ok {
|
|
panic("Failed to resolve symlink target: " + path)
|
|
}
|
|
newEntry := &DiffEntry{SymlinkTarget: target}
|
|
snap[path] = newEntry
|
|
d.addFsEntryDiff(diffs, newEntry, path)
|
|
continue
|
|
} else if file.Mode.IsRegular() {
|
|
content := SanitizeInternalSymbolName(string(file.Data))
|
|
newEntry := &DiffEntry{Content: content, MTime: file.ModTime, IsWritten: d.WrittenFiles.Has(path)}
|
|
snap[path] = newEntry
|
|
d.addFsEntryDiff(diffs, newEntry, path)
|
|
}
|
|
}
|
|
if d.serializedDiff != nil {
|
|
for path := range d.serializedDiff.Snap {
|
|
if fileInfo := d.MapFs().GetFileInfo(path); fileInfo == nil {
|
|
// report deleted
|
|
d.addFsEntryDiff(diffs, nil, path)
|
|
}
|
|
}
|
|
}
|
|
var defaultLibs collections.SyncSet[string]
|
|
if d.DefaultLibs != nil && d.DefaultLibs() != nil {
|
|
d.DefaultLibs().Range(func(libPath string) bool {
|
|
defaultLibs.Add(libPath)
|
|
return true
|
|
})
|
|
}
|
|
d.serializedDiff = &Snapshot{
|
|
Snap: snap,
|
|
DefaultLibs: &defaultLibs,
|
|
}
|
|
diffKeys := slices.Collect(maps.Keys(diffs))
|
|
slices.Sort(diffKeys)
|
|
for _, path := range diffKeys {
|
|
fmt.Fprint(baseline, "//// ["+path+"] ", diffs[path], "\n")
|
|
}
|
|
fmt.Fprintln(baseline)
|
|
*d.WrittenFiles = collections.SyncSet[string]{} // Reset written files after baseline
|
|
}
|
|
|
|
var internalSymbolRegex = regexp.MustCompile(`\x{FFFD}@[^@]+@[0-9]+`)
|
|
|
|
// Replaces internal symbol names of shape \uFFFD@symbolName@123 with \uFFFD@symbolName@<symbolId>
|
|
// // to avoid baselining differences in symbol ids, which can change between runs.
|
|
func SanitizeInternalSymbolName(s string) string {
|
|
if !strings.Contains(s, "\uFFFD@") {
|
|
return s
|
|
}
|
|
return internalSymbolRegex.ReplaceAllStringFunc(s, func(match string) string {
|
|
idStart := strings.LastIndex(match, "@")
|
|
return match[:idStart] + "@<symbolId>"
|
|
})
|
|
}
|
|
|
|
func (d *FSDiffer) addFsEntryDiff(diffs map[string]string, newDirContent *DiffEntry, path string) {
|
|
var oldDirContent *DiffEntry
|
|
var defaultLibs *collections.SyncSet[string]
|
|
if d.serializedDiff != nil {
|
|
oldDirContent = d.serializedDiff.Snap[path]
|
|
defaultLibs = d.serializedDiff.DefaultLibs
|
|
}
|
|
// todo handle more cases of fs changes
|
|
if oldDirContent == nil {
|
|
if d.DefaultLibs == nil || d.DefaultLibs() == nil || !d.DefaultLibs().Has(path) {
|
|
if newDirContent.SymlinkTarget != "" {
|
|
diffs[path] = "-> " + newDirContent.SymlinkTarget + " *new*"
|
|
} else {
|
|
diffs[path] = "*new* \n" + newDirContent.Content
|
|
}
|
|
}
|
|
} else if newDirContent == nil {
|
|
diffs[path] = "*deleted*"
|
|
} else if newDirContent.Content != oldDirContent.Content {
|
|
diffs[path] = "*modified* \n" + newDirContent.Content
|
|
} else if newDirContent.IsWritten {
|
|
diffs[path] = "*rewrite with same content*"
|
|
} else if newDirContent.MTime != oldDirContent.MTime {
|
|
diffs[path] = "*mTime changed*"
|
|
} else if defaultLibs != nil && defaultLibs.Has(path) && d.DefaultLibs != nil && d.DefaultLibs() != nil && !d.DefaultLibs().Has(path) {
|
|
// Lib file that was read
|
|
diffs[path] = "*Lib*\n" + newDirContent.Content
|
|
}
|
|
}
|
|
|
|
// FileChange represents a filesystem change detected between snapshots.
|
|
type FileChange struct {
|
|
Path string
|
|
Deleted bool
|
|
}
|
|
|
|
func (d *FSDiffer) ChangedPaths() []FileChange {
|
|
if d.serializedDiff == nil {
|
|
return nil
|
|
}
|
|
|
|
var changes []FileChange
|
|
oldSnap := d.serializedDiff
|
|
|
|
// Check current files against previous snapshot.
|
|
for path, file := range d.MapFs().Entries() {
|
|
if file.Mode&fs.ModeSymlink != 0 || !file.Mode.IsRegular() {
|
|
continue
|
|
}
|
|
if old, ok := oldSnap.Snap[path]; !ok {
|
|
// New file.
|
|
changes = append(changes, FileChange{Path: path})
|
|
} else if string(file.Data) != old.Content || file.ModTime != old.MTime {
|
|
// Modified or touched file.
|
|
changes = append(changes, FileChange{Path: path})
|
|
}
|
|
}
|
|
|
|
// Check for deleted files.
|
|
for path := range oldSnap.Snap {
|
|
if fileInfo := d.MapFs().GetFileInfo(path); fileInfo == nil {
|
|
changes = append(changes, FileChange{Path: path, Deleted: true})
|
|
}
|
|
}
|
|
|
|
return changes
|
|
}
|