Skip to content

Commit

Permalink
feat: comment provider (#688)
Browse files Browse the repository at this point in the history
Closes partially #669

### Summary of Changes

Add a custom comment provider. It can now associate comments to
declarations with annotations.
  • Loading branch information
lars-reimann authored Oct 24, 2023
1 parent ea8fe29 commit e4a1b35
Show file tree
Hide file tree
Showing 3 changed files with 323 additions and 0 deletions.
33 changes: 33 additions & 0 deletions src/language/documentation/safe-ds-comment-provider.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
6 changes: 6 additions & 0 deletions src/language/safe-ds-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DeepPartial,
DefaultSharedModuleContext,
inject,
JSDocDocumentationProvider,
LangiumServices,
LangiumSharedServices,
Module,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -75,6 +77,10 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
Classes: (services) => 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),
},
Expand Down
284 changes: 284 additions & 0 deletions tests/language/documentation/safe-ds-comment-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
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<T>
`,
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);
});
});

/**
* A description of a test case for the comment provider.
*/
interface CommentProviderTest {
/**
* A short description of the test case.
*/
testName: string;

/**
* The code to test.
*/
code: string;

/**
* A predicate to find the node to test.
*/
predicate: (node: unknown) => node is AstNode;

/**
* The expected comment.
*/
expectedComment: string | undefined;
}

0 comments on commit e4a1b35

Please sign in to comment.