Skip to content

Port go-to-definition baseline tests #1437

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
48 changes: 46 additions & 2 deletions internal/fourslash/_scripts/convertFourslash.mts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
case "baselineFindAllReferences":
// `verify.baselineFindAllReferences(...)`
return [parseBaselineFindAllReferencesArgs(callExpression.arguments)];
case "baselineGoToDefinition":
case "baselineGetDefinitionAtPosition":
// Both of these take the same arguments, but differ in that...
// - `verify.baselineGoToDefinition(...)` called getDefinitionAndBoundSpan
// - `verify.baselineGetDefinitionAtPosition(...)` called getDefinitionAtPosition
// LSP doesn't have two separate commands though. It's unclear how we would model bound spans though.
return [parseBaselineGoToDefinitionArgs(callExpression.arguments)];
}
}
// `goTo....`
Expand Down Expand Up @@ -668,6 +675,27 @@ function parseBaselineFindAllReferencesArgs(args: readonly ts.Expression[]): Ver
};
}

function parseBaselineGoToDefinitionArgs(args: readonly ts.Expression[]): VerifyBaselineGoToDefinitionCmd {
const newArgs = [];
for (const arg of args) {
if (ts.isStringLiteral(arg)) {
newArgs.push(getGoStringLiteral(arg.text));
}
else if (arg.getText() === "...test.ranges()") {
return {
kind: "verifyBaselineGoToDefinition",
markers: [],
ranges: true,
};
}
}

return {
kind: "verifyBaselineGoToDefinition",
markers: newArgs,
};
}

