Skip to content
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
123 changes: 115 additions & 8 deletions internal/fourslash/_scripts/convertFourslash.mts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
case "renameInfoSucceeded":
case "renameInfoFailed":
return parseRenameInfo(func.text, callExpression.arguments);
case "getSemanticDiagnostics":
case "getSuggestionDiagnostics":
case "getSyntacticDiagnostics":
return parseVerifyDiagnostics(func.text, callExpression.arguments);
}
}
// `goTo....`
Expand Down Expand Up @@ -1260,6 +1264,105 @@ function parseBaselineInlayHints(args: readonly ts.Expression[]): [VerifyBaselin
}];
}

function parseVerifyDiagnostics(funcName: string, args: readonly ts.Expression[]): [VerifyDiagnosticsCmd] | undefined {
if (!args[0] || !ts.isArrayLiteralExpression(args[0])) {
console.error(`Expected an array literal argument in verify.${funcName}`);
return undefined;
}
const goArgs: string[] = [];
for (const expr of args[0].elements) {
const diag = parseExpectedDiagnostic(expr);
if (diag === undefined) {
return undefined;
}
goArgs.push(diag);
}
return [{
kind: "verifyDiagnostics",
arg: goArgs.length > 0 ? `[]*lsproto.Diagnostic{\n${goArgs.join(",\n")},\n}` : "nil",
isSuggestion: funcName === "getSuggestionDiagnostics",
}];
}

function parseExpectedDiagnostic(expr: ts.Expression): string | undefined {
if (!ts.isObjectLiteralExpression(expr)) {
console.error(`Expected object literal expression for expected diagnostic, got ${expr.getText()}`);
return undefined;
}

const diagnosticProps: string[] = [];

for (const prop of expr.properties) {
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
console.error(`Expected property assignment with identifier name for expected diagnostic, got ${prop.getText()}`);
return undefined;
}

const propName = prop.name.text;
const init = prop.initializer;

switch (propName) {
case "message": {
let messageInit;
if (messageInit = getStringLiteralLike(init)) {
messageInit.text = messageInit.text.replace("/tests/cases/fourslash", "");
diagnosticProps.push(`Message: ${getGoStringLiteral(messageInit.text)},`);
}
else {
console.error(`Expected string literal for diagnostic message, got ${init.getText()}`);
return undefined;
}
break;
}
case "code": {
let codeInit;
if (codeInit = getNumericLiteral(init)) {
diagnosticProps.push(`Code: &lsproto.IntegerOrString{Integer: PtrTo[int32](${codeInit.text})},`);
}
else {
console.error(`Expected numeric literal for diagnostic code, got ${init.getText()}`);
return undefined;
}
break;
}
case "range": {
// Handle range references like ranges[0]
const rangeArg = parseBaselineMarkerOrRangeArg(init);
if (rangeArg) {
diagnosticProps.push(`Range: ${rangeArg}.LSRange,`);
}
else {
console.error(`Expected range reference for diagnostic range, got ${init.getText()}`);
return undefined;
}
break;
}
case "reportsDeprecated": {
if (init.kind === ts.SyntaxKind.TrueKeyword) {
diagnosticProps.push(`Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagDeprecated},`);
}
break;
}
case "reportsUnnecessary": {
if (init.kind === ts.SyntaxKind.TrueKeyword) {
diagnosticProps.push(`Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagUnnecessary},`);
}
break;
}
default:
console.error(`Unrecognized property in expected diagnostic: ${propName}`);
return undefined;
}
}

if (diagnosticProps.length === 0) {
console.error(`No valid properties found in diagnostic object`);
return undefined;
}

return `&lsproto.Diagnostic{\n${diagnosticProps.join("\n")}\n}`;
}

function stringToTristate(s: string): string {
switch (s) {
case "true":
Expand Down Expand Up @@ -1395,7 +1498,7 @@ function parseBaselineMarkerOrRangeArg(arg: ts.Expression): string | undefined {
return result;
}
}
console.error(`Unrecognized argument in verify.baselineRename: ${arg.getText()}`);
console.error(`Unrecognized range argument: ${arg.getText()}`);
return undefined;
}

Expand Down Expand Up @@ -1716,12 +1819,6 @@ interface VerifyBaselineFindAllReferencesCmd {
ranges?: boolean;
}

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

