vendor tsgo
This commit is contained in:
610
tools/tsgo/internal/testutil/autoimporttestutil/fixtures.go
Normal file
610
tools/tsgo/internal/testutil/autoimporttestutil/fixtures.go
Normal file
@@ -0,0 +1,610 @@
|
||||
package autoimporttestutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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/projecttestutil"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
)
|
||||
|
||||
// FileHandle represents a file created for an autoimport lifecycle test.
|
||||
type FileHandle struct {
|
||||
fileName string
|
||||
content string
|
||||
}
|
||||
|
||||
func (f FileHandle) FileName() string { return f.fileName }
|
||||
func (f FileHandle) Content() string { return f.content }
|
||||
func (f FileHandle) URI() lsproto.DocumentUri { return lsconv.FileNameToDocumentURI(f.fileName) }
|
||||
|
||||
// ProjectFileHandle adds export metadata for TypeScript source files.
|
||||
type ProjectFileHandle struct {
|
||||
FileHandle
|
||||
exportIdentifier string
|
||||
}
|
||||
|
||||
// NodeModulesPackageHandle describes a generated package under node_modules.
|
||||
type NodeModulesPackageHandle struct {
|
||||
Name string
|
||||
Directory string
|
||||
packageJSON FileHandle
|
||||
declaration FileHandle
|
||||
}
|
||||
|
||||
func (p NodeModulesPackageHandle) PackageJSONFile() FileHandle { return p.packageJSON }
|
||||
func (p NodeModulesPackageHandle) DeclarationFile() FileHandle { return p.declaration }
|
||||
|
||||
// MonorepoHandle exposes the generated monorepo layout including root and packages.
|
||||
type MonorepoHandle struct {
|
||||
root string
|
||||
rootNodeModules []NodeModulesPackageHandle
|
||||
rootDependencies []string
|
||||
packages []ProjectHandle
|
||||
rootTSConfig FileHandle
|
||||
rootPackageJSON FileHandle
|
||||
}
|
||||
|
||||
func (m MonorepoHandle) Root() string { return m.root }
|
||||
func (m MonorepoHandle) RootNodeModules() []NodeModulesPackageHandle {
|
||||
return slices.Clone(m.rootNodeModules)
|
||||
}
|
||||
func (m MonorepoHandle) RootDependencies() []string { return slices.Clone(m.rootDependencies) }
|
||||
func (m MonorepoHandle) Packages() []ProjectHandle { return slices.Clone(m.packages) }
|
||||
func (m MonorepoHandle) Package(index int) ProjectHandle {
|
||||
if index < 0 || index >= len(m.packages) {
|
||||
panic(fmt.Sprintf("package index %d out of range", index))
|
||||
}
|
||||
return m.packages[index]
|
||||
}
|
||||
func (m MonorepoHandle) RootTSConfig() FileHandle { return m.rootTSConfig }
|
||||
func (m MonorepoHandle) RootPackageJSONFile() FileHandle { return m.rootPackageJSON }
|
||||
|
||||
// ProjectHandle exposes the generated project layout for a fixture project root.
|
||||
type ProjectHandle struct {
|
||||
root string
|
||||
files []ProjectFileHandle
|
||||
tsconfig FileHandle
|
||||
packageJSON FileHandle
|
||||
nodeModules []NodeModulesPackageHandle
|
||||
dependencies []string
|
||||
}
|
||||
|
||||
func (p ProjectHandle) Root() string { return p.root }
|
||||
func (p ProjectHandle) Files() []ProjectFileHandle { return slices.Clone(p.files) }
|
||||
func (p ProjectHandle) File(index int) ProjectFileHandle {
|
||||
if index < 0 || index >= len(p.files) {
|
||||
panic(fmt.Sprintf("file index %d out of range", index))
|
||||
}
|
||||
return p.files[index]
|
||||
}
|
||||
func (p ProjectHandle) TSConfig() FileHandle { return p.tsconfig }
|
||||
func (p ProjectHandle) PackageJSONFile() FileHandle { return p.packageJSON }
|
||||
func (p ProjectHandle) NodeModules() []NodeModulesPackageHandle {
|
||||
return slices.Clone(p.nodeModules)
|
||||
}
|
||||
func (p ProjectHandle) Dependencies() []string { return slices.Clone(p.dependencies) }
|
||||
|
||||
func (p ProjectHandle) NodeModuleByName(name string) *NodeModulesPackageHandle {
|
||||
for i := range p.nodeModules {
|
||||
if p.nodeModules[i].Name == name {
|
||||
return &p.nodeModules[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fixture encapsulates a fully-initialized auto import lifecycle test session.
|
||||
type Fixture struct {
|
||||
session *project.Session
|
||||
utils *projecttestutil.SessionUtils
|
||||
projects []ProjectHandle
|
||||
}
|
||||
|
||||
func (f *Fixture) Session() *project.Session { return f.session }
|
||||
func (f *Fixture) Utils() *projecttestutil.SessionUtils { return f.utils }
|
||||
func (f *Fixture) Projects() []ProjectHandle { return slices.Clone(f.projects) }
|
||||
func (f *Fixture) Project(index int) ProjectHandle {
|
||||
if index < 0 || index >= len(f.projects) {
|
||||
panic(fmt.Sprintf("project index %d out of range", index))
|
||||
}
|
||||
return f.projects[index]
|
||||
}
|
||||
func (f *Fixture) SingleProject() ProjectHandle { return f.Project(0) }
|
||||
|
||||
// MonorepoFixture encapsulates a fully-initialized monorepo lifecycle test session.
|
||||
type MonorepoFixture struct {
|
||||
session *project.Session
|
||||
utils *projecttestutil.SessionUtils
|
||||
monorepo MonorepoHandle
|
||||
extra []FileHandle
|
||||
}
|
||||
|
||||
func (f *MonorepoFixture) Session() *project.Session { return f.session }
|
||||
func (f *MonorepoFixture) Utils() *projecttestutil.SessionUtils { return f.utils }
|
||||
func (f *MonorepoFixture) Monorepo() MonorepoHandle { return f.monorepo }
|
||||
func (f *MonorepoFixture) ExtraFiles() []FileHandle { return slices.Clone(f.extra) }
|
||||
func (f *MonorepoFixture) ExtraFile(path string) FileHandle {
|
||||
normalized := normalizeAbsolutePath(path)
|
||||
for _, handle := range f.extra {
|
||||
if handle.fileName == normalized {
|
||||
return handle
|
||||
}
|
||||
}
|
||||
panic("extra file not found: " + path)
|
||||
}
|
||||
|
||||
// MonorepoPackageTemplate captures the reusable settings for a package.json scope:
|
||||
// the node_modules packages that exist alongside the package.json and the dependency
|
||||
// names that should be written into that package.json. When DependencyNames is empty,
|
||||
// all available node_modules packages in scope are used.
|
||||
type MonorepoPackageTemplate struct {
|
||||
Name string
|
||||
NodeModuleNames []string
|
||||
DependencyNames []string
|
||||
}
|
||||
|
||||
// MonorepoSetupConfig describes the monorepo root and packages to create.
|
||||
// The embedded MonorepoPackageTemplate describes the monorepo root package located at
|
||||
// Root. DependencyNames defaults to NodeModuleNames when empty.
|
||||
// Package.MonorepoPackageTemplate.DependencyNames defaults to the union of the root
|
||||
// node_modules packages and the package's own NodeModuleNames when empty.
|
||||
type MonorepoSetupConfig struct {
|
||||
Root string
|
||||
MonorepoPackageTemplate
|
||||
Packages []MonorepoPackageConfig
|
||||
ExtraFiles []TextFileSpec
|
||||
Symlinks []SymlinkSpec
|
||||
}
|
||||
|
||||
type MonorepoPackageConfig struct {
|
||||
FileCount int
|
||||
MonorepoPackageTemplate
|
||||
}
|
||||
|
||||
// TextFileSpec describes an additional file to place in the fixture.
|
||||
type TextFileSpec struct {
|
||||
Path string
|
||||
Content string
|
||||
}
|
||||
|
||||
// SymlinkSpec describes a symlink to create in the fixture.
|
||||
type SymlinkSpec struct {
|
||||
Link string // The symlink path
|
||||
Target string // The target path the symlink points to
|
||||
}
|
||||
|
||||
// SetupMonorepoLifecycleSession builds a monorepo workspace with root-level node_modules
|
||||
// and multiple packages, each potentially with their own node_modules.
|
||||
// The structure is:
|
||||
//
|
||||
// root/
|
||||
// ├── tsconfig.json (base config)
|
||||
// ├── package.json
|
||||
// ├── node_modules/
|
||||
// │ └── <rootNodeModuleCount packages>
|
||||
// └── packages/
|
||||
// ├── package-a/
|
||||
// │ ├── tsconfig.json
|
||||
// │ ├── package.json
|
||||
// │ ├── node_modules/
|
||||
// │ │ └── <package-specific packages>
|
||||
// │ └── *.ts files
|
||||
// └── package-b/
|
||||
// └── ...
|
||||
func SetupMonorepoLifecycleSession(t *testing.T, config MonorepoSetupConfig) *MonorepoFixture {
|
||||
t.Helper()
|
||||
builder := newFileMapBuilder(nil)
|
||||
|
||||
monorepoRoot := normalizeAbsolutePath(config.Root)
|
||||
monorepoName := config.MonorepoPackageTemplate.Name
|
||||
if monorepoName == "" {
|
||||
monorepoName = "monorepo"
|
||||
}
|
||||
|
||||
// Add root tsconfig.json
|
||||
rootTSConfigPath := tspath.CombinePaths(monorepoRoot, "tsconfig.json")
|
||||
rootTSConfigContent := "{\n \"compilerOptions\": {\n \"module\": \"esnext\",\n \"target\": \"esnext\",\n \"strict\": true,\n \"baseUrl\": \".\",\n \"allowJs\": true,\n \"checkJs\": true\n }\n}\n"
|
||||
builder.AddTextFile(rootTSConfigPath, rootTSConfigContent)
|
||||
rootTSConfig := FileHandle{fileName: rootTSConfigPath, content: rootTSConfigContent}
|
||||
|
||||
// Add root node_modules
|
||||
rootNodeModulesDir := tspath.CombinePaths(monorepoRoot, "node_modules")
|
||||
rootNodeModules := builder.AddNodeModulesPackagesWithNames(rootNodeModulesDir, config.NodeModuleNames)
|
||||
|
||||
// Add root package.json with dependencies (default to all root node_modules if unspecified)
|
||||
rootDependencies := selectPackagesByName(rootNodeModules, config.DependencyNames)
|
||||
rootPackageJSON := builder.addRootPackageJSON(monorepoRoot, monorepoName, rootDependencies)
|
||||
rootDependencyNames := packageNames(rootDependencies)
|
||||
|
||||
// Build each package in packages/
|
||||
packagesDir := tspath.CombinePaths(monorepoRoot, "packages")
|
||||
packageHandles := make([]ProjectHandle, 0, len(config.Packages))
|
||||
for _, pkg := range config.Packages {
|
||||
pkgDir := tspath.CombinePaths(packagesDir, pkg.Name)
|
||||
builder.AddLocalProject(pkgDir, pkg.FileCount)
|
||||
|
||||
var pkgNodeModules []NodeModulesPackageHandle
|
||||
if len(pkg.NodeModuleNames) > 0 {
|
||||
pkgNodeModulesDir := tspath.CombinePaths(pkgDir, "node_modules")
|
||||
pkgNodeModules = builder.AddNodeModulesPackagesWithNames(pkgNodeModulesDir, pkg.NodeModuleNames)
|
||||
}
|
||||
|
||||
availableDeps := append(slices.Clone(rootNodeModules), pkgNodeModules...)
|
||||
selectedDeps := selectPackagesByName(availableDeps, pkg.DependencyNames)
|
||||
if len(selectedDeps) > 0 {
|
||||
builder.AddPackageJSONWithDependenciesNamed(pkgDir, pkg.Name, selectedDeps)
|
||||
}
|
||||
}
|
||||
|
||||
// Add arbitrary extra files
|
||||
extraHandles := make([]FileHandle, 0, len(config.ExtraFiles))
|
||||
for _, extra := range config.ExtraFiles {
|
||||
builder.AddTextFile(extra.Path, extra.Content)
|
||||
extraHandles = append(extraHandles, FileHandle{fileName: normalizeAbsolutePath(extra.Path), content: extra.Content})
|
||||
}
|
||||
|
||||
// Add symlinks
|
||||
for _, symlink := range config.Symlinks {
|
||||
builder.AddSymlink(symlink.Link, symlink.Target)
|
||||
}
|
||||
|
||||
// Build project handles after all packages are created
|
||||
for _, pkg := range config.Packages {
|
||||
pkgDir := tspath.CombinePaths(packagesDir, pkg.Name)
|
||||
if record, ok := builder.projects[pkgDir]; ok {
|
||||
packageHandles = append(packageHandles, record.toHandles())
|
||||
}
|
||||
}
|
||||
|
||||
session, sessionUtils := projecttestutil.Setup(builder.Files())
|
||||
t.Cleanup(session.Close)
|
||||
|
||||
// Build root node_modules handle by looking at the project record for the workspace root
|
||||
// (created as side effect of AddNodeModulesPackages)
|
||||
var rootNodeModulesHandles []NodeModulesPackageHandle
|
||||
if rootRecord, ok := builder.projects[monorepoRoot]; ok {
|
||||
rootNodeModulesHandles = rootRecord.nodeModules
|
||||
}
|
||||
|
||||
return &MonorepoFixture{
|
||||
session: session,
|
||||
utils: sessionUtils,
|
||||
monorepo: MonorepoHandle{
|
||||
root: monorepoRoot,
|
||||
rootNodeModules: rootNodeModulesHandles,
|
||||
rootDependencies: rootDependencyNames,
|
||||
packages: packageHandles,
|
||||
rootTSConfig: rootTSConfig,
|
||||
rootPackageJSON: rootPackageJSON,
|
||||
},
|
||||
extra: extraHandles,
|
||||
}
|
||||
}
|
||||
|
||||
// SetupLifecycleSession builds a basic single-project workspace configured with the
|
||||
// requested number of TypeScript files and a single synthetic node_modules package.
|
||||
func SetupLifecycleSession(t *testing.T, projectRoot string, fileCount int) *Fixture {
|
||||
t.Helper()
|
||||
builder := newFileMapBuilder(nil)
|
||||
builder.AddLocalProject(projectRoot, fileCount)
|
||||
nodeModulesDir := tspath.CombinePaths(projectRoot, "node_modules")
|
||||
deps := builder.AddNodeModulesPackages(nodeModulesDir, 1)
|
||||
builder.AddPackageJSONWithDependencies(projectRoot, deps)
|
||||
session, sessionUtils := projecttestutil.Setup(builder.Files())
|
||||
t.Cleanup(session.Close)
|
||||
return &Fixture{
|
||||
session: session,
|
||||
utils: sessionUtils,
|
||||
projects: builder.projectHandles(),
|
||||
}
|
||||
}
|
||||
|
||||
type fileMapBuilder struct {
|
||||
files map[string]any
|
||||
nextPackageID int
|
||||
nextProjectID int
|
||||
projects map[string]*projectRecord
|
||||
}
|
||||
|
||||
type projectRecord struct {
|
||||
root string
|
||||
sourceFiles []projectFile
|
||||
tsconfig FileHandle
|
||||
packageJSON *FileHandle
|
||||
nodeModules []NodeModulesPackageHandle
|
||||
dependencies []string
|
||||
}
|
||||
|
||||
type projectFile struct {
|
||||
FileName string
|
||||
ExportIdentifier string
|
||||
Content string
|
||||
}
|
||||
|
||||
func newFileMapBuilder(initial map[string]any) *fileMapBuilder {
|
||||
b := &fileMapBuilder{
|
||||
files: make(map[string]any),
|
||||
projects: make(map[string]*projectRecord),
|
||||
}
|
||||
if len(initial) == 0 {
|
||||
return b
|
||||
}
|
||||
for path, content := range initial {
|
||||
b.files[normalizeAbsolutePath(path)] = content
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *fileMapBuilder) ensureProjectRecord(root string) *projectRecord {
|
||||
if record, ok := b.projects[root]; ok {
|
||||
return record
|
||||
}
|
||||
record := &projectRecord{root: root}
|
||||
b.projects[root] = record
|
||||
return record
|
||||
}
|
||||
|
||||
func (b *fileMapBuilder) projectHandles() []ProjectHandle {
|
||||
keys := slices.Collect(maps.Keys(b.projects))
|
||||
slices.Sort(keys)
|
||||
result := make([]ProjectHandle, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
result = append(result, b.projects[key].toHandles())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *projectRecord) toHandles() ProjectHandle {
|
||||
files := make([]ProjectFileHandle, len(r.sourceFiles))
|
||||
for i, file := range r.sourceFiles {
|
||||
files[i] = ProjectFileHandle{
|
||||
FileHandle: FileHandle{fileName: file.FileName, content: file.Content},
|
||||
exportIdentifier: file.ExportIdentifier,
|
||||
}
|
||||
}
|
||||
packageJSON := FileHandle{}
|
||||
if r.packageJSON != nil {
|
||||
packageJSON = *r.packageJSON
|
||||
}
|
||||
return ProjectHandle{
|
||||
root: r.root,
|
||||
files: files,
|
||||
tsconfig: r.tsconfig,
|
||||
packageJSON: packageJSON,
|
||||
nodeModules: slices.Clone(r.nodeModules),
|
||||
dependencies: slices.Clone(r.dependencies),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *fileMapBuilder) Files() map[string]any {
|
||||
return maps.Clone(b.files)
|
||||
}
|
||||
|
||||
func (b *fileMapBuilder) AddTextFile(path string, contents string) {
|
||||
b.ensureFiles()
|
||||
b.files[normalizeAbsolutePath(path)] = contents
|
||||
}
|
||||
|
||||
// AddSymlink creates a symlink from linkPath to targetPath.
|
||||
// The targetPath should be an absolute path.
|
||||
func (b *fileMapBuilder) AddSymlink(linkPath string, targetPath string) {
|
||||
b.ensureFiles()
|
||||
b.files[normalizeAbsolutePath(linkPath)] = vfstest.Symlink(normalizeAbsolutePath(targetPath))
|
||||
}
|
||||
|
||||
func (b *fileMapBuilder) AddNodeModulesPackages(nodeModulesDir string, count int) []NodeModulesPackageHandle {
|
||||
packages := make([]NodeModulesPackageHandle, 0, count)
|
||||
for range count {
|
||||
packages = append(packages, b.AddNodeModulesPackage(nodeModulesDir))
|
||||
}
|
||||
return packages
|
||||
}
|
||||
|
||||
func (b *fileMapBuilder) AddNodeModulesPackagesWithNames(nodeModulesDir string, names []string) []NodeModulesPackageHandle {
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
packages := make([]NodeModulesPackageHandle, 0, len(names))
|
||||
for _, name := range names {
|
||||
packages = append(packages, b.AddNamedNodeModulesPackage(nodeModulesDir, name))
|
||||
}
|
||||
return packages
|
||||
}
|
||||
|
||||
func (b *fileMapBuilder) AddNodeModulesPackage(nodeModulesDir string) NodeModulesPackageHandle {
|
||||
return b.AddNamedNodeModulesPackage(nodeModulesDir, "")
|
||||
}
|
||||
|
||||
func (b *fileMapBuilder) AddNamedNodeModulesPackage(nodeModulesDir string, name string) NodeModulesPackageHandle {
|
||||
b.ensureFiles()
|
||||
normalizedDir := normalizeAbsolutePath(nodeModulesDir)
|
||||
if tspath.GetBaseFileName(normalizedDir) != "node_modules" {
|
||||
panic("nodeModulesDir must point to a node_modules directory: " + nodeModulesDir)
|
||||
}
|
||||
b.nextPackageID++
|
||||
resolvedName := name
|
||||
if resolvedName == "" {
|
||||
resolvedName = fmt.Sprintf("pkg%d", b.nextPackageID)
|
||||
}
|
||||
exportName := sanitizeIdentifier(resolvedName) + "_value"
|
||||
pkgDir := tspath.CombinePaths(normalizedDir, resolvedName)
|
||||
packageJSONPath := tspath.CombinePaths(pkgDir, "package.json")
|
||||
packageJSONContent := fmt.Sprintf(`{"name":"%s","types":"index.d.ts"}`, resolvedName)
|
||||
b.files[packageJSONPath] = packageJSONContent
|
||||
declarationPath := tspath.CombinePaths(pkgDir, "index.d.ts")
|
||||
declarationContent := fmt.Sprintf("export declare const %s: number;\n", exportName)
|
||||
b.files[declarationPath] = declarationContent
|
||||
packageHandle := NodeModulesPackageHandle{
|
||||
Name: resolvedName,
|
||||
Directory: pkgDir,
|
||||
packageJSON: FileHandle{fileName: packageJSONPath, content: packageJSONContent},
|
||||
declaration: FileHandle{fileName: declarationPath, content: declarationContent},
|
||||
}
|
||||
projectRoot := tspath.GetDirectoryPath(normalizedDir)
|
||||
record := b.ensureProjectRecord(projectRoot)
|
||||
record.nodeModules = append(record.nodeModules, packageHandle)
|
||||
return packageHandle
|
||||
}
|
||||
|
||||
func (b *fileMapBuilder) AddLocalProject(projectDir string, fileCount int) {
|
||||
b.ensureFiles()
|
||||
if fileCount < 0 {
|
||||
panic("fileCount must be non-negative")
|
||||
}
|
||||
dir := normalizeAbsolutePath(projectDir)
|
||||
record := b.ensureProjectRecord(dir)
|
||||
b.nextProjectID++
|
||||
tsConfigPath := tspath.CombinePaths(dir, "tsconfig.json")
|
||||
tsConfigContent := "{\n \"compilerOptions\": {\n \"module\": \"esnext\",\n \"target\": \"esnext\",\n \"strict\": true,\n \"allowJs\": true,\n \"checkJs\": true\n }\n}\n"
|
||||
b.files[tsConfigPath] = tsConfigContent
|
||||
record.tsconfig = FileHandle{fileName: tsConfigPath, content: tsConfigContent}
|
||||
for i := 1; i <= fileCount; i++ {
|
||||
path := tspath.CombinePaths(dir, fmt.Sprintf("file%d.ts", i))
|
||||
exportName := fmt.Sprintf("localExport%d_%d", b.nextProjectID, i)
|
||||
content := fmt.Sprintf("export const %s = %d;\n", exportName, i)
|
||||
b.files[path] = content
|
||||
record.sourceFiles = append(record.sourceFiles, projectFile{FileName: path, ExportIdentifier: exportName, Content: content})
|
||||
}
|
||||
}
|
||||
|
||||
func (b *fileMapBuilder) AddPackageJSONWithDependencies(projectDir string, deps []NodeModulesPackageHandle) FileHandle {
|
||||
b.nextProjectID++
|
||||
return b.AddPackageJSONWithDependenciesNamed(projectDir, fmt.Sprintf("local-project-%d", b.nextProjectID), deps)
|
||||
}
|
||||
|
||||
func (b *fileMapBuilder) AddPackageJSONWithDependenciesNamed(projectDir string, packageName string, deps []NodeModulesPackageHandle) FileHandle {
|
||||
b.ensureFiles()
|
||||
dir := normalizeAbsolutePath(projectDir)
|
||||
packageJSONPath := tspath.CombinePaths(dir, "package.json")
|
||||
dependencyLines := make([]string, 0, len(deps))
|
||||
for _, dep := range deps {
|
||||
dependencyLines = append(dependencyLines, fmt.Sprintf("\"%s\": \"*\"", dep.Name))
|
||||
}
|
||||
var builder strings.Builder
|
||||
name := packageName
|
||||
if name == "" {
|
||||
b.nextProjectID++
|
||||
name = fmt.Sprintf("local-project-%d", b.nextProjectID)
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("{\n \"name\": \"%s\"", name))
|
||||
if len(dependencyLines) > 0 {
|
||||
builder.WriteString(",\n \"dependencies\": {\n ")
|
||||
builder.WriteString(strings.Join(dependencyLines, ",\n "))
|
||||
builder.WriteString("\n }\n")
|
||||
} else {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString("}\n")
|
||||
content := builder.String()
|
||||
b.files[packageJSONPath] = content
|
||||
record := b.ensureProjectRecord(dir)
|
||||
packageHandle := FileHandle{fileName: packageJSONPath, content: content}
|
||||
record.packageJSON = &packageHandle
|
||||
record.dependencies = packageNames(deps)
|
||||
return packageHandle
|
||||
}
|
||||
|
||||
// addRootPackageJSON creates a root package.json for a monorepo without creating a project record.
|
||||
// This is used to set up the root workspace config without treating it as a project.
|
||||
func (b *fileMapBuilder) addRootPackageJSON(rootDir string, packageName string, deps []NodeModulesPackageHandle) FileHandle {
|
||||
b.ensureFiles()
|
||||
dir := normalizeAbsolutePath(rootDir)
|
||||
packageJSONPath := tspath.CombinePaths(dir, "package.json")
|
||||
dependencyLines := make([]string, 0, len(deps))
|
||||
for _, dep := range deps {
|
||||
dependencyLines = append(dependencyLines, fmt.Sprintf("\"%s\": \"*\"", dep.Name))
|
||||
}
|
||||
var builder strings.Builder
|
||||
pkgName := packageName
|
||||
if pkgName == "" {
|
||||
pkgName = "monorepo-root"
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("{\n \"name\": \"%s\",\n \"private\": true", pkgName))
|
||||
if len(dependencyLines) > 0 {
|
||||
builder.WriteString(",\n \"dependencies\": {\n ")
|
||||
builder.WriteString(strings.Join(dependencyLines, ",\n "))
|
||||
builder.WriteString("\n }\n")
|
||||
} else {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString("}\n")
|
||||
content := builder.String()
|
||||
b.files[packageJSONPath] = content
|
||||
return FileHandle{fileName: packageJSONPath, content: content}
|
||||
}
|
||||
|
||||
func selectPackagesByName(available []NodeModulesPackageHandle, names []string) []NodeModulesPackageHandle {
|
||||
if len(names) == 0 {
|
||||
return slices.Clone(available)
|
||||
}
|
||||
result := make([]NodeModulesPackageHandle, 0, len(names))
|
||||
for _, name := range names {
|
||||
found := false
|
||||
for _, candidate := range available {
|
||||
if candidate.Name == name {
|
||||
result = append(result, candidate)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
panic("dependency not found: " + name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func packageNames(deps []NodeModulesPackageHandle) []string {
|
||||
if len(deps) == 0 {
|
||||
return nil
|
||||
}
|
||||
names := make([]string, 0, len(deps))
|
||||
for _, dep := range deps {
|
||||
names = append(names, dep.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func sanitizeIdentifier(name string) string {
|
||||
sanitized := strings.Map(func(r rune) rune {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return r
|
||||
}
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return r
|
||||
}
|
||||
if r >= '0' && r <= '9' {
|
||||
return r
|
||||
}
|
||||
if r == '_' || r == '-' {
|
||||
return '_'
|
||||
}
|
||||
return -1
|
||||
}, name)
|
||||
if sanitized == "" {
|
||||
return "pkg"
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
func (b *fileMapBuilder) ensureFiles() {
|
||||
if b.files == nil {
|
||||
b.files = make(map[string]any)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAbsolutePath(path string) string {
|
||||
normalized := tspath.NormalizePath(path)
|
||||
if !tspath.PathIsAbsolute(normalized) {
|
||||
panic("paths used in lifecycle tests must be absolute: " + path)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
250
tools/tsgo/internal/testutil/baseline/baseline.go
Normal file
250
tools/tsgo/internal/testutil/baseline/baseline.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package baseline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/repo"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
"github.com/peter-evans/patience"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Subfolder string
|
||||
IsSubmodule bool
|
||||
IsSubmoduleAccepted bool
|
||||
IsSubmoduleTriaged bool
|
||||
DiffFixupOld func(string) string
|
||||
DiffFixupNew func(string) string
|
||||
SkipDiffWithOld bool
|
||||
}
|
||||
|
||||
const NoContent = "<no content>"
|
||||
|
||||
func Run(t *testing.T, fileName string, actual string, opts Options) {
|
||||
origSubfolder := opts.Subfolder
|
||||
|
||||
{
|
||||
subfolder := opts.Subfolder
|
||||
if opts.IsSubmodule {
|
||||
subfolder = filepath.Join("submodule", subfolder)
|
||||
}
|
||||
|
||||
localPath := filepath.Join(localRoot, subfolder, fileName)
|
||||
referencePath := filepath.Join(referenceRoot, subfolder, fileName)
|
||||
|
||||
// Record this baseline for tracking unused baselines
|
||||
recordBaseline(t, filepath.Join(subfolder, fileName))
|
||||
|
||||
writeComparison(t, actual, localPath, referencePath, false)
|
||||
}
|
||||
|
||||
if !opts.IsSubmodule || opts.SkipDiffWithOld {
|
||||
// Not a submodule, no diffs.
|
||||
return
|
||||
}
|
||||
|
||||
submoduleReference := filepath.Join(submoduleReferenceRoot, fileName)
|
||||
submoduleExpected := readFileOrNoContent(submoduleReference)
|
||||
|
||||
const (
|
||||
submoduleFolder = "submodule"
|
||||
submoduleAcceptedFolder = "submoduleAccepted"
|
||||
submoduleTriagedFolder = "submoduleTriaged"
|
||||
)
|
||||
|
||||
diffFileName := fileName + ".diff"
|
||||
diffKey := origSubfolder + "/" + diffFileName
|
||||
isSubmoduleAccepted := opts.IsSubmoduleAccepted || submoduleAcceptedFileNames().Has(diffKey)
|
||||
isSubmoduleTriaged := opts.IsSubmoduleTriaged || submoduleTriagedFileNames().Has(diffKey)
|
||||
|
||||
if isSubmoduleAccepted && isSubmoduleTriaged {
|
||||
t.Fatalf("diff file %s/%s is in both submoduleAccepted and submoduleTriaged; it should only be in one", origSubfolder, diffFileName)
|
||||
}
|
||||
|
||||
var outRoot string
|
||||
switch {
|
||||
case isSubmoduleAccepted:
|
||||
outRoot = submoduleAcceptedFolder
|
||||
case isSubmoduleTriaged:
|
||||
outRoot = submoduleTriagedFolder
|
||||
default:
|
||||
outRoot = submoduleFolder
|
||||
}
|
||||
|
||||
allRoots := [3]string{submoduleFolder, submoduleAcceptedFolder, submoduleTriagedFolder}
|
||||
|
||||
diff := getBaselineDiff(t, actual, submoduleExpected, fileName, opts.DiffFixupOld, opts.DiffFixupNew)
|
||||
|
||||
for _, root := range allRoots {
|
||||
localPath := filepath.Join(localRoot, root, origSubfolder, diffFileName)
|
||||
referencePath := filepath.Join(referenceRoot, root, origSubfolder, diffFileName)
|
||||
|
||||
// Record this baseline for tracking unused baselines
|
||||
recordBaseline(t, filepath.Join(root, origSubfolder, diffFileName))
|
||||
|
||||
if root == outRoot {
|
||||
writeComparison(t, diff, localPath, referencePath, false)
|
||||
} else {
|
||||
writeComparison(t, NoContent, localPath, referencePath, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var submoduleAcceptedFileNames = sync.OnceValue(func() *collections.Set[string] {
|
||||
return readFileNameSet(filepath.Join(repo.TestDataPath(), "submoduleAccepted.txt"))
|
||||
})
|
||||
|
||||
var submoduleTriagedFileNames = sync.OnceValue(func() *collections.Set[string] {
|
||||
return readFileNameSet(filepath.Join(repo.TestDataPath(), "submoduleTriaged.txt"))
|
||||
})
|
||||
|
||||
func readFileNameSet(path string) *collections.Set[string] {
|
||||
var set collections.Set[string]
|
||||
|
||||
if content, err := os.ReadFile(path); err == nil {
|
||||
for line := range strings.SplitSeq(string(content), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
set.Add(line)
|
||||
}
|
||||
} else {
|
||||
panic(fmt.Sprintf("failed to read file %s: %v", path, err))
|
||||
}
|
||||
|
||||
return &set
|
||||
}
|
||||
|
||||
func readFileOrNoContent(fileName string) string {
|
||||
content, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return NoContent
|
||||
}
|
||||
return string(content)
|
||||
}
|
||||
|
||||
func DiffText(oldName string, newName string, expected string, actual string) string {
|
||||
lines := patience.Diff(stringutil.SplitLines(expected), stringutil.SplitLines(actual))
|
||||
return patience.UnifiedDiffTextWithOptions(lines, patience.UnifiedDiffOptions{
|
||||
Precontext: 3,
|
||||
Postcontext: 3,
|
||||
SrcHeader: oldName,
|
||||
DstHeader: newName,
|
||||
})
|
||||
}
|
||||
|
||||
func getBaselineDiff(t *testing.T, actual string, expected string, fileName string, fixupOld func(string) string, fixupNew func(string) string) string {
|
||||
if fixupOld != nil {
|
||||
expected = fixupOld(expected)
|
||||
}
|
||||
if fixupNew != nil {
|
||||
actual = fixupNew(actual)
|
||||
}
|
||||
if actual == expected {
|
||||
return NoContent
|
||||
}
|
||||
s := DiffText("old."+fileName, "new."+fileName, expected, actual)
|
||||
|
||||
// If the diff is empty (just headers, no hunks), return NoContent
|
||||
if !strings.Contains(s, "@@") {
|
||||
return NoContent
|
||||
}
|
||||
|
||||
// Remove line numbers from unified diff headers; this avoids adding/deleting
|
||||
// lines in our baselines from causing knock-on header changes later in the diff.
|
||||
|
||||
aCurLine := 1
|
||||
bCurLine := 1
|
||||
s = fixUnifiedDiff.ReplaceAllStringFunc(s, func(match string) string {
|
||||
var aLine, aLineCount, bLine, bLineCount int
|
||||
if _, err := fmt.Sscanf(match, "@@ -%d,%d +%d,%d @@", &aLine, &aLineCount, &bLine, &bLineCount); err != nil {
|
||||
panic(fmt.Sprintf("failed to parse unified diff header: %v", err))
|
||||
}
|
||||
aDiff := aLine - aCurLine
|
||||
bDiff := bLine - bCurLine
|
||||
aCurLine = aLine
|
||||
bCurLine = bLine
|
||||
|
||||
// Keep surrounded by @@, to make GitHub's grammar happy.
|
||||
// https://github.com/textmate/diff.tmbundle/blob/0593bb775eab1824af97ef2172fd38822abd97d7/Syntaxes/Diff.plist#L68
|
||||
return fmt.Sprintf("@@= skipped -%d, +%d lines =@@", aDiff, bDiff)
|
||||
})
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
var fixUnifiedDiff = regexp.MustCompile(`@@ -\d+,\d+ \+\d+,\d+ @@`)
|
||||
|
||||
func RunAgainstSubmodule(t *testing.T, fileName string, actual string, opts Options) {
|
||||
// Record this baseline for tracking unused baselines
|
||||
recordBaseline(t, filepath.Join(opts.Subfolder, fileName))
|
||||
|
||||
local := filepath.Join(localRoot, opts.Subfolder, fileName)
|
||||
reference := filepath.Join(submoduleReferenceRoot, opts.Subfolder, fileName)
|
||||
writeComparison(t, actual, local, reference, true)
|
||||
}
|
||||
|
||||
func writeComparison(t *testing.T, actualContent string, local, reference string, comparingAgainstSubmodule bool) {
|
||||
if actualContent == "" {
|
||||
panic("the generated content was \"\". Return 'baseline.NoContent' if no baselining is required.")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(local), 0o755); err != nil {
|
||||
t.Error(fmt.Errorf("failed to create directories for the local baseline file %s: %w", local, err))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Stat(local); err == nil {
|
||||
if err := os.Remove(local); err != nil {
|
||||
t.Error(fmt.Errorf("failed to remove the local baseline file %s: %w", local, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
expected := NoContent
|
||||
foundExpected := false
|
||||
if content, err := os.ReadFile(reference); err == nil {
|
||||
expected = string(content)
|
||||
foundExpected = true
|
||||
}
|
||||
|
||||
if expected != actualContent || actualContent == NoContent && foundExpected {
|
||||
if actualContent == NoContent {
|
||||
if err := os.WriteFile(local+".delete", []byte{}, 0o644); err != nil {
|
||||
t.Error(fmt.Errorf("failed to write the local baseline file %s: %w", local+".delete", err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := os.WriteFile(local, []byte(actualContent), 0o644); err != nil {
|
||||
t.Error(fmt.Errorf("failed to write the local baseline file %s: %w", local, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(reference); err != nil {
|
||||
if comparingAgainstSubmodule {
|
||||
t.Errorf("the baseline file %s does not exist in the TypeScript submodule", reference)
|
||||
} else {
|
||||
t.Errorf("new baseline created at %s.", local)
|
||||
}
|
||||
} else if comparingAgainstSubmodule {
|
||||
t.Errorf("the baseline file %s does not match the reference in the TypeScript submodule", reference)
|
||||
} else {
|
||||
t.Errorf("the baseline file %s has changed. (Run `hereby baseline-accept` if the new baseline is correct.)", reference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
localRoot = filepath.Join(repo.TestDataPath(), "baselines", "local")
|
||||
referenceRoot = filepath.Join(repo.TestDataPath(), "baselines", "reference")
|
||||
submoduleReferenceRoot = filepath.Join(repo.TypeScriptSubmodulePath(), "tests", "baselines", "reference")
|
||||
)
|
||||
25
tools/tsgo/internal/testutil/baseline/baseline_test.go
Normal file
25
tools/tsgo/internal/testutil/baseline/baseline_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package baseline
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSubmoduleAcceptedFilesExist(t *testing.T) {
|
||||
t.Parallel()
|
||||
for name := range submoduleAcceptedFileNames().Keys() {
|
||||
if _, err := os.Stat(filepath.Join(referenceRoot, "submoduleAccepted", name)); err != nil {
|
||||
t.Errorf("submoduleAccepted.txt references %q, but the baseline file does not exist", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmoduleTriagedFilesExist(t *testing.T) {
|
||||
t.Parallel()
|
||||
for name := range submoduleTriagedFileNames().Keys() {
|
||||
if _, err := os.Stat(filepath.Join(referenceRoot, "submoduleTriaged", name)); err != nil {
|
||||
t.Errorf("submoduleTriaged.txt references %q, but the baseline file does not exist", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
103
tools/tsgo/internal/testutil/baseline/testmain.go
Normal file
103
tools/tsgo/internal/testutil/baseline/testmain.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package baseline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
)
|
||||
|
||||
var (
|
||||
// recordedBaselines tracks all baseline file paths that were written during the test run.
|
||||
recordedBaselines collections.SyncSet[string]
|
||||
|
||||
// trackingInitialized is set to true when Track() is called.
|
||||
trackingInitialized bool
|
||||
|
||||
// trackingDir is the directory where tracking files should be written.
|
||||
// If non-empty, baseline tracking is enabled.
|
||||
// Set by Herebyfile.mjs when running full test suites with tracking enabled.
|
||||
trackingDir = os.Getenv("TSGO_BASELINE_TRACKING_DIR")
|
||||
)
|
||||
|
||||
// Track sets up baseline tracking and returns a cleanup function that writes the tracking file.
|
||||
// It should be called from TestMain using defer:
|
||||
//
|
||||
// func TestMain(m *testing.M) {
|
||||
// defer baseline.Track()()
|
||||
// m.Run()
|
||||
// }
|
||||
func Track() func() {
|
||||
trackingInitialized = true
|
||||
|
||||
if trackingDir == "" {
|
||||
return func() {}
|
||||
}
|
||||
|
||||
// Hash the entire call stack to create a unique filename per calling package.
|
||||
// This must be done in Track(), not in the deferred cleanup, because
|
||||
// the deferred function's call stack won't include the caller's info.
|
||||
var pcs [32]uintptr
|
||||
n := runtime.Callers(2, pcs[:]) // Skip Track and runtime.Callers
|
||||
h := fnv.New64a()
|
||||
frames := runtime.CallersFrames(pcs[:n])
|
||||
for {
|
||||
frame, more := frames.Next()
|
||||
h.Write([]byte(frame.File))
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
trackingPath := filepath.Join(trackingDir, fmt.Sprintf("%016x.txt", h.Sum64()))
|
||||
|
||||
return func() {
|
||||
// After tests complete, write the recorded baselines
|
||||
writeRecordedBaselines(trackingPath)
|
||||
}
|
||||
}
|
||||
|
||||
// recordBaseline adds a baseline file path to the recorded set.
|
||||
// The path should be relative to the baselines/reference directory.
|
||||
func recordBaseline(t testing.TB, relativePath string) {
|
||||
if trackingDir != "" {
|
||||
if !trackingInitialized {
|
||||
t.Error("baseline: package uses baselines but TestMain did not call baseline.Track(). " +
|
||||
"Please add a TestMain function with: defer baseline.Track()()")
|
||||
return
|
||||
}
|
||||
recordedBaselines.Add(relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
// writeRecordedBaselines writes the list of recorded baseline files to a tracking file.
|
||||
func writeRecordedBaselines(trackingPath string) {
|
||||
if recordedBaselines.Size() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if err := doWriteRecordedBaselines(trackingPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "baseline: failed to write tracking file %s: %v\n", trackingPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func doWriteRecordedBaselines(trackingPath string) error {
|
||||
f, err := os.Create(trackingPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
w := bufio.NewWriter(f)
|
||||
for baseline := range recordedBaselines.Keys() {
|
||||
if _, err := fmt.Fprintln(w, baseline); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
||||
29
tools/tsgo/internal/testutil/emittestutil/emittestutil.go
Normal file
29
tools/tsgo/internal/testutil/emittestutil/emittestutil.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package emittestutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/parsetestutil"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
// Checks that pretty-printing the given file matches the expected output.
|
||||
func CheckEmit(t *testing.T, emitContext *printer.EmitContext, file *ast.SourceFile, expected string) {
|
||||
t.Helper()
|
||||
printer := printer.NewPrinter(
|
||||
printer.PrinterOptions{
|
||||
NewLine: core.NewLineKindLF,
|
||||
},
|
||||
printer.PrintHandlers{},
|
||||
emitContext,
|
||||
)
|
||||
text := printer.EmitSourceFile(file)
|
||||
actual := strings.TrimSuffix(text, "\n")
|
||||
assert.Equal(t, expected, actual)
|
||||
file2 := parsetestutil.ParseTypeScript(text, file.LanguageVariant == core.LanguageVariantJSX)
|
||||
parsetestutil.CheckDiagnosticsMessage(t, file2, "error on reparse: ")
|
||||
}
|
||||
74
tools/tsgo/internal/testutil/filefixture/filefixture.go
Normal file
74
tools/tsgo/internal/testutil/filefixture/filefixture.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package filefixture
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Fixture interface {
|
||||
Name() string
|
||||
Path() string
|
||||
SkipIfNotExist(t testing.TB)
|
||||
ReadFile(t testing.TB) string
|
||||
}
|
||||
|
||||
type fromFile struct {
|
||||
name string
|
||||
path string
|
||||
contents func() (string, error)
|
||||
}
|
||||
|
||||
func FromFile(name string, path string) Fixture {
|
||||
return &fromFile{
|
||||
name: name,
|
||||
path: path,
|
||||
// Cache the file contents and errors.
|
||||
contents: sync.OnceValues(func() (string, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
return string(b), err
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fromFile) Name() string { return f.name }
|
||||
func (f *fromFile) Path() string { return f.path }
|
||||
|
||||
func (f *fromFile) SkipIfNotExist(tb testing.TB) {
|
||||
tb.Helper()
|
||||
|
||||
if _, err := os.Stat(f.path); err != nil {
|
||||
tb.Skipf("Test fixture %q does not exist", f.path)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fromFile) ReadFile(tb testing.TB) string {
|
||||
tb.Helper()
|
||||
|
||||
contents, err := f.contents()
|
||||
if err != nil {
|
||||
tb.Fatalf("Failed to read test fixture %q: %v", f.path, err)
|
||||
}
|
||||
return contents
|
||||
}
|
||||
|
||||
type fromString struct {
|
||||
name string
|
||||
path string
|
||||
contents string
|
||||
}
|
||||
|
||||
func FromString(name string, path string, contents string) Fixture {
|
||||
return &fromString{
|
||||
name: name,
|
||||
path: path,
|
||||
contents: contents,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fromString) Name() string { return f.name }
|
||||
func (f *fromString) Path() string { return f.path }
|
||||
|
||||
func (f *fromString) SkipIfNotExist(tb testing.TB) {}
|
||||
|
||||
func (f *fromString) ReadFile(tb testing.TB) string { return f.contents }
|
||||
16
tools/tsgo/internal/testutil/fixtures/benchfixtures.go
Normal file
16
tools/tsgo/internal/testutil/fixtures/benchfixtures.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package fixtures
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/repo"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/filefixture"
|
||||
)
|
||||
|
||||
var BenchFixtures = []filefixture.Fixture{
|
||||
filefixture.FromString("empty.ts", "empty.ts", ""),
|
||||
filefixture.FromFile("checker.ts", filepath.Join(repo.TypeScriptSubmodulePath(), "src/compiler/checker.ts")),
|
||||
filefixture.FromFile("dom.generated.d.ts", filepath.Join(repo.TypeScriptSubmodulePath(), "src/lib/dom.generated.d.ts")),
|
||||
filefixture.FromFile("Herebyfile.mjs", filepath.Join(repo.TypeScriptSubmodulePath(), "Herebyfile.mjs")),
|
||||
filefixture.FromFile("jsxComplexSignatureHasApplicabilityError.tsx", filepath.Join(repo.TypeScriptSubmodulePath(), "tests/cases/compiler/jsxComplexSignatureHasApplicabilityError.tsx")),
|
||||
}
|
||||
177
tools/tsgo/internal/testutil/fsbaselineutil/differ.go
Normal file
177
tools/tsgo/internal/testutil/fsbaselineutil/differ.go
Normal file
@@ -0,0 +1,177 @@
|
||||
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
|
||||
}
|
||||
1234
tools/tsgo/internal/testutil/harnessutil/harnessutil.go
Normal file
1234
tools/tsgo/internal/testutil/harnessutil/harnessutil.go
Normal file
File diff suppressed because it is too large
Load Diff
45
tools/tsgo/internal/testutil/harnessutil/recorderfs.go
Normal file
45
tools/tsgo/internal/testutil/harnessutil/recorderfs.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package harnessutil
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
)
|
||||
|
||||
type OutputRecorderFS struct {
|
||||
vfs.FS
|
||||
outputsMut sync.Mutex
|
||||
outputsMap map[string]int
|
||||
outputs []*TestFile
|
||||
}
|
||||
|
||||
func NewOutputRecorderFS(fs vfs.FS) vfs.FS {
|
||||
return &OutputRecorderFS{FS: fs}
|
||||
}
|
||||
|
||||
func (fs *OutputRecorderFS) WriteFile(path string, data string) error {
|
||||
if err := fs.FS.WriteFile(path, data); err != nil {
|
||||
return err
|
||||
}
|
||||
path = fs.Realpath(path)
|
||||
fs.outputsMut.Lock()
|
||||
defer fs.outputsMut.Unlock()
|
||||
if index, ok := fs.outputsMap[path]; ok {
|
||||
fs.outputs[index] = &TestFile{UnitName: path, Content: data}
|
||||
} else {
|
||||
index := len(fs.outputs)
|
||||
if fs.outputsMap == nil {
|
||||
fs.outputsMap = make(map[string]int)
|
||||
}
|
||||
fs.outputsMap[path] = index
|
||||
fs.outputs = append(fs.outputs, &TestFile{UnitName: path, Content: data})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *OutputRecorderFS) Outputs() []*TestFile {
|
||||
fs.outputsMut.Lock()
|
||||
defer fs.outputsMut.Unlock()
|
||||
return slices.Clone(fs.outputs)
|
||||
}
|
||||
364
tools/tsgo/internal/testutil/harnessutil/sourcemap_recorder.go
Normal file
364
tools/tsgo/internal/testutil/harnessutil/sourcemap_recorder.go
Normal file
@@ -0,0 +1,364 @@
|
||||
package harnessutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/sourcemap"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
type writerAggregator struct {
|
||||
strings.Builder
|
||||
}
|
||||
|
||||
func (w *writerAggregator) WriteStringf(format string, args ...any) {
|
||||
w.WriteString(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (w *writerAggregator) WriteLine(s string) {
|
||||
w.WriteString(s)
|
||||
w.WriteString("\r\n")
|
||||
}
|
||||
|
||||
func (w *writerAggregator) WriteLinef(format string, args ...any) {
|
||||
w.WriteStringf(format+"\r\n", args...)
|
||||
}
|
||||
|
||||
type sourceMapSpanWithDecodeErrors struct {
|
||||
sourceMapSpan *sourcemap.Mapping
|
||||
decodeErrors []string
|
||||
}
|
||||
|
||||
type decodedMapping struct {
|
||||
sourceMapSpan *sourcemap.Mapping
|
||||
error error
|
||||
}
|
||||
|
||||
type sourceMapDecoder struct {
|
||||
sourceMapMappings string
|
||||
mappings *sourcemap.MappingsDecoder
|
||||
}
|
||||
|
||||
func newSourceMapDecoder(sourceMap *sourcemap.RawSourceMap) *sourceMapDecoder {
|
||||
return &sourceMapDecoder{
|
||||
sourceMapMappings: sourceMap.Mappings,
|
||||
mappings: sourcemap.DecodeMappings(sourceMap.Mappings),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *sourceMapDecoder) decodeNextEncodedSourceMapSpan() *decodedMapping {
|
||||
value, done := d.mappings.Next()
|
||||
if done {
|
||||
mapping := &decodedMapping{
|
||||
error: d.mappings.Error(),
|
||||
sourceMapSpan: d.mappings.State(),
|
||||
}
|
||||
if mapping.error == nil {
|
||||
mapping.error = errors.New("No encoded entry found")
|
||||
}
|
||||
return mapping
|
||||
}
|
||||
return &decodedMapping{sourceMapSpan: value}
|
||||
}
|
||||
|
||||
func (d *sourceMapDecoder) hasCompletedDecoding() bool {
|
||||
return d.mappings.Pos() == len(d.sourceMapMappings)
|
||||
}
|
||||
|
||||
func (d *sourceMapDecoder) getRemainingDecodeString() string {
|
||||
return d.sourceMapMappings[d.mappings.Pos():]
|
||||
}
|
||||
|
||||
type sourceMapSpanWriter struct {
|
||||
sourceMapRecorder *writerAggregator
|
||||
sourceMapSources []string
|
||||
sourceMapNames []string
|
||||
jsFile *TestFile
|
||||
jsLineMap []core.TextPos
|
||||
tsCode string
|
||||
tsLineMap []core.TextPos
|
||||
spansOnSingleLine []sourceMapSpanWithDecodeErrors
|
||||
prevWrittenSourcePos int
|
||||
nextJsLineToWrite int
|
||||
spanMarkerContinues bool
|
||||
sourceMapDecoder *sourceMapDecoder
|
||||
}
|
||||
|
||||
func newSourceMapSpanWriter(sourceMapRecorder *writerAggregator, sourceMap *sourcemap.RawSourceMap, jsFile *TestFile) *sourceMapSpanWriter {
|
||||
writer := &sourceMapSpanWriter{
|
||||
sourceMapRecorder: sourceMapRecorder,
|
||||
sourceMapSources: sourceMap.Sources,
|
||||
sourceMapNames: sourceMap.Names,
|
||||
jsFile: jsFile,
|
||||
jsLineMap: core.ComputeECMALineStarts(jsFile.Content),
|
||||
spansOnSingleLine: make([]sourceMapSpanWithDecodeErrors, 0),
|
||||
prevWrittenSourcePos: 0,
|
||||
nextJsLineToWrite: 0,
|
||||
spanMarkerContinues: false,
|
||||
sourceMapDecoder: newSourceMapDecoder(sourceMap),
|
||||
}
|
||||
|
||||
sourceMapRecorder.WriteLine("===================================================================")
|
||||
sourceMapRecorder.WriteLinef("JsFile: %s", sourceMap.File)
|
||||
sourceMapRecorder.WriteLinef("mapUrl: %s", sourcemap.TryGetSourceMappingURL(sourcemap.CreateECMALineInfo(jsFile.Content, writer.jsLineMap)))
|
||||
sourceMapRecorder.WriteLinef("sourceRoot: %s", sourceMap.SourceRoot)
|
||||
sourceMapRecorder.WriteLinef("sources: %s", strings.Join(sourceMap.Sources, ","))
|
||||
if len(sourceMap.SourcesContent) > 0 {
|
||||
content, err := json.Marshal(sourceMap.SourcesContent)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sourceMapRecorder.WriteLinef("sourcesContent: %s", content)
|
||||
}
|
||||
sourceMapRecorder.WriteLine("===================================================================")
|
||||
return writer
|
||||
}
|
||||
|
||||
func (w *sourceMapSpanWriter) getSourceMapSpanString(mapEntry *sourcemap.Mapping, getAbsentNameIndex bool) string {
|
||||
var mapString writerAggregator
|
||||
mapString.WriteStringf("Emitted(%d, %d)", mapEntry.GeneratedLine+1, mapEntry.GeneratedCharacter+1)
|
||||
if mapEntry.IsSourceMapping() {
|
||||
mapString.WriteStringf(" Source(%d, %d) + SourceIndex(%d)", mapEntry.SourceLine+1, mapEntry.SourceCharacter+1, mapEntry.SourceIndex)
|
||||
if mapEntry.NameIndex >= 0 && int(mapEntry.NameIndex) < len(w.sourceMapNames) {
|
||||
mapString.WriteStringf(" name (%s)", w.sourceMapNames[mapEntry.NameIndex])
|
||||
} else {
|
||||
if mapEntry.NameIndex != sourcemap.MissingName || getAbsentNameIndex {
|
||||
mapString.WriteStringf(" nameIndex (%d)", mapEntry.NameIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
return mapString.String()
|
||||
}
|
||||
|
||||
func (w *sourceMapSpanWriter) recordSourceMapSpan(sourceMapSpan *sourcemap.Mapping) {
|
||||
// verify the decoded span is same as the new span
|
||||
decodeResult := w.sourceMapDecoder.decodeNextEncodedSourceMapSpan()
|
||||
var decodeErrors []string
|
||||
if decodeResult.error != nil || !decodeResult.sourceMapSpan.Equals(sourceMapSpan) {
|
||||
if decodeResult.error != nil {
|
||||
decodeErrors = []string{"!!^^ !!^^ There was decoding error in the sourcemap at this location: " + decodeResult.error.Error()}
|
||||
} else {
|
||||
decodeErrors = []string{"!!^^ !!^^ The decoded span from sourcemap's mapping entry does not match what was encoded for this span:"}
|
||||
}
|
||||
decodeErrors = append(
|
||||
decodeErrors,
|
||||
"!!^^ !!^^ Decoded span from sourcemap's mappings entry: "+
|
||||
w.getSourceMapSpanString(decodeResult.sourceMapSpan, true /*getAbsentNameIndex*/)+
|
||||
" Span encoded by the emitter:"+
|
||||
w.getSourceMapSpanString(sourceMapSpan, true /*getAbsentNameIndex*/),
|
||||
)
|
||||
}
|
||||
|
||||
if len(w.spansOnSingleLine) > 0 && w.spansOnSingleLine[0].sourceMapSpan.GeneratedLine != sourceMapSpan.GeneratedLine {
|
||||
// On different line from the one that we have been recording till now,
|
||||
w.writeRecordedSpans()
|
||||
w.spansOnSingleLine = nil
|
||||
}
|
||||
w.spansOnSingleLine = append(w.spansOnSingleLine, sourceMapSpanWithDecodeErrors{
|
||||
sourceMapSpan: sourceMapSpan,
|
||||
decodeErrors: decodeErrors,
|
||||
})
|
||||
}
|
||||
|
||||
func (w *sourceMapSpanWriter) recordNewSourceFileSpan(sourceMapSpan *sourcemap.Mapping, newSourceFileCode string) {
|
||||
continuesLine := false
|
||||
if len(w.spansOnSingleLine) > 0 && int(w.spansOnSingleLine[0].sourceMapSpan.GeneratedCharacter) == sourceMapSpan.GeneratedLine { // !!! char == line seems like a bug in Strada?
|
||||
w.writeRecordedSpans()
|
||||
w.spansOnSingleLine = nil
|
||||
w.nextJsLineToWrite-- // walk back one line to reprint the line
|
||||
continuesLine = true
|
||||
}
|
||||
|
||||
w.recordSourceMapSpan(sourceMapSpan)
|
||||
|
||||
if len(w.spansOnSingleLine) != 1 {
|
||||
panic("expected a single span")
|
||||
}
|
||||
|
||||
w.sourceMapRecorder.WriteLine("-------------------------------------------------------------------")
|
||||
if continuesLine {
|
||||
w.sourceMapRecorder.WriteLinef("emittedFile:%s (%d, %d)", w.jsFile.UnitName, sourceMapSpan.GeneratedLine+1, sourceMapSpan.GeneratedCharacter+1)
|
||||
} else {
|
||||
w.sourceMapRecorder.WriteLinef("emittedFile:%s", w.jsFile.UnitName)
|
||||
}
|
||||
w.sourceMapRecorder.WriteLinef("sourceFile:%s", w.sourceMapSources[w.spansOnSingleLine[0].sourceMapSpan.SourceIndex])
|
||||
w.sourceMapRecorder.WriteLine("-------------------------------------------------------------------")
|
||||
|
||||
w.tsLineMap = core.ComputeECMALineStarts(newSourceFileCode)
|
||||
w.tsCode = newSourceFileCode
|
||||
w.prevWrittenSourcePos = 0
|
||||
}
|
||||
|
||||
func (w *sourceMapSpanWriter) close() {
|
||||
// Write the lines pending on the single line
|
||||
w.writeRecordedSpans()
|
||||
|
||||
if !w.sourceMapDecoder.hasCompletedDecoding() {
|
||||
w.sourceMapRecorder.WriteLine("!!!! **** There are more source map entries in the sourceMap's mapping than what was encoded")
|
||||
w.sourceMapRecorder.WriteLinef("!!!! **** Remaining decoded string: %s", w.sourceMapDecoder.getRemainingDecodeString())
|
||||
}
|
||||
|
||||
// write remaining js lines
|
||||
w.writeJsFileLines(len(w.jsLineMap))
|
||||
}
|
||||
|
||||
func (w *sourceMapSpanWriter) getTextOfLine(line int, lineMap []core.TextPos, code string) string {
|
||||
startPos := lineMap[line]
|
||||
var endPos core.TextPos
|
||||
if line+1 < len(lineMap) {
|
||||
endPos = lineMap[line+1]
|
||||
} else {
|
||||
endPos = core.TextPos(len(code))
|
||||
}
|
||||
text := code[startPos:endPos]
|
||||
if line == 0 {
|
||||
return stringutil.RemoveByteOrderMark(text)
|
||||
}
|
||||
// return line == 0 ? Utils.removeByteOrderMark(text) : text;
|
||||
return text
|
||||
}
|
||||
|
||||
func (w *sourceMapSpanWriter) writeJsFileLines(endJsLine int) {
|
||||
for ; w.nextJsLineToWrite < endJsLine; w.nextJsLineToWrite++ {
|
||||
w.sourceMapRecorder.WriteStringf(">>>%s", w.getTextOfLine(w.nextJsLineToWrite, w.jsLineMap, w.jsFile.Content))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *sourceMapSpanWriter) writeRecordedSpans() {
|
||||
recordedSpanWriter := recordedSpanWriter{w: w}
|
||||
recordedSpanWriter.writeRecordedSpans()
|
||||
}
|
||||
|
||||
type recordedSpanWriter struct {
|
||||
markerIds []string
|
||||
prevEmittedCol int
|
||||
w *sourceMapSpanWriter
|
||||
}
|
||||
|
||||
func (sw *recordedSpanWriter) getMarkerId(markerIndex int) string {
|
||||
markerId := ""
|
||||
if sw.w.spanMarkerContinues {
|
||||
if markerIndex != 0 {
|
||||
panic("expected markerIndex to be 0")
|
||||
}
|
||||
markerId = "1->"
|
||||
} else {
|
||||
markerId = strconv.Itoa(markerIndex + 1)
|
||||
if len(markerId) < 2 {
|
||||
markerId += " "
|
||||
}
|
||||
markerId += ">"
|
||||
}
|
||||
return markerId
|
||||
}
|
||||
|
||||
func (sw *recordedSpanWriter) iterateSpans(fn func(currentSpan *sourceMapSpanWithDecodeErrors, index int)) {
|
||||
sw.prevEmittedCol = 0
|
||||
for i := range len(sw.w.spansOnSingleLine) {
|
||||
fn(&sw.w.spansOnSingleLine[i], i)
|
||||
sw.prevEmittedCol = int(sw.w.spansOnSingleLine[i].sourceMapSpan.GeneratedCharacter)
|
||||
}
|
||||
}
|
||||
|
||||
func (sw *recordedSpanWriter) writeSourceMapIndent(indentLength int, indentPrefix string) {
|
||||
sw.w.sourceMapRecorder.WriteString(indentPrefix)
|
||||
for range indentLength {
|
||||
sw.w.sourceMapRecorder.WriteString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
func (sw *recordedSpanWriter) writeSourceMapMarker(currentSpan *sourceMapSpanWithDecodeErrors, index int) {
|
||||
sw.writeSourceMapMarkerEx(currentSpan, index, int(currentSpan.sourceMapSpan.GeneratedCharacter), false /*endContinues*/)
|
||||
}
|
||||
|
||||
func (sw *recordedSpanWriter) writeSourceMapMarkerEx(currentSpan *sourceMapSpanWithDecodeErrors, index int, endColumn int, endContinues bool) {
|
||||
markerId := sw.getMarkerId(index)
|
||||
sw.markerIds = append(sw.markerIds, markerId)
|
||||
sw.writeSourceMapIndent(sw.prevEmittedCol, markerId)
|
||||
for i := sw.prevEmittedCol; i < endColumn; i++ {
|
||||
sw.w.sourceMapRecorder.WriteString("^")
|
||||
}
|
||||
if endContinues {
|
||||
sw.w.sourceMapRecorder.WriteString("->")
|
||||
}
|
||||
sw.w.sourceMapRecorder.WriteLine("")
|
||||
sw.w.spanMarkerContinues = endContinues
|
||||
}
|
||||
|
||||
func (sw *recordedSpanWriter) writeSourceMapSourceText(currentSpan *sourceMapSpanWithDecodeErrors, index int) {
|
||||
// Convert UTF-16 character offset from the source map to a byte position.
|
||||
sourcePos := scanner.ComputePositionOfLineAndUTF16Character(
|
||||
sw.w.tsLineMap,
|
||||
currentSpan.sourceMapSpan.SourceLine,
|
||||
currentSpan.sourceMapSpan.SourceCharacter,
|
||||
sw.w.tsCode,
|
||||
true, /*allowEdits*/
|
||||
)
|
||||
var sourceText string
|
||||
if sw.w.prevWrittenSourcePos < sourcePos {
|
||||
// Position that goes forward, get text
|
||||
sourceText = sw.w.tsCode[sw.w.prevWrittenSourcePos:sourcePos]
|
||||
}
|
||||
|
||||
// If there are decode errors, write
|
||||
for _, decodeError := range currentSpan.decodeErrors {
|
||||
sw.writeSourceMapIndent(sw.prevEmittedCol, sw.markerIds[index])
|
||||
sw.w.sourceMapRecorder.WriteLine(decodeError)
|
||||
}
|
||||
|
||||
tsCodeLineMap := core.ComputeECMALineStarts(sourceText)
|
||||
for i := range tsCodeLineMap {
|
||||
if i == 0 {
|
||||
sw.writeSourceMapIndent(sw.prevEmittedCol, sw.markerIds[index])
|
||||
} else {
|
||||
sw.writeSourceMapIndent(sw.prevEmittedCol, " >")
|
||||
}
|
||||
sw.w.sourceMapRecorder.WriteString(sw.w.getTextOfLine(i, tsCodeLineMap, sourceText))
|
||||
if i == len(tsCodeLineMap)-1 {
|
||||
sw.w.sourceMapRecorder.WriteLine("")
|
||||
}
|
||||
}
|
||||
|
||||
sw.w.prevWrittenSourcePos = sourcePos
|
||||
}
|
||||
|
||||
func (sw *recordedSpanWriter) writeSpanDetails(currentSpan *sourceMapSpanWithDecodeErrors, index int) {
|
||||
sw.w.sourceMapRecorder.WriteLinef("%s%s", sw.markerIds[index], sw.w.getSourceMapSpanString(currentSpan.sourceMapSpan, false /*getAbsentNameIndex*/))
|
||||
}
|
||||
|
||||
func (sw *recordedSpanWriter) writeRecordedSpans() {
|
||||
w := sw.w
|
||||
writeSourceMapMarker := sw.writeSourceMapMarker
|
||||
writeSourceMapSourceText := sw.writeSourceMapSourceText
|
||||
writeSpanDetails := sw.writeSpanDetails
|
||||
|
||||
if len(w.spansOnSingleLine) > 0 {
|
||||
currentJsLine := w.spansOnSingleLine[0].sourceMapSpan.GeneratedLine
|
||||
|
||||
// Write js line
|
||||
w.writeJsFileLines(currentJsLine + 1)
|
||||
|
||||
// Emit markers
|
||||
sw.iterateSpans(writeSourceMapMarker)
|
||||
|
||||
jsFileText := w.getTextOfLine(currentJsLine+1, w.jsLineMap, w.jsFile.Content) // TODO: Strada is wrong here, we should be looking at `currentJsLine`, not `currentJsLine+1`
|
||||
if sw.prevEmittedCol < len(jsFileText)-1 {
|
||||
// There is remaining text on this line that will be part of next source span so write marker that continues
|
||||
sw.writeSourceMapMarkerEx(nil /*currentSpan*/, len(w.spansOnSingleLine), len(jsFileText)-1 /*endColumn*/, true /*endContinues*/)
|
||||
}
|
||||
|
||||
// Emit Source text
|
||||
sw.iterateSpans(writeSourceMapSourceText)
|
||||
|
||||
// Emit column number etc
|
||||
sw.iterateSpans(writeSpanDetails)
|
||||
|
||||
w.sourceMapRecorder.WriteLine("---")
|
||||
}
|
||||
}
|
||||
95
tools/tsgo/internal/testutil/jstest/node.go
Normal file
95
tools/tsgo/internal/testutil/jstest/node.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package jstest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/repo"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
const loaderScript = `import script from "./script.mjs";
|
||||
process.stdout.write(JSON.stringify(await script(...process.argv.slice(2))));`
|
||||
|
||||
var getNodeExeOnce = sync.OnceValue(func() string {
|
||||
const exeName = "node"
|
||||
exe, err := exec.LookPath(exeName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return exe
|
||||
})
|
||||
|
||||
// EvalNodeScript imports a Node.js script that default-exports a single function,
|
||||
// calls it with the provided arguments, and unmarshals the JSON-stringified
|
||||
// awaited return value into T.
|
||||
func EvalNodeScript[T any](t testing.TB, script string, dir string, args ...string) (result T, err error) {
|
||||
return evalNodeScript[T](t, script, loaderScript, dir, args...)
|
||||
}
|
||||
|
||||
// EvalNodeScriptWithTS is like EvalNodeScript, but provides the TypeScript
|
||||
// library to the script as the first argument.
|
||||
func EvalNodeScriptWithTS[T any](t testing.TB, script string, dir string, args ...string) (result T, err error) {
|
||||
if dir == "" {
|
||||
dir = t.TempDir()
|
||||
}
|
||||
tsSrc := tspath.NormalizePath(filepath.Join(repo.RootPath(), "node_modules/typescript/lib/typescript.js"))
|
||||
if tsSrc[0] == '/' {
|
||||
tsSrc = "file://" + tsSrc
|
||||
} else {
|
||||
tsSrc = "file:///" + tsSrc
|
||||
}
|
||||
tsLoaderScript := fmt.Sprintf(`import script from "./script.mjs";
|
||||
import * as ts from "%s";
|
||||
process.stdout.write(JSON.stringify(await script(ts, ...process.argv.slice(2))));`, tsSrc)
|
||||
return evalNodeScript[T](t, script, tsLoaderScript, dir, args...)
|
||||
}
|
||||
|
||||
func SkipIfNoNodeJS(t testing.TB) {
|
||||
t.Helper()
|
||||
if getNodeExeOnce() == "" {
|
||||
t.Skip("Node.js not found")
|
||||
}
|
||||
}
|
||||
|
||||
func evalNodeScript[T any](t testing.TB, script string, loader string, dir string, args ...string) (result T, err error) {
|
||||
t.Helper()
|
||||
exe := getNodeExe(t)
|
||||
scriptPath := dir + "/script.mjs"
|
||||
if err = os.WriteFile(scriptPath, []byte(script), 0o644); err != nil {
|
||||
return result, err
|
||||
}
|
||||
loaderPath := dir + "/loader.mjs"
|
||||
if err = os.WriteFile(loaderPath, []byte(loader), 0o644); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
execArgs := make([]string, 0, 1+len(args))
|
||||
execArgs = append(execArgs, loaderPath)
|
||||
execArgs = append(execArgs, args...)
|
||||
execCmd := exec.Command(exe, execArgs...)
|
||||
execCmd.Dir = dir
|
||||
output, err := execCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to run node: %w\n%s", err, output)
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(output, &result); err != nil {
|
||||
return result, fmt.Errorf("failed to unmarshal JSON output: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getNodeExe(t testing.TB) string {
|
||||
if exe := getNodeExeOnce(); exe != "" {
|
||||
return exe
|
||||
}
|
||||
t.Fatal("Node.js not found")
|
||||
return ""
|
||||
}
|
||||
326
tools/tsgo/internal/testutil/lsptestutil/lspclient.go
Normal file
326
tools/tsgo/internal/testutil/lsptestutil/lspclient.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package lsptestutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/jsonrpc"
|
||||
"github.com/microsoft/typescript-go/internal/lsp"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
// clientTransport wires a test client to a server using real LSP
|
||||
// "Content-Length"-framed JSON streamed over byte pipes, exactly like
|
||||
// communication with a real editor. This exercises the full marshal/unmarshal
|
||||
// round-trip of every protocol data structure. The two directions are:
|
||||
//
|
||||
// client --(clientOut)--> serverIn --> server
|
||||
// server --(serverOut)--> clientIn --> client
|
||||
type clientTransport struct {
|
||||
serverIn lsp.Reader // server reads client->server messages
|
||||
serverOut lsp.Writer // server writes server->client messages
|
||||
clientIn lsp.Reader // client reads server->client messages
|
||||
clientOut lsp.Writer // client writes client->server messages
|
||||
closeClientOut func() // closes the client->server direction
|
||||
closeServerOut func() // closes the server->client direction
|
||||
}
|
||||
|
||||
func newClientTransport() clientTransport {
|
||||
clientToServerReader, clientToServerWriter := io.Pipe()
|
||||
serverToClientReader, serverToClientWriter := io.Pipe()
|
||||
return clientTransport{
|
||||
serverIn: lsp.ToReader(clientToServerReader),
|
||||
serverOut: lsp.ToWriter(serverToClientWriter),
|
||||
clientIn: lsp.ToReader(serverToClientReader),
|
||||
clientOut: lsp.ToWriter(clientToServerWriter),
|
||||
closeClientOut: func() { _ = clientToServerWriter.Close() },
|
||||
closeServerOut: func() { _ = serverToClientWriter.Close() },
|
||||
}
|
||||
}
|
||||
|
||||
// ServerRequestHandler handles server-initiated requests and returns the response to send back.
|
||||
type ServerRequestHandler func(ctx context.Context, req *lsproto.RequestMessage) *lsproto.ResponseMessage
|
||||
|
||||
// ServerNotificationHandler handles server-initiated notifications (e.g., $/progress).
|
||||
type ServerNotificationHandler func(ctx context.Context, req *lsproto.RequestMessage)
|
||||
|
||||
// LSPClient provides infrastructure for communicating with an LSP server in tests.
|
||||
type LSPClient struct {
|
||||
Server *lsp.Server
|
||||
inputWriter lsp.Writer
|
||||
outputReader lsp.Reader
|
||||
id int32
|
||||
ctx context.Context
|
||||
|
||||
// inputWriterMu serializes writes to the server. The test goroutine (sending
|
||||
// requests/notifications) and the MessageRouter goroutine (sending responses
|
||||
// to server-initiated requests) both write to the same stream; a single
|
||||
// message is written as multiple underlying writes (header, body, flush), so
|
||||
// concurrent writers must not interleave.
|
||||
inputWriterMu sync.Mutex
|
||||
|
||||
// OnServerRequest handles server-initiated requests (e.g., workspace/configuration).
|
||||
// If nil, all server requests receive a MethodNotFound error.
|
||||
onServerRequest ServerRequestHandler
|
||||
|
||||
// OnServerNotification handles server-initiated notifications (e.g., $/progress).
|
||||
// If nil, notifications are ignored.
|
||||
OnServerNotification ServerNotificationHandler
|
||||
|
||||
// Async message handling
|
||||
pendingRequests map[jsonrpc.ID]chan *lsproto.ResponseMessage
|
||||
pendingRequestsMu sync.Mutex
|
||||
}
|
||||
|
||||
// writeToServer writes a message to the server, serializing concurrent writers.
|
||||
func (c *LSPClient) writeToServer(msg *lsproto.Message) error {
|
||||
c.inputWriterMu.Lock()
|
||||
defer c.inputWriterMu.Unlock()
|
||||
return c.inputWriter.Write(msg)
|
||||
}
|
||||
|
||||
// NewLSPClient creates an LSPClient wrapping the given server and pipes.
|
||||
func NewLSPClient(t *testing.T, serverOpts lsp.ServerOptions, onServerRequest ServerRequestHandler) (*LSPClient, func() error) {
|
||||
transport := newClientTransport()
|
||||
serverOpts.In = transport.serverIn
|
||||
serverOpts.Out = transport.serverOut
|
||||
|
||||
server := lsp.NewServer(&serverOpts)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
client := &LSPClient{
|
||||
Server: server,
|
||||
inputWriter: transport.clientOut,
|
||||
outputReader: transport.clientIn,
|
||||
pendingRequests: make(map[jsonrpc.ID]chan *lsproto.ResponseMessage),
|
||||
onServerRequest: onServerRequest,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
// Start server goroutine
|
||||
g.Go(func() error {
|
||||
defer transport.closeServerOut()
|
||||
return server.Run(ctx)
|
||||
})
|
||||
|
||||
// Start async message router
|
||||
g.Go(func() error {
|
||||
return client.MessageRouter(ctx)
|
||||
})
|
||||
|
||||
return client, func() error {
|
||||
cancel()
|
||||
transport.closeClientOut()
|
||||
if err := g.Wait(); err != nil && !errors.Is(err, context.Canceled) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NextID returns the next request ID.
|
||||
func (c *LSPClient) NextID() int32 {
|
||||
id := c.id
|
||||
c.id++
|
||||
return id
|
||||
}
|
||||
|
||||
// MessageRouter runs in a goroutine and routes incoming messages from the server.
|
||||
// It handles responses to client requests and server-initiated requests.
|
||||
// It continues draining the output channel until it is closed (EOF), even after
|
||||
// context cancellation, to prevent the server's writeLoop from blocking on a send.
|
||||
func (c *LSPClient) MessageRouter(ctx context.Context) error {
|
||||
for {
|
||||
msg, err := c.outputReader.Read()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to read message: %w", err)
|
||||
}
|
||||
|
||||
// After context cancellation, keep draining but don't process messages.
|
||||
if ctx.Err() != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate message can be marshaled
|
||||
if err := json.MarshalWrite(io.Discard, msg); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to encode message as JSON: %w", err)
|
||||
}
|
||||
|
||||
switch msg.Kind {
|
||||
case jsonrpc.MessageKindResponse:
|
||||
c.handleResponse(ctx, msg.AsResponse())
|
||||
case jsonrpc.MessageKindRequest:
|
||||
if err := c.handleServerRequest(ctx, msg.AsRequest()); err != nil {
|
||||
return err
|
||||
}
|
||||
case jsonrpc.MessageKindNotification:
|
||||
if c.OnServerNotification != nil {
|
||||
c.OnServerNotification(ctx, msg.AsRequest())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleResponse routes a response message to the waiting request goroutine.
|
||||
func (c *LSPClient) handleResponse(ctx context.Context, resp *lsproto.ResponseMessage) {
|
||||
if resp.ID == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.pendingRequestsMu.Lock()
|
||||
respChan, ok := c.pendingRequests[*resp.ID]
|
||||
if ok {
|
||||
delete(c.pendingRequests, *resp.ID)
|
||||
}
|
||||
c.pendingRequestsMu.Unlock()
|
||||
|
||||
if ok {
|
||||
select {
|
||||
case respChan <- resp:
|
||||
// sent response
|
||||
case <-ctx.Done():
|
||||
// context cancelled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleServerRequest handles requests initiated by the server (e.g., workspace/configuration).
|
||||
func (c *LSPClient) handleServerRequest(ctx context.Context, req *lsproto.RequestMessage) error {
|
||||
var response *lsproto.ResponseMessage
|
||||
|
||||
if c.onServerRequest != nil {
|
||||
response = c.onServerRequest(ctx, req)
|
||||
}
|
||||
|
||||
if response == nil {
|
||||
// Default: unknown server request
|
||||
response = &lsproto.ResponseMessage{
|
||||
ID: req.ID,
|
||||
JSONRPC: req.JSONRPC,
|
||||
Error: &jsonrpc.ResponseError{
|
||||
Code: int32(lsproto.ErrorCodeMethodNotFound),
|
||||
Message: fmt.Sprintf("Unknown method: %s", req.Method),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Send response back to server
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.writeToServer(response.Message()); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to write server request response: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteMsg validates and sends a message to the server.
|
||||
// This is an untyped low-level method; prefer SendRequest and SendNotification for typed interactions.
|
||||
func (c *LSPClient) WriteMsg(t *testing.T, msg *lsproto.Message) {
|
||||
assert.NilError(t, json.MarshalWrite(io.Discard, msg), "failed to encode message as JSON")
|
||||
if err := c.writeToServer(msg); err != nil {
|
||||
t.Fatalf("failed to write message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SendRequest sends a typed request and waits for the response.
|
||||
func SendRequest[Params, Resp any](t *testing.T, c *LSPClient, info lsproto.RequestInfo[Params, Resp], params Params) (*lsproto.Message, Resp, bool) {
|
||||
id := c.NextID()
|
||||
reqID := lsproto.NewID(lsproto.IntegerOrString{Integer: &id})
|
||||
req := info.NewRequestMessage(reqID, params)
|
||||
|
||||
resp, ok := c.SendRequestWorker(t, req, reqID)
|
||||
if !ok {
|
||||
return nil, *new(Resp), false
|
||||
}
|
||||
// The result arrives as a raw json.Value; decode it into Resp.
|
||||
result, err := info.UnmarshalResult(resp.Result)
|
||||
return resp.Message(), result, err == nil
|
||||
}
|
||||
|
||||
// SendRequestAsync sends a typed request and returns a waiter for its response.
|
||||
func SendRequestAsync[Params, Resp any](t *testing.T, c *LSPClient, info lsproto.RequestInfo[Params, Resp], params Params) func() (*lsproto.Message, Resp, bool) {
|
||||
id := c.NextID()
|
||||
reqID := lsproto.NewID(lsproto.IntegerOrString{Integer: &id})
|
||||
req := info.NewRequestMessage(reqID, params)
|
||||
|
||||
responseChan := c.startRequestWorker(t, req, reqID)
|
||||
return func() (*lsproto.Message, Resp, bool) {
|
||||
resp, ok := c.waitForResponse(t, reqID, responseChan)
|
||||
if !ok {
|
||||
return nil, *new(Resp), false
|
||||
}
|
||||
result, err := info.UnmarshalResult(resp.Result)
|
||||
return resp.Message(), result, err == nil
|
||||
}
|
||||
}
|
||||
|
||||
// This is an untyped version of SendRequest. Prefer to use SendRequest when possible.
|
||||
func (c *LSPClient) SendRequestWorker(t *testing.T, req *lsproto.RequestMessage, reqID *jsonrpc.ID) (*lsproto.ResponseMessage, bool) {
|
||||
responseChan := c.startRequestWorker(t, req, reqID)
|
||||
return c.waitForResponse(t, reqID, responseChan)
|
||||
}
|
||||
|
||||
func (c *LSPClient) startRequestWorker(t *testing.T, req *lsproto.RequestMessage, reqID *jsonrpc.ID) chan *lsproto.ResponseMessage {
|
||||
responseChan := make(chan *lsproto.ResponseMessage, 1)
|
||||
c.pendingRequestsMu.Lock()
|
||||
c.pendingRequests[*reqID] = responseChan
|
||||
c.pendingRequestsMu.Unlock()
|
||||
|
||||
c.WriteMsg(t, req.Message())
|
||||
return responseChan
|
||||
}
|
||||
|
||||
func (c *LSPClient) waitForResponse(t *testing.T, reqID *jsonrpc.ID, responseChan <-chan *lsproto.ResponseMessage) (*lsproto.ResponseMessage, bool) {
|
||||
ctx := t.Context()
|
||||
var resp *lsproto.ResponseMessage
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.pendingRequestsMu.Lock()
|
||||
delete(c.pendingRequests, *reqID)
|
||||
c.pendingRequestsMu.Unlock()
|
||||
t.Fatalf("Request cancelled: %v", ctx.Err())
|
||||
return nil, false
|
||||
case resp = <-responseChan:
|
||||
if resp == nil {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
return resp, true
|
||||
}
|
||||
|
||||
// SendNotification sends a typed notification.
|
||||
func SendNotification[Params any](t *testing.T, c *LSPClient, info lsproto.NotificationInfo[Params], params Params) {
|
||||
notification := info.NewNotificationMessage(
|
||||
params,
|
||||
)
|
||||
c.WriteMsg(t, notification.Message())
|
||||
}
|
||||
|
||||
func (c *LSPClient) SetCompilerOptionsForInferredProjects(options *core.CompilerOptions) {
|
||||
c.Server.SetCompilerOptionsForInferredProjects(c.ctx, options)
|
||||
}
|
||||
88
tools/tsgo/internal/testutil/parsetestutil/parsetestutil.go
Normal file
88
tools/tsgo/internal/testutil/parsetestutil/parsetestutil.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package parsetestutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnosticwriter"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
// Simplifies parsing an input string into a SourceFile for testing purposes.
|
||||
func ParseTypeScript(text string, jsx bool) *ast.SourceFile {
|
||||
fileName := core.IfElse(jsx, "/main.tsx", "/main.ts")
|
||||
file := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: fileName,
|
||||
Path: tspath.Path(fileName),
|
||||
}, text, core.GetScriptKindFromFileName(fileName))
|
||||
return file
|
||||
}
|
||||
|
||||
// Asserts that the given file has no parse diagnostics.
|
||||
func CheckDiagnostics(t *testing.T, file *ast.SourceFile) {
|
||||
t.Helper()
|
||||
if len(file.Diagnostics()) > 0 {
|
||||
var b strings.Builder
|
||||
diagnosticwriter.WriteFormatDiagnostics(&b, diagnosticwriter.FromASTDiagnostics(file.Diagnostics()), &diagnosticwriter.FormattingOptions{
|
||||
NewLine: "\n",
|
||||
})
|
||||
t.Error(b.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Asserts that the given file has no parse diagnostics and asserts the given message.
|
||||
func CheckDiagnosticsMessage(t *testing.T, file *ast.SourceFile, message string) {
|
||||
t.Helper()
|
||||
if len(file.Diagnostics()) > 0 {
|
||||
var b strings.Builder
|
||||
diagnosticwriter.WriteFormatDiagnostics(&b, diagnosticwriter.FromASTDiagnostics(file.Diagnostics()), &diagnosticwriter.FormattingOptions{
|
||||
NewLine: "\n",
|
||||
})
|
||||
t.Error(message + b.String())
|
||||
}
|
||||
}
|
||||
|
||||
func newSyntheticRecursiveVisitor() *ast.NodeVisitor {
|
||||
var v *ast.NodeVisitor
|
||||
v = ast.NewNodeVisitor(
|
||||
func(node *ast.Node) *ast.Node {
|
||||
return v.VisitEachChild(node)
|
||||
},
|
||||
&ast.NodeFactory{},
|
||||
ast.NodeVisitorHooks{
|
||||
VisitNode: func(node *ast.Node, v *ast.NodeVisitor) *ast.Node {
|
||||
if node != nil {
|
||||
node.Loc = core.UndefinedTextRange()
|
||||
}
|
||||
return v.VisitNode(node)
|
||||
},
|
||||
VisitToken: func(node *ast.Node, v *ast.NodeVisitor) *ast.Node {
|
||||
if node != nil {
|
||||
node.Loc = core.UndefinedTextRange()
|
||||
}
|
||||
return v.VisitNode(node)
|
||||
},
|
||||
VisitNodes: func(nodes *ast.NodeList, v *ast.NodeVisitor) *ast.NodeList {
|
||||
if nodes != nil {
|
||||
nodes.Loc = core.UndefinedTextRange()
|
||||
}
|
||||
return v.VisitNodes(nodes)
|
||||
},
|
||||
VisitModifiers: func(nodes *ast.ModifierList, v *ast.NodeVisitor) *ast.ModifierList {
|
||||
if nodes != nil {
|
||||
nodes.Loc = core.UndefinedTextRange()
|
||||
}
|
||||
return v.VisitModifiers(nodes)
|
||||
},
|
||||
},
|
||||
)
|
||||
return v
|
||||
}
|
||||
|
||||
// Sets the Loc of the given node and every Node in its subtree to an undefined TextRange (-1,-1).
|
||||
func MarkSyntheticRecursive(node *ast.Node) {
|
||||
newSyntheticRecursiveVisitor().VisitNode(node)
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
// Code generated by moq; DO NOT EDIT.
|
||||
// github.com/matryer/moq
|
||||
|
||||
package projecttestutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/project"
|
||||
)
|
||||
|
||||
// Ensure, that ClientMock does implement project.Client.
|
||||
// If this is not the case, regenerate this file with moq.
|
||||
var _ project.Client = &ClientMock{}
|
||||
|
||||
// ClientMock is a mock implementation of project.Client.
|
||||
//
|
||||
// func TestSomethingThatUsesClient(t *testing.T) {
|
||||
//
|
||||
// // make and configure a mocked project.Client
|
||||
// mockedClient := &ClientMock{
|
||||
// IsActiveFunc: func() bool {
|
||||
// panic("mock out the IsActive method")
|
||||
// },
|
||||
// ProgressFinishFunc: func(message *diagnostics.Message, args ...any) {
|
||||
// panic("mock out the ProgressFinish method")
|
||||
// },
|
||||
// ProgressStartFunc: func(message *diagnostics.Message, args ...any) {
|
||||
// panic("mock out the ProgressStart method")
|
||||
// },
|
||||
// PublishDiagnosticsFunc: func(ctx context.Context, params *lsproto.PublishDiagnosticsParams) error {
|
||||
// panic("mock out the PublishDiagnostics method")
|
||||
// },
|
||||
// RefreshCodeLensFunc: func(ctx context.Context) error {
|
||||
// panic("mock out the RefreshCodeLens method")
|
||||
// },
|
||||
// RefreshDiagnosticsFunc: func(ctx context.Context) error {
|
||||
// panic("mock out the RefreshDiagnostics method")
|
||||
// },
|
||||
// RefreshInlayHintsFunc: func(ctx context.Context) error {
|
||||
// panic("mock out the RefreshInlayHints method")
|
||||
// },
|
||||
// SendTelemetryFunc: func(ctx context.Context, telemetry lsproto.TelemetryEvent) error {
|
||||
// panic("mock out the SendTelemetry method")
|
||||
// },
|
||||
// UnwatchFilesFunc: func(ctx context.Context, id project.WatcherID) error {
|
||||
// panic("mock out the UnwatchFiles method")
|
||||
// },
|
||||
// WatchFilesFunc: func(ctx context.Context, id project.WatcherID, watchers []*lsproto.FileSystemWatcher) error {
|
||||
// panic("mock out the WatchFiles method")
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // use mockedClient in code that requires project.Client
|
||||
// // and then make assertions.
|
||||
//
|
||||
// }
|
||||
type ClientMock struct {
|
||||
// IsActiveFunc mocks the IsActive method.
|
||||
IsActiveFunc func() bool
|
||||
|
||||
// ProgressFinishFunc mocks the ProgressFinish method.
|
||||
ProgressFinishFunc func(message *diagnostics.Message, args ...any)
|
||||
|
||||
// ProgressStartFunc mocks the ProgressStart method.
|
||||
ProgressStartFunc func(message *diagnostics.Message, args ...any)
|
||||
|
||||
// PublishDiagnosticsFunc mocks the PublishDiagnostics method.
|
||||
PublishDiagnosticsFunc func(ctx context.Context, params *lsproto.PublishDiagnosticsParams) error
|
||||
|
||||
// RefreshCodeLensFunc mocks the RefreshCodeLens method.
|
||||
RefreshCodeLensFunc func(ctx context.Context) error
|
||||
|
||||
// RefreshDiagnosticsFunc mocks the RefreshDiagnostics method.
|
||||
RefreshDiagnosticsFunc func(ctx context.Context) error
|
||||
|
||||
// RefreshInlayHintsFunc mocks the RefreshInlayHints method.
|
||||
RefreshInlayHintsFunc func(ctx context.Context) error
|
||||
|
||||
// SendTelemetryFunc mocks the SendTelemetry method.
|
||||
SendTelemetryFunc func(ctx context.Context, telemetry lsproto.TelemetryEvent) error
|
||||
|
||||
// UnwatchFilesFunc mocks the UnwatchFiles method.
|
||||
UnwatchFilesFunc func(ctx context.Context, id project.WatcherID) error
|
||||
|
||||
// WatchFilesFunc mocks the WatchFiles method.
|
||||
WatchFilesFunc func(ctx context.Context, id project.WatcherID, watchers []*lsproto.FileSystemWatcher) error
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// IsActive holds details about calls to the IsActive method.
|
||||
IsActive []struct{}
|
||||
// ProgressFinish holds details about calls to the ProgressFinish method.
|
||||
ProgressFinish []struct {
|
||||
// Message is the message argument value.
|
||||
Message *diagnostics.Message
|
||||
// Args is the args argument value.
|
||||
Args []any
|
||||
}
|
||||
// ProgressStart holds details about calls to the ProgressStart method.
|
||||
ProgressStart []struct {
|
||||
// Message is the message argument value.
|
||||
Message *diagnostics.Message
|
||||
// Args is the args argument value.
|
||||
Args []any
|
||||
}
|
||||
// PublishDiagnostics holds details about calls to the PublishDiagnostics method.
|
||||
PublishDiagnostics []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// Params is the params argument value.
|
||||
Params *lsproto.PublishDiagnosticsParams
|
||||
}
|
||||
// RefreshCodeLens holds details about calls to the RefreshCodeLens method.
|
||||
RefreshCodeLens []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
}
|
||||
// RefreshDiagnostics holds details about calls to the RefreshDiagnostics method.
|
||||
RefreshDiagnostics []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
}
|
||||
// RefreshInlayHints holds details about calls to the RefreshInlayHints method.
|
||||
RefreshInlayHints []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
}
|
||||
// SendTelemetry holds details about calls to the SendTelemetry method.
|
||||
SendTelemetry []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// Telemetry is the telemetry argument value.
|
||||
Telemetry lsproto.TelemetryEvent
|
||||
}
|
||||
// UnwatchFiles holds details about calls to the UnwatchFiles method.
|
||||
UnwatchFiles []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// ID is the id argument value.
|
||||
ID project.WatcherID
|
||||
}
|
||||
// WatchFiles holds details about calls to the WatchFiles method.
|
||||
WatchFiles []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// ID is the id argument value.
|
||||
ID project.WatcherID
|
||||
// Watchers is the watchers argument value.
|
||||
Watchers []*lsproto.FileSystemWatcher
|
||||
}
|
||||
}
|
||||
lockIsActive sync.RWMutex
|
||||
lockProgressFinish sync.RWMutex
|
||||
lockProgressStart sync.RWMutex
|
||||
lockPublishDiagnostics sync.RWMutex
|
||||
lockRefreshCodeLens sync.RWMutex
|
||||
lockRefreshDiagnostics sync.RWMutex
|
||||
lockRefreshInlayHints sync.RWMutex
|
||||
lockSendTelemetry sync.RWMutex
|
||||
lockUnwatchFiles sync.RWMutex
|
||||
lockWatchFiles sync.RWMutex
|
||||
}
|
||||
|
||||
// IsActive calls IsActiveFunc.
|
||||
func (mock *ClientMock) IsActive() bool {
|
||||
callInfo := struct{}{}
|
||||
mock.lockIsActive.Lock()
|
||||
mock.calls.IsActive = append(mock.calls.IsActive, callInfo)
|
||||
mock.lockIsActive.Unlock()
|
||||
if mock.IsActiveFunc == nil {
|
||||
var bOut bool
|
||||
return bOut
|
||||
}
|
||||
return mock.IsActiveFunc()
|
||||
}
|
||||
|
||||
// IsActiveCalls gets all the calls that were made to IsActive.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedClient.IsActiveCalls())
|
||||
func (mock *ClientMock) IsActiveCalls() []struct{} {
|
||||
var calls []struct{}
|
||||
mock.lockIsActive.RLock()
|
||||
calls = mock.calls.IsActive
|
||||
mock.lockIsActive.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// ProgressFinish calls ProgressFinishFunc.
|
||||
func (mock *ClientMock) ProgressFinish(message *diagnostics.Message, args ...any) {
|
||||
callInfo := struct {
|
||||
Message *diagnostics.Message
|
||||
Args []any
|
||||
}{
|
||||
Message: message,
|
||||
Args: args,
|
||||
}
|
||||
mock.lockProgressFinish.Lock()
|
||||
mock.calls.ProgressFinish = append(mock.calls.ProgressFinish, callInfo)
|
||||
mock.lockProgressFinish.Unlock()
|
||||
if mock.ProgressFinishFunc == nil {
|
||||
return
|
||||
}
|
||||
mock.ProgressFinishFunc(message, args...)
|
||||
}
|
||||
|
||||
// ProgressFinishCalls gets all the calls that were made to ProgressFinish.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedClient.ProgressFinishCalls())
|
||||
func (mock *ClientMock) ProgressFinishCalls() []struct {
|
||||
Message *diagnostics.Message
|
||||
Args []any
|
||||
} {
|
||||
var calls []struct {
|
||||
Message *diagnostics.Message
|
||||
Args []any
|
||||
}
|
||||
mock.lockProgressFinish.RLock()
|
||||
calls = mock.calls.ProgressFinish
|
||||
mock.lockProgressFinish.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// ProgressStart calls ProgressStartFunc.
|
||||
func (mock *ClientMock) ProgressStart(message *diagnostics.Message, args ...any) {
|
||||
callInfo := struct {
|
||||
Message *diagnostics.Message
|
||||
Args []any
|
||||
}{
|
||||
Message: message,
|
||||
Args: args,
|
||||
}
|
||||
mock.lockProgressStart.Lock()
|
||||
mock.calls.ProgressStart = append(mock.calls.ProgressStart, callInfo)
|
||||
mock.lockProgressStart.Unlock()
|
||||
if mock.ProgressStartFunc == nil {
|
||||
return
|
||||
}
|
||||
mock.ProgressStartFunc(message, args...)
|
||||
}
|
||||
|
||||
// ProgressStartCalls gets all the calls that were made to ProgressStart.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedClient.ProgressStartCalls())
|
||||
func (mock *ClientMock) ProgressStartCalls() []struct {
|
||||
Message *diagnostics.Message
|
||||
Args []any
|
||||
} {
|
||||
var calls []struct {
|
||||
Message *diagnostics.Message
|
||||
Args []any
|
||||
}
|
||||
mock.lockProgressStart.RLock()
|
||||
calls = mock.calls.ProgressStart
|
||||
mock.lockProgressStart.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// PublishDiagnostics calls PublishDiagnosticsFunc.
|
||||
func (mock *ClientMock) PublishDiagnostics(ctx context.Context, params *lsproto.PublishDiagnosticsParams) error {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
Params *lsproto.PublishDiagnosticsParams
|
||||
}{
|
||||
Ctx: ctx,
|
||||
Params: params,
|
||||
}
|
||||
mock.lockPublishDiagnostics.Lock()
|
||||
mock.calls.PublishDiagnostics = append(mock.calls.PublishDiagnostics, callInfo)
|
||||
mock.lockPublishDiagnostics.Unlock()
|
||||
if mock.PublishDiagnosticsFunc == nil {
|
||||
var errOut error
|
||||
return errOut
|
||||
}
|
||||
return mock.PublishDiagnosticsFunc(ctx, params)
|
||||
}
|
||||
|
||||
// PublishDiagnosticsCalls gets all the calls that were made to PublishDiagnostics.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedClient.PublishDiagnosticsCalls())
|
||||
func (mock *ClientMock) PublishDiagnosticsCalls() []struct {
|
||||
Ctx context.Context
|
||||
Params *lsproto.PublishDiagnosticsParams
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
Params *lsproto.PublishDiagnosticsParams
|
||||
}
|
||||
mock.lockPublishDiagnostics.RLock()
|
||||
calls = mock.calls.PublishDiagnostics
|
||||
mock.lockPublishDiagnostics.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// RefreshCodeLens calls RefreshCodeLensFunc.
|
||||
func (mock *ClientMock) RefreshCodeLens(ctx context.Context) error {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
}{
|
||||
Ctx: ctx,
|
||||
}
|
||||
mock.lockRefreshCodeLens.Lock()
|
||||
mock.calls.RefreshCodeLens = append(mock.calls.RefreshCodeLens, callInfo)
|
||||
mock.lockRefreshCodeLens.Unlock()
|
||||
if mock.RefreshCodeLensFunc == nil {
|
||||
var errOut error
|
||||
return errOut
|
||||
}
|
||||
return mock.RefreshCodeLensFunc(ctx)
|
||||
}
|
||||
|
||||
// RefreshCodeLensCalls gets all the calls that were made to RefreshCodeLens.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedClient.RefreshCodeLensCalls())
|
||||
func (mock *ClientMock) RefreshCodeLensCalls() []struct {
|
||||
Ctx context.Context
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
}
|
||||
mock.lockRefreshCodeLens.RLock()
|
||||
calls = mock.calls.RefreshCodeLens
|
||||
mock.lockRefreshCodeLens.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// RefreshDiagnostics calls RefreshDiagnosticsFunc.
|
||||
func (mock *ClientMock) RefreshDiagnostics(ctx context.Context) error {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
}{
|
||||
Ctx: ctx,
|
||||
}
|
||||
mock.lockRefreshDiagnostics.Lock()
|
||||
mock.calls.RefreshDiagnostics = append(mock.calls.RefreshDiagnostics, callInfo)
|
||||
mock.lockRefreshDiagnostics.Unlock()
|
||||
if mock.RefreshDiagnosticsFunc == nil {
|
||||
var errOut error
|
||||
return errOut
|
||||
}
|
||||
return mock.RefreshDiagnosticsFunc(ctx)
|
||||
}
|
||||
|
||||
// RefreshDiagnosticsCalls gets all the calls that were made to RefreshDiagnostics.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedClient.RefreshDiagnosticsCalls())
|
||||
func (mock *ClientMock) RefreshDiagnosticsCalls() []struct {
|
||||
Ctx context.Context
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
}
|
||||
mock.lockRefreshDiagnostics.RLock()
|
||||
calls = mock.calls.RefreshDiagnostics
|
||||
mock.lockRefreshDiagnostics.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// RefreshInlayHints calls RefreshInlayHintsFunc.
|
||||
func (mock *ClientMock) RefreshInlayHints(ctx context.Context) error {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
}{
|
||||
Ctx: ctx,
|
||||
}
|
||||
mock.lockRefreshInlayHints.Lock()
|
||||
mock.calls.RefreshInlayHints = append(mock.calls.RefreshInlayHints, callInfo)
|
||||
mock.lockRefreshInlayHints.Unlock()
|
||||
if mock.RefreshInlayHintsFunc == nil {
|
||||
var errOut error
|
||||
return errOut
|
||||
}
|
||||
return mock.RefreshInlayHintsFunc(ctx)
|
||||
}
|
||||
|
||||
// RefreshInlayHintsCalls gets all the calls that were made to RefreshInlayHints.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedClient.RefreshInlayHintsCalls())
|
||||
func (mock *ClientMock) RefreshInlayHintsCalls() []struct {
|
||||
Ctx context.Context
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
}
|
||||
mock.lockRefreshInlayHints.RLock()
|
||||
calls = mock.calls.RefreshInlayHints
|
||||
mock.lockRefreshInlayHints.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// SendTelemetry calls SendTelemetryFunc.
|
||||
func (mock *ClientMock) SendTelemetry(ctx context.Context, telemetry lsproto.TelemetryEvent) error {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
Telemetry lsproto.TelemetryEvent
|
||||
}{
|
||||
Ctx: ctx,
|
||||
Telemetry: telemetry,
|
||||
}
|
||||
mock.lockSendTelemetry.Lock()
|
||||
mock.calls.SendTelemetry = append(mock.calls.SendTelemetry, callInfo)
|
||||
mock.lockSendTelemetry.Unlock()
|
||||
if mock.SendTelemetryFunc == nil {
|
||||
var errOut error
|
||||
return errOut
|
||||
}
|
||||
return mock.SendTelemetryFunc(ctx, telemetry)
|
||||
}
|
||||
|
||||
// SendTelemetryCalls gets all the calls that were made to SendTelemetry.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedClient.SendTelemetryCalls())
|
||||
func (mock *ClientMock) SendTelemetryCalls() []struct {
|
||||
Ctx context.Context
|
||||
Telemetry lsproto.TelemetryEvent
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
Telemetry lsproto.TelemetryEvent
|
||||
}
|
||||
mock.lockSendTelemetry.RLock()
|
||||
calls = mock.calls.SendTelemetry
|
||||
mock.lockSendTelemetry.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// UnwatchFiles calls UnwatchFilesFunc.
|
||||
func (mock *ClientMock) UnwatchFiles(ctx context.Context, id project.WatcherID) error {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
ID project.WatcherID
|
||||
}{
|
||||
Ctx: ctx,
|
||||
ID: id,
|
||||
}
|
||||
mock.lockUnwatchFiles.Lock()
|
||||
mock.calls.UnwatchFiles = append(mock.calls.UnwatchFiles, callInfo)
|
||||
mock.lockUnwatchFiles.Unlock()
|
||||
if mock.UnwatchFilesFunc == nil {
|
||||
var errOut error
|
||||
return errOut
|
||||
}
|
||||
return mock.UnwatchFilesFunc(ctx, id)
|
||||
}
|
||||
|
||||
// UnwatchFilesCalls gets all the calls that were made to UnwatchFiles.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedClient.UnwatchFilesCalls())
|
||||
func (mock *ClientMock) UnwatchFilesCalls() []struct {
|
||||
Ctx context.Context
|
||||
ID project.WatcherID
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
ID project.WatcherID
|
||||
}
|
||||
mock.lockUnwatchFiles.RLock()
|
||||
calls = mock.calls.UnwatchFiles
|
||||
mock.lockUnwatchFiles.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// WatchFiles calls WatchFilesFunc.
|
||||
func (mock *ClientMock) WatchFiles(ctx context.Context, id project.WatcherID, watchers []*lsproto.FileSystemWatcher) error {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
ID project.WatcherID
|
||||
Watchers []*lsproto.FileSystemWatcher
|
||||
}{
|
||||
Ctx: ctx,
|
||||
ID: id,
|
||||
Watchers: watchers,
|
||||
}
|
||||
mock.lockWatchFiles.Lock()
|
||||
mock.calls.WatchFiles = append(mock.calls.WatchFiles, callInfo)
|
||||
mock.lockWatchFiles.Unlock()
|
||||
if mock.WatchFilesFunc == nil {
|
||||
var errOut error
|
||||
return errOut
|
||||
}
|
||||
return mock.WatchFilesFunc(ctx, id, watchers)
|
||||
}
|
||||
|
||||
// WatchFilesCalls gets all the calls that were made to WatchFiles.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedClient.WatchFilesCalls())
|
||||
func (mock *ClientMock) WatchFilesCalls() []struct {
|
||||
Ctx context.Context
|
||||
ID project.WatcherID
|
||||
Watchers []*lsproto.FileSystemWatcher
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
ID project.WatcherID
|
||||
Watchers []*lsproto.FileSystemWatcher
|
||||
}
|
||||
mock.lockWatchFiles.RLock()
|
||||
calls = mock.calls.WatchFiles
|
||||
mock.lockWatchFiles.RUnlock()
|
||||
return calls
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Code generated by moq; DO NOT EDIT.
|
||||
// github.com/matryer/moq
|
||||
|
||||
package projecttestutil
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/project/ata"
|
||||
)
|
||||
|
||||
// Ensure, that NpmExecutorMock does implement ata.NpmExecutor.
|
||||
// If this is not the case, regenerate this file with moq.
|
||||
var _ ata.NpmExecutor = &NpmExecutorMock{}
|
||||
|
||||
// NpmExecutorMock is a mock implementation of ata.NpmExecutor.
|
||||
//
|
||||
// func TestSomethingThatUsesNpmExecutor(t *testing.T) {
|
||||
//
|
||||
// // make and configure a mocked ata.NpmExecutor
|
||||
// mockedNpmExecutor := &NpmExecutorMock{
|
||||
// NpmInstallFunc: func(cwd string, args []string) ([]byte, error) {
|
||||
// panic("mock out the NpmInstall method")
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // use mockedNpmExecutor in code that requires ata.NpmExecutor
|
||||
// // and then make assertions.
|
||||
//
|
||||
// }
|
||||
type NpmExecutorMock struct {
|
||||
// NpmInstallFunc mocks the NpmInstall method.
|
||||
NpmInstallFunc func(cwd string, args []string) ([]byte, error)
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// NpmInstall holds details about calls to the NpmInstall method.
|
||||
NpmInstall []struct {
|
||||
// Cwd is the cwd argument value.
|
||||
Cwd string
|
||||
// Args is the args argument value.
|
||||
Args []string
|
||||
}
|
||||
}
|
||||
lockNpmInstall sync.RWMutex
|
||||
}
|
||||
|
||||
// NpmInstall calls NpmInstallFunc.
|
||||
func (mock *NpmExecutorMock) NpmInstall(cwd string, args []string) ([]byte, error) {
|
||||
callInfo := struct {
|
||||
Cwd string
|
||||
Args []string
|
||||
}{
|
||||
Cwd: cwd,
|
||||
Args: args,
|
||||
}
|
||||
mock.lockNpmInstall.Lock()
|
||||
mock.calls.NpmInstall = append(mock.calls.NpmInstall, callInfo)
|
||||
mock.lockNpmInstall.Unlock()
|
||||
if mock.NpmInstallFunc == nil {
|
||||
var (
|
||||
bytesOut []byte
|
||||
errOut error
|
||||
)
|
||||
return bytesOut, errOut
|
||||
}
|
||||
return mock.NpmInstallFunc(cwd, args)
|
||||
}
|
||||
|
||||
// NpmInstallCalls gets all the calls that were made to NpmInstall.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedNpmExecutor.NpmInstallCalls())
|
||||
func (mock *NpmExecutorMock) NpmInstallCalls() []struct {
|
||||
Cwd string
|
||||
Args []string
|
||||
} {
|
||||
var calls []struct {
|
||||
Cwd string
|
||||
Args []string
|
||||
}
|
||||
mock.lockNpmInstall.RLock()
|
||||
calls = mock.calls.NpmInstall
|
||||
mock.lockNpmInstall.RUnlock()
|
||||
return calls
|
||||
}
|
||||
323
tools/tsgo/internal/testutil/projecttestutil/projecttestutil.go
Normal file
323
tools/tsgo/internal/testutil/projecttestutil/projecttestutil.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package projecttestutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/bundled"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/glob"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/project"
|
||||
"github.com/microsoft/typescript-go/internal/project/logging"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/baseline"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/iovfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/osvfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
)
|
||||
|
||||
//go:generate go tool github.com/matryer/moq -stub -fmt goimports -pkg projecttestutil -out clientmock_generated.go ../../project Client
|
||||
//go:generate npx dprint fmt clientmock_generated.go
|
||||
|
||||
//go:generate go tool github.com/matryer/moq -stub -fmt goimports -pkg projecttestutil -out npmexecutormock_generated.go ../../project/ata NpmExecutor
|
||||
//go:generate npx dprint fmt npmexecutormock_generated.go
|
||||
|
||||
const (
|
||||
TestTypingsLocation = "/home/src/Library/Caches/typescript"
|
||||
)
|
||||
|
||||
type TypingsInstallerOptions struct {
|
||||
TypesRegistry []string
|
||||
PackageToFile map[string]string
|
||||
}
|
||||
|
||||
type SessionUtils struct {
|
||||
currentDirectory string
|
||||
fsFromFileMap iovfs.FsWithSys
|
||||
fs vfs.FS
|
||||
client *ClientMock
|
||||
npmExecutor *NpmExecutorMock
|
||||
tiOptions *TypingsInstallerOptions
|
||||
logger logging.LogCollector
|
||||
}
|
||||
|
||||
func (h *SessionUtils) FsFromFileMap() iovfs.FsWithSys {
|
||||
return h.fsFromFileMap
|
||||
}
|
||||
|
||||
func (h *SessionUtils) Client() *ClientMock {
|
||||
return h.client
|
||||
}
|
||||
|
||||
func (h *SessionUtils) NpmExecutor() *NpmExecutorMock {
|
||||
return h.npmExecutor
|
||||
}
|
||||
|
||||
func (h *SessionUtils) SetupNpmExecutorForTypingsInstaller() {
|
||||
if h.tiOptions == nil {
|
||||
return
|
||||
}
|
||||
|
||||
h.npmExecutor.NpmInstallFunc = func(cwd string, packageNames []string) ([]byte, error) {
|
||||
// packageNames is actually npmInstallArgs due to interface misnaming
|
||||
npmInstallArgs := packageNames
|
||||
lenNpmInstallArgs := len(npmInstallArgs)
|
||||
if lenNpmInstallArgs < 3 {
|
||||
return nil, fmt.Errorf("unexpected npm install: %s %v", cwd, npmInstallArgs)
|
||||
}
|
||||
|
||||
if lenNpmInstallArgs == 3 && npmInstallArgs[2] == "types-registry@latest" {
|
||||
// Write typings file
|
||||
err := h.fs.WriteFile(cwd+"/node_modules/types-registry/index.json", h.createTypesRegistryFileContent())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find the packages: they start at index 2 and continue until we hit a flag starting with --
|
||||
packageEnd := lenNpmInstallArgs
|
||||
for i := 2; i < lenNpmInstallArgs; i++ {
|
||||
if strings.HasPrefix(npmInstallArgs[i], "--") {
|
||||
packageEnd = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, atTypesPackageTs := range npmInstallArgs[2:packageEnd] {
|
||||
// @types/packageName@TsVersionToUse
|
||||
atTypesPackage := atTypesPackageTs
|
||||
// Remove version suffix
|
||||
if versionIndex := strings.LastIndex(atTypesPackage, "@"); versionIndex > 6 { // "@types/".length is 7, so version @ must be after
|
||||
atTypesPackage = atTypesPackage[:versionIndex]
|
||||
}
|
||||
// Extract package name from @types/packageName
|
||||
packageBaseName := atTypesPackage[7:] // Remove "@types/" prefix
|
||||
content, ok := h.tiOptions.PackageToFile[packageBaseName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("content not provided for %s", packageBaseName)
|
||||
}
|
||||
err := h.fs.WriteFile(cwd+"/node_modules/@types/"+packageBaseName+"/index.d.ts", content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SessionUtils) ToPath(fileName string) tspath.Path {
|
||||
return tspath.ToPath(fileName, h.currentDirectory, h.fs.UseCaseSensitiveFileNames())
|
||||
}
|
||||
|
||||
func (h *SessionUtils) FS() vfs.FS {
|
||||
return h.fs
|
||||
}
|
||||
|
||||
// WatchesFile reports whether any registered file watcher would match the given
|
||||
// file path. It handles both absolute glob patterns and relative patterns with
|
||||
// a base URI. On case-insensitive file systems the paths in glob patterns are
|
||||
// lowercased, so callers should pass the lowercased path.
|
||||
func (h *SessionUtils) WatchesFile(filePath string) bool {
|
||||
for _, call := range h.client.WatchFilesCalls() {
|
||||
for _, watcher := range call.Watchers {
|
||||
if watcher.GlobPattern.Pattern != nil {
|
||||
if g, err := glob.Parse(*watcher.GlobPattern.Pattern); err == nil && g.Match(filePath) {
|
||||
return true
|
||||
}
|
||||
} else if watcher.GlobPattern.RelativePattern != nil {
|
||||
rp := watcher.GlobPattern.RelativePattern
|
||||
baseUri := string(*rp.BaseUri.URI)
|
||||
// Convert base URI (e.g. "file:///home/projects") to a directory path
|
||||
// with trailing separator for proper prefix matching on path boundaries.
|
||||
baseDir := lsproto.DocumentUri(baseUri).FileName()
|
||||
baseDir = tspath.EnsureTrailingDirectorySeparator(baseDir)
|
||||
if strings.HasPrefix(filePath, baseDir) {
|
||||
relativePath := filePath[len(baseDir):]
|
||||
if g, err := glob.Parse(rp.Pattern); err == nil && g.Match(relativePath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *SessionUtils) Logs() string {
|
||||
return h.logger.String()
|
||||
}
|
||||
|
||||
func (h *SessionUtils) BaselineLogs(t *testing.T) {
|
||||
baseline.Run(t, t.Name()+".log", h.Logs(), baseline.Options{
|
||||
Subfolder: "project",
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
typesRegistryConfigTextOnce sync.Once
|
||||
typesRegistryConfigText string
|
||||
)
|
||||
|
||||
func TypesRegistryConfigText() string {
|
||||
typesRegistryConfigTextOnce.Do(func() {
|
||||
var result strings.Builder
|
||||
for key, value := range TypesRegistryConfig() {
|
||||
if result.Len() != 0 {
|
||||
result.WriteString(",")
|
||||
}
|
||||
result.WriteString(fmt.Sprintf("\n \"%s\": \"%s\"", key, value))
|
||||
|
||||
}
|
||||
typesRegistryConfigText = result.String()
|
||||
})
|
||||
return typesRegistryConfigText
|
||||
}
|
||||
|
||||
var (
|
||||
typesRegistryConfigOnce sync.Once
|
||||
typesRegistryConfig map[string]string
|
||||
)
|
||||
|
||||
func TypesRegistryConfig() map[string]string {
|
||||
typesRegistryConfigOnce.Do(func() {
|
||||
typesRegistryConfig = map[string]string{
|
||||
"latest": "1.3.0",
|
||||
"ts2.0": "1.0.0",
|
||||
"ts2.1": "1.0.0",
|
||||
"ts2.2": "1.2.0",
|
||||
"ts2.3": "1.3.0",
|
||||
"ts2.4": "1.3.0",
|
||||
"ts2.5": "1.3.0",
|
||||
"ts2.6": "1.3.0",
|
||||
"ts2.7": "1.3.0",
|
||||
}
|
||||
})
|
||||
return typesRegistryConfig
|
||||
}
|
||||
|
||||
func (h *SessionUtils) createTypesRegistryFileContent() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("{\n \"entries\": {")
|
||||
for index, entry := range h.tiOptions.TypesRegistry {
|
||||
h.appendTypesRegistryConfig(&builder, index, entry)
|
||||
}
|
||||
index := len(h.tiOptions.TypesRegistry)
|
||||
for key := range h.tiOptions.PackageToFile {
|
||||
if !slices.Contains(h.tiOptions.TypesRegistry, key) {
|
||||
h.appendTypesRegistryConfig(&builder, index, key)
|
||||
index++
|
||||
}
|
||||
}
|
||||
builder.WriteString("\n }\n}")
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func (h *SessionUtils) appendTypesRegistryConfig(builder *strings.Builder, index int, entry string) {
|
||||
if index > 0 {
|
||||
builder.WriteString(",")
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("\n \"%s\": {%s\n }", entry, TypesRegistryConfigText()))
|
||||
}
|
||||
|
||||
func Setup(files map[string]any) (*project.Session, *SessionUtils) {
|
||||
return SetupWithTypingsInstaller(files, &TypingsInstallerOptions{})
|
||||
}
|
||||
|
||||
func SetupWithRealFS() (*project.Session, *SessionUtils) {
|
||||
fs := bundled.WrapFS(osvfs.FS())
|
||||
clientMock := &ClientMock{}
|
||||
npmExecutorMock := &NpmExecutorMock{}
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sessionUtils := &SessionUtils{
|
||||
currentDirectory: wd,
|
||||
fs: fs,
|
||||
client: clientMock,
|
||||
npmExecutor: npmExecutorMock,
|
||||
logger: logging.NewTestLogger(),
|
||||
}
|
||||
|
||||
return project.NewSession(&project.SessionInit{
|
||||
BackgroundCtx: context.Background(),
|
||||
FS: fs,
|
||||
Client: clientMock,
|
||||
NpmExecutor: npmExecutorMock,
|
||||
Logger: sessionUtils.logger,
|
||||
Options: &project.SessionOptions{
|
||||
CurrentDirectory: wd,
|
||||
DefaultLibraryPath: bundled.LibPath(),
|
||||
PositionEncoding: lsproto.PositionEncodingKindUTF8,
|
||||
WatchEnabled: true,
|
||||
LoggingEnabled: true,
|
||||
PushDiagnosticsEnabled: true,
|
||||
},
|
||||
}), sessionUtils
|
||||
}
|
||||
|
||||
func SetupWithOptions(files map[string]any, options *project.SessionOptions) (*project.Session, *SessionUtils) {
|
||||
return SetupWithOptionsAndTypingsInstaller(files, options, &TypingsInstallerOptions{})
|
||||
}
|
||||
|
||||
func SetupWithTypingsInstaller(files map[string]any, tiOptions *TypingsInstallerOptions) (*project.Session, *SessionUtils) {
|
||||
return SetupWithOptionsAndTypingsInstaller(files, nil, tiOptions)
|
||||
}
|
||||
|
||||
func SetupWithOptionsAndTypingsInstaller(files map[string]any, options *project.SessionOptions, tiOptions *TypingsInstallerOptions) (*project.Session, *SessionUtils) {
|
||||
init, sessionUtils := GetSessionInitOptions(files, options, tiOptions)
|
||||
session := project.NewSession(init)
|
||||
|
||||
return session, sessionUtils
|
||||
}
|
||||
|
||||
func WithRequestID(ctx context.Context) context.Context {
|
||||
return core.WithRequestID(ctx, "0")
|
||||
}
|
||||
|
||||
func GetSessionInitOptions(files map[string]any, options *project.SessionOptions, tiOptions *TypingsInstallerOptions) (*project.SessionInit, *SessionUtils) {
|
||||
fsFromFileMap := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/)
|
||||
fs := bundled.WrapFS(fsFromFileMap)
|
||||
clientMock := &ClientMock{}
|
||||
npmExecutorMock := &NpmExecutorMock{}
|
||||
sessionUtils := &SessionUtils{
|
||||
currentDirectory: "/",
|
||||
fsFromFileMap: fsFromFileMap.(iovfs.FsWithSys),
|
||||
fs: fs,
|
||||
client: clientMock,
|
||||
npmExecutor: npmExecutorMock,
|
||||
tiOptions: tiOptions,
|
||||
logger: logging.NewTestLogger(),
|
||||
}
|
||||
|
||||
// Configure the npm executor mock to handle typings installation
|
||||
sessionUtils.SetupNpmExecutorForTypingsInstaller()
|
||||
|
||||
// Use provided options or create default ones
|
||||
if options == nil {
|
||||
options = &project.SessionOptions{
|
||||
CurrentDirectory: "/",
|
||||
DefaultLibraryPath: bundled.LibPath(),
|
||||
TypingsLocation: TestTypingsLocation,
|
||||
PositionEncoding: lsproto.PositionEncodingKindUTF8,
|
||||
WatchEnabled: true,
|
||||
LoggingEnabled: true,
|
||||
PushDiagnosticsEnabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
return &project.SessionInit{
|
||||
BackgroundCtx: context.Background(),
|
||||
Options: options,
|
||||
FS: fs,
|
||||
Client: clientMock,
|
||||
NpmExecutor: npmExecutorMock,
|
||||
Logger: sessionUtils.logger,
|
||||
}, sessionUtils
|
||||
}
|
||||
7
tools/tsgo/internal/testutil/race/norace.go
Normal file
7
tools/tsgo/internal/testutil/race/norace.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !race
|
||||
|
||||
// Package israce reports if the Go race detector is enabled.
|
||||
package race
|
||||
|
||||
// Enabled reports if the race detector is enabled.
|
||||
const Enabled = false
|
||||
7
tools/tsgo/internal/testutil/race/race.go
Normal file
7
tools/tsgo/internal/testutil/race/race.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build race
|
||||
|
||||
// Package israce reports if the Go race detector is enabled.
|
||||
package race
|
||||
|
||||
// Enabled reports if the race detector is enabled.
|
||||
const Enabled = true
|
||||
@@ -0,0 +1,51 @@
|
||||
package stringtestutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
func Dedent(text string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
// Remove blank lines in the beginning and end
|
||||
// and convert all tabs in the beginning of line to spaces
|
||||
startLine := -1
|
||||
lastLine := 0
|
||||
for i, line := range lines {
|
||||
firstNonWhite := strings.IndexFunc(line, func(r rune) bool {
|
||||
return !stringutil.IsWhiteSpaceLike(r)
|
||||
})
|
||||
if firstNonWhite > 0 {
|
||||
line = strings.ReplaceAll(line[0:firstNonWhite], "\t", " ") + line[firstNonWhite:]
|
||||
lines[i] = line
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
if startLine == -1 {
|
||||
startLine = i
|
||||
}
|
||||
lastLine = i
|
||||
}
|
||||
}
|
||||
lines = lines[startLine : lastLine+1]
|
||||
mappedLines := make([]string, len(lines))
|
||||
for i, line := range lines {
|
||||
if trimmed := strings.TrimSpace(line); trimmed == "" {
|
||||
mappedLines[i] = ""
|
||||
} else {
|
||||
mappedLines[i] = line
|
||||
}
|
||||
}
|
||||
indentation := stringutil.GuessIndentation(mappedLines)
|
||||
if indentation > 0 {
|
||||
for i := range lines {
|
||||
if len(lines[i]) > indentation {
|
||||
lines[i] = lines[i][indentation:]
|
||||
} else {
|
||||
lines[i] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
49
tools/tsgo/internal/testutil/testutil.go
Normal file
49
tools/tsgo/internal/testutil/testutil.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/testutil/race"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func AssertPanics(tb testing.TB, fn func(), expected any, msgAndArgs ...any) {
|
||||
tb.Helper()
|
||||
|
||||
var got any
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
got = recover()
|
||||
}()
|
||||
fn()
|
||||
}()
|
||||
|
||||
assert.Assert(tb, got != nil, msgAndArgs...)
|
||||
assert.Equal(tb, got, expected, msgAndArgs...)
|
||||
}
|
||||
|
||||
func RecoverAndFail(t *testing.T, msg string) {
|
||||
if r := recover(); r != nil {
|
||||
stack := debug.Stack()
|
||||
t.Fatalf("%s:\n%v\n%s", msg, r, string(stack))
|
||||
}
|
||||
}
|
||||
|
||||
var testProgramIsSingleThreaded = sync.OnceValue(func() bool {
|
||||
// Leave Program in SingleThreaded mode unless explicitly configured or in race mode.
|
||||
if v := os.Getenv("TS_TEST_PROGRAM_SINGLE_THREADED"); v != "" {
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return !race.Enabled
|
||||
})
|
||||
|
||||
func TestProgramIsSingleThreaded() bool {
|
||||
return testProgramIsSingleThreaded()
|
||||
}
|
||||
260
tools/tsgo/internal/testutil/tsbaseline/error_baseline.go
Normal file
260
tools/tsgo/internal/testutil/tsbaseline/error_baseline.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package tsbaseline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnosticwriter"
|
||||
"github.com/microsoft/typescript-go/internal/locale"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/baseline"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/harnessutil"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
// IO
|
||||
const harnessNewLine = "\r\n"
|
||||
|
||||
var formatOpts = &diagnosticwriter.FormattingOptions{
|
||||
NewLine: harnessNewLine,
|
||||
}
|
||||
|
||||
var (
|
||||
diagnosticsLocationPrefix = regexp.MustCompile(`(?im)^(lib.*\.d\.ts)\(\d+,\d+\)`)
|
||||
diagnosticsLocationPattern = regexp.MustCompile(`(?i)(lib.*\.d\.ts):\d+:\d+`)
|
||||
)
|
||||
|
||||
func DoErrorBaseline(t *testing.T, baselinePath string, inputFiles []*harnessutil.TestFile, errors []*ast.Diagnostic, pretty bool, opts baseline.Options) {
|
||||
baselinePath = tsExtension.ReplaceAllString(baselinePath, ".errors.txt")
|
||||
var errorBaseline string
|
||||
if len(errors) > 0 {
|
||||
errorBaseline = GetErrorBaseline(t, inputFiles, diagnosticwriter.WrapASTDiagnostics(errors), diagnosticwriter.CompareASTDiagnostics, pretty)
|
||||
} else {
|
||||
errorBaseline = baseline.NoContent
|
||||
}
|
||||
baseline.Run(t, baselinePath, errorBaseline, opts)
|
||||
}
|
||||
|
||||
func minimalDiagnosticsToString(diagnostics []diagnosticwriter.Diagnostic, pretty bool) string {
|
||||
var output strings.Builder
|
||||
if pretty {
|
||||
diagnosticwriter.FormatDiagnosticsWithColorAndContext(&output, diagnostics, formatOpts)
|
||||
} else {
|
||||
diagnosticwriter.WriteFormatDiagnostics(&output, diagnostics, formatOpts)
|
||||
}
|
||||
return output.String()
|
||||
}
|
||||
|
||||
func GetErrorBaseline[T diagnosticwriter.Diagnostic](t *testing.T, inputFiles []*harnessutil.TestFile, diagnostics []T, compareDiagnostics func(a, b T) int, pretty bool) string {
|
||||
t.Helper()
|
||||
outputLines := iterateErrorBaseline(t, inputFiles, diagnostics, compareDiagnostics, pretty)
|
||||
|
||||
if pretty {
|
||||
var summaryBuilder strings.Builder
|
||||
diagnosticwriter.WriteErrorSummaryText(
|
||||
&summaryBuilder,
|
||||
diagnosticwriter.ToDiagnostics(diagnostics),
|
||||
formatOpts,
|
||||
)
|
||||
summary := removeTestPathPrefixes(summaryBuilder.String(), false)
|
||||
outputLines = append(outputLines, summary)
|
||||
}
|
||||
return strings.Join(outputLines, "")
|
||||
}
|
||||
|
||||
func iterateErrorBaseline[T diagnosticwriter.Diagnostic](t *testing.T, inputFiles []*harnessutil.TestFile, inputDiagnostics []T, compareDiagnostics func(a, b T) int, pretty bool) []string {
|
||||
t.Helper()
|
||||
diagnostics := slices.Clone(inputDiagnostics)
|
||||
slices.SortFunc(diagnostics, compareDiagnostics)
|
||||
|
||||
var outputLines strings.Builder
|
||||
// Count up all errors that were found in files other than lib.d.ts so we don't miss any
|
||||
totalErrorsReportedInNonLibraryNonTsconfigFiles := 0
|
||||
errorsReported := 0
|
||||
|
||||
firstLine := true
|
||||
|
||||
newLine := func() string {
|
||||
if firstLine {
|
||||
firstLine = false
|
||||
return ""
|
||||
}
|
||||
return "\r\n"
|
||||
}
|
||||
|
||||
var result []string
|
||||
|
||||
outputErrorText := func(diag diagnosticwriter.Diagnostic) {
|
||||
message := diagnosticwriter.FlattenDiagnosticMessage(diag, harnessNewLine, locale.Default)
|
||||
|
||||
var errLines []string
|
||||
for line := range strings.SplitSeq(removeTestPathPrefixes(message, false), "\n") {
|
||||
line = strings.TrimSuffix(line, "\r")
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
out := fmt.Sprintf("!!! %s TS%d: %s", diag.Category().Name(), diag.Code(), line)
|
||||
errLines = append(errLines, out)
|
||||
}
|
||||
|
||||
for _, info := range diag.RelatedInformation() {
|
||||
var location string
|
||||
if info.File() != nil {
|
||||
location = " " + formatLocation(info.File(), info.Pos(), formatOpts, func(output io.Writer, text string, formatStyle string) { fmt.Fprint(output, text) })
|
||||
}
|
||||
location = removeTestPathPrefixes(location, false)
|
||||
if len(location) > 0 && isDefaultLibraryFile(info.File().FileName()) {
|
||||
location = diagnosticsLocationPattern.ReplaceAllString(location, "$1:--:--")
|
||||
}
|
||||
errLines = append(errLines, fmt.Sprintf("!!! related TS%d%s: %s", info.Code(), location, diagnosticwriter.FlattenDiagnosticMessage(info, harnessNewLine, locale.Default)))
|
||||
}
|
||||
|
||||
for _, e := range errLines {
|
||||
outputLines.WriteString(newLine())
|
||||
outputLines.WriteString(e)
|
||||
}
|
||||
|
||||
errorsReported++
|
||||
|
||||
// do not count errors from lib.d.ts here, they are computed separately as numLibraryDiagnostics
|
||||
// if lib.d.ts is explicitly included in input files and there are some errors in it (i.e. because of duplicate identifiers)
|
||||
// then they will be added twice thus triggering 'total errors' assertion with condition
|
||||
// Similarly for tsconfig, which may be in the input files and contain errors.
|
||||
// 'totalErrorsReportedInNonLibraryNonTsconfigFiles + numLibraryDiagnostics + numTsconfigDiagnostics, diagnostics.length
|
||||
|
||||
if diag.File() == nil || !isDefaultLibraryFile(diag.File().FileName()) && !isTsConfigFile(diag.File().FileName()) {
|
||||
totalErrorsReportedInNonLibraryNonTsconfigFiles++
|
||||
}
|
||||
}
|
||||
|
||||
topDiagnostics := minimalDiagnosticsToString(diagnosticwriter.ToDiagnostics(diagnostics), pretty)
|
||||
topDiagnostics = removeTestPathPrefixes(topDiagnostics, false)
|
||||
topDiagnostics = diagnosticsLocationPrefix.ReplaceAllString(topDiagnostics, "$1(--,--)")
|
||||
|
||||
result = append(result, topDiagnostics+harnessNewLine+harnessNewLine)
|
||||
|
||||
// Report global errors
|
||||
for _, error := range diagnostics {
|
||||
if error.File() == nil {
|
||||
outputErrorText(error)
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, outputLines.String())
|
||||
outputLines.Reset()
|
||||
errorsReported = 0
|
||||
|
||||
// 'merge' the lines of each input file with any errors associated with it
|
||||
dupeCase := map[string]int{}
|
||||
for _, inputFile := range inputFiles {
|
||||
// Filter down to the errors in the file
|
||||
fileErrors := core.Filter(diagnostics, func(e T) bool {
|
||||
return e.File() != nil &&
|
||||
tspath.ComparePaths(removeTestPathPrefixes(e.File().FileName(), false), removeTestPathPrefixes(inputFile.UnitName, false), tspath.ComparePathsOptions{}) == 0
|
||||
})
|
||||
|
||||
// Header
|
||||
fmt.Fprintf(
|
||||
&outputLines,
|
||||
"%s==== %s (%d errors) ====",
|
||||
newLine(),
|
||||
removeTestPathPrefixes(inputFile.UnitName, false),
|
||||
len(fileErrors),
|
||||
)
|
||||
|
||||
// Make sure we emit something for every error
|
||||
markedErrorCount := 0
|
||||
// For each line, emit the line followed by any error squiggles matching this line
|
||||
|
||||
lineStarts := core.ComputeECMALineStarts(inputFile.Content)
|
||||
lines := lineDelimiter.Split(inputFile.Content, -1)
|
||||
|
||||
for lineIndex, line := range lines {
|
||||
if len(line) > 0 && line[len(line)-1] == '\r' {
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
|
||||
thisLineStart := int(lineStarts[lineIndex])
|
||||
var nextLineStart int
|
||||
// On the last line of the file, fake the next line start number so that we handle errors on the last character of the file correctly
|
||||
if lineIndex == len(lines)-1 {
|
||||
nextLineStart = len(inputFile.Content)
|
||||
} else {
|
||||
nextLineStart = int(lineStarts[lineIndex+1])
|
||||
}
|
||||
// Emit this line from the original file
|
||||
outputLines.WriteString(newLine())
|
||||
outputLines.WriteString(" ")
|
||||
outputLines.WriteString(line)
|
||||
for _, errDiagnostic := range fileErrors {
|
||||
// Does any error start or continue on to this line? Emit squiggles
|
||||
errStart := errDiagnostic.Pos()
|
||||
end := errStart + errDiagnostic.Len()
|
||||
if end >= thisLineStart && (errStart < nextLineStart || lineIndex == len(lines)-1) {
|
||||
// How many characters from the start of this line the error starts at (could be positive or negative)
|
||||
relativeOffset := errStart - thisLineStart
|
||||
// How many characters of the error are on this line (might be longer than this line in reality)
|
||||
length := (end - errStart) - max(0, thisLineStart-errStart)
|
||||
// Calculate the start of the squiggle
|
||||
squiggleStart := max(0, relativeOffset)
|
||||
// TODO/REVIEW: this doesn't work quite right in the browser if a multi file test has files whose names are just the right length relative to one another
|
||||
outputLines.WriteString(newLine())
|
||||
outputLines.WriteString(" ")
|
||||
outputLines.WriteString(nonWhitespace.ReplaceAllString(line[:squiggleStart], " "))
|
||||
// This was `new Array(count).join("~")`; which maps 0 to "", 1 to "", 2 to "~", 3 to "~~", etc.
|
||||
squiggleEnd := max(squiggleStart, min(squiggleStart+length, len(line)))
|
||||
outputLines.WriteString(strings.Repeat("~", utf8.RuneCountInString(line[squiggleStart:squiggleEnd])))
|
||||
// If the error ended here, or we're at the end of the file, emit its message
|
||||
if lineIndex == len(lines)-1 || nextLineStart > end {
|
||||
outputErrorText(errDiagnostic)
|
||||
markedErrorCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify we didn't miss any errors in this file
|
||||
assert.Check(t, cmp.Equal(markedErrorCount, len(fileErrors)), "count of errors in "+inputFile.UnitName)
|
||||
_, isDupe := dupeCase[sanitizeTestFilePath(inputFile.UnitName)]
|
||||
result = append(result, outputLines.String())
|
||||
if isDupe {
|
||||
// Case-duplicated files on a case-insensitive build will have errors reported in both the dupe and the original
|
||||
// thanks to the canse-insensitive path comparison on the error file path - We only want to count those errors once
|
||||
// for the assert below, so we subtract them here.
|
||||
totalErrorsReportedInNonLibraryNonTsconfigFiles -= errorsReported
|
||||
}
|
||||
outputLines.Reset()
|
||||
errorsReported = 0
|
||||
}
|
||||
|
||||
numLibraryDiagnostics := core.CountWhere(
|
||||
diagnostics,
|
||||
func(d T) bool {
|
||||
return d.File() != nil && (isDefaultLibraryFile(d.File().FileName()) || isBuiltFile(d.File().FileName()))
|
||||
},
|
||||
)
|
||||
numTsconfigDiagnostics := core.CountWhere(
|
||||
diagnostics,
|
||||
func(d T) bool {
|
||||
return d.File() != nil && isTsConfigFile(d.File().FileName())
|
||||
},
|
||||
)
|
||||
// Verify we didn't miss any errors in total
|
||||
assert.Check(t, cmp.Equal(totalErrorsReportedInNonLibraryNonTsconfigFiles+numLibraryDiagnostics+numTsconfigDiagnostics, len(diagnostics)), "total number of errors")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func formatLocation(file diagnosticwriter.FileLike, pos int, formatOpts *diagnosticwriter.FormattingOptions, writeWithStyleAndReset diagnosticwriter.FormattedWriter) string {
|
||||
var output strings.Builder
|
||||
diagnosticwriter.WriteLocation(&output, file, pos, formatOpts, writeWithStyleAndReset)
|
||||
return output.String()
|
||||
}
|
||||
285
tools/tsgo/internal/testutil/tsbaseline/js_emit_baseline.go
Normal file
285
tools/tsgo/internal/testutil/tsbaseline/js_emit_baseline.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package tsbaseline
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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/diagnosticwriter"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/baseline"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/harnessutil"
|
||||
"github.com/microsoft/typescript-go/internal/tsoptions"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
func DoJSEmitBaseline(
|
||||
t *testing.T,
|
||||
baselinePath string,
|
||||
header string,
|
||||
options *core.CompilerOptions,
|
||||
result *harnessutil.CompilationResult,
|
||||
tsConfigFiles []*harnessutil.TestFile,
|
||||
toBeCompiled []*harnessutil.TestFile,
|
||||
otherFiles []*harnessutil.TestFile,
|
||||
harnessSettings *harnessutil.HarnessOptions,
|
||||
opts baseline.Options,
|
||||
) {
|
||||
if !options.NoEmit.IsTrue() && !options.EmitDeclarationOnly.IsTrue() && result.JS.Size() == 0 && len(result.Diagnostics) == 0 {
|
||||
t.Fatal("Expected at least one js file to be emitted or at least one error to be created.")
|
||||
}
|
||||
|
||||
// check js output
|
||||
var tsCode strings.Builder
|
||||
tsSources := core.Concatenate(otherFiles, toBeCompiled)
|
||||
tsCode.WriteString("//// [")
|
||||
tsCode.WriteString(header)
|
||||
tsCode.WriteString("] ////\r\n\r\n")
|
||||
|
||||
for i, file := range tsSources {
|
||||
tsCode.WriteString("//// [")
|
||||
tsCode.WriteString(tspath.GetBaseFileName(file.UnitName))
|
||||
tsCode.WriteString("]\r\n")
|
||||
tsCode.WriteString(file.Content)
|
||||
if i < len(tsSources)-1 {
|
||||
tsCode.WriteString("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
var jsCode strings.Builder
|
||||
for file := range result.JS.Values() {
|
||||
if jsCode.Len() > 0 && !strings.HasSuffix(jsCode.String(), "\n") {
|
||||
jsCode.WriteString("\r\n")
|
||||
}
|
||||
if len(result.Diagnostics) == 0 && strings.HasSuffix(file.UnitName, tspath.ExtensionJson) {
|
||||
fileParseResult := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: file.UnitName,
|
||||
Path: tspath.Path(file.UnitName),
|
||||
}, file.Content, core.ScriptKindJSON)
|
||||
if len(fileParseResult.Diagnostics()) > 0 {
|
||||
jsCode.WriteString(GetErrorBaseline(t, []*harnessutil.TestFile{file}, diagnosticwriter.WrapASTDiagnostics(fileParseResult.Diagnostics()), diagnosticwriter.CompareASTDiagnostics, false /*pretty*/))
|
||||
continue
|
||||
}
|
||||
}
|
||||
jsCode.WriteString(fileOutput(file, harnessSettings))
|
||||
}
|
||||
|
||||
if result.DTS.Size() > 0 {
|
||||
jsCode.WriteString("\r\n\r\n")
|
||||
for declFile := range result.DTS.Values() {
|
||||
jsCode.WriteString(fileOutput(declFile, harnessSettings))
|
||||
}
|
||||
}
|
||||
|
||||
declFileContext := prepareDeclarationCompilationContext(
|
||||
toBeCompiled,
|
||||
otherFiles,
|
||||
result,
|
||||
harnessSettings,
|
||||
options,
|
||||
"", /*currentDirectory*/
|
||||
)
|
||||
declFileCompilationResult := compileDeclarationFiles(t, declFileContext, result.Symlinks)
|
||||
|
||||
if declFileCompilationResult != nil && len(declFileCompilationResult.declResult.Diagnostics) > 0 {
|
||||
jsCode.WriteString("\r\n\r\n//// [DtsFileErrors]\r\n")
|
||||
jsCode.WriteString("\r\n\r\n")
|
||||
jsCode.WriteString(GetErrorBaseline(
|
||||
t,
|
||||
slices.Concat(tsConfigFiles, declFileCompilationResult.declInputFiles, declFileCompilationResult.declOtherFiles),
|
||||
diagnosticwriter.WrapASTDiagnostics(declFileCompilationResult.declResult.Diagnostics),
|
||||
diagnosticwriter.CompareASTDiagnostics,
|
||||
false, /*pretty*/
|
||||
))
|
||||
}
|
||||
|
||||
if !options.NoCheck.IsTrue() && !options.NoEmit.IsTrue() {
|
||||
testConfig := make(map[string]string)
|
||||
testConfig["noCheck"] = "true"
|
||||
withoutChecking := result.Repeat(testConfig)
|
||||
compareResultFileSets := func(a *collections.OrderedMap[string, *harnessutil.TestFile], b *collections.OrderedMap[string, *harnessutil.TestFile]) {
|
||||
for key, doc := range a.Entries() {
|
||||
original := b.GetOrZero(key)
|
||||
if original == nil {
|
||||
jsCode.WriteString("\r\n\r\n!!!! File ")
|
||||
jsCode.WriteString(removeTestPathPrefixes(doc.UnitName, false /*retainTrailingDirectorySeparator*/))
|
||||
jsCode.WriteString(" missing from original emit, but present in noCheck emit\r\n")
|
||||
jsCode.WriteString(fileOutput(doc, harnessSettings))
|
||||
} else if original.Content != doc.Content {
|
||||
jsCode.WriteString("\r\n\r\n!!!! File ")
|
||||
jsCode.WriteString(removeTestPathPrefixes(doc.UnitName, false /*retainTrailingDirectorySeparator*/))
|
||||
jsCode.WriteString(" differs from original emit in noCheck emit\r\n")
|
||||
var fileName string
|
||||
if harnessSettings.FullEmitPaths {
|
||||
fileName = removeTestPathPrefixes(doc.UnitName, false /*retainTrailingDirectorySeparator*/)
|
||||
} else {
|
||||
fileName = tspath.GetBaseFileName(doc.UnitName)
|
||||
}
|
||||
jsCode.WriteString("//// [")
|
||||
jsCode.WriteString(fileName)
|
||||
jsCode.WriteString("]\r\n")
|
||||
expected := original.Content
|
||||
actual := doc.Content
|
||||
jsCode.WriteString(baseline.DiffText("Expected\tThe full check baseline", "Actual\twith noCheck set", expected, actual))
|
||||
}
|
||||
}
|
||||
}
|
||||
compareResultFileSets(&withoutChecking.DTS, &result.DTS)
|
||||
compareResultFileSets(&withoutChecking.JS, &result.JS)
|
||||
}
|
||||
|
||||
if tspath.FileExtensionIsOneOf(baselinePath, []string{tspath.ExtensionTs, tspath.ExtensionTsx}) {
|
||||
baselinePath = tspath.ChangeExtension(baselinePath, tspath.ExtensionJs)
|
||||
}
|
||||
|
||||
var actual string
|
||||
if jsCode.Len() > 0 {
|
||||
actual = tsCode.String() + "\r\n\r\n" + jsCode.String()
|
||||
} else {
|
||||
actual = baseline.NoContent
|
||||
}
|
||||
|
||||
baseline.Run(t, baselinePath, actual, opts)
|
||||
}
|
||||
|
||||
func fileOutput(file *harnessutil.TestFile, settings *harnessutil.HarnessOptions) string {
|
||||
var fileName string
|
||||
if settings.FullEmitPaths {
|
||||
fileName = removeTestPathPrefixes(file.UnitName, false /*retainTrailingDirectorySeparator*/)
|
||||
} else {
|
||||
fileName = tspath.GetBaseFileName(file.UnitName)
|
||||
}
|
||||
return "//// [" + fileName + "]\r\n" + file.Content
|
||||
}
|
||||
|
||||
type declarationCompilationContext struct {
|
||||
declInputFiles []*harnessutil.TestFile
|
||||
declOtherFiles []*harnessutil.TestFile
|
||||
harnessSettings *harnessutil.HarnessOptions
|
||||
options *core.CompilerOptions
|
||||
currentDirectory string
|
||||
configFile *tsoptions.TsConfigSourceFile
|
||||
}
|
||||
|
||||
func prepareDeclarationCompilationContext(
|
||||
inputFiles []*harnessutil.TestFile,
|
||||
otherFiles []*harnessutil.TestFile,
|
||||
result *harnessutil.CompilationResult,
|
||||
harnessSettings *harnessutil.HarnessOptions,
|
||||
options *core.CompilerOptions,
|
||||
// Current directory is needed for rwcRunner to be able to use currentDirectory defined in json file
|
||||
currentDirectory string,
|
||||
) *declarationCompilationContext {
|
||||
if options.Declaration.IsTrue() && len(result.Diagnostics) == 0 {
|
||||
if options.EmitDeclarationOnly.IsTrue() {
|
||||
if result.JS.Size() > 0 {
|
||||
panic("Only declaration files should be generated when emitDeclarationOnly:true")
|
||||
}
|
||||
if result.DTS.Size() == 0 && !options.NoEmit.IsTrue() {
|
||||
panic("Expected at least one declaration file to be emitted when emitDeclarationOnly:true and no errors were generated")
|
||||
}
|
||||
} else if result.DTS.Size() != result.GetNumberOfJSFiles(false /*includeJson*/) {
|
||||
panic("There were no errors and declFiles generated did not match number of js files generated")
|
||||
}
|
||||
}
|
||||
|
||||
var declInputFiles []*harnessutil.TestFile
|
||||
var declOtherFiles []*harnessutil.TestFile
|
||||
|
||||
findUnit := func(fileName string, units []*harnessutil.TestFile) *harnessutil.TestFile {
|
||||
for _, unit := range units {
|
||||
if unit.UnitName == fileName {
|
||||
return unit
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
findResultCodeFile := func(fileName string) *harnessutil.TestFile {
|
||||
sourceFile := result.Program.GetSourceFile(fileName)
|
||||
if sourceFile == nil {
|
||||
panic("Program has no source file with name '" + fileName + "'")
|
||||
}
|
||||
// Is this file going to be emitted separately
|
||||
var sourceFileName string
|
||||
|
||||
if len(options.OutDir) != 0 {
|
||||
sourceFilePath := tspath.GetNormalizedAbsolutePath(sourceFile.FileName(), result.Host.GetCurrentDirectory())
|
||||
sourceFilePath = strings.Replace(sourceFilePath, result.Program.CommonSourceDirectory(), "", 1)
|
||||
sourceFileName = tspath.CombinePaths(options.OutDir, sourceFilePath)
|
||||
} else {
|
||||
sourceFileName = sourceFile.FileName()
|
||||
}
|
||||
|
||||
dTsFileName := tspath.RemoveFileExtension(sourceFileName) + tspath.GetDeclarationEmitExtensionForPath(sourceFileName)
|
||||
return result.DTS.GetOrZero(dTsFileName)
|
||||
}
|
||||
|
||||
addDtsFile := func(file *harnessutil.TestFile, dtsFiles []*harnessutil.TestFile) []*harnessutil.TestFile {
|
||||
if tspath.IsDeclarationFileName(file.UnitName) || tspath.HasJSONFileExtension(file.UnitName) {
|
||||
dtsFiles = append(dtsFiles, file)
|
||||
} else if tspath.HasTSFileExtension(file.UnitName) || (tspath.HasJSFileExtension(file.UnitName) && options.GetAllowJS()) {
|
||||
declFile := findResultCodeFile(file.UnitName)
|
||||
if declFile != nil && findUnit(declFile.UnitName, declInputFiles) == nil && findUnit(declFile.UnitName, declOtherFiles) == nil {
|
||||
dtsFiles = append(dtsFiles, &harnessutil.TestFile{
|
||||
UnitName: declFile.UnitName,
|
||||
Content: strings.TrimPrefix(declFile.Content, "\uFEFF"),
|
||||
})
|
||||
}
|
||||
}
|
||||
return dtsFiles
|
||||
}
|
||||
|
||||
// if the .d.ts is non-empty, confirm it compiles correctly as well
|
||||
if options.Declaration.IsTrue() && len(result.Diagnostics) == 0 && result.DTS.Size() > 0 {
|
||||
for _, file := range inputFiles {
|
||||
declInputFiles = addDtsFile(file, declInputFiles)
|
||||
}
|
||||
for _, file := range otherFiles {
|
||||
declOtherFiles = addDtsFile(file, declOtherFiles)
|
||||
}
|
||||
return &declarationCompilationContext{
|
||||
declInputFiles: declInputFiles,
|
||||
declOtherFiles: declOtherFiles,
|
||||
harnessSettings: harnessSettings,
|
||||
options: options,
|
||||
currentDirectory: core.IfElse(len(currentDirectory) > 0, currentDirectory, harnessSettings.CurrentDirectory),
|
||||
configFile: result.Program.Program().CommandLine().ConfigFile,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type declarationCompilationResult struct {
|
||||
declInputFiles []*harnessutil.TestFile
|
||||
declOtherFiles []*harnessutil.TestFile
|
||||
declResult *harnessutil.CompilationResult
|
||||
}
|
||||
|
||||
func compileDeclarationFiles(t *testing.T, context *declarationCompilationContext, symlinks map[string]string) *declarationCompilationResult {
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
var tsconfig *tsoptions.ParsedCommandLine
|
||||
if context.configFile != nil {
|
||||
tsconfig = &tsoptions.ParsedCommandLine{
|
||||
ConfigFile: context.configFile,
|
||||
}
|
||||
}
|
||||
declFileCompilationResult := harnessutil.CompileFilesEx(t,
|
||||
context.declInputFiles,
|
||||
context.declOtherFiles,
|
||||
context.harnessSettings,
|
||||
context.options,
|
||||
context.currentDirectory,
|
||||
symlinks,
|
||||
tsconfig)
|
||||
return &declarationCompilationResult{
|
||||
context.declInputFiles,
|
||||
context.declOtherFiles,
|
||||
declFileCompilationResult,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package tsbaseline
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/testutil/baseline"
|
||||
)
|
||||
|
||||
func DoModuleResolutionBaseline(t *testing.T, baselinePath string, trace string, opts baseline.Options) {
|
||||
baselinePath = tsExtension.ReplaceAllString(baselinePath, ".trace.json")
|
||||
var errorBaseline string
|
||||
if trace != "" {
|
||||
errorBaseline = trace
|
||||
} else {
|
||||
errorBaseline = baseline.NoContent
|
||||
}
|
||||
baseline.Run(t, baselinePath, errorBaseline, opts)
|
||||
}
|
||||
124
tools/tsgo/internal/testutil/tsbaseline/sourcemap_baseline.go
Normal file
124
tools/tsgo/internal/testutil/tsbaseline/sourcemap_baseline.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package tsbaseline
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/sourcemap"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/baseline"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/harnessutil"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
func DoSourcemapBaseline(
|
||||
t *testing.T,
|
||||
baselinePath string,
|
||||
header string,
|
||||
options *core.CompilerOptions,
|
||||
result *harnessutil.CompilationResult,
|
||||
harnessSettings *harnessutil.HarnessOptions,
|
||||
opts baseline.Options,
|
||||
) {
|
||||
declMaps := options.GetAreDeclarationMapsEnabled()
|
||||
if options.InlineSourceMap.IsTrue() {
|
||||
if result.Maps.Size() > 0 && !declMaps {
|
||||
t.Fatal("No sourcemap files should be generated if inlineSourceMaps was set.")
|
||||
}
|
||||
return
|
||||
} else if options.SourceMap.IsTrue() || declMaps {
|
||||
expectedMapCount := 0
|
||||
if options.SourceMap.IsTrue() {
|
||||
expectedMapCount += result.GetNumberOfJSFiles( /*includeJSON*/ false)
|
||||
}
|
||||
if declMaps {
|
||||
expectedMapCount += result.GetNumberOfJSFiles( /*includeJSON*/ true)
|
||||
}
|
||||
if result.Maps.Size() != expectedMapCount {
|
||||
t.Fatal("Number of sourcemap files should be same as js files.")
|
||||
}
|
||||
|
||||
var sourceMapCode string
|
||||
if options.NoEmitOnError.IsTrue() && len(result.Diagnostics) != 0 || result.Maps.Size() == 0 {
|
||||
sourceMapCode = baseline.NoContent
|
||||
} else {
|
||||
var sourceMapCodeBuilder strings.Builder
|
||||
for sourceMap := range result.Maps.Values() {
|
||||
if sourceMapCodeBuilder.Len() > 0 {
|
||||
sourceMapCodeBuilder.WriteString("\r\n")
|
||||
}
|
||||
sourceMapCodeBuilder.WriteString(fileOutput(sourceMap, harnessSettings))
|
||||
if !options.InlineSourceMap.IsTrue() {
|
||||
sourceMapCodeBuilder.WriteString(createSourceMapPreviewLink(sourceMap, result))
|
||||
}
|
||||
}
|
||||
sourceMapCode = sourceMapCodeBuilder.String()
|
||||
}
|
||||
|
||||
if tspath.FileExtensionIsOneOf(baselinePath, []string{tspath.ExtensionTs, tspath.ExtensionTsx}) {
|
||||
baselinePath = tspath.ChangeExtension(baselinePath, tspath.ExtensionJs+".map")
|
||||
}
|
||||
|
||||
baseline.Run(t, baselinePath, sourceMapCode, opts)
|
||||
}
|
||||
}
|
||||
|
||||
func createSourceMapPreviewLink(sourceMap *harnessutil.TestFile, result *harnessutil.CompilationResult) string {
|
||||
var sourcemapJSON sourcemap.RawSourceMap
|
||||
if err := json.Unmarshal([]byte(sourceMap.Content), &sourcemapJSON); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
outputJSFile := core.Find(result.Outputs(), func(td *harnessutil.TestFile) bool {
|
||||
return strings.HasSuffix(td.UnitName, sourcemapJSON.File)
|
||||
})
|
||||
|
||||
// !!! Strada uses a fallible approach to associating inputs and outputs derived from a source map output. The
|
||||
// !!! commented logic below should be used after the Strada migration is complete:
|
||||
|
||||
////inputsAndOutputs := result.GetInputsAndOutputsForFile(sourceMap.UnitName)
|
||||
////outputJSFile := inputsAndOutputs.Js
|
||||
|
||||
if outputJSFile == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var sourceTDs []*harnessutil.TestFile
|
||||
////if len(sourcemapJSON.Sources) == len(inputsAndOutputs.Inputs) {
|
||||
//// sourceTDs = inputsAndOutputs.Inputs
|
||||
////} else {
|
||||
sourceTDs = core.Map(sourcemapJSON.Sources, func(s string) *harnessutil.TestFile {
|
||||
return core.Find(result.Inputs(), func(td *harnessutil.TestFile) bool {
|
||||
return strings.HasSuffix(td.UnitName, s)
|
||||
})
|
||||
})
|
||||
if slices.Contains(sourceTDs, nil) {
|
||||
return ""
|
||||
}
|
||||
////}
|
||||
|
||||
var hash strings.Builder
|
||||
hash.WriteString("\n//// https://sokra.github.io/source-map-visualization#base64,")
|
||||
hash.WriteString(base64EncodeChunk(outputJSFile.Content))
|
||||
hash.WriteString(",")
|
||||
hash.WriteString(base64EncodeChunk(sourceMap.Content))
|
||||
for _, td := range sourceTDs {
|
||||
hash.WriteString(",")
|
||||
hash.WriteString(base64EncodeChunk(td.Content))
|
||||
}
|
||||
hash.WriteRune('\n')
|
||||
return hash.String()
|
||||
}
|
||||
|
||||
func base64EncodeChunk(s string) string {
|
||||
s = url.QueryEscape(s)
|
||||
s, err := url.QueryUnescape(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString([]byte(s))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package tsbaseline
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/baseline"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/harnessutil"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
func DoSourcemapRecordBaseline(
|
||||
t *testing.T,
|
||||
baselinePath string,
|
||||
header string,
|
||||
options *core.CompilerOptions,
|
||||
result *harnessutil.CompilationResult,
|
||||
harnessSettings *harnessutil.HarnessOptions,
|
||||
opts baseline.Options,
|
||||
) {
|
||||
actual := baseline.NoContent
|
||||
if options.SourceMap.IsTrue() || options.InlineSourceMap.IsTrue() || options.DeclarationMap.IsTrue() {
|
||||
record := removeTestPathPrefixes(result.GetSourceMapRecord(), false /*retainTrailingDirectorySeparator*/)
|
||||
if !(options.NoEmitOnError.IsTrue() && len(result.Diagnostics) > 0) && len(record) > 0 {
|
||||
actual = record
|
||||
}
|
||||
}
|
||||
|
||||
if tspath.FileExtensionIsOneOf(baselinePath, []string{tspath.ExtensionTs, tspath.ExtensionTsx}) {
|
||||
baselinePath = tspath.ChangeExtension(baselinePath, ".sourcemap.txt")
|
||||
}
|
||||
|
||||
baseline.Run(t, baselinePath, actual, opts)
|
||||
}
|
||||
490
tools/tsgo/internal/testutil/tsbaseline/type_symbol_baseline.go
Normal file
490
tools/tsgo/internal/testutil/tsbaseline/type_symbol_baseline.go
Normal file
@@ -0,0 +1,490 @@
|
||||
package tsbaseline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/nodebuilder"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/testutil"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/baseline"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/harnessutil"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
var (
|
||||
codeLinesRegexp = regexp.MustCompile("[\r\u2028\u2029]|\r?\n")
|
||||
bracketLineRegex = regexp.MustCompile(`^\s*[{|}]\s*$`)
|
||||
lineEndRegex = regexp.MustCompile(`\r?\n`)
|
||||
)
|
||||
|
||||
func DoTypeAndSymbolBaseline(
|
||||
t *testing.T,
|
||||
baselinePath string,
|
||||
header string,
|
||||
program compiler.ProgramLike,
|
||||
allFiles []*harnessutil.TestFile,
|
||||
opts baseline.Options,
|
||||
skipTypeBaselines bool,
|
||||
skipSymbolBaselines bool,
|
||||
hasErrorBaseline bool,
|
||||
) {
|
||||
// The full walker simulates the types that you would get from doing a full
|
||||
// compile. The pull walker simulates the types you get when you just do
|
||||
// a type query for a random node (like how the LS would do it). Most of the
|
||||
// time, these will be the same. However, occasionally, they can be different.
|
||||
// Specifically, when the compiler internally depends on symbol IDs to order
|
||||
// things, then we may see different results because symbols can be created in a
|
||||
// different order with 'pull' operations, and thus can produce slightly differing
|
||||
// output.
|
||||
//
|
||||
// For example, with a full type check, we may see a type displayed as: number | string
|
||||
// But with a pull type check, we may see it as: string | number
|
||||
//
|
||||
// These types are equivalent, but depend on what order the compiler observed
|
||||
// certain parts of the program.
|
||||
|
||||
fullWalker := newTypeWriterWalker(program, hasErrorBaseline)
|
||||
|
||||
t.Run("type", func(t *testing.T) {
|
||||
defer testutil.RecoverAndFail(t, "Panic on creating type baseline for test "+header)
|
||||
|
||||
// !!! Remove once the type baselines print node reuse lines
|
||||
typesOpts := opts
|
||||
typesOpts.DiffFixupOld = func(s string) string {
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(s))
|
||||
|
||||
perfStats := false
|
||||
for line := range strings.SplitSeq(s, "\n") {
|
||||
if isTypeBaselineNodeReuseLine(line) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !perfStats && strings.HasPrefix(line, "=== Performance Stats ===") {
|
||||
perfStats = true
|
||||
continue
|
||||
} else if perfStats {
|
||||
if strings.HasPrefix(line, "=== ") {
|
||||
perfStats = false
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
relativePrefixNew = "=== "
|
||||
relativePrefixOld = relativePrefixNew + "./"
|
||||
)
|
||||
if rest, ok := strings.CutPrefix(line, relativePrefixOld); ok {
|
||||
line = relativePrefixNew + rest
|
||||
}
|
||||
|
||||
sb.WriteString(line)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
return sb.String()[:sb.Len()-1]
|
||||
}
|
||||
|
||||
checkBaselines(t, baselinePath, allFiles, fullWalker, header, typesOpts, false /*isSymbolBaseline*/)
|
||||
})
|
||||
t.Run("symbol", func(t *testing.T) {
|
||||
defer testutil.RecoverAndFail(t, "Panic on creating symbol baseline for test "+header)
|
||||
checkBaselines(t, baselinePath, allFiles, fullWalker, header, opts, true /*isSymbolBaseline*/)
|
||||
})
|
||||
}
|
||||
|
||||
func isTypeBaselineNodeReuseLine(line string) bool {
|
||||
line, ok := strings.CutPrefix(line, ">")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
line = strings.TrimLeft(line[1:], " ")
|
||||
line, ok = strings.CutPrefix(line, ":")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c := range line {
|
||||
switch c {
|
||||
case ' ', '^', '\r':
|
||||
// Okay
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func checkBaselines(
|
||||
t *testing.T,
|
||||
baselinePath string,
|
||||
allFiles []*harnessutil.TestFile,
|
||||
fullWalker *typeWriterWalker,
|
||||
header string,
|
||||
opts baseline.Options,
|
||||
isSymbolBaseline bool,
|
||||
) {
|
||||
fullExtension := core.IfElse(isSymbolBaseline, ".symbols", ".types")
|
||||
outputFileName := tsExtension.ReplaceAllString(baselinePath, fullExtension)
|
||||
fullBaseline := generateBaseline(allFiles, fullWalker, header, isSymbolBaseline)
|
||||
baseline.Run(t, outputFileName, fullBaseline, opts)
|
||||
}
|
||||
|
||||
func generateBaseline(
|
||||
allFiles []*harnessutil.TestFile,
|
||||
fullWalker *typeWriterWalker,
|
||||
header string,
|
||||
isSymbolBaseline bool,
|
||||
) string {
|
||||
var result strings.Builder
|
||||
// !!! Perf baseline
|
||||
var perfLines []string
|
||||
// prePerformanceValues := getPerformanceBaselineValues()
|
||||
baselines := iterateBaseline(allFiles, fullWalker, isSymbolBaseline)
|
||||
for _, value := range baselines {
|
||||
result.WriteString(value)
|
||||
}
|
||||
// postPerformanceValues := getPerformanceBaselineValues()
|
||||
|
||||
if !isSymbolBaseline {
|
||||
// !!! Perf baselines
|
||||
// const perfStats: [name: string, reportThreshold: number, beforeValue: number, afterValue: number][] = [];
|
||||
// perfStats.push(["Strict subtype cache", 1000, prePerformanceValues.strictSubtype, postPerformanceValues.strictSubtype]);
|
||||
// perfStats.push(["Subtype cache", 1000, prePerformanceValues.subtype, postPerformanceValues.subtype]);
|
||||
// perfStats.push(["Identity cache", 1000, prePerformanceValues.identity, postPerformanceValues.identity]);
|
||||
// perfStats.push(["Assignability cache", 1000, prePerformanceValues.assignability, postPerformanceValues.assignability]);
|
||||
// perfStats.push(["Type Count", 1000, prePerformanceValues.typeCount, postPerformanceValues.typeCount]);
|
||||
// perfStats.push(["Instantiation count", 1500, prePerformanceValues.instantiation, postPerformanceValues.instantiation]);
|
||||
// perfStats.push(["Symbol count", 45000, prePerformanceValues.symbol, postPerformanceValues.symbol]);
|
||||
|
||||
// if (perfStats.some(([, threshold, , postValue]) => postValue >= threshold)) {
|
||||
// perfLines.push(`=== Performance Stats ===`);
|
||||
// for (const [name, threshold, preValue, postValue] of perfStats) {
|
||||
// if (postValue >= threshold) {
|
||||
// const preString = valueToString(preValue);
|
||||
// const postString = valueToString(postValue);
|
||||
// if (preString === postString) {
|
||||
// perfLines.push(`${name}: ${preString}`);
|
||||
// }
|
||||
// else {
|
||||
// perfLines.push(`${name}: ${preString} -> ${postString}`);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// perfLines.push("");
|
||||
// perfLines.push("");
|
||||
// }
|
||||
}
|
||||
|
||||
if result.Len() > 0 {
|
||||
return fmt.Sprintf("//// [%s] ////\r\n\r\n%s%s", header, strings.Join(perfLines, "\n"), result.String())
|
||||
}
|
||||
return baseline.NoContent
|
||||
}
|
||||
|
||||
func iterateBaseline(allFiles []*harnessutil.TestFile, fullWalker *typeWriterWalker, isSymbolBaseline bool) []string {
|
||||
var baselines []string
|
||||
|
||||
for _, file := range allFiles {
|
||||
unitName := file.UnitName
|
||||
var typeLines strings.Builder
|
||||
typeLines.WriteString("=== ")
|
||||
typeLines.WriteString(unitName)
|
||||
typeLines.WriteString(" ===\r\n")
|
||||
codeLines := codeLinesRegexp.Split(file.Content, -1)
|
||||
var results []*typeWriterResult
|
||||
if isSymbolBaseline {
|
||||
results = fullWalker.getSymbols(unitName)
|
||||
} else {
|
||||
results = fullWalker.getTypes(unitName)
|
||||
}
|
||||
lastIndexWritten := -1
|
||||
for _, result := range results {
|
||||
if isSymbolBaseline && result.symbol == "" {
|
||||
return baselines
|
||||
}
|
||||
if lastIndexWritten == -1 {
|
||||
typeLines.WriteString(strings.Join(codeLines[:result.line+1], "\r\n"))
|
||||
typeLines.WriteString("\r\n")
|
||||
} else if lastIndexWritten != result.line {
|
||||
if !(lastIndexWritten+1 < len(codeLines) &&
|
||||
(bracketLineRegex.MatchString(codeLines[lastIndexWritten+1]) || strings.TrimSpace(codeLines[lastIndexWritten+1]) == "")) {
|
||||
typeLines.WriteString("\r\n")
|
||||
}
|
||||
typeLines.WriteString(strings.Join(codeLines[lastIndexWritten+1:result.line+1], "\r\n"))
|
||||
typeLines.WriteString("\r\n")
|
||||
}
|
||||
lastIndexWritten = result.line
|
||||
typeOrSymbolString := core.IfElse(isSymbolBaseline, result.symbol, result.typ)
|
||||
lineText := lineDelimiter.ReplaceAllString(result.sourceText, "")
|
||||
typeLines.WriteString(">")
|
||||
fmt.Fprintf(&typeLines, "%s : %s", lineText, typeOrSymbolString)
|
||||
typeLines.WriteString("\r\n")
|
||||
if result.underline != "" {
|
||||
typeLines.WriteString(">")
|
||||
for range len(lineText) {
|
||||
typeLines.WriteString(" ")
|
||||
}
|
||||
typeLines.WriteString(" : ")
|
||||
typeLines.WriteString(result.underline)
|
||||
typeLines.WriteString("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
if lastIndexWritten+1 < len(codeLines) {
|
||||
if !(lastIndexWritten+1 < len(codeLines) &&
|
||||
(bracketLineRegex.MatchString(codeLines[lastIndexWritten+1]) || strings.TrimSpace(codeLines[lastIndexWritten+1]) == "")) {
|
||||
typeLines.WriteString("\r\n")
|
||||
}
|
||||
typeLines.WriteString(strings.Join(codeLines[lastIndexWritten+1:], "\r\n"))
|
||||
}
|
||||
typeLines.WriteString("\r\n")
|
||||
|
||||
baselines = append(
|
||||
baselines,
|
||||
removeTestPathPrefixes(typeLines.String(), false /*retainTrailingDirectorySeparator*/),
|
||||
)
|
||||
}
|
||||
|
||||
return baselines
|
||||
}
|
||||
|
||||
type typeWriterWalker struct {
|
||||
program compiler.ProgramLike
|
||||
hadErrorBaseline bool
|
||||
currentSourceFile *ast.SourceFile
|
||||
declarationTextCache map[*ast.Node]string
|
||||
}
|
||||
|
||||
func newTypeWriterWalker(program compiler.ProgramLike, hadErrorBaseline bool) *typeWriterWalker {
|
||||
return &typeWriterWalker{
|
||||
program: program,
|
||||
hadErrorBaseline: hadErrorBaseline,
|
||||
declarationTextCache: make(map[*ast.Node]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (walker *typeWriterWalker) getTypeCheckerForCurrentFile() (*checker.Checker, func()) {
|
||||
// If we don't use the right checker for the file, its contents won't be up to date
|
||||
// since the types/symbols baselines appear to depend on files having been checked.
|
||||
return walker.program.Program().GetTypeCheckerForFile(context.Background(), walker.currentSourceFile)
|
||||
}
|
||||
|
||||
type typeWriterResult struct {
|
||||
line int
|
||||
sourceText string
|
||||
symbol string
|
||||
typ string
|
||||
underline string // !!!
|
||||
}
|
||||
|
||||
func (walker *typeWriterWalker) getTypes(filename string) []*typeWriterResult {
|
||||
sourceFile := walker.program.GetSourceFile(filename)
|
||||
walker.currentSourceFile = sourceFile
|
||||
return walker.visitNode(sourceFile.AsNode(), false /*isSymbolWalk*/)
|
||||
}
|
||||
|
||||
func (walker *typeWriterWalker) getSymbols(filename string) []*typeWriterResult {
|
||||
sourceFile := walker.program.GetSourceFile(filename)
|
||||
walker.currentSourceFile = sourceFile
|
||||
return walker.visitNode(sourceFile.AsNode(), true /*isSymbolWalk*/)
|
||||
}
|
||||
|
||||
func (walker *typeWriterWalker) visitNode(node *ast.Node, isSymbolWalk bool) []*typeWriterResult {
|
||||
nodes := forEachASTNode(node)
|
||||
var results []*typeWriterResult
|
||||
for _, n := range nodes {
|
||||
if ast.IsExpressionNode(n) || n.Kind == ast.KindIdentifier || ast.IsDeclarationName(n) {
|
||||
result := walker.writeTypeOrSymbol(n, isSymbolWalk)
|
||||
if result != nil {
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func forEachASTNode(node *ast.Node) []*ast.Node {
|
||||
var result []*ast.Node
|
||||
work := []*ast.Node{node}
|
||||
|
||||
var resChildren []*ast.Node
|
||||
addChild := func(child *ast.Node) bool {
|
||||
resChildren = append(resChildren, child)
|
||||
return false
|
||||
}
|
||||
|
||||
for len(work) > 0 {
|
||||
elem := work[len(work)-1]
|
||||
work = work[:len(work)-1]
|
||||
if elem.Flags&ast.NodeFlagsReparsed == 0 || elem.Kind == ast.KindAsExpression || elem.Kind == ast.KindSatisfiesExpression ||
|
||||
((elem.Parent.Kind == ast.KindSatisfiesExpression || elem.Parent.Kind == ast.KindAsExpression) && elem == elem.Parent.Expression()) {
|
||||
if elem.Flags&ast.NodeFlagsReparsed == 0 || elem.Parent.Kind == ast.KindAsExpression || elem.Parent.Kind == ast.KindSatisfiesExpression {
|
||||
result = append(result, elem)
|
||||
}
|
||||
elem.ForEachChild(addChild)
|
||||
slices.Reverse(resChildren)
|
||||
work = append(work, resChildren...)
|
||||
resChildren = resChildren[:0]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (walker *typeWriterWalker) writeTypeOrSymbol(node *ast.Node, isSymbolWalk bool) *typeWriterResult {
|
||||
actualPos := scanner.SkipTrivia(walker.currentSourceFile.Text(), node.Pos())
|
||||
line := scanner.GetECMALineOfPosition(walker.currentSourceFile, actualPos)
|
||||
sourceText := scanner.GetSourceTextOfNodeFromSourceFile(walker.currentSourceFile, node, false /*includeTrivia*/)
|
||||
fileChecker, done := walker.getTypeCheckerForCurrentFile()
|
||||
defer done()
|
||||
|
||||
ctx, putCtx := printer.GetEmitContext()
|
||||
defer putCtx()
|
||||
|
||||
if !isSymbolWalk {
|
||||
// Don't try to get the type of something that's already a type.
|
||||
// Exception for `T` in `type T = something` because that may evaluate to some interesting type.
|
||||
if ast.IsPartOfTypeNode(node) ||
|
||||
(node.Kind == ast.KindAsExpression || node.Kind == ast.KindSatisfiesExpression) && node.Type().Flags&ast.NodeFlagsReparsed != 0 ||
|
||||
ast.IsIdentifier(node) &&
|
||||
(ast.GetMeaningFromDeclaration(node.Parent)&ast.SemanticMeaningValue) == 0 &&
|
||||
!(ast.IsTypeOrJSTypeAliasDeclaration(node.Parent) && node == node.Parent.Name()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ast.IsOmittedExpression(node) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var t *checker.Type
|
||||
// Workaround to ensure we output 'C' instead of 'typeof C' for base class expressions
|
||||
if ast.IsExpressionWithTypeArgumentsInClassExtendsClause(node.Parent) {
|
||||
t = fileChecker.GetTypeAtLocation(node.Parent)
|
||||
}
|
||||
if t == nil || checker.IsTypeAny(t) {
|
||||
t = fileChecker.GetTypeAtLocation(node)
|
||||
}
|
||||
var typeString string
|
||||
// var underline string
|
||||
if !walker.hadErrorBaseline &&
|
||||
checker.IsTypeAny(t) &&
|
||||
!ast.IsBindingElement(node.Parent) &&
|
||||
!ast.IsPropertyAccessOrQualifiedName(node.Parent) &&
|
||||
!ast.IsLabelName(node) &&
|
||||
!ast.IsGlobalScopeAugmentation(node.Parent) &&
|
||||
!ast.IsMetaProperty(node.Parent) &&
|
||||
!isImportStatementName(node) &&
|
||||
!isExportStatementName(node) &&
|
||||
!isIntrinsicJsxTag(node, walker.currentSourceFile) {
|
||||
typeString = t.AsIntrinsicType().IntrinsicName()
|
||||
} else {
|
||||
ctx.Reset()
|
||||
builder := checker.NewNodeBuilder(fileChecker, ctx)
|
||||
typeFormatFlags := checker.TypeFormatFlagsNoTruncation | checker.TypeFormatFlagsAllowUniqueESSymbolType | checker.TypeFormatFlagsGenerateNamesForShadowedTypeParams
|
||||
typeNode := builder.TypeToTypeNode(t, node.Parent, nodebuilder.Flags(typeFormatFlags&checker.TypeFormatFlagsNodeBuilderFlagsMask)|nodebuilder.FlagsIgnoreErrors, nodebuilder.InternalFlagsAllowUnresolvedNames, nil)
|
||||
if ast.IsIdentifier(node) && ast.IsTypeAliasDeclaration(node.Parent) && node.Parent.Name() == node && ast.IsIdentifier(typeNode) && typeNode.Text() == node.Text() {
|
||||
// for a complex type alias `type T = ...`, showing "T : T" isn't very helpful for type tests. When the type produced is the same as
|
||||
// the name of the type alias, recreate the type string without reusing the alias name
|
||||
typeNode = builder.TypeToTypeNode(t, node.Parent, nodebuilder.Flags((typeFormatFlags|checker.TypeFormatFlagsInTypeAlias)&checker.TypeFormatFlagsNodeBuilderFlagsMask)|nodebuilder.FlagsIgnoreErrors, nodebuilder.InternalFlagsAllowUnresolvedNames, nil)
|
||||
}
|
||||
|
||||
// !!! TODO: port underline printer, memoize
|
||||
writer := printer.NewTextWriter("", 0)
|
||||
printer := printer.NewPrinter(printer.PrinterOptions{RemoveComments: true}, printer.PrintHandlers{}, ctx)
|
||||
printer.Write(typeNode, walker.currentSourceFile, writer, nil)
|
||||
typeString = writer.String()
|
||||
}
|
||||
return &typeWriterResult{
|
||||
line: line,
|
||||
sourceText: sourceText,
|
||||
typ: typeString,
|
||||
// underline: underline, // !!! TODO: underline
|
||||
}
|
||||
}
|
||||
|
||||
symbol := fileChecker.GetSymbolAtLocation(node)
|
||||
if symbol == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var symbolString strings.Builder
|
||||
symbolString.Grow(256)
|
||||
symbolString.WriteString("Symbol(")
|
||||
symbolString.WriteString(ast.EscapeAllInternalSymbolNames(fileChecker.SymbolToStringEx(symbol, node.Parent, ast.SymbolFlagsNone, checker.SymbolFormatFlagsAllowAnyNodeKind)))
|
||||
count := 0
|
||||
for _, declaration := range symbol.Declarations {
|
||||
if count >= 5 {
|
||||
fmt.Fprintf(&symbolString, " ... and %d more", len(symbol.Declarations)-count)
|
||||
break
|
||||
}
|
||||
count++
|
||||
symbolString.WriteString(", ")
|
||||
if declText, ok := walker.declarationTextCache[declaration]; ok {
|
||||
symbolString.WriteString(declText)
|
||||
continue
|
||||
}
|
||||
|
||||
declSourceFile := ast.GetSourceFileOfNode(declaration)
|
||||
declLine, declChar := scanner.GetECMALineAndUTF16CharacterOfPosition(declSourceFile, declaration.Pos())
|
||||
fileName := tspath.GetBaseFileName(declSourceFile.FileName())
|
||||
symbolString.WriteString("Decl(")
|
||||
symbolString.WriteString(fileName)
|
||||
symbolString.WriteString(", ")
|
||||
if isDefaultLibraryFile(fileName) {
|
||||
symbolString.WriteString("--, --)")
|
||||
} else {
|
||||
fmt.Fprintf(&symbolString, "%d, %d)", declLine, int(declChar))
|
||||
}
|
||||
}
|
||||
symbolString.WriteString(")")
|
||||
return &typeWriterResult{
|
||||
line: line,
|
||||
sourceText: sourceText,
|
||||
symbol: symbolString.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func isImportStatementName(node *ast.Node) bool {
|
||||
if ast.IsImportSpecifier(node.Parent) && (node == node.Parent.Name() || node == node.Parent.PropertyName()) {
|
||||
return true
|
||||
}
|
||||
if ast.IsImportClause(node.Parent) && node == node.Parent.Name() {
|
||||
return true
|
||||
}
|
||||
if ast.IsImportEqualsDeclaration(node.Parent) && node == node.Parent.Name() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isExportStatementName(node *ast.Node) bool {
|
||||
if ast.IsExportAssignment(node.Parent) && node == node.Parent.Expression() {
|
||||
return true
|
||||
}
|
||||
if ast.IsExportSpecifier(node.Parent) && (node == node.Parent.Name() || node == node.Parent.PropertyName()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isIntrinsicJsxTag(node *ast.Node, sourceFile *ast.SourceFile) bool {
|
||||
if !(ast.IsJsxOpeningElement(node.Parent) || ast.IsJsxClosingElement(node.Parent) || ast.IsJsxSelfClosingElement(node.Parent)) {
|
||||
return false
|
||||
}
|
||||
if node.Parent.TagName() != node {
|
||||
return false
|
||||
}
|
||||
text := scanner.GetSourceTextOfNodeFromSourceFile(sourceFile, node, false /*includeTrivia*/)
|
||||
return scanner.IsIntrinsicJsxName(text)
|
||||
}
|
||||
71
tools/tsgo/internal/testutil/tsbaseline/util.go
Normal file
71
tools/tsgo/internal/testutil/tsbaseline/util.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package tsbaseline
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
var (
|
||||
lineDelimiter = regexp.MustCompile("\r?\n")
|
||||
nonWhitespace = regexp.MustCompile(`\S`)
|
||||
tsExtension = regexp.MustCompile(`\.tsx?$`)
|
||||
testPathCharacters = regexp.MustCompile(`[\^<>:"|?*%]`)
|
||||
testPathDotDot = regexp.MustCompile(`\.\.\/`)
|
||||
)
|
||||
|
||||
var (
|
||||
libFolder = "built/local/"
|
||||
builtFolder = "/.ts"
|
||||
)
|
||||
|
||||
var (
|
||||
testPathPrefixReplacer = strings.NewReplacer(
|
||||
"/.ts/", "",
|
||||
"/.lib/", "",
|
||||
"/.src/", "",
|
||||
"bundled:///libs/", "",
|
||||
"file:///./ts/", "file:///",
|
||||
"file:///./lib/", "file:///",
|
||||
"file:///./src/", "file:///",
|
||||
)
|
||||
testPathTrailingReplacerTrailingSeparator = strings.NewReplacer(
|
||||
"/.ts/", "/",
|
||||
"/.lib/", "/",
|
||||
"/.src/", "/",
|
||||
"bundled:///libs/", "/",
|
||||
"file:///./ts/", "file:///",
|
||||
"file:///./lib/", "file:///",
|
||||
"file:///./src/", "file:///",
|
||||
)
|
||||
)
|
||||
|
||||
func removeTestPathPrefixes(text string, retainTrailingDirectorySeparator bool) string {
|
||||
if retainTrailingDirectorySeparator {
|
||||
return testPathTrailingReplacerTrailingSeparator.Replace(text)
|
||||
}
|
||||
return testPathPrefixReplacer.Replace(text)
|
||||
}
|
||||
|
||||
func isDefaultLibraryFile(filePath string) bool {
|
||||
fileName := tspath.GetBaseFileName(filePath)
|
||||
return strings.HasPrefix(fileName, "lib.") && strings.HasSuffix(fileName, tspath.ExtensionDts)
|
||||
}
|
||||
|
||||
func isBuiltFile(filePath string) bool {
|
||||
return strings.HasPrefix(filePath, libFolder) || strings.HasPrefix(filePath, tspath.EnsureTrailingDirectorySeparator(builtFolder))
|
||||
}
|
||||
|
||||
func isTsConfigFile(path string) bool {
|
||||
// !!! fix to check for just prefixes/suffixes
|
||||
return strings.Contains(path, "tsconfig") && strings.Contains(path, "json")
|
||||
}
|
||||
|
||||
func sanitizeTestFilePath(name string) string {
|
||||
path := testPathCharacters.ReplaceAllString(name, "_")
|
||||
path = tspath.NormalizeSlashes(path)
|
||||
path = testPathDotDot.ReplaceAllString(path, "__dotdot/")
|
||||
path = string(tspath.ToPath(path, "", false /*useCaseSensitiveFileNames*/))
|
||||
return strings.TrimPrefix(path, "/")
|
||||
}
|
||||
Reference in New Issue
Block a user