diff --git a/packages/safe-ds-lang/src/language/flow/safe-ds-call-graph-computer.ts b/packages/safe-ds-lang/src/language/flow/safe-ds-call-graph-computer.ts new file mode 100644 index 000000000..893b7f106 --- /dev/null +++ b/packages/safe-ds-lang/src/language/flow/safe-ds-call-graph-computer.ts @@ -0,0 +1,32 @@ +import { AstNode, type AstNodeLocator, getDocument, streamAllContents, WorkspaceCache } from 'langium'; +import { isSdsCall, type SdsCall } from '../generated/ast.js'; +import type { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js'; +import type { SafeDsServices } from '../safe-ds-module.js'; + +export class SafeDsCallGraphComputer { + private readonly astNodeLocator: AstNodeLocator; + private readonly nodeMapper: SafeDsNodeMapper; + + /** + * Stores the calls inside the node with the given ID. + */ + private readonly callCache: WorkspaceCache; + + constructor(services: SafeDsServices) { + this.astNodeLocator = services.workspace.AstNodeLocator; + this.nodeMapper = services.helpers.NodeMapper; + + this.callCache = new WorkspaceCache(services.shared); + } + + getCalls(node: AstNode): SdsCall[] { + const key = this.getNodeId(node); + return this.callCache.get(key, () => streamAllContents(node).filter(isSdsCall).toArray()); + } + + private getNodeId(node: AstNode) { + const documentUri = getDocument(node).uri.toString(); + const nodePath = this.astNodeLocator.getAstNodePath(node); + return `${documentUri}~${nodePath}`; + } +} diff --git a/packages/safe-ds-lang/src/language/lsp/safe-ds-call-hierarchy-provider.ts b/packages/safe-ds-lang/src/language/lsp/safe-ds-call-hierarchy-provider.ts new file mode 100644 index 000000000..97d08f8f4 --- /dev/null +++ b/packages/safe-ds-lang/src/language/lsp/safe-ds-call-hierarchy-provider.ts @@ -0,0 +1,195 @@ +import { + AbstractCallHierarchyProvider, + type AstNode, + type CstNode, + findLeafNodeAtOffset, + getContainerOfType, + getDocument, + type NodeKindProvider, + type ReferenceDescription, + type Stream, +} from 'langium'; +import type { + CallHierarchyIncomingCall, + CallHierarchyOutgoingCall, + Range, + SymbolKind, + SymbolTag, +} from 'vscode-languageserver'; +import type { SafeDsCallGraphComputer } from '../flow/safe-ds-call-graph-computer.js'; +import { + isSdsDeclaration, + isSdsParameter, + type SdsCall, + type SdsCallable, + type SdsDeclaration, +} from '../generated/ast.js'; +import type { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js'; +import type { SafeDsServices } from '../safe-ds-module.js'; +import type { SafeDsNodeInfoProvider } from './safe-ds-node-info-provider.js'; + +export class SafeDsCallHierarchyProvider extends AbstractCallHierarchyProvider { + private readonly callGraphComputer: SafeDsCallGraphComputer; + private readonly nodeInfoProvider: SafeDsNodeInfoProvider; + private readonly nodeKindProvider: NodeKindProvider; + private readonly nodeMapper: SafeDsNodeMapper; + + constructor(services: SafeDsServices) { + super(services); + + this.callGraphComputer = services.flow.CallGraphComputer; + this.nodeInfoProvider = services.lsp.NodeInfoProvider; + this.nodeKindProvider = services.shared.lsp.NodeKindProvider; + this.nodeMapper = services.helpers.NodeMapper; + } + + protected override getCallHierarchyItem(targetNode: AstNode): { + kind: SymbolKind; + tags?: SymbolTag[]; + detail?: string; + } { + return { + kind: this.nodeKindProvider.getSymbolKind(targetNode), + tags: this.nodeInfoProvider.getTags(targetNode), + detail: this.nodeInfoProvider.getDetails(targetNode), + }; + } + + protected getIncomingCalls( + node: AstNode, + references: Stream, + ): CallHierarchyIncomingCall[] | undefined { + const result: CallHierarchyIncomingCall[] = []; + + this.getUniquePotentialCallers(references).forEach((caller) => { + if (!caller.$cstNode) { + /* c8 ignore next 2 */ + return; + } + + const callerNameCstNode = this.nameProvider.getNameNode(caller); + if (!callerNameCstNode) { + /* c8 ignore next 2 */ + return; + } + + // Find all calls inside the caller that refer to the given node. This can also handle aliases. + const callsOfNode = this.getCallsOf(caller, node); + if (callsOfNode.length === 0 || callsOfNode.some((it) => !it.$cstNode)) { + return; + } + + const callerDocumentUri = getDocument(caller).uri.toString(); + + result.push({ + from: { + name: callerNameCstNode.text, + range: caller.$cstNode.range, + selectionRange: callerNameCstNode.range, + uri: callerDocumentUri, + ...this.getCallHierarchyItem(caller), + }, + fromRanges: callsOfNode.map((it) => it.$cstNode!.range), + }); + }); + + if (result.length === 0) { + return undefined; + } + + return result; + } + + /** + * Returns all declarations that contain at least one of the given references. Some of them might not be actual + * callers, since the references might not occur in a call. This has to be checked later. + */ + private getUniquePotentialCallers(references: Stream): Stream { + return references + .map((it) => { + const document = this.documents.getOrCreateDocument(it.sourceUri); + const rootNode = document.parseResult.value; + if (!rootNode.$cstNode) { + /* c8 ignore next 2 */ + return undefined; + } + + const targetNode = findLeafNodeAtOffset(rootNode.$cstNode, it.segment.offset); + if (!targetNode) { + /* c8 ignore next 2 */ + return undefined; + } + + const containingDeclaration = getContainerOfType(targetNode.astNode, isSdsDeclaration); + if (isSdsParameter(containingDeclaration)) { + // For parameters, we return their containing callable instead + return getContainerOfType(containingDeclaration.$container, isSdsDeclaration); + } else { + return containingDeclaration; + } + }) + .distinct() + .filter(isSdsDeclaration); + } + + private getCallsOf(caller: AstNode, callee: AstNode): SdsCall[] { + return this.callGraphComputer + .getCalls(caller) + .filter((call) => this.nodeMapper.callToCallable(call) === callee); + } + + protected getOutgoingCalls(node: AstNode): CallHierarchyOutgoingCall[] | undefined { + const calls = this.callGraphComputer.getCalls(node); + const callsGroupedByCallable = new Map< + string, + { callable: SdsCallable; callableNameCstNode: CstNode; callableDocumentUri: string; fromRanges: Range[] } + >(); + + // Group calls by the callable they refer to + calls.forEach((call) => { + const callCstNode = call.$cstNode; + if (!callCstNode) { + /* c8 ignore next 2 */ + return; + } + + const callable = this.nodeMapper.callToCallable(call); + if (!callable?.$cstNode) { + /* c8 ignore next 2 */ + return; + } + + const callableNameCstNode = this.nameProvider.getNameNode(callable); + if (!callableNameCstNode) { + /* c8 ignore next 2 */ + return; + } + + const callableDocumentUri = getDocument(callable).uri.toString(); + const callableId = callableDocumentUri + '~' + callableNameCstNode.text; + + const previousFromRanges = callsGroupedByCallable.get(callableId)?.fromRanges ?? []; + callsGroupedByCallable.set(callableId, { + callable, + callableNameCstNode, + fromRanges: [...previousFromRanges, callCstNode.range], + callableDocumentUri, + }); + }); + + if (callsGroupedByCallable.size === 0) { + return undefined; + } + + return Array.from(callsGroupedByCallable.values()).map((call) => ({ + to: { + name: call.callableNameCstNode.text, + range: call.callable.$cstNode!.range, + selectionRange: call.callableNameCstNode.range, + uri: call.callableDocumentUri, + ...this.getCallHierarchyItem(call.callable), + }, + fromRanges: call.fromRanges, + })); + } +} diff --git a/packages/safe-ds-lang/src/language/lsp/safe-ds-document-symbol-provider.ts b/packages/safe-ds-lang/src/language/lsp/safe-ds-document-symbol-provider.ts index 012a93ee8..302fe256e 100644 --- a/packages/safe-ds-lang/src/language/lsp/safe-ds-document-symbol-provider.ts +++ b/packages/safe-ds-lang/src/language/lsp/safe-ds-document-symbol-provider.ts @@ -1,9 +1,7 @@ -import { AstNode, DefaultDocumentSymbolProvider, LangiumDocument } from 'langium'; -import { DocumentSymbol, SymbolTag } from 'vscode-languageserver'; -import { SafeDsServices } from '../safe-ds-module.js'; -import { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js'; +import { type AstNode, DefaultDocumentSymbolProvider, type LangiumDocument } from 'langium'; +import type { DocumentSymbol } from 'vscode-languageserver'; +import type { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js'; import { - isSdsAnnotatedObject, isSdsAnnotation, isSdsAttribute, isSdsClass, @@ -12,16 +10,20 @@ import { isSdsPipeline, isSdsSegment, } from '../generated/ast.js'; -import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js'; +import type { SafeDsServices } from '../safe-ds-module.js'; +import type { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js'; +import type { SafeDsNodeInfoProvider } from './safe-ds-node-info-provider.js'; export class SafeDsDocumentSymbolProvider extends DefaultDocumentSymbolProvider { private readonly builtinAnnotations: SafeDsAnnotations; + private readonly nodeInfoProvider: SafeDsNodeInfoProvider; private readonly typeComputer: SafeDsTypeComputer; constructor(services: SafeDsServices) { super(services); this.builtinAnnotations = services.builtins.Annotations; + this.nodeInfoProvider = services.lsp.NodeInfoProvider; this.typeComputer = services.types.TypeComputer; } @@ -34,8 +36,8 @@ export class SafeDsDocumentSymbolProvider extends DefaultDocumentSymbolProvider { name: name ?? nameNode.text, kind: this.nodeKindProvider.getSymbolKind(node), - tags: this.getTags(node), - detail: this.getDetails(node), + tags: this.nodeInfoProvider.getTags(node), + detail: this.nodeInfoProvider.getDetails(node), range: cstNode.range, selectionRange: nameNode.range, children: this.getChildSymbols(document, node), @@ -60,22 +62,6 @@ export class SafeDsDocumentSymbolProvider extends DefaultDocumentSymbolProvider } } - private getDetails(node: AstNode): string | undefined { - if (isSdsFunction(node) || isSdsSegment(node)) { - const type = this.typeComputer.computeType(node); - return type?.toString(); - } - return undefined; - } - - private getTags(node: AstNode): SymbolTag[] | undefined { - if (isSdsAnnotatedObject(node) && this.builtinAnnotations.isDeprecated(node)) { - return [SymbolTag.Deprecated]; - } else { - return undefined; - } - } - private isLeaf(node: AstNode): boolean { return ( isSdsAnnotation(node) || diff --git a/packages/safe-ds-lang/src/language/lsp/safe-ds-node-info-provider.ts b/packages/safe-ds-lang/src/language/lsp/safe-ds-node-info-provider.ts new file mode 100644 index 000000000..26a056831 --- /dev/null +++ b/packages/safe-ds-lang/src/language/lsp/safe-ds-node-info-provider.ts @@ -0,0 +1,40 @@ +import { AstNode } from 'langium'; +import { SymbolTag } from 'vscode-languageserver'; +import type { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js'; +import { isSdsAnnotatedObject, isSdsFunction, isSdsSegment } from '../generated/ast.js'; +import type { SafeDsServices } from '../safe-ds-module.js'; +import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js'; + +export class SafeDsNodeInfoProvider { + private readonly builtinAnnotations: SafeDsAnnotations; + private readonly typeComputer: SafeDsTypeComputer; + + constructor(services: SafeDsServices) { + this.builtinAnnotations = services.builtins.Annotations; + this.typeComputer = services.types.TypeComputer; + } + + /** + * Returns the detail string for the given node. This can be used, for example, to provide document symbols or call + * hierarchies. + */ + getDetails(node: AstNode): string | undefined { + if (isSdsFunction(node) || isSdsSegment(node)) { + const type = this.typeComputer.computeType(node); + return type?.toString(); + } + return undefined; + } + + /** + * Returns the tags for the given node. This can be used, for example, to provide document symbols or call + * hierarchies. + */ + getTags(node: AstNode): SymbolTag[] | undefined { + if (isSdsAnnotatedObject(node) && this.builtinAnnotations.isDeprecated(node)) { + return [SymbolTag.Deprecated]; + } else { + return undefined; + } + } +} diff --git a/packages/safe-ds-lang/src/language/partialEvaluation/safe-ds-partial-evaluator.ts b/packages/safe-ds-lang/src/language/partialEvaluation/safe-ds-partial-evaluator.ts index 1c63bb134..17975f5c4 100644 --- a/packages/safe-ds-lang/src/language/partialEvaluation/safe-ds-partial-evaluator.ts +++ b/packages/safe-ds-lang/src/language/partialEvaluation/safe-ds-partial-evaluator.ts @@ -82,10 +82,7 @@ export class SafeDsPartialEvaluator { } // Try to evaluate the node without parameter substitutions and cache the result - const documentUri = getDocument(node).uri.toString(); - const nodePath = this.astNodeLocator.getAstNodePath(node); - const key = `${documentUri}~${nodePath}`; - const resultWithoutSubstitutions = this.cache.get(key, () => + const resultWithoutSubstitutions = this.cache.get(this.getNodeId(node), () => this.doEvaluateWithSubstitutions(node, NO_SUBSTITUTIONS), ); if (resultWithoutSubstitutions.isFullyEvaluated || isEmpty(substitutions)) { @@ -96,6 +93,12 @@ export class SafeDsPartialEvaluator { } /* c8 ignore stop */ } + private getNodeId(node: AstNode) { + const documentUri = getDocument(node).uri.toString(); + const nodePath = this.astNodeLocator.getAstNodePath(node); + return `${documentUri}~${nodePath}`; + } + private doEvaluateWithSubstitutions( node: AstNode | undefined, substitutions: ParameterSubstitutions, diff --git a/packages/safe-ds-lang/src/language/safe-ds-module.ts b/packages/safe-ds-lang/src/language/safe-ds-module.ts index d01e8ae76..b531db32c 100644 --- a/packages/safe-ds-lang/src/language/safe-ds-module.ts +++ b/packages/safe-ds-lang/src/language/safe-ds-module.ts @@ -14,13 +14,16 @@ import { SafeDsClasses } from './builtins/safe-ds-classes.js'; import { SafeDsEnums } from './builtins/safe-ds-enums.js'; import { SafeDsCommentProvider } from './documentation/safe-ds-comment-provider.js'; import { SafeDsDocumentationProvider } from './documentation/safe-ds-documentation-provider.js'; +import { SafeDsCallGraphComputer } from './flow/safe-ds-call-graph-computer.js'; import { SafeDsGeneratedModule, SafeDsGeneratedSharedModule } from './generated/module.js'; import { SafeDsPythonGenerator } from './generation/safe-ds-python-generator.js'; import { SafeDsValueConverter } from './grammar/safe-ds-value-converter.js'; import { SafeDsNodeMapper } from './helpers/safe-ds-node-mapper.js'; +import { SafeDsCallHierarchyProvider } from './lsp/safe-ds-call-hierarchy-provider.js'; import { SafeDsDocumentSymbolProvider } from './lsp/safe-ds-document-symbol-provider.js'; import { SafeDsFormatter } from './lsp/safe-ds-formatter.js'; import { SafeDsInlayHintProvider } from './lsp/safe-ds-inlay-hint-provider.js'; +import { SafeDsNodeInfoProvider } from './lsp/safe-ds-node-info-provider.js'; import { SafeDsNodeKindProvider } from './lsp/safe-ds-node-kind-provider.js'; import { SafeDsSemanticTokenProvider } from './lsp/safe-ds-semantic-token-provider.js'; import { SafeDsSignatureHelpProvider } from './lsp/safe-ds-signature-help-provider.js'; @@ -48,12 +51,18 @@ export type SafeDsAddedServices = { evaluation: { PartialEvaluator: SafeDsPartialEvaluator; }; + flow: { + CallGraphComputer: SafeDsCallGraphComputer; + }; generation: { PythonGenerator: SafeDsPythonGenerator; }; helpers: { NodeMapper: SafeDsNodeMapper; }; + lsp: { + NodeInfoProvider: SafeDsNodeInfoProvider; + }; types: { ClassHierarchy: SafeDsClassHierarchy; CoreTypes: SafeDsCoreTypes; @@ -89,6 +98,9 @@ export const SafeDsModule: Module new SafeDsPartialEvaluator(services), }, + flow: { + CallGraphComputer: (services) => new SafeDsCallGraphComputer(services), + }, generation: { PythonGenerator: (services) => new SafeDsPythonGenerator(services), }, @@ -96,9 +108,11 @@ export const SafeDsModule: Module new SafeDsNodeMapper(services), }, lsp: { + CallHierarchyProvider: (services) => new SafeDsCallHierarchyProvider(services), DocumentSymbolProvider: (services) => new SafeDsDocumentSymbolProvider(services), Formatter: () => new SafeDsFormatter(), InlayHintProvider: (services) => new SafeDsInlayHintProvider(services), + NodeInfoProvider: (services) => new SafeDsNodeInfoProvider(services), SemanticTokenProvider: (services) => new SafeDsSemanticTokenProvider(services), SignatureHelp: (services) => new SafeDsSignatureHelpProvider(services), }, diff --git a/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts b/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts index 3de64a850..5b4acc3d3 100644 --- a/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts +++ b/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts @@ -123,11 +123,13 @@ export class SafeDsTypeComputer { if (!node) { return UnknownType; } + return this.nodeTypeCache.get(this.getNodeId(node), () => this.doComputeType(node).unwrap()); + } + private getNodeId(node: AstNode) { const documentUri = getDocument(node).uri.toString(); const nodePath = this.astNodeLocator.getAstNodePath(node); - const key = `${documentUri}~${nodePath}`; - return this.nodeTypeCache.get(key, () => this.doComputeType(node).unwrap()); + return `${documentUri}~${nodePath}`; } private doComputeType(node: AstNode): Type { diff --git a/packages/safe-ds-lang/tests/language/lsp/formatting/creator.ts b/packages/safe-ds-lang/tests/language/lsp/formatting/creator.ts index f6112392a..a0b596c55 100644 --- a/packages/safe-ds-lang/tests/language/lsp/formatting/creator.ts +++ b/packages/safe-ds-lang/tests/language/lsp/formatting/creator.ts @@ -1,10 +1,10 @@ -import { listTestSafeDsFiles, uriToShortenedTestResourceName } from '../../../helpers/testResources.js'; import fs from 'fs'; -import { Diagnostic } from 'vscode-languageserver'; -import { createSafeDsServices } from '../../../../src/language/safe-ds-module.js'; import { EmptyFileSystem, URI } from 'langium'; +import { Diagnostic } from 'vscode-languageserver'; +import { createSafeDsServices } from '../../../../src/language/index.js'; import { getSyntaxErrors } from '../../../helpers/diagnostics.js'; import { TestDescription, TestDescriptionError } from '../../../helpers/testDescription.js'; +import { listTestSafeDsFiles, uriToShortenedTestResourceName } from '../../../helpers/testResources.js'; const services = createSafeDsServices(EmptyFileSystem).SafeDs; const rootResourceName = 'formatting'; diff --git a/packages/safe-ds-lang/tests/language/lsp/safe-ds-call-hierarchy-provider.test.ts b/packages/safe-ds-lang/tests/language/lsp/safe-ds-call-hierarchy-provider.test.ts new file mode 100644 index 000000000..2fea6b0dd --- /dev/null +++ b/packages/safe-ds-lang/tests/language/lsp/safe-ds-call-hierarchy-provider.test.ts @@ -0,0 +1,321 @@ +import { NodeFileSystem } from 'langium/node'; +import { clearDocuments, parseHelper } from 'langium/test'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { type CallHierarchyItem } from 'vscode-languageserver'; +import { createSafeDsServices } from '../../../src/language/index.js'; +import { findTestRanges } from '../../helpers/testRanges.js'; + +const services = createSafeDsServices(NodeFileSystem).SafeDs; +const callHierarchyProvider = services.lsp.CallHierarchyProvider!; +const workspaceManager = services.shared.workspace.WorkspaceManager; +const parse = parseHelper(services); + +describe('SafeDsCallHierarchyProvider', async () => { + beforeEach(async () => { + // Load the builtin library + await workspaceManager.initializeWorkspace([]); + }); + + afterEach(async () => { + await clearDocuments(services); + }); + + describe('incomingCalls', () => { + const testCases: IncomingCallTest[] = [ + { + testName: 'unused', + code: `class »«C`, + expectedIncomingCalls: undefined, + }, + { + testName: 'single caller, single call', + code: ` + class »«C() + class D() + + pipeline myPipeline { + C(); + D(); + } + `, + expectedIncomingCalls: [ + { + fromName: 'myPipeline', + fromRangesLength: 1, + }, + ], + }, + { + testName: 'single caller, multiple calls', + code: ` + class »«C() + class D() + + pipeline myPipeline { + C(); + () -> C(); + () { C() }; + D(); + } + `, + expectedIncomingCalls: [ + { + fromName: 'myPipeline', + fromRangesLength: 3, + }, + ], + }, + { + testName: 'multiple callers', + code: ` + class »«C() + class D() + + pipeline myPipeline { + C(); + C(); + D(); + } + + segment mySegment(myParam: C = C()) { + C(); + C(); + D(); + } + `, + expectedIncomingCalls: [ + { + fromName: 'myPipeline', + fromRangesLength: 2, + }, + { + fromName: 'mySegment', + fromRangesLength: 3, + }, + ], + }, + { + testName: 'only referenced', + code: ` + class »«C() + + pipeline p { + C; + } + `, + expectedIncomingCalls: undefined, + }, + ]; + + it.each(testCases)('should list all incoming calls ($testName)', async ({ code, expectedIncomingCalls }) => { + const result = await getActualSimpleIncomingCalls(code); + expect(result).toStrictEqual(expectedIncomingCalls); + }); + }); + + describe('outgoingCalls', () => { + const testCases: OutgoingCallTest[] = [ + { + testName: 'no calls', + code: `pipeline »«p {}`, + expectedOutgoingCalls: undefined, + }, + { + testName: 'single callee, single call', + code: ` + fun f() + + pipeline »«p { + f(); + } + `, + expectedOutgoingCalls: [ + { + toName: 'f', + fromRangesLength: 1, + }, + ], + }, + { + testName: 'single callee, multiple calls', + code: ` + fun f() + + pipeline »«p { + f(); + () -> f(); + () { f() }; + } + `, + expectedOutgoingCalls: [ + { + toName: 'f', + fromRangesLength: 3, + }, + ], + }, + { + testName: 'multiple callees', + code: ` + fun f() + fun g() + + pipeline »«p { + f(); + f(); + g(); + } + `, + expectedOutgoingCalls: [ + { + toName: 'f', + fromRangesLength: 2, + }, + { + toName: 'g', + fromRangesLength: 1, + }, + ], + }, + { + testName: 'only references', + code: ` + fun f() + + pipeline »«p { + f; + } + `, + expectedOutgoingCalls: undefined, + }, + ]; + + it.each(testCases)('should list all outgoing calls ($testName)', async ({ code, expectedOutgoingCalls }) => { + const result = await getActualSimpleOutgoingCalls(code); + expect(result).toStrictEqual(expectedOutgoingCalls); + }); + }); +}); + +const getActualSimpleIncomingCalls = async (code: string): Promise => { + return callHierarchyProvider + .incomingCalls({ + item: await getUniqueCallHierarchyItem(code), + }) + ?.map((call) => ({ + fromName: call.from.name, + fromRangesLength: call.fromRanges.length, + })); +}; + +const getActualSimpleOutgoingCalls = async (code: string): Promise => { + return callHierarchyProvider + .outgoingCalls({ + item: await getUniqueCallHierarchyItem(code), + }) + ?.map((call) => ({ + toName: call.to.name, + fromRangesLength: call.fromRanges.length, + })); +}; + +const getUniqueCallHierarchyItem = async (code: string): Promise => { + const document = await parse(code); + + const testRangesResult = findTestRanges(code, document.uri); + if (testRangesResult.isErr) { + throw new Error(testRangesResult.error.message); + } else if (testRangesResult.value.length !== 1) { + throw new Error(`Expected exactly one test range, but got ${testRangesResult.value.length}.`); + } + const testRangeStart = testRangesResult.value[0]!.start; + + const items = + callHierarchyProvider.prepareCallHierarchy(document, { + textDocument: { + uri: document.textDocument.uri, + }, + position: { + line: testRangeStart.line, + // Since the test range cannot be placed inside the identifier, we place it in front of the identifier. + // Then we need to move the cursor one character to the right to be inside the identifier. + character: testRangeStart.character + 1, + }, + }) ?? []; + + if (items.length !== 1) { + throw new Error(`Expected exactly one call hierarchy item, but got ${items.length}.`); + } + + return items[0]!; +}; + +/** + * A test case for {@link SafeDsCallHierarchyProvider.incomingCalls}. + */ +interface IncomingCallTest { + /** + * A short description of the test case. + */ + testName: string; + + /** + * The code to parse. + */ + code: string; + + /** + * The expected incoming calls. + */ + expectedIncomingCalls: SimpleIncomingCall[] | undefined; +} + +/** + * A simplified variant of {@link CallHierarchyIncomingCall}. + */ +interface SimpleIncomingCall { + /** + * The name of the caller. + */ + fromName: string; + + /** + * The number of calls in the caller. + */ + fromRangesLength: number; +} + +/** + * A test case for {@link SafeDsCallHierarchyProvider.outgoingCalls}. + */ +interface OutgoingCallTest { + /** + * A short description of the test case. + */ + testName: string; + + /** + * The code to parse. + */ + code: string; + + /** + * The expected outgoing calls. + */ + expectedOutgoingCalls: SimpleOutgoingCall[] | undefined; +} + +/** + * A simplified variant of {@link CallHierarchyOutgoingCall}. + */ +interface SimpleOutgoingCall { + /** + * The name of the callee. + */ + toName: string; + + /** + * The number of calls in the callee. + */ + fromRangesLength: number; +} diff --git a/packages/safe-ds-lang/tests/language/lsp/safe-ds-document-symbol-provider.test.ts b/packages/safe-ds-lang/tests/language/lsp/safe-ds-document-symbol-provider.test.ts index 4f1d49bd0..6c14a7334 100644 --- a/packages/safe-ds-lang/tests/language/lsp/safe-ds-document-symbol-provider.test.ts +++ b/packages/safe-ds-lang/tests/language/lsp/safe-ds-document-symbol-provider.test.ts @@ -1,8 +1,8 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { NodeFileSystem } from 'langium/node'; import { clearDocuments, parseDocument, textDocumentParams } from 'langium/test'; -import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { DocumentSymbol, SymbolKind, SymbolTag } from 'vscode-languageserver'; -import { NodeFileSystem } from 'langium/node'; +import { createSafeDsServices } from '../../../src/language/index.js'; const services = createSafeDsServices(NodeFileSystem).SafeDs; const symbolProvider = services.lsp.DocumentSymbolProvider!; diff --git a/packages/safe-ds-lang/tests/language/lsp/safe-ds-semantic-token-provider.test.ts b/packages/safe-ds-lang/tests/language/lsp/safe-ds-semantic-token-provider.test.ts index 73585a296..cf846a06c 100644 --- a/packages/safe-ds-lang/tests/language/lsp/safe-ds-semantic-token-provider.test.ts +++ b/packages/safe-ds-lang/tests/language/lsp/safe-ds-semantic-token-provider.test.ts @@ -1,9 +1,9 @@ -import { afterEach, beforeEach, describe, it } from 'vitest'; -import { clearDocuments, highlightHelper } from 'langium/test'; -import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; -import { SemanticTokenTypes } from 'vscode-languageserver'; import { AssertionError } from 'assert'; import { NodeFileSystem } from 'langium/node'; +import { clearDocuments, highlightHelper } from 'langium/test'; +import { afterEach, beforeEach, describe, it } from 'vitest'; +import { SemanticTokenTypes } from 'vscode-languageserver'; +import { createSafeDsServices } from '../../../src/language/index.js'; const services = createSafeDsServices(NodeFileSystem).SafeDs; diff --git a/packages/safe-ds-lang/tests/resources/partial evaluation/recursive cases/calls/of enum variants/main.sdstest b/packages/safe-ds-lang/tests/resources/partial evaluation/recursive cases/calls/of enum variants/main.sdstest index 131b375a8..1a48baa2a 100644 --- a/packages/safe-ds-lang/tests/resources/partial evaluation/recursive cases/calls/of enum variants/main.sdstest +++ b/packages/safe-ds-lang/tests/resources/partial evaluation/recursive cases/calls/of enum variants/main.sdstest @@ -14,7 +14,7 @@ pipeline test { // $TEST$ serialization MyEnumVariantWithParameters(?, 3) »MyEnum.MyEnumVariantWithParameters()«; - // $TEST$ serialization MyEnumVariantWithParameters(1, 3) + // $TEST$ serialization MyEnumVariantWithParameters(1, 3) »MyEnum.MyEnumVariantWithParameters(1)«; // $TEST$ serialization MyEnumVariantWithParameters(1, 2)