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()
|
||||
}
|
||||
Reference in New Issue
Block a user