vendor tsgo
This commit is contained in:
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