diff --git a/src/language/documentation/safe-ds-comment-provider.ts b/src/language/documentation/safe-ds-comment-provider.ts new file mode 100644 index 000000000..6e54eb088 --- /dev/null +++ b/src/language/documentation/safe-ds-comment-provider.ts @@ -0,0 +1,33 @@ +import { AstNode, DefaultCommentProvider, isAstNodeWithComment } from 'langium'; +import { + isSdsBlockLambdaResult, + isSdsDeclaration, + isSdsParameter, + isSdsPlaceholder, + isSdsResult, + isSdsTypeParameter, +} from '../generated/ast.js'; + +export class SafeDsCommentProvider extends DefaultCommentProvider { + override getComment(node: AstNode): string | undefined { + /* c8 ignore start */ if (isAstNodeWithComment(node)) { + return node.$comment; + } /* c8 ignore stop */ else if ( + !isSdsDeclaration(node) || + isSdsBlockLambdaResult(node) || + isSdsParameter(node) || + isSdsPlaceholder(node) || + isSdsResult(node) || + isSdsTypeParameter(node) + ) { + return undefined; + } + + // The annotation call list is the previous sibling of the declaration in the CST, so we must step past it + if (node.annotationCallList) { + return super.getComment(node.annotationCallList); + } else { + return super.getComment(node); + } + } +} diff --git a/src/language/safe-ds-module.ts b/src/language/safe-ds-module.ts index 830b0d914..d4fa2304f 100644 --- a/src/language/safe-ds-module.ts +++ b/src/language/safe-ds-module.ts @@ -4,6 +4,7 @@ import { DeepPartial, DefaultSharedModuleContext, inject, + JSDocDocumentationProvider, LangiumServices, LangiumSharedServices, Module, @@ -31,6 +32,7 @@ import { SafeDsDocumentSymbolProvider } from './lsp/safe-ds-document-symbol-prov import { SafeDsDocumentBuilder } from './workspace/safe-ds-document-builder.js'; import { SafeDsEnums } from './builtins/safe-ds-enums.js'; import { SafeDsInlayHintProvider } from './lsp/safe-ds-inlay-hint-provider.js'; +import { SafeDsCommentProvider } from './documentation/safe-ds-comment-provider.js'; /** * Declaration of custom services - add your own service classes here. @@ -75,6 +77,10 @@ export const SafeDsModule: Module new SafeDsClasses(services), Enums: (services) => new SafeDsEnums(services), }, + documentation: { + CommentProvider: (services) => new SafeDsCommentProvider(services), + DocumentationProvider: (services) => new JSDocDocumentationProvider(services), + }, evaluation: { PartialEvaluator: (services) => new SafeDsPartialEvaluator(services), }, diff --git a/tests/language/documentation/safe-ds-comment-provider.test.ts b/tests/language/documentation/safe-ds-comment-provider.test.ts new file mode 100644 index 000000000..99e784bc0 --- /dev/null +++ b/tests/language/documentation/safe-ds-comment-provider.test.ts @@ -0,0 +1,266 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; +import { AstNode, EmptyFileSystem } from 'langium'; +import { clearDocuments } from 'langium/test'; +import { getNodeOfType } from '../../helpers/nodeFinder.js'; +import { AssertionError } from 'assert'; +import { + isSdsAnnotation, + isSdsAttribute, + isSdsBlockLambdaResult, + isSdsEnumVariant, + isSdsExpressionStatement, + isSdsParameter, + isSdsPlaceholder, + isSdsResult, + isSdsTypeParameter, +} from '../../../src/language/generated/ast.js'; + +const services = createSafeDsServices(EmptyFileSystem).SafeDs; +const commentProvider = services.documentation.CommentProvider; +const testComment = '/* test */'; + +describe('SafeDsCommentProvider', () => { + afterEach(async () => { + await clearDocuments(services); + }); + + const testCases: CommentProviderTest[] = [ + { + testName: 'commented module member (without annotations)', + code: ` + ${testComment} + annotation MyAnnotation + `, + predicate: isSdsAnnotation, + expectedComment: testComment, + }, + { + testName: 'commented module member (with annotations, missing package)', + code: ` + ${testComment} + @Test + annotation MyAnnotation + `, + predicate: isSdsAnnotation, + expectedComment: undefined, + }, + { + testName: 'commented module member (with annotations, with package)', + code: ` + package test + + ${testComment} + @Test + annotation MyAnnotation + `, + predicate: isSdsAnnotation, + expectedComment: testComment, + }, + { + testName: 'uncommented module member', + code: ` + annotation MyAnnotation + `, + predicate: isSdsAnnotation, + expectedComment: undefined, + }, + { + testName: 'commented class member (without annotations)', + code: ` + class MyClass { + ${testComment} + attr a: Int + } + `, + predicate: isSdsAttribute, + expectedComment: testComment, + }, + { + testName: 'commented class member (with annotations)', + code: ` + class MyClass { + ${testComment} + @Test + attr a: Int + } + `, + predicate: isSdsAttribute, + expectedComment: testComment, + }, + { + testName: 'uncommented class member', + code: ` + class MyClass { + attr a: Int + } + `, + predicate: isSdsAttribute, + expectedComment: undefined, + }, + { + testName: 'commented enum variant (without annotations)', + code: ` + enum MyEnum { + ${testComment} + MyEnumVariant + } + `, + predicate: isSdsEnumVariant, + expectedComment: testComment, + }, + { + testName: 'commented enum variant (with annotations)', + code: ` + enum MyEnum { + ${testComment} + @Test + MyEnumVariant + } + `, + predicate: isSdsEnumVariant, + expectedComment: testComment, + }, + { + testName: 'uncommented enum variant', + code: ` + enum MyEnum { + MyEnumVariant + } + `, + predicate: isSdsEnumVariant, + expectedComment: undefined, + }, + { + testName: 'commented block lambda result', + code: ` + pipeline myPipeline { + () { + ${testComment} + yield r = 1; + } + } + `, + predicate: isSdsBlockLambdaResult, + expectedComment: undefined, + }, + { + testName: 'uncommented block lambda result', + code: ` + pipeline myPipeline { + () { + yield r = 1; + } + } + `, + predicate: isSdsBlockLambdaResult, + expectedComment: undefined, + }, + { + testName: 'commented parameter', + code: ` + segment mySegment(${testComment} p: Int) {} + `, + predicate: isSdsParameter, + expectedComment: undefined, + }, + { + testName: 'uncommented parameter', + code: ` + segment mySegment(p: Int) {} + `, + predicate: isSdsParameter, + expectedComment: undefined, + }, + { + testName: 'commented placeholder', + code: ` + segment mySegment() { + ${testComment} + val p = 1; + } + `, + predicate: isSdsPlaceholder, + expectedComment: undefined, + }, + { + testName: 'uncommented placeholder', + code: ` + segment mySegment(p: Int) { + val p = 1; + } + `, + predicate: isSdsPlaceholder, + expectedComment: undefined, + }, + { + testName: 'commented result', + code: ` + segment mySegment() -> (${testComment} r: Int) {} + `, + predicate: isSdsResult, + expectedComment: undefined, + }, + { + testName: 'uncommented result', + code: ` + segment mySegment() -> (r: Int) {} + `, + predicate: isSdsResult, + expectedComment: undefined, + }, + { + testName: 'commented type parameter', + code: ` + class MyClass<${testComment} T> + `, + predicate: isSdsTypeParameter, + expectedComment: undefined, + }, + { + testName: 'uncommented type parameter', + code: ` + class MyClass + `, + predicate: isSdsTypeParameter, + expectedComment: undefined, + }, + { + testName: 'commented not-a-declaration', + code: ` + segment mySegment(p: Int) { + ${testComment} + f(); + } + `, + predicate: isSdsExpressionStatement, + expectedComment: undefined, + }, + { + testName: 'uncommented not-a-declaration', + code: ` + segment mySegment(p: Int) { + f(); + } + `, + predicate: isSdsExpressionStatement, + expectedComment: undefined, + }, + ]; + + it.each(testCases)('$testName', async ({ code, predicate, expectedComment }) => { + const node = await getNodeOfType(services, code, predicate); + if (!node) { + throw new AssertionError({ message: 'Node not found.' }); + } + + expect(commentProvider.getComment(node)).toStrictEqual(expectedComment); + }); +}); + +interface CommentProviderTest { + testName: string; + code: string; + predicate: (node: unknown) => node is AstNode; + expectedComment: string | undefined; +}