UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

200 lines 11.5 kB
"use strict"; 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