Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
6 changes: 4 additions & 2 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1245,8 +1245,10 @@ namespace ts {
return result;
}

export function group<T>(values: readonly T[], getGroupId: (value: T) => string): readonly (readonly T[])[] {
return arrayFrom(arrayToMultiMap(values, getGroupId).values());
export function group<T>(values: readonly T[], getGroupId: (value: T) => string): readonly (readonly T[])[];
export function group<T, R>(values: readonly T[], getGroupId: (value: T) => string, resultSelector: (values: readonly T[]) => R): R[];
export function group<T>(values: readonly T[], getGroupId: (value: T) => string, resultSelector: (values: readonly T[]) => readonly T[] = identity): readonly (readonly T[])[] {
return arrayFrom(arrayToMultiMap(values, getGroupId).values(), resultSelector);
}

export function clone<T>(object: T): T {
Expand Down
45 changes: 45 additions & 0 deletions src/harness/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,51 @@ namespace ts.server {
return notImplemented();
}

private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem {
return {
file: item.file,
name: item.name,
kind: item.kind,
span: this.decodeSpan(item.span, item.file),
selectionSpan: this.decodeSpan(item.selectionSpan, item.file)
};
}

prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | undefined {
const args = this.createFileLocationRequestArgs(fileName, position);
const request = this.processRequest<protocol.PrepareCallHierarchyRequest>(CommandNames.PrepareCallHierarchy, args);
const response = this.processResponse<protocol.PrepareCallHierarchyResponse>(request);
return response.body && this.convertCallHierarchyItem(response.body);
}

private convertCallHierarchyIncomingCall(item: protocol.CallHierarchyIncomingCall): CallHierarchyIncomingCall {
return {
from: this.convertCallHierarchyItem(item.from),
fromSpans: item.fromSpans.map(span => this.decodeSpan(span, item.from.file))
};
}

provideCallHierarchyIncomingCalls(fileName: string, position: number) {
const args = this.createFileLocationRequestArgs(fileName, position);
const request = this.processRequest<protocol.ProvideCallHierarchyIncomingCallsRequest>(CommandNames.PrepareCallHierarchy, args);
const response = this.processResponse<protocol.ProvideCallHierarchyIncomingCallsResponse>(request);
return response.body.map(item => this.convertCallHierarchyIncomingCall(item));
}

private convertCallHierarchyOutgoingCall(file: string, item: protocol.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall {
return {
to: this.convertCallHierarchyItem(item.to),
fromSpans: item.fromSpans.map(span => this.decodeSpan(span, file))
};
}

provideCallHierarchyOutgoingCalls(fileName: string, position: number) {
const args = this.createFileLocationRequestArgs(fileName, position);
const request = this.processRequest<protocol.ProvideCallHierarchyOutgoingCallsRequest>(CommandNames.PrepareCallHierarchy, args);
const response = this.processResponse<protocol.ProvideCallHierarchyOutgoingCallsResponse>(request);
return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item));
}

getProgram(): Program {
throw new Error("SourceFile objects are not serializable through the server protocol.");
}
Expand Down
181 changes: 174 additions & 7 deletions src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -730,11 +730,8 @@ namespace FourSlash {
if (!range) {
this.raiseError(`goToDefinitionsAndBoundSpan failed - found a TextSpan ${JSON.stringify(defs.textSpan)} when it wasn't expected.`);
}
else if (defs.textSpan.start !== range.pos || defs.textSpan.length !== range.end - range.pos) {
const expected: ts.TextSpan = {
start: range.pos, length: range.end - range.pos
};
this.raiseError(`goToDefinitionsAndBoundSpan failed - expected to find TextSpan ${JSON.stringify(expected)} but got ${JSON.stringify(defs.textSpan)}`);
else {
this.assertTextSpanEqualsRange(defs.textSpan, range, "goToDefinitionsAndBoundSpan failed");
}
}

Expand Down Expand Up @@ -3133,6 +3130,142 @@ namespace FourSlash {
Harness.IO.log(stringify(codeFixes));
}

public verifyCallHierarchy(options: false | FourSlashInterface.VerifyCallHierarchyOptions | Range) {
const callHierarchyItem = this.languageService.prepareCallHierarchy(this.activeFile.fileName, this.currentCaretPosition)!;
this.assertCallHierarchyItemMatches(callHierarchyItem, options);
}

public verifyCallHierarchyIncomingCalls(options: FourSlashInterface.Sequence<FourSlashInterface.VerifyCallHierarchyIncomingCallOptions | Range>) {
const incomingCalls = this.languageService.provideCallHierarchyIncomingCalls(this.activeFile.fileName, this.currentCaretPosition)!;
this.assertCallHierarchyIncomingCallsMatch(incomingCalls, options);
}

public verifyCallHierarchyOutgoingCalls(options: FourSlashInterface.Sequence<FourSlashInterface.VerifyCallHierarchyOutgoingCallOptions | Range>) {
const outgoingCalls = this.languageService.provideCallHierarchyOutgoingCalls(this.activeFile.fileName, this.currentCaretPosition)!;
this.assertCallHierarchyOutgoingCallsMatch(outgoingCalls, options);
}

private assertSequence<T, U>(actual: readonly T[] | undefined, expected: FourSlashInterface.Sequence<U>, equate: (actual: T, expected: U) => boolean | undefined, assertion: (actual: T, expected: U, message?: string) => void, message?: string, stringifyActual: (actual: T) => string = stringifyFallback, stringifyExpected: (expected: U) => string = stringifyFallback) {
// normalize expected input
if (!expected) {
expected = { exact: true, values: ts.emptyArray };
}
else if (ts.isArray(expected)) {
expected = { exact: false, values: expected };
}

if (expected.exact) {
if (actual === undefined || actual.length === 0) {
if (expected.values.length !== 0) {
this.raiseError(`${prefixMessage(message)}Expected sequence to have exactly ${expected.values.length} item${expected.values.length > 1 ? "s" : ""} but got ${actual ? "an empty array" : "undefined"} instead.\nExpected:\n${stringifyArray(expected.values, stringifyExpected)}`);
}
return;
}
if (expected.values.length === 0) {
this.raiseError(`${prefixMessage(message)}Expected sequence to be empty but got ${actual.length} item${actual.length > 1 ? "s" : ""} instead.\nActual:\n${stringifyArray(actual, stringifyActual)}`);
return;
}
}
else if (actual === undefined || actual.length === 0) {
if (expected.values.length !== 0) {
this.raiseError(`${prefixMessage(message)}Expected sequence to have at least ${expected.values.length} item${expected.values.length > 1 ? "s" : ""} but got ${actual ? "an empty array" : "undefined"} instead.\nExpected:\n${stringifyArray(expected.values, stringifyExpected)}`);
}
return;
}

let expectedIndex = 0;
let actualIndex = 0;
while (expectedIndex < expected.values.length && actualIndex < actual.length) {
const actualItem = actual[actualIndex];
actualIndex++;
const expectedItem = expected.values[expectedIndex];
const result = expected.exact || equate(actualItem, expectedItem);
if (result) {
assertion(actualItem, expectedItem, message);
expectedIndex++;
}
else if (expected.exact) {
this.raiseError(`${prefixMessage(message)}Expected item at index ${expectedIndex} to be ${stringifyExpected(expectedItem)} but got ${stringifyActual(actualItem)} instead.`);
}
else if (result === undefined) {
this.raiseError(`${prefixMessage(message)}Unable to compare items`);
}
}

if (expectedIndex < expected.values.length) {
const expectedItem = expected.values[expectedIndex];
this.raiseError(`${prefixMessage(message)}Expected array to contain ${stringifyExpected(expectedItem)} but it was not found.`);
}
}

private assertCallHierarchyItemMatches(callHierarchyItem: ts.CallHierarchyItem | undefined, options: false | FourSlashInterface.VerifyCallHierarchyOptions | Range, message?: string) {
if (!options) {
assert.isUndefined(callHierarchyItem, this.messageAtLastKnownMarker(`${prefixMessage(message)}Expected location to not have a call hierarchy`));
}
else {
assert.isDefined(callHierarchyItem, this.messageAtLastKnownMarker(`${prefixMessage(message)}Expected location to have a call hierarchy`));
if (!callHierarchyItem) return;
if (isRange(options)) {
options = { selectionRange: options };
}
if (options.kind !== undefined) {
assert.equal(callHierarchyItem.kind, options.kind, this.messageAtLastKnownMarker(`${prefixMessage(message)}Invalid kind`));
}
if (options.range) {
assert.equal(callHierarchyItem.file, options.range.fileName, this.messageAtLastKnownMarker(`${prefixMessage(message)}Incorrect file for call hierarchy item`));
this.assertTextSpanEqualsRange(callHierarchyItem.span, options.range, `${prefixMessage(message)}Incorrect range for declaration`);
}
if (options.selectionRange) {
assert.equal(callHierarchyItem.file, options.selectionRange.fileName, this.messageAtLastKnownMarker(`${prefixMessage(message)}Incorrect file for call hierarchy item`));
this.assertTextSpanEqualsRange(callHierarchyItem.selectionSpan, options.selectionRange, `${prefixMessage(message)}Incorrect selectionRange for declaration`);
}
if (options.incoming !== undefined) {
const incomingCalls = this.languageService.provideCallHierarchyIncomingCalls(callHierarchyItem.file, callHierarchyItem.selectionSpan.start);
this.assertCallHierarchyIncomingCallsMatch(incomingCalls, options.incoming, message);
}
if (options.outgoing !== undefined) {
const outgoingCalls = this.languageService.provideCallHierarchyOutgoingCalls(callHierarchyItem.file, callHierarchyItem.selectionSpan.start);
this.assertCallHierarchyOutgoingCallsMatch(outgoingCalls, options.outgoing, message);
}
}
}

private assertCallHierarchyIncomingCallsMatch(incomingCalls: ts.CallHierarchyIncomingCall[], options: FourSlashInterface.Sequence<FourSlashInterface.VerifyCallHierarchyIncomingCallOptions | Range>, message?: string) {
this.assertSequence(incomingCalls, options,
(actual, expected) => {
expected = normalizeCallHierarchyIncomingCallOptions(expected);
const from = normalizeCallHierarchyOptions(expected.from);
return from.selectionRange && textSpanEqualsRange(actual.from.selectionSpan, from.selectionRange);
},
(actual, expected, message) => {
expected = normalizeCallHierarchyIncomingCallOptions(expected);
const from = normalizeCallHierarchyOptions(expected.from);
this.assertCallHierarchyItemMatches(actual.from, from, message);
if (expected.fromRanges !== undefined) this.assertSequence(actual.fromSpans, expected.fromRanges, textSpanEqualsRange, ts.noop, `${prefixMessage(message)}Invalid fromRange`);
}, `${prefixMessage(message)}Invalid incoming calls`);
}

private assertCallHierarchyOutgoingCallsMatch(outgoingCalls: ts.CallHierarchyOutgoingCall[], options: FourSlashInterface.Sequence<FourSlashInterface.VerifyCallHierarchyOutgoingCallOptions | Range>, message?: string) {
this.assertSequence(outgoingCalls, options,
(actual, expected) => {
expected = normalizeCallHierarchyOutgoingCallOptions(expected);
const to = normalizeCallHierarchyOptions(expected.to);
return to.selectionRange && textSpanEqualsRange(actual.to.selectionSpan, to.selectionRange);
},
(actual, expected, message) => {
expected = normalizeCallHierarchyOutgoingCallOptions(expected);
const to = normalizeCallHierarchyOptions(expected.to);
this.assertCallHierarchyItemMatches(actual.to, to, message);
if (expected.fromRanges !== undefined) this.assertSequence(actual.fromSpans, expected.fromRanges, textSpanEqualsRange, ts.noop, `${prefixMessage(message)}Invalid fromRange`);
}, `${prefixMessage(message)}Invalid outgoing calls`);
}

private assertTextSpanEqualsRange(span: ts.TextSpan, range: Range, message?: string) {
if (!textSpanEqualsRange(span, range)) {
this.raiseError(`${prefixMessage(message)}Expected to find TextSpan ${JSON.stringify({ start: range.pos, length: range.end - range.pos })} but got ${JSON.stringify(span)} instead.`);
}
}

private getLineContent(index: number) {
const text = this.getFileContent(this.activeFile.fileName);
const pos = this.languageServiceAdapterHost.lineAndCharacterToPosition(this.activeFile.fileName, { line: index, character: 0 });
Expand Down Expand Up @@ -3266,6 +3399,40 @@ namespace FourSlash {
}
}

function prefixMessage(message: string | undefined) {
return message ? `${message} - ` : "";
}

function stringifyArray<T>(values: readonly T[], stringify: (value: T) => string, indent = " ") {
return values.length ? `${indent}[\n${indent} ${values.map(stringify).join(`,\n${indent} `)}\n${indent}]` : `${indent}[]`;
}

function stringifyFallback(value: unknown) {
return JSON.stringify(value);
}

function textSpanEqualsRange(span: ts.TextSpan, range: Range) {
return span.start === range.pos && span.length === range.end - range.pos;
}

function isRange(value: object): value is Range {
return ts.hasProperty(value, "pos")
&& ts.hasProperty(value, "end")
&& ts.hasProperty(value, "fileName");
}

function normalizeCallHierarchyOptions(options: Range | FourSlashInterface.VerifyCallHierarchyOptions) {
return isRange(options) ? { selectionRange: options } : options;
}

function normalizeCallHierarchyIncomingCallOptions(options: Range | FourSlashInterface.VerifyCallHierarchyIncomingCallOptions) {
return isRange(options) ? { from: options } : options;
}

function normalizeCallHierarchyOutgoingCallOptions(options: Range | FourSlashInterface.VerifyCallHierarchyOutgoingCallOptions) {
return isRange(options) ? { to: options } : options;
}

function updateTextRangeForTextChanges({ pos, end }: ts.TextRange, textChanges: readonly ts.TextChange[]): ts.TextRange {
forEachTextChange(textChanges, change => {
const update = (p: number): number => updatePosition(p, change.span.start, ts.textSpanEnd(change.span), change.newText);
Expand Down Expand Up @@ -3327,7 +3494,7 @@ namespace FourSlash {
function runCode(code: string, state: TestState): void {
// Compile and execute the test
const wrappedCode =
`(function(test, goTo, plugins, verify, edit, debug, format, cancellation, classification, completion, verifyOperationIsCancelled) {
`(function(test, goTo, plugins, verify, edit, debug, format, cancellation, classification, completion, sequence, verifyOperationIsCancelled) {
${code}
})`;
try {
Expand All @@ -3341,7 +3508,7 @@ ${code}
const cancellation = new FourSlashInterface.Cancellation(state);
// eslint-disable-next-line no-eval
const f = eval(wrappedCode);
f(test, goTo, plugins, verify, edit, debug, format, cancellation, FourSlashInterface.Classification, FourSlashInterface.Completion, verifyOperationIsCancelled);
f(test, goTo, plugins, verify, edit, debug, format, cancellation, FourSlashInterface.Classification, FourSlashInterface.Completion, FourSlashInterface.Sequence, verifyOperationIsCancelled);
}
catch (err) {
throw err;
Expand Down
Loading