Skip to content

Commit fb063b8

Browse files
authored
[Roxygen2] Doc Support with Params (#2185)
* feat: support `cli_abort`, `throw`, `stop_*` * refactor: support more output functions * refactor: support more write functions * feat: provide support for requesting comments of params * feat(roxygen): param and expand retrieval
1 parent 1fc0f86 commit fb063b8

File tree

4 files changed

+185
-0
lines changed

4 files changed

+185
-0
lines changed

src/project/plugins/project-discovery/flowr-analyzer-project-discovery-plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class DefaultFlowrAnalyzerProjectDiscoveryPlugin extends FlowrAnalyzerProjectDis
7070
const requests: (RParseRequest | FlowrFile<string>)[] = [];
7171
/* the dummy approach of collecting all files, group R and Rmd files, and be done with it */
7272
for(const file of getAllFilesSync(args.content, /.*/, this.ignorePathsRegex)) {
73+
console.log(`Discovered file: ${file}`);
7374
const relativePath = path.relative(args.content, file);
7475
if(this.supportedExtensions.test(relativePath) && (!this.onlyTraversePaths || this.onlyTraversePaths.test(relativePath)) && !this.excludePathsRegex.test(platformDirname(relativePath))) {
7576
requests.push({ content: file, request: 'file' });
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import type { AstIdMap, ParentInformation } from '../lang-4.x/ast/model/processing/decorate';
2+
import type { NodeId } from '../lang-4.x/ast/model/processing/node-id';
3+
import type { RoxygenTag, RoxygenTagParam } from './roxygen-ast';
4+
import { KnownRoxygenTags } from './roxygen-ast';
5+
import { RType } from '../lang-4.x/ast/model/type';
6+
import type { RNode } from '../lang-4.x/ast/model/model';
7+
import { parseRoxygenComment, parseRoxygenCommentsOfNode } from './roxygen-parse';
8+
9+
export interface DocumentationInfo {
10+
doc?: Documentation;
11+
}
12+
export type Documentation = RoxygenTag | readonly RoxygenTag[];
13+
14+
type CommentRetriever<Node extends RType> = (node: Extract<RNode<ParentInformation>, { type: Node }>, idMap: AstIdMap<ParentInformation & DocumentationInfo>) => Documentation | undefined;
15+
type CommentRetrievers = { [Node in RType]?: CommentRetriever<Node> };
16+
const CommentRetriever: CommentRetrievers = {
17+
[RType.Comment]: n => parseRoxygenComment([n.lexeme]),
18+
[RType.Parameter]: (n, idMap) => {
19+
// get the documentation of the parent function
20+
const doc = n.info.parent ? getDocumentationOf(n.info.parent, idMap) : undefined;
21+
const paramName = n.lexeme;
22+
if(doc && paramName) {
23+
if(Array.isArray(doc)) {
24+
const res = (doc as RoxygenTag[]).filter(t => t.type === KnownRoxygenTags.Param && t.value.name === paramName);
25+
if(res.length === 1) {
26+
return res[0];
27+
} else {
28+
return res;
29+
}
30+
} else {
31+
if((doc as RoxygenTag).type === KnownRoxygenTags.Param && (doc as RoxygenTagParam).value.name === paramName) {
32+
return doc;
33+
}
34+
}
35+
}
36+
return undefined;
37+
}
38+
};
39+
40+
41+
/**
42+
* Given a normalized AST and a node ID, returns the Roxygen documentation (if any) associated with that node.
43+
* Please note that this does more than {@link parseRoxygenCommentsOfNode}, as it also traverses up the AST to find documentation.
44+
* Additionally, this function instruments the normalized AST to cache the parsed documentation for future queries.
45+
* @param idMap - The AST ID map to use for looking up nodes and traversing the AST.
46+
* @param nodeId - The ID of the node to get documentation for.
47+
*/
48+
export function getDocumentationOf(nodeId: NodeId, idMap: AstIdMap<ParentInformation & DocumentationInfo>): Documentation | undefined {
49+
const node = idMap.get(nodeId);
50+
if(!node) {
51+
return undefined;
52+
} else if(node.info.doc) {
53+
return node.info.doc;
54+
}
55+
const retriever = CommentRetriever[node.type as RType] ?? ((c: RNode<ParentInformation>, a: AstIdMap) => parseRoxygenCommentsOfNode(c, a)?.tags);
56+
const doc = retriever(node as never, idMap);
57+
if(doc) {
58+
// cache the documentation for future queries
59+
const expanded = expandInheritsOfTags(doc, idMap);
60+
(node.info as DocumentationInfo).doc = expanded;
61+
return expanded;
62+
}
63+
return doc;
64+
}
65+
66+
function expandInheritsOfTags(tags: RoxygenTag | readonly RoxygenTag[], idMap: AstIdMap<ParentInformation & DocumentationInfo>): RoxygenTag | readonly RoxygenTag[] {
67+
const expandedTags: RoxygenTag[] = [];
68+
const tagArray: readonly RoxygenTag[] = Array.isArray(tags) ? tags : [tags];
69+
for(const tag of tagArray) {
70+
const expanded: RoxygenTag | readonly RoxygenTag[] | undefined = expandInheritOfTag(tag, tagArray, idMap);
71+
if(!expanded) {
72+
continue;
73+
}
74+
if(Array.isArray(expanded)) {
75+
expandedTags.push(...expanded as readonly RoxygenTag[]);
76+
} else {
77+
expandedTags.push(expanded as RoxygenTag);
78+
}
79+
}
80+
if(expandedTags.length === 1) {
81+
return expandedTags[0];
82+
}
83+
return expandedTags;
84+
}
85+
86+
function getDocumentationOfByName(name: string, idMap: AstIdMap<ParentInformation & DocumentationInfo>): Documentation | undefined {
87+
for(const [, node] of idMap) {
88+
const nodeName = node.lexeme ?? node.info.fullLexeme;
89+
if(nodeName !== name) {
90+
continue;
91+
}
92+
return getDocumentationOf(node.info.id, idMap);
93+
}
94+
}
95+
96+
function filterDocumentationForParams(doc: Documentation | undefined, filter: (r: RoxygenTag) => boolean): Documentation | undefined {
97+
if(!doc) {
98+
return doc;
99+
}
100+
if(Array.isArray(doc)) {
101+
return doc.filter(filter) as readonly RoxygenTag[];
102+
} else {
103+
return filter(doc as RoxygenTag) ? doc : undefined;
104+
}
105+
106+
}
107+
108+
function expandInheritOfTag(tag: RoxygenTag, otherTags: readonly RoxygenTag[], idMap: AstIdMap<ParentInformation & DocumentationInfo>): Documentation | undefined {
109+
if(tag.type === KnownRoxygenTags.Inherit) {
110+
const inheritDoc = getDocumentationOfByName(tag.value.source, idMap);
111+
return filterDocumentationForParams(inheritDoc, t => tag.value.components.includes(t.type));
112+
} else if(tag.type === KnownRoxygenTags.InheritDotParams) {
113+
const inheritDoc = getDocumentationOfByName(tag.value.source, idMap);
114+
return filterDocumentationForParams(inheritDoc, t => t.type === KnownRoxygenTags.Param && t.value.name === '...');
115+
} else if(tag.type === KnownRoxygenTags.InheritParams) {
116+
const inheritDoc = getDocumentationOfByName(tag.value, idMap);
117+
const alreadyExplainedParams = new Set(otherTags.filter(t => t.type === KnownRoxygenTags.Param).map(t => t.value.name));
118+
return filterDocumentationForParams(inheritDoc, t => t.type === KnownRoxygenTags.Param && !alreadyExplainedParams.has(t.value.name));
119+
}
120+
return tag;
121+
}

src/r-bridge/roxygen2/roxygen-parse.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ function prepareCommentContext(commentText: readonly string[]): string[] {
2626
/**
2727
* Parses the roxygen comments attached to a node into a RoxygenBlock AST node.
2828
* Will return `undefined` if there are no valid roxygen comments attached to the node.
29+
* Please note that this does *not* do any clever mapping of parameters or requests.
30+
* For a higher-level function that also traverses up the AST to find comments attached to parent nodes, see {@link getDocumentationOf}.
2931
* @param node - The node to parse the roxygen comments for
3032
* @param idMap - An optional id map to traverse up the AST to find comments attached to parent nodes
3133
*/
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { assert, describe, test } from 'vitest';
2+
import type { SingleSlicingCriterion } from '../../../../src/slicing/criterion/parse';
3+
import { slicingCriterionToId } from '../../../../src/slicing/criterion/parse';
4+
import type { Documentation } from '../../../../src/r-bridge/roxygen2/documentation-provider';
5+
import { getDocumentationOf } from '../../../../src/r-bridge/roxygen2/documentation-provider';
6+
import { FlowrAnalyzerBuilder } from '../../../../src/project/flowr-analyzer-builder';
7+
import { withTreeSitter } from '../../_helper/shell';
8+
import { requestFromInput } from '../../../../src/r-bridge/retriever';
9+
import { KnownRoxygenTags } from '../../../../src/r-bridge/roxygen2/roxygen-ast';
10+
11+
12+
describe('Provide Comments', withTreeSitter(ts => {
13+
function check(code: string, requests: Record<SingleSlicingCriterion, Documentation>): void {
14+
test.each(Object.entries(requests))('Provide docs for criterion $0', async(request, expect) => {
15+
const analyzer = await new FlowrAnalyzerBuilder().setParser(ts).build();
16+
analyzer.addRequest(requestFromInput(code));
17+
const normalize = await analyzer.normalize();
18+
const criterion = slicingCriterionToId(request as SingleSlicingCriterion, normalize.idMap);
19+
const docs = getDocumentationOf(criterion, normalize.idMap);
20+
assert.deepStrictEqual(docs, expect);
21+
});
22+
}
23+
24+
check(`
25+
#' This is an important function description.
26+
#'
27+
#' @param arg1 Description for argument one.
28+
#' @param arg2 Description for argument two.
29+
#' @param special.arg A special argument
30+
f <- function(arg1 = NULL, arg2 = "value", special.arg = FALSE) {
31+
return(TRUE)
32+
}
33+
34+
#' Another function
35+
#' @inheritParams f
36+
#' @param x Some x value
37+
#' @param arg1 Description for argument one.
38+
g <- function(x, arg1, arg2) { }
39+
`, {
40+
'7@f': [
41+
{ type: KnownRoxygenTags.Text, value: 'This is an important function description.' },
42+
{ type: KnownRoxygenTags.Param, value: { name: 'arg1', description: 'Description for argument one.' } },
43+
{ type: KnownRoxygenTags.Param, value: { name: 'arg2', description: 'Description for argument two.' } },
44+
{ type: KnownRoxygenTags.Param, value: { name: 'special.arg', description: 'A special argument' } }
45+
],
46+
'$3': { type: KnownRoxygenTags.Param, value: { name: 'arg1', description: 'Description for argument one.' } },
47+
'$6': { type: KnownRoxygenTags.Param, value: { name: 'arg2', description: 'Description for argument two.' } },
48+
'$9': { type: KnownRoxygenTags.Param, value: { name: 'special.arg', description: 'A special argument' } },
49+
'15@g': [
50+
{ type: KnownRoxygenTags.Text, value: 'Another function' },
51+
// is expanded!
52+
{ type: KnownRoxygenTags.Param, value: { name: 'arg2', description: 'Description for argument two.' } },
53+
{ type: KnownRoxygenTags.Param, value: { name: 'special.arg', description: 'A special argument' } },
54+
{ type: KnownRoxygenTags.Param, value: { name: 'x', description: 'Some x value' } },
55+
{ type: KnownRoxygenTags.Param, value: { name: 'arg1', description: 'Description for argument one.' } }
56+
],
57+
'$21': { type: KnownRoxygenTags.Param, value: { name: 'x', description: 'Some x value' } },
58+
'$23': { type: KnownRoxygenTags.Param, value: { name: 'arg1', description: 'Description for argument one.' } },
59+
'$25': { type: KnownRoxygenTags.Param, value: { name: 'arg2', description: 'Description for argument two.' } } // expanded
60+
});
61+
}));

0 commit comments

Comments
 (0)