Skip to content

Commit 9bad1c1

Browse files
authored
[Plugin] Vignette, Test-Info and general meta-file-plugin support (#2142)
* refactor: refine project discover configurability * feat: meta loader and files reset fixes + improved project output * feat: one file may have more than one role! * feat: vignette plugin, allow file plugins to continue * feat: test info plugin (closes #2106) * refactor: clean up
1 parent 293d680 commit 9bad1c1

21 files changed

+296
-85
lines changed

src/project/context/flowr-analyzer-files-context.ts

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,16 @@ export type RoleBasedFiles = {
4141
[FileRole.News]: FlowrNewsFile[];
4242
[FileRole.Namespace]: FlowrNamespaceFile[];
4343
/* currently no special support */
44-
[FileRole.Source]: FlowrFileProvider[];
45-
[FileRole.Data]: FlowrFileProvider[];
46-
[FileRole.Other]: FlowrFileProvider[];
44+
[FileRole.Vignette]: FlowrFileProvider[];
45+
[FileRole.Test]: FlowrFileProvider[];
46+
[FileRole.Source]: FlowrFileProvider[];
47+
[FileRole.Data]: FlowrFileProvider[];
48+
[FileRole.Other]: FlowrFileProvider[];
4749
}
4850

49-
function wrapFile(file: string | FlowrFileProvider | RParseRequestFromFile, role?: FileRole): FlowrFileProvider {
51+
function wrapFile(file: string | FlowrFileProvider | RParseRequestFromFile, roles?: readonly FileRole[]): FlowrFileProvider {
5052
if(typeof file === 'string') {
51-
return new FlowrTextFile(file, role);
53+
return new FlowrTextFile(file, roles);
5254
} else if('request' in file) {
5355
return FlowrFile.fromRequest(file);
5456
} else {
@@ -130,14 +132,7 @@ export class FlowrAnalyzerFilesContext extends AbstractFlowrAnalyzerContext<RPro
130132
private readonly consideredFiles: string[] = [];
131133

132134
/* files that are part of the analysis, e.g. source files */
133-
private byRole: RoleBasedFiles = {
134-
[FileRole.Description]: [],
135-
[FileRole.News]: [],
136-
[FileRole.Namespace]: [],
137-
[FileRole.Source]: [],
138-
[FileRole.Data]: [],
139-
[FileRole.Other]: []
140-
} satisfies Record<FileRole, FlowrFileProvider[]>;
135+
private byRole: RoleBasedFiles = Object.fromEntries<FlowrFileProvider[]>(Object.values(FileRole).map(k => [k, []])) as RoleBasedFiles;
141136

142137
constructor(
143138
loadingOrder: FlowrAnalyzerLoadingOrderContext,
@@ -152,6 +147,9 @@ export class FlowrAnalyzerFilesContext extends AbstractFlowrAnalyzerContext<RPro
152147
public reset(): void {
153148
this.loadingOrder.reset();
154149
this.files = new Map<FilePath, FlowrFileProvider>();
150+
this.consideredFiles.length = 0;
151+
this.inlineFiles.length = 0;
152+
this.byRole = Object.fromEntries<FlowrFileProvider[]>(Object.values(FileRole).map(k => [k, []])) as RoleBasedFiles;
155153
}
156154

157155
/**
@@ -191,7 +189,7 @@ export class FlowrAnalyzerFilesContext extends AbstractFlowrAnalyzerContext<RPro
191189
if(isParseRequest(req)) {
192190
this.addRequest(req);
193191
} else {
194-
this.addFile(req, req.role);
192+
this.addFile(req, req.roles);
195193
}
196194
}
197195
}
@@ -209,8 +207,8 @@ export class FlowrAnalyzerFilesContext extends AbstractFlowrAnalyzerContext<RPro
209207
* Add a file to the context. If the file has a special role, it will be added to the corresponding list of special files.
210208
* This method also applies any registered {@link FlowrAnalyzerFilePlugin}s to the file before adding it to the context.
211209
*/
212-
public addFile(file: string | FlowrFileProvider | RParseRequestFromFile, role?: FileRole) {
213-
const f = this.fileLoadPlugins(wrapFile(file, role));
210+
public addFile(file: string | FlowrFileProvider | RParseRequestFromFile, roles?: readonly FileRole[]) {
211+
const f = this.fileLoadPlugins(wrapFile(file, roles));
214212

215213
if(f.path() === FlowrFile.INLINE_PATH) {
216214
this.inlineFiles.push(f);
@@ -220,8 +218,10 @@ export class FlowrAnalyzerFilesContext extends AbstractFlowrAnalyzerContext<RPro
220218
this.files.set(f.path(), f);
221219
}
222220

223-
if(f.role) {
224-
this.byRole[f.role].push(f as never);
221+
if(f.roles) {
222+
for(const r of f.roles) {
223+
this.byRole[r].push(f as never);
224+
}
225225
}
226226

227227
return f;
@@ -266,8 +266,16 @@ export class FlowrAnalyzerFilesContext extends AbstractFlowrAnalyzerContext<RPro
266266
for(const loader of this.fileLoaders) {
267267
if(loader.applies(f.path())) {
268268
fileLog.debug(`Applying file loader ${loader.name} to file ${f.path()}`);
269-
fFinal = loader.processor(this.ctx, f);
270-
break;
269+
const res = loader.processor(this.ctx, fFinal);
270+
if(Array.isArray(res)) {
271+
fFinal = res[0];
272+
if(!res[1]) {
273+
break;
274+
}
275+
} else {
276+
fFinal = res;
277+
break;
278+
}
271279
}
272280
}
273281
return fFinal;

src/project/context/flowr-file.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { PathLike } from 'fs';
22
import fs from 'fs';
3-
import { guard } from '../../util/assert';
43
import type { RParseRequest } from '../../r-bridge/retriever';
54

65
/**
@@ -20,7 +19,11 @@ export enum FileRole {
2019
/** The `NAMESPACE` file in R packages, currently not specially supported. */
2120
Namespace = 'namespace',
2221
/** The `NEWS` file in R packages */
23-
News = 'news',
22+
News = 'news',
23+
/** Vignette files, e.g., R Markdown files in the `vignettes/` folder */
24+
Vignette = 'vignette',
25+
/** Test source files, e.g., files in the `tests/` folder */
26+
Test = 'test',
2427
/** Data files, e.g., `R/sysdata.rda`, currently not specially supported. */
2528
Data = 'data',
2629
/**
@@ -49,11 +52,11 @@ export type StringableContent = { toString(): string };
4952
*/
5053
export interface FlowrFileProvider<Content extends { toString(): string } = { toString(): string }> {
5154
/**
52-
* The role of this file, if any, in general your file should _not_ decide for itself what role it has in the project context,
55+
* The role(s) of this file, if any, in general your file should _not_ decide for itself what role it has in the project context,
5356
* this is for the loaders plugins to decide (cf. {@link PluginType}) as they can, e.g., respect ignore files, updated mappings, etc.
5457
* However, they will 1) set this role as soon as they decide on it (using {@link assignRole}) and 2) try to respect an already assigned role (however, user configurations may override this).
5558
*/
56-
role?: FileRole;
59+
roles?: readonly FileRole[];
5760

5861
/**
5962
* The path to the file, this is used for identification and logging purposes.
@@ -82,14 +85,18 @@ export interface FlowrFileProvider<Content extends { toString(): string } = { to
8285
* See {@link FlowrTextFile} for a text-file specific implementation and {@link FlowrInlineTextFile} for inline text files.
8386
*/
8487
export abstract class FlowrFile<Content extends StringableContent = StringableContent> implements FlowrFileProvider<Content> {
85-
private contentCache: Content | undefined;
86-
protected filePath: PathLike;
87-
public readonly role?: FileRole;
88+
private contentCache: Content | undefined;
89+
protected filePath: PathLike;
90+
private _roles?: FileRole[];
8891
public static readonly INLINE_PATH = '@inline';
8992

90-
public constructor(filePath: PathLike, role?: FileRole) {
93+
public constructor(filePath: PathLike, roles?: readonly FileRole[]) {
9194
this.filePath = filePath;
92-
this.role = role;
95+
this._roles = roles ? Array.from(roles) : undefined;
96+
}
97+
98+
public get roles(): readonly FileRole[] | undefined {
99+
return this._roles;
93100
}
94101

95102
public path(): string {
@@ -106,8 +113,14 @@ export abstract class FlowrFile<Content extends StringableContent = StringableCo
106113
protected abstract loadContent(): Content;
107114

108115
public assignRole(role: FileRole): void {
109-
guard(this.role === undefined || this.role === role, `File ${this.filePath.toString()} already has a role assigned: ${this.role}`);
110-
(this as { role?: FileRole }).role = role;
116+
if(this._roles === undefined) {
117+
this._roles = [role];
118+
} else if(this._roles.includes(role)) {
119+
// already assigned
120+
return;
121+
} else {
122+
this._roles.push(role);
123+
}
111124
}
112125

113126
/**

src/project/plugins/file-plugins/files/flowr-description-file.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class FlowrDescriptionFile extends FlowrFile<DeepReadonly<DCF>> {
2020
* and handle role assignments.
2121
*/
2222
constructor(file: FlowrFileProvider) {
23-
super(file.path(), file.role);
23+
super(file.path(), file.roles);
2424
this.wrapped = file;
2525
}
2626

src/project/plugins/file-plugins/files/flowr-jupyter-file.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class FlowrJupyterFile extends FlowrFile<string> {
1515
* @param file - the file to load as R Markdown
1616
*/
1717
constructor(file: FlowrFileProvider<string>) {
18-
super(file.path(), FileRole.Source);
18+
super(file.path(), file.roles ? [...file.roles, FileRole.Source] : [FileRole.Source]);
1919
this.wrapped = file;
2020
}
2121

src/project/plugins/file-plugins/files/flowr-namespace-file.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class FlowrNamespaceFile extends FlowrFile<NamespaceFormat> {
2424
* and handle role assignments.
2525
*/
2626
constructor(file: FlowrFileProvider) {
27-
super(file.path(), file.role);
27+
super(file.path(), file.roles);
2828
this.wrapped = file;
2929
}
3030

src/project/plugins/file-plugins/files/flowr-news-file.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class FlowrNewsFile extends FlowrFile<NewsChunk[]> {
2020
* and handle role assignments.
2121
*/
2222
constructor(file: FlowrFileProvider) {
23-
super(file.path(), file.role);
23+
super(file.path(), file.roles);
2424
this.wrapped = file;
2525
}
2626

src/project/plugins/file-plugins/files/flowr-rmarkdown-file.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class FlowrRMarkdownFile extends FlowrFile<string> {
1717
* @param file - the file to load as R Markdown
1818
*/
1919
constructor(file: FlowrFileProvider<string>) {
20-
super(file.path(), FileRole.Source);
20+
super(file.path(), file.roles ? [...file.roles, FileRole.Source] : [FileRole.Source]);
2121
this.wrapped = file;
2222
}
2323

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ import type { FlowrAnalyzerContext } from '../../context/flowr-analyzer-context'
1212
*
1313
* It is up to the construction to ensure that no two file plugins {@link applies} to the same file, otherwise, the loading order
1414
* of these plugins will determine which plugin gets to process the file.
15+
* On transforming a file, your plugin can indicate whether other plugins should still get to process the file,
16+
* by returning a tuple of `[transformedFile, <boolean>]` where a boolean `true` indicates that other plugins should still get to process the file.
17+
* One example of a plugin doing this is the {@link FlowrAnalyzerMetaVignetteFilesPlugin}.
1518
*
1619
* See {@link DefaultFlowrAnalyzerFilePlugin} for the no-op default implementation.
1720
*/
18-
export abstract class FlowrAnalyzerFilePlugin extends FlowrAnalyzerPlugin<FlowrFileProvider, FlowrFileProvider> {
21+
export abstract class FlowrAnalyzerFilePlugin extends FlowrAnalyzerPlugin<FlowrFileProvider, FlowrFileProvider | [file: FlowrFileProvider, cont: boolean]> {
1922
public readonly type = PluginType.FileLoad;
2023

2124
/**

src/project/plugins/file-plugins/flowr-analyzer-namespace-file-plugin.ts renamed to src/project/plugins/file-plugins/flowr-analyzer-namespace-files-plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const NamespaceFilePattern = /^NAMESPACE(\.txt)?$/i;
1212
/**
1313
* This plugin provides support for R `NAMESPACE` files.
1414
*/
15-
export class FlowrAnalyzerNamespaceFilePlugin extends FlowrAnalyzerFilePlugin {
15+
export class FlowrAnalyzerNamespaceFilesPlugin extends FlowrAnalyzerFilePlugin {
1616
public readonly name = 'flowr-analyzer-namespace-file-plugin';
1717
public readonly description = 'This plugin provides support for NAMESPACE files and extracts their content into the NAMESPACEFormat.';
1818
public readonly version = new SemVer('0.1.0');
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { FlowrAnalyzerFilePlugin } from './flowr-analyzer-file-plugin';
2+
import { SemVer } from 'semver';
3+
import type { PathLike } from 'fs';
4+
import type { FlowrAnalyzerContext } from '../../context/flowr-analyzer-context';
5+
import type { FlowrFileProvider } from '../../context/flowr-file';
6+
import { FileRole } from '../../context/flowr-file';
7+
import {
8+
platformDirname
9+
} from '../../../dataflow/internal/process/functions/call/built-in/built-in-source';
10+
11+
const TestPathPattern = /tests?/i;
12+
13+
/**
14+
* This plugin provides supports for the identification of test files.
15+
* If you use multiple plugins, this should be included *before* other plugins.
16+
*/
17+
export class FlowrAnalyzerMetaTestFilesPlugin extends FlowrAnalyzerFilePlugin {
18+
public readonly name = 'flowr-analyzer-test-files-plugin';
19+
public readonly description = 'This plugin provides support for loading test files.';
20+
public readonly version = new SemVer('0.1.0');
21+
private readonly pathPattern: RegExp;
22+
23+
/**
24+
* Creates a new instance of the TEST file plugin.
25+
* @param pathPattern - The pathPattern to identify TEST files, see {@link TestPathPattern} for the default pathPattern.
26+
*/
27+
constructor(pathPattern: RegExp = TestPathPattern) {
28+
super();
29+
this.pathPattern = pathPattern;
30+
}
31+
32+
public applies(file: PathLike): boolean {
33+
return this.pathPattern.test(platformDirname(file.toString()));
34+
}
35+
36+
/**
37+
* Processes the given file, assigning it the {@link FileRole.Test} role.
38+
* Given that the file may still need to be processed by other plugins, this method returns the `true` flag for that purpose.
39+
*/
40+
public process(_ctx: FlowrAnalyzerContext, file: FlowrFileProvider): [FlowrFileProvider, true] {
41+
file.assignRole(FileRole.Test);
42+
return [file, true];
43+
}
44+
}

0 commit comments

Comments
 (0)