-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
e4a1b35
commit ff70b07
Showing
4 changed files
with
306 additions
and
6 deletions.
There are no files selected for viewing
88 changes: 88 additions & 0 deletions
88
src/language/documentation/safe-ds-documentation-provider.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
213 changes: 213 additions & 0 deletions
213
tests/language/documentation/safe-ds-documentation-provider.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |