Skip to content

Commit bc21346

Browse files
committed
Refactor fourslash testing for codeFixes
1 parent cbaea99 commit bc21346

2 files changed

Lines changed: 125 additions & 33 deletions

File tree

src/harness/fourslash.ts

Lines changed: 120 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ namespace FourSlash {
4444

4545
markers: Marker[];
4646

47+
/**
48+
* Inserted in source files by surrounding desired text
49+
* in a range with `[|` and `|]`. For example,
50+
*
51+
* [|text in range|]
52+
*
53+
* is a range with `text in range` "selected".
54+
*/
4755
ranges: Range[];
4856
}
4957

@@ -84,6 +92,15 @@ namespace FourSlash {
8492
end: number;
8593
}
8694

95+
export interface ErrorIdentifier {
96+
code: number;
97+
/**
98+
* In a file where there is more than one error with code `code`, `count` refers
99+
* to which 0-indexed error, sorted by order of occurence, to consider.
100+
*/
101+
count: number;
102+
}
103+
87104
export import IndentStyle = ts.IndentStyle;
88105

89106
const entityMap = ts.createMap({
@@ -1575,11 +1592,11 @@ namespace FourSlash {
15751592
let runningOffset = 0;
15761593
edits = edits.sort((a, b) => a.span.start - b.span.start);
15771594
// Get a snapshot of the content of the file so we can make sure any formatting edits didn't destroy non-whitespace characters
1578-
const oldContent = this.getFileContent(this.activeFile.fileName);
1579-
for (let j = 0; j < edits.length; j++) {
1580-
this.languageServiceAdapterHost.editScript(fileName, edits[j].span.start + runningOffset, ts.textSpanEnd(edits[j].span) + runningOffset, edits[j].newText);
1581-
this.updateMarkersForEdit(fileName, edits[j].span.start + runningOffset, ts.textSpanEnd(edits[j].span) + runningOffset, edits[j].newText);
1582-
const change = (edits[j].span.start - ts.textSpanEnd(edits[j].span)) + edits[j].newText.length;
1595+
const oldContent = this.getFileContent(fileName);
1596+
for (const edit of edits) {
1597+
this.languageServiceAdapterHost.editScript(fileName, edit.span.start + runningOffset, ts.textSpanEnd(edit.span) + runningOffset, edit.newText);
1598+
this.updateMarkersForEdit(fileName, edit.span.start + runningOffset, ts.textSpanEnd(edit.span) + runningOffset, edit.newText);
1599+
const change = (edit.span.start - ts.textSpanEnd(edit.span)) + edit.newText.length;
15831600
runningOffset += change;
15841601
// TODO: Consider doing this at least some of the time for higher fidelity. Currently causes a failure (bug 707150)
15851602
// this.languageService.getScriptLexicalStructure(fileName);
@@ -1595,6 +1612,12 @@ namespace FourSlash {
15951612
return runningOffset;
15961613
}
15971614

1615+
private applyCodeAction(action: ts.CodeAction): void {
1616+
for (const filechange of action.changes) {
1617+
this.applyEdits(filechange.fileName, filechange.textChanges, /*isFormattingEdit*/ false);
1618+
}
1619+
}
1620+
15981621
public copyFormatOptions(): ts.FormatCodeSettings {
15991622
return ts.clone(this.formatCodeSettings);
16001623
}
@@ -2019,45 +2042,105 @@ namespace FourSlash {
20192042
}
20202043
}
20212044

2022-
private getCodeFixes(errorCode?: number) {
2045+
/**
2046+
* Compares expected text to the text that would be in the sole range
2047+
* (ie: [|...|]) in the file after applying the codefix corresponding
2048+
* to the error with errorCode, or of the sole error in the source file.
2049+
*
2050+
* Because codefixes are only applied on the working file, it is unsafe
2051+
* to apply this more than once (consider a refactoring across files).
2052+
*/
2053+
public verifyCodeFixAtPosition(expectedText: string, errorCode?: number) {
2054+
const ranges = this.getRanges();
2055+
if (ranges.length !== 1) {
2056+
this.raiseError("Exactly one range should be specified in the testfile.");
2057+
}
2058+
20232059
const fileName = this.activeFile.fileName;
2024-
const diagnostics = this.getDiagnostics(fileName);
2060+
const codeFix: ts.CodeAction = this.getCodeFix(fileName, errorCode ? { code: errorCode, count: 0 } : undefined);
20252061

2026-
if (diagnostics.length === 0) {
2027-
this.raiseError("Errors expected.");
2062+
if (!codeFix) {
2063+
this.raiseError("Should find exactly one codefix.");
20282064
}
20292065

2030-
if (diagnostics.length > 1 && errorCode !== undefined) {
2031-
this.raiseError("When there's more than one error, you must specify the errror to fix.");
2066+
const fileChange = ts.find(codeFix.changes, change => change.fileName === fileName);
2067+
if (!fileChange) {
2068+
this.raiseError("CodeFix found doesn't provide any changes in this file.");
20322069
}
20332070

2034-
const diagnostic = !errorCode ? diagnostics[0] : ts.find(diagnostics, d => d.code == errorCode);
2071+
this.applyEdits(fileChange.fileName, fileChange.textChanges, /*isFormattingEdit*/ false);
2072+
const actualText = this.rangeText(ranges[0]);
20352073

2036-
return this.languageService.getCodeFixesAtPosition(fileName, diagnostic.start, diagnostic.length, [diagnostic.code]);
2074+
if (this.removeWhitespace(actualText) !== this.removeWhitespace(expectedText)) {
2075+
this.raiseError(`Actual text doesn't match expected text. Actual: '${actualText}' Expected: '${expectedText}'`);
2076+
}
20372077
}
20382078

2039-
public verifyCodeFixAtPosition(expectedText: string, errorCode?: number) {
2040-
const ranges = this.getRanges();
2041-
if (ranges.length == 0) {
2042-
this.raiseError("At least one range should be specified in the testfile.");
2079+
/**
2080+
* Applies fixes for the errors in fileName and compares the results to
2081+
* expectedContents after all fixes have been applied.
2082+
*
2083+
* Note: applying one codefix may generate another (eg: remove duplicate implements
2084+
* may generate an extends -> interface conversion fix).
2085+
* @param expectedContents The contents of the file after the fixes are applied.
2086+
* @param fileName The file to check. If not supplied, the current open file is used.
2087+
* @param errorsToFix An array of errors for which quickfixes will be applied. If not
2088+
* supplied, all codefixes in the file are applied until none are left, starting from
2089+
* the first available codefix.
2090+
*
2091+
*/
2092+
public verifyFileAfterCodeFix(expectedContents: string, fileName?: string, errorsToFix?: ErrorIdentifier[]) {
2093+
fileName = fileName ? fileName : this.activeFile.fileName;
2094+
2095+
if (errorsToFix) {
2096+
for (const error of errorsToFix) {
2097+
const fix = this.getCodeFix(fileName, error);
2098+
if (fix === undefined) {
2099+
this.raiseError(`Couldn't find the ${error.count}'th error with code ${error.code}.`);
2100+
}
2101+
this.applyCodeAction(fix);
2102+
}
20432103
}
2044-
2045-
const actual = this.getCodeFixes(errorCode);
2046-
2047-
if (!actual || actual.length == 0) {
2048-
this.raiseError("No codefixes returned.");
2104+
else {
2105+
let fix: ts.CodeAction;
2106+
while (fix = this.getCodeFix(fileName)) {
2107+
this.applyCodeAction(fix);
2108+
}
20492109
}
20502110

2051-
if (actual.length > 1) {
2052-
this.raiseError("More than 1 codefix returned.");
2111+
const actualContents: string = this.getFileContent(fileName);
2112+
if (this.removeWhitespace(actualContents) !== this.removeWhitespace(expectedContents)) {
2113+
this.raiseError(`Actual text doesn't match expected text. Actual:\n${actualContents}\n\nExpected:\n${expectedContents}`);
20532114
}
2115+
}
20542116

2055-
this.applyEdits(actual[0].changes[0].fileName, actual[0].changes[0].textChanges, /*isFormattingEdit*/ false);
2056-
const actualText = this.rangeText(ranges[0]);
2117+
/**
2118+
* Rerieves a codefix satisfying the parameters, or undefined if no such codefix is found.
2119+
* @param fileName Path to file where error should be retrieved from.
2120+
* @param error We get the `error.count`'th codefix with code `error.code`.
2121+
*
2122+
* If undefined, we get the first codefix available.
2123+
*/
2124+
private getCodeFix(fileName: string, error?: ErrorIdentifier): ts.CodeAction | undefined {
2125+
const diagnostics: ts.Diagnostic[] = this.getDiagnostics(fileName);
2126+
const errorCount = error ? error.count : 0;
20572127

2058-
if (this.removeWhitespace(actualText) !== this.removeWhitespace(expectedText)) {
2059-
this.raiseError(`Actual text doesn't match expected text. Actual: '${actualText}' Expected: '${expectedText}'`);
2128+
let countSeen = 0;
2129+
for (const diagnostic of diagnostics) {
2130+
if (error && error.code !== diagnostic.code) {
2131+
continue;
2132+
}
2133+
const action = this.languageService.getCodeFixesAtPosition(fileName, diagnostic.start, diagnostic.length, [diagnostic.code]);
2134+
if (action) {
2135+
if (action.length > errorCount - countSeen) {
2136+
return action[errorCount - countSeen];
2137+
}
2138+
else {
2139+
countSeen += action.length;
2140+
}
2141+
}
20602142
}
2143+
return undefined;
20612144
}
20622145

20632146
public verifyDocCommentTemplate(expected?: ts.TextInsertion) {
@@ -2344,14 +2427,14 @@ namespace FourSlash {
23442427
}
23452428

23462429
public verifyCodeFixAvailable(negative: boolean, errorCode?: number) {
2347-
const fixes = this.getCodeFixes(errorCode);
2430+
const codeFix = this.getCodeFix(this.activeFile.fileName, errorCode ? { code: errorCode, count: 0 } : undefined);
23482431

2349-
if (negative && fixes && fixes.length > 0) {
2350-
this.raiseError(`verifyCodeFixAvailable failed - expected no fixes, actual: ${fixes.length}`);
2432+
if (negative && codeFix) {
2433+
this.raiseError(`verifyCodeFixAvailable failed - expected no fixes but found one.`);
23512434
}
23522435

2353-
if (!negative && (fixes === undefined || fixes.length === 0)) {
2354-
this.raiseError(`verifyCodeFixAvailable failed - expected code fixes, actual: 0`);
2436+
if (!(negative || codeFix)) {
2437+
this.raiseError(`verifyCodeFixAvailable failed - expected code fixes but none found.`);
23552438
}
23562439
}
23572440

@@ -3329,6 +3412,10 @@ namespace FourSlashInterface {
33293412
this.state.verifyCodeFixAtPosition(expectedText, errorCode);
33303413
}
33313414

3415+
public fileAfterCodeFixes(expectedContents: string, fileName?: string, errorsToFix?: FourSlash.ErrorIdentifier[]): void {
3416+
this.state.verifyFileAfterCodeFix(expectedContents, fileName, errorsToFix);
3417+
}
3418+
33323419
public navigationBar(json: any) {
33333420
this.state.verifyNavigationBar(json);
33343421
}

tests/cases/fourslash/fourslash.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ declare namespace FourSlashInterface {
9898
start: number;
9999
end: number;
100100
}
101+
interface ErrorIdentifier {
102+
code: number;
103+
count: number
104+
}
101105
class test_ {
102106
markers(): Marker[];
103107
markerNames(): string[];
@@ -210,6 +214,7 @@ declare namespace FourSlashInterface {
210214
DocCommentTemplate(expectedText: string, expectedOffset: number, empty?: boolean): void;
211215
noDocCommentTemplate(): void;
212216
codeFixAtPosition(expectedText: string, errorCode?: number): void;
217+
fileAfterCodeFixes(expectedContents: string, fileName?: string, errorsToFix?: ErrorIdentifier[]): void;
213218

214219
navigationBar(json: any): void;
215220
navigationTree(json: any): void;

0 commit comments

Comments
 (0)