Skip to content

[draft] find all references fourslash #1274

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions internal/collections/multimap.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ type MultiMap[K comparable, V comparable] struct {
M map[K][]V
}

func GroupBy[K comparable, V comparable](items []V, groupId func(V) K) *MultiMap[K, V] {
m := &MultiMap[K, V]{}
for _, item := range items {
m.Add(groupId(item), item)
}
return m
}

func (s *MultiMap[K, V]) Has(key K) bool {
_, ok := s.M[key]
return ok
Expand Down
51 changes: 47 additions & 4 deletions internal/fourslash/_scripts/convertFourslash.mts
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,16 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
console.error(`Expected identifiers for namespace and function, got ${namespace.getText()} and ${func.getText()}`);
return undefined;
}
// `verify.completions(...)`
if (namespace.text === "verify" && func.text === "completions") {
return parseVerifyCompletionsArgs(callExpression.arguments);
// `verify.(...)`
if (namespace.text === "verify") {
switch (func.text) {
case "completions":
// `verify.completions(...)`
return parseVerifyCompletionsArgs(callExpression.arguments);
case "baselineFindAllReferences":
// `verify.baselineFindAllReferences(...)`
return [parseBaselineFindAllReferencesArgs(callExpression.arguments)];
}
}
// `goTo.marker(...)`
if (namespace.text === "goTo" && func.text === "marker") {
Expand Down Expand Up @@ -405,6 +412,27 @@ function parseExpectedCompletionItem(expr: ts.Expression): string | undefined {
return undefined; // Unsupported expression type
}

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

return {
kind: "verifyBaselineFindAllReferences",
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 @@ -530,12 +558,18 @@ interface VerifyCompletionsArgs {
exact?: string;
}

interface VerifyBaselineFindAllReferencesCmd {
kind: "verifyBaselineFindAllReferences";
markers: string[];
ranges?: boolean;
}

interface GoToMarkerCmd {
kind: "goToMarker";
marker: string;
}

type Cmd = VerifyCompletionsCmd | GoToMarkerCmd;
type Cmd = VerifyCompletionsCmd | GoToMarkerCmd | VerifyBaselineFindAllReferencesCmd;

function generateVerifyCompletions({ marker, args, isNewIdentifierLocation }: VerifyCompletionsCmd): string {
let expectedList = "nil";
Expand All @@ -560,6 +594,13 @@ function generateVerifyCompletions({ marker, args, isNewIdentifierLocation }: Ve
return `f.VerifyCompletions(t, ${marker}, ${expectedList})`;
}

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

function generateGoToMarker({ marker }: GoToMarkerCmd): string {
return `f.GoToMarker(t, ${marker})`;
}
Expand All @@ -568,6 +609,8 @@ function generateCmd(cmd: Cmd): string {
switch (cmd.kind) {
case "verifyCompletions":
return generateVerifyCompletions(cmd as VerifyCompletionsCmd);
case "verifyBaselineFindAllReferences":
return generateBaselineFindAllReferences(cmd as VerifyBaselineFindAllReferencesCmd);
case "goToMarker":
return generateGoToMarker(cmd as GoToMarkerCmd);
default:
Expand Down
83 changes: 74 additions & 9 deletions internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
id int32

testData *TestData
baseline *baselineFromTest

Check failure on line 33 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

undefined: baselineFromTest

Check failure on line 33 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

undefined: baselineFromTest

Check failure on line 33 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / baselines

undefined: baselineFromTest

Check failure on line 33 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / baselines

undefined: baselineFromTest

Check failure on line 33 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

undefined: baselineFromTest

Check failure on line 33 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

undefined: baselineFromTest

Check failure on line 33 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

undefined: baselineFromTest

Check failure on line 33 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

undefined: baselineFromTest

currentCaretPosition lsproto.Position
currentFilename string
Expand Down Expand Up @@ -250,16 +251,28 @@
if !ok {
t.Fatalf("Marker %s not found", markerName)
}
f.ensureActiveFile(t, marker.FileName)
f.ensureActiveFile(t, marker.fileName)
f.currentCaretPosition = marker.LSPosition
f.currentFilename = marker.FileName
f.currentFilename = marker.fileName
f.lastKnownMarkerName = marker.Name
}

func (f *FourslashTest) GoToPosAndFile(t *testing.T, pos lsproto.Position, fileName string) {
// GoToRangeStart
f.ensureActiveFile(t, fileName)
f.currentCaretPosition = pos
f.currentFilename = fileName
// !!! this.selectionEnd = -1
}

func (f *FourslashTest) Markers() []*Marker {
return f.testData.Markers
}

func (f *FourslashTest) Ranges() []*RangeMarker {
return f.testData.Ranges
}

func (f *FourslashTest) ensureActiveFile(t *testing.T, filename string) {
if f.activeFilename != filename {
file := core.Find(f.testData.Files, func(f *TestFileInfo) bool {
Expand All @@ -283,6 +296,15 @@
})
}

func (f *FourslashTest) currentTextDocumentPositionParams() lsproto.TextDocumentPositionParams {
return lsproto.TextDocumentPositionParams{
TextDocument: lsproto.TextDocumentIdentifier{
Uri: ls.FileNameToDocumentURI(f.currentFilename),
},
Position: f.currentCaretPosition,
}
}

func getLanguageKind(filename string) lsproto.LanguageKind {
if tspath.FileExtensionIsOneOf(
filename,
Expand Down Expand Up @@ -357,13 +379,8 @@

func (f *FourslashTest) verifyCompletionsWorker(t *testing.T, expected *VerifyCompletionsExpectedList) {
params := &lsproto.CompletionParams{
TextDocumentPositionParams: lsproto.TextDocumentPositionParams{
TextDocument: lsproto.TextDocumentIdentifier{
Uri: ls.FileNameToDocumentURI(f.currentFilename),
},
Position: f.currentCaretPosition,
},
Context: &lsproto.CompletionContext{},
TextDocumentPositionParams: f.currentTextDocumentPositionParams(),
Context: &lsproto.CompletionContext{},
}
resMsg := f.sendRequest(t, lsproto.MethodTextDocumentCompletion, params)
if resMsg == nil {
Expand Down Expand Up @@ -503,6 +520,54 @@
}
}

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
})
}

if f.baseline != nil {
t.Fatalf("Another baseline is already in progress")
} else {
f.baseline = &baselineFromTest{

Check failure on line 544 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

undefined: baselineFromTest

Check failure on line 544 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

undefined: baselineFromTest

Check failure on line 544 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / baselines

undefined: baselineFromTest

Check failure on line 544 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / baselines

undefined: baselineFromTest

Check failure on line 544 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

undefined: baselineFromTest) (typecheck)

Check failure on line 544 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

undefined: baselineFromTest) (typecheck)

Check failure on line 544 in internal/fourslash/fourslash.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

undefined: baselineFromTest (typecheck)
baseline: &strings.Builder{},
testName: t.Name(),
ext: ".baseline.jsonc",
}
}

for _, markerOrRange := range referenceLocations {
// worker in `baselineEachMarkerOrRange`
f.GoToPosAndFile(t, markerOrRange.LSPos(), markerOrRange.FileName())
params := &lsproto.ReferenceParams{
TextDocumentPositionParams: f.currentTextDocumentPositionParams(),
Context: &lsproto.ReferenceContext{},
}
resMsg := f.sendRequest(t, lsproto.MethodTextDocumentReferences, params)
if resMsg == nil {
t.Fatalf("Nil response received for completion request at marker %s", f.lastKnownMarkerName)
}
result := resMsg.AsResponse().Result
if result, ok := result.([]*lsproto.Location); ok {
// !!! TODO verifyFindAllReferences(t, markerOrRange)
} else {
t.Fatalf("Unexpected response type at marker %s: %v", f.lastKnownMarkerName, result)
}
}
}

func ptrTo[T any](v T) *T {
return &v
}
33 changes: 28 additions & 5 deletions internal/fourslash/test_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,35 @@ type RangeMarker struct {
LSRange lsproto.Range
}

func (r *RangeMarker) LSPos() lsproto.Position {
return r.LSRange.Start
}

func (r *RangeMarker) FileName() string {
return r.fileName
}

type Marker struct {
FileName string
fileName string
Position int
LSPosition lsproto.Position
Name string
Data map[string]interface{}
}

func (m *Marker) LSPos() lsproto.Position {
return m.LSPosition
}

func (m *Marker) FileName() string {
return m.fileName
}

type MarkerOrRange interface {
FileName() string
LSPos() lsproto.Position
}

type TestData struct {
Files []*TestFileInfo
MarkerPositions map[string]*Marker
Expand Down Expand Up @@ -211,8 +232,10 @@ func parseFileContent(fileName string, content string, fileOptions map[string]st
if rangeStart.marker != nil {
closedRange.Marker = rangeStart.marker
} else {
// RangeMarker is not added to list of markers
closedRange.Marker = &Marker{FileName: fileName}
// A default RangeMarker is not added to list of markers. If the RangeMarker was created by parsing an actual marker within the range
// in the test file, then the marker should have been added to the marker list when the marker was parsed.
// Similarly, if the RangeMarker has a name, this means that there was a named marker parsed within the range (and has been already included in the marker list)
closedRange.Marker = &Marker{fileName: fileName}
}

rangeMarkers = append(rangeMarkers, closedRange)
Expand Down Expand Up @@ -269,7 +292,7 @@ func parseFileContent(fileName string, content string, fileOptions map[string]st
// 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,
fileName: fileName,
Position: openMarker.position,
Name: markerNameText,
}
Expand Down Expand Up @@ -372,7 +395,7 @@ func getObjectMarker(fileName string, location *locationInformation, text string
}

marker := &Marker{
FileName: fileName,
fileName: fileName,
Position: location.position,
Data: markerValue,
}
Expand Down
22 changes: 22 additions & 0 deletions internal/fourslash/tests/gen/ambientShorthandFindAllRefs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package fourslash_test

import (
"testing"

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

Check failure on line 6 in internal/fourslash/tests/gen/ambientShorthandFindAllRefs_test.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

could not import github.com/microsoft/typescript-go/internal/fourslash (-: # github.com/microsoft/typescript-go/internal/fourslash
"github.com/microsoft/typescript-go/internal/testutil"
)

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

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @Filename: declarations.d.ts
declare module "jquery";
// @Filename: user.ts
import {/*1*/x} from "jquery";
// @Filename: user2.ts
import {/*2*/x} from "jquery";`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyBaselineFindAllReferences(t, "1", "2")
}
22 changes: 22 additions & 0 deletions internal/fourslash/tests/gen/constructorFindAllReferences1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package fourslash_test

import (
"testing"

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

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

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `export class C {
/**/public constructor() { }
public foo() { }
}

new C().foo();`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyBaselineFindAllReferences(t, "")
}
22 changes: 22 additions & 0 deletions internal/fourslash/tests/gen/constructorFindAllReferences2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package fourslash_test

import (
"testing"

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

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

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `export class C {
/**/private constructor() { }
public foo() { }
}

new C().foo();`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyBaselineFindAllReferences(t, "")
}
22 changes: 22 additions & 0 deletions internal/fourslash/tests/gen/constructorFindAllReferences3_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package fourslash_test

import (
"testing"

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

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

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `export class C {
/**/constructor() { }
public foo() { }
}

new C().foo();`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyBaselineFindAllReferences(t, "")
}
Loading
Loading