1577 lines
60 KiB
TypeScript
1577 lines
60 KiB
TypeScript
import * as cp from "child_process";
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
import * as ts from "typescript";
|
|
import * as url from "url";
|
|
import which from "which";
|
|
|
|
const stradaFourslashPath = path.resolve(import.meta.dirname, "../", "../", "../", "_submodules", "TypeScript", "tests", "cases", "fourslash");
|
|
|
|
let inputFileSet: Set<string> | undefined;
|
|
|
|
const failingTestsPath = path.join(import.meta.dirname, "failingTests.txt");
|
|
const manualTestsPath = path.join(import.meta.dirname, "manualTests.txt");
|
|
|
|
const outputDir = path.join(import.meta.dirname, "../", "tests", "gen");
|
|
|
|
const unparsedFiles: string[] = [];
|
|
|
|
function getFailingTests(): Set<string> {
|
|
const failingTestsList = fs.readFileSync(failingTestsPath, "utf-8").split("\n").map(line => line.trim().substring(4)).filter(line => line.length > 0);
|
|
return new Set(failingTestsList);
|
|
}
|
|
|
|
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 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 });
|
|
|
|
parseTypeScriptFiles(getFailingTests(), getManualTests(), stradaFourslashPath);
|
|
console.log(unparsedFiles.join("\n"));
|
|
const gofmt = which.sync("go");
|
|
cp.execFileSync(gofmt, ["tool", "mvdan.cc/gofumpt", "-lang=go1.25", "-w", outputDir]);
|
|
}
|
|
|
|
function parseTypeScriptFiles(failingTests: Set<string>, 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(failingTests, manualTests, filePath);
|
|
}
|
|
else if (file.endsWith(".ts") && !manualTests.has(file.slice(0, -3))) {
|
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
const test = parseFileContent(file, content);
|
|
if (test) {
|
|
const testContent = generateGoTest(failingTests, test);
|
|
const testPath = path.join(outputDir, `${test.name}_test.go`);
|
|
fs.writeFileSync(testPath, testContent, "utf-8");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function parseFileContent(filename: string, content: string): GoTest | undefined {
|
|
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(".ts", "").replace(".", ""),
|
|
content: getTestInput(content),
|
|
commands: [],
|
|
};
|
|
for (const statement of statements) {
|
|
const result = parseFourslashStatement(statement);
|
|
if (!result) {
|
|
unparsedFiles.push(filename);
|
|
return undefined;
|
|
}
|
|
else {
|
|
goTest.commands.push(...result);
|
|
}
|
|
}
|
|
return goTest;
|
|
}
|
|
|
|
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")}\``;
|
|
}
|
|
|
|
/**
|
|
* Parses a Strada fourslash statement and returns the corresponding Corsa commands.
|
|
* @returns an array of commands if the statement is a valid fourslash command, or `false` if the statement could not be parsed.
|
|
*/
|
|
function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
|
|
if (ts.isVariableStatement(statement)) {
|
|
// variable declarations (for ranges and markers), e.g. `const range = test.ranges()[0];`
|
|
return [];
|
|
}
|
|
else if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression)) {
|
|
const callExpression = statement.expression;
|
|
if (!ts.isPropertyAccessExpression(callExpression.expression)) {
|
|
console.error(`Expected property access expression, got ${callExpression.expression.getText()}`);
|
|
return undefined;
|
|
}
|
|
const namespace = callExpression.expression.expression;
|
|
const func = callExpression.expression.name;
|
|
if (!(ts.isIdentifier(namespace) || namespace.getText() === "verify.not") || !ts.isIdentifier(func)) {
|
|
console.error(`Expected identifiers for namespace and function, got ${namespace.getText()} and ${func.getText()}`);
|
|
return undefined;
|
|
}
|
|
if (!ts.isIdentifier(namespace)) {
|
|
switch (func.text) {
|
|
case "quickInfoExists":
|
|
return parseQuickInfoArgs("notQuickInfoExists", callExpression.arguments);
|
|
}
|
|
console.error(`Unrecognized fourslash statement: ${statement.getText()}`);
|
|
return undefined;
|
|
}
|
|
// `verify.(...)`
|
|
if (namespace.text === "verify") {
|
|
switch (func.text) {
|
|
case "completions":
|
|
// `verify.completions(...)`
|
|
return parseVerifyCompletionsArgs(callExpression.arguments);
|
|
case "quickInfoAt":
|
|
case "quickInfoExists":
|
|
case "quickInfoIs":
|
|
case "quickInfos":
|
|
// `verify.quickInfo...(...)`
|
|
return parseQuickInfoArgs(func.text, 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 "baselineGoToDefinition":
|
|
case "baselineGetDefinitionAtPosition":
|
|
// Both of these take the same arguments, but differ in that...
|
|
// - `verify.baselineGoToDefinition(...)` called getDefinitionAndBoundSpan
|
|
// - `verify.baselineGetDefinitionAtPosition(...)` called getDefinitionAtPosition
|
|
// LSP doesn't have two separate commands though. It's unclear how we would model bound spans though.
|
|
return parseBaselineGoToDefinitionArgs(callExpression.arguments);
|
|
case "baselineRename":
|
|
case "baselineRenameAtRangesWithText":
|
|
// `verify.baselineRename...(...)`
|
|
return parseBaselineRenameArgs(func.text, callExpression.arguments);
|
|
case "renameInfoSucceeded":
|
|
case "renameInfoFailed":
|
|
return parseRenameInfo(func.text, callExpression.arguments);
|
|
}
|
|
}
|
|
// `goTo....`
|
|
if (namespace.text === "goTo") {
|
|
return parseGoToArgs(callExpression.arguments, func.text);
|
|
}
|
|
// `edit....`
|
|
if (namespace.text === "edit") {
|
|
const result = parseEditStatement(func.text, callExpression.arguments);
|
|
if (!result) {
|
|
return undefined;
|
|
}
|
|
return [result];
|
|
}
|
|
// !!! other fourslash commands
|
|
}
|
|
console.error(`Unrecognized fourslash statement: ${statement.getText()}`);
|
|
return undefined;
|
|
}
|
|
|
|
function parseEditStatement(funcName: string, args: readonly ts.Expression[]): EditCmd | undefined {
|
|
switch (funcName) {
|
|
case "insert":
|
|
case "paste":
|
|
case "insertLine": {
|
|
let arg0;
|
|
if (args.length !== 1 || !(arg0 = getStringLiteralLike(args[0]))) {
|
|
console.error(`Expected a single string literal argument in edit.${funcName}, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
return undefined;
|
|
}
|
|
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]))) {
|
|
console.error(`Expected a single string literal argument in edit.insert, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
return undefined;
|
|
}
|
|
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))) {
|
|
console.error(`Expected numeric literal argument in edit.backspace, got ${arg.getText()}`);
|
|
return undefined;
|
|
}
|
|
return {
|
|
kind: "edit",
|
|
goStatement: `f.Backspace(t, ${arg0.text})`,
|
|
};
|
|
}
|
|
return {
|
|
kind: "edit",
|
|
goStatement: `f.Backspace(t, 1)`,
|
|
};
|
|
}
|
|
default:
|
|
console.error(`Unrecognized edit function: ${funcName}`);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function getGoStringLiteral(text: string): string {
|
|
return `${JSON.stringify(text)}`;
|
|
}
|
|
|
|
function parseGoToArgs(args: readonly ts.Expression[], funcName: string): GoToCmd[] | undefined {
|
|
switch (funcName) {
|
|
case "marker": {
|
|
const arg = args[0];
|
|
if (arg === undefined) {
|
|
return [{
|
|
kind: "goTo",
|
|
funcName: "marker",
|
|
args: [`""`],
|
|
}];
|
|
}
|
|
let strArg;
|
|
if (!(strArg = getStringLiteralLike(arg))) {
|
|
console.error(`Unrecognized argument in goTo.marker: ${arg.getText()}`);
|
|
return undefined;
|
|
}
|
|
return [{
|
|
kind: "goTo",
|
|
funcName: "marker",
|
|
args: [getGoStringLiteral(strArg.text)],
|
|
}];
|
|
}
|
|
case "file": {
|
|
if (args.length !== 1) {
|
|
console.error(`Expected a single argument in goTo.file, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
return undefined;
|
|
}
|
|
let arg0;
|
|
if (arg0 = getStringLiteralLike(args[0])) {
|
|
return [{
|
|
kind: "goTo",
|
|
funcName: "file",
|
|
args: [getGoStringLiteral(arg0.text)],
|
|
}];
|
|
}
|
|
else if (arg0 = getNumericLiteral(args[0])) {
|
|
return [{
|
|
kind: "goTo",
|
|
funcName: "fileNumber",
|
|
args: [arg0.text],
|
|
}];
|
|
}
|
|
console.error(`Expected string or number literal argument in goTo.file, got ${args[0].getText()}`);
|
|
return undefined;
|
|
}
|
|
case "position": {
|
|
let arg0;
|
|
if (args.length !== 1 || !(arg0 = getNumericLiteral(args[0]))) {
|
|
console.error(`Expected a single numeric literal argument in goTo.position, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
return undefined;
|
|
}
|
|
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]))) {
|
|
console.error(`Expected two string literal arguments in goTo.select, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
return undefined;
|
|
}
|
|
return [{
|
|
kind: "goTo",
|
|
funcName: "select",
|
|
args: [getGoStringLiteral(arg0.text), getGoStringLiteral(arg1.text)],
|
|
}];
|
|
}
|
|
default:
|
|
console.error(`Unrecognized goTo function: ${funcName}`);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function parseVerifyCompletionsArgs(args: readonly ts.Expression[]): VerifyCompletionsCmd[] | undefined {
|
|
const cmds = [];
|
|
for (const arg of args) {
|
|
const result = parseVerifyCompletionArg(arg);
|
|
if (!result) {
|
|
return undefined;
|
|
}
|
|
cmds.push(result);
|
|
}
|
|
return cmds;
|
|
}
|
|
|
|
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): VerifyCompletionsCmd | undefined {
|
|
let marker: string | undefined;
|
|
let goArgs: VerifyCompletionsArgs | undefined;
|
|
const obj = getObjectLiteralExpression(arg);
|
|
if (!obj) {
|
|
console.error(`Expected object literal expression in verify.completions, got ${arg.getText()}`);
|
|
return undefined;
|
|
}
|
|
let isNewIdentifierLocation: true | undefined;
|
|
for (const prop of obj.properties) {
|
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
|
|
console.error(`Expected property assignment with identifier name, got ${prop.getText()}`);
|
|
return undefined;
|
|
}
|
|
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)) {
|
|
console.error(`Expected string literal in marker array, got ${elem.getText()}`);
|
|
return undefined; // !!! parse marker arrays?
|
|
}
|
|
marker += `${getGoStringLiteral(elem.text)}, `;
|
|
}
|
|
marker += "}";
|
|
}
|
|
else if (markerInit = getObjectLiteralExpression(init)) {
|
|
// !!! parse marker objects?
|
|
console.error(`Unrecognized marker initializer: ${markerInit.getText()}`);
|
|
return undefined;
|
|
}
|
|
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 {
|
|
console.error(`Unrecognized marker initializer: ${init.getText()}`);
|
|
return undefined;
|
|
}
|
|
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))) {
|
|
console.error(`Expected array literal expression for completion.globalsPlus items, got ${maybeItems.getText()}`);
|
|
return undefined;
|
|
}
|
|
expected = `${funcName}(\n[]fourslash.CompletionsExpectedItem{`;
|
|
for (const elem of items.elements) {
|
|
const result = parseExpectedCompletionItem(elem);
|
|
if (!result) {
|
|
return undefined;
|
|
}
|
|
expected += "\n" + result + ",";
|
|
}
|
|
expected += "\n}";
|
|
if (maybeOpts) {
|
|
let opts;
|
|
if (!(opts = getObjectLiteralExpression(maybeOpts))) {
|
|
console.error(`Expected object literal expression for completion.globalsPlus options, got ${maybeOpts.getText()}`);
|
|
return undefined;
|
|
}
|
|
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 {
|
|
console.error(`Expected boolean literal for noLib, got ${noLib.initializer.getText()}`);
|
|
return undefined;
|
|
}
|
|
}
|
|
else {
|
|
console.error(`Expected noLib property in completion.globalsPlus options, got ${maybeOpts.getText()}`);
|
|
return undefined;
|
|
}
|
|
}
|
|
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);
|
|
if (!result) {
|
|
return undefined;
|
|
}
|
|
expected += "\n" + result + ",";
|
|
}
|
|
}
|
|
else {
|
|
const result = parseExpectedCompletionItem(init);
|
|
if (!result) {
|
|
return undefined;
|
|
}
|
|
expected += "\n" + result + ",";
|
|
}
|
|
expected += "\n}";
|
|
}
|
|
if (propName === "includes") {
|
|
(goArgs ??= {}).includes = expected;
|
|
}
|
|
else if (propName === "exact") {
|
|
(goArgs ??= {}).exact = expected;
|
|
}
|
|
else {
|
|
(goArgs ??= {}).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)) {
|
|
return undefined; // Shouldn't happen
|
|
}
|
|
excludes += `\n${getGoStringLiteral(elem.text)},`;
|
|
}
|
|
}
|
|
excludes += "\n}";
|
|
(goArgs ??= {}).excludes = excludes;
|
|
break;
|
|
}
|
|
case "isNewIdentifierLocation":
|
|
if (init.kind === ts.SyntaxKind.TrueKeyword) {
|
|
isNewIdentifierLocation = true;
|
|
}
|
|
break;
|
|
case "preferences":
|
|
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:
|
|
console.error(`Unrecognized expected completion item: ${init.parent.getText()}`);
|
|
return undefined;
|
|
}
|
|
}
|
|
return {
|
|
kind: "verifyCompletions",
|
|
marker: marker ? marker : "nil",
|
|
args: goArgs,
|
|
isNewIdentifierLocation: isNewIdentifierLocation,
|
|
};
|
|
}
|
|
|
|
function parseExpectedCompletionItem(expr: ts.Expression): string | undefined {
|
|
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 isDeprecated = false; // !!!
|
|
let isOptional = false;
|
|
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)) {
|
|
console.error(`Expected property assignment with identifier name for completion item, got ${prop.getText()}`);
|
|
return undefined;
|
|
}
|
|
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 {
|
|
console.error(`Expected string literal for completion item name, got ${init.getText()}`);
|
|
return undefined;
|
|
}
|
|
break;
|
|
}
|
|
case "sortText":
|
|
const result = parseSortText(init);
|
|
if (!result) {
|
|
return undefined;
|
|
}
|
|
itemProps.push(`SortText: PtrTo(string(${result})),`);
|
|
if (result === "ls.SortTextOptionalMember") {
|
|
isOptional = true;
|
|
}
|
|
break;
|
|
case "insertText": {
|
|
let insertTextInit;
|
|
if (insertTextInit = getStringLiteralLike(init)) {
|
|
insertText = insertTextInit.text;
|
|
}
|
|
else if (init.getText() === "undefined") {
|
|
// Ignore
|
|
}
|
|
else {
|
|
console.error(`Expected string literal for insertText, got ${init.getText()}`);
|
|
return undefined;
|
|
}
|
|
break;
|
|
}
|
|
case "filterText": {
|
|
let filterTextInit;
|
|
if (filterTextInit = getStringLiteralLike(init)) {
|
|
filterText = filterTextInit.text;
|
|
}
|
|
else {
|
|
console.error(`Expected string literal for filterText, got ${init.getText()}`);
|
|
return undefined;
|
|
}
|
|
break;
|
|
}
|
|
case "isRecommended":
|
|
if (init.kind === ts.SyntaxKind.TrueKeyword) {
|
|
itemProps.push(`Preselect: PtrTo(true),`);
|
|
}
|
|
break;
|
|
case "kind":
|
|
const kind = parseKind(init);
|
|
if (!kind) {
|
|
return undefined;
|
|
}
|
|
itemProps.push(`Kind: PtrTo(${kind}),`);
|
|
break;
|
|
case "kindModifiers":
|
|
const modifiers = parseKindModifiers(init);
|
|
if (!modifiers) {
|
|
return undefined;
|
|
}
|
|
({ isDeprecated, isOptional, extensions } = modifiers);
|
|
break;
|
|
case "text": {
|
|
let textInit;
|
|
if (textInit = getStringLiteralLike(init)) {
|
|
itemProps.push(`Detail: PtrTo(${getGoStringLiteral(textInit.text)}),`);
|
|
}
|
|
else {
|
|
console.error(`Expected string literal for text, got ${init.getText()}`);
|
|
return undefined;
|
|
}
|
|
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 {
|
|
console.error(`Expected string literal for documentation, got ${init.getText()}`);
|
|
return undefined;
|
|
}
|
|
break;
|
|
}
|
|
case "isFromUncheckedFile":
|
|
break; // Ignored
|
|
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;
|
|
}
|
|
default:
|
|
console.error(`Unrecognized property in expected completion item: ${propName}`);
|
|
return undefined; // Unsupported property
|
|
}
|
|
}
|
|
if (!name) {
|
|
return undefined; // Shouldn't happen
|
|
}
|
|
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: PtrTo(${getGoStringLiteral(filterText)}),`);
|
|
if (insertText) itemProps.unshift(`InsertText: PtrTo(${getGoStringLiteral(insertText)}),`);
|
|
itemProps.unshift(`Label: ${getGoStringLiteral(name!)},`);
|
|
return `&lsproto.CompletionItem{\n${itemProps.join("\n")}}`;
|
|
}
|
|
console.error(`Expected string literal or object literal for expected completion item, got ${expr.getText()}`);
|
|
return undefined; // Unsupported expression type
|
|
}
|
|
|
|
function parseBaselineFindAllReferencesArgs(args: readonly ts.Expression[]): [VerifyBaselineFindAllReferencesCmd] | undefined {
|
|
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 {
|
|
console.error(`Unrecognized argument in verify.baselineFindAllReferences: ${arg.getText()}`);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyBaselineFindAllReferences",
|
|
markers: newArgs,
|
|
}];
|
|
}
|
|
|
|
function parseBaselineDocumentHighlightsArgs(args: readonly ts.Expression[]): [VerifyBaselineDocumentHighlightsCmd] | undefined {
|
|
const newArgs: string[] = [];
|
|
let preferences: string | undefined;
|
|
for (const arg of args) {
|
|
let strArg;
|
|
if (strArg = getArrayLiteralExpression(arg)) {
|
|
for (const elem of strArg.elements) {
|
|
const newArg = parseBaselineMarkerOrRangeArg(elem);
|
|
if (!newArg) {
|
|
return undefined;
|
|
}
|
|
newArgs.push(newArg);
|
|
}
|
|
}
|
|
else if (ts.isObjectLiteralExpression(arg)) {
|
|
// !!! todo when multiple files supported in lsp
|
|
}
|
|
else if (strArg = parseBaselineMarkerOrRangeArg(arg)) {
|
|
newArgs.push(strArg);
|
|
}
|
|
else {
|
|
console.error(`Unrecognized argument in verify.baselineDocumentHighlights: ${arg.getText()}`);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
if (newArgs.length === 0) {
|
|
newArgs.push("ToAny(f.Ranges())...");
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyBaselineDocumentHighlights",
|
|
args: newArgs,
|
|
preferences: preferences ? preferences : "nil /*preferences*/",
|
|
}];
|
|
}
|
|
|
|
function parseBaselineGoToDefinitionArgs(args: readonly ts.Expression[]): [VerifyBaselineGoToDefinitionCmd] | undefined {
|
|
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: "verifyBaselineGoToDefinition",
|
|
markers: [],
|
|
ranges: true,
|
|
}];
|
|
}
|
|
else {
|
|
console.error(`Unrecognized argument in verify.baselineGoToDefinition: ${arg.getText()}`);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
return [{
|
|
kind: "verifyBaselineGoToDefinition",
|
|
markers: newArgs,
|
|
}];
|
|
}
|
|
|
|
function parseRenameInfo(funcName: "renameInfoSucceeded" | "renameInfoFailed", args: readonly ts.Expression[]): [VerifyRenameInfoCmd] | undefined {
|
|
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)) {
|
|
console.error(`Expected object literal expression for preferences, got ${prefArg.getText()}`);
|
|
return undefined;
|
|
}
|
|
const parsedPreferences = parseUserPreferences(prefArg);
|
|
if (!parsedPreferences) {
|
|
console.error(`Unrecognized user preferences in ${funcName}: ${prefArg.getText()}`);
|
|
return undefined;
|
|
}
|
|
}
|
|
return [{ kind: funcName, preferences }];
|
|
}
|
|
|
|
function parseBaselineRenameArgs(funcName: string, args: readonly ts.Expression[]): [VerifyBaselineRenameCmd] | undefined {
|
|
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);
|
|
if (!newArg) {
|
|
return undefined;
|
|
}
|
|
newArgs.push(newArg);
|
|
}
|
|
}
|
|
else if (ts.isObjectLiteralExpression(arg)) {
|
|
preferences = parseUserPreferences(arg);
|
|
if (!preferences) {
|
|
console.error(`Unrecognized user preferences in verify.baselineRename: ${arg.getText()}`);
|
|
return undefined;
|
|
}
|
|
continue;
|
|
}
|
|
else if (typedArg = parseBaselineMarkerOrRangeArg(arg)) {
|
|
newArgs.push(typedArg);
|
|
}
|
|
else {
|
|
return undefined;
|
|
}
|
|
}
|
|
return [{
|
|
kind: funcName === "baselineRenameAtRangesWithText" ? "verifyBaselineRenameAtRangesWithText" : "verifyBaselineRename",
|
|
args: newArgs,
|
|
preferences: preferences ? preferences : "nil /*preferences*/",
|
|
}];
|
|
}
|
|
|
|
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 | undefined {
|
|
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":
|
|
preferences.push(`QuotePreference: ls.QuotePreference(${prop.initializer.getText()})`);
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
return undefined;
|
|
}
|
|
}
|
|
if (preferences.length === 0) {
|
|
return "nil /*preferences*/";
|
|
}
|
|
return `&ls.UserPreferences{${preferences.join(",")}}`;
|
|
}
|
|
|
|
function parseBaselineMarkerOrRangeArg(arg: ts.Expression): string | undefined {
|
|
if (ts.isStringLiteral(arg)) {
|
|
return getGoStringLiteral(arg.text);
|
|
}
|
|
else if (ts.isIdentifier(arg) || (ts.isElementAccessExpression(arg) && ts.isIdentifier(arg.expression))) {
|
|
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?.getText().includes("ranges")) {
|
|
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) {
|
|
// `const [range_0, ..., range_n, ...] = test.ranges();` and arg is `range_n`
|
|
if (elem.dotDotDotToken === undefined) {
|
|
return `f.Ranges()[${i}]`;
|
|
}
|
|
// `const [range_0, ..., ...rest] = test.ranges();` and arg is `rest[n]`
|
|
if (ts.isElementAccessExpression(arg)) {
|
|
return `f.Ranges()[${i + parseInt(arg.argumentExpression!.getText())}]`;
|
|
}
|
|
// `const [range_0, ..., ...rest] = test.ranges();` and arg is `rest`
|
|
return `ToAny(f.Ranges()[${i}:])...`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const init = getNodeOfKind(arg, ts.isCallExpression);
|
|
if (init) {
|
|
const result = getRangesByTextArg(init);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
else if (ts.isCallExpression(arg)) {
|
|
const result = getRangesByTextArg(arg);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
}
|
|
console.error(`Unrecognized argument in verify.baselineRename: ${arg.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;
|
|
}
|
|
|
|
function parseBaselineQuickInfo(args: ts.NodeArray<ts.Expression>): VerifyBaselineQuickInfoCmd[] | undefined {
|
|
if (args.length !== 0) {
|
|
// !!!
|
|
return undefined;
|
|
}
|
|
return [{
|
|
kind: "verifyBaselineQuickInfo",
|
|
}];
|
|
}
|
|
|
|
function parseQuickInfoArgs(funcName: string, args: readonly ts.Expression[]): VerifyQuickInfoCmd[] | undefined {
|
|
// We currently don't support 'expectedTags'.
|
|
switch (funcName) {
|
|
case "quickInfoAt": {
|
|
if (args.length < 1 || args.length > 3) {
|
|
console.error(`Expected 1 or 2 arguments in quickInfoIs, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
return undefined;
|
|
}
|
|
let arg0;
|
|
if (!(arg0 = getStringLiteralLike(args[0]))) {
|
|
console.error(`Expected string literal for first argument in quickInfoAt, got ${args[0].getText()}`);
|
|
return undefined;
|
|
}
|
|
const marker = getGoStringLiteral(arg0.text);
|
|
let text: string | undefined;
|
|
let arg1;
|
|
if (args[1]) {
|
|
if (!(arg1 = getStringLiteralLike(args[1]))) {
|
|
console.error(`Expected string literal for second argument in quickInfoAt, got ${args[1].getText()}`);
|
|
return undefined;
|
|
}
|
|
text = getGoStringLiteral(arg1.text);
|
|
}
|
|
let docs: string | undefined;
|
|
let arg2;
|
|
if (args[2]) {
|
|
if (!(arg2 = getStringLiteralLike(args[2])) && args[2].getText() !== "undefined") {
|
|
console.error(`Expected string literal or undefined for third argument in quickInfoAt, got ${args[2].getText()}`);
|
|
return undefined;
|
|
}
|
|
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]))) {
|
|
console.error(`Expected a single object literal argument in quickInfos, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
return undefined;
|
|
}
|
|
for (const prop of arg0.properties) {
|
|
if (!ts.isPropertyAssignment(prop)) {
|
|
console.error(`Expected property assignment in quickInfos, got ${prop.getText()}`);
|
|
return undefined;
|
|
}
|
|
if (!(ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name) || ts.isNumericLiteral(prop.name))) {
|
|
console.error(`Expected identifier or literal for property name in quickInfos, got ${prop.name.getText()}`);
|
|
return undefined;
|
|
}
|
|
const marker = getGoStringLiteral(prop.name.text);
|
|
let text: string | undefined;
|
|
let docs: string | undefined;
|
|
let init;
|
|
if (init = getArrayLiteralExpression(prop.initializer)) {
|
|
if (init.elements.length !== 2) {
|
|
console.error(`Expected two elements in array literal for quickInfos property, got ${init.getText()}`);
|
|
return undefined;
|
|
}
|
|
let textExp, docsExp;
|
|
if (!(textExp = getStringLiteralLike(init.elements[0])) || !(docsExp = getStringLiteralLike(init.elements[1]))) {
|
|
console.error(`Expected string literals in array literal for quickInfos property, got ${init.getText()}`);
|
|
return undefined;
|
|
}
|
|
text = getGoStringLiteral(textExp.text);
|
|
docs = getGoStringLiteral(docsExp.text);
|
|
}
|
|
else if (init = getStringLiteralLike(prop.initializer)) {
|
|
text = getGoStringLiteral(init.text);
|
|
}
|
|
else {
|
|
console.error(`Expected string literal or array literal for quickInfos property, got ${prop.initializer.getText()}`);
|
|
return undefined;
|
|
}
|
|
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) {
|
|
console.error(`Expected 1 or 2 arguments in quickInfoIs, got ${args.map(arg => arg.getText()).join(", ")}`);
|
|
return undefined;
|
|
}
|
|
let arg0;
|
|
if (!(arg0 = getStringLiteralLike(args[0]))) {
|
|
console.error(`Expected string literal for first argument in quickInfoIs, got ${args[0].getText()}`);
|
|
return undefined;
|
|
}
|
|
const text = getGoStringLiteral(arg0.text);
|
|
let docs: string | undefined;
|
|
if (args[1]) {
|
|
let arg1;
|
|
if (!(arg1 = getStringLiteralLike(args[1]))) {
|
|
console.error(`Expected string literal for second argument in quickInfoIs, got ${args[1].getText()}`);
|
|
return undefined;
|
|
}
|
|
docs = getGoStringLiteral(arg1.text);
|
|
}
|
|
return [{
|
|
kind: "quickInfoIs",
|
|
text,
|
|
docs,
|
|
}];
|
|
}
|
|
}
|
|
console.error(`Unrecognized quick info function: ${funcName}`);
|
|
return undefined;
|
|
}
|
|
|
|
function parseBaselineSignatureHelp(args: ts.NodeArray<ts.Expression>): Cmd {
|
|
if (args.length !== 0) {
|
|
// All calls are currently empty!
|
|
throw new Error("Expected no arguments in verify.baselineSignatureHelp");
|
|
}
|
|
return {
|
|
kind: "verifyBaselineSignatureHelp",
|
|
};
|
|
}
|
|
|
|
function parseKind(expr: ts.Expression): string | undefined {
|
|
if (!ts.isStringLiteral(expr)) {
|
|
console.error(`Expected string literal for kind, got ${expr.getText()}`);
|
|
return undefined;
|
|
}
|
|
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[]; } | undefined {
|
|
if (!ts.isStringLiteral(expr)) {
|
|
console.error(`Expected string literal for kind modifiers, got ${expr.getText()}`);
|
|
return undefined;
|
|
}
|
|
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,
|
|
};
|
|
}
|
|
|
|
function parseSortText(expr: ts.Expression): string | undefined {
|
|
if (ts.isCallExpression(expr) && expr.expression.getText() === "completion.SortText.Deprecated") {
|
|
return `ls.DeprecateSortText(${parseSortText(expr.arguments[0])})`;
|
|
}
|
|
const text = expr.getText();
|
|
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:
|
|
console.error(`Unrecognized sort text: ${text}`);
|
|
return undefined; // !!! support deprecated/obj literal prop/etc
|
|
}
|
|
}
|
|
|
|
interface VerifyCompletionsCmd {
|
|
kind: "verifyCompletions";
|
|
marker: string;
|
|
isNewIdentifierLocation?: true;
|
|
args?: VerifyCompletionsArgs | "nil";
|
|
}
|
|
|
|
interface VerifyCompletionsArgs {
|
|
includes?: string;
|
|
excludes?: string;
|
|
exact?: string;
|
|
unsorted?: string;
|
|
}
|
|
|
|
interface VerifyBaselineFindAllReferencesCmd {
|
|
kind: "verifyBaselineFindAllReferences";
|
|
markers: string[];
|
|
ranges?: boolean;
|
|
}
|
|
|
|
interface VerifyBaselineFindAllReferencesCmd {
|
|
kind: "verifyBaselineFindAllReferences";
|
|
markers: string[];
|
|
ranges?: boolean;
|
|
}
|
|
|
|
interface VerifyBaselineGoToDefinitionCmd {
|
|
kind: "verifyBaselineGoToDefinition";
|
|
markers: string[];
|
|
ranges?: boolean;
|
|
}
|
|
|
|
interface VerifyBaselineQuickInfoCmd {
|
|
kind: "verifyBaselineQuickInfo";
|
|
}
|
|
|
|
interface VerifyBaselineSignatureHelpCmd {
|
|
kind: "verifyBaselineSignatureHelp";
|
|
}
|
|
|
|
interface VerifyBaselineRenameCmd {
|
|
kind: "verifyBaselineRename" | "verifyBaselineRenameAtRangesWithText";
|
|
args: string[];
|
|
preferences: string;
|
|
}
|
|
|
|
interface VerifyBaselineDocumentHighlightsCmd {
|
|
kind: "verifyBaselineDocumentHighlights";
|
|
args: 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 VerifyQuickInfoCmd {
|
|
kind: "quickInfoIs" | "quickInfoAt" | "quickInfoExists" | "notQuickInfoExists";
|
|
marker?: string;
|
|
text?: string;
|
|
docs?: string;
|
|
}
|
|
|
|
interface VerifyRenameInfoCmd {
|
|
kind: "renameInfoSucceeded" | "renameInfoFailed";
|
|
preferences: string;
|
|
}
|
|
|
|
type Cmd =
|
|
| VerifyCompletionsCmd
|
|
| VerifyBaselineFindAllReferencesCmd
|
|
| VerifyBaselineDocumentHighlightsCmd
|
|
| VerifyBaselineGoToDefinitionCmd
|
|
| VerifyBaselineQuickInfoCmd
|
|
| VerifyBaselineSignatureHelpCmd
|
|
| GoToCmd
|
|
| EditCmd
|
|
| VerifyQuickInfoCmd
|
|
| VerifyBaselineRenameCmd
|
|
| VerifyRenameInfoCmd;
|
|
|
|
function generateVerifyCompletions({ marker, args, isNewIdentifierLocation }: VerifyCompletionsCmd): string {
|
|
let expectedList: string;
|
|
if (args === "nil") {
|
|
expectedList = "nil";
|
|
}
|
|
else {
|
|
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")}
|
|
},
|
|
}`;
|
|
}
|
|
return `f.VerifyCompletions(t, ${marker}, ${expectedList})`;
|
|
}
|
|
|
|
function generateBaselineFindAllReferences({ markers, ranges }: VerifyBaselineFindAllReferencesCmd): string {
|
|
if (ranges || markers.length === 0) {
|
|
return `f.VerifyBaselineFindAllReferences(t)`;
|
|
}
|
|
return `f.VerifyBaselineFindAllReferences(t, ${markers.join(", ")})`;
|
|
}
|
|
|
|
function generateBaselineDocumentHighlights({ args, preferences }: VerifyBaselineDocumentHighlightsCmd): string {
|
|
return `f.VerifyBaselineDocumentHighlights(t, ${preferences}, ${args.join(", ")})`;
|
|
}
|
|
|
|
function generateBaselineGoToDefinition({ markers, ranges }: VerifyBaselineGoToDefinitionCmd): string {
|
|
if (ranges || markers.length === 0) {
|
|
return `f.VerifyBaselineGoToDefinition(t)`;
|
|
}
|
|
return `f.VerifyBaselineGoToDefinition(t, ${markers.join(", ")})`;
|
|
}
|
|
|
|
function generateGoToCommand({ funcName, args }: GoToCmd): string {
|
|
const funcNameCapitalized = funcName.charAt(0).toUpperCase() + funcName.slice(1);
|
|
return `f.GoTo${funcNameCapitalized}(t, ${args.join(", ")})`;
|
|
}
|
|
|
|
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 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 generateCmd(cmd: Cmd): string {
|
|
switch (cmd.kind) {
|
|
case "verifyCompletions":
|
|
return generateVerifyCompletions(cmd);
|
|
case "verifyBaselineFindAllReferences":
|
|
return generateBaselineFindAllReferences(cmd);
|
|
case "verifyBaselineDocumentHighlights":
|
|
return generateBaselineDocumentHighlights(cmd);
|
|
case "verifyBaselineGoToDefinition":
|
|
return generateBaselineGoToDefinition(cmd);
|
|
case "verifyBaselineQuickInfo":
|
|
// Quick Info -> Hover
|
|
return `f.VerifyBaselineHover(t)`;
|
|
case "verifyBaselineSignatureHelp":
|
|
return `f.VerifyBaselineSignatureHelp(t)`;
|
|
case "goTo":
|
|
return generateGoToCommand(cmd);
|
|
case "edit":
|
|
return cmd.goStatement;
|
|
case "quickInfoAt":
|
|
case "quickInfoIs":
|
|
case "quickInfoExists":
|
|
case "notQuickInfoExists":
|
|
return generateQuickInfoCommand(cmd);
|
|
case "verifyBaselineRename":
|
|
case "verifyBaselineRenameAtRangesWithText":
|
|
return generateBaselineRename(cmd);
|
|
case "renameInfoSucceeded":
|
|
return `f.VerifyRenameSucceeded(t, ${cmd.preferences})`;
|
|
case "renameInfoFailed":
|
|
return `f.VerifyRenameFailed(t, ${cmd.preferences})`;
|
|
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(failingTests: Set<string>, test: GoTest): string {
|
|
const testName = (test.name[0].toUpperCase() + test.name.substring(1)).replaceAll("-", "_");
|
|
const content = test.content;
|
|
const commands = test.commands.map(cmd => generateCmd(cmd)).join("\n");
|
|
const imports = [`"github.com/microsoft/typescript-go/internal/fourslash"`];
|
|
// Only include these imports if the commands use them to avoid unused import errors.
|
|
if (commands.includes("core.")) {
|
|
imports.unshift(`"github.com/microsoft/typescript-go/internal/core"`);
|
|
}
|
|
if (commands.includes("ls.")) {
|
|
imports.push(`"github.com/microsoft/typescript-go/internal/ls"`);
|
|
}
|
|
if (commands.includes("lsproto.")) {
|
|
imports.push(`"github.com/microsoft/typescript-go/internal/lsp/lsproto"`);
|
|
}
|
|
if (usesFourslashUtil(commands)) {
|
|
imports.push(`. "github.com/microsoft/typescript-go/internal/fourslash/tests/util"`);
|
|
}
|
|
imports.push(`"github.com/microsoft/typescript-go/internal/testutil"`);
|
|
const template = `package fourslash_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
${imports.join("\n\t")}
|
|
)
|
|
|
|
func Test${testName}(t *testing.T) {
|
|
t.Parallel()
|
|
${failingTests.has(testName) ? "t.Skip()" : ""}
|
|
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
|
|
const content = ${content}
|
|
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
|
|
${commands}
|
|
}`;
|
|
return template;
|
|
}
|
|
|
|
function usesFourslashUtil(goTxt: string): boolean {
|
|
for (const [_, constant] of completionConstants) {
|
|
if (goTxt.includes(constant)) {
|
|
return true;
|
|
}
|
|
}
|
|
for (const [_, constant] of completionPlus) {
|
|
if (goTxt.includes(constant)) {
|
|
return true;
|
|
}
|
|
}
|
|
return goTxt.includes("Ignored")
|
|
|| goTxt.includes("DefaultCommitCharacters")
|
|
|| goTxt.includes("PtrTo")
|
|
|| goTxt.includes("ToAny");
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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();
|
|
}
|