Skip to content

Add baseline tests from signature help #1485

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 8 commits into from
Aug 4, 2025
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion internal/fourslash/_scripts/convertFourslash.mts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
return [parseBaselineFindAllReferencesArgs(callExpression.arguments)];
case "baselineQuickInfo":
return [parseBaselineQuickInfo(callExpression.arguments)];
case "baselineSignatureHelp":
return [parseBaselineSignatureHelp(callExpression.arguments)];
case "baselineGoToDefinition":
case "baselineGetDefinitionAtPosition":
// Both of these take the same arguments, but differ in that...
Expand Down Expand Up @@ -856,6 +858,16 @@ function parseQuickInfoArgs(funcName: string, args: readonly ts.Expression[]): V
return undefined;
}

function parseBaselineSignatureHelp(args: ts.NodeArray<ts.Expression>): Cmd {
if (args.length !== 0) {
// All calls are currently empty!
throw new Error("Expected no arguments in verify.baselineSignatureHelp");
}
return {
kind: "verifyBaselineSignatureHelp",
};
}

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 @@ -1007,6 +1019,10 @@ interface VerifyBaselineQuickInfoCmd {
kind: "verifyBaselineQuickInfo";
}

interface VerifyBaselineSignatureHelpCmd {
kind: "verifyBaselineSignatureHelp";
}

