Skip to content

Commit

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

### Summary of Changes

Add a custom documentation provider. It can now handle tags `@param`,
`@result`, and `@typeParam` to set the documentation for parameters,
results, and type parameters of a callable respectively.
  • Loading branch information
lars-reimann committed Oct 24, 2023
1 parent e4a1b35 commit ff70b07
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 6 deletions.
88 changes: 88 additions & 0 deletions src/language/documentation/safe-ds-documentation-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
AstNode,
getContainerOfType,
isJSDoc,
JSDocComment,
JSDocDocumentationProvider,
JSDocRenderOptions,
parseJSDoc,
} from 'langium';
import {
isSdsCallable,
isSdsParameter,
isSdsResult,
isSdsTypeParameter,
SdsParameter,
SdsResult,
SdsTypeParameter,
} from '../generated/ast.js';

export class SafeDsDocumentationProvider extends JSDocDocumentationProvider {
override getDocumentation(node: AstNode): string | undefined {
if (isSdsParameter(node) || isSdsResult(node) || isSdsTypeParameter(node)) {
const containingCallable = getContainerOfType(node, isSdsCallable);
/* c8 ignore start */
if (!containingCallable) {
return undefined;
}
/* c8 ignore stop */

const comment = this.getJSDocComment(containingCallable);
if (!comment) {
return undefined;
}

return this.getMatchingTagContent(comment, node);
} else {
const comment = this.getJSDocComment(node);
return comment?.toMarkdown(this.createJSDocRenderOptions(node));
}
}

private getJSDocComment(node: AstNode): JSDocComment | undefined {
const comment = this.commentProvider.getComment(node);
if (comment && isJSDoc(comment)) {
return parseJSDoc(comment);
}
return undefined;
}

private getMatchingTagContent(
comment: JSDocComment,
node: SdsParameter | SdsResult | SdsTypeParameter,
): string | undefined {
const name = node.name;
/* c8 ignore start */
if (!name) {
return undefined;
}
/* c8 ignore stop */

const tagName = this.getTagName(node);
const matchRegex = new RegExp(`^${name}\\s+(?<content>.*)`, 'u');

return comment
.getTags(tagName)
.map((it) => it.content.toMarkdown(this.createJSDocRenderOptions(node)))
.find((it) => matchRegex.test(it))
?.match(matchRegex)?.groups?.content;
}

private getTagName(node: SdsParameter | SdsResult | SdsTypeParameter): string {
if (isSdsParameter(node)) {
return 'param';
} else if (isSdsResult(node)) {
return 'result';
} else {
return 'typeParam';
}
}

private createJSDocRenderOptions(node: AstNode): JSDocRenderOptions {
return {
renderLink: (link, display) => {
return this.documentationLinkRenderer(node, link, display);
},
};
}
}
4 changes: 2 additions & 2 deletions src/language/safe-ds-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
DeepPartial,
DefaultSharedModuleContext,
inject,
JSDocDocumentationProvider,
LangiumServices,
LangiumSharedServices,
Module,
Expand Down Expand Up @@ -33,6 +32,7 @@ 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';
import { SafeDsDocumentationProvider } from './documentation/safe-ds-documentation-provider.js';

