4716 lines
194 KiB
TypeScript
4716 lines
194 KiB
TypeScript
#!/usr/bin/env -S node --experimental-strip-types --no-warnings
|
|
|
|
// Usage: node --experimental-strip-types --no-warnings convertFourslash.mts [inputFileList]
|
|
|
|
import { $ } from "execa";
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
import * as ts from "typescript";
|
|
import * as url from "url";
|
|
|
|
const stradaFourslashPath = path.resolve(import.meta.dirname, "../", "../", "../", "_submodules", "TypeScript", "tests", "cases", "fourslash");
|
|
|
|
let inputFileSet: Set<string>;
|
|
|
|
const manualTestsPath = path.join(import.meta.dirname, "manualTests.txt");
|
|
|
|
const outputDir = path.join(import.meta.dirname, "../", "tests", "gen");
|
|
|
|
const unparsedFiles: { file: string; error: string; }[] = [];
|
|
const unparsedReportPath = path.join(import.meta.dirname, "unparsedTests.txt");
|
|
|
|
// Go import paths used in generated test files.
|
|
const IMPORT_FOURSLASH = `"github.com/microsoft/typescript-go/internal/fourslash"`;
|
|
const IMPORT_TESTUTIL = `"github.com/microsoft/typescript-go/internal/testutil"`;
|
|
const IMPORT_CORE = `"github.com/microsoft/typescript-go/internal/core"`;
|
|
const IMPORT_LS = `"github.com/microsoft/typescript-go/internal/ls"`;
|
|
const IMPORT_LSUTIL = `"github.com/microsoft/typescript-go/internal/ls/lsutil"`;
|
|
const IMPORT_LSPROTO = `"github.com/microsoft/typescript-go/internal/lsp/lsproto"`;
|
|
const IMPORT_UTIL = `. "github.com/microsoft/typescript-go/internal/fourslash/tests/util"`;
|
|
|
|
// Code fix IDs that have been implemented in the Go port.
|
|
// Tests for code fixes not in this set will be skipped during conversion.
|
|
const allowedCodeFixIds = new Set([
|
|
"fixMissingImport",
|
|
"fixMissingTypeAnnotationOnExports",
|
|
"fixClassIncorrectlyImplementsInterface",
|
|
]);
|
|
|
|
// File name prefixes for code fix tests that are allowed even without a fixId.
|
|
// These correspond to tests using verify.codeFix() or verify.codeFixAvailable()
|
|
// that don't include a fixId field.
|
|
const allowedCodeFixDescriptionPrefixes = [
|
|
"Import ",
|
|
"Add import from ",
|
|
"Update import from ",
|
|
"Implement interface '",
|
|
"Change 'import' to 'import type'",
|
|
"Add annotation of type",
|
|
"Add return type",
|
|
"Add satisfies and an inline type assertion",
|
|
"Annotate types of properties expando function",
|
|
"Extract default export to variable",
|
|
"Extract base class to variable",
|
|
"Extract binding expressions to variable",
|
|
"Extract to variable and replace with",
|
|
"Mark array literal as const",
|
|
];
|
|
|
|
function getManualTests(): Set<string> {
|
|
if (!fs.existsSync(manualTestsPath)) {
|
|
return new Set();
|
|
}
|
|
const manualTestsList = fs.readFileSync(manualTestsPath, "utf-8").split("\n").map(line => line.trim()).filter(line => line.length > 0);
|
|
return new Set(manualTestsList);
|
|
}
|
|
|
|
export async function main() {
|
|
const args = process.argv.slice(2);
|
|
const inputFilesPath = args[0];
|
|
if (inputFilesPath) {
|
|
const inputFiles = fs.readFileSync(inputFilesPath, "utf-8")
|
|
.split("\n").map(line => line.trim())
|
|
.filter(line => line.length > 0)
|
|
.map(line => path.basename(line));
|
|
inputFileSet = new Set(inputFiles);
|
|
}
|
|
|
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
|
|
// Write testmain_test.go for baseline tracking
|
|
const testMainContent = `package fourslash_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/microsoft/typescript-go/internal/core"
|
|
"github.com/microsoft/typescript-go/internal/testutil/baseline"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
core.ApplyDebugStackLimit()
|
|
defer baseline.Track()()
|
|
m.Run()
|
|
}
|
|
`;
|
|
fs.writeFileSync(path.join(outputDir, "testmain_test.go"), testMainContent, "utf-8");
|
|
|
|
parseTypeScriptFiles(getManualTests(), stradaFourslashPath);
|
|
|
|
unparsedFiles.sort((a, b) => a.file.localeCompare(b.file, "en-US"));
|
|
fs.writeFileSync(unparsedReportPath, unparsedFiles.map(({ file, error }) => `${file} parse error: ${JSON.stringify(error)}`).join("\n"), "utf-8");
|
|
console.log(`Failed to parse ${unparsedFiles.length} files. See ${unparsedReportPath} for details.`);
|
|
await $`dprint fmt ${outputDir}/**/*.go`;
|
|
}
|
|
|
|
function hasTSExtension(file: string): boolean {
|
|
return file.endsWith(".ts") ||
|
|
file.endsWith(".tsx");
|
|
}
|
|
|
|
function parseTypeScriptFiles(manualTests: Set<string>, folder: string): void {
|
|
const files = fs.readdirSync(folder);
|
|
|
|
files.forEach(file => {
|
|
const filePath = path.join(folder, file);
|
|
const stat = fs.statSync(filePath);
|
|
if (inputFileSet && !inputFileSet.has(file)) {
|
|
return;
|
|
}
|
|
|
|
if (stat.isDirectory()) {
|
|
parseTypeScriptFiles(manualTests, filePath);
|
|
}
|
|
else if (hasTSExtension(file) && !manualTests.has(file.slice(0, -3)) && file !== "fourslash.ts") {
|
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
const isServer = filePath.split(path.sep).includes("server");
|
|
try {
|
|
const test = parseFileContent(file, content);
|
|
if (test === NO_TEST) return;
|
|
const testContent = generateGoTest(test, isServer);
|
|
const testPath = path.join(outputDir, `${test.name}_test.go`);
|
|
fs.writeFileSync(testPath, testContent, "utf-8");
|
|
}
|
|
catch (e) {
|
|
const message = e instanceof Error ? e.message : String(e);
|
|
console.error(`Error parsing file ${file}: ${message}`);
|
|
unparsedFiles.push({ file, error: message });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
const NO_TEST: unique symbol = Symbol("NO_TEST");
|
|
type NoTest = typeof NO_TEST;
|
|
|
|
function parseFileContent(filename: string, content: string): GoTest | NoTest {
|
|
console.error(`Parsing file: ${filename}`);
|
|
const sourceFile = ts.createSourceFile("temp.ts", content, ts.ScriptTarget.Latest, true /*setParentNodes*/);
|
|
const statements = sourceFile.statements;
|
|
const goTest: GoTest = {
|
|
name: filename.replace(".tsx", "").replace(".ts", "").replace(".", ""),
|
|
content: getTestInput(content),
|
|
commands: [],
|
|
};
|
|
for (const statement of statements) {
|
|
const result = parseFourslashStatement(statement);
|
|
goTest.commands.push(...result);
|
|
}
|
|
if (goTest.commands.length === 0) {
|
|
console.error(`No commands parsed in file (skipping): ${filename}`);
|
|
return NO_TEST;
|
|
}
|
|
validateCodeFixCommands(goTest.commands);
|
|
return goTest;
|
|
}
|
|
|
|
function validateCodeFixCommands(commands: Cmd[]): void {
|
|
const hasCodeFixCmd = commands.some(c => c.kind === "verifyCodeFix" || c.kind === "verifyCodeFixAvailable" || c.kind === "verifyCodeFixAll");
|
|
if (!hasCodeFixCmd) {
|
|
return;
|
|
}
|
|
// Every codeFixAll must use an allowed fixId.
|
|
for (const cmd of commands) {
|
|
if (cmd.kind === "verifyCodeFixAll" && !allowedCodeFixIds.has(cmd.fixId)) {
|
|
throw new Error(`Unsupported code fix ID: ${cmd.fixId}`);
|
|
}
|
|
}
|
|
// If there are codeFix/codeFixAvailable commands but no codeFixAll with an allowed ID,
|
|
// the test is only accepted if its descriptions match allowed patterns.
|
|
const hasAllowedCodeFixAll = commands.some(c => c.kind === "verifyCodeFixAll" && allowedCodeFixIds.has(c.fixId));
|
|
const hasCodeFixOrAvailable = commands.some(c => c.kind === "verifyCodeFix" || c.kind === "verifyCodeFixAvailable");
|
|
if (hasCodeFixOrAvailable && !hasAllowedCodeFixAll) {
|
|
const allAllowed = commands.every(c => {
|
|
if (c.kind === "verifyCodeFix") {
|
|
return allowedCodeFixDescriptionPrefixes.some(p => c.description.startsWith(p));
|
|
}
|
|
if (c.kind === "verifyCodeFixAvailable") {
|
|
// Empty descriptions means "assert no fixes available", which is always allowed.
|
|
return c.descriptions.length === 0 || c.descriptions.every(d => allowedCodeFixDescriptionPrefixes.some(p => d.startsWith(p)));
|
|
}
|
|
return true;
|
|
});
|
|
if (!allAllowed) {
|
|
throw new Error(`Code fix test has no allowed fixId and descriptions do not match any allowed prefix`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getTestInput(content: string): string {
|
|
const lines = content.split("\n").map(line => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
let testInput: string[] = [];
|
|
for (const line of lines) {
|
|
let newLine = "";
|
|
if (line.startsWith("////")) {
|
|
const parts = line.substring(4).split("`");
|
|
for (let i = 0; i < parts.length; i++) {
|
|
if (i > 0) {
|
|
newLine += `\` + "\`" + \``;
|
|
}
|
|
newLine += parts[i];
|
|
}
|
|
testInput.push(newLine);
|
|
}
|
|
else if (line.startsWith("// @") || line.startsWith("//@")) {
|
|
testInput.push(line);
|
|
}
|
|
// !!! preserve non-input comments?
|
|
}
|
|
|
|
// chomp leading spaces
|
|
if (
|
|
!testInput.some(line =>
|
|
line.length != 0 &&
|
|
!line.startsWith(" ") &&
|
|
!line.startsWith("// ") &&
|
|
!line.startsWith("//@")
|
|
)
|
|
) {
|
|
testInput = testInput.map(line => {
|
|
if (line.startsWith(" ")) return line.substring(1);
|
|
return line;
|
|
});
|
|
}
|
|
return `\`${testInput.join("\n")}\``;
|
|
}
|
|
|
|
function getBadStatementText(statement: ts.Statement): string {
|
|
if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression)) {
|
|
return statement.expression.expression.getText() + "(...)";
|
|
}
|
|
return statement.getText();
|
|
}
|
|
|
|
interface VerifyAssertion {
|
|
name: string;
|
|
negated: boolean;
|
|
}
|
|
|
|
function parseVerifyAssertion(access: ts.PropertyAccessExpression): VerifyAssertion | undefined {
|
|
if (ts.isIdentifier(access.expression) && access.expression.text === "verify") {
|
|
return {
|
|
name: access.name.text,
|
|
negated: false,
|
|
};
|
|
}
|
|
|
|
if (
|
|
ts.isPropertyAccessExpression(access.expression) &&
|
|
ts.isIdentifier(access.expression.expression) &&
|
|
access.expression.expression.text === "verify" &&
|
|
access.expression.name.text === "not"
|
|
) {
|
|
return {
|
|
name: access.name.text,
|
|
negated: true,
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function isVerifyCompletionsCall(expression: ts.Expression): expression is ts.CallExpression {
|
|
if (!ts.isCallExpression(expression) || !ts.isPropertyAccessExpression(expression.expression)) {
|
|
return false;
|
|
}
|
|
const assertion = parseVerifyAssertion(expression.expression);
|
|
return !!assertion && !assertion.negated && assertion.name === "completions";
|
|
}
|
|
|
|
function parseFourslashStatement(statement: ts.Statement): Cmd[] {
|
|
if (ts.isVariableStatement(statement)) {
|
|
// variable declarations (for ranges and markers), e.g. `const range = test.ranges()[0];`
|
|
return [];
|
|
}
|
|
else if (ts.isEmptyStatement(statement)) {
|
|
// Stray semicolons, e.g. `;;`
|
|
return [];
|
|
}
|
|
else if (ts.isForOfStatement(statement)) {
|
|
return parseForOfStatement(statement);
|
|
}
|
|
else if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression)) {
|
|
const callExpression = statement.expression;
|
|
if (!ts.isPropertyAccessExpression(callExpression.expression)) {
|
|
throw new Error(`Expected property access expression, got ${callExpression.expression.getText()}`);
|
|
}
|
|
const accessExpression = callExpression.expression;
|
|
const verifyAssertion = parseVerifyAssertion(accessExpression);
|
|
|
|
if (verifyAssertion?.negated) {
|
|
switch (verifyAssertion.name) {
|
|
case "quickInfoExists":
|
|
return parseQuickInfoArgs("notQuickInfoExists", callExpression.arguments);
|
|
case "codeFixAvailable":
|
|
return parseCodeFixAvailableArgs("notCodeFixAvailable", callExpression.arguments);
|
|
}
|
|
throw new Error(`Unrecognized fourslash statement: ${getBadStatementText(statement)}`);
|
|
}
|
|
|
|
const expression = accessExpression.expression;
|
|
if (isVerifyCompletionsCall(expression) && accessExpression.name.text === "andApplyCodeAction") {
|
|
return parseVerifyCompletionsArgs(expression.arguments, callExpression.arguments);
|
|
}
|
|
|
|
// `verify.(...)`
|
|
if (verifyAssertion) {
|
|
switch (verifyAssertion.name) {
|
|
case "completions":
|
|
// `verify.completions(...)`
|
|
return parseVerifyCompletionsArgs(callExpression.arguments);
|
|
case "applyCodeActionFromCompletion":
|
|
// `verify.applyCodeActionFromCompletion(...)`
|
|
return parseVerifyApplyCodeActionFromCompletionArgs(callExpression.arguments);
|
|
case "importFixAtPosition":
|
|
// `verify.importFixAtPosition(...)`
|
|
return parseImportFixAtPositionArgs(callExpression.arguments);
|
|
case "importFixModuleSpecifiers":
|
|
// `verify.importFixModuleSpecifiers(...)`
|
|
return parseImportFixModuleSpecifiersArgs(callExpression.arguments);
|
|
case "currentLineContentIs":
|
|
case "currentFileContentIs":
|
|
case "indentationIs":
|
|
case "indentationAtPositionIs":
|
|
case "textAtCaretIs":
|
|
return parseCurrentContentIsArgs(verifyAssertion.name, callExpression.arguments);
|
|
case "quickInfoAt":
|
|
case "quickInfoExists":
|
|
case "quickInfoIs":
|
|
case "quickInfos":
|
|
// `verify.quickInfo...(...)`
|
|
return parseQuickInfoArgs(verifyAssertion.name, callExpression.arguments);
|
|
case "organizeImports":
|
|
// `verify.organizeImports(...)`
|
|
return parseOrganizeImportsArgs(callExpression.arguments);
|
|
case "baselineFindAllReferences":
|
|
// `verify.baselineFindAllReferences(...)`
|
|
return parseBaselineFindAllReferencesArgs(callExpression.arguments);
|
|
case "baselineDocumentHighlights":
|
|
return parseBaselineDocumentHighlightsArgs(callExpression.arguments);
|
|
case "baselineQuickInfo":
|
|
return parseBaselineQuickInfo(callExpression.arguments);
|
|
case "baselineSignatureHelp":
|
|
return [parseBaselineSignatureHelp(callExpression.arguments)];
|
|
case "signatureHelp":
|
|
return parseSignatureHelp(callExpression.arguments);
|
|
case "noSignatureHelp":
|
|
return parseNoSignatureHelp(callExpression.arguments);
|
|
case "signatureHelpPresentForTriggerReason":
|
|
return parseSignatureHelpPresentForTriggerReason(callExpression.arguments);
|
|
case "noSignatureHelpForTriggerReason":
|
|
return parseNoSignatureHelpForTriggerReason(callExpression.arguments);
|
|
case "baselineSmartSelection":
|
|
return [parseBaselineSmartSelection(callExpression.arguments)];
|
|
case "baselineCallHierarchy":
|
|
return [parseBaselineCallHierarchy(callExpression.arguments)];
|
|
case "baselineGoToDefinition":
|
|
case "baselineGetDefinitionAtPosition":
|
|
case "baselineGoToType":
|
|
case "baselineGoToImplementation":
|
|
case "baselineGoToSourceDefinition":
|
|
// Both `baselineGoToDefinition` and `baselineGetDefinitionAtPosition` take the same
|
|
// arguments, but differ in that...
|
|
// - `verify.baselineGoToDefinition(...)` called getDefinitionAndBoundSpan
|
|
// - `verify.baselineGetDefinitionAtPosition(...)` called getDefinitionAtPosition
|
|
// LSP doesn't have two separate commands though.
|
|
return parseBaselineGoToDefinitionArgs(verifyAssertion.name, callExpression.arguments);
|
|
case "baselineRename":
|
|
case "baselineRenameAtRangesWithText":
|
|
// `verify.baselineRename...(...)`
|
|
return parseBaselineRenameArgs(verifyAssertion.name, callExpression.arguments);
|
|
case "baselineInlayHints":
|
|
return parseBaselineInlayHints(callExpression.arguments);
|
|
case "baselineLinkedEditing":
|
|
return [{ kind: "verifyBaselineLinkedEditing" }];
|
|
case "linkedEditing":
|
|
return parseVerifyLinkedEditing(callExpression.arguments);
|
|
case "renameInfoSucceeded":
|
|
case "renameInfoFailed":
|
|
return parseRenameInfo(verifyAssertion.name, callExpression.arguments);
|
|
case "getEditsForFileRename":
|
|
return parseGetEditsForFileRename(callExpression.arguments);
|
|
case "getSemanticDiagnostics":
|
|
case "getSuggestionDiagnostics":
|
|
case "getSyntacticDiagnostics":
|
|
return parseVerifyDiagnostics(verifyAssertion.name, callExpression.arguments);
|
|
case "baselineSyntacticDiagnostics":
|
|
case "baselineSyntacticAndSemanticDiagnostics":
|
|
return [{ kind: "verifyBaselineDiagnostics" }];
|
|
case "navigateTo":
|
|
return parseVerifyNavigateTo(callExpression.arguments);
|
|
case "outliningSpansInCurrentFile":
|
|
case "outliningHintSpansInCurrentFile":
|
|
return parseOutliningSpansArgs(callExpression.arguments);
|
|
case "navigationTree":
|
|
return parseVerifyNavTree(callExpression.arguments);
|
|
case "navigationBar":
|
|
return []; // Deprecated.
|
|
case "numberOfErrorsInCurrentFile":
|
|
return parseNumberOfErrorsInCurrentFile(callExpression.arguments);
|
|
case "noErrors":
|
|
return [{ kind: "verifyNoErrors" }];
|
|
case "errorExistsAtRange":
|
|
return parseErrorExistsAtRange(callExpression.arguments);
|
|
case "currentLineContentIs":
|
|
return parseCurrentLineContentIs(callExpression.arguments);
|
|
case "currentFileContentIs":
|
|
return parseCurrentFileContentIs(callExpression.arguments);
|
|
case "errorExistsBetweenMarkers":
|
|
return parseErrorExistsBetweenMarkers(callExpression.arguments);
|
|
case "errorExistsAfterMarker":
|
|
return parseErrorExistsAfterMarker(callExpression.arguments);
|
|
case "errorExistsBeforeMarker":
|
|
return parseErrorExistsBeforeMarker(callExpression.arguments);
|
|
case "codeFix":
|
|
return parseCodeFixArgs(callExpression.arguments);
|
|
case "codeFixAvailable":
|
|
return parseCodeFixAvailableArgs(verifyAssertion.name, callExpression.arguments);
|
|
case "rangeAfterCodeFix":
|
|
return parseRangeAfterCodeFixArgs(callExpression.arguments);
|
|
case "codeFixAll":
|
|
return parseCodeFixAllArgs(callExpression.arguments);
|
|
case "semanticClassificationsAre":
|
|
return parseSemanticClassificationsAre(callExpression.arguments);
|
|
case "syntacticClassificationsAre":
|
|
return [];
|
|
case "docCommentTemplateAt":
|
|
return [parseDocCommentTemplateAtArgs(callExpression.arguments)];
|
|
case "noDocCommentTemplateAt":
|
|
return [parseNoDocCommentTemplateAtArgs(callExpression.arguments)];
|
|
}
|
|
}
|
|
|
|
if (!ts.isIdentifier(expression)) {
|
|
throw new Error(`Unrecognized fourslash statement: ${getBadStatementText(statement)}`);
|
|
}
|
|
|
|
// `goTo....`
|
|
if (expression.text === "goTo") {
|
|
return parseGoToArgs(callExpression.arguments, accessExpression.name.text);
|
|
}
|
|
// `edit....`
|
|
if (expression.text === "edit") {
|
|
const result = parseEditStatement(accessExpression.name.text, callExpression.arguments);
|
|
return [result];
|
|
}
|
|
if (expression.text === "format") {
|
|
return parseFormatStatement(accessExpression.name.text, callExpression.arguments);
|
|
}
|
|
// !!! other fourslash commands
|
|
}
|
|
throw new Error(`Unrecognized fourslash statement: ${getBadStatementText(statement)}`);
|
|
}
|
|
|
|
function parseForOfStatement(statement: ts.ForOfStatement): Cmd[] {
|
|
const initializer = statement.initializer;
|
|
let variableName: string | undefined;
|
|
if (ts.isVariableDeclarationList(initializer) && initializer.declarations.length === 1 && ts.isIdentifier(initializer.declarations[0].name)) {
|
|
variableName = initializer.declarations[0].name.text;
|
|
}
|
|
else if (ts.isIdentifier(initializer)) {
|
|
variableName = initializer.text;
|
|
}
|
|
if (!variableName) {
|
|
throw new Error(`Unrecognized fourslash statement: ${statement.getText()}`);
|
|
}
|
|
|
|
let rangeExpression: string;
|
|
const exprText = statement.expression.getText();
|
|
if (exprText === "test.markerNames()") {
|
|
rangeExpression = "f.MarkerNames()";
|
|
}
|
|
else if (exprText === "test.markers()") {
|
|
rangeExpression = "f.Markers()";
|
|
}
|
|
else if (ts.isArrayLiteralExpression(statement.expression)) {
|
|
const elements = statement.expression.elements.map(element => {
|
|
const text = getStringLiteralLike(element)?.text;
|
|
if (text === undefined) {
|
|
throw new Error(`Expected string literal in for-of array, got ${element.getText()}`);
|
|
}
|
|
return getGoStringLiteral(text);
|
|
});
|
|
rangeExpression = `[]string{${elements.join(", ")}}`;
|
|
}
|
|
else {
|
|
throw new Error(`Unrecognized fourslash statement: ${statement.getText()}`);
|
|
}
|
|
|
|
const body = ts.isBlock(statement.statement) ? statement.statement.statements : [statement.statement];
|
|
return [{
|
|
kind: "forOf",
|
|
variableName,
|
|
rangeExpression,
|
|
bodyCommands: body.flatMap(parseFourslashStatement),
|
|
}];
|
|
}
|
|
|
|
function parseEditStatement(funcName: string, args: readonly ts.Expression[]): EditCmd {
|
|
switch (funcName) {
|
|
case "insert":
|
|
case "paste":
|
|
case "insertLine": {
|
|
let arg0;
|
|
if (args.length !== 1 || !(arg0 = getStringLiteralLike(args[0]))) {
|
|
throw new Error(`Expected a single string literal argument in edit.${funcName}, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
return {
|
|
kind: "edit",
|
|
goStatement: `f.${funcName.charAt(0).toUpperCase() + funcName.slice(1)}(t, ${getGoStringLiteral(arg0.text)})`,
|
|
};
|
|
}
|
|
case "replaceLine": {
|
|
let arg0, arg1;
|
|
if (args.length !== 2 || !(arg0 = getNumericLiteral(args[0])) || !(arg1 = getStringLiteralLike(args[1]))) {
|
|
throw new Error(`Expected a single string literal argument in edit.insert, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
return {
|
|
kind: "edit",
|
|
goStatement: `f.ReplaceLine(t, ${arg0.text}, ${getGoStringLiteral(arg1.text)})`,
|
|
};
|
|
}
|
|
case "backspace": {
|
|
const arg = args[0];
|
|
if (args[0]) {
|
|
let arg0;
|
|
if (!(arg0 = getNumericLiteral(arg))) {
|
|
throw new Error(`Expected numeric literal argument in edit.backspace, got ${arg.getText()}`);
|
|
}
|
|
return {
|
|
kind: "edit",
|
|
goStatement: `f.Backspace(t, ${arg0.text})`,
|
|
};
|
|
}
|
|
return {
|
|
kind: "edit",
|
|
goStatement: `f.Backspace(t, 1)`,
|
|
};
|
|
}
|
|
case "deleteAtCaret": {
|
|
const arg = args[0];
|
|
if (arg) {
|
|
let arg0;
|
|
if (arg0 = getNumericLiteral(arg)) {
|
|
return {
|
|
kind: "edit",
|
|
goStatement: `f.DeleteAtCaret(t, ${arg0.text})`,
|
|
};
|
|
}
|
|
// Handle 'string'.length expressions
|
|
const lengthValue = getStringLengthExpression(arg);
|
|
if (lengthValue !== undefined) {
|
|
return {
|
|
kind: "edit",
|
|
goStatement: `f.DeleteAtCaret(t, ${lengthValue})`,
|
|
};
|
|
}
|
|
throw new Error(`Expected numeric literal argument in edit.deleteAtCaret, got ${arg.getText()}`);
|
|
}
|
|
return {
|
|
kind: "edit",
|
|
goStatement: `f.DeleteAtCaret(t, 1)`,
|
|
};
|
|
}
|
|
default:
|
|
throw new Error(`Unrecognized edit function: ${funcName}`);
|
|
}
|
|
}
|
|
|
|
function parseFormatStatement(funcName: string, args: readonly ts.Expression[]): FormatCmd[] {
|
|
switch (funcName) {
|
|
case "document": {
|
|
return [{
|
|
kind: "format",
|
|
goStatement: `f.FormatDocument(t, "")`,
|
|
}];
|
|
}
|
|
case "setOption":
|
|
var optName = getStringLiteralLike(args[0])!.text;
|
|
if (optName == "newline") {
|
|
optName = "NewLineCharacter";
|
|
}
|
|
var optValue = args[1].getText();
|
|
if (
|
|
(args[1].kind == ts.SyntaxKind.TrueKeyword || args[1].kind == ts.SyntaxKind.FalseKeyword)
|
|
) {
|
|
optValue = stringToTristate(args[1].getText());
|
|
}
|
|
const varName = "opts" + args[1].pos;
|
|
return [{
|
|
kind: "format",
|
|
goStatement: `${varName} := f.GetOptions()`,
|
|
}, {
|
|
kind: "format",
|
|
goStatement: `${varName}.FormatCodeSettings.${optName.charAt(0).toUpperCase() + optName.slice(1)} = ${optValue}`,
|
|
}, {
|
|
kind: "format",
|
|
goStatement: `f.Configure(t, ${varName})`,
|
|
}];
|
|
case "selection": {
|
|
const startMarker = getStringLiteralLike(args[0])?.text;
|
|
const endMarker = getStringLiteralLike(args[1])?.text;
|
|
if (startMarker === undefined || endMarker === undefined) {
|
|
throw new Error(`format.selection: expected two string literal marker names`);
|
|
}
|
|
return [{
|
|
kind: "format",
|
|
goStatement: `f.FormatSelection(t, ${JSON.stringify(startMarker)}, ${JSON.stringify(endMarker)})`,
|
|
}];
|
|
}
|
|
case "onType":
|
|
case "copyFormatOptions":
|
|
case "setFormatOptions":
|
|
default:
|
|
throw new Error(`Unrecognized format function: ${funcName}`);
|
|
}
|
|
}
|
|
|
|
function parseCurrentContentIsArgs(funcName: string, args: readonly ts.Expression[]): VerifyContentCmd[] {
|
|
switch (funcName) {
|
|
case "currentFileContentIs":
|
|
return [{
|
|
kind: "verifyContent",
|
|
goStatement: `f.VerifyCurrentFileContent(t, ${getGoStringLiteralFromNode(args[0])!})`,
|
|
}];
|
|
case "currentLineContentIs":
|
|
return [{
|
|
kind: "verifyContent",
|
|
goStatement: `f.VerifyCurrentLineContent(t, ${getGoStringLiteralFromNode(args[0])!})`,
|
|
}];
|
|
case "indentationIs":
|
|
// return [{
|
|
// kind: "verifyContent",
|
|
// goStatement: `f.VerifyIndentation(t, ${getNumericLiteral(args[0])?.text})`,
|
|
// }];
|
|
case "indentationAtPositionIs":
|
|
case "textAtCaretIs":
|
|
// return [{
|
|
// kind: "verifyContent",
|
|
// goStatement: `f.VerifyTextAtCaret(t, ${getGoStringLiteral(args[0].getText())})`,
|
|
// }];
|
|
default:
|
|
throw new Error(`Unrecognized verify content function: ${funcName}`);
|
|
}
|
|
}
|
|
|
|
function parseDocCommentTemplateAtArgs(args: readonly ts.Expression[]): VerifyDocCommentTemplateCmd {
|
|
if (args.length < 3 || args.length > 4) {
|
|
throw new Error(`Expected 3-4 arguments in verify.docCommentTemplateAt, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
return {
|
|
kind: "verifyDocCommentTemplate",
|
|
marker: parseMarkerInput(args[0]),
|
|
expectedOffset: getNumericLiteral(args[1])?.text ?? "0",
|
|
expectedText: getGoStringLiteralFromNode(args[2]),
|
|
generateReturnInDocTemplate: args[3] ? parseGenerateReturnInDocTemplateOption(args[3]) : undefined,
|
|
};
|
|
}
|
|
|
|
function parseNoDocCommentTemplateAtArgs(args: readonly ts.Expression[]): VerifyNoDocCommentTemplateCmd {
|
|
if (args.length !== 1) {
|
|
throw new Error(`Expected 1 argument in verify.noDocCommentTemplateAt, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
return {
|
|
kind: "verifyNoDocCommentTemplate",
|
|
marker: parseMarkerInput(args[0]),
|
|
};
|
|
}
|
|
|
|
function parseMarkerInput(arg: ts.Expression): string {
|
|
const literal = getStringLiteralLike(arg);
|
|
if (literal) {
|
|
return getGoStringLiteral(literal.text);
|
|
}
|
|
if (ts.isIdentifier(arg)) {
|
|
return arg.text;
|
|
}
|
|
throw new Error(`Expected marker input, got ${arg.getText()}`);
|
|
}
|
|
|
|
function parseGenerateReturnInDocTemplateOption(arg: ts.Expression): string | undefined {
|
|
const obj = getObjectLiteralExpression(arg);
|
|
if (!obj) {
|
|
throw new Error(`Expected object literal for doc comment template options, got ${arg.getText()}`);
|
|
}
|
|
for (const prop of obj.properties) {
|
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
|
|
continue;
|
|
}
|
|
if (prop.name.text === "generateReturnInDocTemplate") {
|
|
if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
return "new(true)";
|
|
}
|
|
if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
return "new(false)";
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function getGoMultiLineStringLiteral(text: string): string {
|
|
if (!text.includes("`") && !text.includes("\\")) {
|
|
return "`" + text + "`";
|
|
}
|
|
return getGoStringLiteral(text);
|
|
}
|
|
|
|
function getGoStringLiteral(text: string): string {
|
|
return `${JSON.stringify(text)}`;
|
|
}
|
|
|
|
function getGoStringLiteralFromNode(node: ts.Node): string {
|
|
const stringLiteralLike = getStringLiteralLike(node);
|
|
if (stringLiteralLike) {
|
|
return getGoMultiLineStringLiteral(stringLiteralLike.text);
|
|
}
|
|
switch (node.kind) {
|
|
case ts.SyntaxKind.BinaryExpression: {
|
|
const binaryExpr = node as ts.BinaryExpression;
|
|
const left = getGoStringLiteralFromNode(binaryExpr.left);
|
|
const right = getGoStringLiteralFromNode(binaryExpr.right);
|
|
const op = binaryExpr.operatorToken.getText();
|
|
return left + op + right;
|
|
}
|
|
default:
|
|
throw new Error(`Unhandled case ${node.kind} in getGoStringLiteralFromNode: ${node.getText()}`);
|
|
}
|
|
}
|
|
|
|
function parseGoToArgs(args: readonly ts.Expression[], funcName: string): GoToCmd[] {
|
|
switch (funcName) {
|
|
case "marker": {
|
|
const arg = args[0];
|
|
if (arg === undefined) {
|
|
return [{
|
|
kind: "goTo",
|
|
funcName: "marker",
|
|
args: [`""`],
|
|
}];
|
|
}
|
|
let strArg;
|
|
if (!(strArg = getStringLiteralLike(arg))) {
|
|
throw new Error(`Unrecognized argument in goTo.marker: ${arg.getText()}`);
|
|
}
|
|
return [{
|
|
kind: "goTo",
|
|
funcName: "marker",
|
|
args: [getGoStringLiteral(strArg.text)],
|
|
}];
|
|
}
|
|
case "file": {
|
|
if (args.length !== 1) {
|
|
throw new Error(`Expected a single argument in goTo.file, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
let arg0;
|
|
if (arg0 = getStringLiteralLike(args[0])) {
|
|
const text = arg0.text.replace("tests/cases/fourslash/server/", "").replace("tests/cases/fourslash/", "");
|
|
return [{
|
|
kind: "goTo",
|
|
funcName: "file",
|
|
args: [getGoStringLiteral(text)],
|
|
}];
|
|
}
|
|
else if (arg0 = getNumericLiteral(args[0])) {
|
|
return [{
|
|
kind: "goTo",
|
|
funcName: "fileNumber",
|
|
args: [arg0.text],
|
|
}];
|
|
}
|
|
throw new Error(`Expected string or number literal argument in goTo.file, got ${args[0].getText()}`);
|
|
}
|
|
case "position": {
|
|
let arg0;
|
|
if (args.length !== 1 || !(arg0 = getNumericLiteral(args[0]))) {
|
|
throw new Error(`Expected a single numeric literal argument in goTo.position, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
return [{
|
|
kind: "goTo",
|
|
funcName: "position",
|
|
args: [`${arg0.text}`],
|
|
}];
|
|
}
|
|
case "eof":
|
|
return [{
|
|
kind: "goTo",
|
|
funcName: "EOF",
|
|
args: [],
|
|
}];
|
|
case "bof":
|
|
return [{
|
|
kind: "goTo",
|
|
funcName: "BOF",
|
|
args: [],
|
|
}];
|
|
case "select": {
|
|
let arg0, arg1;
|
|
if (args.length !== 2 || !(arg0 = getStringLiteralLike(args[0])) || !(arg1 = getStringLiteralLike(args[1]))) {
|
|
throw new Error(`Expected two string literal arguments in goTo.select, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
return [{
|
|
kind: "goTo",
|
|
funcName: "select",
|
|
args: [getGoStringLiteral(arg0.text), getGoStringLiteral(arg1.text)],
|
|
}];
|
|
}
|
|
default:
|
|
throw new Error(`Unrecognized goTo function: ${funcName}`);
|
|
}
|
|
}
|
|
|
|
function parseVerifyCompletionsArgs(args: readonly ts.Expression[], codeActionArgs?: readonly ts.Expression[]): VerifyCompletionsCmd[] {
|
|
const cmds = [];
|
|
const codeAction = codeActionArgs?.[0] && parseAndApplyCodeActionArg(codeActionArgs[0]);
|
|
for (const arg of args) {
|
|
const result = parseVerifyCompletionArg(arg, codeAction);
|
|
if (codeActionArgs?.length) {
|
|
result.andApplyCodeActionArgs = parseAndApplyCodeActionArg(codeActionArgs[0]);
|
|
}
|
|
cmds.push(result);
|
|
}
|
|
return cmds;
|
|
}
|
|
|
|
function parseVerifyApplyCodeActionFromCompletionArgs(args: readonly ts.Expression[]): VerifyApplyCodeActionFromCompletionCmd[] {
|
|
const cmds: VerifyApplyCodeActionFromCompletionCmd[] = [];
|
|
if (args.length !== 2) {
|
|
throw new Error(`Expected two arguments in verify.applyCodeActionFromCompletion, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
if (!ts.isStringLiteralLike(args[0]) && args[0].getText() !== "undefined") {
|
|
throw new Error(`Expected string literal or "undefined" in verify.applyCodeActionFromCompletion, got ${args[0].getText()}`);
|
|
}
|
|
const markerName = getStringLiteralLike(args[0])?.text;
|
|
const marker = markerName === undefined ? "nil" : `new(${getGoStringLiteral(markerName)})`;
|
|
const options = parseVerifyApplyCodeActionArgs(args[1]);
|
|
|
|
cmds.push({ kind: "verifyApplyCodeActionFromCompletion", marker, options });
|
|
return cmds;
|
|
}
|
|
|
|
function parseVerifyApplyCodeActionArgs(arg: ts.Expression): string {
|
|
const obj = getObjectLiteralExpression(arg);
|
|
if (!obj) {
|
|
throw new Error(`Expected object literal for verify.applyCodeActionFromCompletion options, got ${arg.getText()}`);
|
|
}
|
|
let nameInit, sourceInit, descInit, dataInit;
|
|
const props: string[] = [];
|
|
for (const prop of obj.properties) {
|
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
|
|
if (ts.isShorthandPropertyAssignment(prop) && prop.name.text === "preferences") {
|
|
continue; // !!! parse once preferences are supported in fourslash
|
|
}
|
|
throw new Error(`Expected property assignment with identifier name in verify.applyCodeActionFromCompletion options, got ${prop.getText()}`);
|
|
}
|
|
const propName = prop.name.text;
|
|
const init = prop.initializer;
|
|
switch (propName) {
|
|
case "name":
|
|
nameInit = getStringLiteralLike(init);
|
|
if (!nameInit) {
|
|
throw new Error(`Expected string literal for name in verify.applyCodeActionFromCompletion options, got ${init.getText()}`);
|
|
}
|
|
props.push(`Name: ${getGoStringLiteral(nameInit.text)},`);
|
|
break;
|
|
case "source":
|
|
sourceInit = getStringLiteralLike(init);
|
|
if (!sourceInit) {
|
|
throw new Error(`Expected string literal for source in verify.applyCodeActionFromCompletion options, got ${init.getText()}`);
|
|
}
|
|
props.push(`Source: ${getGoStringLiteral(sourceInit.text)},`);
|
|
break;
|
|
case "data":
|
|
dataInit = getObjectLiteralExpression(init);
|
|
if (!dataInit) {
|
|
throw new Error(`Expected object literal for data in verify.applyCodeActionFromCompletion options, got ${init.getText()}`);
|
|
}
|
|
const dataProps: string[] = [];
|
|
for (const dataProp of dataInit.properties) {
|
|
if (!ts.isPropertyAssignment(dataProp) || !ts.isIdentifier(dataProp.name)) {
|
|
throw new Error(`Expected property assignment with identifier name in verify.applyCodeActionFromCompletion data, got ${dataProp.getText()}`);
|
|
}
|
|
const dataPropName = dataProp.name.text;
|
|
switch (dataPropName) {
|
|
case "moduleSpecifier":
|
|
const moduleSpecifierInit = getStringLiteralLike(dataProp.initializer);
|
|
if (!moduleSpecifierInit) {
|
|
throw new Error(`Expected string literal for moduleSpecifier in verify.applyCodeActionFromCompletion data, got ${dataProp.initializer.getText()}`);
|
|
}
|
|
dataProps.push(`ModuleSpecifier: ${getGoStringLiteral(moduleSpecifierInit.text)},`);
|
|
break;
|
|
}
|
|
}
|
|
props.push(`AutoImportFix: &lsproto.AutoImportFix{\n${dataProps.join("\n")}\n},`);
|
|
break;
|
|
case "description":
|
|
descInit = getStringLiteralLike(init);
|
|
if (!descInit) {
|
|
throw new Error(`Expected string literal for description in verify.applyCodeActionFromCompletion options, got ${init.getText()}`);
|
|
}
|
|
props.push(`Description: ${getGoStringLiteral(descInit.text)},`);
|
|
break;
|
|
case "newFileContent":
|
|
const newFileContentInit = getStringLiteralLike(init);
|
|
if (!newFileContentInit) {
|
|
throw new Error(`Expected string literal for newFileContent in verify.applyCodeActionFromCompletion options, got ${init.getText()}`);
|
|
}
|
|
props.push(`NewFileContent: new(${getGoMultiLineStringLiteral(newFileContentInit.text)}),`);
|
|
break;
|
|
case "newRangeContent":
|
|
const newRangeContentInit = getStringLiteralLike(init);
|
|
if (!newRangeContentInit) {
|
|
throw new Error(`Expected string literal for newRangeContent in verify.applyCodeActionFromCompletion options, got ${init.getText()}`);
|
|
}
|
|
props.push(`NewRangeContent: new(${getGoMultiLineStringLiteral(newRangeContentInit.text)}),`);
|
|
break;
|
|
case "preferences":
|
|
// Few if any tests use non-default preferences
|
|
break;
|
|
default:
|
|
throw new Error(`Unrecognized property in verify.applyCodeActionFromCompletion options: ${prop.getText()}`);
|
|
}
|
|
}
|
|
if (!nameInit) {
|
|
throw new Error(`Expected name property in verify.applyCodeActionFromCompletion options`);
|
|
}
|
|
if (!sourceInit && !dataInit) {
|
|
throw new Error(`Expected source property in verify.applyCodeActionFromCompletion options`);
|
|
}
|
|
if (!descInit) {
|
|
throw new Error(`Expected description property in verify.applyCodeActionFromCompletion options`);
|
|
}
|
|
return `&fourslash.ApplyCodeActionFromCompletionOptions{\n${props.join("\n")}\n}`;
|
|
}
|
|
|
|
function parseImportFixAtPositionArgs(args: readonly ts.Expression[]): VerifyImportFixAtPositionCmd[] {
|
|
if (args.length < 1 || args.length > 3) {
|
|
throw new Error(`Expected 1-3 arguments in verify.importFixAtPosition, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
const arrayArg = getArrayLiteralExpression(args[0]);
|
|
if (!arrayArg) {
|
|
throw new Error(`Expected array literal for first argument in verify.importFixAtPosition, got ${args[0].getText()}`);
|
|
}
|
|
const expectedTexts: string[] = [];
|
|
for (const elem of arrayArg.elements) {
|
|
const strElem = getStringLiteralLike(elem);
|
|
if (!strElem) {
|
|
throw new Error(`Expected string literal in verify.importFixAtPosition array, got ${elem.getText()}`);
|
|
}
|
|
expectedTexts.push(getGoMultiLineStringLiteral(strElem.text));
|
|
}
|
|
|
|
// If the array is empty, we should still generate valid Go code
|
|
if (expectedTexts.length === 0) {
|
|
expectedTexts.push(""); // This will be handled specially in code generation
|
|
}
|
|
|
|
let preferences: string | undefined;
|
|
if (args.length > 2 && ts.isObjectLiteralExpression(args[2])) {
|
|
preferences = parseUserPreferences(args[2]);
|
|
}
|
|
return [{
|
|
kind: "verifyImportFixAtPosition",
|
|
expectedTexts,
|
|
preferences: preferences || "nil /*preferences*/",
|
|
}];
|
|
}
|
|
|
|
function parseImportFixModuleSpecifiersArgs(args: readonly ts.Expression[]): [VerifyImportFixModuleSpecifiersCmd] {
|
|
if (args.length < 2 || args.length > 3) {
|
|
throw new Error(`Expected 2-3 arguments in verify.importFixModuleSpecifiers, got ${args.length}`);
|
|
}
|
|
|
|
const markerArg = getStringLiteralLike(args[0]);
|
|
if (!markerArg) {
|
|
throw new Error(`Expected string literal for marker in verify.importFixModuleSpecifiers, got ${args[0].getText()}`);
|
|
}
|
|
const markerName = getGoStringLiteral(markerArg.text);
|
|
|
|
const arrayArg = getArrayLiteralExpression(args[1]);
|
|
if (!arrayArg) {
|
|
throw new Error(`Expected array literal for module specifiers in verify.importFixModuleSpecifiers, got ${args[1].getText()}`);
|
|
}
|
|
|
|
const moduleSpecifiers: string[] = [];
|
|
for (const elem of arrayArg.elements) {
|
|
const strElem = getStringLiteralLike(elem);
|
|
if (!strElem) {
|
|
throw new Error(`Expected string literal in module specifiers array, got ${elem.getText()}`);
|
|
}
|
|
moduleSpecifiers.push(getGoStringLiteral(strElem.text));
|
|
}
|
|
|
|
let preferences = "nil /*preferences*/";
|
|
if (args.length > 2 && ts.isObjectLiteralExpression(args[2])) {
|
|
const parsedPrefs = parseUserPreferences(args[2]);
|
|
preferences = parsedPrefs;
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyImportFixModuleSpecifiers",
|
|
markerName,
|
|
moduleSpecifiers,
|
|
preferences,
|
|
}];
|
|
}
|
|
|
|
const completionConstants = new Map([
|
|
["completion.globals", "CompletionGlobals"],
|
|
["completion.globalTypes", "CompletionGlobalTypes"],
|
|
["completion.classElementKeywords", "CompletionClassElementKeywords"],
|
|
["completion.classElementInJsKeywords", "CompletionClassElementInJSKeywords"],
|
|
["completion.constructorParameterKeywords", "CompletionConstructorParameterKeywords"],
|
|
["completion.functionMembersWithPrototype", "CompletionFunctionMembersWithPrototype"],
|
|
["completion.functionMembers", "CompletionFunctionMembers"],
|
|
["completion.typeKeywords", "CompletionTypeKeywords"],
|
|
["completion.undefinedVarEntry", "CompletionUndefinedVarItem"],
|
|
["completion.typeAssertionKeywords", "CompletionTypeAssertionKeywords"],
|
|
["completion.globalThisEntry", "CompletionGlobalThisItem"],
|
|
]);
|
|
|
|
const completionPlus = new Map([
|
|
["completion.globalsPlus", "CompletionGlobalsPlus"],
|
|
["completion.globalTypesPlus", "CompletionGlobalTypesPlus"],
|
|
["completion.functionMembersPlus", "CompletionFunctionMembersPlus"],
|
|
["completion.functionMembersWithPrototypePlus", "CompletionFunctionMembersWithPrototypePlus"],
|
|
["completion.globalsInJsPlus", "CompletionGlobalsInJSPlus"],
|
|
["completion.typeKeywordsPlus", "CompletionTypeKeywordsPlus"],
|
|
]);
|
|
|
|
function parseVerifyCompletionArg(arg: ts.Expression, codeActionArgs?: VerifyApplyCodeActionArgs): VerifyCompletionsCmd {
|
|
let marker: string | undefined;
|
|
let goArgs: VerifyCompletionsArgs | undefined;
|
|
const defaultGoArgs: VerifyCompletionsArgs = { preferences: "nil /*preferences*/" };
|
|
const obj = getObjectLiteralExpression(arg);
|
|
if (!obj) {
|
|
throw new Error(`Expected object literal expression in verify.completions, got ${arg.getText()}`);
|
|
}
|
|
let isNewIdentifierLocation: true | undefined;
|
|
for (const prop of obj.properties) {
|
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
|
|
if (ts.isShorthandPropertyAssignment(prop) && prop.name.text === "preferences") {
|
|
continue; // !!! parse once preferences are supported in fourslash
|
|
}
|
|
throw new Error(`Expected property assignment with identifier name, got ${prop.getText()}`);
|
|
}
|
|
const propName = prop.name.text;
|
|
const init = prop.initializer;
|
|
switch (propName) {
|
|
case "marker": {
|
|
let markerInit;
|
|
if (markerInit = getStringLiteralLike(init)) {
|
|
marker = getGoStringLiteral(markerInit.text);
|
|
}
|
|
else if (markerInit = getArrayLiteralExpression(init)) {
|
|
marker = "[]string{";
|
|
for (const elem of markerInit.elements) {
|
|
if (!ts.isStringLiteral(elem)) {
|
|
throw new Error(`Expected string literal in marker array, got ${elem.getText()}`); // !!! parse marker arrays?
|
|
}
|
|
marker += `${getGoStringLiteral(elem.text)}, `;
|
|
}
|
|
marker += "}";
|
|
}
|
|
else if (markerInit = getObjectLiteralExpression(init)) {
|
|
// !!! parse marker objects?
|
|
throw new Error(`Unrecognized marker initializer: ${markerInit.getText()}`);
|
|
}
|
|
else if (init.getText() === "test.markers()") {
|
|
marker = "f.Markers()";
|
|
}
|
|
else if (
|
|
ts.isCallExpression(init)
|
|
&& init.expression.getText() === "test.marker"
|
|
&& ts.isStringLiteralLike(init.arguments[0])
|
|
) {
|
|
marker = getGoStringLiteral(init.arguments[0].text);
|
|
}
|
|
else {
|
|
throw new Error(`Unrecognized marker initializer: ${init.getText()}`);
|
|
}
|
|
break;
|
|
}
|
|
case "exact":
|
|
case "includes":
|
|
case "unsorted": {
|
|
if (init.getText() === "undefined") {
|
|
return {
|
|
kind: "verifyCompletions",
|
|
marker: marker ? marker : "nil",
|
|
args: "nil",
|
|
};
|
|
}
|
|
let expected: string;
|
|
const initText = init.getText();
|
|
if (completionConstants.has(initText)) {
|
|
expected = completionConstants.get(initText)!;
|
|
}
|
|
else if (completionPlus.keys().some(funcName => initText.startsWith(funcName))) {
|
|
const tsFunc = completionPlus.keys().find(funcName => initText.startsWith(funcName));
|
|
const funcName = completionPlus.get(tsFunc!)!;
|
|
const maybeItems = (init as ts.CallExpression).arguments[0];
|
|
const maybeOpts = (init as ts.CallExpression).arguments[1];
|
|
let items;
|
|
if (!(items = getArrayLiteralExpression(maybeItems))) {
|
|
throw new Error(`Expected array literal expression for completion.globalsPlus items, got ${maybeItems.getText()}`);
|
|
}
|
|
expected = `${funcName}(\n[]fourslash.CompletionsExpectedItem{`;
|
|
for (const elem of items.elements) {
|
|
const result = parseExpectedCompletionItem(elem, codeActionArgs);
|
|
expected += "\n" + result + ",";
|
|
}
|
|
expected += "\n}";
|
|
if (maybeOpts) {
|
|
let opts;
|
|
if (!(opts = getObjectLiteralExpression(maybeOpts))) {
|
|
throw new Error(`Expected object literal expression for completion.globalsPlus options, got ${maybeOpts.getText()}`);
|
|
}
|
|
const noLib = opts.properties[0];
|
|
if (noLib && ts.isPropertyAssignment(noLib) && noLib.name.getText() === "noLib") {
|
|
if (noLib.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
expected += ", true";
|
|
}
|
|
else if (noLib.initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
expected += ", false";
|
|
}
|
|
else {
|
|
throw new Error(`Expected boolean literal for noLib, got ${noLib.initializer.getText()}`);
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(`Expected noLib property in completion.globalsPlus options, got ${maybeOpts.getText()}`);
|
|
}
|
|
}
|
|
else if (tsFunc === "completion.globalsPlus" || tsFunc === "completion.globalsInJsPlus") {
|
|
expected += ", false"; // Default for noLib
|
|
}
|
|
expected += ")";
|
|
}
|
|
else {
|
|
expected = "[]fourslash.CompletionsExpectedItem{";
|
|
let items;
|
|
if (items = getArrayLiteralExpression(init)) {
|
|
for (const elem of items.elements) {
|
|
const result = parseExpectedCompletionItem(elem);
|
|
expected += "\n" + result + ",";
|
|
}
|
|
}
|
|
else {
|
|
const result = parseExpectedCompletionItem(init);
|
|
expected += "\n" + result + ",";
|
|
}
|
|
expected += "\n}";
|
|
}
|
|
if (propName === "includes") {
|
|
(goArgs ??= defaultGoArgs).includes = expected;
|
|
}
|
|
else if (propName === "exact") {
|
|
(goArgs ??= defaultGoArgs).exact = expected;
|
|
}
|
|
else {
|
|
(goArgs ??= defaultGoArgs).unsorted = expected;
|
|
}
|
|
break;
|
|
}
|
|
case "excludes": {
|
|
let excludes = "[]string{";
|
|
let item;
|
|
if (item = getStringLiteralLike(init)) {
|
|
excludes += `\n${getGoStringLiteral(item.text)},`;
|
|
}
|
|
else if (item = getArrayLiteralExpression(init)) {
|
|
for (const elem of item.elements) {
|
|
if (!ts.isStringLiteral(elem)) {
|
|
throw new Error(`Expected string literal in excludes array, got ${elem.getText()}`);
|
|
}
|
|
excludes += `\n${getGoStringLiteral(elem.text)},`;
|
|
}
|
|
}
|
|
excludes += "\n}";
|
|
(goArgs ??= defaultGoArgs).excludes = excludes;
|
|
break;
|
|
}
|
|
case "isNewIdentifierLocation":
|
|
if (init.kind === ts.SyntaxKind.TrueKeyword) {
|
|
isNewIdentifierLocation = true;
|
|
}
|
|
break;
|
|
case "preferences": {
|
|
if (!ts.isObjectLiteralExpression(init)) {
|
|
throw new Error(`Expected object literal for user preferences, got ${init.getText()}`);
|
|
}
|
|
const preferences = parseUserPreferences(init);
|
|
(goArgs ??= defaultGoArgs).preferences = preferences;
|
|
break;
|
|
}
|
|
case "triggerCharacter":
|
|
break; // !!! parse once they're supported in fourslash
|
|
case "defaultCommitCharacters":
|
|
case "optionalReplacementSpan": // the only two tests that use this will require manual conversion
|
|
case "isGlobalCompletion":
|
|
break; // Ignored, unused
|
|
default:
|
|
throw new Error(`Unrecognized expected completion item: ${init.parent.getText()}`);
|
|
}
|
|
}
|
|
return {
|
|
kind: "verifyCompletions",
|
|
marker: marker ? marker : "nil",
|
|
args: goArgs,
|
|
isNewIdentifierLocation: isNewIdentifierLocation,
|
|
};
|
|
}
|
|
|
|
function parseExpectedCompletionItem(expr: ts.Expression, codeActionArgs?: VerifyApplyCodeActionArgs): string {
|
|
if (completionConstants.has(expr.getText())) {
|
|
return completionConstants.get(expr.getText())!;
|
|
}
|
|
let strExpr;
|
|
if (strExpr = getStringLiteralLike(expr)) {
|
|
return getGoStringLiteral(strExpr.text);
|
|
}
|
|
if (strExpr = getObjectLiteralExpression(expr)) {
|
|
let isOptional = false;
|
|
const completionItemTags = new Set<string>();
|
|
let sourceInit: ts.StringLiteralLike | undefined;
|
|
let extensions: string[] = []; // !!!
|
|
let itemProps: string[] = [];
|
|
let name: string | undefined;
|
|
let insertText: string | undefined;
|
|
let filterText: string | undefined;
|
|
let replacementSpanIdx: string | undefined;
|
|
for (const prop of strExpr.properties) {
|
|
if (!(ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) || !ts.isIdentifier(prop.name)) {
|
|
throw new Error(`Expected property assignment with identifier name for completion item, got ${prop.getText()}`);
|
|
}
|
|
const propName = prop.name.text;
|
|
const init = ts.isPropertyAssignment(prop) ? prop.initializer : prop.name;
|
|
switch (propName) {
|
|
case "name": {
|
|
let nameInit;
|
|
if (nameInit = getStringLiteralLike(init)) {
|
|
name = nameInit.text;
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal for completion item name, got ${init.getText()}`);
|
|
}
|
|
break;
|
|
}
|
|
case "sortText":
|
|
const sortText = parseSortText(init);
|
|
itemProps.push(`SortText: new(string(${sortText.expression})),`);
|
|
if (sortText.expression === "ls.SortTextOptionalMember") {
|
|
isOptional = true;
|
|
}
|
|
if (sortText.deprecated) {
|
|
completionItemTags.add("lsproto.CompletionItemTagDeprecated");
|
|
}
|
|
break;
|
|
case "insertText": {
|
|
let insertTextInit;
|
|
if (insertTextInit = getStringLiteralLike(init)) {
|
|
insertText = insertTextInit.text;
|
|
}
|
|
else if (init.getText() === "undefined") {
|
|
// Ignore
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal for insertText, got ${init.getText()}`);
|
|
}
|
|
break;
|
|
}
|
|
case "filterText": {
|
|
let filterTextInit;
|
|
if (filterTextInit = getStringLiteralLike(init)) {
|
|
filterText = filterTextInit.text;
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal for filterText, got ${init.getText()}`);
|
|
}
|
|
break;
|
|
}
|
|
case "isRecommended":
|
|
if (init.kind === ts.SyntaxKind.TrueKeyword) {
|
|
itemProps.push(`Preselect: new(true),`);
|
|
}
|
|
break;
|
|
case "kind":
|
|
const kind = parseKind(init);
|
|
itemProps.push(`Kind: new(${kind}),`);
|
|
break;
|
|
case "kindModifiers":
|
|
const modifiers = parseKindModifiers(init);
|
|
({ isOptional, extensions } = modifiers);
|
|
if (modifiers.isDeprecated) {
|
|
completionItemTags.add("lsproto.CompletionItemTagDeprecated");
|
|
}
|
|
break;
|
|
case "text": {
|
|
let textInit;
|
|
if (textInit = getStringLiteralLike(init)) {
|
|
itemProps.push(`Detail: new(${getGoStringLiteral(textInit.text)}),`);
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal for text, got ${init.getText()}`);
|
|
}
|
|
break;
|
|
}
|
|
case "documentation": {
|
|
let docInit;
|
|
if (docInit = getStringLiteralLike(init)) {
|
|
itemProps.push(`Documentation: &lsproto.StringOrMarkupContent{
|
|
MarkupContent: &lsproto.MarkupContent{
|
|
Kind: lsproto.MarkupKindMarkdown,
|
|
Value: ${getGoStringLiteral(docInit.text)},
|
|
},
|
|
},`);
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal for documentation, got ${init.getText()}`);
|
|
}
|
|
break;
|
|
}
|
|
case "isFromUncheckedFile":
|
|
break; // Ignored
|
|
case "hasAction":
|
|
itemProps.push("AdditionalTextEdits: fourslash.AnyTextEdits,");
|
|
break;
|
|
case "source":
|
|
case "sourceDisplay":
|
|
if (sourceInit !== undefined) {
|
|
break;
|
|
}
|
|
if (sourceInit = getStringLiteralLike(init)) {
|
|
if (propName === "source" && sourceInit.text.endsWith("/")) {
|
|
// source: "ClassMemberSnippet/"
|
|
itemProps.push(`Data: &lsproto.CompletionItemData{
|
|
Source: ${getGoStringLiteral(sourceInit.text)},
|
|
},`);
|
|
break;
|
|
}
|
|
itemProps.push(`Data: &lsproto.CompletionItemData{
|
|
AutoImport: &lsproto.AutoImportFix{
|
|
ModuleSpecifier: ${getGoStringLiteral(sourceInit.text)},
|
|
},
|
|
},`);
|
|
}
|
|
else if (init.getText().startsWith("completion.CompletionSource.")) {
|
|
const source = init.getText().slice("completion.CompletionSource.".length);
|
|
switch (source) {
|
|
// Ignore switch snippet sources
|
|
case "SwitchCases": {
|
|
continue;
|
|
}
|
|
default:
|
|
throw new Error(`Unrecognized source in expected completion item: ${init.getText()}`);
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal for source/sourceDisplay, got ${init.getText()}`);
|
|
}
|
|
break;
|
|
case "commitCharacters":
|
|
// !!! support these later
|
|
break;
|
|
case "replacementSpan": {
|
|
let span;
|
|
if (ts.isIdentifier(init)) {
|
|
span = getNodeOfKind(init, (n: ts.Node): n is ts.Node => !ts.isIdentifier(n));
|
|
}
|
|
else {
|
|
span = init;
|
|
}
|
|
if (span?.getText().startsWith("test.ranges()[")) {
|
|
replacementSpanIdx = span.getText().match(/\d+/)?.[0];
|
|
}
|
|
break;
|
|
}
|
|
case "isSnippet":
|
|
if (init.kind === ts.SyntaxKind.TrueKeyword) {
|
|
itemProps.push(`InsertTextFormat: new(lsproto.InsertTextFormatSnippet),`);
|
|
}
|
|
break;
|
|
default:
|
|
throw new Error(`Unrecognized property in expected completion item: ${propName}`); // Unsupported property
|
|
}
|
|
}
|
|
if (!name) {
|
|
throw new Error(`Expected name property in expected completion item`);
|
|
}
|
|
if (codeActionArgs && codeActionArgs.name === name && codeActionArgs.source === sourceInit?.text) {
|
|
itemProps.push(`LabelDetails: &lsproto.CompletionItemLabelDetails{
|
|
Description: new(${getGoStringLiteral(codeActionArgs.source)}),
|
|
},`);
|
|
}
|
|
if (replacementSpanIdx) {
|
|
itemProps.push(`TextEdit: &lsproto.TextEditOrInsertReplaceEdit{
|
|
TextEdit: &lsproto.TextEdit{
|
|
NewText: ${getGoStringLiteral(name)},
|
|
Range: f.Ranges()[${replacementSpanIdx}].LSRange,
|
|
},
|
|
},`);
|
|
}
|
|
if (isOptional) {
|
|
insertText ??= name;
|
|
filterText ??= name;
|
|
name += "?";
|
|
}
|
|
if (filterText) itemProps.unshift(`FilterText: new(${getGoStringLiteral(filterText)}),`);
|
|
if (insertText) itemProps.unshift(`InsertText: new(${getGoStringLiteral(insertText)}),`);
|
|
const tags = formatCompletionItemTags(completionItemTags);
|
|
if (tags) itemProps.push(tags);
|
|
itemProps.unshift(`Label: ${getGoStringLiteral(name!)},`);
|
|
return `&lsproto.CompletionItem{\n${itemProps.join("\n")}}`;
|
|
}
|
|
throw new Error(`Expected string literal or object literal for expected completion item, got ${expr.getText()}`); // Unsupported expression type
|
|
}
|
|
|
|
function parseAndApplyCodeActionArg(arg: ts.Expression): VerifyApplyCodeActionArgs {
|
|
const obj = getObjectLiteralExpression(arg);
|
|
if (!obj) {
|
|
throw new Error(`Expected object literal for code action argument, got ${arg.getText()}`);
|
|
}
|
|
const nameProperty = obj.properties.find(prop =>
|
|
ts.isPropertyAssignment(prop) &&
|
|
ts.isIdentifier(prop.name) &&
|
|
prop.name.text === "name" &&
|
|
ts.isStringLiteralLike(prop.initializer)
|
|
) as ts.PropertyAssignment;
|
|
if (!nameProperty) {
|
|
throw new Error(`Expected name property in code action argument, got ${obj.getText()}`);
|
|
}
|
|
const sourceProperty = obj.properties.find(prop =>
|
|
ts.isPropertyAssignment(prop) &&
|
|
ts.isIdentifier(prop.name) &&
|
|
prop.name.text === "source" &&
|
|
ts.isStringLiteralLike(prop.initializer)
|
|
) as ts.PropertyAssignment;
|
|
if (!sourceProperty) {
|
|
throw new Error(`Expected source property in code action argument, got ${obj.getText()}`);
|
|
}
|
|
const descriptionProperty = obj.properties.find(prop =>
|
|
ts.isPropertyAssignment(prop) &&
|
|
ts.isIdentifier(prop.name) &&
|
|
prop.name.text === "description" &&
|
|
ts.isStringLiteralLike(prop.initializer)
|
|
) as ts.PropertyAssignment;
|
|
if (!descriptionProperty) {
|
|
throw new Error(`Expected description property in code action argument, got ${obj.getText()}`);
|
|
}
|
|
const newFileContentProperty = obj.properties.find(prop =>
|
|
ts.isPropertyAssignment(prop) &&
|
|
ts.isIdentifier(prop.name) &&
|
|
prop.name.text === "newFileContent" &&
|
|
ts.isStringLiteralLike(prop.initializer)
|
|
) as ts.PropertyAssignment;
|
|
if (!newFileContentProperty) {
|
|
throw new Error(`Expected newFileContent property in code action argument, got ${obj.getText()}`);
|
|
}
|
|
return {
|
|
name: (nameProperty.initializer as ts.StringLiteralLike).text,
|
|
source: (sourceProperty.initializer as ts.StringLiteralLike).text,
|
|
description: (descriptionProperty.initializer as ts.StringLiteralLike).text,
|
|
newFileContent: (newFileContentProperty.initializer as ts.StringLiteralLike).text,
|
|
};
|
|
}
|
|
|
|
function parseBaselineFindAllReferencesArgs(args: readonly ts.Expression[]): [VerifyBaselineFindAllReferencesCmd] {
|
|
const newArgs = [];
|
|
for (const arg of args) {
|
|
let strArg;
|
|
if (strArg = getStringLiteralLike(arg)) {
|
|
newArgs.push(getGoStringLiteral(strArg.text));
|
|
}
|
|
else if (arg.getText() === "...test.markerNames()") {
|
|
newArgs.push("f.MarkerNames()...");
|
|
}
|
|
else if (arg.getText() === "...test.ranges()") {
|
|
return [{
|
|
kind: "verifyBaselineFindAllReferences",
|
|
markers: [],
|
|
ranges: true,
|
|
}];
|
|
}
|
|
else {
|
|
throw new Error(`Unrecognized argument in verify.baselineFindAllReferences: ${arg.getText()}`);
|
|
}
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyBaselineFindAllReferences",
|
|
markers: newArgs,
|
|
}];
|
|
}
|
|
|
|
function parseBaselineDocumentHighlightsArgs(args: readonly ts.Expression[]): [VerifyBaselineDocumentHighlightsCmd] {
|
|
const newArgs: string[] = [];
|
|
let preferences: string | undefined;
|
|
let filesToSearch: string[] | undefined;
|
|
for (const arg of args) {
|
|
let strArg;
|
|
if (strArg = getArrayLiteralExpression(arg)) {
|
|
for (const elem of strArg.elements) {
|
|
const newArg = parseBaselineMarkerOrRangeArg(elem);
|
|
newArgs.push(newArg);
|
|
}
|
|
}
|
|
else if (ts.isCallExpression(arg) && arg.getText().includes("test.ranges()")) {
|
|
newArgs.push("ToAny(f.Ranges())...");
|
|
}
|
|
else if (ts.isObjectLiteralExpression(arg)) {
|
|
for (const prop of arg.properties) {
|
|
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === "filesToSearch" && ts.isArrayLiteralExpression(prop.initializer)) {
|
|
filesToSearch = [];
|
|
for (const e of prop.initializer.elements) {
|
|
if (ts.isStringLiteral(e)) {
|
|
filesToSearch.push(JSON.stringify(e.text));
|
|
}
|
|
else if (ts.isPropertyAccessExpression(e) && e.name.text === "fileName") {
|
|
// e.g. test.ranges()[0].fileName -> f.Ranges()[0].FileName()
|
|
const obj = e.expression;
|
|
if (ts.isElementAccessExpression(obj) && ts.isCallExpression(obj.expression) && obj.expression.getText().includes("ranges")) {
|
|
const index = obj.argumentExpression?.getText();
|
|
if (index !== undefined) {
|
|
filesToSearch.push(`f.Ranges()[${index}].FileName()`);
|
|
continue;
|
|
}
|
|
}
|
|
// e.g. range.fileName where `const range = test.ranges()[0]`
|
|
if (ts.isIdentifier(obj)) {
|
|
const resolved = parseRangeVariable(obj);
|
|
if (resolved) {
|
|
filesToSearch.push(`${resolved}.FileName()`);
|
|
continue;
|
|
}
|
|
}
|
|
// Fallback: skip filesToSearch entirely
|
|
filesToSearch = undefined;
|
|
break;
|
|
}
|
|
else {
|
|
// Unsupported expression; skip filesToSearch
|
|
filesToSearch = undefined;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
newArgs.push(parseBaselineMarkerOrRangeArg(arg));
|
|
}
|
|
}
|
|
|
|
if (newArgs.length === 0) {
|
|
newArgs.push("ToAny(f.Ranges())...");
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyBaselineDocumentHighlights",
|
|
args: newArgs,
|
|
preferences: preferences ? preferences : "nil /*preferences*/",
|
|
filesToSearch,
|
|
}];
|
|
}
|
|
|
|
function parseBaselineGoToDefinitionArgs(
|
|
funcName: "baselineGoToDefinition" | "baselineGoToType" | "baselineGetDefinitionAtPosition" | "baselineGoToImplementation" | "baselineGoToSourceDefinition",
|
|
args: readonly ts.Expression[],
|
|
): [VerifyBaselineGoToDefinitionCmd] {
|
|
let boundSpan: true | undefined;
|
|
if (funcName === "baselineGoToDefinition") {
|
|
boundSpan = true;
|
|
}
|
|
let kind: "verifyBaselineGoToDefinition" | "verifyBaselineGoToType" | "verifyBaselineGoToImplementation" | "verifyBaselineGoToSourceDefinition";
|
|
switch (funcName) {
|
|
case "baselineGoToDefinition":
|
|
case "baselineGetDefinitionAtPosition":
|
|
kind = "verifyBaselineGoToDefinition";
|
|
break;
|
|
case "baselineGoToType":
|
|
kind = "verifyBaselineGoToType";
|
|
break;
|
|
case "baselineGoToImplementation":
|
|
kind = "verifyBaselineGoToImplementation";
|
|
break;
|
|
case "baselineGoToSourceDefinition":
|
|
kind = "verifyBaselineGoToSourceDefinition";
|
|
break;
|
|
}
|
|
const newArgs = [];
|
|
for (const arg of args) {
|
|
let strArg;
|
|
if (strArg = getStringLiteralLike(arg)) {
|
|
newArgs.push(getGoStringLiteral(strArg.text));
|
|
}
|
|
else if (arg.getText() === "...test.markerNames()") {
|
|
newArgs.push("f.MarkerNames()...");
|
|
}
|
|
else if (arg.getText() === "...test.ranges()") {
|
|
return [{
|
|
kind,
|
|
markers: [],
|
|
ranges: true,
|
|
boundSpan,
|
|
}];
|
|
}
|
|
else {
|
|
throw new Error(`Unrecognized argument in verify.${funcName}: ${arg.getText()}`);
|
|
}
|
|
}
|
|
|
|
return [{
|
|
kind,
|
|
markers: newArgs,
|
|
boundSpan,
|
|
}];
|
|
}
|
|
|
|
function parseRenameInfo(funcName: "renameInfoSucceeded" | "renameInfoFailed", args: readonly ts.Expression[]): [VerifyRenameInfoCmd] {
|
|
let preferences = "nil /*preferences*/";
|
|
let prefArg;
|
|
switch (funcName) {
|
|
case "renameInfoSucceeded":
|
|
if (args[6]) {
|
|
prefArg = args[6];
|
|
}
|
|
break;
|
|
case "renameInfoFailed":
|
|
if (args[1]) {
|
|
prefArg = args[1];
|
|
}
|
|
break;
|
|
}
|
|
if (prefArg) {
|
|
if (!ts.isObjectLiteralExpression(prefArg)) {
|
|
throw new Error(`Expected object literal expression for preferences, got ${prefArg.getText()}`);
|
|
}
|
|
const parsedPreferences = parseUserPreferences(prefArg);
|
|
preferences = parsedPreferences;
|
|
}
|
|
return [{ kind: funcName, preferences }];
|
|
}
|
|
|
|
function parseGetEditsForFileRename(args: readonly ts.Expression[]): [VerifyGetEditsForFileRenameCmd] {
|
|
if (args.length !== 1 || !ts.isObjectLiteralExpression(args[0])) {
|
|
throw new Error(`Expected a single object literal argument in verify.getEditsForFileRename, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
|
|
let oldPath: string | undefined;
|
|
let newPath: string | undefined;
|
|
let newFileContents = "map[string]string{}";
|
|
let preferences = "nil /*preferences*/";
|
|
|
|
for (const prop of args[0].properties) {
|
|
if (!ts.isPropertyAssignment(prop)) {
|
|
throw new Error(`Expected property assignment in verify.getEditsForFileRename argument, got ${prop.getText()}`);
|
|
}
|
|
const name = prop.name.getText();
|
|
switch (name) {
|
|
case "oldPath": {
|
|
const value = getStringLiteralLike(prop.initializer);
|
|
if (!value) {
|
|
throw new Error(`Expected string literal for oldPath, got ${prop.initializer.getText()}`);
|
|
}
|
|
oldPath = getGoStringLiteral(value.text);
|
|
break;
|
|
}
|
|
case "newPath": {
|
|
const value = getStringLiteralLike(prop.initializer);
|
|
if (!value) {
|
|
throw new Error(`Expected string literal for newPath, got ${prop.initializer.getText()}`);
|
|
}
|
|
newPath = getGoStringLiteral(value.text);
|
|
break;
|
|
}
|
|
case "newFileContents": {
|
|
const obj = getObjectLiteralExpression(prop.initializer);
|
|
if (!obj) {
|
|
throw new Error(`Expected object literal for newFileContents, got ${prop.initializer.getText()}`);
|
|
}
|
|
const entries: string[] = [];
|
|
for (const entry of obj.properties) {
|
|
if (!ts.isPropertyAssignment(entry)) {
|
|
throw new Error(`Expected property assignment in verify.getEditsForFileRename argument, got ${prop.getText()}`);
|
|
}
|
|
const key = getStringLiteralLike(entry.name);
|
|
const value = getStringLiteralLike(entry.initializer);
|
|
if (!key || !value) {
|
|
throw new Error(`Expected string literal key/value in newFileContents, got ${entry.getText()}`);
|
|
}
|
|
entries.push(`${getGoStringLiteral(key.text)}: ${getGoMultiLineStringLiteral(value.text)}`);
|
|
}
|
|
newFileContents = entries.length === 0
|
|
? "map[string]string{}"
|
|
: `map[string]string{\n${entries.join(",\n")},\n}`;
|
|
break;
|
|
}
|
|
case "preferences": {
|
|
if (!ts.isObjectLiteralExpression(prop.initializer)) {
|
|
throw new Error(`Expected object literal for preferences, got ${prop.initializer.getText()}`);
|
|
}
|
|
preferences = parseUserPreferences(prop.initializer);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!oldPath || !newPath) {
|
|
throw new Error(`Expected oldPath and newPath in verify.getEditsForFileRename`);
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyGetEditsForFileRename",
|
|
oldPath,
|
|
newPath,
|
|
newFileContents,
|
|
preferences,
|
|
}];
|
|
}
|
|
|
|
function parseBaselineRenameArgs(funcName: string, args: readonly ts.Expression[]): [VerifyBaselineRenameCmd] {
|
|
let newArgs: string[] = [];
|
|
let preferences: string | undefined;
|
|
for (const arg of args) {
|
|
let typedArg;
|
|
if ((typedArg = getArrayLiteralExpression(arg))) {
|
|
for (const elem of typedArg.elements) {
|
|
const newArg = parseBaselineMarkerOrRangeArg(elem);
|
|
newArgs.push(newArg);
|
|
}
|
|
}
|
|
else if (ts.isObjectLiteralExpression(arg)) {
|
|
preferences = parseUserPreferences(arg);
|
|
continue;
|
|
}
|
|
else {
|
|
newArgs.push(parseBaselineMarkerOrRangeArg(arg));
|
|
}
|
|
}
|
|
return [{
|
|
kind: funcName === "baselineRenameAtRangesWithText" ? "verifyBaselineRenameAtRangesWithText" : "verifyBaselineRename",
|
|
args: newArgs,
|
|
preferences: preferences ? preferences : "nil /*preferences*/",
|
|
}];
|
|
}
|
|
|
|
function parseBaselineInlayHints(args: readonly ts.Expression[]): [VerifyBaselineInlayHintsCmd] {
|
|
let preferences: string | undefined;
|
|
// Parse span
|
|
if (args.length > 0) {
|
|
if (args[0].getText() !== "undefined") {
|
|
throw new Error(`Unsupported span argument in verify.baselineInlayHints: ${args[0].getText()}`);
|
|
}
|
|
}
|
|
// Parse preferences
|
|
if (args.length > 1) {
|
|
if (ts.isObjectLiteralExpression(args[1])) {
|
|
preferences = parseUserPreferences(args[1]);
|
|
}
|
|
}
|
|
return [{
|
|
kind: "verifyBaselineInlayHints",
|
|
span: "nil /*span*/", // Only supporteed manually
|
|
preferences: preferences ? preferences : "nil /*preferences*/",
|
|
}];
|
|
}
|
|
|
|
function parseVerifyLinkedEditing(args: readonly ts.Expression[]): [VerifyLinkedEditingCmd] {
|
|
var ranges = "map[string][]lsproto.Range" + args[0].getText().replaceAll("undefined", "nil");
|
|
return [{
|
|
kind: "verifyLinkedEditing",
|
|
ranges,
|
|
}];
|
|
}
|
|
|
|
function parseVerifyDiagnostics(funcName: string, args: readonly ts.Expression[]): [VerifyDiagnosticsCmd] {
|
|
if (!args[0] || !ts.isArrayLiteralExpression(args[0])) {
|
|
throw new Error(`Expected an array literal argument in verify.${funcName}`);
|
|
}
|
|
const goArgs: string[] = [];
|
|
for (const expr of args[0].elements) {
|
|
const diag = parseExpectedDiagnostic(expr);
|
|
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 {
|
|
if (!ts.isObjectLiteralExpression(expr)) {
|
|
throw new Error(`Expected object literal expression for expected diagnostic, got ${expr.getText()}`);
|
|
}
|
|
|
|
const diagnosticProps: string[] = [];
|
|
|
|
for (const prop of expr.properties) {
|
|
if (!ts.isPropertyAssignment(prop) || !(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))) {
|
|
throw new Error(`Expected property assignment with identifier name for expected diagnostic, got ${prop.getText()}`);
|
|
}
|
|
|
|
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: lsproto.StringOrMarkupContent{String: new(${getGoStringLiteral(messageInit.text)})},`);
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal for diagnostic message, got ${init.getText()}`);
|
|
}
|
|
break;
|
|
}
|
|
case "code": {
|
|
let codeInit;
|
|
if (codeInit = getNumericLiteral(init)) {
|
|
diagnosticProps.push(`Code: &lsproto.IntegerOrString{Integer: new(int32(${codeInit.text}))},`);
|
|
}
|
|
else {
|
|
throw new Error(`Expected numeric literal for diagnostic code, got ${init.getText()}`);
|
|
}
|
|
break;
|
|
}
|
|
case "range": {
|
|
// Handle range references like ranges[0]
|
|
const rangeArg = parseBaselineMarkerOrRangeArg(init);
|
|
diagnosticProps.push(`Range: ${rangeArg}.LSRange,`);
|
|
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:
|
|
throw new Error(`Unrecognized property in expected diagnostic: ${propName}`);
|
|
}
|
|
}
|
|
|
|
if (diagnosticProps.length === 0) {
|
|
throw new Error(`No valid properties found in diagnostic object`);
|
|
}
|
|
|
|
return `&lsproto.Diagnostic{\n${diagnosticProps.join("\n")}\n}`;
|
|
}
|
|
|
|
function parseNumberOfErrorsInCurrentFile(args: readonly ts.Expression[]): [VerifyNumberOfErrorsInCurrentFileCmd] {
|
|
let arg0;
|
|
if (args.length !== 1 || !(arg0 = getNumericLiteral(args[0]))) {
|
|
throw new Error(`Expected a single numeric literal argument in verify.numberOfErrorsInCurrentFile, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
return [{
|
|
kind: "verifyNumberOfErrorsInCurrentFile",
|
|
expectedCount: parseInt(arg0.text, 10),
|
|
}];
|
|
}
|
|
|
|
function parseErrorExistsAtRange(args: readonly ts.Expression[]): [VerifyErrorExistsAtRangeCmd] {
|
|
if (args.length < 2 || args.length > 3) {
|
|
throw new Error(`Expected 2 or 3 arguments in verify.errorExistsAtRange, got ${args.length}`);
|
|
}
|
|
|
|
// First arg is a range
|
|
const rangeArg = parseBaselineMarkerOrRangeArg(args[0]);
|
|
|
|
// Second arg is error code
|
|
let codeArg;
|
|
if (!(codeArg = getNumericLiteral(args[1]))) {
|
|
throw new Error(`Expected numeric literal for code in verify.errorExistsAtRange, got ${args[1].getText()}`);
|
|
}
|
|
|
|
// Third arg is optional message
|
|
let message = "";
|
|
if (args[2]) {
|
|
const messageArg = getStringLiteralLike(args[2]);
|
|
if (!messageArg) {
|
|
throw new Error(`Expected string literal for message in verify.errorExistsAtRange, got ${args[2].getText()}`);
|
|
}
|
|
message = messageArg.text;
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyErrorExistsAtRange",
|
|
range: rangeArg,
|
|
code: parseInt(codeArg.text, 10),
|
|
message: message,
|
|
}];
|
|
}
|
|
|
|
function parseCurrentLineContentIs(args: readonly ts.Expression[]): [VerifyCurrentLineContentIsCmd] {
|
|
let arg0;
|
|
if (args.length !== 1 || !(arg0 = getStringLiteralLike(args[0]))) {
|
|
throw new Error(`Expected a single string literal argument in verify.currentLineContentIs, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
return [{
|
|
kind: "verifyCurrentLineContentIs",
|
|
text: arg0.text,
|
|
}];
|
|
}
|
|
|
|
function parseCurrentFileContentIs(args: readonly ts.Expression[]): [VerifyCurrentFileContentIsCmd] {
|
|
let arg0;
|
|
if (args.length !== 1 || !(arg0 = getStringLiteralLike(args[0]))) {
|
|
throw new Error(`Expected a single string literal argument in verify.currentFileContentIs, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
return [{
|
|
kind: "verifyCurrentFileContentIs",
|
|
text: arg0.text,
|
|
}];
|
|
}
|
|
|
|
function parseErrorExistsBetweenMarkers(args: readonly ts.Expression[]): [VerifyErrorExistsBetweenMarkersCmd] {
|
|
if (args.length !== 2) {
|
|
throw new Error(`Expected 2 arguments in verify.errorExistsBetweenMarkers, got ${args.length}`);
|
|
}
|
|
let startMarker, endMarker;
|
|
if (!(startMarker = getStringLiteralLike(args[0])) || !(endMarker = getStringLiteralLike(args[1]))) {
|
|
throw new Error(`Expected string literal arguments in verify.errorExistsBetweenMarkers, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
return [{
|
|
kind: "verifyErrorExistsBetweenMarkers",
|
|
startMarker: startMarker.text,
|
|
endMarker: endMarker.text,
|
|
}];
|
|
}
|
|
|
|
function parseErrorExistsAfterMarker(args: readonly ts.Expression[]): [VerifyErrorExistsAfterMarkerCmd] {
|
|
let markerName = "";
|
|
if (args.length > 0) {
|
|
const arg0 = getStringLiteralLike(args[0]);
|
|
if (!arg0) {
|
|
throw new Error(`Expected string literal argument in verify.errorExistsAfterMarker, got ${args[0].getText()}`);
|
|
}
|
|
markerName = arg0.text;
|
|
}
|
|
return [{
|
|
kind: "verifyErrorExistsAfterMarker",
|
|
markerName: markerName,
|
|
}];
|
|
}
|
|
|
|
function parseErrorExistsBeforeMarker(args: readonly ts.Expression[]): [VerifyErrorExistsBeforeMarkerCmd] {
|
|
let markerName = "";
|
|
if (args.length > 0) {
|
|
const arg0 = getStringLiteralLike(args[0]);
|
|
if (!arg0) {
|
|
throw new Error(`Expected string literal argument in verify.errorExistsBeforeMarker, got ${args[0].getText()}`);
|
|
}
|
|
markerName = arg0.text;
|
|
}
|
|
return [{
|
|
kind: "verifyErrorExistsBeforeMarker",
|
|
markerName: markerName,
|
|
}];
|
|
}
|
|
|
|
function parseCodeFixArgs(args: readonly ts.Expression[]): [VerifyCodeFixCmd] {
|
|
if (args.length !== 1) {
|
|
throw new Error(`Expected 1 argument in verify.codeFix, got ${args.length}`);
|
|
}
|
|
const obj = getObjectLiteralExpression(args[0]);
|
|
if (!obj) {
|
|
throw new Error(`Expected object literal in verify.codeFix, got ${args[0].getText()}`);
|
|
}
|
|
|
|
const sourceFile = args[0].getSourceFile();
|
|
let description = "";
|
|
let newFileContent: string | undefined;
|
|
let newRangeContent: string | undefined;
|
|
let index = 0;
|
|
let applyChanges = false;
|
|
let preferences = "nil /*preferences*/";
|
|
|
|
for (const prop of obj.properties) {
|
|
const name = getPropertyName(prop);
|
|
if (!name) continue;
|
|
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
if (name === "description") {
|
|
const resolved = resolveDescriptionExpression(prop.name, sourceFile);
|
|
if (resolved) description = resolved;
|
|
}
|
|
continue;
|
|
}
|
|
if (!ts.isPropertyAssignment(prop)) continue;
|
|
switch (name) {
|
|
case "description": {
|
|
const resolved = resolveDescriptionExpression(prop.initializer, sourceFile);
|
|
if (resolved) description = resolved;
|
|
break;
|
|
}
|
|
case "newFileContent": {
|
|
const str = getStringLiteralLike(prop.initializer);
|
|
if (str) newFileContent = str.text;
|
|
break;
|
|
}
|
|
case "newRangeContent": {
|
|
const str = getStringLiteralLike(prop.initializer);
|
|
if (str) newRangeContent = str.text;
|
|
break;
|
|
}
|
|
case "index": {
|
|
const num = getNumericLiteral(prop.initializer);
|
|
if (num) index = parseInt(num.text);
|
|
break;
|
|
}
|
|
case "applyChanges": {
|
|
if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
applyChanges = true;
|
|
}
|
|
break;
|
|
}
|
|
case "preferences": {
|
|
const prefs = getObjectLiteralExpression(prop.initializer);
|
|
if (!prefs) {
|
|
throw new Error(`Expected object literal for preferences in verify.codeFix, got ${prop.initializer.getText()}`);
|
|
}
|
|
preferences = parseUserPreferences(prefs);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyCodeFix",
|
|
description,
|
|
newFileContent,
|
|
newRangeContent,
|
|
index,
|
|
applyChanges,
|
|
preferences,
|
|
}];
|
|
}
|
|
|
|
function parseCodeFixAvailableArgs(funcName: string, args: readonly ts.Expression[]): [VerifyCodeFixAvailableCmd] {
|
|
switch (funcName) {
|
|
case "codeFixAvailable": {
|
|
const descriptions: string[] = [];
|
|
let expectNone = false;
|
|
|
|
if (args.length === 1) {
|
|
const sourceFile = args[0].getSourceFile();
|
|
const arrayArg = getArrayLiteralExpression(args[0]);
|
|
if (arrayArg) {
|
|
if (arrayArg.elements.length === 0) {
|
|
expectNone = true;
|
|
}
|
|
for (const elem of arrayArg.elements) {
|
|
const obj = getObjectLiteralExpression(elem);
|
|
if (obj) {
|
|
for (const prop of obj.properties) {
|
|
if (getPropertyName(prop) === "description") {
|
|
let resolved: string | undefined;
|
|
if (ts.isPropertyAssignment(prop)) {
|
|
resolved = resolveDescriptionExpression(prop.initializer, sourceFile);
|
|
}
|
|
else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
resolved = resolveDescriptionExpression(prop.name, sourceFile);
|
|
}
|
|
if (resolved) {
|
|
descriptions.push(resolved);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return [{
|
|
kind: "verifyCodeFixAvailable",
|
|
descriptions,
|
|
unavailableDescriptions: [],
|
|
expectNone,
|
|
}];
|
|
}
|
|
case "notCodeFixAvailable":
|
|
if (args.length === 0) {
|
|
return [{
|
|
kind: "verifyCodeFixAvailable",
|
|
descriptions: [],
|
|
unavailableDescriptions: [],
|
|
expectNone: true,
|
|
}];
|
|
}
|
|
if (args.length === 1) {
|
|
const [descriptionExpression] = args;
|
|
const description = resolveDescriptionExpression(descriptionExpression, descriptionExpression.getSourceFile());
|
|
if (!description) {
|
|
throw new Error(`Unsupported argument in verify.not.codeFixAvailable: ${descriptionExpression.getText()}`);
|
|
}
|
|
return [{
|
|
kind: "verifyCodeFixAvailable",
|
|
descriptions: [],
|
|
unavailableDescriptions: [description],
|
|
expectNone: false,
|
|
}];
|
|
}
|
|
throw new Error(`Expected 0 or 1 arguments in verify.not.codeFixAvailable, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
default:
|
|
throw new Error(`Unrecognized codeFixAvailable function: ${funcName}`);
|
|
}
|
|
}
|
|
|
|
function parseRangeAfterCodeFixArgs(args: readonly ts.Expression[]): [VerifyRangeAfterCodeFixCmd] {
|
|
const [expectedTextArg, includeWhiteSpaceArg, errorCodeArg, indexArg] = args;
|
|
const expectedText = expectedTextArg && getStringLiteralLike(expectedTextArg);
|
|
if (!expectedText) {
|
|
throw new Error(`Expected string literal argument in verify.rangeAfterCodeFix, got ${expectedTextArg?.getText()}`);
|
|
}
|
|
|
|
let includeWhiteSpace = false;
|
|
if (includeWhiteSpaceArg !== undefined && includeWhiteSpaceArg.kind !== ts.SyntaxKind.UndefinedKeyword) {
|
|
includeWhiteSpace = includeWhiteSpaceArg.kind === ts.SyntaxKind.TrueKeyword;
|
|
}
|
|
|
|
let errorCode = 0;
|
|
if (errorCodeArg !== undefined && errorCodeArg.kind !== ts.SyntaxKind.UndefinedKeyword) {
|
|
const parsedErrorCode = getNumericLiteral(errorCodeArg);
|
|
if (!parsedErrorCode) {
|
|
throw new Error(`Expected numeric literal errorCode in verify.rangeAfterCodeFix, got ${errorCodeArg.getText()}`);
|
|
}
|
|
errorCode = parseInt(parsedErrorCode.text);
|
|
}
|
|
|
|
let index = 0;
|
|
if (indexArg !== undefined && indexArg.kind !== ts.SyntaxKind.UndefinedKeyword) {
|
|
const parsedIndex = getNumericLiteral(indexArg);
|
|
if (!parsedIndex) {
|
|
throw new Error(`Expected numeric literal index in verify.rangeAfterCodeFix, got ${indexArg.getText()}`);
|
|
}
|
|
index = parseInt(parsedIndex.text);
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyRangeAfterCodeFix",
|
|
expectedText: expectedText.text,
|
|
includeWhiteSpace,
|
|
errorCode,
|
|
index,
|
|
}];
|
|
}
|
|
|
|
function parseCodeFixAllArgs(args: readonly ts.Expression[]): [VerifyCodeFixAllCmd] {
|
|
if (args.length !== 1) {
|
|
throw new Error(`Expected 1 argument in verify.codeFixAll, got ${args.length}`);
|
|
}
|
|
const obj = getObjectLiteralExpression(args[0]);
|
|
if (!obj) {
|
|
throw new Error(`Expected object literal in verify.codeFixAll, got ${args[0].getText()}`);
|
|
}
|
|
|
|
let fixId = "";
|
|
let newFileContent = "";
|
|
|
|
for (const prop of obj.properties) {
|
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
|
|
switch (prop.name.text) {
|
|
case "fixId": {
|
|
const str = getStringLiteralLike(prop.initializer);
|
|
if (str) fixId = str.text;
|
|
break;
|
|
}
|
|
case "newFileContent": {
|
|
const str = getStringLiteralLike(prop.initializer);
|
|
if (str) newFileContent = str.text;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyCodeFixAll",
|
|
fixId,
|
|
newFileContent,
|
|
}];
|
|
}
|
|
|
|
function stringToTristate(s: string): string {
|
|
switch (s) {
|
|
case "true":
|
|
return "core.TSTrue";
|
|
case "false":
|
|
return "core.TSFalse";
|
|
default:
|
|
return "core.TSUnknown";
|
|
}
|
|
}
|
|
|
|
function parseUserPreferences(arg: ts.ObjectLiteralExpression): string {
|
|
const inlayHintPreferences: string[] = [];
|
|
const moduleSpecifierPreferences: string[] = [];
|
|
const preferences: string[] = [];
|
|
for (const prop of arg.properties) {
|
|
if (ts.isPropertyAssignment(prop)) {
|
|
switch (prop.name.getText()) {
|
|
// !!! other preferences
|
|
case "providePrefixAndSuffixTextForRename":
|
|
preferences.push(`UseAliasesForRename: ${stringToTristate(prop.initializer.getText())}`);
|
|
break;
|
|
case "quotePreference":
|
|
if (!ts.isStringLiteralLike(prop.initializer)) {
|
|
throw new Error(`Expected string literal for quotePreference, got ${prop.initializer.getText()}`);
|
|
}
|
|
preferences.push(`QuotePreference: lsutil.QuotePreference(${getGoStringLiteral(prop.initializer.text)})`);
|
|
break;
|
|
case "autoImportSpecifierExcludeRegexes":
|
|
const regexArrayArg = getArrayLiteralExpression(prop.initializer);
|
|
if (!regexArrayArg) {
|
|
throw new Error(`Expected array literal for autoImportSpecifierExcludeRegexes, got ${prop.initializer.getText()}`);
|
|
}
|
|
const regexes: string[] = [];
|
|
for (const elem of regexArrayArg.elements) {
|
|
const strElem = getStringLiteralLike(elem);
|
|
if (!strElem) {
|
|
throw new Error(`Expected string literal in autoImportSpecifierExcludeRegexes array, got ${elem.getText()}`);
|
|
}
|
|
regexes.push(getGoStringLiteral(strElem.text));
|
|
}
|
|
moduleSpecifierPreferences.push(`AutoImportSpecifierExcludeRegexes: []string{${regexes.join(", ")}}`);
|
|
break;
|
|
case "importModuleSpecifierPreference":
|
|
if (!ts.isStringLiteralLike(prop.initializer)) {
|
|
throw new Error(`Expected string literal for importModuleSpecifierPreference, got ${prop.initializer.getText()}`);
|
|
}
|
|
moduleSpecifierPreferences.push(`ImportModuleSpecifierPreference: ${prop.initializer.getText()}`);
|
|
break;
|
|
case "importModuleSpecifierEnding":
|
|
if (!ts.isStringLiteralLike(prop.initializer)) {
|
|
throw new Error(`Expected string literal for importModuleSpecifierEnding, got ${prop.initializer.getText()}`);
|
|
}
|
|
moduleSpecifierPreferences.push(`ImportModuleSpecifierEnding: ${prop.initializer.getText()}`);
|
|
break;
|
|
case "allowRenameOfImportPath":
|
|
preferences.push(`AllowRenameOfImportPath: ${stringToTristate(prop.initializer.getText())}`);
|
|
break;
|
|
case "preferTypeOnlyAutoImports":
|
|
preferences.push(`PreferTypeOnlyAutoImports: ${stringToTristate(prop.initializer.getText())}`);
|
|
break;
|
|
case "organizeImportsTypeOrder":
|
|
if (!ts.isStringLiteralLike(prop.initializer)) {
|
|
throw new Error(`Expected string literal for organizeImportsTypeOrder, got ${prop.initializer.getText()}`);
|
|
}
|
|
switch (prop.initializer.text) {
|
|
case "last":
|
|
preferences.push(`OrganizeImportsTypeOrder: lsutil.OrganizeImportsTypeOrderLast`);
|
|
break;
|
|
case "inline":
|
|
preferences.push(`OrganizeImportsTypeOrder: lsutil.OrganizeImportsTypeOrderInline`);
|
|
break;
|
|
case "first":
|
|
preferences.push(`OrganizeImportsTypeOrder: lsutil.OrganizeImportsTypeOrderFirst`);
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported organizeImportsTypeOrder value: ${prop.initializer.text}`);
|
|
}
|
|
break;
|
|
case "autoImportFileExcludePatterns":
|
|
const arrayArg = getArrayLiteralExpression(prop.initializer);
|
|
if (!arrayArg) {
|
|
throw new Error(`Expected array literal for autoImportFileExcludePatterns, got ${prop.initializer.getText()}`);
|
|
}
|
|
const patterns: string[] = [];
|
|
for (const elem of arrayArg.elements) {
|
|
const strElem = getStringLiteralLike(elem);
|
|
if (!strElem) {
|
|
throw new Error(`Expected string literal in autoImportFileExcludePatterns array, got ${elem.getText()}`);
|
|
}
|
|
patterns.push(getGoStringLiteral(strElem.text));
|
|
}
|
|
preferences.push(`AutoImportFileExcludePatterns: []string{${patterns.join(", ")}}`);
|
|
break;
|
|
case "includeInlayParameterNameHints":
|
|
let paramHint;
|
|
if (!ts.isStringLiteralLike(prop.initializer)) {
|
|
throw new Error(`Expected string literal for includeInlayParameterNameHints, got ${prop.initializer.getText()}`);
|
|
}
|
|
switch (prop.initializer.text) {
|
|
case "none":
|
|
paramHint = "lsutil.IncludeInlayParameterNameHintsNone";
|
|
break;
|
|
case "literals":
|
|
paramHint = "lsutil.IncludeInlayParameterNameHintsLiterals";
|
|
break;
|
|
case "all":
|
|
paramHint = "lsutil.IncludeInlayParameterNameHintsAll";
|
|
break;
|
|
}
|
|
inlayHintPreferences.push(`IncludeInlayParameterNameHints: ${paramHint}`);
|
|
break;
|
|
case "includeInlayParameterNameHintsWhenArgumentMatchesName":
|
|
inlayHintPreferences.push(`IncludeInlayParameterNameHintsWhenArgumentMatchesName: ${stringToTristate(prop.initializer.getText())}`);
|
|
break;
|
|
case "includeInlayFunctionParameterTypeHints":
|
|
inlayHintPreferences.push(`IncludeInlayFunctionParameterTypeHints: ${stringToTristate(prop.initializer.getText())}`);
|
|
break;
|
|
case "includeInlayVariableTypeHints":
|
|
inlayHintPreferences.push(`IncludeInlayVariableTypeHints: ${stringToTristate(prop.initializer.getText())}`);
|
|
break;
|
|
case "includeInlayVariableTypeHintsWhenTypeMatchesName":
|
|
inlayHintPreferences.push(`IncludeInlayVariableTypeHintsWhenTypeMatchesName: ${stringToTristate(prop.initializer.getText())}`);
|
|
break;
|
|
case "includeInlayPropertyDeclarationTypeHints":
|
|
inlayHintPreferences.push(`IncludeInlayPropertyDeclarationTypeHints: ${stringToTristate(prop.initializer.getText())}`);
|
|
break;
|
|
case "includeInlayFunctionLikeReturnTypeHints":
|
|
inlayHintPreferences.push(`IncludeInlayFunctionLikeReturnTypeHints: ${stringToTristate(prop.initializer.getText())}`);
|
|
break;
|
|
case "includeInlayEnumMemberValueHints":
|
|
inlayHintPreferences.push(`IncludeInlayEnumMemberValueHints: ${stringToTristate(prop.initializer.getText())}`);
|
|
break;
|
|
case "interactiveInlayHints":
|
|
// Ignore, deprecated
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(`Expected property assignment in user preferences object, got ${prop.getText()}`);
|
|
}
|
|
}
|
|
|
|
if (inlayHintPreferences.length > 0) {
|
|
preferences.push(`InlayHints: lsutil.InlayHintsPreferences{${inlayHintPreferences.join(",")}}`);
|
|
}
|
|
if (moduleSpecifierPreferences.length > 0) {
|
|
preferences.push(...moduleSpecifierPreferences);
|
|
}
|
|
if (preferences.length === 0) {
|
|
return "nil /*preferences*/";
|
|
}
|
|
return `&lsutil.UserPreferences{${preferences.join(",")}}`;
|
|
}
|
|
|
|
function parseBaselineMarkerOrRangeArg(arg: ts.Expression): string {
|
|
if (ts.isStringLiteral(arg)) {
|
|
return getGoStringLiteral(arg.text);
|
|
}
|
|
else if (ts.isIdentifier(arg) || (ts.isElementAccessExpression(arg) && ts.isIdentifier(arg.expression))) {
|
|
const result = parseRangeVariable(arg);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
const init = getNodeOfKind(arg, ts.isCallExpression);
|
|
if (init) {
|
|
const result = getRangesByTextArg(init);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
else if (ts.isElementAccessExpression(arg) && ts.isCallExpression(arg.expression) && arg.expression.getText().includes("ranges")) {
|
|
// `test.ranges()[n]`
|
|
const index = arg.argumentExpression?.getText();
|
|
if (index !== undefined) {
|
|
return `f.Ranges()[${index}]`;
|
|
}
|
|
}
|
|
else if (ts.isCallExpression(arg)) {
|
|
const result = getRangesByTextArg(arg);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
// Handle `.filter(r => !(r.marker && r.marker.data.KEY))` patterns
|
|
const filterResult = parseFilterExpression(arg);
|
|
if (filterResult) {
|
|
return filterResult;
|
|
}
|
|
}
|
|
if (arg.getText() === "test.markers()") {
|
|
return "ToAny(f.Markers())...";
|
|
}
|
|
throw new Error(`Unrecognized marker or range argument: ${arg.getText()}`);
|
|
}
|
|
|
|
function parseRangeVariable(arg: ts.Identifier | ts.ElementAccessExpression): string | undefined {
|
|
const argName = ts.isIdentifier(arg) ? arg.text : (arg.expression as ts.Identifier).text;
|
|
const file = arg.getSourceFile();
|
|
const varStmts = file.statements.filter(ts.isVariableStatement);
|
|
for (const varStmt of varStmts) {
|
|
for (const decl of varStmt.declarationList.declarations) {
|
|
if (ts.isArrayBindingPattern(decl.name) && decl.initializer) {
|
|
// Resolve the initializer to a Go expression for the source array
|
|
const sourceExpr = resolveRangesExpression(decl.initializer, varStmts);
|
|
if (!sourceExpr) continue;
|
|
for (let i = 0; i < decl.name.elements.length; i++) {
|
|
const elem = decl.name.elements[i];
|
|
if (ts.isBindingElement(elem) && ts.isIdentifier(elem.name) && elem.name.text === argName) {
|
|
if (elem.dotDotDotToken === undefined) {
|
|
return `${sourceExpr}[${i}]`;
|
|
}
|
|
if (ts.isElementAccessExpression(arg)) {
|
|
return `${sourceExpr}[${i + parseInt(arg.argumentExpression!.getText())}]`;
|
|
}
|
|
return `ToAny(${sourceExpr}[${i}:])...`;
|
|
}
|
|
}
|
|
}
|
|
// `const ranges = test.ranges();` and arg is `ranges[n]`
|
|
if (ts.isIdentifier(decl.name) && decl.name.text === argName && decl.initializer?.getText().includes("ranges")) {
|
|
if (ts.isElementAccessExpression(arg)) {
|
|
return `f.Ranges()[${arg.argumentExpression!.getText()}]`;
|
|
}
|
|
// `const range = test.ranges()[0]` used directly as `range`
|
|
if (ts.isIdentifier(arg) && ts.isElementAccessExpression(decl.initializer) && ts.isCallExpression(decl.initializer.expression) && decl.initializer.argumentExpression) {
|
|
return `f.Ranges()[${decl.initializer.argumentExpression.getText()}]`;
|
|
}
|
|
}
|
|
// `const cRanges = ranges.get("C")` or `const cRanges = test.rangesByText().get("C")`
|
|
if (ts.isIdentifier(decl.name) && decl.name.text === argName && decl.initializer && ts.isCallExpression(decl.initializer)) {
|
|
const initText = decl.initializer.getText();
|
|
if (initText.includes("rangesByText") || (ts.isPropertyAccessExpression(decl.initializer.expression) && decl.initializer.expression.name.text === "get")) {
|
|
// Try to find the .get("text") argument
|
|
const getCall = decl.initializer;
|
|
if (getCall.arguments.length === 1 && ts.isStringLiteralLike(getCall.arguments[0])) {
|
|
return `ToAny(f.GetRangesByText().Get(${getGoStringLiteral(getCall.arguments[0].text)}))...`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Resolves a range initializer expression to a Go expression.
|
|
* Handles `test.ranges()`, `test.rangesByText().get("X")`, and variable references to those.
|
|
*/
|
|
function resolveRangesExpression(expr: ts.Expression, varStmts: ts.VariableStatement[]): string | undefined {
|
|
const text = expr.getText();
|
|
if (text.includes("test.ranges()")) {
|
|
return "f.Ranges()";
|
|
}
|
|
if (text.includes("rangesByText()") && ts.isCallExpression(expr) && expr.arguments.length === 1 && ts.isStringLiteralLike(expr.arguments[0])) {
|
|
return `f.GetRangesByText().Get(${getGoStringLiteral(expr.arguments[0].text)})`;
|
|
}
|
|
// Handle `someVar.get("text")` where someVar resolves to rangesByText()
|
|
if (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && expr.expression.name.text === "get" && expr.arguments.length === 1 && ts.isStringLiteralLike(expr.arguments[0])) {
|
|
const obj = expr.expression.expression;
|
|
if (ts.isIdentifier(obj)) {
|
|
const resolved = resolveIdentifier(obj, varStmts);
|
|
if (resolved?.includes("rangesByText")) {
|
|
return `f.GetRangesByText().Get(${getGoStringLiteral(expr.arguments[0].text)})`;
|
|
}
|
|
}
|
|
}
|
|
// It might be a variable reference like `const [d0, d1] = dRanges;`
|
|
if (ts.isIdentifier(expr)) {
|
|
for (const varStmt of varStmts) {
|
|
for (const decl of varStmt.declarationList.declarations) {
|
|
if (ts.isIdentifier(decl.name) && decl.name.text === expr.text && decl.initializer) {
|
|
return resolveRangesExpression(decl.initializer, varStmts);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function resolveIdentifier(id: ts.Identifier, varStmts: ts.VariableStatement[]): string | undefined {
|
|
for (const varStmt of varStmts) {
|
|
for (const decl of varStmt.declarationList.declarations) {
|
|
if (ts.isIdentifier(decl.name) && decl.name.text === id.text && decl.initializer) {
|
|
return decl.initializer.getText();
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function getRangesByTextArg(arg: ts.CallExpression): string | undefined {
|
|
if (arg.getText().startsWith("test.rangesByText()")) {
|
|
if (ts.isStringLiteralLike(arg.arguments[0])) {
|
|
return `ToAny(f.GetRangesByText().Get(${getGoStringLiteral(arg.arguments[0].text)}))...`;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Handles `.filter(r => !(r.marker && r.marker.data.KEY))` patterns on range arrays.
|
|
* Converts to `ToAny(core.Filter(source, func(r *fourslash.RangeMarker) bool { ... }))...`
|
|
*/
|
|
function parseFilterExpression(arg: ts.CallExpression): string | undefined {
|
|
if (!ts.isPropertyAccessExpression(arg.expression) || arg.expression.name.text !== "filter") {
|
|
return undefined;
|
|
}
|
|
if (arg.arguments.length !== 1) {
|
|
return undefined;
|
|
}
|
|
const filterArg = arg.arguments[0];
|
|
if (!ts.isArrowFunction(filterArg)) {
|
|
return undefined;
|
|
}
|
|
|
|
// Resolve the source (the thing being filtered)
|
|
const sourceExpr = arg.expression.expression;
|
|
let sourceGo: string | undefined;
|
|
|
|
if (ts.isIdentifier(sourceExpr)) {
|
|
const file = sourceExpr.getSourceFile();
|
|
const varStmts = file.statements.filter(ts.isVariableStatement);
|
|
sourceGo = resolveRangesExpression(sourceExpr, varStmts);
|
|
}
|
|
else if (ts.isCallExpression(sourceExpr)) {
|
|
// e.g. test.ranges().filter(...)
|
|
if (sourceExpr.getText().includes("test.ranges()")) {
|
|
sourceGo = "f.Ranges()";
|
|
}
|
|
}
|
|
|
|
if (!sourceGo) {
|
|
return undefined;
|
|
}
|
|
|
|
// Parse the filter body to generate a Go predicate
|
|
const predicate = parseFilterPredicate(filterArg);
|
|
if (!predicate) {
|
|
return undefined;
|
|
}
|
|
|
|
return `ToAny(core.Filter(${sourceGo}, ${predicate}))...`;
|
|
}
|
|
|
|
function parseFilterPredicate(arrow: ts.ArrowFunction): string | undefined {
|
|
// Handle `r => !(r.marker && r.marker.data.KEY)` → filter OUT ranges with marker.data.KEY
|
|
const body = arrow.body;
|
|
if (!ts.isExpression(body)) {
|
|
return undefined;
|
|
}
|
|
|
|
const paramName = arrow.parameters[0]?.name.getText();
|
|
if (!paramName) {
|
|
return undefined;
|
|
}
|
|
|
|
// Check for `!(r.marker && r.marker.data.KEY)` pattern
|
|
if (ts.isPrefixUnaryExpression(body) && body.operator === ts.SyntaxKind.ExclamationToken) {
|
|
const inner = ts.isParenthesizedExpression(body.operand) ? body.operand.expression : body.operand;
|
|
if (ts.isBinaryExpression(inner) && inner.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
// Right side should be `r.marker.data.KEY`
|
|
const right = inner.right;
|
|
if (ts.isPropertyAccessExpression(right) && right.expression.getText() === `${paramName}.marker.data`) {
|
|
const key = right.name.text;
|
|
return `func(r *fourslash.RangeMarker) bool { return r.Marker == nil || r.Marker.Data[${getGoStringLiteral(key)}] == nil }`;
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function parseBaselineQuickInfo(args: ts.NodeArray<ts.Expression>): VerifyBaselineQuickInfoCmd[] {
|
|
if (args.length === 0) {
|
|
return [{
|
|
kind: "verifyBaselineQuickInfo",
|
|
}];
|
|
}
|
|
// First arg is verbosityLevels: { markerName: number | number[] }
|
|
const verbosityArg = args[0];
|
|
if (!ts.isObjectLiteralExpression(verbosityArg)) {
|
|
throw new Error(`Expected object literal expression for verify.baselineQuickInfo argument, got ${verbosityArg.getText()}`);
|
|
}
|
|
const verbosityLevels: Record<string, number[]> = {};
|
|
for (const prop of verbosityArg.properties) {
|
|
if (!ts.isPropertyAssignment(prop)) {
|
|
throw new Error(`Expected property assignment in baselineQuickInfo verbosity levels, got ${prop.getText()}`);
|
|
}
|
|
let name: string;
|
|
if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
|
|
name = prop.name.text;
|
|
}
|
|
else if (ts.isNumericLiteral(prop.name)) {
|
|
name = prop.name.text;
|
|
}
|
|
else {
|
|
throw new Error(`Expected identifier, string, or numeric literal for property name in baselineQuickInfo verbosity levels, got ${prop.name.getText()}`);
|
|
}
|
|
if (ts.isArrayLiteralExpression(prop.initializer)) {
|
|
const levels: number[] = [];
|
|
for (const elem of prop.initializer.elements) {
|
|
if (!ts.isNumericLiteral(elem)) {
|
|
throw new Error(`Expected numeric literal in baselineQuickInfo verbosity levels array, got ${elem.getText()}`);
|
|
}
|
|
levels.push(Number(elem.text));
|
|
}
|
|
verbosityLevels[name] = levels;
|
|
}
|
|
else if (ts.isNumericLiteral(prop.initializer)) {
|
|
verbosityLevels[name] = [Number(prop.initializer.text)];
|
|
}
|
|
else {
|
|
throw new Error(`Expected numeric literal or array literal for baselineQuickInfo verbosity level, got ${prop.initializer.getText()}`);
|
|
}
|
|
}
|
|
return [{
|
|
kind: "verifyBaselineQuickInfo",
|
|
verbosityLevels,
|
|
}];
|
|
}
|
|
|
|
function parseQuickInfoArgs(funcName: string, args: readonly ts.Expression[]): VerifyQuickInfoCmd[] {
|
|
// We currently don't support 'expectedTags'.
|
|
switch (funcName) {
|
|
case "quickInfoAt": {
|
|
if (args.length < 1 || args.length > 3) {
|
|
throw new Error(`Expected 1 or 2 arguments in quickInfoIs, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
let arg0;
|
|
if (!(arg0 = getStringLiteralLike(args[0]))) {
|
|
throw new Error(`Expected string literal for first argument in quickInfoAt, got ${args[0].getText()}`);
|
|
}
|
|
const marker = getGoStringLiteral(arg0.text);
|
|
let text: string | undefined;
|
|
let arg1;
|
|
if (args[1]) {
|
|
if (!(arg1 = getStringLiteralLike(args[1]))) {
|
|
throw new Error(`Expected string literal for second argument in quickInfoAt, got ${args[1].getText()}`);
|
|
}
|
|
text = getGoStringLiteral(arg1.text);
|
|
}
|
|
let docs: string | undefined;
|
|
let arg2;
|
|
if (args[2]) {
|
|
if (!(arg2 = getStringLiteralLike(args[2])) && args[2].getText() !== "undefined") {
|
|
throw new Error(`Expected string literal or undefined for third argument in quickInfoAt, got ${args[2].getText()}`);
|
|
}
|
|
if (arg2) {
|
|
docs = getGoStringLiteral(arg2.text);
|
|
}
|
|
}
|
|
return [{
|
|
kind: "quickInfoAt",
|
|
marker,
|
|
text,
|
|
docs,
|
|
}];
|
|
}
|
|
case "quickInfos": {
|
|
const cmds: VerifyQuickInfoCmd[] = [];
|
|
let arg0;
|
|
if (args.length !== 1 || !(arg0 = getObjectLiteralExpression(args[0]))) {
|
|
throw new Error(`Expected a single object literal argument in quickInfos, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
for (const prop of arg0.properties) {
|
|
if (!ts.isPropertyAssignment(prop)) {
|
|
throw new Error(`Expected property assignment in quickInfos, got ${prop.getText()}`);
|
|
}
|
|
if (!(ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name) || ts.isNumericLiteral(prop.name))) {
|
|
throw new Error(`Expected identifier or literal for property name in quickInfos, got ${prop.name.getText()}`);
|
|
}
|
|
const marker = getGoStringLiteral(prop.name.text);
|
|
let text: string;
|
|
let docs: string | undefined;
|
|
let init;
|
|
if (init = getArrayLiteralExpression(prop.initializer)) {
|
|
if (init.elements.length !== 2) {
|
|
throw new Error(`Expected two elements in array literal for quickInfos property, got ${init.getText()}`);
|
|
}
|
|
let textExp, docsExp;
|
|
if (!(textExp = getStringLiteralLike(init.elements[0])) || !(docsExp = getStringLiteralLike(init.elements[1]))) {
|
|
throw new Error(`Expected string literals in array literal for quickInfos property, got ${init.getText()}`);
|
|
}
|
|
text = getGoStringLiteral(textExp.text);
|
|
docs = getGoStringLiteral(docsExp.text);
|
|
}
|
|
else if (init = getStringLiteralLike(prop.initializer)) {
|
|
text = getGoStringLiteral(init.text);
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal or array literal for quickInfos property, got ${prop.initializer.getText()}`);
|
|
}
|
|
cmds.push({
|
|
kind: "quickInfoAt",
|
|
marker,
|
|
text,
|
|
docs,
|
|
});
|
|
}
|
|
return cmds;
|
|
}
|
|
case "quickInfoExists":
|
|
return [{
|
|
kind: "quickInfoExists",
|
|
}];
|
|
case "notQuickInfoExists":
|
|
return [{
|
|
kind: "notQuickInfoExists",
|
|
}];
|
|
case "quickInfoIs": {
|
|
if (args.length < 1 || args.length > 2) {
|
|
throw new Error(`Expected 1 or 2 arguments in quickInfoIs, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
}
|
|
let arg0;
|
|
if (!(arg0 = getStringLiteralLike(args[0]))) {
|
|
throw new Error(`Expected string literal for first argument in quickInfoIs, got ${args[0].getText()}`);
|
|
}
|
|
const text = getGoStringLiteral(arg0.text);
|
|
let docs: string | undefined;
|
|
if (args[1]) {
|
|
let arg1;
|
|
if (!(arg1 = getStringLiteralLike(args[1]))) {
|
|
throw new Error(`Expected string literal for second argument in quickInfoIs, got ${args[1].getText()}`);
|
|
}
|
|
docs = getGoStringLiteral(arg1.text);
|
|
}
|
|
return [{
|
|
kind: "quickInfoIs",
|
|
text,
|
|
docs,
|
|
}];
|
|
}
|
|
}
|
|
throw new Error(`Unrecognized quick info function: ${funcName}`);
|
|
}
|
|
|
|
function parseOrganizeImportsArgs(args: readonly ts.Expression[]): [VerifyOrganizeImportsCmd] {
|
|
if (args.length < 1 || args.length > 3) {
|
|
throw new Error(`Expected 1-3 arguments in verify.organizeImports, got ${args.length}`);
|
|
}
|
|
|
|
const expectedContent = getStringLiteralLike(args[0]);
|
|
if (!expectedContent) {
|
|
throw new Error(`Expected string literal as first argument in verify.organizeImports, got ${args[0].getText()}`);
|
|
}
|
|
|
|
let mode = "lsproto.CodeActionKindSourceOrganizeImports";
|
|
if (args.length >= 2 && args[1].getText() !== "undefined") {
|
|
const modeExpr = args[1];
|
|
if (
|
|
ts.isPropertyAccessExpression(modeExpr) &&
|
|
modeExpr.expression.getText() === "ts.OrganizeImportsMode"
|
|
) {
|
|
const modeName = modeExpr.name.text;
|
|
switch (modeName) {
|
|
case "RemoveUnused":
|
|
mode = "lsproto.CodeActionKindSourceRemoveUnusedImports";
|
|
break;
|
|
case "SortAndCombine":
|
|
mode = "lsproto.CodeActionKindSourceSortImports";
|
|
break;
|
|
case "All":
|
|
mode = "lsproto.CodeActionKindSourceOrganizeImports";
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported organize imports mode: ${modeName}`);
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(`Unsupported organize imports mode: ${modeExpr.getText()}`);
|
|
}
|
|
}
|
|
|
|
let preferences = "nil";
|
|
if (args.length >= 3 && args[2].getText() !== "undefined") {
|
|
const prefsObj = getObjectLiteralExpression(args[2]);
|
|
if (!prefsObj) {
|
|
throw new Error(`Expected object literal for preferences in verify.organizeImports, got ${args[2].getText()}`);
|
|
}
|
|
|
|
const prefsFields: string[] = [];
|
|
for (const prop of prefsObj.properties) {
|
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
|
|
continue;
|
|
}
|
|
const propName = prop.name.text;
|
|
const propValue = prop.initializer;
|
|
|
|
const goFieldName = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
|
|
if (propName === "organizeImportsIgnoreCase") {
|
|
if (ts.isStringLiteral(propValue) && propValue.text === "auto") {
|
|
prefsFields.push(`${goFieldName}: core.TSUnknown`);
|
|
}
|
|
else if (propValue.kind === ts.SyntaxKind.TrueKeyword) {
|
|
prefsFields.push(`${goFieldName}: core.TSTrue`);
|
|
}
|
|
else if (propValue.kind === ts.SyntaxKind.FalseKeyword) {
|
|
prefsFields.push(`${goFieldName}: core.TSFalse`);
|
|
}
|
|
else {
|
|
throw new Error(`Unsupported value for organizeImportsIgnoreCase: ${propValue.getText()}`);
|
|
}
|
|
}
|
|
else if (propName === "organizeImportsSort") {
|
|
if (ts.isStringLiteral(propValue)) {
|
|
switch (propValue.text) {
|
|
case "auto":
|
|
prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortAuto`);
|
|
break;
|
|
case "ordinal":
|
|
prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortOrdinal`);
|
|
break;
|
|
case "ordinalIgnoreCase":
|
|
prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortOrdinalIgnoreCase`);
|
|
break;
|
|
case "natural":
|
|
prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortNatural`);
|
|
break;
|
|
case "naturalIgnoreCase":
|
|
prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortNaturalIgnoreCase`);
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported value for organizeImportsSort: ${propValue.text}`);
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal for organizeImportsSort, got ${propValue.getText()}`);
|
|
}
|
|
}
|
|
else if (propName === "organizeImportsCollation") {
|
|
if (ts.isStringLiteral(propValue)) {
|
|
if (propValue.text === "unicode") {
|
|
prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsCollationUnicode`);
|
|
}
|
|
else if (propValue.text === "ordinal") {
|
|
prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsCollationOrdinal`);
|
|
}
|
|
else {
|
|
throw new Error(`Unsupported value for organizeImportsCollation: ${propValue.text}`);
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal for organizeImportsCollation, got ${propValue.getText()}`);
|
|
}
|
|
}
|
|
else if (propName === "organizeImportsCaseFirst") {
|
|
if (ts.isStringLiteral(propValue)) {
|
|
if (propValue.text === "upper") {
|
|
prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsCaseFirstUpper`);
|
|
}
|
|
else if (propValue.text === "lower") {
|
|
prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsCaseFirstLower`);
|
|
}
|
|
else {
|
|
throw new Error(`Unsupported value for organizeImportsCaseFirst: ${propValue.text}`);
|
|
}
|
|
}
|
|
else if (propValue.kind === ts.SyntaxKind.FalseKeyword) {
|
|
prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsCaseFirstFalse`);
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal or false for organizeImportsCaseFirst, got ${propValue.getText()}`);
|
|
}
|
|
}
|
|
else if (propName === "organizeImportsTypeOrder") {
|
|
if (ts.isStringLiteral(propValue)) {
|
|
const typeOrderValue = propValue.text;
|
|
switch (typeOrderValue) {
|
|
case "last":
|
|
prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsTypeOrderLast`);
|
|
break;
|
|
case "inline":
|
|
prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsTypeOrderInline`);
|
|
break;
|
|
case "first":
|
|
prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsTypeOrderFirst`);
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported value for organizeImportsTypeOrder: ${typeOrderValue}`);
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal for organizeImportsTypeOrder, got ${propValue.getText()}`);
|
|
}
|
|
}
|
|
// Boolean fields that are now Tristate
|
|
else if (propName === "organizeImportsNumericCollation" || propName === "organizeImportsAccentCollation") {
|
|
if (propValue.kind === ts.SyntaxKind.TrueKeyword) {
|
|
prefsFields.push(`${goFieldName}: core.TSTrue`);
|
|
}
|
|
else if (propValue.kind === ts.SyntaxKind.FalseKeyword) {
|
|
prefsFields.push(`${goFieldName}: core.TSFalse`);
|
|
}
|
|
else {
|
|
throw new Error(`Expected boolean for ${propName}, got ${propValue.getText()}`);
|
|
}
|
|
}
|
|
// organizeImportsLocale is a plain string, not a pointer
|
|
else if (propName === "organizeImportsLocale") {
|
|
if (ts.isStringLiteral(propValue)) {
|
|
prefsFields.push(`${goFieldName}: ${getGoStringLiteral(propValue.text)}`);
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal for organizeImportsLocale, got ${propValue.getText()}`);
|
|
}
|
|
}
|
|
// Default handling for other string properties
|
|
else if (ts.isStringLiteral(propValue)) {
|
|
prefsFields.push(`${goFieldName}: new(${getGoStringLiteral(propValue.text)})`);
|
|
}
|
|
else if (propValue.kind === ts.SyntaxKind.TrueKeyword) {
|
|
prefsFields.push(`${goFieldName}: core.TSTrue`);
|
|
}
|
|
else if (propValue.kind === ts.SyntaxKind.FalseKeyword) {
|
|
prefsFields.push(`${goFieldName}: core.TSFalse`);
|
|
}
|
|
else {
|
|
prefsFields.push(`${goFieldName}: ${propValue.getText()}`);
|
|
}
|
|
}
|
|
|
|
if (prefsFields.length > 0) {
|
|
preferences = `&lsutil.UserPreferences{\n${prefsFields.join(",\n")},\n}`;
|
|
}
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyOrganizeImports",
|
|
expectedContent: expectedContent.text,
|
|
mode,
|
|
preferences,
|
|
}];
|
|
}
|
|
|
|
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 parseSignatureHelpOptions(obj: ts.ObjectLiteralExpression): VerifySignatureHelpOptions {
|
|
const options: VerifySignatureHelpOptions = {};
|
|
|
|
for (const prop of obj.properties) {
|
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
|
|
console.error(`Unexpected property in signatureHelp options: ${prop.getText()}`);
|
|
continue;
|
|
}
|
|
const name = prop.name.text;
|
|
const value = prop.initializer;
|
|
|
|
switch (name) {
|
|
case "marker": {
|
|
if (ts.isStringLiteral(value)) {
|
|
options.marker = value.text;
|
|
}
|
|
else if (ts.isArrayLiteralExpression(value)) {
|
|
const markers: string[] = [];
|
|
for (const elem of value.elements) {
|
|
if (ts.isStringLiteral(elem)) {
|
|
markers.push(elem.text);
|
|
}
|
|
else {
|
|
throw new Error(`Expected string literal in marker array, got ${elem.getText()}`);
|
|
}
|
|
}
|
|
options.marker = markers;
|
|
}
|
|
else {
|
|
throw new Error(`Expected string or array for marker, got ${value.getText()}`);
|
|
}
|
|
break;
|
|
}
|
|
case "text": {
|
|
const str = getStringLiteralLike(value);
|
|
if (!str) {
|
|
throw new Error(`Expected string for text, got ${value.getText()}`);
|
|
}
|
|
options.text = str.text;
|
|
break;
|
|
}
|
|
case "docComment": {
|
|
const str = getStringLiteralLike(value);
|
|
if (!str) {
|
|
throw new Error(`Expected string for docComment, got ${value.getText()}`);
|
|
}
|
|
options.docComment = str.text;
|
|
break;
|
|
}
|
|
case "parameterCount": {
|
|
const num = getNumericLiteral(value);
|
|
if (!num) {
|
|
throw new Error(`Expected number for parameterCount, got ${value.getText()}`);
|
|
}
|
|
options.parameterCount = parseInt(num.text, 10);
|
|
break;
|
|
}
|
|
case "parameterName": {
|
|
const str = getStringLiteralLike(value);
|
|
if (!str) {
|
|
throw new Error(`Expected string for parameterName, got ${value.getText()}`);
|
|
}
|
|
options.parameterName = str.text;
|
|
break;
|
|
}
|
|
case "parameterSpan": {
|
|
const str = getStringLiteralLike(value);
|
|
if (!str) {
|
|
throw new Error(`Expected string for parameterSpan, got ${value.getText()}`);
|
|
}
|
|
options.parameterSpan = str.text;
|
|
break;
|
|
}
|
|
case "parameterDocComment": {
|
|
const str = getStringLiteralLike(value);
|
|
if (!str) {
|
|
throw new Error(`Expected string for parameterDocComment, got ${value.getText()}`);
|
|
}
|
|
options.parameterDocComment = str.text;
|
|
break;
|
|
}
|
|
case "overloadsCount": {
|
|
const num = getNumericLiteral(value);
|
|
if (!num) {
|
|
throw new Error(`Expected number for overloadsCount, got ${value.getText()}`);
|
|
}
|
|
options.overloadsCount = parseInt(num.text, 10);
|
|
break;
|
|
}
|
|
case "overrideSelectedItemIndex": {
|
|
const num = getNumericLiteral(value);
|
|
if (!num) {
|
|
throw new Error(`Expected number for overrideSelectedItemIndex, got ${value.getText()}`);
|
|
}
|
|
options.overrideSelectedItemIndex = parseInt(num.text, 10);
|
|
break;
|
|
}
|
|
case "triggerReason": {
|
|
// triggerReason is an object like { kind: "invoked" } or { kind: "characterTyped", triggerCharacter: "(" }
|
|
// For now, just pass it through as a string representation
|
|
options.triggerReason = value.getText();
|
|
break;
|
|
}
|
|
case "argumentCount":
|
|
// ignore
|
|
break;
|
|
case "isVariadic": {
|
|
if (value.kind === ts.SyntaxKind.TrueKeyword) {
|
|
options.isVariadic = true;
|
|
}
|
|
else if (value.kind === ts.SyntaxKind.FalseKeyword) {
|
|
options.isVariadic = false;
|
|
}
|
|
else {
|
|
throw new Error(`Expected boolean for isVariadic, got ${value.getText()}`);
|
|
}
|
|
break;
|
|
}
|
|
case "tags":
|
|
// ignore
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown signatureHelp option: ${name}`);
|
|
}
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function parseSignatureHelp(args: ts.NodeArray<ts.Expression>): Cmd[] {
|
|
const allOptions: VerifySignatureHelpOptions[] = [];
|
|
|
|
for (const arg of args) {
|
|
if (ts.isObjectLiteralExpression(arg)) {
|
|
const opts = parseSignatureHelpOptions(arg);
|
|
allOptions.push(opts);
|
|
}
|
|
else if (ts.isIdentifier(arg)) {
|
|
// Could be a variable reference like `help2` - skip for now
|
|
throw new Error(`signatureHelp with variable reference not supported: ${arg.getText()}`);
|
|
}
|
|
else {
|
|
throw new Error(`Unexpected argument type in signatureHelp: ${arg.getText()}`);
|
|
}
|
|
}
|
|
|
|
if (allOptions.length === 0) {
|
|
throw new Error("signatureHelp requires at least one options object");
|
|
}
|
|
|
|
return [{
|
|
kind: "verifySignatureHelp",
|
|
options: allOptions,
|
|
}];
|
|
}
|
|
|
|
function parseNoSignatureHelp(args: ts.NodeArray<ts.Expression>): Cmd[] {
|
|
const markers: string[] = [];
|
|
|
|
for (const arg of args) {
|
|
if (ts.isStringLiteral(arg)) {
|
|
markers.push(arg.text);
|
|
}
|
|
else if (ts.isSpreadElement(arg)) {
|
|
// Handle ...test.markerNames()
|
|
const expr = arg.expression;
|
|
if (
|
|
ts.isCallExpression(expr) &&
|
|
ts.isPropertyAccessExpression(expr.expression) &&
|
|
ts.isIdentifier(expr.expression.expression) &&
|
|
expr.expression.expression.text === "test" &&
|
|
ts.isIdentifier(expr.expression.name) &&
|
|
expr.expression.name.text === "markerNames"
|
|
) {
|
|
// This means "all markers" - we'll handle this specially in the generator
|
|
return [{
|
|
kind: "verifyNoSignatureHelp",
|
|
markers: ["...test.markerNames()"],
|
|
}];
|
|
}
|
|
throw new Error(`Unsupported spread in noSignatureHelp: ${arg.getText()}`);
|
|
}
|
|
else {
|
|
throw new Error(`Unexpected argument in noSignatureHelp: ${arg.getText()}`);
|
|
}
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyNoSignatureHelp",
|
|
markers,
|
|
}];
|
|
}
|
|
|
|
interface SignatureHelpTriggerReason {
|
|
kind: "invoked" | "characterTyped" | "retrigger";
|
|
triggerCharacter?: string;
|
|
}
|
|
|
|
function parseTriggerReason(arg: ts.Expression): SignatureHelpTriggerReason | "undefined" {
|
|
// Handle undefined literal
|
|
if (ts.isIdentifier(arg) && arg.text === "undefined") {
|
|
return "undefined";
|
|
}
|
|
|
|
if (!ts.isObjectLiteralExpression(arg)) {
|
|
throw new Error(`Expected object literal for trigger reason, got ${arg.getText()}`);
|
|
}
|
|
|
|
let kind: "invoked" | "characterTyped" | "retrigger" | undefined;
|
|
let triggerCharacter: string | undefined;
|
|
|
|
for (const prop of arg.properties) {
|
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
|
|
throw new Error(`Unexpected property in trigger reason: ${prop.getText()}`);
|
|
}
|
|
const name = prop.name.text;
|
|
if (name === "kind") {
|
|
if (!ts.isStringLiteral(prop.initializer)) {
|
|
throw new Error(`Expected string literal for kind, got ${prop.initializer.getText()}`);
|
|
}
|
|
const k = prop.initializer.text;
|
|
if (k === "invoked" || k === "characterTyped" || k === "retrigger") {
|
|
kind = k;
|
|
}
|
|
else {
|
|
throw new Error(`Unknown trigger reason kind: ${k}`);
|
|
}
|
|
}
|
|
else if (name === "triggerCharacter") {
|
|
if (!ts.isStringLiteral(prop.initializer)) {
|
|
throw new Error(`Expected string literal for triggerCharacter, got ${prop.initializer.getText()}`);
|
|
}
|
|
triggerCharacter = prop.initializer.text;
|
|
}
|
|
}
|
|
|
|
if (!kind) {
|
|
throw new Error(`Missing kind in trigger reason`);
|
|
}
|
|
|
|
return { kind, triggerCharacter };
|
|
}
|
|
|
|
function parseSignatureHelpPresentForTriggerReason(args: ts.NodeArray<ts.Expression>): Cmd[] {
|
|
if (args.length === 0) {
|
|
throw new Error("signatureHelpPresentForTriggerReason requires at least one argument");
|
|
}
|
|
|
|
const triggerReason = parseTriggerReason(args[0]);
|
|
|
|
const markers: string[] = [];
|
|
for (let i = 1; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (ts.isStringLiteral(arg)) {
|
|
markers.push(arg.text);
|
|
}
|
|
else {
|
|
throw new Error(`Unexpected argument in signatureHelpPresentForTriggerReason: ${arg.getText()}`);
|
|
}
|
|
}
|
|
|
|
return [{
|
|
kind: "verifySignatureHelpPresent",
|
|
triggerReason: triggerReason === "undefined" ? undefined : triggerReason,
|
|
markers,
|
|
}];
|
|
}
|
|
|
|
function parseNoSignatureHelpForTriggerReason(args: ts.NodeArray<ts.Expression>): Cmd[] {
|
|
if (args.length === 0) {
|
|
throw new Error("noSignatureHelpForTriggerReason requires at least one argument");
|
|
}
|
|
|
|
const triggerReason = parseTriggerReason(args[0]);
|
|
|
|
const markers: string[] = [];
|
|
for (let i = 1; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (ts.isStringLiteral(arg)) {
|
|
markers.push(arg.text);
|
|
}
|
|
else {
|
|
throw new Error(`Unexpected argument in noSignatureHelpForTriggerReason: ${arg.getText()}`);
|
|
}
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyNoSignatureHelpForTriggerReason",
|
|
triggerReason: triggerReason === "undefined" ? undefined : triggerReason,
|
|
markers,
|
|
}];
|
|
}
|
|
|
|
function parseBaselineSmartSelection(args: ts.NodeArray<ts.Expression>): Cmd {
|
|
if (args.length !== 0) {
|
|
// All calls are currently empty!
|
|
throw new Error("Expected no arguments in verify.baselineSmartSelection");
|
|
}
|
|
return {
|
|
kind: "verifyBaselineSmartSelection",
|
|
};
|
|
}
|
|
|
|
function parseBaselineCallHierarchy(args: ts.NodeArray<ts.Expression>): Cmd {
|
|
if (args.length !== 0) {
|
|
throw new Error("Expected no arguments in verify.baselineCallHierarchy");
|
|
}
|
|
return {
|
|
kind: "verifyBaselineCallHierarchy",
|
|
};
|
|
}
|
|
|
|
function parseOutliningSpansArgs(args: readonly ts.Expression[]): [VerifyOutliningSpansCmd] {
|
|
if (args.length === 0) {
|
|
throw new Error("Expected at least one argument in verify.outliningSpansInCurrentFile");
|
|
}
|
|
|
|
let spans: string = "";
|
|
// Optional second argument for kind filter
|
|
let foldingRangeKind: string | undefined;
|
|
if (args.length > 1) {
|
|
const kindArg = getStringLiteralLike(args[1]);
|
|
if (!kindArg) {
|
|
throw new Error(`Expected string literal for outlining kind, got ${args[1].getText()}`);
|
|
}
|
|
switch (kindArg.text) {
|
|
case "comment":
|
|
foldingRangeKind = "lsproto.FoldingRangeKindComment";
|
|
break;
|
|
case "region":
|
|
foldingRangeKind = "lsproto.FoldingRangeKindRegion";
|
|
break;
|
|
case "imports":
|
|
foldingRangeKind = "lsproto.FoldingRangeKindImports";
|
|
break;
|
|
case "code":
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown folding range kind: ${kindArg.text}`);
|
|
}
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyOutliningSpans",
|
|
spans,
|
|
foldingRangeKind,
|
|
}];
|
|
}
|
|
|
|
function parseSemanticClassificationsAre(args: readonly ts.Expression[]): [VerifySemanticClassificationsCmd] | [] {
|
|
if (args.length < 1) {
|
|
throw new Error("semanticClassificationsAre requires at least a format argument");
|
|
}
|
|
|
|
const formatArg = args[0];
|
|
if (!ts.isStringLiteralLike(formatArg)) {
|
|
throw new Error("semanticClassificationsAre first argument must be a string literal");
|
|
}
|
|
|
|
const format = formatArg.text;
|
|
|
|
// Only handle "2020" format for semantic tokens
|
|
if (format !== "2020") {
|
|
// Skip other formats like "original"
|
|
return [];
|
|
}
|
|
|
|
const tokens: Array<{ type: string; text: string; }> = [];
|
|
|
|
// Parse the classification tokens (c2.semanticToken("type", "text"))
|
|
for (let i = 1; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (!ts.isCallExpression(arg)) {
|
|
throw new Error(`Expected call expression for token at index ${i}`);
|
|
}
|
|
|
|
if (!ts.isPropertyAccessExpression(arg.expression) || arg.expression.name.text !== "semanticToken") {
|
|
throw new Error(`Expected semanticToken call at index ${i}`);
|
|
}
|
|
|
|
if (arg.arguments.length < 2) {
|
|
throw new Error(`semanticToken requires 2 arguments at index ${i}`);
|
|
}
|
|
|
|
const typeArg = arg.arguments[0];
|
|
const textArg = arg.arguments[1];
|
|
|
|
if (!ts.isStringLiteralLike(typeArg) || !ts.isStringLiteralLike(textArg)) {
|
|
throw new Error(`semanticToken arguments must be string literals at index ${i}`);
|
|
}
|
|
|
|
// Map TypeScript's internal "member" type to LSP's "method" type
|
|
let tokenType = typeArg.text;
|
|
tokenType = tokenType.replace(/\bmember\b/g, "method");
|
|
|
|
tokens.push({
|
|
type: tokenType,
|
|
text: textArg.text,
|
|
});
|
|
}
|
|
|
|
return [{
|
|
kind: "verifySemanticClassifications",
|
|
format,
|
|
tokens,
|
|
}];
|
|
}
|
|
|
|
function parseKind(expr: ts.Expression): string {
|
|
if (!ts.isStringLiteral(expr)) {
|
|
throw new Error(`Expected string literal for kind, got ${expr.getText()}`);
|
|
}
|
|
switch (expr.text) {
|
|
case "primitive type":
|
|
case "keyword":
|
|
return "lsproto.CompletionItemKindKeyword";
|
|
case "const":
|
|
case "let":
|
|
case "var":
|
|
case "local var":
|
|
case "alias":
|
|
case "parameter":
|
|
return "lsproto.CompletionItemKindVariable";
|
|
case "property":
|
|
case "getter":
|
|
case "setter":
|
|
return "lsproto.CompletionItemKindField";
|
|
case "function":
|
|
case "local function":
|
|
return "lsproto.CompletionItemKindFunction";
|
|
case "method":
|
|
case "construct":
|
|
case "call":
|
|
case "index":
|
|
return "lsproto.CompletionItemKindMethod";
|
|
case "enum":
|
|
return "lsproto.CompletionItemKindEnum";
|
|
case "enum member":
|
|
return "lsproto.CompletionItemKindEnumMember";
|
|
case "module":
|
|
case "external module name":
|
|
return "lsproto.CompletionItemKindModule";
|
|
case "class":
|
|
case "type":
|
|
return "lsproto.CompletionItemKindClass";
|
|
case "interface":
|
|
return "lsproto.CompletionItemKindInterface";
|
|
case "warning":
|
|
return "lsproto.CompletionItemKindText";
|
|
case "script":
|
|
return "lsproto.CompletionItemKindFile";
|
|
case "directory":
|
|
return "lsproto.CompletionItemKindFolder";
|
|
case "string":
|
|
return "lsproto.CompletionItemKindConstant";
|
|
default:
|
|
return "lsproto.CompletionItemKindProperty";
|
|
}
|
|
}
|
|
|
|
const fileKindModifiers = new Set([".d.ts", ".ts", ".tsx", ".js", ".jsx", ".json"]);
|
|
|
|
function parseKindModifiers(expr: ts.Expression): { isOptional: boolean; isDeprecated: boolean; extensions: string[]; } {
|
|
if (!ts.isStringLiteral(expr)) {
|
|
throw new Error(`Expected string literal for kind modifiers, got ${expr.getText()}`);
|
|
}
|
|
let isOptional = false;
|
|
let isDeprecated = false;
|
|
const extensions: string[] = [];
|
|
const modifiers = expr.text.split(",");
|
|
for (const modifier of modifiers) {
|
|
switch (modifier) {
|
|
case "optional":
|
|
isOptional = true;
|
|
break;
|
|
case "deprecated":
|
|
isDeprecated = true;
|
|
break;
|
|
default:
|
|
if (fileKindModifiers.has(modifier)) {
|
|
extensions.push(modifier);
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
isOptional,
|
|
isDeprecated,
|
|
extensions,
|
|
};
|
|
}
|
|
|
|
interface ParsedSortText {
|
|
expression: string;
|
|
deprecated: boolean;
|
|
}
|
|
|
|
function parseSortText(expr: ts.Expression): ParsedSortText {
|
|
if (ts.isCallExpression(expr) && expr.expression.getText() === "completion.SortText.Deprecated") {
|
|
const inner = parseSortText(expr.arguments[0]);
|
|
return {
|
|
expression: `ls.DeprecateSortText(${inner.expression})`,
|
|
deprecated: true,
|
|
};
|
|
}
|
|
|
|
return { expression: parseSortTextExpression(expr.getText()), deprecated: false };
|
|
}
|
|
|
|
function parseSortTextExpression(text: string): string {
|
|
switch (text) {
|
|
case "completion.SortText.LocalDeclarationPriority":
|
|
return "ls.SortTextLocalDeclarationPriority";
|
|
case "completion.SortText.LocationPriority":
|
|
return "ls.SortTextLocationPriority";
|
|
case "completion.SortText.OptionalMember":
|
|
return "ls.SortTextOptionalMember";
|
|
case "completion.SortText.MemberDeclaredBySpreadAssignment":
|
|
return "ls.SortTextMemberDeclaredBySpreadAssignment";
|
|
case "completion.SortText.SuggestedClassMembers":
|
|
return "ls.SortTextSuggestedClassMembers";
|
|
case "completion.SortText.GlobalsOrKeywords":
|
|
return "ls.SortTextGlobalsOrKeywords";
|
|
case "completion.SortText.AutoImportSuggestions":
|
|
return "ls.SortTextAutoImportSuggestions";
|
|
case "completion.SortText.ClassMemberSnippets":
|
|
return "ls.SortTextClassMemberSnippets";
|
|
case "completion.SortText.JavascriptIdentifiers":
|
|
return "ls.SortTextJavascriptIdentifiers";
|
|
default:
|
|
throw new Error(`Unrecognized sort text: ${text}`); // !!! support deprecated/obj literal prop/etc
|
|
}
|
|
}
|
|
|
|
function formatCompletionItemTags(tags: Set<string>): string | undefined {
|
|
if (tags.size === 0) {
|
|
return undefined;
|
|
}
|
|
return `Tags: &[]lsproto.CompletionItemTag{${[...tags].join(", ")}},`;
|
|
}
|
|
|
|
function parseVerifyNavigateTo(args: ts.NodeArray<ts.Expression>): [VerifyNavToCmd] {
|
|
const goArgs = [];
|
|
for (const arg of args) {
|
|
const result = parseVerifyNavigateToArg(arg);
|
|
goArgs.push(result);
|
|
}
|
|
return [{
|
|
kind: "verifyNavigateTo",
|
|
args: goArgs,
|
|
}];
|
|
}
|
|
|
|
function parseVerifyNavigateToArg(arg: ts.Expression): string {
|
|
if (!ts.isObjectLiteralExpression(arg)) {
|
|
throw new Error(`Expected object literal expression for verify.navigateTo argument, got ${arg.getText()}`);
|
|
}
|
|
let prefs;
|
|
const items = [];
|
|
let pattern: string | undefined;
|
|
for (const prop of arg.properties) {
|
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
|
|
throw new Error(`Expected property assignment with identifier name for verify.navigateTo argument, got ${prop.getText()}`);
|
|
}
|
|
const propName = prop.name.text;
|
|
switch (propName) {
|
|
case "pattern": {
|
|
let patternInit = getStringLiteralLike(prop.initializer);
|
|
if (!patternInit) {
|
|
throw new Error(`Expected string literal for pattern in verify.navigateTo argument, got ${prop.initializer.getText()}`);
|
|
}
|
|
pattern = getGoStringLiteral(patternInit.text);
|
|
break;
|
|
}
|
|
case "fileName":
|
|
// no longer supported
|
|
continue;
|
|
case "expected": {
|
|
const init = prop.initializer;
|
|
if (!ts.isArrayLiteralExpression(init)) {
|
|
throw new Error(`Expected array literal expression for expected property in verify.navigateTo argument, got ${init.getText()}`);
|
|
}
|
|
for (const elem of init.elements) {
|
|
const result = parseNavToItem(elem);
|
|
items.push(result);
|
|
}
|
|
break;
|
|
}
|
|
case "excludeLibFiles": {
|
|
if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
prefs = `&lsutil.UserPreferences{ExcludeLibrarySymbolsInNavTo: core.TSFalse}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!prefs) {
|
|
prefs = "nil";
|
|
}
|
|
return `{
|
|
Pattern: ${pattern ? pattern : '""'},
|
|
Preferences: ${prefs},
|
|
Exact: new([]*lsproto.SymbolInformation{${items.length ? items.join(",\n") + ",\n" : ""}}),
|
|
}`;
|
|
}
|
|
|
|
function parseVerifyNavTree(args: readonly ts.Expression[]): [VerifyNavTreeCmd] {
|
|
// Ignore arguments and use baseline tests intead.
|
|
return [{
|
|
kind: "verifyNavigationTree",
|
|
}];
|
|
}
|
|
|
|
function parseNavToItem(arg: ts.Expression): string {
|
|
let item = getNodeOfKind(arg, ts.isObjectLiteralExpression);
|
|
if (!item) {
|
|
throw new Error(`Expected object literal expression for navigateTo item, got ${arg.getText()}`);
|
|
}
|
|
const itemProps: string[] = [];
|
|
for (const prop of item.properties) {
|
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
|
|
throw new Error(`Expected property assignment with identifier name for navigateTo item, got ${prop.getText()}`);
|
|
}
|
|
const propName = prop.name.text;
|
|
const init = prop.initializer;
|
|
switch (propName) {
|
|
case "name": {
|
|
let nameInit;
|
|
if (!(nameInit = getStringLiteralLike(init))) {
|
|
throw new Error(`Expected string literal for name in navigateTo item, got ${init.getText()}`);
|
|
}
|
|
itemProps.push(`Name: ${getGoStringLiteral(nameInit.text)}`);
|
|
break;
|
|
}
|
|
case "kind": {
|
|
const goKind = getSymbolKind(init);
|
|
itemProps.push(`Kind: lsproto.${goKind}`);
|
|
break;
|
|
}
|
|
case "kindModifiers": {
|
|
if (init.getText().includes("deprecated")) {
|
|
itemProps.push(`Tags: &[]lsproto.SymbolTag{lsproto.SymbolTagDeprecated}`);
|
|
}
|
|
break;
|
|
}
|
|
case "range": {
|
|
if (ts.isIdentifier(init) || (ts.isElementAccessExpression(init) && ts.isIdentifier(init.expression))) {
|
|
let parsedRange = parseRangeVariable(init);
|
|
if (parsedRange) {
|
|
itemProps.push(`Location: ${parsedRange}.LSLocation()`);
|
|
continue;
|
|
}
|
|
}
|
|
if (ts.isElementAccessExpression(init) && init.expression.getText() === "test.ranges()") {
|
|
itemProps.push(`Location: f.Ranges()[${parseInt(init.argumentExpression.getText())}].LSLocation()`);
|
|
continue;
|
|
}
|
|
throw new Error(`Expected range variable for range in navigateTo item, got ${init.getText()}`);
|
|
}
|
|
case "containerName": {
|
|
let nameInit;
|
|
if (!(nameInit = getStringLiteralLike(init))) {
|
|
throw new Error(`Expected string literal for container name in navigateTo item, got ${init.getText()}`);
|
|
}
|
|
itemProps.push(`ContainerName: new(${getGoStringLiteral(nameInit.text)})`);
|
|
break;
|
|
}
|
|
default:
|
|
// ignore other properties
|
|
}
|
|
}
|
|
return `{\n${itemProps.join(",\n")},\n}`;
|
|
}
|
|
|
|
function getSymbolKind(kind: ts.Expression): string {
|
|
let result;
|
|
if (!(result = getStringLiteralLike(kind))) {
|
|
throw new Error(`Expected string literal for symbol kind, got ${kind.getText()}`);
|
|
}
|
|
return getSymbolKindWorker(result.text);
|
|
}
|
|
|
|
function getSymbolKindWorker(kind: string): string {
|
|
switch (kind) {
|
|
case "script":
|
|
return "SymbolKindFile";
|
|
case "module":
|
|
return "SymbolKindNamespace";
|
|
case "class":
|
|
case "local class":
|
|
return "SymbolKindClass";
|
|
case "interface":
|
|
return "SymbolKindInterface";
|
|
case "type":
|
|
return "SymbolKindClass";
|
|
case "enum":
|
|
return "SymbolKindEnum";
|
|
case "enum member":
|
|
return "SymbolKindEnumMember";
|
|
case "var":
|
|
case "local var":
|
|
case "using":
|
|
case "await using":
|
|
return "SymbolKindVariable";
|
|
case "function":
|
|
case "local function":
|
|
return "SymbolKindFunction";
|
|
case "method":
|
|
return "SymbolKindMethod";
|
|
case "getter":
|
|
case "setter":
|
|
case "property":
|
|
case "accessor":
|
|
return "SymbolKindProperty";
|
|
case "constructor":
|
|
case "construct":
|
|
return "SymbolKindConstructor";
|
|
case "call":
|
|
case "index":
|
|
return "SymbolKindFunction";
|
|
case "parameter":
|
|
return "SymbolKindVariable";
|
|
case "type parameter":
|
|
return "SymbolKindTypeParameter";
|
|
case "primitive type":
|
|
return "SymbolKindObject";
|
|
case "const":
|
|
case "let":
|
|
return "SymbolKindVariable";
|
|
case "directory":
|
|
return "SymbolKindPackage";
|
|
case "external module name":
|
|
return "SymbolKindModule";
|
|
case "string":
|
|
return "SymbolKindString";
|
|
case "type":
|
|
return "SymbolKindClass";
|
|
default:
|
|
return "SymbolKindVariable";
|
|
}
|
|
}
|
|
|
|
interface VerifyCompletionsCmd {
|
|
kind: "verifyCompletions";
|
|
marker: string;
|
|
isNewIdentifierLocation?: true;
|
|
args?: VerifyCompletionsArgs | "nil";
|
|
andApplyCodeActionArgs?: VerifyApplyCodeActionArgs;
|
|
}
|
|
|
|
interface VerifyCompletionsArgs {
|
|
includes?: string;
|
|
excludes?: string;
|
|
exact?: string;
|
|
unsorted?: string;
|
|
preferences: string;
|
|
}
|
|
|
|
interface VerifyApplyCodeActionArgs {
|
|
name: string;
|
|
source: string;
|
|
description: string;
|
|
newFileContent: string;
|
|
}
|
|
|
|
interface VerifyApplyCodeActionFromCompletionCmd {
|
|
kind: "verifyApplyCodeActionFromCompletion";
|
|
marker: string;
|
|
options: string;
|
|
}
|
|
|
|
interface VerifyBaselineFindAllReferencesCmd {
|
|
kind: "verifyBaselineFindAllReferences";
|
|
markers: string[];
|
|
ranges?: boolean;
|
|
}
|
|
|
|
interface VerifyBaselineGoToDefinitionCmd {
|
|
kind: "verifyBaselineGoToDefinition" | "verifyBaselineGoToType" | "verifyBaselineGoToImplementation" | "verifyBaselineGoToSourceDefinition";
|
|
markers: string[];
|
|
boundSpan?: true;
|
|
ranges?: boolean;
|
|
}
|
|
|
|
interface VerifyBaselineQuickInfoCmd {
|
|
kind: "verifyBaselineQuickInfo";
|
|
verbosityLevels?: Record<string, number[]>;
|
|
}
|
|
|
|
interface VerifyBaselineSignatureHelpCmd {
|
|
kind: "verifyBaselineSignatureHelp";
|
|
}
|
|
|
|
interface VerifyBaselineSmartSelection {
|
|
kind: "verifyBaselineSmartSelection";
|
|
}
|
|
|
|
interface VerifyBaselineCallHierarchy {
|
|
kind: "verifyBaselineCallHierarchy";
|
|
}
|
|
|
|
interface VerifyBaselineRenameCmd {
|
|
kind: "verifyBaselineRename" | "verifyBaselineRenameAtRangesWithText";
|
|
args: string[];
|
|
preferences: string;
|
|
}
|
|
|
|
interface VerifyBaselineDocumentHighlightsCmd {
|
|
kind: "verifyBaselineDocumentHighlights";
|
|
args: string[];
|
|
preferences: string;
|
|
filesToSearch?: string[];
|
|
}
|
|
|
|
interface VerifyBaselineInlayHintsCmd {
|
|
kind: "verifyBaselineInlayHints";
|
|
span: string;
|
|
preferences: string;
|
|
}
|
|
|
|
interface VerifyImportFixAtPositionCmd {
|
|
kind: "verifyImportFixAtPosition";
|
|
expectedTexts: string[];
|
|
preferences: string;
|
|
}
|
|
|
|
interface VerifyImportFixModuleSpecifiersCmd {
|
|
kind: "verifyImportFixModuleSpecifiers";
|
|
markerName: string;
|
|
moduleSpecifiers: string[];
|
|
preferences: string;
|
|
}
|
|
|
|
interface GoToCmd {
|
|
kind: "goTo";
|
|
// !!! `selectRange` and `rangeStart` require parsing variables and `test.ranges()[n]`
|
|
funcName: "marker" | "file" | "fileNumber" | "EOF" | "BOF" | "position" | "select";
|
|
args: string[];
|
|
}
|
|
|
|
interface EditCmd {
|
|
kind: "edit";
|
|
goStatement: string;
|
|
}
|
|
|
|
interface FormatCmd {
|
|
kind: "format";
|
|
goStatement: string;
|
|
}
|
|
|
|
interface VerifyContentCmd {
|
|
kind: "verifyContent";
|
|
goStatement: string;
|
|
}
|
|
|
|
interface VerifyQuickInfoCmd {
|
|
kind: "quickInfoIs" | "quickInfoAt" | "quickInfoExists" | "notQuickInfoExists";
|
|
marker?: string;
|
|
text?: string;
|
|
docs?: string;
|
|
}
|
|
|
|
interface VerifyOrganizeImportsCmd {
|
|
kind: "verifyOrganizeImports";
|
|
expectedContent: string;
|
|
mode: string;
|
|
preferences: string;
|
|
}
|
|
|
|
interface VerifyRenameInfoCmd {
|
|
kind: "renameInfoSucceeded" | "renameInfoFailed";
|
|
preferences: string;
|
|
}
|
|
|
|
interface VerifyGetEditsForFileRenameCmd {
|
|
kind: "verifyGetEditsForFileRename";
|
|
oldPath: string;
|
|
newPath: string;
|
|
newFileContents: string;
|
|
preferences: string;
|
|
}
|
|
|
|
interface VerifyBaselineLinkedEditingCmd {
|
|
kind: "verifyBaselineLinkedEditing";
|
|
}
|
|
interface VerifyLinkedEditingCmd {
|
|
kind: "verifyLinkedEditing";
|
|
ranges: string;
|
|
}
|
|
|
|
interface VerifyDiagnosticsCmd {
|
|
kind: "verifyDiagnostics";
|
|
arg: string;
|
|
isSuggestion: boolean;
|
|
}
|
|
|
|
interface VerifyBaselineDiagnosticsCmd {
|
|
kind: "verifyBaselineDiagnostics";
|
|
}
|
|
|
|
interface VerifyNavToCmd {
|
|
kind: "verifyNavigateTo";
|
|
args: string[];
|
|
}
|
|
|
|
interface VerifySignatureHelpOptions {
|
|
marker?: string | string[];
|
|
text?: string;
|
|
docComment?: string;
|
|
parameterCount?: number;
|
|
parameterName?: string;
|
|
parameterSpan?: string;
|
|
parameterDocComment?: string;
|
|
overloadsCount?: number;
|
|
overrideSelectedItemIndex?: number;
|
|
triggerReason?: string;
|
|
isVariadic?: boolean;
|
|
}
|
|
|
|
interface VerifySignatureHelpCmd {
|
|
kind: "verifySignatureHelp";
|
|
options: VerifySignatureHelpOptions[];
|
|
}
|
|
|
|
interface VerifyNoSignatureHelpCmd {
|
|
kind: "verifyNoSignatureHelp";
|
|
markers: string[];
|
|
}
|
|
|
|
interface VerifySignatureHelpPresentCmd {
|
|
kind: "verifySignatureHelpPresent";
|
|
triggerReason?: SignatureHelpTriggerReason;
|
|
markers: string[];
|
|
}
|
|
|
|
interface VerifyNoSignatureHelpForTriggerReasonCmd {
|
|
kind: "verifyNoSignatureHelpForTriggerReason";
|
|
triggerReason?: SignatureHelpTriggerReason;
|
|
markers: string[];
|
|
}
|
|
|
|
interface VerifyOutliningSpansCmd {
|
|
kind: "verifyOutliningSpans";
|
|
spans: string;
|
|
foldingRangeKind?: string;
|
|
}
|
|
|
|
interface VerifyNavTreeCmd {
|
|
kind: "verifyNavigationTree";
|
|
}
|
|
|
|
interface VerifyNumberOfErrorsInCurrentFileCmd {
|
|
kind: "verifyNumberOfErrorsInCurrentFile";
|
|
expectedCount: number;
|
|
}
|
|
|
|
interface VerifyNoErrorsCmd {
|
|
kind: "verifyNoErrors";
|
|
}
|
|
|
|
interface VerifyErrorExistsAtRangeCmd {
|
|
kind: "verifyErrorExistsAtRange";
|
|
range: string;
|
|
code: number;
|
|
message: string;
|
|
}
|
|
|
|
interface VerifyCurrentLineContentIsCmd {
|
|
kind: "verifyCurrentLineContentIs";
|
|
text: string;
|
|
}
|
|
|
|
interface VerifyCurrentFileContentIsCmd {
|
|
kind: "verifyCurrentFileContentIs";
|
|
text: string;
|
|
}
|
|
|
|
interface VerifyErrorExistsBetweenMarkersCmd {
|
|
kind: "verifyErrorExistsBetweenMarkers";
|
|
startMarker: string;
|
|
endMarker: string;
|
|
}
|
|
|
|
interface VerifyErrorExistsAfterMarkerCmd {
|
|
kind: "verifyErrorExistsAfterMarker";
|
|
markerName: string;
|
|
}
|
|
|
|
interface VerifyErrorExistsBeforeMarkerCmd {
|
|
kind: "verifyErrorExistsBeforeMarker";
|
|
markerName: string;
|
|
}
|
|
|
|
interface VerifyCodeFixCmd {
|
|
kind: "verifyCodeFix";
|
|
description: string;
|
|
newFileContent?: string;
|
|
newRangeContent?: string;
|
|
index: number;
|
|
applyChanges: boolean;
|
|
preferences: string;
|
|
}
|
|
|
|
interface VerifyCodeFixAvailableCmd {
|
|
kind: "verifyCodeFixAvailable";
|
|
descriptions: string[];
|
|
unavailableDescriptions: string[];
|
|
expectNone: boolean;
|
|
}
|
|
|
|
interface VerifyRangeAfterCodeFixCmd {
|
|
kind: "verifyRangeAfterCodeFix";
|
|
expectedText: string;
|
|
includeWhiteSpace: boolean;
|
|
errorCode: number;
|
|
index: number;
|
|
}
|
|
|
|
interface VerifyCodeFixAllCmd {
|
|
kind: "verifyCodeFixAll";
|
|
fixId: string;
|
|
newFileContent: string;
|
|
}
|
|
|
|
interface VerifyDocCommentTemplateCmd {
|
|
kind: "verifyDocCommentTemplate";
|
|
marker: string;
|
|
expectedOffset: string;
|
|
expectedText: string;
|
|
generateReturnInDocTemplate?: string;
|
|
}
|
|
|
|
interface VerifyNoDocCommentTemplateCmd {
|
|
kind: "verifyNoDocCommentTemplate";
|
|
marker: string;
|
|
}
|
|
|
|
interface ForOfCmd {
|
|
kind: "forOf";
|
|
variableName: string;
|
|
rangeExpression: string;
|
|
bodyCommands: Cmd[];
|
|
}
|
|
|
|
interface VerifySemanticClassificationsCmd {
|
|
kind: "verifySemanticClassifications";
|
|
format: string;
|
|
tokens: Array<{ type: string; text: string; }>;
|
|
}
|
|
|
|
type Cmd =
|
|
| VerifyCompletionsCmd
|
|
| VerifyApplyCodeActionFromCompletionCmd
|
|
| VerifyBaselineFindAllReferencesCmd
|
|
| VerifyBaselineDocumentHighlightsCmd
|
|
| VerifyBaselineGoToDefinitionCmd
|
|
| VerifyBaselineQuickInfoCmd
|
|
| VerifyBaselineSignatureHelpCmd
|
|
| VerifyBaselineSmartSelection
|
|
| VerifySignatureHelpCmd
|
|
| VerifyNoSignatureHelpCmd
|
|
| VerifySignatureHelpPresentCmd
|
|
| VerifyNoSignatureHelpForTriggerReasonCmd
|
|
| VerifyBaselineCallHierarchy
|
|
| GoToCmd
|
|
| FormatCmd
|
|
| EditCmd
|
|
| VerifyContentCmd
|
|
| VerifyQuickInfoCmd
|
|
| VerifyOrganizeImportsCmd
|
|
| VerifyBaselineRenameCmd
|
|
| VerifyRenameInfoCmd
|
|
| VerifyGetEditsForFileRenameCmd
|
|
| VerifyBaselineLinkedEditingCmd
|
|
| VerifyLinkedEditingCmd
|
|
| VerifyNavToCmd
|
|
| VerifyNavTreeCmd
|
|
| VerifyBaselineInlayHintsCmd
|
|
| VerifyImportFixAtPositionCmd
|
|
| VerifyImportFixModuleSpecifiersCmd
|
|
| VerifyDiagnosticsCmd
|
|
| VerifyBaselineDiagnosticsCmd
|
|
| VerifySemanticClassificationsCmd
|
|
| VerifyOutliningSpansCmd
|
|
| VerifyNumberOfErrorsInCurrentFileCmd
|
|
| VerifyNoErrorsCmd
|
|
| VerifyErrorExistsAtRangeCmd
|
|
| VerifyCurrentLineContentIsCmd
|
|
| VerifyCurrentFileContentIsCmd
|
|
| VerifyErrorExistsBetweenMarkersCmd
|
|
| VerifyErrorExistsAfterMarkerCmd
|
|
| VerifyErrorExistsBeforeMarkerCmd
|
|
| VerifyCodeFixCmd
|
|
| VerifyCodeFixAvailableCmd
|
|
| VerifyRangeAfterCodeFixCmd
|
|
| VerifyCodeFixAllCmd
|
|
| VerifyDocCommentTemplateCmd
|
|
| VerifyNoDocCommentTemplateCmd
|
|
| ForOfCmd;
|
|
|
|
function generateVerifyOutliningSpans({ foldingRangeKind }: VerifyOutliningSpansCmd): string {
|
|
if (foldingRangeKind) {
|
|
return `f.VerifyOutliningSpans(t, ${foldingRangeKind})`;
|
|
}
|
|
return `f.VerifyOutliningSpans(t)`;
|
|
}
|
|
|
|
function generateVerifyCompletions({ marker, args, isNewIdentifierLocation, andApplyCodeActionArgs }: VerifyCompletionsCmd, imports: Set<string>): string {
|
|
let expectedList: string;
|
|
if (args === "nil") {
|
|
expectedList = "nil";
|
|
}
|
|
else {
|
|
imports.add(IMPORT_UTIL);
|
|
const expected = [];
|
|
if (args?.includes) expected.push(`Includes: ${args.includes},`);
|
|
if (args?.excludes) expected.push(`Excludes: ${args.excludes},`);
|
|
if (args?.exact) expected.push(`Exact: ${args.exact},`);
|
|
if (args?.unsorted) expected.push(`Unsorted: ${args.unsorted},`);
|
|
// !!! isIncomplete
|
|
const commitCharacters = isNewIdentifierLocation ? "[]string{}" : "DefaultCommitCharacters";
|
|
expectedList = `&fourslash.CompletionsExpectedList{
|
|
IsIncomplete: false,
|
|
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
|
|
CommitCharacters: &${commitCharacters},
|
|
EditRange: Ignored,
|
|
},
|
|
Items: &fourslash.CompletionsExpectedItems{
|
|
${expected.join("\n")}
|
|
},
|
|
${args?.preferences && !args.preferences.startsWith("nil") ? `UserPreferences: ${args.preferences},` : ""}
|
|
}`;
|
|
}
|
|
|
|
const call = `f.VerifyCompletions(t, ${marker}, ${expectedList})`;
|
|
if (andApplyCodeActionArgs) {
|
|
return `${call}.AndApplyCodeAction(t, &fourslash.CompletionsExpectedCodeAction{
|
|
Name: ${getGoStringLiteral(andApplyCodeActionArgs.name)},
|
|
Source: ${getGoStringLiteral(andApplyCodeActionArgs.source)},
|
|
Description: ${getGoStringLiteral(andApplyCodeActionArgs.description)},
|
|
NewFileContent: ${getGoMultiLineStringLiteral(andApplyCodeActionArgs.newFileContent)},
|
|
})`;
|
|
}
|
|
return call;
|
|
}
|
|
|
|
function generateVerifyApplyCodeActionFromCompletion({ marker, options }: VerifyApplyCodeActionFromCompletionCmd): string {
|
|
return `f.VerifyApplyCodeActionFromCompletion(t, ${marker}, ${options})`;
|
|
}
|
|
|
|
function generateBaselineFindAllReferences({ markers, ranges }: VerifyBaselineFindAllReferencesCmd): string {
|
|
if (ranges || markers.length === 0) {
|
|
return `f.VerifyBaselineFindAllReferences(t)`;
|
|
}
|
|
return `f.VerifyBaselineFindAllReferences(t, ${markers.join(", ")})`;
|
|
}
|
|
|
|
function generateBaselineDocumentHighlights({ args, preferences, filesToSearch }: VerifyBaselineDocumentHighlightsCmd): string {
|
|
if (filesToSearch) {
|
|
const filesGo = `[]string{${filesToSearch.join(", ")}}`;
|
|
return `f.VerifyBaselineDocumentHighlightsWithOptions(t, ${preferences}, ${filesGo}, ${args.join(", ")})`;
|
|
}
|
|
return `f.VerifyBaselineDocumentHighlights(t, ${preferences}, ${args.join(", ")})`;
|
|
}
|
|
|
|
function generateBaselineGoToDefinition({ markers, ranges, kind, boundSpan }: VerifyBaselineGoToDefinitionCmd): string {
|
|
const originalSelectionRange = boundSpan ? "true" : "false";
|
|
switch (kind) {
|
|
case "verifyBaselineGoToDefinition":
|
|
if (ranges || markers.length === 0) {
|
|
return `f.VerifyBaselineGoToDefinition(t, ${originalSelectionRange})`;
|
|
}
|
|
return `f.VerifyBaselineGoToDefinition(t, ${originalSelectionRange}, ${markers.join(", ")})`;
|
|
case "verifyBaselineGoToType":
|
|
if (ranges || markers.length === 0) {
|
|
return `f.VerifyBaselineGoToTypeDefinition(t)`;
|
|
}
|
|
return `f.VerifyBaselineGoToTypeDefinition(t, ${markers.join(", ")})`;
|
|
case "verifyBaselineGoToImplementation":
|
|
if (ranges || markers.length === 0) {
|
|
return `f.VerifyBaselineGoToImplementation(t)`;
|
|
}
|
|
return `f.VerifyBaselineGoToImplementation(t, ${markers.join(", ")})`;
|
|
case "verifyBaselineGoToSourceDefinition":
|
|
if (ranges || markers.length === 0) {
|
|
return `f.VerifyBaselineGoToSourceDefinition(t)`;
|
|
}
|
|
return `f.VerifyBaselineGoToSourceDefinition(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(", ")})`;
|
|
}
|
|
|
|
function generateQuickInfoCommand({ kind, marker, text, docs }: VerifyQuickInfoCmd): string {
|
|
switch (kind) {
|
|
case "quickInfoIs":
|
|
return `f.VerifyQuickInfoIs(t, ${text!}, ${docs ? docs : `""`})`;
|
|
case "quickInfoAt":
|
|
return `f.VerifyQuickInfoAt(t, ${marker!}, ${text ? text : `""`}, ${docs ? docs : `""`})`;
|
|
case "quickInfoExists":
|
|
return `f.VerifyQuickInfoExists(t)`;
|
|
case "notQuickInfoExists":
|
|
return `f.VerifyNotQuickInfoExists(t)`;
|
|
}
|
|
}
|
|
|
|
function generateOrganizeImports({ expectedContent, mode, preferences }: VerifyOrganizeImportsCmd): string {
|
|
return `f.VerifyOrganizeImports(t,
|
|
${getGoMultiLineStringLiteral(expectedContent)},
|
|
${mode},
|
|
${preferences},
|
|
)`;
|
|
}
|
|
|
|
function generateBaselineRename({ kind, args, preferences }: VerifyBaselineRenameCmd): string {
|
|
switch (kind) {
|
|
case "verifyBaselineRename":
|
|
return `f.VerifyBaselineRename(t, ${preferences}, ${args.join(", ")})`;
|
|
case "verifyBaselineRenameAtRangesWithText":
|
|
return `f.VerifyBaselineRenameAtRangesWithText(t, ${preferences}, ${args.join(", ")})`;
|
|
}
|
|
}
|
|
|
|
function generateBaselineInlayHints({ span, preferences }: VerifyBaselineInlayHintsCmd): string {
|
|
return `f.VerifyBaselineInlayHints(t, ${span}, ${preferences})`;
|
|
}
|
|
|
|
function generateImportFixAtPosition({ expectedTexts, preferences }: VerifyImportFixAtPositionCmd): string {
|
|
// Handle empty array case
|
|
if (expectedTexts.length === 1 && expectedTexts[0] === "") {
|
|
return `f.VerifyImportFixAtPosition(t, []string{}, ${preferences})`;
|
|
}
|
|
return `f.VerifyImportFixAtPosition(t, []string{\n${expectedTexts.join(",\n")},\n}, ${preferences})`;
|
|
}
|
|
|
|
function generateImportFixModuleSpecifiers({ markerName, moduleSpecifiers, preferences }: VerifyImportFixModuleSpecifiersCmd): string {
|
|
const specifiersArray = moduleSpecifiers.length === 0
|
|
? "[]string{}"
|
|
: `[]string{${moduleSpecifiers.join(", ")}}`;
|
|
return `f.VerifyImportFixModuleSpecifiers(t, ${markerName}, ${specifiersArray}, ${preferences})`;
|
|
}
|
|
|
|
function generateSignatureHelpExpected(opts: VerifySignatureHelpOptions): string {
|
|
const fields: string[] = [];
|
|
|
|
if (opts.text !== undefined) {
|
|
fields.push(`Text: ${getGoStringLiteral(opts.text)}`);
|
|
}
|
|
if (opts.docComment !== undefined) {
|
|
fields.push(`DocComment: ${getGoStringLiteral(opts.docComment)}`);
|
|
}
|
|
if (opts.parameterCount !== undefined) {
|
|
fields.push(`ParameterCount: ${opts.parameterCount}`);
|
|
}
|
|
if (opts.parameterName !== undefined) {
|
|
fields.push(`ParameterName: ${getGoStringLiteral(opts.parameterName)}`);
|
|
}
|
|
if (opts.parameterSpan !== undefined) {
|
|
fields.push(`ParameterSpan: ${getGoStringLiteral(opts.parameterSpan)}`);
|
|
}
|
|
if (opts.parameterDocComment !== undefined) {
|
|
fields.push(`ParameterDocComment: ${getGoStringLiteral(opts.parameterDocComment)}`);
|
|
}
|
|
if (opts.overloadsCount !== undefined) {
|
|
fields.push(`OverloadsCount: ${opts.overloadsCount}`);
|
|
}
|
|
if (opts.overrideSelectedItemIndex !== undefined) {
|
|
fields.push(`OverrideSelectedItemIndex: ${opts.overrideSelectedItemIndex}`);
|
|
}
|
|
if (opts.isVariadic !== undefined) {
|
|
fields.push(`IsVariadic: ${opts.isVariadic}`);
|
|
fields.push(`IsVariadicSet: true`);
|
|
}
|
|
|
|
return `fourslash.VerifySignatureHelpOptions{${fields.join(", ")}}`;
|
|
}
|
|
|
|
function generateSignatureHelp({ options }: VerifySignatureHelpCmd): string {
|
|
const lines: string[] = [];
|
|
|
|
for (const opts of options) {
|
|
const expected = generateSignatureHelpExpected(opts);
|
|
|
|
// Add comments for unsupported options
|
|
const unsupportedComments: string[] = [];
|
|
|
|
if (opts.marker !== undefined) {
|
|
const markers = Array.isArray(opts.marker) ? opts.marker : [opts.marker];
|
|
for (const marker of markers) {
|
|
lines.push(`f.GoToMarker(t, ${getGoStringLiteral(marker)})`);
|
|
for (const comment of unsupportedComments) {
|
|
lines.push(comment);
|
|
}
|
|
lines.push(`f.VerifySignatureHelp(t, ${expected})`);
|
|
}
|
|
}
|
|
else {
|
|
// No marker specified, use current position
|
|
for (const comment of unsupportedComments) {
|
|
lines.push(comment);
|
|
}
|
|
lines.push(`f.VerifySignatureHelp(t, ${expected})`);
|
|
}
|
|
}
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function generateNoSignatureHelp({ markers }: VerifyNoSignatureHelpCmd): string {
|
|
if (markers.length === 1 && markers[0] === "...test.markerNames()") {
|
|
// All markers
|
|
return `f.VerifyNoSignatureHelpForMarkers(t, f.MarkerNames()...)`;
|
|
}
|
|
if (markers.length === 0) {
|
|
// Current position
|
|
return `f.VerifyNoSignatureHelp(t)`;
|
|
}
|
|
// Specific markers
|
|
const markerArgs = markers.map(m => getGoStringLiteral(m)).join(", ");
|
|
return `f.VerifyNoSignatureHelpForMarkers(t, ${markerArgs})`;
|
|
}
|
|
|
|
function generateTriggerContext(triggerReason: SignatureHelpTriggerReason | undefined): string {
|
|
if (!triggerReason) {
|
|
return "nil";
|
|
}
|
|
switch (triggerReason.kind) {
|
|
case "invoked":
|
|
return `&lsproto.SignatureHelpContext{TriggerKind: lsproto.SignatureHelpTriggerKindInvoked}`;
|
|
case "characterTyped":
|
|
return `&lsproto.SignatureHelpContext{TriggerKind: lsproto.SignatureHelpTriggerKindTriggerCharacter, TriggerCharacter: new(${getGoStringLiteral(triggerReason.triggerCharacter ?? "")}), IsRetrigger: false}`;
|
|
case "retrigger":
|
|
return `&lsproto.SignatureHelpContext{TriggerKind: lsproto.SignatureHelpTriggerKindTriggerCharacter, TriggerCharacter: new(${getGoStringLiteral(triggerReason.triggerCharacter ?? "")}), IsRetrigger: true}`;
|
|
default:
|
|
throw new Error(`Unknown trigger reason kind: ${triggerReason}`);
|
|
}
|
|
}
|
|
|
|
function generateSignatureHelpPresent({ triggerReason, markers }: VerifySignatureHelpPresentCmd): string {
|
|
const context = generateTriggerContext(triggerReason);
|
|
if (markers.length === 0) {
|
|
// Current position
|
|
return `f.VerifySignatureHelpPresent(t, ${context})`;
|
|
}
|
|
// Specific markers
|
|
const markerArgs = markers.map(m => getGoStringLiteral(m)).join(", ");
|
|
return `f.VerifySignatureHelpPresentForMarkers(t, ${context}, ${markerArgs})`;
|
|
}
|
|
|
|
function generateNoSignatureHelpForTriggerReason({ triggerReason, markers }: VerifyNoSignatureHelpForTriggerReasonCmd): string {
|
|
const context = generateTriggerContext(triggerReason);
|
|
if (markers.length === 0) {
|
|
// Current position
|
|
return `f.VerifyNoSignatureHelpWithContext(t, ${context})`;
|
|
}
|
|
// Specific markers
|
|
const markerArgs = markers.map(m => getGoStringLiteral(m)).join(", ");
|
|
return `f.VerifyNoSignatureHelpForMarkersWithContext(t, ${context}, ${markerArgs})`;
|
|
}
|
|
|
|
function generateDocCommentTemplate({ marker, expectedOffset, expectedText, generateReturnInDocTemplate }: VerifyDocCommentTemplateCmd): string {
|
|
return `f.VerifyJSDocCompletion(t, ${marker}, ${expectedOffset}, ${expectedText}, ${generateReturnInDocTemplate ?? "nil"})`;
|
|
}
|
|
|
|
function generateNoDocCommentTemplate({ marker }: VerifyNoDocCommentTemplateCmd): string {
|
|
return `f.VerifyNoJSDocCompletion(t, ${marker})`;
|
|
}
|
|
|
|
function generateNavigateTo({ args }: VerifyNavToCmd): string {
|
|
return `f.VerifyWorkspaceSymbol(t, []*fourslash.VerifyWorkspaceSymbolCase{\n${args.join(", ")}})`;
|
|
}
|
|
|
|
function generateSemanticClassifications({ format, tokens }: VerifySemanticClassificationsCmd): string {
|
|
const tokensStr = tokens.map(t => `{Type: ${getGoStringLiteral(t.type)}, Text: ${getGoStringLiteral(t.text)}}`).join(",\n\t\t");
|
|
const maybeComma = tokens.length > 0 ? "," : "";
|
|
return `f.VerifySemanticTokens(t, []fourslash.SemanticToken{
|
|
${tokensStr}${maybeComma}
|
|
})`;
|
|
}
|
|
|
|
function generateCmd(cmd: Cmd, imports: Set<string>): string {
|
|
switch (cmd.kind) {
|
|
case "verifyCompletions":
|
|
return generateVerifyCompletions(cmd, imports);
|
|
case "verifyApplyCodeActionFromCompletion":
|
|
return generateVerifyApplyCodeActionFromCompletion(cmd);
|
|
case "verifyBaselineFindAllReferences":
|
|
return generateBaselineFindAllReferences(cmd);
|
|
case "verifyBaselineDocumentHighlights":
|
|
return generateBaselineDocumentHighlights(cmd);
|
|
case "verifyBaselineGoToDefinition":
|
|
case "verifyBaselineGoToType":
|
|
case "verifyBaselineGoToImplementation":
|
|
case "verifyBaselineGoToSourceDefinition":
|
|
return generateBaselineGoToDefinition(cmd);
|
|
case "verifyBaselineQuickInfo":
|
|
// Quick Info -> Hover
|
|
if (cmd.verbosityLevels && Object.keys(cmd.verbosityLevels).length > 0) {
|
|
const entries = Object.entries(cmd.verbosityLevels).map(
|
|
([marker, levels]) => `${getGoStringLiteral(marker)}: {${levels.join(", ")}}`,
|
|
).join(", ");
|
|
return `f.VerifyBaselineHoverWithVerbosity(t, map[string][]int{${entries}})`;
|
|
}
|
|
return `f.VerifyBaselineHover(t)`;
|
|
case "verifyBaselineSignatureHelp":
|
|
return `f.VerifyBaselineSignatureHelp(t)`;
|
|
case "verifyBaselineSmartSelection":
|
|
return `f.VerifyBaselineSelectionRanges(t)`;
|
|
case "verifyBaselineCallHierarchy":
|
|
return `f.VerifyBaselineCallHierarchy(t)`;
|
|
case "verifyLinkedEditing":
|
|
return `f.VerifyLinkedEditing(t, ${cmd.ranges})`;
|
|
case "goTo":
|
|
return generateGoToCommand(cmd);
|
|
case "edit":
|
|
case "format":
|
|
case "verifyContent":
|
|
return cmd.goStatement;
|
|
case "quickInfoAt":
|
|
case "quickInfoIs":
|
|
case "quickInfoExists":
|
|
case "notQuickInfoExists":
|
|
return generateQuickInfoCommand(cmd);
|
|
case "verifyOrganizeImports":
|
|
return generateOrganizeImports(cmd);
|
|
case "verifyBaselineRename":
|
|
case "verifyBaselineRenameAtRangesWithText":
|
|
return generateBaselineRename(cmd);
|
|
case "renameInfoSucceeded":
|
|
return `f.VerifyRenameSucceeded(t, ${cmd.preferences})`;
|
|
case "renameInfoFailed":
|
|
return `f.VerifyRenameFailed(t, ${cmd.preferences})`;
|
|
case "verifyGetEditsForFileRename":
|
|
return `f.VerifyWillRenameFilesEdits(t, ${cmd.oldPath}, ${cmd.newPath}, ${cmd.newFileContents}, ${cmd.preferences})`;
|
|
case "verifyBaselineInlayHints":
|
|
return generateBaselineInlayHints(cmd);
|
|
case "verifyBaselineLinkedEditing":
|
|
return `f.VerifyBaselineLinkedEditing(t)`;
|
|
case "verifyImportFixAtPosition":
|
|
return generateImportFixAtPosition(cmd);
|
|
case "verifyImportFixModuleSpecifiers":
|
|
return generateImportFixModuleSpecifiers(cmd);
|
|
case "verifyDiagnostics":
|
|
const funcName = cmd.isSuggestion ? "VerifySuggestionDiagnostics" : "VerifyNonSuggestionDiagnostics";
|
|
return `f.${funcName}(t, ${cmd.arg})`;
|
|
case "verifyBaselineDiagnostics":
|
|
return `f.VerifyBaselineNonSuggestionDiagnostics(t)`;
|
|
case "verifyNavigateTo":
|
|
return generateNavigateTo(cmd);
|
|
case "verifySignatureHelp":
|
|
return generateSignatureHelp(cmd);
|
|
case "verifyNoSignatureHelp":
|
|
return generateNoSignatureHelp(cmd);
|
|
case "verifySignatureHelpPresent":
|
|
return generateSignatureHelpPresent(cmd);
|
|
case "verifyNoSignatureHelpForTriggerReason":
|
|
return generateNoSignatureHelpForTriggerReason(cmd);
|
|
case "verifyOutliningSpans":
|
|
return generateVerifyOutliningSpans(cmd);
|
|
case "verifyNavigationTree":
|
|
return `f.VerifyBaselineDocumentSymbol(t)`;
|
|
case "verifyNumberOfErrorsInCurrentFile":
|
|
return `f.VerifyNumberOfErrorsInCurrentFile(t, ${cmd.expectedCount})`;
|
|
case "verifyNoErrors":
|
|
return `f.VerifyNoErrors(t)`;
|
|
case "verifyErrorExistsAtRange":
|
|
return `f.VerifyErrorExistsAtRange(t, ${cmd.range}, ${cmd.code}, ${getGoStringLiteral(cmd.message)})`;
|
|
case "verifyCurrentLineContentIs":
|
|
return `f.VerifyCurrentLineContent(t, ${getGoStringLiteral(cmd.text)})`;
|
|
case "verifyCurrentFileContentIs":
|
|
return `f.VerifyCurrentFileContent(t, ${getGoStringLiteral(cmd.text)})`;
|
|
case "verifyErrorExistsBetweenMarkers":
|
|
return `f.VerifyErrorExistsBetweenMarkers(t, ${getGoStringLiteral(cmd.startMarker)}, ${getGoStringLiteral(cmd.endMarker)})`;
|
|
case "verifyErrorExistsAfterMarker":
|
|
return `f.VerifyErrorExistsAfterMarker(t, ${getGoStringLiteral(cmd.markerName)})`;
|
|
case "verifyErrorExistsBeforeMarker":
|
|
return `f.VerifyErrorExistsBeforeMarker(t, ${getGoStringLiteral(cmd.markerName)})`;
|
|
case "verifyCodeFix":
|
|
return `f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{
|
|
Description: ${getGoStringLiteral(cmd.description)},
|
|
${
|
|
cmd.newRangeContent !== undefined
|
|
? `\tNewRangeContent: ${getGoMultiLineStringLiteral(cmd.newRangeContent)},`
|
|
: `\tNewFileContent: ${getGoMultiLineStringLiteral(cmd.newFileContent ?? "")},`
|
|
}
|
|
Index: ${cmd.index},${
|
|
cmd.applyChanges ? `
|
|
ApplyChanges: true,` : ``
|
|
}${
|
|
cmd.preferences !== "nil /*preferences*/" ? `
|
|
UserPreferences: ${cmd.preferences},` : ``
|
|
}
|
|
})`;
|
|
case "verifyCodeFixAvailable":
|
|
if (cmd.unavailableDescriptions.length > 0) {
|
|
return `f.VerifyCodeFixNotAvailable(t, ${cmd.unavailableDescriptions.map(d => getGoStringLiteral(d)).join(", ")})`;
|
|
}
|
|
if (cmd.expectNone) {
|
|
return `f.VerifyCodeFixNotAvailable(t)`;
|
|
}
|
|
if (cmd.descriptions.length === 0) {
|
|
return `f.VerifyCodeFixAvailable(t, nil)`;
|
|
}
|
|
return `f.VerifyCodeFixAvailable(t, []string{${cmd.descriptions.map(d => getGoStringLiteral(d)).join(", ")}})`;
|
|
case "verifyRangeAfterCodeFix":
|
|
return `f.VerifyRangeAfterCodeFix(t, ${getGoMultiLineStringLiteral(cmd.expectedText)}, ${cmd.includeWhiteSpace}, ${cmd.errorCode}, ${cmd.index})`;
|
|
case "verifyCodeFixAll":
|
|
return `f.VerifyCodeFixAll(t, fourslash.VerifyCodeFixAllOptions{
|
|
FixID: ${getGoStringLiteral(cmd.fixId)},
|
|
NewFileContent: ${getGoMultiLineStringLiteral(cmd.newFileContent)},
|
|
})`;
|
|
case "verifySemanticClassifications":
|
|
return generateSemanticClassifications(cmd);
|
|
case "verifyDocCommentTemplate":
|
|
return generateDocCommentTemplate(cmd);
|
|
case "verifyNoDocCommentTemplate":
|
|
return generateNoDocCommentTemplate(cmd);
|
|
case "forOf":
|
|
return `for _, ${cmd.variableName} := range ${cmd.rangeExpression} {
|
|
${cmd.bodyCommands.map(c => generateCmd(c, imports)).join("\n")}
|
|
}`;
|
|
default:
|
|
let neverCommand: never = cmd;
|
|
throw new Error(`Unknown command kind: ${neverCommand as Cmd["kind"]}`);
|
|
}
|
|
}
|
|
|
|
interface GoTest {
|
|
name: string;
|
|
content: string;
|
|
commands: Cmd[];
|
|
}
|
|
|
|
function generateGoTest(test: GoTest, isServer: boolean): string {
|
|
const testName = (test.name[0].toUpperCase() + test.name.substring(1)).replaceAll("-", "_").replaceAll(/[^a-zA-Z0-9_]/g, "");
|
|
const neededImports = new Set<string>();
|
|
neededImports.add(IMPORT_FOURSLASH);
|
|
neededImports.add(IMPORT_TESTUTIL);
|
|
const hasDocCommentTemplateCommands = test.commands.some(hasDocCommentTemplateCommand);
|
|
const content = hasDocCommentTemplateCommands && /@Filename: .*\.jsx?/i.test(test.content)
|
|
? prependLineToGoStringLiteral(test.content, "// @allowJs: true")
|
|
: test.content;
|
|
const commands = test.commands.map(cmd => generateCmd(cmd, neededImports)).join("\n");
|
|
// Scan the generated command code for package-qualified names that may come from
|
|
// parsed command fields (e.g. UserPreferences generated during parsing).
|
|
// These qualified names (core., ls., lsutil., lsproto.) are safe to detect via regex
|
|
// because they won't appear in TypeScript test content strings.
|
|
if (/\bcore\./.test(commands)) {
|
|
neededImports.add(IMPORT_CORE);
|
|
}
|
|
|
|
function prependLineToGoStringLiteral(literal: string, line: string): string {
|
|
if (!literal.startsWith("`")) {
|
|
throw new Error(`Expected raw Go string literal, got ${literal}`);
|
|
}
|
|
return "`" + line + "\n" + literal.slice(1);
|
|
}
|
|
if (/\bls\./.test(commands)) {
|
|
neededImports.add(IMPORT_LS);
|
|
}
|
|
if (/\blsutil\./.test(commands)) {
|
|
neededImports.add(IMPORT_LSUTIL);
|
|
}
|
|
if (/\blsproto\./.test(commands)) {
|
|
neededImports.add(IMPORT_LSPROTO);
|
|
}
|
|
// ToAny, DefaultCommitCharacters, and Completion* are Go-specific identifiers from
|
|
// the util package, safe to detect via regex since they won't appear in TypeScript test
|
|
// content. Other util symbols (like Ignored) are tracked explicitly during generation
|
|
// to avoid false positives from English words in test content strings.
|
|
if (/\bToAny\b|\bDefaultCommitCharacters\b|\bCompletion[A-Z]\w+\b/.test(commands)) {
|
|
neededImports.add(IMPORT_UTIL);
|
|
}
|
|
// Sort imports for deterministic output. Dot imports sort last.
|
|
const imports = Array.from(neededImports).sort((a, b) => {
|
|
const aDot = a.startsWith(".");
|
|
const bDot = b.startsWith(".");
|
|
if (aDot !== bDot) return aDot ? 1 : -1;
|
|
return a.localeCompare(b);
|
|
});
|
|
const template = `// Code generated by convertFourslash; DO NOT EDIT.
|
|
// To modify this test, run "npm run makemanual ${test.name}"
|
|
|
|
package fourslash_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
${imports.join("\n\t")}
|
|
)
|
|
|
|
func Test${testName}(t *testing.T) {
|
|
fourslash.SkipIfFailing(t)
|
|
t.Parallel()
|
|
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
|
|
const content = ${content}
|
|
${
|
|
hasDocCommentTemplateCommands ? `capabilities := fourslash.GetDefaultCapabilities()
|
|
capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = new(false)
|
|
f, done := fourslash.NewFourslash(t, capabilities, content)` : `f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)`
|
|
}
|
|
defer done()
|
|
${isServer ? `f.MarkTestAsStradaServer()\n` : ""}${commands}
|
|
}`;
|
|
return template;
|
|
}
|
|
|
|
function hasDocCommentTemplateCommand(cmd: Cmd): boolean {
|
|
if (cmd.kind === "verifyDocCommentTemplate" || cmd.kind === "verifyNoDocCommentTemplate") {
|
|
return true;
|
|
}
|
|
if (cmd.kind === "forOf") {
|
|
return cmd.bodyCommands.some(hasDocCommentTemplateCommand);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function getNodeOfKind<T extends ts.Node>(node: ts.Node, hasKind: (n: ts.Node) => n is T): T | undefined {
|
|
if (hasKind(node)) {
|
|
return node;
|
|
}
|
|
if (ts.isIdentifier(node)) {
|
|
const init = getInitializer(node);
|
|
if (init && hasKind(init)) {
|
|
return init;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function getObjectLiteralExpression(node: ts.Node): ts.ObjectLiteralExpression | undefined {
|
|
return getNodeOfKind(node, ts.isObjectLiteralExpression);
|
|
}
|
|
|
|
function getStringLiteralLike(node: ts.Node): ts.StringLiteralLike | undefined {
|
|
return getNodeOfKind(node, ts.isStringLiteralLike);
|
|
}
|
|
|
|
// Build a map from diagnostic property names (e.g. "Extract_base_class_to_variable")
|
|
// to their message text, by loading diagnosticMessages.json and applying the same
|
|
// key-generation algorithm used by TypeScript's processDiagnosticMessages script.
|
|
const diagnosticMessagesByPropName: Map<string, string> = (() => {
|
|
const messagesPath = path.resolve(import.meta.dirname, "../", "../", "../", "_submodules", "TypeScript", "src", "compiler", "diagnosticMessages.json");
|
|
const raw = JSON.parse(fs.readFileSync(messagesPath, "utf-8"));
|
|
const map = new Map<string, string>();
|
|
for (const messageText of Object.keys(raw)) {
|
|
const propName = messageText.split("").map((ch: string) => {
|
|
if (ch === "*") return "_Asterisk";
|
|
if (ch === "/") return "_Slash";
|
|
if (ch === ":") return "_Colon";
|
|
return /\w/.test(ch) ? ch : "_";
|
|
}).join("")
|
|
.replace(/_+/g, "_")
|
|
.replace(/^_(\D)/, "$1")
|
|
.replace(/_$/, "");
|
|
map.set(propName, messageText);
|
|
}
|
|
return map;
|
|
})();
|
|
|
|
// Resolve a description value from various expression forms:
|
|
// - String literal: "Add return type 'void'"
|
|
// - ts.Diagnostics.X.message property access
|
|
// - Variable identifier referencing a const string in the same file
|
|
function resolveDescriptionExpression(expr: ts.Expression, sourceFile: ts.SourceFile): string | undefined {
|
|
// String literal
|
|
const str = getStringLiteralLike(expr);
|
|
if (str) return str.text;
|
|
|
|
// [ts.Diagnostics.Foo.message, "arg0", "arg1", ...]
|
|
if (ts.isArrayLiteralExpression(expr) && expr.elements.length > 0) {
|
|
const [diagnostic] = expr.elements;
|
|
const template = resolveDescriptionExpression(diagnostic, sourceFile);
|
|
if (template) {
|
|
let message = template;
|
|
for (let i = 1; i < expr.elements.length; i++) {
|
|
const arg = resolveDescriptionExpression(expr.elements[i], sourceFile);
|
|
if (arg === undefined) {
|
|
return undefined;
|
|
}
|
|
message = message.replaceAll(`{${i - 1}}`, arg);
|
|
}
|
|
return message;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// ts.Diagnostics.Foo_bar.message
|
|
if (ts.isPropertyAccessExpression(expr) && expr.name.text === "message") {
|
|
const inner = expr.expression;
|
|
if (
|
|
ts.isPropertyAccessExpression(inner) && ts.isPropertyAccessExpression(inner.expression)
|
|
&& ts.isIdentifier(inner.expression.name) && inner.expression.name.text === "Diagnostics"
|
|
&& ts.isIdentifier(inner.name)
|
|
) {
|
|
const diagKey = inner.name.text;
|
|
const message = diagnosticMessagesByPropName.get(diagKey);
|
|
if (message) return message;
|
|
}
|
|
}
|
|
|
|
// Variable reference: look for a const string declaration in the same file
|
|
if (ts.isIdentifier(expr)) {
|
|
const varName = expr.text;
|
|
for (const stmt of sourceFile.statements) {
|
|
if (ts.isVariableStatement(stmt)) {
|
|
for (const decl of stmt.declarationList.declarations) {
|
|
if (ts.isIdentifier(decl.name) && decl.name.text === varName && decl.initializer) {
|
|
const initStr = getStringLiteralLike(decl.initializer);
|
|
if (initStr) return initStr.text;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
// Get the name of a property in an object literal, whether it's an identifier or string literal.
|
|
function getPropertyName(prop: ts.ObjectLiteralElementLike): string | undefined {
|
|
if (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) {
|
|
if (ts.isIdentifier(prop.name)) return prop.name.text;
|
|
if (ts.isStringLiteral(prop.name)) return prop.name.text;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function getNumericLiteral(node: ts.Node): ts.NumericLiteral | undefined {
|
|
return getNodeOfKind(node, ts.isNumericLiteral);
|
|
}
|
|
|
|
function getArrayLiteralExpression(node: ts.Node): ts.ArrayLiteralExpression | undefined {
|
|
return getNodeOfKind(node, ts.isArrayLiteralExpression);
|
|
}
|
|
|
|
// Parses expressions like 'string'.length or "string".length and returns the length value
|
|
function getStringLengthExpression(node: ts.Node): number | undefined {
|
|
if (ts.isPropertyAccessExpression(node) && node.name.text === "length") {
|
|
const stringLiteral = getStringLiteralLike(node.expression);
|
|
if (stringLiteral) {
|
|
return stringLiteral.text.length;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function getInitializer(name: ts.Identifier): ts.Expression | undefined {
|
|
const file = name.getSourceFile();
|
|
const varStmts = file.statements.filter(ts.isVariableStatement);
|
|
for (const varStmt of varStmts) {
|
|
const decls = varStmt.declarationList.declarations.filter(varDecl => {
|
|
if (ts.isIdentifier(varDecl.name)) {
|
|
return varDecl.name.text === name.text;
|
|
}
|
|
return false;
|
|
});
|
|
if (decls[0]) {
|
|
return decls[0].initializer;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
if (url.fileURLToPath(import.meta.url) == process.argv[1]) {
|
|
main().catch(e => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|
|
}
|