256 lines
9.6 KiB
Go
256 lines
9.6 KiB
Go
package tsbaseline
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"unicode/utf8"
|
|
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnosticwriter"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/testutil/baseline"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/testutil/harnessutil"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/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, errors, pretty)
|
|
} else {
|
|
errorBaseline = baseline.NoContent
|
|
}
|
|
baseline.Run(t, baselinePath, errorBaseline, opts)
|
|
}
|
|
|
|
func minimalDiagnosticsToString(diagnostics []*ast.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 *testing.T, inputFiles []*harnessutil.TestFile, diagnostics []*ast.Diagnostic, pretty bool) string {
|
|
t.Helper()
|
|
outputLines := iterateErrorBaseline(t, inputFiles, diagnostics, pretty)
|
|
|
|
if pretty {
|
|
var summaryBuilder strings.Builder
|
|
diagnosticwriter.WriteErrorSummaryText(
|
|
&summaryBuilder,
|
|
diagnostics,
|
|
formatOpts)
|
|
summary := removeTestPathPrefixes(summaryBuilder.String(), false)
|
|
outputLines = append(outputLines, summary)
|
|
}
|
|
return strings.Join(outputLines, "")
|
|
}
|
|
|
|
func iterateErrorBaseline(t *testing.T, inputFiles []*harnessutil.TestFile, inputDiagnostics []*ast.Diagnostic, pretty bool) []string {
|
|
t.Helper()
|
|
diagnostics := slices.Clone(inputDiagnostics)
|
|
slices.SortFunc(diagnostics, ast.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 *ast.Diagnostic) {
|
|
message := diagnosticwriter.FlattenDiagnosticMessage(diag, harnessNewLine)
|
|
|
|
var errLines []string
|
|
for _, line := range strings.Split(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.Loc().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)))
|
|
}
|
|
|
|
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(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 *ast.Diagnostic) 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.Loc().Pos()
|
|
end := errDiagnostic.Loc().End()
|
|
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 *ast.Diagnostic) bool {
|
|
return d.File() != nil && (isDefaultLibraryFile(d.File().FileName()) || isBuiltFile(d.File().FileName()))
|
|
})
|
|
numTsconfigDiagnostics := core.CountWhere(
|
|
diagnostics,
|
|
func(d *ast.Diagnostic) 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 *ast.SourceFile, pos int, formatOpts *diagnosticwriter.FormattingOptions, writeWithStyleAndReset diagnosticwriter.FormattedWriter) string {
|
|
var output strings.Builder
|
|
diagnosticwriter.WriteLocation(&output, file, pos, formatOpts, writeWithStyleAndReset)
|
|
return output.String()
|
|
}
|