Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
219e300
Add missing ast.IsVariableDeclarationList test
ahejlsberg Aug 31, 2025
1288cdc
Handle rename in fourslash converter + skeleton verification functions
ahejlsberg Sep 3, 2025
fc64898
WIP: baselineRename
gabritto Sep 4, 2025
9118e60
port baseline rename tests
gabritto Sep 5, 2025
719ae9b
temporarily accept all baselines
gabritto Sep 5, 2025
5c32809
WIP: verify baselines during cleanup to handle multiple baseline calls
gabritto Sep 8, 2025
4047497
WIP: re-split baseline files per command
gabritto Sep 8, 2025
0983fa7
blank line fix for baselines, setup diffing for rename baselines
gabritto Sep 8, 2025
f5e8c7a
fix marker position in baselines
gabritto Sep 9, 2025
b0eba8b
more fixes
gabritto Sep 9, 2025
99c2f67
more fixes
gabritto Sep 10, 2025
2ff410d
accept baselines
gabritto Sep 10, 2025
62ad46c
update failing tests
gabritto Sep 10, 2025
0fb1a46
update makeManual
gabritto Sep 10, 2025
b723a67
improvements: rename info failed, parse rename preferences
gabritto Sep 10, 2025
fe0ca4a
add user preferences argument
gabritto Sep 11, 2025
0e15b6a
refactor
gabritto Sep 11, 2025
0814695
Merge branch 'main' into fourslash-rename
gabritto Sep 11, 2025
7dccc62
format
gabritto Sep 11, 2025
b91d0c9
delete unused baselines
gabritto Sep 11, 2025
fd226a3
fix casing in baseline files
gabritto Sep 11, 2025
66af090
skip fourslash if no submodule
gabritto Sep 11, 2025
75efe46
manually ported test
gabritto Sep 11, 2025
5c70193
Merge branch 'gabritto/fourslash-rename' of https://github.com/micros…
gabritto Sep 11, 2025
4f015c7
only trim CR
gabritto Sep 11, 2025
0093174
normalize command name for test output files
gabritto Sep 12, 2025
46d95f2
update baselines
gabritto Sep 12, 2025
46c7124
Merge branch 'main' into gabritto/fourslash-rename
gabritto Sep 12, 2025
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.endsWith("\r") ? line.slice(0, -1) : line);
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
2 changes: 2 additions & 0 deletions internal/fourslash/_scripts/manualTests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ completionListInClosedFunction05
completionsAtIncompleteObjectLiteralProperty
completionsSelfDeclaring1
completionsWithDeprecatedTag4
renameDefaultKeyword
renameForDefaultExport01
tsxCompletion12
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