vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View File

@@ -0,0 +1,509 @@
package fourslash
import (
"fmt"
"slices"
"strings"
"testing"
"unicode/utf8"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/stringutil"
"github.com/microsoft/typescript-go/internal/testrunner"
"github.com/microsoft/typescript-go/internal/tspath"
)
// Inserted in source files by surrounding desired text
// in a range with `[|` and `|]`. For example,
//
// [|text in range|]
//
// is a range with `text in range` "selected".
type RangeMarker struct {
fileName string
Range core.TextRange
LSRange lsproto.Range
Marker *Marker
}
func (r *RangeMarker) LSPos() lsproto.Position {
return r.LSRange.Start
}
func (r *RangeMarker) FileName() string {
return r.fileName
}
func (r *RangeMarker) GetName() *string {
if r.Marker == nil {
return nil
}
return r.Marker.Name
}
func (r *RangeMarker) LSLocation() lsproto.Location {
return lsproto.Location{
Uri: lsconv.FileNameToDocumentURI(r.fileName),
Range: r.LSRange,
}
}
type Marker struct {
fileName string
Position int
LSPosition lsproto.Position
Name *string // `nil` for anonymous markers such as `{| "foo": "bar" |}`
Data map[string]any
}
func (m *Marker) LSPos() lsproto.Position {
return m.LSPosition
}
func (m *Marker) FileName() string {
return m.fileName
}
func (m *Marker) GetName() *string {
return m.Name
}
func (m *Marker) MakerWithSymlink(fileName string) *Marker {
return &Marker{
fileName: fileName,
Position: m.Position,
LSPosition: m.LSPosition,
Name: m.Name,
Data: m.Data,
}
}
type MarkerOrRange interface {
FileName() string
LSPos() lsproto.Position
GetName() *string
}
type TestData struct {
Files []*TestFileInfo
MarkerPositions map[string]*Marker
Markers []*Marker
Symlinks map[string]string
GlobalOptions map[string]string
Ranges []*RangeMarker
}
func (t *TestData) isStateBaseliningEnabled() bool {
return isStateBaseliningEnabled(t.GlobalOptions)
}
type testFileWithMarkers struct {
file *TestFileInfo
markers []*Marker
ranges []*RangeMarker
}
func isStateBaseliningEnabled(globalOptions map[string]string) bool {
return globalOptions["statebaseline"] == "true"
}
func ParseTestData(t *testing.T, contents string, fileName string) TestData {
// List of all the subfiles we've parsed out
var files []*TestFileInfo
markerPositions := make(map[string]*Marker)
var markers []*Marker
var ranges []*RangeMarker
filesWithMarker, symlinks, _, globalOptions, e := testrunner.ParseTestFilesAndSymlinksWithOptions(
contents,
fileName,
parseFileContent,
testrunner.ParseTestFilesOptions{
AllowImplicitFirstFile: true,
},
)
if e != nil {
t.Fatalf("Error parsing fourslash data: %s", e.Error())
}
hasTSConfig := false
for _, file := range filesWithMarker {
files = append(files, file.file)
hasTSConfig = hasTSConfig || isConfigFile(file.file.fileName)
markers = append(markers, file.markers...)
ranges = append(ranges, file.ranges...)
for _, marker := range file.markers {
if marker.Name == nil {
if marker.Data != nil {
// The marker is an anonymous object marker, which does not need a name. Markers are only set into markerPositions if they have a name
continue
}
t.Fatalf(`Marker at position %v is unnamed`, marker.Position)
}
if existing, ok := markerPositions[*marker.Name]; ok {
t.Fatalf(`Duplicate marker name: "%s" at %v and %v`, *marker.Name, marker.Position, existing.Position)
}
markerPositions[*marker.Name] = marker
}
}
if hasTSConfig && hasUnsupportedGlobalOptionsWithConfig(globalOptions) && !isStateBaseliningEnabled(globalOptions) {
t.Fatalf("It is not allowed to use global options along with config files.")
}
return TestData{
Files: files,
MarkerPositions: markerPositions,
Markers: markers,
Symlinks: symlinks,
GlobalOptions: globalOptions,
Ranges: ranges,
}
}
func hasUnsupportedGlobalOptionsWithConfig(globalOptions map[string]string) bool {
for option := range globalOptions {
switch strings.ToLower(option) {
case "symlink", "link", "usecasesensitivefilenames":
continue
default:
return true
}
}
return false
}
func isConfigFile(fileName string) bool {
fileName = strings.ToLower(fileName)
return strings.HasSuffix(fileName, "tsconfig.json") || strings.HasSuffix(fileName, "jsconfig.json")
}
type locationInformation struct {
position int
sourcePosition int
sourceLine int
sourceColumn int
}
type rangeLocationInformation struct {
locationInformation
marker *Marker
}
type TestFileInfo struct {
fileName string
// The contents of the file (with markers, etc stripped out)
Content string
emit bool
}
// FileName implements lsconv.Script.
func (t *TestFileInfo) FileName() string {
return t.fileName
}
// Text implements lsconv.Script.
func (t *TestFileInfo) Text() string {
return t.Content
}
var _ lsconv.Script = (*TestFileInfo)(nil)
const emitThisFileOption = "emitthisfile"
type parserState int
const (
stateNone parserState = iota
stateInSlashStarMarker
stateInObjectMarker
)
func parseFileContent(fileName string, content string, fileOptions map[string]string) (*testFileWithMarkers, error) {
fileName = tspath.GetNormalizedAbsolutePath(fileName, "/")
content = chompLeadingSpace(content)
// The file content (minus metacharacters) so far
var output strings.Builder
var markers []*Marker
/// A stack of the open range markers that are still unclosed
openRanges := []rangeLocationInformation{}
/// A list of closed ranges we've collected so far
rangeMarkers := []*RangeMarker{}
// The total number of metacharacters removed from the file (so far)
difference := 0
// One-based current position data
line := 1
column := 1
// The current marker (or maybe multi-line comment?) we're parsing, possibly
var openMarker *locationInformation
// The latest position of the start of an unflushed plain text area
lastNormalCharPosition := 0
flush := func(lastSafeCharIndex int) {
if lastSafeCharIndex != -1 {
output.WriteString(content[lastNormalCharPosition:lastSafeCharIndex])
} else {
output.WriteString(content[lastNormalCharPosition:])
}
}
state := stateNone
previousCharacter, i := utf8.DecodeRuneInString(content)
var size int
var currentCharacter rune
for ; i < len(content); i = i + size {
currentCharacter, size = utf8.DecodeRuneInString(content[i:])
switch state {
case stateNone:
if previousCharacter == '[' && currentCharacter == '|' {
// found a range start
openRanges = append(openRanges, rangeLocationInformation{
locationInformation: locationInformation{
position: (i - 1) - difference,
sourcePosition: i - 1,
sourceLine: line,
sourceColumn: column,
},
})
// copy all text up to marker position
flush(i - 1)
lastNormalCharPosition = i + 1
difference += 2
} else if previousCharacter == '|' && currentCharacter == ']' {
// found a range end
if len(openRanges) == 0 {
return nil, reportError(fileName, line, column, "Found range end with no matching start.")
}
rangeStart := openRanges[len(openRanges)-1]
openRanges = openRanges[:len(openRanges)-1]
closedRange := &RangeMarker{
fileName: fileName,
Range: core.NewTextRange(rangeStart.position, (i-1)-difference),
Marker: rangeStart.marker,
}
rangeMarkers = append(rangeMarkers, closedRange)
// copy all text up to range marker position
flush(i - 1)
lastNormalCharPosition = i + 1
difference += 2
} else if previousCharacter == '/' && currentCharacter == '*' && (i+1 >= len(content) || content[i+1] != '/') {
// found a possible marker start
state = stateInSlashStarMarker
openMarker = &locationInformation{
position: (i - 1) - difference,
sourcePosition: i - 1,
sourceLine: line,
sourceColumn: column - 1,
}
} else if previousCharacter == '{' && currentCharacter == '|' {
// found an object marker start
state = stateInObjectMarker
openMarker = &locationInformation{
position: (i - 1) - difference,
sourcePosition: i - 1,
sourceLine: line,
sourceColumn: column,
}
flush(i - 1)
}
case stateInObjectMarker:
// Object markers are only ever terminated by |} and have no content restrictions
if previousCharacter == '|' && currentCharacter == '}' {
objectMarkerData := strings.TrimSpace(content[openMarker.sourcePosition+2 : i-1])
marker, e := getObjectMarker(fileName, openMarker, objectMarkerData)
if e != nil {
return nil, e
}
if len(openRanges) > 0 {
openRanges[len(openRanges)-1].marker = marker
}
markers = append(markers, marker)
// Set the current start to point to the end of the current marker to ignore its text
lastNormalCharPosition = i + 1
difference += i + 1 - openMarker.sourcePosition
// Reset the state
openMarker = nil
state = stateNone
}
case stateInSlashStarMarker:
if previousCharacter == '*' && currentCharacter == '/' {
// Record the marker
// start + 2 to ignore the */, -1 on the end to ignore the * (/ is next)
markerNameText := strings.TrimSpace(content[openMarker.sourcePosition+2 : i-1])
marker := &Marker{
fileName: fileName,
Position: openMarker.position,
Name: &markerNameText,
}
if len(openRanges) > 0 {
openRanges[len(openRanges)-1].marker = marker
}
markers = append(markers, marker)
// Set the current start to point to the end of the current marker to ignore its text
flush(openMarker.sourcePosition)
lastNormalCharPosition = i + 1
difference += i + 1 - openMarker.sourcePosition
// Reset the state
openMarker = nil
state = stateNone
} else if !(stringutil.IsDigit(currentCharacter) ||
stringutil.IsASCIILetter(currentCharacter) ||
currentCharacter == '$' ||
currentCharacter == '_') { // Invalid marker character
if currentCharacter == '*' && i < len(content)-1 && content[i+1] == '/' {
// The marker is about to be closed, ignore the 'invalid' char
} else {
// We've hit a non-valid marker character, so we were actually in a block comment
// Bail out the text we've gathered so far back into the output
flush(i)
lastNormalCharPosition = i
openMarker = nil
state = stateNone
}
}
}
if currentCharacter == '\n' && previousCharacter == '\r' {
// Ignore trailing \n after \r
continue
} else if currentCharacter == '\n' || currentCharacter == '\r' {
line++
column = 1
continue
}
column++
if i >= lastNormalCharPosition {
previousCharacter = currentCharacter
} else {
previousCharacter = utf8.RuneError // reset to avoid accidentally reusing marker delimiters as part of other markers
}
}
// Add the remaining text
flush(-1)
if len(openRanges) > 0 {
openRange := openRanges[0]
return nil, reportError(fileName, openRange.sourceLine, openRange.sourceColumn, "Unterminated range.")
}
if openMarker != nil {
return nil, reportError(fileName, openMarker.sourceLine, openMarker.sourceColumn, "Unterminated marker.")
}
outputString := output.String()
// Set LS positions for markers
lineMap := lsconv.ComputeLSPLineStarts(outputString)
converters := lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *lsconv.LSPLineMap {
return lineMap
})
emit := fileOptions[emitThisFileOption] == "true"
testFileInfo := &TestFileInfo{
fileName: fileName,
Content: outputString,
emit: emit,
}
slices.SortStableFunc(rangeMarkers, func(a, b *RangeMarker) int {
if a.Range.Pos() != b.Range.Pos() {
return a.Range.Pos() - b.Range.Pos()
}
return b.Range.End() - a.Range.End()
})
for _, marker := range markers {
marker.LSPosition = converters.PositionToLineAndCharacter(testFileInfo, core.TextPos(marker.Position))
}
for _, rangeMarker := range rangeMarkers {
rangeMarker.LSRange = lsproto.Range{
Start: converters.PositionToLineAndCharacter(testFileInfo, core.TextPos(rangeMarker.Range.Pos())),
End: converters.PositionToLineAndCharacter(testFileInfo, core.TextPos(rangeMarker.Range.End())),
}
}
return &testFileWithMarkers{
file: testFileInfo,
markers: markers,
ranges: rangeMarkers,
}, nil
}
func getObjectMarker(fileName string, location *locationInformation, text string) (*Marker, error) {
// Attempt to parse the marker value as JSON
var v any
e := json.Unmarshal([]byte("{ "+text+" }"), &v)
if e != nil {
return nil, reportError(fileName, location.sourceLine, location.sourceColumn, "Unable to parse marker text "+text)
}
markerValue, ok := v.(map[string]any)
if !ok || len(markerValue) == 0 {
return nil, reportError(fileName, location.sourceLine, location.sourceColumn, "Object markers can not be empty")
}
marker := &Marker{
fileName: fileName,
Position: location.position,
Data: markerValue,
}
// Object markers can be anonymous
if markerValue["name"] != nil {
if name, ok := markerValue["name"].(string); ok && name != "" {
marker.Name = &name
}
}
return marker, nil
}
func reportError(fileName string, line int, col int, message string) error {
return &fourslashError{fmt.Sprintf("%v (%v,%v): %v", fileName, line, col, message)}
}
func chompLeadingSpace(content string) string {
lines := strings.Split(content, "\n")
for _, line := range lines {
if len(line) > 0 && line[0] != ' ' {
return content
}
}
result := make([]string, len(lines))
for i, line := range lines {
if len(line) > 0 {
result[i] = line[1:]
}
}
return strings.Join(result, "\n")
}
type fourslashError struct {
err string
}
func (e *fourslashError) Error() string {
return e.err
}