@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
200 lines • 11.5 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FlowrAnalyzerDefaultProjectDiscoveryPlugin = exports.FlowrAnalyzerFullProjectDiscoveryPlugin = exports.excludeRequestsForPaths = exports.ignorePathsWith = exports.discoverRSourcesRegex = exports.FlowrAnalyzerProjectDiscoveryPlugin = void 0;
exports.collectRequests = collectRequests;
const flowr_analyzer_plugin_1 = require("../flowr-analyzer-plugin");
const semver_1 = require("semver");
const flowr_file_1 = require("../../context/flowr-file");
const files_1 = require("../../../util/files");
const built_in_source_1 = require("../../../dataflow/internal/process/functions/call/built-in/built-in-source");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const classify_project_kind_1 = require("../../context/classify-project-kind");
const glob_1 = require("../../../util/glob");
const flowr_analyzer_rprofile_file_plugin_1 = require("../file-plugins/flowr-analyzer-rprofile-file-plugin");
/**
* This is the base class for all plugins that discover files in a project for analysis.
* These plugins interplay with the {@link FlowrAnalyzerFilesContext} to gather information about the files in the project.
* See {@link FlowrAnalyzerDefaultProjectDiscoveryPlugin} for the default implementation.
*
* In general, these plugins only trigger for a {@link RProjectAnalysisRequest} with the idea to discover all files in a project.
*/
class FlowrAnalyzerProjectDiscoveryPlugin extends flowr_analyzer_plugin_1.FlowrAnalyzerPlugin {
type = flowr_analyzer_plugin_1.PluginType.ProjectDiscovery;
static defaultPlugin() {
return new FlowrAnalyzerDefaultProjectDiscoveryPlugin();
}
}
exports.FlowrAnalyzerProjectDiscoveryPlugin = FlowrAnalyzerProjectDiscoveryPlugin;
// `.Rprofile`/`Rprofile.site` carry no extension but are plain R sources
exports.discoverRSourcesRegex = /(\.(r|rmd|rmarkdown|ipynb|qmd|rnw)|(^|[\\/])\.?Rprofile(\.site)?)$/i;
// matched against the posix path relative to the project root
exports.ignorePathsWith = /(^|\/)(\.git|\.svn|\.hg|node_modules|__pycache__|\.Rproj\.user|\.uvr|Rtmp[^/]*|(packrat|renv|rv)\/(lib|library|src|staging|sandbox|bundles)[^/]*)(\/|$)/i;
exports.excludeRequestsForPaths = /vignettes?|tests?|revdep|inst|data/i;
/**
* Turn the walked `files` (absolute paths under `root`) into the discovery result: R sources become
* {@link RParseRequest}s, everything else -- and R sources under an excluded directory -- becomes a
* {@link FlowrTextFile}; `.Rprofile` files get both so they are tagged. This is the single emit implementation
* shared by the greedy and the intelligent discovery plugins.
*/
function collectRequests(files, root, opts = {}) {
const supportedExtensions = opts.supportedExtensions ?? exports.discoverRSourcesRegex;
const excludePathsRegex = opts.excludePathsRegex ?? exports.excludeRequestsForPaths;
const onlyTraversePaths = opts.onlyTraversePaths;
const requests = [];
for (const file of files) {
const relativePath = path_1.default.relative(root, file);
if (supportedExtensions.test(relativePath) && (!onlyTraversePaths || onlyTraversePaths.test(relativePath)) && !excludePathsRegex.test((0, built_in_source_1.platformDirname)(relativePath))) {
requests.push({ content: file, request: 'file' });
if (flowr_analyzer_rprofile_file_plugin_1.RprofileFilePattern.test((0, built_in_source_1.platformBasename)(relativePath))) {
// parse requests skip the file plugins, so emit a file too to have the profile tagged
requests.push(new flowr_file_1.FlowrTextFile(file));
}
}
else {
requests.push(new flowr_file_1.FlowrTextFile(file));
}
}
return requests;
}
/**
* The greedy discovery implementation: every file below the root becomes a {@link RParseRequest} (R and Rmd files)
* or a {@link FlowrTextFile} (the rest). This is what {@link FlowrAnalyzerDefaultProjectDiscoveryPlugin} falls back
* to in `full` mode.
*/
class FlowrAnalyzerFullProjectDiscoveryPlugin extends FlowrAnalyzerProjectDiscoveryPlugin {
name = 'full-project-discovery-plugin';
description = 'Collects every file below the project root (greedy discovery).';
version = new semver_1.SemVer('0.0.0');
supportedExtensions;
ignorePathsRegex;
excludePathsRegex = exports.excludeRequestsForPaths;
onlyTraversePaths;
/**
* Creates a new instance of the greedy project discovery plugin.
* @param triggerOnExtensions - the regex to trigger R source file discovery on (and hence analyze them as R files)
* @param ignorePathsRegex - the regex to ignore certain paths entirely
* @param excludePathsRegex - the regex to exclude certain paths from being requested as R files (they are still collected as text files)
* @param onlyTraversePaths - if set, only paths matching this regex are traversed
*/
constructor({ triggerOnExtensions = exports.discoverRSourcesRegex, ignorePathsRegex = exports.ignorePathsWith, excludePathsRegex = exports.excludeRequestsForPaths, onlyTraversePaths } = {}) {
super();
this.supportedExtensions = triggerOnExtensions;
this.ignorePathsRegex = ignorePathsRegex;
this.excludePathsRegex = excludePathsRegex;
this.onlyTraversePaths = onlyTraversePaths;
}
process(context, args) {
if (!fs_1.default.existsSync(args.content)) {
return [];
}
const failOnInaccessiblePath = context.config.project.failOnInaccessiblePath ?? false;
return collectRequests((0, files_1.getAllFilesSync)(args.content, /.*/, this.ignorePathsRegex, args.content, failOnInaccessiblePath), args.content, { supportedExtensions: this.supportedExtensions, excludePathsRegex: this.excludePathsRegex, onlyTraversePaths: this.onlyTraversePaths });
}
}
exports.FlowrAnalyzerFullProjectDiscoveryPlugin = FlowrAnalyzerFullProjectDiscoveryPlugin;
// noise directories pruned from the scoped walk on top of `ignorePathsWith`: build output, checks, rendered docs and packaged data
const noiseDirs = /(^|\/)(\.git|\.svn|\.hg|node_modules|__pycache__|\.Rproj\.user|\.uvr|[^/]*\.Rcheck|(packrat|renv|rv)\/(lib|library|src|staging|sandbox|bundles|cache)[^/]*|build|dist|_build|_site|_book|\.quarto|\.cache|Rtmp[^/]*|man|inst\/(extdata|doc))(\/|$)/i;
// binary/data blobs, dropped even inside an otherwise kept directory
const noiseFiles = /\.(rds|rda|rdata|rd|png|jpe?g|gif|svg|pdf|ico|zip|tar|t?gz|bz2|xz|so|o|a|dll|dylib|exe|jar|woff2?|ttf|eot|feather|parquet|xls[xm]?|docx?|pptx?)$/i;
const descriptionFilePattern = /^DESCRIPTION(\.(txt|in))?$/i;
// metadata the analysis still needs when scoped (mirrors the file-role plugin patterns; local to avoid a cycle)
const metadataFilePatterns = [
descriptionFilePattern,
/^NAMESPACE(\.txt)?$/i,
/^NEWS(\.(rd|md))?$/i,
/^(renv|rv|uvr|packrat)\.lock$/i,
/^(rproject|uvr)\.toml$/i,
/license(\.md|\.txt)?$/i,
flowr_analyzer_rprofile_file_plugin_1.RprofileFilePattern,
flowr_analyzer_rprofile_file_plugin_1.RenvironFilePattern
];
const testOrVignetteDir = /(^|\/)(tests?|vignettes?)(\/|$)/i;
/** compile the `project.discovery.perKind` include/exclude globs into matchers */
function resolveRules(override) {
return {
include: (override?.include ?? []).map(glob_1.globMatcher),
exclude: (override?.exclude ?? []).map(glob_1.globMatcher)
};
}
/** over-approximating default: R sources, role metadata and `tests/`/`vignettes/` count as project files */
function keptByDefault(rel) {
const base = path_1.default.basename(rel);
return exports.discoverRSourcesRegex.test(rel) || metadataFilePatterns.some(p => p.test(base)) || testOrVignetteDir.test(rel);
}
/** whether root-relative `rel` is kept; an explicit `ignore` or `perKind` exclude wins over everything */
function keep(rel, rules, ignore) {
if (noiseFiles.test(rel) || ignore.some(m => m(rel)) || rules.exclude.some(m => m(rel))) {
return false;
}
return keptByDefault(rel) || rules.include.some(m => m(rel));
}
/** the lower-cased `DESCRIPTION` `Type:` field, read without the full DCF parser to avoid a cycle */
function descriptionType(file) {
let content;
try {
content = fs_1.default.readFileSync(file, 'utf8');
}
catch {
return '';
}
const m = /^Type:[ \t]*(.+)$/im.exec(content.replace(/^\uFEFF/, ''));
return (m?.[1] ?? '').trim().toLowerCase();
}
/**
* flowR's default discovery: walk the project once (pruning noise directories), classify the {@link ProjectKind}
* from what the walk sees, then keep only the files that kind needs. `project.discovery.full` restores the greedy
* {@link FlowrAnalyzerFullProjectDiscoveryPlugin}, `project.discovery.perKind` overrides the kept set per kind.
*/
class FlowrAnalyzerDefaultProjectDiscoveryPlugin extends FlowrAnalyzerProjectDiscoveryPlugin {
name = 'default-project-discovery-plugin';
description = 'Detects the project kind and discovers only the files it needs (unless project.discovery.full).';
version = new semver_1.SemVer('1.0.0');
process(context, args) {
if (!fs_1.default.existsSync(args.content)) {
return [];
}
const root = args.content;
const failOn = context.config.project.failOnInaccessiblePath ?? false;
const discovery = context.config.project.discovery;
if (discovery?.full) {
return collectRequests((0, files_1.getAllFilesSync)(root, /.*/, exports.ignorePathsWith, root, failOn), root);
}
const opts = (0, classify_project_kind_1.resolveClassifyOptions)(context.config.project.classification);
// one walk gathers the candidate files and the classification signals, so the kind is known the moment it ends
const files = [];
const names = new Set();
const entries = new Map();
const descriptionTypes = [];
let descriptionCount = 0;
for (const file of (0, files_1.getAllFilesSync)(root, /.*/, noiseDirs, root, failOn)) {
files.push(file);
const base = path_1.default.basename(file);
const lower = base.toLowerCase();
names.add(lower);
if (opts.shinyEntryFiles.has(lower) && !entries.has(lower)) {
entries.set(lower, () => {
try {
return fs_1.default.readFileSync(file, 'utf8');
}
catch {
return undefined;
}
});
}
if (descriptionFilePattern.test(base)) {
descriptionCount++;
descriptionTypes.push(descriptionType(file));
}
}
const kind = (0, classify_project_kind_1.classifyProjectKind)({ names, entries, descriptionTypes, descriptionCount }, opts);
const rules = resolveRules(discovery?.perKind?.[kind]);
const ignore = (discovery?.ignore ?? []).map(glob_1.globMatcher);
return collectRequests(files.filter(f => keep(path_1.default.relative(root, f), rules, ignore)), root);
}
}
exports.FlowrAnalyzerDefaultProjectDiscoveryPlugin = FlowrAnalyzerDefaultProjectDiscoveryPlugin;
//# sourceMappingURL=flowr-analyzer-project-discovery-plugin.js.map