Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
197 changes: 193 additions & 4 deletions internal/fourslash/_scripts/convertFourslash.mts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function parseFileContent(filename: string, content: string): GoTest | undefined
}

function getTestInput(content: string): string {
const lines = content.split("\n");
const lines = content.split("\n").map(line => line.trimEnd());
let testInput: string[] = [];
for (const line of lines) {
let newLine = "";
Expand All @@ -118,7 +118,14 @@ function getTestInput(content: string): string {
}

// chomp leading spaces
if (!testInput.some(line => line.length != 0 && !line.startsWith(" ") && !line.startsWith("// "))) {
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;
Expand Down Expand Up @@ -182,6 +189,13 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
// - `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....`
Expand Down Expand Up @@ -793,6 +807,151 @@ function parseBaselineGoToDefinitionArgs(args: readonly ts.Expression[]): [Verif
}];
}

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 = parseBaselineRenameArg(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 = parseBaselineRenameArg(arg)) {
newArgs.push(typedArg);
}
else {
return undefined;
}
}
return [{
kind: funcName === "baselineRenameAtRangesWithText" ? "verifyBaselineRenameAtRangesWithText" : "verifyBaselineRename",
args: newArgs,
preferences: preferences ? preferences : "nil /*preferences*/",
}];
}

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: PtrTo(${prop.initializer.getText()})`);
break;
case "quotePreference":
preferences.push(`QuotePreference: PtrTo(ls.QuotePreference(${prop.initializer.getText()}))`);
break;
}
}
else {
return undefined;
}
}
if (preferences.length === 0) {
return "nil /*preferences*/";
}
return `&ls.UserPreferences{${preferences.join(",")}}`;
}

function parseBaselineRenameArg(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 {
if (args.length !== 0) {
// All calls are currently empty!
Expand Down Expand Up @@ -1097,6 +1256,12 @@ interface VerifyBaselineSignatureHelpCmd {
kind: "verifyBaselineSignatureHelp";
}

interface VerifyBaselineRenameCmd {
kind: "verifyBaselineRename" | "verifyBaselineRenameAtRangesWithText";
args: string[];
preferences: string;
}

interface GoToCmd {
kind: "goTo";
// !!! `selectRange` and `rangeStart` require parsing variables and `test.ranges()[n]`
Expand All @@ -1116,6 +1281,11 @@ interface VerifyQuickInfoCmd {
docs?: string;
}

interface VerifyRenameInfoCmd {
kind: "renameInfoSucceeded" | "renameInfoFailed";
preferences: string;
}

type Cmd =
| VerifyCompletionsCmd
| VerifyBaselineFindAllReferencesCmd
Expand All @@ -1124,7 +1294,9 @@ type Cmd =
| VerifyBaselineSignatureHelpCmd
| GoToCmd
| EditCmd
| VerifyQuickInfoCmd;
| VerifyQuickInfoCmd
| VerifyBaselineRenameCmd
| VerifyRenameInfoCmd;

function generateVerifyCompletions({ marker, args, isNewIdentifierLocation }: VerifyCompletionsCmd): string {
let expectedList: string;
Expand Down Expand Up @@ -1185,6 +1357,15 @@ function generateQuickInfoCommand({ kind, marker, text, docs }: VerifyQuickInfoC
}
}

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":
Expand All @@ -1207,6 +1388,13 @@ function generateCmd(cmd: Cmd): string {
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"]}`);
Expand Down Expand Up @@ -1267,7 +1455,8 @@ function usesHelper(goTxt: string): boolean {
}
return goTxt.includes("Ignored")
|| goTxt.includes("DefaultCommitCharacters")
|| goTxt.includes("PtrTo");
|| goTxt.includes("PtrTo")
|| goTxt.includes("ToAny");
}

