Skip to content

Commit ff70b07

Browse files
authored
feat: documentation provider (#689)
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.
1 parent e4a1b35 commit ff70b07

File tree

4 files changed

+306
-6
lines changed

4 files changed

+306
-6
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {
2+
AstNode,
3+
getContainerOfType,
4+
isJSDoc,
5+
JSDocComment,
6+
JSDocDocumentationProvider,
7+
JSDocRenderOptions,
8+
parseJSDoc,
9+
} from 'langium';
10+
import {
11+
isSdsCallable,
12+
isSdsParameter,
13+
isSdsResult,
14+
isSdsTypeParameter,
15+
SdsParameter,
16+
SdsResult,
17+
SdsTypeParameter,
18+
} from '../generated/ast.js';
19+
20+
export class SafeDsDocumentationProvider extends JSDocDocumentationProvider {
21+
override getDocumentation(node: AstNode): string | undefined {
22+
if (isSdsParameter(node) || isSdsResult(node) || isSdsTypeParameter(node)) {
23+
const containingCallable = getContainerOfType(node, isSdsCallable);
24+
/* c8 ignore start */
25+
if (!containingCallable) {
26+
return undefined;
27+
}
28+
/* c8 ignore stop */
29+
30+
const comment = this.getJSDocComment(containingCallable);
31+
if (!comment) {
32+
return undefined;
33+
}
34+
35+
return this.getMatchingTagContent(comment, node);
36+
} else {
37+
const comment = this.getJSDocComment(node);
38+
return comment?.toMarkdown(this.createJSDocRenderOptions(node));
39+
}
40+
}
41+
42+
private getJSDocComment(node: AstNode): JSDocComment | undefined {
43+
const comment = this.commentProvider.getComment(node);
44+
if (comment && isJSDoc(comment)) {
45+
return parseJSDoc(comment);
46+
}
47+
return undefined;
48+
}
49+
50+
private getMatchingTagContent(
51+
comment: JSDocComment,
52+
node: SdsParameter | SdsResult | SdsTypeParameter,
53+
): string | undefined {
54+
const name = node.name;
55+
/* c8 ignore start */
56+
if (!name) {
57+
return undefined;
58+
}
59+
/* c8 ignore stop */
60+
61+
const tagName = this.getTagName(node);
62+
const matchRegex = new RegExp(`^${name}\\s+(?<content>.*)`, 'u');
63+
64+
return comment
65+
.getTags(tagName)
66+
.map((it) => it.content.toMarkdown(this.createJSDocRenderOptions(node)))
67+
.find((it) => matchRegex.test(it))
68+
?.match(matchRegex)?.groups?.content;
69+
}
70+
71+
private getTagName(node: SdsParameter | SdsResult | SdsTypeParameter): string {
72+
if (isSdsParameter(node)) {
73+
return 'param';
74+
} else if (isSdsResult(node)) {
75+
return 'result';
76+
} else {
77+
return 'typeParam';
78+
}
79+
}
80+
81+
private createJSDocRenderOptions(node: AstNode): JSDocRenderOptions {
82+
return {
83+
renderLink: (link, display) => {
84+
return this.documentationLinkRenderer(node, link, display);
85+
},
86+
};
87+
}
88+
}

src/language/safe-ds-module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
DeepPartial,
55
DefaultSharedModuleContext,
66
inject,
7-
JSDocDocumentationProvider,
87
LangiumServices,
98
LangiumSharedServices,
109
Module,
@@ -33,6 +32,7 @@ import { SafeDsDocumentBuilder } from './workspace/safe-ds-document-builder.js';
3332
import { SafeDsEnums } from './builtins/safe-ds-enums.js';
3433
import { SafeDsInlayHintProvider } from './lsp/safe-ds-inlay-hint-provider.js';
3534
import { SafeDsCommentProvider } from './documentation/safe-ds-comment-provider.js';
35+
import { SafeDsDocumentationProvider } from './documentation/safe-ds-documentation-provider.js';
3636

3737
/**
3838
* Declaration of custom services - add your own service classes here.
@@ -79,7 +79,7 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
7979
},
8080
documentation: {
8181
CommentProvider: (services) => new SafeDsCommentProvider(services),
82-
DocumentationProvider: (services) => new JSDocDocumentationProvider(services),
82+
DocumentationProvider: (services) => new SafeDsDocumentationProvider(services),
8383
},
8484
evaluation: {
8585
PartialEvaluator: (services) => new SafeDsPartialEvaluator(services),

src/resources/builtins/safeds/lang/codeGeneration.sdsstub

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ package safeds.lang
44
* The specification of a corresponding function call in Python. By default, the function is called as specified in the
55
* stub.
66
*
7-
* @param callSpecification
8-
* The specification of corresponding Python call. The specification can contain template expression, which are
9-
* replaced by the corresponding arguments of the function call. `$this` is replaced by the receiver of the call.
10-
* `$param` is replaced by the value of the parameter called `param`. Otherwise, the string is used as-is.
7+
* The specification can contain template expressions, which are replaced by the corresponding arguments of the function
8+
* call. `$this` is replaced by the receiver of the call. `$param` is replaced by the value of the parameter called
9+
* `param`. Otherwise, the string is used as-is.
1110
*/
1211
@Target([AnnotationTarget.Function])
1312
annotation PythonCall(
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { afterEach, describe, expect, it } from 'vitest';
2+
import { createSafeDsServices } from '../../../src/language/safe-ds-module.js';
3+
import { AstNode, EmptyFileSystem } from 'langium';
4+
import { clearDocuments } from 'langium/test';
5+
import { getNodeOfType } from '../../helpers/nodeFinder.js';
6+
import {
7+
isSdsAnnotation,
8+
isSdsFunction,
9+
isSdsParameter,
10+
isSdsResult,
11+
isSdsTypeParameter,
12+
} from '../../../src/language/generated/ast.js';
13+
14+
const services = createSafeDsServices(EmptyFileSystem).SafeDs;
15+
const documentationProvider = services.documentation.DocumentationProvider;
16+
const testDocumentation = 'Lorem ipsum.';
17+
18+
describe('SafeDsDocumentationProvider', () => {
19+
afterEach(async () => {
20+
await clearDocuments(services);
21+
});
22+
23+
const testCases: DocumentationProviderTest[] = [
24+
{
25+
testName: 'module member',
26+
code: `
27+
/**
28+
* ${testDocumentation}
29+
*/
30+
annotation MyAnnotation
31+
`,
32+
predicate: isSdsAnnotation,
33+
expectedDocumentation: testDocumentation,
34+
},
35+
{
36+
testName: 'documented parameter',
37+
code: `
38+
/**
39+
* @param param ${testDocumentation}
40+
*/
41+
fun myFunction(param: String)
42+
`,
43+
predicate: isSdsParameter,
44+
expectedDocumentation: testDocumentation,
45+
},
46+
{
47+
testName: 'documented parameter (duplicate)',
48+
code: `
49+
/**
50+
* @param param ${testDocumentation}
51+
* @param param bla
52+
*/
53+
fun myFunction(param: String)
54+
`,
55+
predicate: isSdsParameter,
56+
expectedDocumentation: testDocumentation,
57+
},
58+
{
59+
testName: 'undocumented parameter',
60+
code: `
61+
/**
62+
* @param param ${testDocumentation}
63+
*/
64+
fun myFunction(param2: String)
65+
`,
66+
predicate: isSdsParameter,
67+
expectedDocumentation: undefined,
68+
},
69+
{
70+
testName: 'parameter (no documentation on containing callable)',
71+
code: `
72+
fun myFunction(p: Int)
73+
`,
74+
predicate: isSdsParameter,
75+
expectedDocumentation: undefined,
76+
},
77+
{
78+
testName: 'documented result',
79+
code: `
80+
/**
81+
* @result res ${testDocumentation}
82+
*/
83+
fun myFunction() -> (res: String)
84+
`,
85+
predicate: isSdsResult,
86+
expectedDocumentation: testDocumentation,
87+
},
88+
{
89+
testName: 'documented result (duplicate)',
90+
code: `
91+
/**
92+
* @result res ${testDocumentation}
93+
* @result res bla
94+
*/
95+
fun myFunction() -> (res: String)
96+
`,
97+
predicate: isSdsResult,
98+
expectedDocumentation: testDocumentation,
99+
},
100+
{
101+
testName: 'undocumented result',
102+
code: `
103+
/**
104+
* @result res ${testDocumentation}
105+
*/
106+
fun myFunction() -> (res2: String)
107+
`,
108+
predicate: isSdsResult,
109+
expectedDocumentation: undefined,
110+
},
111+
{
112+
testName: 'result (no documentation on containing callable)',
113+
code: `
114+
fun myFunction() -> r: Int
115+
`,
116+
predicate: isSdsResult,
117+
expectedDocumentation: undefined,
118+
},
119+
{
120+
testName: 'documented type parameter',
121+
code: `
122+
enum MyEnum {
123+
/**
124+
* @typeParam T
125+
* ${testDocumentation}
126+
*/
127+
MyEnumVariant<T>
128+
}
129+
`,
130+
predicate: isSdsTypeParameter,
131+
expectedDocumentation: testDocumentation,
132+
},
133+
{
134+
testName: 'documented type parameter (duplicate)',
135+
code: `
136+
enum MyEnum {
137+
/**
138+
* @typeParam T ${testDocumentation}
139+
* @typeParam T bla
140+
*/
141+
MyEnumVariant<T>
142+
}
143+
`,
144+
predicate: isSdsTypeParameter,
145+
expectedDocumentation: testDocumentation,
146+
},
147+
{
148+
testName: 'undocumented type parameter',
149+
code: `
150+
enum MyEnum {
151+
/**
152+
* @typeParam T
153+
* ${testDocumentation}
154+
*/
155+
MyEnumVariant<T2>
156+
}
157+
`,
158+
predicate: isSdsTypeParameter,
159+
expectedDocumentation: undefined,
160+
},
161+
{
162+
testName: 'type parameter (no documentation on containing callable)',
163+
code: `
164+
fun myFunction<T>()
165+
`,
166+
predicate: isSdsTypeParameter,
167+
expectedDocumentation: undefined,
168+
},
169+
];
170+
171+
it.each(testCases)('$testName', async ({ code, predicate, expectedDocumentation }) => {
172+
const node = await getNodeOfType(services, code, predicate);
173+
expect(documentationProvider.getDocumentation(node)).toStrictEqual(expectedDocumentation);
174+
});
175+
176+
it('should resolve links', async () => {
177+
const code = `
178+
/**
179+
* {@link myFunction2}
180+
*/
181+
fun myFunction1()
182+
183+
fun myFunction2()
184+
`;
185+
const node = await getNodeOfType(services, code, isSdsFunction);
186+
expect(documentationProvider.getDocumentation(node)).toMatch(/\[myFunction2\]\(.*\)/u);
187+
});
188+
});
189+
190+
/**
191+
* A description of a test case for the documentation provider.
192+
*/
193+
interface DocumentationProviderTest {
194+
/**
195+
* A short description of the test case.
196+
*/
197+
testName: string;
198+
199+
/**
200+
* The code to test.
201+
*/
202+
code: string;
203+
204+
/**
205+
* A predicate to find the node to test.
206+
*/
207+
predicate: (node: unknown) => node is AstNode;
208+
209+
/**
210+
* The expected documentation.
211+
*/
212+
expectedDocumentation: string | undefined;
213+
}

0 commit comments

Comments
 (0)