diff --git a/src/project/context/flowr-analyzer-context.ts b/src/project/context/flowr-analyzer-context.ts index 9506b7a2024..2496df52616 100644 --- a/src/project/context/flowr-analyzer-context.ts +++ b/src/project/context/flowr-analyzer-context.ts @@ -19,6 +19,8 @@ import type { FlowrAnalyzerProjectDiscoveryPlugin } from '../plugins/project-discovery/flowr-analyzer-project-discovery-plugin'; import type { FlowrAnalyzerFilePlugin } from '../plugins/file-plugins/flowr-analyzer-file-plugin'; +import type { ReadOnlyFlowrAnalyzerFunctionsContext } from './flowr-analyzer-functions-context'; +import { FlowrAnalyzerFunctionsContext } from './flowr-analyzer-functions-context'; import { arraysGroupBy } from '../../util/collections/arrays'; import type { fileProtocol, RParseRequestFromFile, RParseRequests } from '../../r-bridge/retriever'; import { requestFromInput } from '../../r-bridge/retriever'; @@ -45,6 +47,8 @@ export interface ReadOnlyFlowrAnalyzerContext { * The configuration options used by the analyzer. */ readonly config: FlowrConfigOptions; + + readonly functions: ReadOnlyFlowrAnalyzerFunctionsContext; } /** @@ -60,9 +64,11 @@ export interface ReadOnlyFlowrAnalyzerContext { * If you are just interested in inspecting the context, you can use {@link ReadOnlyFlowrAnalyzerContext} instead (e.g., via {@link inspect}). */ export class FlowrAnalyzerContext implements ReadOnlyFlowrAnalyzerContext { - public readonly files: FlowrAnalyzerFilesContext; - public readonly deps: FlowrAnalyzerDependenciesContext; - public readonly config: FlowrConfigOptions; + public readonly files: FlowrAnalyzerFilesContext; + public readonly deps: FlowrAnalyzerDependenciesContext; + public readonly config: FlowrConfigOptions; + public readonly functions: FlowrAnalyzerFunctionsContext; + constructor(config: FlowrConfigOptions, plugins: ReadonlyMap) { this.config = config; @@ -70,6 +76,7 @@ export class FlowrAnalyzerContext implements ReadOnlyFlowrAnalyzerContext { this.files = new FlowrAnalyzerFilesContext(loadingOrder, (plugins.get(PluginType.ProjectDiscovery) ?? []) as FlowrAnalyzerProjectDiscoveryPlugin[], (plugins.get(PluginType.FileLoad) ?? []) as FlowrAnalyzerFilePlugin[]); this.deps = new FlowrAnalyzerDependenciesContext(this, (plugins.get(PluginType.DependencyIdentification) ?? []) as FlowrAnalyzerPackageVersionsPlugin[]); + this.functions = new FlowrAnalyzerFunctionsContext(this, (plugins.get(PluginType.DependencyIdentification) ?? []) as FlowrAnalyzerPackageVersionsPlugin[]); } /** delegate request addition */ diff --git a/src/project/context/flowr-analyzer-functions-context.ts b/src/project/context/flowr-analyzer-functions-context.ts new file mode 100644 index 00000000000..20675fde30d --- /dev/null +++ b/src/project/context/flowr-analyzer-functions-context.ts @@ -0,0 +1,64 @@ +import { AbstractFlowrAnalyzerContext } from './abstract-flowr-analyzer-context'; +import type { FlowrAnalyzerContext } from './flowr-analyzer-context'; +import { + FlowrAnalyzerPackageVersionsPlugin +} from '../plugins/package-version-plugins/flowr-analyzer-package-versions-plugin'; + +export enum FunctionTypes { + Function = 'function', + Symbol = 'symbol', + S3 = 'S3' +} + +export interface FunctionInfo { + name: string; + packageOrigin: string; + isExported: boolean; + isS3Generic: boolean; + className?: string; + inferredType?: string; +} + +export interface ReadOnlyFlowrAnalyzerFunctionsContext { + readonly name: string; + + getFunctionInfo(name: string, className?: string): FunctionInfo | undefined; +} + +export class FlowrAnalyzerFunctionsContext extends AbstractFlowrAnalyzerContext implements ReadOnlyFlowrAnalyzerFunctionsContext { + public readonly name = 'flowr-analyzer-functions-context'; + + private functionInfo: Map = new Map(); + + public constructor(ctx: FlowrAnalyzerContext, plugins?: readonly FlowrAnalyzerPackageVersionsPlugin[]) { + super(ctx, FlowrAnalyzerPackageVersionsPlugin.defaultPlugin(), plugins); + } + + public addFunctionInfo(functionInfo: FunctionInfo) { + const fi = this.functionInfo.get(functionInfo.name); + if(fi) { + // merge? + } else { + this.functionInfo.set(functionInfo.name, functionInfo); + } + } + + public getFunctionInfo(name: string, className?: string): FunctionInfo | undefined { + if(className) { + return this.functionInfo.get(`${name}.${className}`); + } else if(name.includes('.')){ + const parts = name.split('.'); + const splitClassName = parts.pop(); + const splitName = parts.join('.'); + if(this.functionInfo.has(splitName)) { + const value = this.functionInfo.get(splitName); + return value?.className === splitClassName ? value : undefined; + } + } + return this.functionInfo.get(name); + } + + public reset(): void { + this.functionInfo = new Map(); + } +} \ No newline at end of file diff --git a/src/project/plugins/file-plugins/flowr-analyzer-namespace-file-plugin.ts b/src/project/plugins/file-plugins/flowr-analyzer-namespace-file-plugin.ts new file mode 100644 index 00000000000..a262d9fe57b --- /dev/null +++ b/src/project/plugins/file-plugins/flowr-analyzer-namespace-file-plugin.ts @@ -0,0 +1,37 @@ +import { FlowrAnalyzerFilePlugin } from './flowr-analyzer-file-plugin'; +import { SemVer } from 'semver'; +import type { PathLike } from 'fs'; +import type { FlowrAnalyzerContext } from '../../context/flowr-analyzer-context'; +import type { FlowrFileProvider } from '../../context/flowr-file'; +import { FileRole } from '../../context/flowr-file'; +import { FlowrNamespaceFile } from './flowr-namespace-file'; +import { platformBasename } from '../../../dataflow/internal/process/functions/call/built-in/built-in-source'; + +const NamespaceFilePattern = /^(NAMESPACE(\.txt)?)$/i; + +/** + * This plugin provides support for R `NAMESPACE` files. + */ +export class FlowrAnalyzerNamespaceFilePlugin extends FlowrAnalyzerFilePlugin { + public readonly name = 'flowr-analyzer-namespace-file-plugin'; + public readonly description = 'This plugin provides support for NAMESPACE files and extracts their content into the NAMESPACEFormat.'; + public readonly version = new SemVer('0.1.0'); + private readonly pattern: RegExp; + + /** + * Creates a new instance of the NAMESPACE file plugin. + * @param filePattern - The pattern to identify NAMESPACE files, see {@link NamespaceFilePattern} for the default pattern. + */ + constructor(filePattern: RegExp = NamespaceFilePattern) { + super(); + this.pattern = filePattern; + } + + public applies(file: PathLike): boolean { + return this.pattern.test(platformBasename(file.toString())); + } + + public process(_ctx: FlowrAnalyzerContext, file: FlowrFileProvider): FlowrNamespaceFile { + return FlowrNamespaceFile.from(file, FileRole.Namespace); + } +} \ No newline at end of file diff --git a/src/project/plugins/file-plugins/flowr-namespace-file.ts b/src/project/plugins/file-plugins/flowr-namespace-file.ts new file mode 100644 index 00000000000..f4a83452799 --- /dev/null +++ b/src/project/plugins/file-plugins/flowr-namespace-file.ts @@ -0,0 +1,48 @@ +import type { FileRole, FlowrFileProvider } from '../../context/flowr-file'; +import { FlowrFile } from '../../context/flowr-file'; +import { parseNamespace } from '../../../util/files'; + +export interface NamespaceInfo { + exportedSymbols: string[]; + exportedFunctions: string[]; + exportS3Generics: Map; + loadsWithSideEffects: boolean; +} + +export interface NamespaceFormat { + current: NamespaceInfo; + [packageName: string]: NamespaceInfo; +} + +/** + * + */ +export class FlowrNamespaceFile extends FlowrFile { + private readonly wrapped: FlowrFileProvider; + + /** + * + */ + constructor(file: FlowrFileProvider) { + super(file.path(), file.role); + this.wrapped = file; + } + + /** + * @see {@link parseNamespace} for details on the parsing logic. + */ + protected loadContent(): NamespaceFormat { + return parseNamespace(this.wrapped); + } + + + /** + * Namespace file lifter, this does not re-create if already a namespace file + */ + public static from(file: FlowrFileProvider | FlowrNamespaceFile, role?: FileRole): FlowrNamespaceFile { + if(role) { + file.assignRole(role); + } + return file instanceof FlowrNamespaceFile ? file : new FlowrNamespaceFile(file); + } +} \ No newline at end of file diff --git a/src/project/plugins/flowr-analyzer-plugin-defaults.ts b/src/project/plugins/flowr-analyzer-plugin-defaults.ts index 3214ad78453..589320c63e1 100644 --- a/src/project/plugins/flowr-analyzer-plugin-defaults.ts +++ b/src/project/plugins/flowr-analyzer-plugin-defaults.ts @@ -9,6 +9,7 @@ import { import { FlowrAnalyzerRmdFilePlugin } from './file-plugins/notebooks/flowr-analyzer-rmd-file-plugin'; import { FlowrAnalyzerQmdFilePlugin } from './file-plugins/notebooks/flowr-analyzer-qmd-file-plugin'; import { FlowrAnalyzerJupyterFilePlugin } from './file-plugins/notebooks/flowr-analyzer-jupyter-file-plugin'; +import { FlowrAnalyzerNamespaceFilePlugin } from './file-plugins/flowr-analyzer-namespace-file-plugin'; /** * Provides the default set of Flowr Analyzer plugins. @@ -21,5 +22,6 @@ export function FlowrAnalyzerPluginDefaults(): FlowrAnalyzerPlugin[] { new FlowrAnalyzerRmdFilePlugin(), new FlowrAnalyzerQmdFilePlugin(), new FlowrAnalyzerJupyterFilePlugin(), + new FlowrAnalyzerNamespaceFilePlugin() ]; } diff --git a/src/project/plugins/package-version-plugins/flowr-analyzer-package-versions-description-file-plugin.ts b/src/project/plugins/package-version-plugins/flowr-analyzer-package-versions-description-file-plugin.ts index a27d412f252..ce487f64304 100644 --- a/src/project/plugins/package-version-plugins/flowr-analyzer-package-versions-description-file-plugin.ts +++ b/src/project/plugins/package-version-plugins/flowr-analyzer-package-versions-description-file-plugin.ts @@ -41,7 +41,13 @@ export class FlowrAnalyzerPackageVersionsDescriptionFilePlugin extends FlowrAnal const [, name, operator, version] = match; const range = Package.parsePackageVersionRange(operator, version); - ctx.deps.addDependency(new Package(name, type, undefined, range)); + ctx.deps.addDependency(new Package( + { + name: name, + type: type, + versionConstraints: range ? [range] : undefined + } + )); } } } diff --git a/src/project/plugins/package-version-plugins/flowr-analyzer-package-versions-namespace-file-plugin.ts b/src/project/plugins/package-version-plugins/flowr-analyzer-package-versions-namespace-file-plugin.ts new file mode 100644 index 00000000000..69219ad68b4 --- /dev/null +++ b/src/project/plugins/package-version-plugins/flowr-analyzer-package-versions-namespace-file-plugin.ts @@ -0,0 +1,63 @@ +import { FlowrAnalyzerPackageVersionsPlugin } from './flowr-analyzer-package-versions-plugin'; +import { + descriptionFileLog +} from '../file-plugins/flowr-analyzer-description-file-plugin'; +import { SemVer } from 'semver'; +import { Package } from './package'; +import type { FlowrAnalyzerContext } from '../../context/flowr-analyzer-context'; +import { FileRole } from '../../context/flowr-file'; +import type { NamespaceFormat } from '../file-plugins/flowr-namespace-file'; + +export class FlowrAnalyzerPackageVersionsNamespaceFilePlugin extends FlowrAnalyzerPackageVersionsPlugin { + public readonly name = 'flowr-analyzer-package-version-namespace-file-plugin'; + public readonly description = 'This plugin does...'; + public readonly version = new SemVer('0.1.0'); + + process(ctx: FlowrAnalyzerContext): void { + const nmspcFiles = ctx.files.getFilesByRole(FileRole.Namespace); + if(nmspcFiles.length !== 1) { + descriptionFileLog.warn(`Supporting only exactly one NAMESPACE file, found ${nmspcFiles.length}`); + return; + } + + /** this will do the caching etc. for me */ + const deps = nmspcFiles[0].content() as NamespaceFormat; + + for(const pkg in deps) { + const info = deps[pkg]; + ctx.deps.addDependency(new Package( + { + name: pkg, + namespaceInfo: info + } + )); + for(const exportedSymbol of info.exportedSymbols) { + ctx.functions.addFunctionInfo({ + name: exportedSymbol, + packageOrigin: pkg, + isExported: true, + isS3Generic: false, + }); + } + for(const exportedFunction of info.exportedFunctions) { + ctx.functions.addFunctionInfo({ + name: exportedFunction, + packageOrigin: pkg, + isExported: true, + isS3Generic: false, + }); + } + for(const [genericName, classes] of info.exportS3Generics.entries()) { + for(const className of classes) { + ctx.functions.addFunctionInfo({ + name: genericName, + packageOrigin: pkg, + isExported: true, + isS3Generic: true, + className: className, + }); + } + } + } + } +} \ No newline at end of file diff --git a/src/project/plugins/package-version-plugins/package.ts b/src/project/plugins/package-version-plugins/package.ts index 02267dffe13..c33d6e30e50 100644 --- a/src/project/plugins/package-version-plugins/package.ts +++ b/src/project/plugins/package-version-plugins/package.ts @@ -1,36 +1,82 @@ import { Range } from 'semver'; -import { guard, isNotUndefined } from '../../../util/assert'; +import { guard } from '../../../util/assert'; +import type { NamespaceInfo } from '../file-plugins/flowr-namespace-file'; export type PackageType = 'package' | 'system' | 'r'; +export type PackageOptions = { + derivedVersion?: Range; + type?: PackageType; + dependencies?: Package[]; + namespaceInfo?: NamespaceInfo; + versionConstraints?: Range[]; +} + export class Package { public name: string; public derivedVersion?: Range; public type?: PackageType; public dependencies?: Package[]; + public namespaceInfo?: NamespaceInfo; public versionConstraints: Range[] = []; - constructor(name: string, type?: PackageType, dependencies?: Package[], ...versionConstraints: readonly (Range | undefined)[]) { - this.name = name; - this.addInfo(type, dependencies, ...(versionConstraints ?? []).filter(isNotUndefined)); + constructor(info: { name: string } & PackageOptions) { + this.name = info.name; + this.addInfo(info); + } + + has(name: string, className?: string): boolean { + if(!this.namespaceInfo) { + return false; + } + + if(name.includes('.')) { + const [genericSplit, classSplit] = name.split('.'); + const classes = this.namespaceInfo.exportS3Generics.get(genericSplit); + return classes ? classes.includes(classSplit) : false; + } + + if(className) { + const classes = this.namespaceInfo.exportS3Generics.get(name); + return classes ? classes.includes(className) : false; + } + + return this.namespaceInfo.exportedFunctions.includes(name) || this.namespaceInfo.exportedSymbols.includes(name); + } + + s3For(generic: string): string[] { + return this.namespaceInfo?.exportS3Generics.get(generic) ?? []; } public mergeInPlace(other: Package): void { guard(this.name === other.name, 'Can only merge packages with the same name'); this.addInfo( - other.type, - other.dependencies, - ...other.versionConstraints + { + type: other.type, + dependencies: other.dependencies, + namespaceInfo: other.namespaceInfo, + versionConstraints: other.versionConstraints + } ); } - public addInfo(type?: PackageType, dependencies?: Package[], ...versionConstraints: readonly Range[]): void { + public addInfo(info: PackageOptions): void { + const { + type, + dependencies, + namespaceInfo, + versionConstraints + } = info; + if(type !== undefined) { this.type = type; } if(dependencies !== undefined) { this.dependencies = dependencies; } + if(namespaceInfo !== undefined) { + this.namespaceInfo = namespaceInfo; + } if(versionConstraints !== undefined) { this.derivedVersion ??= versionConstraints[0]; diff --git a/src/project/plugins/plugin-registry.ts b/src/project/plugins/plugin-registry.ts index 141ef02c938..9e25e9a0440 100644 --- a/src/project/plugins/plugin-registry.ts +++ b/src/project/plugins/plugin-registry.ts @@ -10,6 +10,7 @@ import { FlowrAnalyzerRmdFilePlugin } from './file-plugins/notebooks/flowr-analy import { FlowrAnalyzerQmdFilePlugin } from './file-plugins/notebooks/flowr-analyzer-qmd-file-plugin'; import { guard } from '../../util/assert'; import { FlowrAnalyzerJupyterFilePlugin } from './file-plugins/notebooks/flowr-analyzer-jupyter-file-plugin'; +import { FlowrAnalyzerNamespaceFilePlugin } from './file-plugins/flowr-analyzer-namespace-file-plugin'; /** * The built-in Flowr Analyzer plugins that are always available. @@ -21,6 +22,7 @@ export const BuiltInPlugins = [ ['file:rmd', FlowrAnalyzerRmdFilePlugin], ['file:qmd', FlowrAnalyzerQmdFilePlugin], ['file:ipynb', FlowrAnalyzerJupyterFilePlugin], + ['file:namespace', FlowrAnalyzerNamespaceFilePlugin], ] as const satisfies [string, PluginProducer][]; export type BuiltInFlowrPluginName = typeof BuiltInPlugins[number][0]; diff --git a/src/queries/catalog/dependencies-query/dependencies-query-executor.ts b/src/queries/catalog/dependencies-query/dependencies-query-executor.ts index 7a613c83ead..9ef0c2ec09f 100644 --- a/src/queries/catalog/dependencies-query/dependencies-query-executor.ts +++ b/src/queries/catalog/dependencies-query/dependencies-query-executor.ts @@ -143,7 +143,8 @@ function getResults(queries: readonly DependenciesQuery[], { dataflow, config, n linkedIds: linked?.length ? linked : undefined, value: value ?? defaultValue, versionConstraints: dep?.versionConstraints, - derivedVersion: dep?.derivedVersion + derivedVersion: dep?.derivedVersion, + namespaceInfo: dep?.namespaceInfo } as DependencyInfo); if(result) { results.push(result); diff --git a/src/queries/catalog/dependencies-query/dependencies-query-format.ts b/src/queries/catalog/dependencies-query/dependencies-query-format.ts index 324b960ee3d..37ca616d3b8 100644 --- a/src/queries/catalog/dependencies-query/dependencies-query-format.ts +++ b/src/queries/catalog/dependencies-query/dependencies-query-format.ts @@ -16,6 +16,7 @@ import { RType } from '../../../r-bridge/lang-4.x/ast/model/type'; import type { CallContextQueryResult } from '../call-context-query/call-context-query-format'; import type { Range } from 'semver'; import type { AsyncOrSync } from 'ts-essentials'; +import type { NamespaceInfo } from '../../../project/plugins/file-plugins/flowr-namespace-file'; export const Unknown = 'unknown'; @@ -43,7 +44,8 @@ export const DefaultDependencyCategories = { functionName: (n.info.fullLexeme ?? n.lexeme).includes(':::') ? ':::' : '::', value: n.namespace, versionConstraints: dep?.versionConstraints, - derivedVersion: dep?.derivedVersion + derivedVersion: dep?.derivedVersion, + namespaceInfo: dep?.namespaceInfo }); } }); @@ -92,7 +94,8 @@ export interface DependencyInfo extends Record{ /** The library name, file, source, destination etc. being sourced, read from, or written to. */ value?: string versionConstraints?: Range[], - derivedVersion?: Range + derivedVersion?: Range, + namespaceInfo?: NamespaceInfo, } function printResultSection(title: string, infos: DependencyInfo[], result: string[]): void { diff --git a/src/util/files.ts b/src/util/files.ts index c20b3eb451f..41de74c9514 100644 --- a/src/util/files.ts +++ b/src/util/files.ts @@ -4,6 +4,7 @@ import { log } from './log'; import LineByLine from 'n-readlines'; import type { RParseRequestFromFile } from '../r-bridge/retriever'; import type { FlowrFileProvider } from '../project/context/flowr-file'; +import type { NamespaceFormat } from '../project/plugins/file-plugins/flowr-namespace-file'; /** * Represents a table, identified by a header and a list of rows. @@ -213,7 +214,78 @@ export function parseDCF(file: FlowrFileProvider): Map { return result; } +/** + * Parses the given NAMESPACE file + * @param file - The file to parse + * @returns NamespaceFormat + */ +export function parseNamespace(file: FlowrFileProvider): NamespaceFormat { + const result = { + current: { + exportedSymbols: [] as string[], + exportedFunctions: [] as string[], + exportS3Generics: new Map(), + loadsWithSideEffects: false, + }, + } as NamespaceFormat; + const fileContent = file.content().toString().replaceAll(cleanLineCommentRegex, '').trim() + .split(/\r?\n/).filter(Boolean); + + for(const line of fileContent) { + const match = line.trim().match(/^(\w+)\(([^)]*)\)$/); + if(!match) { + continue; + } + const [, type, args] = match; + + switch(type) { + case 'exportClasses': + case 'exportMethods': + result.current.exportedFunctions.push(args); + break; + case 'S3method': + { + const parts = args.split(',').map(s => s.trim()); + if(parts.length !== 2) { + continue; + } + const [pkg, func] = parts; + let arr = result.current.exportS3Generics.get(pkg); + if(!arr) { + arr = []; + result.current.exportS3Generics.set(pkg, arr); + } + arr.push(func); + break; + } + case 'export': + result.current.exportedSymbols.push(args); + break; + case 'useDynLib': + { + const parts = args.split(',').map(s => s.trim()); + if(parts.length !== 2) { + continue; + } + const [pkg] = parts; + if(!result[pkg]) { + result[pkg] = { + exportedSymbols: [], + exportedFunctions: [], + exportS3Generics: new Map(), + loadsWithSideEffects: false, + }; + } + result[pkg].loadsWithSideEffects = true; + break; + } + } + } + + return result; +} +const cleanLineCommentRegex = /^#.*$/gm; const cleanSplitRegex = /[\n,]+/; const cleanQuotesRegex = /'/g; diff --git a/test/functionality/project/package.test.ts b/test/functionality/project/package.test.ts index 75156415abc..0b67742fdd8 100644 --- a/test/functionality/project/package.test.ts +++ b/test/functionality/project/package.test.ts @@ -5,11 +5,11 @@ import { Range } from 'semver'; describe('DESCRIPTION-file', function() { describe.sequential('Parsing', function() { test('Library-Versions-Plugin', () => { - const p1 = new Package('Test Package'); - p1.addInfo('package', undefined, new Range('>= 1.3')); - p1.addInfo(undefined, undefined, new Range('<= 2.3')); - p1.addInfo(undefined, undefined, new Range('>= 1.5')); - p1.addInfo(undefined, undefined, new Range('<= 2.2.5')); + const p1 = new Package({ name: 'Test Package' }); + p1.addInfo({ type: 'package', versionConstraints: [new Range('>=1.3')] }); + p1.addInfo({ versionConstraints: [new Range('<=2.3')] }); + p1.addInfo({ versionConstraints: [new Range('>=1.5')] }); + p1.addInfo({ versionConstraints: [new Range('<=2.2.5')] }); assert.isTrue(p1.derivedVersion?.test('1.7.0')); }); diff --git a/test/functionality/project/plugin/namespace-file.test.ts b/test/functionality/project/plugin/namespace-file.test.ts new file mode 100644 index 00000000000..fc524f8946f --- /dev/null +++ b/test/functionality/project/plugin/namespace-file.test.ts @@ -0,0 +1,44 @@ +import { assert, describe, test } from 'vitest'; +import { FlowrAnalyzerContext } from '../../../../src/project/context/flowr-analyzer-context'; +import { arraysGroupBy } from '../../../../src/util/collections/arrays'; +import { FlowrInlineTextFile } from '../../../../src/project/context/flowr-file'; +import { + FlowrAnalyzerNamespaceFilePlugin +} from '../../../../src/project/plugins/file-plugins/flowr-analyzer-namespace-file-plugin'; +import { + FlowrAnalyzerPackageVersionsNamespaceFilePlugin +} from '../../../../src/project/plugins/package-version-plugins/flowr-analyzer-package-versions-namespace-file-plugin'; +import { defaultConfigOptions } from '../../../../src/config'; + +describe('NAMESPACE-file', function() { + const ctx = new FlowrAnalyzerContext( + defaultConfigOptions, + arraysGroupBy([ + new FlowrAnalyzerNamespaceFilePlugin(), + new FlowrAnalyzerPackageVersionsNamespaceFilePlugin() + ], p => p.type) + ); + + ctx.addFile(new FlowrInlineTextFile('NAMESPACE', `# Generated by roxygen2 (4.0.2): do not edit by hand +S3method(as.character,expectation) +S3method(compare,character) +export(auto_test) +export(auto_test_package) +export(colourise) +export(context) +exportClasses(ListReporter) +exportClasses(MinimalReporter) +importFrom(methods,setRefClass) +useDynLib(testthat,duplicate_) +useDynLib(testthat,reassign_function)`)); + ctx.addFile(new FlowrInlineTextFile('pete.R', 'x <- 2')); + ctx.addRequests([{ request: 'file', content: 'pete.R' }]); + ctx.resolvePreAnalysis(); + describe.sequential('Parsing', function() { + test('Library-Versions-Plugin', () => { + const deps = ctx.deps.getDependency('current'); + assert.isDefined(deps); + assert.isTrue(deps.namespaceInfo?.loadsWithSideEffects === false); + }); + }); +}); \ No newline at end of file