/**
* Declaration of custom services - add your own service classes here.
Expand Down Expand Up @@ -79,7 +79,7 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
},
documentation: {
CommentProvider: (services) => new SafeDsCommentProvider(services),
DocumentationProvider: (services) => new JSDocDocumentationProvider(services),
DocumentationProvider: (services) => new SafeDsDocumentationProvider(services),
},
evaluation: {
PartialEvaluator: (services) => new SafeDsPartialEvaluator(services),
Expand Down
7 changes: 3 additions & 4 deletions src/resources/builtins/safeds/lang/codeGeneration.sdsstub
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ package safeds.lang
* The specification of a corresponding function call in Python. By default, the function is called as specified in the
* stub.
*
* @param callSpecification
* The specification of corresponding Python call. The specification can contain template expression, which are
* replaced by the corresponding arguments of the function call. `$this` is replaced by the receiver of the call.
* `$param` is replaced by the value of the parameter called `param`. Otherwise, the string is used as-is.
* The specification can contain template expressions, which are replaced by the corresponding arguments of the function
* call. `$this` is replaced by the receiver of the call. `$param` is replaced by the value of the parameter called
* `param`. Otherwise, the string is used as-is.
*/
@Target([AnnotationTarget.Function])
annotation PythonCall(
Expand Down
213 changes: 213 additions & 0 deletions tests/language/documentation/safe-ds-documentation-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
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 {
isSdsAnnotation,
isSdsFunction,
isSdsParameter,
isSdsResult,
isSdsTypeParameter,
} from '../../../src/language/generated/ast.js';

const services = createSafeDsServices(EmptyFileSystem).SafeDs;
const documentationProvider = services.documentation.DocumentationProvider;
const testDocumentation = 'Lorem ipsum.';

describe('SafeDsDocumentationProvider', () => {
afterEach(async () => {
await clearDocuments(services);
});

const testCases: DocumentationProviderTest[] = [
{
testName: 'module member',
code: `
/**
* ${testDocumentation}
*/
annotation MyAnnotation
`,
predicate: isSdsAnnotation,
expectedDocumentation: testDocumentation,
},
{
testName: 'documented parameter',
code: `
/**
* @param param ${testDocumentation}
*/
fun myFunction(param: String)
`,
predicate: isSdsParameter,
expectedDocumentation: testDocumentation,
},
{
testName: 'documented parameter (duplicate)',
code: `
/**
* @param param ${testDocumentation}
* @param param bla
*/
fun myFunction(param: String)
`,
predicate: isSdsParameter,
expectedDocumentation: testDocumentation,
},
{
testName: 'undocumented parameter',
code: `
/**
* @param param ${testDocumentation}
*/
fun myFunction(param2: String)
`,
predicate: isSdsParameter,
expectedDocumentation: undefined,
},
{
testName: 'parameter (no documentation on containing callable)',
code: `
fun myFunction(p: Int)
`,
predicate: isSdsParameter,
expectedDocumentation: undefined,
},
{
testName: 'documented result',
code: `
/**
* @result res ${testDocumentation}
*/
fun myFunction() -> (res: String)
`,
predicate: isSdsResult,
expectedDocumentation: testDocumentation,
},
{
testName: 'documented result (duplicate)',
code: `
/**
* @result res ${testDocumentation}
* @result res bla
*/
fun myFunction() -> (res: String)
`,
predicate: isSdsResult,
expectedDocumentation: testDocumentation,
},
{
testName: 'undocumented result',
code: `
/**
* @result res ${testDocumentation}
*/
fun myFunction() -> (res2: String)
`,
predicate: isSdsResult,
expectedDocumentation: undefined,
},
{
testName: 'result (no documentation on containing callable)',
code: `
fun myFunction() -> r: Int
`,
predicate: isSdsResult,
expectedDocumentation: undefined,
},
{
testName: 'documented type parameter',
code: `
enum MyEnum {
/**
* @typeParam T
* ${testDocumentation}
*/
MyEnumVariant<T>
}
`,
predicate: isSdsTypeParameter,
expectedDocumentation: testDocumentation,
},
{
testName: 'documented type parameter (duplicate)',
code: `
enum MyEnum {
/**
* @typeParam T ${testDocumentation}
* @typeParam T bla
*/
MyEnumVariant<T>
}
`,
predicate: isSdsTypeParameter,
expectedDocumentation: testDocumentation,
},
{
testName: 'undocumented type parameter',
code: `
enum MyEnum {
/**
* @typeParam T
* ${testDocumentation}
*/
MyEnumVariant<T2>
}
`,
predicate: isSdsTypeParameter,
expectedDocumentation: undefined,
},
{
testName: 'type parameter (no documentation on containing callable)',
code: `
fun myFunction<T>()
`,
predicate: isSdsTypeParameter,
expectedDocumentation: undefined,
},
];

it.each(testCases)('$testName', async ({ code, predicate, expectedDocumentation }) => {
const node = await getNodeOfType(services, code, predicate);
expect(documentationProvider.getDocumentation(node)).toStrictEqual(expectedDocumentation);
});

it('should resolve links', async () => {
const code = `
/**
* {@link myFunction2}
*/
fun myFunction1()
fun myFunction2()
`;
const node = await getNodeOfType(services, code, isSdsFunction);
expect(documentationProvider.getDocumentation(node)).toMatch(/\[myFunction2\]\(.*\)/u);
});
});

/**
* A description of a test case for the documentation provider.
*/
interface DocumentationProviderTest {
/**
* 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 documentation.
*/
expectedDocumentation: string | undefined;
}

0 comments on commit ff70b07

Please sign in to comment.