interface VerifyBaselineGoToDefinitionCmd {
kind: "verifyBaselineGoToDefinition" | "verifyBaselineGoToType";
markers: string[];
Expand Down Expand Up @@ -1789,6 +1886,12 @@ interface VerifyRenameInfoCmd {
preferences: string;
}

interface VerifyDiagnosticsCmd {
kind: "verifyDiagnostics";
arg: string;
isSuggestion: boolean;
}

type Cmd =
| VerifyCompletionsCmd
| VerifyApplyCodeActionFromCompletionCmd
Expand All @@ -1804,7 +1907,8 @@ type Cmd =
| VerifyBaselineRenameCmd
| VerifyRenameInfoCmd
| VerifyBaselineInlayHintsCmd
| VerifyImportFixAtPositionCmd;
| VerifyImportFixAtPositionCmd
| VerifyDiagnosticsCmd;

function generateVerifyCompletions({ marker, args, isNewIdentifierLocation, andApplyCodeActionArgs }: VerifyCompletionsCmd): string {
let expectedList: string;
Expand Down Expand Up @@ -1953,6 +2057,9 @@ function generateCmd(cmd: Cmd): string {
return generateBaselineInlayHints(cmd);
case "verifyImportFixAtPosition":
return generateImportFixAtPosition(cmd);
case "verifyDiagnostics":
const funcName = cmd.isSuggestion ? "VerifySuggestionDiagnostics" : "VerifyNonSuggestionDiagnostics";
return `f.${funcName}(t, ${cmd.arg})`;
default:
let neverCommand: never = cmd;
throw new Error(`Unknown command kind: ${neverCommand as Cmd["kind"]}`);
Expand Down
2 changes: 2 additions & 0 deletions internal/fourslash/_scripts/failingTests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ TestAutoImportCrossProject_symlinks_toDist
TestAutoImportCrossProject_symlinks_toSrc
TestAutoImportFileExcludePatterns3
TestAutoImportJsDocImport1
TestAutoImportModuleNone1
TestAutoImportNodeNextJSRequire
TestAutoImportPathsAliasesAndBarrels
TestAutoImportPnpm
Expand Down Expand Up @@ -328,6 +329,7 @@ TestInstanceTypesForGenericType1
TestJavascriptModules20
TestJavascriptModulesTypeImport
TestJsDocAugments
TestJsDocAugmentsAndExtends
TestJsDocExtends
TestJsDocFunctionSignatures10
TestJsDocFunctionSignatures11
Expand Down
1 change: 1 addition & 0 deletions internal/fourslash/_scripts/manualTests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ completionListInClosedFunction05
completionsAtIncompleteObjectLiteralProperty
completionsSelfDeclaring1
completionsWithDeprecatedTag4
parserCorruptionAfterMapInClass
renameDefaultKeyword
renameForDefaultExport01
tsxCompletion12
81 changes: 78 additions & 3 deletions internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,16 @@ func (f *FourslashTest) Ranges() []*RangeMarker {
return f.testData.Ranges
}

func (f *FourslashTest) getRangesInFile(fileName string) []*RangeMarker {
var rangesInFile []*RangeMarker
for _, rangeMarker := range f.testData.Ranges {
if rangeMarker.FileName() == fileName {
rangesInFile = append(rangesInFile, rangeMarker)
}
}
return rangesInFile
}

func (f *FourslashTest) ensureActiveFile(t *testing.T, filename string) {
if f.activeFilename != filename {
f.openFile(t, filename)
Expand Down Expand Up @@ -913,8 +923,9 @@ func ignorePaths(paths ...string) cmp.Option {
}

var (
completionIgnoreOpts = ignorePaths(".Kind", ".SortText", ".FilterText", ".Data")
autoImportIgnoreOpts = ignorePaths(".Kind", ".SortText", ".FilterText", ".Data", ".LabelDetails", ".Detail", ".AdditionalTextEdits")
completionIgnoreOpts = ignorePaths(".Kind", ".SortText", ".FilterText", ".Data")
autoImportIgnoreOpts = ignorePaths(".Kind", ".SortText", ".FilterText", ".Data", ".LabelDetails", ".Detail", ".AdditionalTextEdits")
diagnosticsIgnoreOpts = ignorePaths(".Severity", ".Source", ".RelatedInformation")
)

func (f *FourslashTest) verifyCompletionItem(t *testing.T, prefix string, actual *lsproto.CompletionItem, expected *lsproto.CompletionItem) {
Expand Down Expand Up @@ -1815,7 +1826,12 @@ func (f *FourslashTest) ReplaceLine(t *testing.T, lineIndex int, text string) {
func (f *FourslashTest) selectLine(t *testing.T, lineIndex int) {
script := f.getScriptInfo(f.activeFilename)
start := script.lineMap.LineStarts[lineIndex]
end := script.lineMap.LineStarts[lineIndex+1] - 1
var end core.TextPos
if lineIndex+1 >= len(script.lineMap.LineStarts) {
end = core.TextPos(len(script.content))
} else {
end = script.lineMap.LineStarts[lineIndex+1] - 1
}
f.selectRange(t, core.NewTextRange(int(start), int(end)))
}

Expand Down Expand Up @@ -2474,6 +2490,65 @@ func (f *FourslashTest) VerifyBaselineInlayHints(
f.addResultToBaseline(t, "Inlay Hints", strings.Join(annotations, "\n\n"))
}

func (f *FourslashTest) VerifyDiagnostics(t *testing.T, expected []*lsproto.Diagnostic) {
f.verifyDiagnostics(t, expected, func(d *lsproto.Diagnostic) bool { return true })
}

// Similar to `VerifyDiagnostics`, but excludes suggestion diagnostics returned from server.
func (f *FourslashTest) VerifyNonSuggestionDiagnostics(t *testing.T, expected []*lsproto.Diagnostic) {
f.verifyDiagnostics(t, expected, func(d *lsproto.Diagnostic) bool { return !isSuggestionDiagnostic(d) })
}

// Similar to `VerifyDiagnostics`, but includes only suggestion diagnostics returned from server.
func (f *FourslashTest) VerifySuggestionDiagnostics(t *testing.T, expected []*lsproto.Diagnostic) {
f.verifyDiagnostics(t, expected, isSuggestionDiagnostic)
}

func (f *FourslashTest) verifyDiagnostics(t *testing.T, expected []*lsproto.Diagnostic, filterDiagnostics func(*lsproto.Diagnostic) bool) {
params := &lsproto.DocumentDiagnosticParams{
TextDocument: lsproto.TextDocumentIdentifier{
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
},
}
resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentDiagnosticInfo, params)
if resMsg == nil {
t.Fatal("Nil response received for diagnostics request")
}
if !resultOk {
t.Fatalf("Unexpected response type for diagnostics request: %T", resMsg.AsResponse().Result)
}

var actualDiagnostics []*lsproto.Diagnostic
if result.FullDocumentDiagnosticReport != nil {
actualDiagnostics = append(actualDiagnostics, result.FullDocumentDiagnosticReport.Items...)
}
actualDiagnostics = core.Filter(actualDiagnostics, filterDiagnostics)
emptyRange := lsproto.Range{}
expectedWithRanges := make([]*lsproto.Diagnostic, len(expected))
for i, diag := range expected {
if diag.Range == emptyRange {
rangesInFile := f.getRangesInFile(f.activeFilename)
if len(rangesInFile) == 0 {
t.Fatalf("No ranges found in file %s to assign to diagnostic with empty range", f.activeFilename)
}
diagWithRange := *diag
diagWithRange.Range = rangesInFile[0].LSRange
expectedWithRanges[i] = &diagWithRange
} else {
expectedWithRanges[i] = diag
}
}
if len(actualDiagnostics) == 0 && len(expectedWithRanges) == 0 {
return
}
assertDeepEqual(t, actualDiagnostics, expectedWithRanges, "Diagnostics do not match expected", diagnosticsIgnoreOpts)
}

func isSuggestionDiagnostic(diag *lsproto.Diagnostic) bool {
return diag.Tags != nil && len(*diag.Tags) > 0 ||
Copy link
Member

Choose a reason for hiding this comment

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

Suggestions don't necessarily have to have tags; e.g. if we were to do the "convert to ESM" fix, that would just be a hint, no tags. The tags just apply a visual change like strikethrough or greying out.

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 an heuristic, as I think we don't have a consistent way of signaling a diagnostic is a suggestion, right? e.g. the unreachable code diagnostic has a tag, but its severity is Error.

Copy link
Member

Choose a reason for hiding this comment

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

It is so long as allowUnreachableCode is true; if it's undefined then it's Hint. But if that's the difference you're checking then yeah we can leave it.

(diag.Severity != nil && *diag.Severity == lsproto.DiagnosticSeverityHint)
}

func isLibFile(fileName string) bool {
baseName := tspath.GetBaseFileName(fileName)
if strings.HasPrefix(baseName, "lib.") && strings.HasSuffix(baseName, ".d.ts") {
Expand Down
19 changes: 19 additions & 0 deletions internal/fourslash/tests/gen/annotateWithTypeFromJSDoc2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package fourslash_test

import (
"testing"

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

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

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @Filename: test123.ts
/** @type {number} */
var [|x|]: string;`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifySuggestionDiagnostics(t, nil)
}
37 changes: 37 additions & 0 deletions internal/fourslash/tests/gen/autoImportModuleNone1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package fourslash_test

import (
"testing"

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

func TestAutoImportModuleNone1(t *testing.T) {
t.Parallel()
t.Skip()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @module: none
// @moduleResolution: bundler
// @target: es5
// @Filename: /node_modules/dep/index.d.ts
export const x: number;
// @Filename: /index.ts
x/**/`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{
IsIncomplete: false,
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
CommitCharacters: &DefaultCommitCharacters,
EditRange: Ignored,
},
Items: &fourslash.CompletionsExpectedItems{
Excludes: []string{
"x",
},
},
})
f.ReplaceLine(t, 0, "import { x } from 'dep'; x;")
f.VerifyNonSuggestionDiagnostics(t, nil)
}
Loading