vendor tsgo
This commit is contained in:
292
tools/tsgo/internal/testrunner/test_case_parser.go
Normal file
292
tools/tsgo/internal/testrunner/test_case_parser.go
Normal file
@@ -0,0 +1,292 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user