From c064e0e4a5d081f92b791a8a96efe580b1cefa98 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Thu, 4 Apr 2024 20:21:54 +0200 Subject: [PATCH] feat: inlay hints for inferred types of lambda parameters (#993) ### Summary of Changes Add optional inlay hints for the inferred types of lambda parameters without manifest types. --- .../lsp/safe-ds-inlay-hint-provider.ts | 86 +++-- .../workspace/safe-ds-settings-provider.ts | 7 + .../lsp/safe-ds-inlay-hint-provider.test.ts | 327 +++++++++++++----- packages/safe-ds-vscode/package.json | 5 + 4 files changed, 313 insertions(+), 112 deletions(-) diff --git a/packages/safe-ds-lang/src/language/lsp/safe-ds-inlay-hint-provider.ts b/packages/safe-ds-lang/src/language/lsp/safe-ds-inlay-hint-provider.ts index 77511b217..09bee8ffb 100644 --- a/packages/safe-ds-lang/src/language/lsp/safe-ds-inlay-hint-provider.ts +++ b/packages/safe-ds-lang/src/language/lsp/safe-ds-inlay-hint-provider.ts @@ -1,4 +1,4 @@ -import { AstNode, AstUtils, DocumentationProvider, interruptAndCheck, LangiumDocument } from 'langium'; +import { AstNode, AstUtils, CstNode, DocumentationProvider, interruptAndCheck, LangiumDocument } from 'langium'; import { CancellationToken, type InlayHint, @@ -7,11 +7,22 @@ import { MarkupContent, } from 'vscode-languageserver'; import { createMarkupContent } from '../documentation/safe-ds-comment-provider.js'; -import { isSdsArgument, isSdsBlockLambdaResult, isSdsPlaceholder, isSdsYield } from '../generated/ast.js'; +import { + isSdsArgument, + isSdsBlockLambdaResult, + isSdsLambda, + isSdsParameter, + isSdsPlaceholder, + isSdsYield, + SdsArgument, + SdsBlockLambdaResult, + SdsPlaceholder, + SdsYield, +} from '../generated/ast.js'; import { Argument } from '../helpers/nodeProperties.js'; import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js'; import { SafeDsServices } from '../safe-ds-module.js'; -import { NamedType } from '../typing/model.js'; +import { NamedType, UnknownType } from '../typing/model.js'; import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js'; import { AbstractInlayHintProvider, InlayHintAcceptor } from 'langium/lsp'; import { SafeDsSettingsProvider } from '../workspace/safe-ds-settings-provider.js'; @@ -54,31 +65,37 @@ export class SafeDsInlayHintProvider extends AbstractInlayHintProvider { return; } - // Show inferred types for named assignees - if ( - (await this.settingsProvider.shouldShowAssigneeTypeInlayHints()) && - (isSdsBlockLambdaResult(node) || isSdsPlaceholder(node) || isSdsYield(node)) - ) { - const type = this.typeComputer.computeType(node); - let tooltip: MarkupContent | undefined = undefined; - if (type instanceof NamedType) { - tooltip = createMarkupContent(this.documentationProvider.getDocumentation(type.declaration)); - } + if (await this.settingsProvider.shouldShowAssigneeTypeInlayHints()) { + this.computeAssigneeTypeInlayHint(node, cstNode, acceptor); + } + + if (await this.settingsProvider.shouldShowLambdaParameterTypeInlayHints()) { + this.computeLambdaParameterTypeInlayHint(node, cstNode, acceptor); + } - acceptor({ - position: cstNode.range.end, - label: `: ${type}`, - kind: InlayHintKind.Type, - tooltip, - }); + if (await this.settingsProvider.shouldShowParameterNameInlayHints()) { + this.computeParameterNameInlayHint(node, cstNode, acceptor); } + } - // Show parameter names for positional arguments - if ( - (await this.settingsProvider.shouldShowParameterNameInlayHints()) && - isSdsArgument(node) && - Argument.isPositional(node) - ) { + private computeAssigneeTypeInlayHint( + node: AstNode | SdsBlockLambdaResult | SdsPlaceholder | SdsYield, + cstNode: CstNode, + acceptor: InlayHintAcceptor, + ) { + if (isSdsBlockLambdaResult(node) || isSdsPlaceholder(node) || isSdsYield(node)) { + this.computeTypeInlayHint(node, cstNode, acceptor); + } + } + + private computeLambdaParameterTypeInlayHint(node: AstNode, cstNode: CstNode, acceptor: InlayHintAcceptor) { + if (isSdsParameter(node) && AstUtils.hasContainerOfType(node, isSdsLambda) && !node.type) { + this.computeTypeInlayHint(node, cstNode, acceptor); + } + } + + private computeParameterNameInlayHint(node: AstNode | SdsArgument, cstNode: CstNode, acceptor: InlayHintAcceptor) { + if (isSdsArgument(node) && Argument.isPositional(node)) { const parameter = this.nodeMapper.argumentToParameter(node); if (parameter) { acceptor({ @@ -90,4 +107,23 @@ export class SafeDsInlayHintProvider extends AbstractInlayHintProvider { } } } + + private computeTypeInlayHint(node: AstNode, cstNode: CstNode, acceptor: InlayHintAcceptor) { + const type = this.typeComputer.computeType(node); + if (type === UnknownType) { + return; + } + + let tooltip: MarkupContent | undefined = undefined; + if (type instanceof NamedType) { + tooltip = createMarkupContent(this.documentationProvider.getDocumentation(type.declaration)); + } + + acceptor({ + position: cstNode.range.end, + label: `: ${type}`, + kind: InlayHintKind.Type, + tooltip, + }); + } } diff --git a/packages/safe-ds-lang/src/language/workspace/safe-ds-settings-provider.ts b/packages/safe-ds-lang/src/language/workspace/safe-ds-settings-provider.ts index da6977363..583ff91d3 100644 --- a/packages/safe-ds-lang/src/language/workspace/safe-ds-settings-provider.ts +++ b/packages/safe-ds-lang/src/language/workspace/safe-ds-settings-provider.ts @@ -13,6 +13,10 @@ export class SafeDsSettingsProvider { return (await this.getInlayHintsSettings()).assigneeTypes?.enabled ?? true; } + async shouldShowLambdaParameterTypeInlayHints(): Promise { + return (await this.getInlayHintsSettings()).lambdaParameterTypes?.enabled ?? true; + } + async shouldShowParameterNameInlayHints(): Promise { return (await this.getInlayHintsSettings()).parameterNames?.enabled ?? true; } @@ -50,6 +54,9 @@ interface InlayHintsSettings { assigneeTypes: { enabled: boolean; }; + lambdaParameterTypes: { + enabled: boolean; + }; parameterNames: { enabled: boolean; }; diff --git a/packages/safe-ds-lang/tests/language/lsp/safe-ds-inlay-hint-provider.test.ts b/packages/safe-ds-lang/tests/language/lsp/safe-ds-inlay-hint-provider.test.ts index d159820f0..c075da4b0 100644 --- a/packages/safe-ds-lang/tests/language/lsp/safe-ds-inlay-hint-provider.test.ts +++ b/packages/safe-ds-lang/tests/language/lsp/safe-ds-inlay-hint-provider.test.ts @@ -11,132 +11,223 @@ const inlayHintProvider = services.lsp.InlayHintProvider!; const parse = parseHelper(services); describe('SafeDsInlayHintProvider', async () => { - const testCases: InlayHintProviderTest[] = [ - { - testName: 'resolved positional argument', - code: ` - fun f(p: Int) + describe('assignee types', () => { + const testCases: InlayHintProviderTest[] = [ + { + testName: 'block lambda result', + code: ` + pipeline myPipeline { + () { + // $TEST$ after ": literal<1>" + yield r»« = 1; + }; + } + `, + }, + { + testName: 'placeholder', + code: ` + pipeline myPipeline { + // $TEST$ after ": literal<1>" + val x»« = 1; + } + `, + }, + { + testName: 'wildcard', + code: ` + pipeline myPipeline { + _ = 1; + } + `, + }, + { + testName: 'yield', + code: ` + segment s() -> r: Int { + // $TEST$ after ": literal<1>" + yield r»« = 1; + } + `, + }, + ]; + it.each(testCases)('should assign the correct inlay hints ($testName)', async ({ code }) => { + const actualInlayHints = await getActualSimpleInlayHints(code); + const expectedInlayHints = getExpectedSimpleInlayHints(code); + + expect(actualInlayHints).toStrictEqual(expectedInlayHints); + }); + + it.each([ + { + testName: 'class', + code: ` + /** + * Lorem ipsum. + */ + class C() pipeline myPipeline { - // $TEST$ before "p = " - f(»«1); + val a = C(); } `, - }, - { - testName: 'unresolved positional argument', - code: ` - fun f() + }, + { + testName: 'enum', + code: ` + /** + * Lorem ipsum. + */ + enum E + + fun f() -> e: E pipeline myPipeline { - f(1); + val a = f(); } `, - }, - { - testName: 'named argument', - code: ` - fun f(p: Int) + }, + { + testName: 'enum variant', + code: ` + enum E { + /** + * Lorem ipsum. + */ + V + } pipeline myPipeline { - f(p = 1); + val a = E.V; } `, - }, - { - testName: 'block lambda result', - code: ` + }, + ])('should set the documentation of named types as tooltip ($testName)', async ({ code }) => { + const actualInlayHints = await getActualInlayHints(code); + const firstInlayHint = actualInlayHints?.[0]; + + expect(firstInlayHint?.tooltip).toStrictEqual({ kind: 'markdown', value: 'Lorem ipsum.' }); + }); + }); + + describe('lambda parameter types', () => { + const testCases: InlayHintProviderTest[] = [ + { + testName: 'standalone block lambda without manifest types', + code: ` pipeline myPipeline { - () { - // $TEST$ after ": literal<1>" - yield r»« = 1; - }; + (p) {}; } `, - }, - { - testName: 'placeholder', - code: ` + }, + { + testName: 'standalone block lambda with manifest types', + code: ` pipeline myPipeline { - // $TEST$ after ": literal<1>" - val x»« = 1; + (p: Int) {}; } `, - }, - { - testName: 'wildcard', - code: ` + }, + { + testName: 'standalone expression lambda without manifest types', + code: ` pipeline myPipeline { - _ = 1; + (p) -> 1; } `, - }, - { - testName: 'yield', - code: ` - segment s() -> r: Int { - // $TEST$ after ": literal<1>" - yield r»« = 1; + }, + { + testName: 'standalone expression lambda with manifest types', + code: ` + pipeline myPipeline { + (p: Int) -> 1; } `, - }, - ]; - it.each(testCases)('should assign the correct inlay hints ($testName)', async ({ code }) => { - const actualInlayHints = await getActualSimpleInlayHints(code); - const expectedInlayHints = getExpectedSimpleInlayHints(code); + }, + { + testName: 'assigned block lambda without manifest types', + code: ` + fun f(callback: (p: Int) -> ()) - expect(actualInlayHints).toStrictEqual(expectedInlayHints); - }); + pipeline myPipeline { + // $TEST$ after ": Int" + f(callback = (p»«) {}); + } + `, + }, + { + testName: 'assigned block lambda with manifest types', + code: ` + fun f(callback: (p: Int) -> ()) - it('should set the documentation of parameters as tooltip', async () => { - const code = ` - /** - * @param p Lorem ipsum. - */ - fun f(p: Int) + pipeline myPipeline { + f(callback = (p: Int) {}); + } + `, + }, + { + testName: 'assigned expression lambda without manifest types', + code: ` + fun f(callback: (p: Int) -> (r: Int)) - pipeline myPipeline { - f(1); - } - `; - const actualInlayHints = await getActualInlayHints(code); - const firstInlayHint = actualInlayHints?.[0]; + pipeline myPipeline { + // $TEST$ after ": Int" + f(callback = (p»«) -> 1); + } + `, + }, + { + testName: 'assigned expression lambda with manifest types', + code: ` + fun f(callback: (p: Int) -> (r: Int)) - expect(firstInlayHint?.tooltip).toStrictEqual({ kind: 'markdown', value: 'Lorem ipsum.' }); - }); + pipeline myPipeline { + f(callback = (p: Int) -> 1); + } + `, + }, + ]; + it.each(testCases)('should assign the correct inlay hints ($testName)', async ({ code }) => { + const actualInlayHints = await getActualSimpleInlayHints(code); + const expectedInlayHints = getExpectedSimpleInlayHints(code); - it.each([ - { - testName: 'class', - code: ` + expect(actualInlayHints).toStrictEqual(expectedInlayHints); + }); + + it.each([ + { + testName: 'class', + code: ` /** * Lorem ipsum. */ class C() + fun f(callback: (p: C) -> ()) + pipeline myPipeline { - val a = C(); + f(callback = (p) {}); } `, - }, - { - testName: 'enum', - code: ` + }, + { + testName: 'enum', + code: ` /** * Lorem ipsum. */ enum E - fun f() -> e: E + fun f(callback: (p: E) -> ()) pipeline myPipeline { - val a = f(); + f(callback = (p) {}); } `, - }, - { - testName: 'enum variant', - code: ` + }, + { + testName: 'enum variant', + code: ` enum E { /** * Lorem ipsum. @@ -144,16 +235,78 @@ describe('SafeDsInlayHintProvider', async () => { V } + fun f(callback: (p: E.V) -> ()) + pipeline myPipeline { - val a = E.V; + f(callback = (p) {}); + } + `, + }, + ])('should set the documentation of named types as tooltip ($testName)', async ({ code }) => { + const actualInlayHints = await getActualInlayHints(code); + const firstInlayHint = actualInlayHints?.[0]; + + expect(firstInlayHint?.tooltip).toStrictEqual({ kind: 'markdown', value: 'Lorem ipsum.' }); + }); + }); + + describe('parameter names', () => { + const testCases: InlayHintProviderTest[] = [ + { + testName: 'resolved positional argument', + code: ` + fun f(p: Int) + + pipeline myPipeline { + // $TEST$ before "p = " + f(»«1); } `, - }, - ])('should set the documentation of named types as tooltip', async ({ code }) => { - const actualInlayHints = await getActualInlayHints(code); - const firstInlayHint = actualInlayHints?.[0]; + }, + { + testName: 'unresolved positional argument', + code: ` + fun f() + + pipeline myPipeline { + f(1); + } + `, + }, + { + testName: 'named argument', + code: ` + fun f(p: Int) + + pipeline myPipeline { + f(p = 1); + } + `, + }, + ]; + it.each(testCases)('should assign the correct inlay hints ($testName)', async ({ code }) => { + const actualInlayHints = await getActualSimpleInlayHints(code); + const expectedInlayHints = getExpectedSimpleInlayHints(code); + + expect(actualInlayHints).toStrictEqual(expectedInlayHints); + }); + + it('should set the documentation of parameters as tooltip', async () => { + const code = ` + /** + * @param p Lorem ipsum. + */ + fun f(p: Int) + + pipeline myPipeline { + f(1); + } + `; + const actualInlayHints = await getActualInlayHints(code); + const firstInlayHint = actualInlayHints?.[0]; - expect(firstInlayHint?.tooltip).toStrictEqual({ kind: 'markdown', value: 'Lorem ipsum.' }); + expect(firstInlayHint?.tooltip).toStrictEqual({ kind: 'markdown', value: 'Lorem ipsum.' }); + }); }); }); diff --git a/packages/safe-ds-vscode/package.json b/packages/safe-ds-vscode/package.json index 36bce2754..e7d654bb9 100644 --- a/packages/safe-ds-vscode/package.json +++ b/packages/safe-ds-vscode/package.json @@ -88,6 +88,11 @@ "default": true, "description": "Show inferred types for named assignees." }, + "safe-ds.inlayHints.lambdaParameterTypes.enabled": { + "type": "boolean", + "default": true, + "description": "Show inferred types for lambda parameters without manifest types." + }, "safe-ds.inlayHints.parameterNames.enabled": { "type": "boolean", "default": true,