-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: call hierarchy provider (#735)
Closes #680 ### Summary of Changes Implement a call hierarchy provider to get incoming & outgoing calls.
- Loading branch information
1 parent
c40347c
commit 168d098
Showing
12 changed files
with
634 additions
and
41 deletions.
There are no files selected for viewing
32 changes: 32 additions & 0 deletions
32
packages/safe-ds-lang/src/language/flow/safe-ds-call-graph-computer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string, SdsCall[]>; | ||
|
||
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}`; | ||
} | ||
} |
195 changes: 195 additions & 0 deletions
195
packages/safe-ds-lang/src/language/lsp/safe-ds-call-hierarchy-provider.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ReferenceDescription>, | ||
): 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<ReferenceDescription>): Stream<SdsDeclaration> { | ||
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, | ||
})); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
packages/safe-ds-lang/src/language/lsp/safe-ds-node-info-provider.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.