Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Use baselines for callHierarchy tests
  • Loading branch information
rbuckton committed Dec 18, 2019
commit 64351ee100d5134059e07dd79eb999c021d795c0
222 changes: 210 additions & 12 deletions src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ namespace FourSlash {
return ts.ScriptSnapshot.fromString(sourceText);
}

const enum CallHierarchyItemDirection {
Root,
Incoming,
Outgoing
}

export class TestState {
// Language service instance
private languageServiceAdapterHost: Harness.LanguageService.LanguageServiceAdapterHost;
Expand Down Expand Up @@ -1404,18 +1410,91 @@ namespace FourSlash {

private alignmentForExtraInfo = 50;

private spanInfoToString(spanInfo: ts.TextSpan, prefixString: string) {
private spanLines(file: FourSlashFile, spanInfo: ts.TextSpan, { selection = false, fullLines = false, lineNumbers = false } = {}) {
if (selection) {
fullLines = true;
}

let contextStartPos = spanInfo.start;
let contextEndPos = contextStartPos + spanInfo.length;
if (fullLines) {
if (contextStartPos > 0) {
while (contextStartPos > 1) {
const ch = file.content.charCodeAt(contextStartPos - 1);
if (ch === ts.CharacterCodes.lineFeed || ch === ts.CharacterCodes.carriageReturn) {
break;
}
contextStartPos--;
}
}
if (contextEndPos < file.content.length) {
while (contextEndPos < file.content.length - 1) {
const ch = file.content.charCodeAt(contextEndPos);
if (ch === ts.CharacterCodes.lineFeed || ch === ts.CharacterCodes.carriageReturn) {
break;
}
contextEndPos++;
}
}
}

let contextString: string;
let contextLineMap: number[];
let contextStart: ts.LineAndCharacter;
let contextEnd: ts.LineAndCharacter;
let selectionStart: ts.LineAndCharacter;
let selectionEnd: ts.LineAndCharacter;
let lineNumberPrefixLength: number;
if (lineNumbers) {
contextString = file.content;
contextLineMap = ts.computeLineStarts(contextString);
contextStart = ts.computeLineAndCharacterOfPosition(contextLineMap, contextStartPos);
contextEnd = ts.computeLineAndCharacterOfPosition(contextLineMap, contextEndPos);
selectionStart = ts.computeLineAndCharacterOfPosition(contextLineMap, spanInfo.start);
selectionEnd = ts.computeLineAndCharacterOfPosition(contextLineMap, ts.textSpanEnd(spanInfo));
lineNumberPrefixLength = (contextEnd.line + 1).toString().length + 2;
}
else {
contextString = file.content.substring(contextStartPos, contextEndPos);
contextLineMap = ts.computeLineStarts(contextString);
contextStart = { line: 0, character: 0 };
contextEnd = { line: contextLineMap.length - 1, character: 0 };
selectionStart = selection ? ts.computeLineAndCharacterOfPosition(contextLineMap, spanInfo.start - contextStartPos) : contextStart;
selectionEnd = selection ? ts.computeLineAndCharacterOfPosition(contextLineMap, ts.textSpanEnd(spanInfo) - contextStartPos) : contextEnd;
lineNumberPrefixLength = 0;
}

const output: string[] = [];
for (let lineNumber = contextStart.line; lineNumber <= contextEnd.line; lineNumber++) {
const spanLine = contextString.substring(contextLineMap[lineNumber], contextLineMap[lineNumber + 1]);
output.push(lineNumbers ? `${`${lineNumber + 1}: `.padStart(lineNumberPrefixLength, " ")}${spanLine}` : spanLine);
if (selection) {
if (lineNumber < selectionStart.line || lineNumber > selectionEnd.line) {
continue;
}

const isEmpty = selectionStart.line === selectionEnd.line && selectionStart.character === selectionEnd.character;
const selectionPadLength = lineNumber === selectionStart.line ? selectionStart.character : 0;
const selectionPad = " ".repeat(selectionPadLength + lineNumberPrefixLength);
const selectionLength = isEmpty ? 0 : Math.max(lineNumber < selectionEnd.line ? spanLine.trimRight().length - selectionPadLength : selectionEnd.character - selectionPadLength, 1);
const selectionLine = isEmpty ? "<" : "^".repeat(selectionLength);
output.push(`${selectionPad}${selectionLine}`);
}
}
return output;
}

private spanInfoToString(spanInfo: ts.TextSpan, prefixString: string, file: FourSlashFile = this.activeFile) {
let resultString = "SpanInfo: " + JSON.stringify(spanInfo);
if (spanInfo) {
const spanString = this.activeFile.content.substr(spanInfo.start, spanInfo.length);
const spanLineMap = ts.computeLineStarts(spanString);
for (let i = 0; i < spanLineMap.length; i++) {
const spanLines = this.spanLines(file, spanInfo);
for (let i = 0; i < spanLines.length; i++) {
if (!i) {
resultString += "\n";
}
resultString += prefixString + spanString.substring(spanLineMap[i], spanLineMap[i + 1]);
resultString += prefixString + spanLines[i];
}
resultString += "\n" + prefixString + ":=> (" + this.getLineColStringAtPosition(spanInfo.start) + ") to (" + this.getLineColStringAtPosition(ts.textSpanEnd(spanInfo)) + ")";
resultString += "\n" + prefixString + ":=> (" + this.getLineColStringAtPosition(spanInfo.start, file) + ") to (" + this.getLineColStringAtPosition(ts.textSpanEnd(spanInfo), file) + ")";
}

return resultString;
Expand Down Expand Up @@ -1694,13 +1773,13 @@ namespace FourSlash {
Harness.IO.log(stringify(help.items[help.selectedItemIndex]));
}

private getBaselineFileNameForInternalFourslashFile() {
private getBaselineFileNameForInternalFourslashFile(ext = ".baseline") {
return this.testData.globalOptions[MetadataOptionNames.baselineFile] ||
ts.getBaseFileName(this.activeFile.fileName).replace(ts.Extension.Ts, ".baseline");
ts.getBaseFileName(this.activeFile.fileName).replace(ts.Extension.Ts, ext);
}

private getBaselineFileNameForContainingTestFile() {
return ts.getBaseFileName(this.originalInputFileName).replace(ts.Extension.Ts, ".baseline");
private getBaselineFileNameForContainingTestFile(ext = ".baseline") {
return ts.getBaseFileName(this.originalInputFileName).replace(ts.Extension.Ts, ext);
}

private getSignatureHelp({ triggerReason }: FourSlashInterface.VerifySignatureHelpOptions): ts.SignatureHelpItems | undefined {
Expand Down Expand Up @@ -3130,6 +3209,125 @@ namespace FourSlash {
Harness.IO.log(stringify(codeFixes));
}

private formatCallHierarchyItemSpan(file: FourSlashFile, span: ts.TextSpan, prefix: string, trailingPrefix = prefix) {
const startLc = this.languageServiceAdapterHost.positionToLineAndCharacter(file.fileName, span.start);
const endLc = this.languageServiceAdapterHost.positionToLineAndCharacter(file.fileName, ts.textSpanEnd(span));
const lines = this.spanLines(file, span, { fullLines: true, lineNumbers: true, selection: true });
let text = "";
text += `${prefix}╭ ${file.fileName}:${startLc.line + 1}:${startLc.character + 1}-${endLc.line + 1}:${endLc.character + 1}\n`;
for (const line of lines) {
text += `${prefix}│ ${line.trimRight()}\n`;
}
text += `${trailingPrefix}╰\n`;
return text;
}

private formatCallHierarchyItemSpans(file: FourSlashFile, spans: ts.TextSpan[], prefix: string, trailingPrefix = prefix) {
let text = "";
for (let i = 0; i < spans.length; i++) {
text += this.formatCallHierarchyItemSpan(file, spans[i], prefix, i < spans.length - 1 ? prefix : trailingPrefix);
}
return text;
}

private formatCallHierarchyItem(file: FourSlashFile, callHierarchyItem: ts.CallHierarchyItem, direction: CallHierarchyItemDirection, seen: ts.Map<boolean>, prefix: string, trailingPrefix: string = prefix) {
const key = `${callHierarchyItem.file}|${JSON.stringify(callHierarchyItem.span)}|${direction}`;
const alreadySeen = seen.has(key);
seen.set(key, true);

const incomingCalls =
direction === CallHierarchyItemDirection.Outgoing ? { result: "skip" } as const :
alreadySeen ? { result: "seen" } as const :
{ result: "show", values: this.languageService.provideCallHierarchyIncomingCalls(callHierarchyItem.file, callHierarchyItem.selectionSpan.start) } as const;

const outgoingCalls =
direction === CallHierarchyItemDirection.Incoming ? { result: "skip" } as const :
alreadySeen ? { result: "seen" } as const :
{ result: "show", values: this.languageService.provideCallHierarchyOutgoingCalls(callHierarchyItem.file, callHierarchyItem.selectionSpan.start) } as const;

let text = "";
text += `${prefix}╭ name: ${callHierarchyItem.name}\n`;
text += `${prefix}├ kind: ${callHierarchyItem.kind}\n`;
text += `${prefix}├ span:\n`;
text += this.formatCallHierarchyItemSpan(file, callHierarchyItem.span, `${prefix}│ `);
text += `${prefix}├ selectionSpan:\n`;
text += this.formatCallHierarchyItemSpan(file, callHierarchyItem.selectionSpan, `${prefix}│ `,
incomingCalls.result !== "skip" || outgoingCalls.result !== "skip" ? `${prefix}│ ` :
`${trailingPrefix}╰ `);

if (incomingCalls.result === "seen") {
if (outgoingCalls.result === "skip") {
text += `${trailingPrefix}╰ incoming: ...\n`;
}
else {
text += `${prefix}├ incoming: ...\n`;
}
}
else if (incomingCalls.result === "show") {
if (!ts.some(incomingCalls.values)) {
if (outgoingCalls.result === "skip") {
text += `${trailingPrefix}╰ incoming: none\n`;
}
else {
text += `${prefix}├ incoming: none\n`;
}
}
else {
text += `${prefix}├ incoming:\n`;
for (let i = 0; i < incomingCalls.values.length; i++) {
const incomingCall = incomingCalls.values[i];
const file = this.findFile(incomingCall.from.file);
text += `${prefix}│ ╭ from:\n`;
text += this.formatCallHierarchyItem(file, incomingCall.from, CallHierarchyItemDirection.Incoming, seen, `${prefix}│ │ `);
text += `${prefix}│ ├ fromSpans:\n`;
text += this.formatCallHierarchyItemSpans(file, incomingCall.fromSpans, `${prefix}│ │ `,
i < incomingCalls.values.length - 1 ? `${prefix}│ ╰ ` :
outgoingCalls.result !== "skip" ? `${prefix}│ ╰ ` :
`${trailingPrefix}╰ ╰ `);
}
}
}

if (outgoingCalls.result === "seen") {
text += `${trailingPrefix}╰ outgoing: ...\n`;
}
else if (outgoingCalls.result === "show") {
if (!ts.some(outgoingCalls.values)) {
text += `${trailingPrefix}╰ outgoing: none\n`;
}
else {
text += `${prefix}├ outgoing:\n`;
for (let i = 0; i < outgoingCalls.values.length; i++) {
const outgoingCall = outgoingCalls.values[i];
const file = this.findFile(outgoingCall.to.file);
text += `${prefix}│ ╭ to:\n`;
text += this.formatCallHierarchyItem(file, outgoingCall.to, CallHierarchyItemDirection.Outgoing, seen, `${prefix}│ │ `);
text += `${prefix}│ ├ fromSpans:\n`;
text += this.formatCallHierarchyItemSpans(file, outgoingCall.fromSpans, `${prefix}│ │ `,
i < outgoingCalls.values.length - 1 ? `${prefix}│ ╰ ` :
`${trailingPrefix}╰ ╰ `);
}
}
}
return text;
}

private formatCallHierarchy(callHierarchyItem: ts.CallHierarchyItem | undefined) {
let text = "";
if (callHierarchyItem) {
const file = this.findFile(callHierarchyItem.file);
text += this.formatCallHierarchyItem(file, callHierarchyItem, CallHierarchyItemDirection.Root, ts.createMap(), "");
}
return text;
}

public baselineCallHierarchy() {
const baselineFile = this.getBaselineFileNameForContainingTestFile(".callHierarchy.txt");
const callHierarchyItem = this.languageService.prepareCallHierarchy(this.activeFile.fileName, this.currentCaretPosition)!;
const text = callHierarchyItem ? this.formatCallHierarchy(callHierarchyItem) : "none";
Harness.Baseline.runBaseline(baselineFile, text);
}

public verifyCallHierarchy(options: false | FourSlashInterface.VerifyCallHierarchyOptions | Range) {
const callHierarchyItem = this.languageService.prepareCallHierarchy(this.activeFile.fileName, this.currentCaretPosition)!;
this.assertCallHierarchyItemMatches(callHierarchyItem, options);
Expand Down Expand Up @@ -3345,8 +3543,8 @@ namespace FourSlash {
return this.tryFindFileWorker(name).file !== undefined;
}

private getLineColStringAtPosition(position: number) {
const pos = this.languageServiceAdapterHost.positionToLineAndCharacter(this.activeFile.fileName, position);
private getLineColStringAtPosition(position: number, file: FourSlashFile = this.activeFile) {
const pos = this.languageServiceAdapterHost.positionToLineAndCharacter(file.fileName, position);
return `line ${(pos.line + 1)}, col ${pos.character}`;
}

Expand Down
4 changes: 4 additions & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,10 @@ namespace FourSlashInterface {
this.state.getEditsForFileRename(options);
}

public baselineCallHierarchy() {
this.state.baselineCallHierarchy();
}

public callHierarchy(options: false | FourSlash.Range | VerifyCallHierarchyOptions) {
this.state.verifyCallHierarchy(options);
}
Expand Down
62 changes: 62 additions & 0 deletions tests/baselines/reference/callHierarchyAccessor.callHierarchy.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
╭ name: bar
├ kind: getter
├ span:
│ ╭ /tests/cases/fourslash/callHierarchyAccessor.ts:6:5-8:6
│ │ 6: get bar() {
│ │ ^^^^^^^^^^^
│ │ 7: return baz();
│ │ ^^^^^^^^^^^^^^^^^^^^^
│ │ 8: }
│ │ ^^^^^
│ ╰
├ selectionSpan:
│ ╭ /tests/cases/fourslash/callHierarchyAccessor.ts:6:9-6:12
│ │ 6: get bar() {
│ │ ^^^
│ ╰
├ incoming:
│ ╭ from:
│ │ ╭ name: foo
│ │ ├ kind: function
│ │ ├ span:
│ │ │ ╭ /tests/cases/fourslash/callHierarchyAccessor.ts:1:1-3:2
│ │ │ │ 1: function foo() {
│ │ │ │ ^^^^^^^^^^^^^^^^
│ │ │ │ 2: new C().bar;
│ │ │ │ ^^^^^^^^^^^^^^^^
│ │ │ │ 3: }
│ │ │ │ ^
│ │ │ ╰
│ │ ├ selectionSpan:
│ │ │ ╭ /tests/cases/fourslash/callHierarchyAccessor.ts:1:10-1:13
│ │ │ │ 1: function foo() {
│ │ │ │ ^^^
│ │ │ ╰
│ │ ╰ incoming: none
│ ├ fromSpans:
│ │ ╭ /tests/cases/fourslash/callHierarchyAccessor.ts:2:13-2:16
│ │ │ 2: new C().bar;
│ │ │ ^^^
│ ╰ ╰
├ outgoing:
│ ╭ to:
│ │ ╭ name: baz
│ │ ├ kind: function
│ │ ├ span:
│ │ │ ╭ /tests/cases/fourslash/callHierarchyAccessor.ts:11:1-12:2
│ │ │ │ 11: function baz() {
│ │ │ │ ^^^^^^^^^^^^^^^^
│ │ │ │ 12: }
│ │ │ │ ^
│ │ │ ╰
│ │ ├ selectionSpan:
│ │ │ ╭ /tests/cases/fourslash/callHierarchyAccessor.ts:11:10-11:13
│ │ │ │ 11: function baz() {
│ │ │ │ ^^^
│ │ │ ╰
│ │ ╰ outgoing: none
│ ├ fromSpans:
│ │ ╭ /tests/cases/fourslash/callHierarchyAccessor.ts:7:16-7:19
│ │ │ 7: return baz();
│ │ │ ^^^
╰ ╰ ╰
Loading