Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: comment provider #688

Merged
merged 2 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}