function getNodeOfKind<T extends ts.Node>(node: ts.Node, hasKind: (n: ts.Node) => n is T): T | undefined {
Expand Down
9 changes: 9 additions & 0 deletions internal/fourslash/_scripts/failingTests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ TestJsDocPropertyDescription6
TestJsDocPropertyDescription7
TestJsDocPropertyDescription8
TestJsDocPropertyDescription9
TestJsDocSee_rename1
TestJsDocTagsWithHyphen
TestJsQuickInfoGenerallyAcceptableSize
TestJsRequireQuickInfo
Expand All @@ -257,6 +258,7 @@ TestJsdocLink2
TestJsdocLink3
TestJsdocLink6
TestJsdocLink_findAllReferences1
TestJsdocLink_rename1
TestJsdocTemplatePrototypeCompletions
TestJsdocThrowsTagCompletion
TestJsdocTypedefTag
Expand Down Expand Up @@ -438,6 +440,13 @@ TestReferencesInComment
TestReferencesInEmptyFile
TestReferencesIsAvailableThroughGlobalNoCrash
TestRegexDetection
TestRenameCrossJsTs01
TestRenameForAliasingExport02
TestRenameFromNodeModulesDep1
TestRenameFromNodeModulesDep2
TestRenameFromNodeModulesDep3
TestRenameFromNodeModulesDep4
TestRenamePrivateFields
TestReverseMappedTypeQuickInfo
TestSelfReferencedExternalModule
TestSignatureHelpInferenceJsDocImportTag
Expand Down
29 changes: 24 additions & 5 deletions internal/fourslash/_scripts/makeManual.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const scriptsDir = import.meta.dirname;
const manualTestsPath = path.join(scriptsDir, "manualTests.txt");
const genDir = path.join(scriptsDir, "../", "tests", "gen");
const manualDir = path.join(scriptsDir, "../", "tests", "manual");
const submoduleDir = path.join(scriptsDir, "../../../", "_submodules", "TypeScript", "tests", "cases", "fourslash");

function main() {
const args = process.argv.slice(2);
Expand All @@ -17,17 +18,35 @@ function main() {
const testName = args[0];
const testFileName = testName;
const genTestFile = path.join(genDir, testFileName + "_test.go");
if (!fs.existsSync(genTestFile)) {
console.error(`Test file not found: '${genTestFile}'. Make sure the test exists in the gen directory first.`);
const submoduleTestFile = path.join(submoduleDir, testFileName + ".ts");
const submoduleServerTestFile = path.join(submoduleDir, "server", testFileName + ".ts");
let testKind: "gen" | "submodule" | "submoduleServer" | undefined;
if (fs.existsSync(genTestFile)) {
testKind = "gen";
}
else if (fs.existsSync(submoduleTestFile)) {
testKind = "submodule";
}
else if (fs.existsSync(submoduleServerTestFile)) {
testKind = "submoduleServer";
}

if (!testKind) {
console.error(
`Could not find test neither as '${genTestFile}', nor as '${submoduleTestFile}' or '${submoduleServerTestFile}'.` +
`Make sure the test exists in the gen directory or in the submodule.`,
);
process.exit(1);
}

if (!fs.existsSync(manualDir)) {
fs.mkdirSync(manualDir, { recursive: true });
}

const manualTestFile = path.join(manualDir, path.basename(genTestFile));
renameAndRemoveSkip(genTestFile, manualTestFile);
if (testKind === "gen") {
const manualTestFile = path.join(manualDir, path.basename(genTestFile));
markAsManual(genTestFile, manualTestFile);
}

let manualTests: string[] = [];
if (fs.existsSync(manualTestsPath)) {
Expand All @@ -42,7 +61,7 @@ function main() {
}
}

function renameAndRemoveSkip(genFilePath: string, manualFilePath: string) {
function markAsManual(genFilePath: string, manualFilePath: string) {
const content = fs.readFileSync(genFilePath, "utf-8");
const updatedContent = content.replace(/^\s*t\.Skip\(\)\s*$/m, "");
fs.writeFileSync(manualFilePath, updatedContent, "utf-8");
Expand Down
1 change: 1 addition & 0 deletions internal/fourslash/_scripts/manualTests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ completionsAtIncompleteObjectLiteralProperty
completionsSelfDeclaring1
completionsWithDeprecatedTag4
tsxCompletion12
renameForDefaultExport01
3 changes: 2 additions & 1 deletion internal/fourslash/_scripts/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"strict": true,
"noEmit": true,
"module": "nodenext",
"allowImportingTsExtensions": true
"allowImportingTsExtensions": true,
"noFallthroughCasesInSwitch": true
}
}
Loading
Loading