function parseKind(expr: ts.Expression): string | undefined {
if (!ts.isStringLiteral(expr)) {
console.error(`Expected string literal for kind, got ${expr.getText()}`);
Expand Down Expand Up @@ -809,6 +837,12 @@ interface VerifyBaselineFindAllReferencesCmd {
ranges?: boolean;
}

interface VerifyBaselineGoToDefinitionCmd {
kind: "verifyBaselineGoToDefinition";
markers: string[];
ranges?: boolean;
}

interface GoToCmd {
kind: "goTo";
// !!! `selectRange` and `rangeStart` require parsing variables and `test.ranges()[n]`
Expand All @@ -821,7 +855,7 @@ interface EditCmd {
goStatement: string;
}

type Cmd = VerifyCompletionsCmd | VerifyBaselineFindAllReferencesCmd | GoToCmd | EditCmd;
type Cmd = VerifyCompletionsCmd | VerifyBaselineFindAllReferencesCmd | VerifyBaselineGoToDefinitionCmd | GoToCmd | EditCmd;

function generateVerifyCompletions({ marker, args, isNewIdentifierLocation }: VerifyCompletionsCmd): string {
let expectedList: string;
Expand Down Expand Up @@ -857,6 +891,13 @@ function generateBaselineFindAllReferences({ markers, ranges }: VerifyBaselineFi
return `f.VerifyBaselineFindAllReferences(t, ${markers.join(", ")})`;
}

function generateBaselineGoToDefinition({ markers, ranges }: VerifyBaselineGoToDefinitionCmd): string {
if (ranges || markers.length === 0) {
return `f.VerifyBaselineGoToDefinition(t)`;
}
return `f.VerifyBaselineGoToDefinition(t, ${markers.join(", ")})`;
}

function generateGoToCommand({ funcName, args }: GoToCmd): string {
const funcNameCapitalized = funcName.charAt(0).toUpperCase() + funcName.slice(1);
return `f.GoTo${funcNameCapitalized}(t, ${args.join(", ")})`;
Expand All @@ -868,12 +909,15 @@ function generateCmd(cmd: Cmd): string {
return generateVerifyCompletions(cmd as VerifyCompletionsCmd);
case "verifyBaselineFindAllReferences":
return generateBaselineFindAllReferences(cmd as VerifyBaselineFindAllReferencesCmd);
case "verifyBaselineGoToDefinition":
return generateBaselineGoToDefinition(cmd as VerifyBaselineGoToDefinitionCmd);
case "goTo":
return generateGoToCommand(cmd as GoToCmd);
case "edit":
return cmd.goStatement;
default:
throw new Error(`Unknown command kind: ${cmd}`);
let neverCommand: never = cmd;
throw new Error(`Unknown command kind: ${neverCommand as Cmd["kind"]}`);
}
}

Expand Down
4 changes: 3 additions & 1 deletion internal/fourslash/_scripts/failingTests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ TestFindAllRefsCommonJsRequire2
TestFindAllRefsCommonJsRequire3
TestFindAllRefsExportEquals
TestFindAllRefsForDefaultExport03
TestFindAllRefsJsDocImportTag
TestFindAllRefsModuleDotExports
TestFindAllRefsReExport_broken
TestFindAllRefs_importType_typeofImport
Expand All @@ -203,6 +202,9 @@ TestGetJavaScriptCompletions8
TestGetJavaScriptCompletions9
TestGetJavaScriptGlobalCompletions1
TestGetJavaScriptQuickInfo8
TestGoToDefinitionBuiltInTypes
TestGoToDefinitionBuiltInValues
TestGoToDefinitionUndefinedSymbols
TestImportCompletionsPackageJsonExportsSpecifierEndsInTs
TestImportCompletionsPackageJsonExportsTrailingSlash1
TestImportCompletionsPackageJsonImportsConditions1
Expand Down
23 changes: 21 additions & 2 deletions internal/fourslash/baselineutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ func (f *FourslashTest) getBaselineForLocationsWithFileContents(spans []*lsproto
}

func (f *FourslashTest) getBaselineForGroupedLocationsWithFileContents(groupedLocations *collections.MultiMap[lsproto.DocumentUri, *lsproto.Location], options baselineFourslashLocationsOptions) string {
// We must always print the file containing the marker,
// but don't want to print it twice at the end if it already
// found in a file with ranges.
foundMarker := false

baselineEntries := []string{}
err := f.vfs.WalkDir("/", func(path string, d vfs.DirEntry, e error) error {
if e != nil {
Expand All @@ -64,16 +69,22 @@ func (f *FourslashTest) getBaselineForGroupedLocationsWithFileContents(groupedLo
return nil
}

locations := groupedLocations.Get(ls.FileNameToDocumentURI(path))
fileName := ls.FileNameToDocumentURI(path)
locations := groupedLocations.Get(fileName)
if len(locations) == 0 {
return nil
}

content, ok := f.vfs.ReadFile(path)
if !ok {
// !!! error?
return nil
}

if options.marker != nil && options.marker.FileName() == path {
foundMarker = true
}

documentSpans := core.Map(locations, func(location *lsproto.Location) *documentSpan {
return &documentSpan{
Location: *location,
Expand All @@ -86,7 +97,15 @@ func (f *FourslashTest) getBaselineForGroupedLocationsWithFileContents(groupedLo
if err != nil && !errors.Is(err, fs.ErrNotExist) {
panic("walkdir error during fourslash baseline: " + err.Error())
}
// !!! foundMarker

if !foundMarker && options.marker != nil {
// If we didn't find the marker in any file, we need to add it.
markerFileName := options.marker.FileName()
if content, ok := f.vfs.ReadFile(markerFileName); ok {
baselineEntries = append(baselineEntries, f.getBaselineContentForFile(markerFileName, content, nil, nil, options))
}
}

// !!! foundAdditionalSpan
// !!! skipDocumentContainingOnlyMarker

Expand Down
106 changes: 90 additions & 16 deletions internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -774,19 +774,7 @@ func (f *FourslashTest) VerifyBaselineFindAllReferences(
t *testing.T,
markers ...string,
) {
// if there are no markers specified, use all ranges
var referenceLocations []MarkerOrRange
if len(markers) == 0 {
referenceLocations = core.Map(f.testData.Ranges, func(r *RangeMarker) MarkerOrRange { return r })
} else {
referenceLocations = core.Map(markers, func(markerName string) MarkerOrRange {
marker, ok := f.testData.MarkerPositions[markerName]
if !ok {
t.Fatalf("Marker '%s' not found", markerName)
}
return marker
})
}
referenceLocations := f.lookupMarkersOrGetRanges(t, markers)

if f.baseline != nil {
t.Fatalf("Error during test '%s': Another baseline is already in progress", t.Name())
Expand All @@ -806,21 +794,23 @@ func (f *FourslashTest) VerifyBaselineFindAllReferences(
for _, markerOrRange := range referenceLocations {
// worker in `baselineEachMarkerOrRange`
f.GoToMarkerOrRange(t, markerOrRange)

params := &lsproto.ReferenceParams{
TextDocumentPositionParams: f.currentTextDocumentPositionParams(),
Context: &lsproto.ReferenceContext{},
}
resMsg := f.sendRequest(t, lsproto.MethodTextDocumentReferences, params)
if resMsg == nil {
if f.lastKnownMarkerName == nil {
t.Fatalf("Unexpected references response type at pos %v", f.currentCaretPosition)
t.Fatalf("Nil response received for references request at pos %v", f.currentCaretPosition)
} else {
t.Fatalf("Nil response received for references request at marker '%s'", *f.lastKnownMarkerName)
}
}

result := resMsg.AsResponse().Result
if result, ok := result.([]*lsproto.Location); ok {
f.baseline.addResult("findAllReferences", f.getBaselineForLocationsWithFileContents(result, baselineFourslashLocationsOptions{
if resultAsLocation, ok := result.([]*lsproto.Location); ok {
f.baseline.addResult("findAllReferences", f.getBaselineForLocationsWithFileContents(resultAsLocation, baselineFourslashLocationsOptions{
marker: markerOrRange.GetMarker(),
markerName: "/*FIND ALL REFS*/",
}))
Expand All @@ -832,9 +822,93 @@ func (f *FourslashTest) VerifyBaselineFindAllReferences(
}
}
}

baseline.Run(t, f.baseline.getBaselineFileName(), f.baseline.content.String(), baseline.Options{})
}

func (f *FourslashTest) VerifyBaselineGoToDefinition(
t *testing.T,
markers ...string,
) {
referenceLocations := f.lookupMarkersOrGetRanges(t, markers)

if f.baseline != nil {
t.Fatalf("Error during test '%s': Another baseline is already in progress", t.Name())
} else {
f.baseline = &baselineFromTest{
content: &strings.Builder{},
baselineName: "goToDef/" + strings.TrimPrefix(t.Name(), "Test"),
ext: ".baseline.jsonc",
}
}

// empty baseline after test completes
defer func() {
f.baseline = nil
}()

for _, markerOrRange := range referenceLocations {
// worker in `baselineEachMarkerOrRange`
f.GoToMarkerOrRange(t, markerOrRange)

params := &lsproto.DefinitionParams{
TextDocumentPositionParams: f.currentTextDocumentPositionParams(),
}
resMsg := f.sendRequest(t, lsproto.MethodTextDocumentDefinition, params)
if resMsg == nil {
if f.lastKnownMarkerName == nil {
t.Fatalf("Nil response received for definition request at pos %v", f.currentCaretPosition)
} else {
t.Fatalf("Nil response received for definition request at marker '%s'", *f.lastKnownMarkerName)
}
}

result := resMsg.AsResponse().Result
if resultAsLocOrLocations, ok := result.(*lsproto.LocationOrLocations); ok {
var resultAsLocations []*lsproto.Location
if resultAsLocOrLocations != nil {
if resultAsLocOrLocations.Locations != nil {
resultAsLocations = core.Map(*resultAsLocOrLocations.Locations, func(loc lsproto.Location) *lsproto.Location {
return &loc
})
} else {
resultAsLocations = []*lsproto.Location{resultAsLocOrLocations.Location}
}
}

f.baseline.addResult("goToDefinition", f.getBaselineForLocationsWithFileContents(resultAsLocations, baselineFourslashLocationsOptions{
marker: markerOrRange.GetMarker(),
markerName: "/*GO TO DEFINITION*/",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This marker is different from both of the ones we have in Strada. Open to adopting one over the other.

}))
} else {
if f.lastKnownMarkerName == nil {
t.Fatalf("Unexpected definition response type at pos %v: %T", f.currentCaretPosition, result)
} else {
t.Fatalf("Unexpected definition response type at marker '%s': %T", *f.lastKnownMarkerName, result)
}
}
}

baseline.Run(t, f.baseline.getBaselineFileName(), f.baseline.content.String(), baseline.Options{})
}

// Collects all named markers if provided, or defaults to anonymous ranges
func (f *FourslashTest) lookupMarkersOrGetRanges(t *testing.T, markers []string) []MarkerOrRange {
var referenceLocations []MarkerOrRange
if len(markers) == 0 {
referenceLocations = core.Map(f.testData.Ranges, func(r *RangeMarker) MarkerOrRange { return r })
} else {
referenceLocations = core.Map(markers, func(markerName string) MarkerOrRange {
marker, ok := f.testData.MarkerPositions[markerName]
if !ok {
t.Fatalf("Marker '%s' not found", markerName)
}
return marker
})
}
return referenceLocations
}

func ptrTo[T any](v T) *T {
return &v
}
Expand Down
53 changes: 53 additions & 0 deletions internal/fourslash/tests/gen/declarationMapGoToDefinition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestDeclarationMapGoToDefinition(t *testing.T) {
t.Parallel()

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @Filename: index.ts
export class Foo {
member: string;
/*2*/methodName(propName: SomeType): void {}
otherMethod() {
if (Math.random() > 0.5) {
return {x: 42};
}
return {y: "yes"};
}
}
export interface SomeType {
member: number;
}
// @Filename: indexdef.d.ts.map
{"version":3,"file":"indexdef.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;IACI,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IACpC,WAAW;;;;;;;CAMd;AAED,MAAM,WAAW,QAAQ;IACrB,MAAM,EAAE,MAAM,CAAC;CAClB"}
// @Filename: indexdef.d.ts
export declare class Foo {
member: string;
methodName(propName: SomeType): void;
otherMethod(): {
x: number;
y?: undefined;
} | {
y: string;
x?: undefined;
};
}
export interface SomeType {
member: number;
}
//# sourceMappingURL=indexdef.d.ts.map
// @Filename: mymodule.ts
import * as mod from "./indexdef";
const instance = new mod.Foo();
instance.[|/*1*/methodName|]({member: 12});`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyBaselineGoToDefinition(t, "1")
}
Loading
Loading