interface GoToCmd {
kind: "goTo";
// !!! `selectRange` and `rangeStart` require parsing variables and `test.ranges()[n]`
Expand All @@ -1031,6 +1047,7 @@ type Cmd =
| VerifyBaselineFindAllReferencesCmd
| VerifyBaselineGoToDefinitionCmd
| VerifyBaselineQuickInfoCmd
| VerifyBaselineSignatureHelpCmd
| GoToCmd
| EditCmd
| VerifyQuickInfoCmd;
Expand Down Expand Up @@ -1105,6 +1122,8 @@ function generateCmd(cmd: Cmd): string {
case "verifyBaselineQuickInfo":
// Quick Info -> Hover
return `f.VerifyBaselineHover(t)`;
case "verifyBaselineSignatureHelp":
return `f.VerifyBaselineSignatureHelp(t)`;
case "goTo":
return generateGoToCommand(cmd);
case "edit":
Expand All @@ -1127,7 +1146,7 @@ interface GoTest {
}

function generateGoTest(failingTests: Set<string>, test: GoTest): string {
const testName = test.name[0].toUpperCase() + test.name.substring(1);
const testName = (test.name[0].toUpperCase() + test.name.substring(1)).replaceAll("-", "_");
const content = test.content;
const commands = test.commands.map(cmd => generateCmd(cmd)).join("\n");
const imports = [`"github.com/microsoft/typescript-go/internal/fourslash"`];
Expand Down
1 change: 1 addition & 0 deletions internal/fourslash/_scripts/failingTests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ TestReferencesIsAvailableThroughGlobalNoCrash
TestRegexDetection
TestReverseMappedTypeQuickInfo
TestSelfReferencedExternalModule
TestSignatureHelpInferenceJsDocImportTag
TestStringCompletionsImportOrExportSpecifier
TestStringCompletionsVsEscaping
TestSyntheticImportFromBabelGeneratedFile1
Expand Down
130 changes: 130 additions & 0 deletions internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,136 @@ func appendLinesForMarkedStringWithLanguage(result []string, ms *lsproto.MarkedS
return result
}

func (f *FourslashTest) VerifyBaselineSignatureHelp(t *testing.T) {
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: "signatureHelp/" + strings.TrimPrefix(t.Name(), "Test"),
ext: ".baseline",
}
}

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

markersAndItems := core.MapFiltered(f.Markers(), func(marker *Marker) (markerAndItem[*lsproto.SignatureHelp], bool) {
if marker.Name == nil {
return markerAndItem[*lsproto.SignatureHelp]{}, false
}

params := &lsproto.SignatureHelpParams{
TextDocument: lsproto.TextDocumentIdentifier{
Uri: ls.FileNameToDocumentURI(f.activeFilename),
},
Position: marker.LSPosition,
}

resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentSignatureHelpInfo, params)
var prefix string
if f.lastKnownMarkerName != nil {
prefix = fmt.Sprintf("At marker '%s': ", *f.lastKnownMarkerName)
} else {
prefix = fmt.Sprintf("At position (Ln %d, Col %d): ", f.currentCaretPosition.Line, f.currentCaretPosition.Character)
}
if resMsg == nil {
t.Fatalf(prefix+"Nil response received for signature help request", f.lastKnownMarkerName)
}
if !resultOk {
t.Fatalf(prefix+"Unexpected response type for signature help request: %T", resMsg.AsResponse().Result)
}

return markerAndItem[*lsproto.SignatureHelp]{Marker: marker, Item: result.SignatureHelp}, true
})

getRange := func(item *lsproto.SignatureHelp) *lsproto.Range {
// SignatureHelp doesn't have a range like hover does
return nil
}

getTooltipLines := func(item, _prev *lsproto.SignatureHelp) []string {
if item == nil || len(item.Signatures) == 0 {
return []string{"No signature help available"}
}

// Show active signature if specified, otherwise first signature
activeSignature := 0
if item.ActiveSignature != nil && int(*item.ActiveSignature) < len(item.Signatures) {
activeSignature = int(*item.ActiveSignature)
}

sig := item.Signatures[activeSignature]

// Build signature display
signatureLine := sig.Label
activeParamLine := ""

// Show active parameter if specified, and the signature text.
if item.ActiveParameter != nil && sig.Parameters != nil {
activeParamIndex := int(*item.ActiveParameter.Uinteger)
if activeParamIndex >= 0 && activeParamIndex < len(*sig.Parameters) {
activeParam := (*sig.Parameters)[activeParamIndex]

// Get the parameter label and bold the
// parameter text within the original string.
activeParamLabel := ""
if activeParam.Label.String != nil {
activeParamLabel = *activeParam.Label.String
} else if activeParam.Label.Tuple != nil {
activeParamLabel = signatureLine[(*activeParam.Label.Tuple)[0]:(*activeParam.Label.Tuple)[1]]
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 is slightly wrong because these are supposed to be UTF-16 offsets. But it's not really "testable' today because we don't even come back with this data.

Copy link
Member

Choose a reason for hiding this comment

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

You could utf16.Encode + slice + utf16.Decode

} else {
t.Fatal("Unsupported param label kind.")
}
signatureLine = strings.Replace(signatureLine, activeParamLabel, "**"+activeParamLabel+"**", 1)
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 should really only be done if we don't come back with the tuple representation.


if activeParam.Documentation != nil {
if activeParam.Documentation.MarkupContent != nil {
activeParamLine = activeParam.Documentation.MarkupContent.Value
} else if activeParam.Documentation.String != nil {
activeParamLine = *activeParam.Documentation.String
}

activeParamLine = fmt.Sprintf("- `%s`: %s", activeParamLabel, activeParamLine)
}

}
}

result := make([]string, 0, 16)
result = append(result, signatureLine)
if activeParamLine != "" {
result = append(result, activeParamLine)
}

// ORIGINALLY we would "only display signature documentation on the last argument when multiple arguments are marked".
// !!!
// Note that this is harder than in Strada, because LSP signature help has no concept of
// applicable spans.
if sig.Documentation != nil {
if sig.Documentation.MarkupContent != nil {
result = append(result, strings.Split(sig.Documentation.MarkupContent.Value, "\n")...)
} else if sig.Documentation.String != nil {
result = append(result, strings.Split(*sig.Documentation.String, "\n")...)
} else {
t.Fatal("Unsupported documentation format.")
}
}

return result
}

f.baseline.addResult("SignatureHelp", annotateContentWithTooltips(t, f, markersAndItems, "signaturehelp", getRange, getTooltipLines))
if jsonStr, err := core.StringifyJson(markersAndItems, "", " "); err == nil {
f.baseline.content.WriteString(jsonStr)
} else {
t.Fatalf("Failed to stringify markers and items for baseline: %v", err)
}
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
Expand Down
33 changes: 33 additions & 0 deletions internal/fourslash/tests/gen/jsDocDontBreakWithNamespaces_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package fourslash_test

import (
"testing"

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

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

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @allowJs: true
// @Filename: jsDocDontBreakWithNamespaces.js
/**
* @returns {module:@nodefuel/web~Webserver~wsServer#hello} Websocket server object
*/
function foo() { }
foo(''/*foo*/);

/**
* @type {module:xxxxx} */
*/
function bar() { }
bar(''/*bar*/);

/** @type {function(module:xxxx, module:xxxx): module:xxxxx} */
function zee() { }
zee(''/*zee*/);`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyBaselineSignatureHelp(t)
}
30 changes: 30 additions & 0 deletions internal/fourslash/tests/gen/jsDocFunctionSignatures5_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package fourslash_test

import (
"testing"

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

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

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @allowJs: true
// @Filename: Foo.js
/**
* Filters a path based on a regexp or glob pattern.
* @param {String} basePath The base path where the search will be performed.
* @param {String} pattern A string defining a regexp of a glob pattern.
* @param {String} type The search pattern type, can be a regexp or a glob.
* @param {Object} options A object containing options to the search.
* @return {Array} A list containing the filtered paths.
*/
function pathFilter(basePath, pattern, type, options){
//...
}
pathFilter(/**/'foo', 'bar', 'baz', {});`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyBaselineSignatureHelp(t)
}
26 changes: 26 additions & 0 deletions internal/fourslash/tests/gen/jsDocFunctionSignatures6_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package fourslash_test

import (
"testing"

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

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

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @allowJs: true
// @Filename: Foo.js
/**
* @param {string} p1 - A string param
* @param {string?} p2 - An optional param
* @param {string} [p3] - Another optional param
* @param {string} [p4="test"] - An optional param with a default value
*/
function f1(p1, p2, p3, p4){}
f1(/*1*/'foo', /*2*/'bar', /*3*/'baz', /*4*/'qux');`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyBaselineSignatureHelp(t)
}
21 changes: 21 additions & 0 deletions internal/fourslash/tests/gen/jsDocSignature-43394_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package fourslash_test

import (
"testing"

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

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

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `/**
* @typedef {Object} Foo
* @property {number} ...
* /**/@typedef {number} Bar
*/`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyBaselineSignatureHelp(t)
}
28 changes: 28 additions & 0 deletions internal/fourslash/tests/gen/jsdocReturnsTag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package fourslash_test

import (
"testing"

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

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

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @allowJs: true
// @Filename: dummy.js
/**
* Find an item
* @template T
* @param {T[]} l
* @param {T} x
* @returns {?T} The names of the found item(s).
*/
function find(l, x) {
}
find(''/**/);`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyBaselineSignatureHelp(t)
}
41 changes: 41 additions & 0 deletions internal/fourslash/tests/gen/quickInfoJsDocTags13_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package fourslash_test

import (
"testing"

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

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

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @allowJs: true
// @checkJs: true
// @filename: ./a.js
/**
* First overload
* @overload
* @param {number} a
* @returns {void}
*/

/**
* Second overload
* @overload
* @param {string} a
* @returns {void}
*/

/**
* @param {string | number} a
* @returns {void}
*/
function f(a) {}

f(/*a*/1);
f(/*b*/"");`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyBaselineSignatureHelp(t)
}
Loading
Loading