293 lines
10 KiB
Go
293 lines
10 KiB
Go
package testrunner
|
|
|
|
import (
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/microsoft/typescript-go/internal/ast"
|
|
"github.com/microsoft/typescript-go/internal/core"
|
|
"github.com/microsoft/typescript-go/internal/parser"
|
|
"github.com/microsoft/typescript-go/internal/scanner"
|
|
"github.com/microsoft/typescript-go/internal/testutil/harnessutil"
|
|
"github.com/microsoft/typescript-go/internal/tsoptions"
|
|
"github.com/microsoft/typescript-go/internal/tsoptions/tsoptionstest"
|
|
"github.com/microsoft/typescript-go/internal/tspath"
|
|
)
|
|
|
|
var lineDelimiter = regexp.MustCompile("\r?\n")
|
|
|
|
// This maps a compiler setting to its value as written in the test file. For example, if a test file contains:
|
|
//
|
|
// // @target: esnext, es2015
|
|
//
|
|
// Then the map will map "target" to "esnext, es2015"
|
|
type rawCompilerSettings map[string]string
|
|
|
|
// All the necessary information to turn a multi file test into useful units for later compilation
|
|
type testUnit struct {
|
|
content string
|
|
name string
|
|
}
|
|
|
|
type testCaseContent struct {
|
|
testUnitData []*testUnit
|
|
tsConfig *tsoptions.ParsedCommandLine
|
|
tsConfigFileUnitData *testUnit
|
|
symlinks map[string]string
|
|
}
|
|
|
|
// Regex for parsing options in the format "@Alpha: Value of any sort"
|
|
var optionRegex = regexp.MustCompile(`(?m)^\/{2}\s*@(\w+)\s*:\s*([^\r\n]*)`)
|
|
|
|
// Regex for parsing @link option
|
|
var linkRegex = regexp.MustCompile(`(?m)^\/{2}\s*@link\s*:\s*([^\r\n]*)\s*->\s*([^\r\n]*)`)
|
|
|
|
// File-specific directives used by fourslash tests
|
|
var fourslashDirectives = []string{"emitthisfile"}
|
|
|
|
// Given a test file containing // @FileName directives,
|
|
// return an array of named units of code to be added to an existing compiler instance.
|
|
func makeUnitsFromTest(code string, fileName string) testCaseContent {
|
|
testUnits, symlinks, currentDirectory, _, _ := ParseTestFilesAndSymlinks(
|
|
code,
|
|
fileName,
|
|
func(filename string, content string, fileOptions map[string]string) (*testUnit, error) {
|
|
return &testUnit{content: content, name: filename}, nil
|
|
},
|
|
)
|
|
|
|
if currentDirectory == "" {
|
|
currentDirectory = srcFolder
|
|
}
|
|
|
|
// unit tests always list files explicitly
|
|
allFiles := make(map[string]string)
|
|
for _, data := range testUnits {
|
|
allFiles[tspath.GetNormalizedAbsolutePath(data.name, currentDirectory)] = data.content
|
|
}
|
|
parseConfigHost := tsoptionstest.NewVFSParseConfigHost(allFiles, currentDirectory, true /*useCaseSensitiveFileNames*/)
|
|
|
|
// check if project has tsconfig.json in the list of files
|
|
var tsConfig *tsoptions.ParsedCommandLine
|
|
var tsConfigFileUnitData *testUnit
|
|
for i, data := range testUnits {
|
|
if harnessutil.GetConfigNameFromFileName(data.name) != "" {
|
|
configFileName := tspath.GetNormalizedAbsolutePath(data.name, currentDirectory)
|
|
path := tspath.ToPath(data.name, parseConfigHost.GetCurrentDirectory(), parseConfigHost.Vfs.UseCaseSensitiveFileNames())
|
|
configJson := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
|
FileName: configFileName,
|
|
Path: path,
|
|
}, data.content, core.ScriptKindJSON)
|
|
tsConfigSourceFile := &tsoptions.TsConfigSourceFile{
|
|
SourceFile: configJson,
|
|
}
|
|
configDir := tspath.GetDirectoryPath(configFileName)
|
|
tsConfig = tsoptions.ParseJsonSourceFileConfigFileContent(
|
|
tsConfigSourceFile,
|
|
parseConfigHost,
|
|
configDir,
|
|
nil, /*existingOptions*/
|
|
nil, /*existingOptionsRaw*/
|
|
configFileName,
|
|
nil, /*resolutionStack*/
|
|
nil, /*extraFileExtensions*/
|
|
nil, /*extendedConfigCache*/
|
|
)
|
|
tsConfigFileUnitData = data
|
|
|
|
// delete tsconfig file entry from the list
|
|
testUnits = slices.Delete(testUnits, i, i+1)
|
|
break
|
|
}
|
|
}
|
|
|
|
return testCaseContent{
|
|
testUnitData: testUnits,
|
|
tsConfig: tsConfig,
|
|
tsConfigFileUnitData: tsConfigFileUnitData,
|
|
symlinks: symlinks,
|
|
}
|
|
}
|
|
|
|
type ParseTestFilesOptions struct {
|
|
// If true, allows test content to appear before the first @Filename directive.
|
|
// In this case, an implicit first file is created using the fileName parameter.
|
|
// This matches the behavior of the TypeScript fourslash test harness.
|
|
AllowImplicitFirstFile bool
|
|
}
|
|
|
|
// Given a test file containing // @FileName and // @symlink directives,
|
|
// return an array of named units of code to be added to an existing compiler instance,
|
|
// along with a map of symlinks and the current directory.
|
|
func ParseTestFilesAndSymlinks[T any](
|
|
code string,
|
|
fileName string,
|
|
parseFile func(filename string, content string, fileOptions map[string]string) (T, error),
|
|
) (units []T, symlinks map[string]string, currentDir string, globalOptions map[string]string, e error) {
|
|
return ParseTestFilesAndSymlinksWithOptions(code, fileName, parseFile, ParseTestFilesOptions{})
|
|
}
|
|
|
|
func ParseTestFilesAndSymlinksWithOptions[T any](
|
|
code string,
|
|
fileName string,
|
|
parseFile func(filename string, content string, fileOptions map[string]string) (T, error),
|
|
options ParseTestFilesOptions,
|
|
) (units []T, symlinks map[string]string, currentDir string, globalOptions map[string]string, e error) {
|
|
// List of all the subfiles we've parsed out
|
|
var testUnits []T
|
|
|
|
lines := lineDelimiter.Split(code, -1)
|
|
|
|
// Stuff related to the subfile we're parsing
|
|
var currentFileContent strings.Builder
|
|
var currentFileName string
|
|
seenContentLine := false
|
|
hasSeenFile := false
|
|
if options.AllowImplicitFirstFile {
|
|
// For fourslash tests, initialize currentFileName to the fileName parameter
|
|
// so content before the first @Filename directive goes into an implicit first file
|
|
currentFileName = fileName
|
|
}
|
|
var currentDirectory string
|
|
var parseError error
|
|
currentFileOptions := make(map[string]string)
|
|
symlinks = make(map[string]string)
|
|
globalOptions = make(map[string]string)
|
|
|
|
for _, line := range lines {
|
|
ok := parseSymlinkFromTest(line, symlinks)
|
|
if ok {
|
|
continue
|
|
}
|
|
if testMetaData := optionRegex.FindStringSubmatch(line); testMetaData != nil {
|
|
// Comment line, check for global/file @options and record them
|
|
metaDataName := strings.ToLower(testMetaData[1])
|
|
metaDataValue := strings.TrimSpace(testMetaData[2])
|
|
if metaDataName == "currentdirectory" {
|
|
currentDirectory = metaDataValue
|
|
}
|
|
if metaDataName != "filename" {
|
|
if metaDataName == "symlink" && currentFileName != "" {
|
|
for link := range strings.SplitSeq(metaDataValue, ",") {
|
|
link = strings.TrimSpace(link)
|
|
if link != "" {
|
|
symlinks[link] = currentFileName
|
|
}
|
|
}
|
|
} else if slices.Contains(fourslashDirectives, metaDataName) {
|
|
// File-specific option
|
|
currentFileOptions[metaDataName] = metaDataValue
|
|
} else {
|
|
// Global option
|
|
if existingValue, ok := globalOptions[metaDataName]; ok && existingValue != metaDataValue {
|
|
// !!! This would break existing submodule tests
|
|
// panic("Duplicate global option: " + metaDataName)
|
|
}
|
|
globalOptions[metaDataName] = metaDataValue
|
|
}
|
|
continue
|
|
}
|
|
|
|
// New metadata statement after having collected some code to go with the previous metadata
|
|
if currentFileName != "" {
|
|
// Store result file - always save for regular tests, but skip empty implicit first file for fourslash
|
|
shouldSaveFile := !options.AllowImplicitFirstFile || currentFileContent.Len() != 0 || hasSeenFile
|
|
if shouldSaveFile {
|
|
hasSeenFile = true
|
|
newTestFile, e := parseFile(currentFileName, currentFileContent.String(), currentFileOptions)
|
|
if e != nil {
|
|
parseError = e
|
|
break
|
|
}
|
|
testUnits = append(testUnits, newTestFile)
|
|
}
|
|
|
|
// Reset local data
|
|
currentFileContent.Reset()
|
|
seenContentLine = false
|
|
currentFileName = metaDataValue
|
|
currentFileOptions = make(map[string]string)
|
|
} else {
|
|
// First metadata marker in the file
|
|
hasContentBeforeFirstFilename := currentFileContent.Len() != 0 && scanner.SkipTrivia(currentFileContent.String(), 0) != currentFileContent.Len()
|
|
if hasContentBeforeFirstFilename && !options.AllowImplicitFirstFile {
|
|
panic("Non-comment test content appears before the first '// @Filename' directive")
|
|
}
|
|
|
|
// If we have content before the first @Filename and AllowImplicitFirstFile is true,
|
|
// we need to save it as an implicit first file before starting the new file
|
|
if hasContentBeforeFirstFilename && options.AllowImplicitFirstFile && currentFileName != "" {
|
|
// Store the implicit first file
|
|
hasSeenFile = true
|
|
newTestFile, e := parseFile(currentFileName, currentFileContent.String(), currentFileOptions)
|
|
if e != nil {
|
|
parseError = e
|
|
break
|
|
}
|
|
testUnits = append(testUnits, newTestFile)
|
|
}
|
|
|
|
// Reset for the new file
|
|
currentFileContent.Reset()
|
|
seenContentLine = false
|
|
currentFileName = strings.TrimSpace(testMetaData[2])
|
|
currentFileOptions = make(map[string]string)
|
|
}
|
|
} else {
|
|
// Subfile content line
|
|
// Append to the current subfile content, inserting a newline if needed
|
|
// For fourslash tests, use seenContentLine to preserve leading blank lines
|
|
// (matching TS fourslash's //// content markers). For compiler tests, use
|
|
// Len() != 0 which drops leading blanks (matching TS's harness behavior).
|
|
if options.AllowImplicitFirstFile {
|
|
if seenContentLine {
|
|
currentFileContent.WriteRune('\n')
|
|
}
|
|
seenContentLine = true
|
|
} else {
|
|
if currentFileContent.Len() != 0 {
|
|
currentFileContent.WriteRune('\n')
|
|
}
|
|
}
|
|
currentFileContent.WriteString(line)
|
|
}
|
|
}
|
|
|
|
// normalize the fileName for the single file case
|
|
if len(testUnits) == 0 && len(currentFileName) == 0 {
|
|
currentFileName = tspath.GetBaseFileName(fileName)
|
|
}
|
|
|
|
// if there are no parse errors so far, parse the rest of the file
|
|
if parseError == nil {
|
|
// EOF, push whatever remains
|
|
newTestFile2, e := parseFile(currentFileName, currentFileContent.String(), currentFileOptions)
|
|
|
|
parseError = e
|
|
testUnits = append(testUnits, newTestFile2)
|
|
}
|
|
|
|
return testUnits, symlinks, currentDirectory, globalOptions, parseError
|
|
}
|
|
|
|
func extractCompilerSettings(content string) rawCompilerSettings {
|
|
opts := make(map[string]string)
|
|
|
|
for _, match := range optionRegex.FindAllStringSubmatch(content, -1) {
|
|
opts[strings.ToLower(match[1])] = strings.TrimSuffix(strings.TrimSpace(match[2]), ";")
|
|
}
|
|
|
|
return opts
|
|
}
|
|
|
|
func parseSymlinkFromTest(line string, symlinks map[string]string) bool {
|
|
linkMetaData := linkRegex.FindStringSubmatch(line)
|
|
if len(linkMetaData) == 0 {
|
|
return false
|
|
}
|
|
|
|
symlinks[strings.TrimSpace(linkMetaData[2])] = strings.TrimSpace(linkMetaData[1])
|
|
return true
|
|
}
|