@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
387 lines • 16.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FlowrAnalyzerFilesContext = void 0;
const abstract_flowr_analyzer_context_1 = require("./abstract-flowr-analyzer-context");
const retriever_1 = require("../../r-bridge/retriever");
const assert_1 = require("../../util/assert");
const flowr_analyzer_project_discovery_plugin_1 = require("../plugins/project-discovery/flowr-analyzer-project-discovery-plugin");
const flowr_analyzer_file_plugin_1 = require("../plugins/file-plugins/flowr-analyzer-file-plugin");
const flowr_file_1 = require("./flowr-file");
const log_1 = require("../../util/log");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const files_1 = require("../../util/files");
const glob_1 = require("../../util/glob");
const classify_project_kind_1 = require("./classify-project-kind");
const fileLog = log_1.log.getSubLogger({ name: 'flowr-analyzer-files-context' });
function wrapFile(file, roles) {
if (typeof file === 'string') {
return new flowr_file_1.FlowrTextFile(file, roles);
}
else if ('request' in file) {
return flowr_file_1.FlowrFile.fromRequest(file);
}
else {
return file;
}
}
/**
* This is the analyzer file context to be modified by all plugins that affect the files.
* If you are interested in inspecting these files, refer to {@link ReadOnlyFlowrAnalyzerFilesContext}.
* Plugins, however, can use this context directly to modify files.
*/
class FlowrAnalyzerFilesContext extends abstract_flowr_analyzer_context_1.AbstractFlowrAnalyzerContext {
name = 'flowr-analyzer-files-context';
loadingOrder;
/* all project files etc., this contains *all* (non-inline) files, loading orders etc. are to be handled by plugins */
files = new Map();
inlineFiles = [];
fileLoaders;
context;
/** these are all the paths of files that have been considered by the dataflow graph (even if not added) */
consideredFiles = [];
/** User-registered project discovery plugins; if non-empty, they replace the default. */
discoveryPlugins;
/* files that are part of the analysis, e.g. source files */
byRole = Object.fromEntries(Object.values(flowr_file_1.FileRole).map(k => [k, []]));
/** cached {@link projectKind}, invalidated whenever the files change (added or reset) */
projectKindCache = undefined;
requestedRoots = [];
/** directories already scanned by {@link discoverImplicitSources}, so a sibling implicit source is not re-triggered for every file added from it */
implicitSourceDirs = new Set();
/** cached {@link root}, fixed on first use as the ids built from it have to stay stable */
rootCache = undefined;
rootResolved = false;
constructor(context, loadingOrder, plugins, fileLoaders) {
super(loadingOrder.getAttachedContext(), flowr_analyzer_project_discovery_plugin_1.FlowrAnalyzerProjectDiscoveryPlugin.defaultPlugin(), []);
this.discoveryPlugins = plugins;
this.context = context;
this.fileLoaders = [...fileLoaders, flowr_analyzer_file_plugin_1.FlowrAnalyzerFilePlugin.defaultPlugin()];
this.loadingOrder = loadingOrder;
}
reset() {
this.loadingOrder.reset();
this.files = new Map();
this.consideredFiles.length = 0;
this.inlineFiles.length = 0;
this.byRole = Object.fromEntries(Object.values(flowr_file_1.FileRole).map(k => [k, []]));
this.projectKindCache = undefined;
this.requestedRoots.length = 0;
this.rootCache = undefined;
this.rootResolved = false;
}
/** The directory the analysis was asked about: a `project`'s folder, or the one holding the requested file(s). */
root() {
if (!this.rootResolved) {
this.rootResolved = true;
this.rootCache = (0, files_1.commonDirectory)(this.requestedRoots);
}
return this.rootCache;
}
/** The path of `filePath` seen from the {@link root}, see {@link relativeTo}. */
relativePath(filePath) {
const root = this.root();
return root === undefined ? filePath : (0, files_1.relativeTo)(root, filePath);
}
receive(event) {
const type = event.type;
switch (type) {
case "full" /* InvalidationEventType.Full */:
this.reset();
break;
case "single-file-invalidate" /* InvalidationEventType.SingleFileInvalidate */:
// only the content of a known file changed, so the file set stays valid -> nothing to do.
// revisit once we add dedicated FileAdded / FileRemoved events.
break;
default:
(0, assert_1.assertUnreachable)(type);
}
}
/**
* Record that a file has been considered during dataflow analysis.
*/
addConsideredFile(path) {
this.consideredFiles.push(path);
this.projectKindCache = undefined;
}
/**
* Get all files that have been considered during dataflow analysis.
*/
consideredFilesList() {
return this.consideredFiles;
}
projectKind() {
return this.projectKindCache ??= this.ctx.config.project.useProjectType ?? this.classifyProject();
}
getRequestedRoots() {
return this.requestedRoots;
}
classifyProject() {
const opts = (0, classify_project_kind_1.resolveClassifyOptions)(this.ctx.config.project.classification);
const descriptions = this.getFilesByRole(flowr_file_1.FileRole.Description);
const reader = (f) => () => {
try {
return f.content().toString();
}
catch {
return undefined;
}
};
// the readable entry files by (lower-cased) name, plus every known file name for the coarser checks
const entries = new Map();
const names = new Set();
for (const f of this.getAllFiles()) {
const name = path_1.default.basename(f.path()).toLowerCase();
names.add(name);
if (opts.shinyEntryFiles.has(name)) {
entries.set(name, reader(f));
}
}
const pending = this.loadingOrder.getUnorderedRequests()
.filter(r => r.request === 'file').map(r => r.content);
for (const p of [...pending, ...this.consideredFiles]) {
const name = path_1.default.basename(p).toLowerCase();
names.add(name);
if (opts.shinyEntryFiles.has(name) && !entries.has(name)) {
entries.set(name, reader(this.getFileByPath(p) ?? new flowr_file_1.FlowrTextFile(p)));
}
}
return (0, classify_project_kind_1.classifyProjectKind)({
names,
entries,
descriptionTypes: descriptions.map(d => d.type() ?? ''),
descriptionCount: descriptions.length
}, opts);
}
/**
* Add multiple requests to the context. This is just a convenience method that calls {@link addRequest} for each request.
*/
addRequests(requests) {
for (const request of requests) {
this.addRequest(request);
}
}
/**
* Add a request to the context. If the request is of type `project`, it will be expanded using the registered {@link FlowrAnalyzerProjectDiscoveryPlugin}s.
* User-registered discovery plugins replace the built-in default; if none are registered, the default runs.
*/
addRequest(request) {
if (request.request !== 'project') {
if (request.request === 'file') {
this.requestedRoots.push(path_1.default.dirname(request.content));
this.discoverImplicitSources(request.content);
}
this.loadingOrder.addRequest(request);
this.projectKindCache = undefined;
return;
}
this.requestedRoots.push(request.content);
const active = this.discoveryPlugins.length > 0
? this.discoveryPlugins
: [flowr_analyzer_project_discovery_plugin_1.FlowrAnalyzerProjectDiscoveryPlugin.defaultPlugin()];
const expandedRequests = active.flatMap(p => p.processor(this.ctx, request));
for (const req of expandedRequests) {
if ((0, retriever_1.isParseRequest)(req)) {
this.addRequest(req);
}
else {
this.addFile(req, req.roles);
}
}
}
/**
* A single-file request otherwise skips project discovery entirely, so a sibling `project.implicitSources`
* entry (e.g. a shiny app's `s.R` next to the analyzed `t.R`) would never be found. If `implicitSources` is
* configured, scan `fileContent`'s directory once and add whatever matches.
*/
discoverImplicitSources(fileContent) {
const implicit = this.ctx.config.project.implicitSources;
if (!implicit || implicit.length === 0) {
return;
}
const dir = path_1.default.dirname(fileContent);
if (this.implicitSourceDirs.has(dir)) {
return;
}
this.implicitSourceDirs.add(dir);
if (!fs_1.default.existsSync(dir) || !fs_1.default.statSync(dir).isDirectory()) {
return;
}
const matchers = implicit.map(entry => (0, glob_1.globMatcher)(entry));
const active = this.discoveryPlugins.length > 0
? this.discoveryPlugins
: [flowr_analyzer_project_discovery_plugin_1.FlowrAnalyzerProjectDiscoveryPlugin.defaultPlugin()];
for (const req of active.flatMap(p => p.processor(this.ctx, { request: 'project', content: dir }))) {
const filePath = (0, retriever_1.isParseRequest)(req) ? (req.request === 'file' ? req.content : undefined) : req.path();
if (filePath === undefined || filePath === fileContent || !matchers.some(m => m(filePath))) {
continue;
}
if ((0, retriever_1.isParseRequest)(req)) {
this.addRequest(req);
}
else {
this.addFile(req, req.roles);
}
}
}
/**
* Add multiple files to the context. This is just a convenience method that calls {@link addFile} for each file.
*/
addFiles(files) {
for (const file of files) {
this.addFile(file);
}
}
/**
* Add a file to the context. If the file has a special role, it will be added to the corresponding list of special files.
* This method also applies any registered {@link FlowrAnalyzerFilePlugin}s to the file before adding it to the context.
*/
addFile(file, roles) {
const f = this.fileLoadPlugins(wrapFile(file, roles));
f.addOnInvalidate(c => {
this.context.analyzer?.receive(c);
});
if (f.path() === flowr_file_1.FlowrFile.INLINE_PATH) {
this.inlineFiles.push(f);
}
else {
const exist = this.files.get(f.path());
(0, assert_1.guard)(exist === undefined || exist === f, `File ${f.path()} already added to the context.`);
this.files.set(f.path(), f);
}
if (f.roles) {
for (const r of f.roles) {
this.byRole[r].push(f);
}
}
this.projectKindCache = undefined;
return f;
}
hasCached(path) {
return this.files.has(path);
}
hasFile(path) {
return this.hasCached(path) || (this.ctx.config.project.resolveUnknownPathsOnDisk && fs_1.default.existsSync(path));
}
exists(p, ignoreCase) {
try {
if (!ignoreCase) {
return this.hasFile(p) ? p : undefined;
}
if (this.hasFile(p)) {
return p;
}
// walk the directory and find the first match
const dir = path_1.default.dirname(p);
const dirLower = dir.toLowerCase();
const file = path_1.default.basename(p).toLowerCase();
// try to find in local known files first
for (const f of this.files.keys()) {
if (path_1.default.dirname(f).toLowerCase() !== dirLower) {
continue;
}
const lf = path_1.default.basename(f).toLowerCase();
if (file === lf) {
return f;
}
}
if (this.ctx.config.project.resolveUnknownPathsOnDisk) {
let files;
if (fs_1.default.existsSync(dir)) {
files = fs_1.default.readdirSync(dir);
}
else {
// try to find a dir in parent
const parentDir = path_1.default.dirname(dir);
if (fs_1.default.existsSync(parentDir)) {
const parentFiles = fs_1.default.readdirSync(parentDir);
const foundDir = parentFiles.find(f => f.toLowerCase() === path_1.default.basename(dir).toLowerCase());
if (foundDir) {
files = fs_1.default.readdirSync(path_1.default.join(parentDir, foundDir));
}
}
}
const found = files?.find(f => f.toLowerCase() === file);
return found ? path_1.default.join(dir, found) : undefined;
}
return undefined;
}
catch (e) {
fileLog.warn(`Could not resolve '${p}': ${e instanceof Error ? e.message : String(e)}`);
return undefined;
}
}
fileLoadPlugins(f) {
let fFinal = f;
for (const loader of this.fileLoaders) {
if (loader.applies(f.path())) {
fileLog.debug(`Applying file loader ${loader.name} to file ${f.path()}`);
const res = loader.processor(this.ctx, fFinal);
if (Array.isArray(res)) {
fFinal = res[0];
if (!res[1]) {
break;
}
}
else {
fFinal = res;
break;
}
}
}
return fFinal;
}
/** Resolve the file at `path`, loading it from disk through the {@link FlowrAnalyzerFilePlugin}s if unknown. */
resolveFile(path) {
const file = this.files.get(path);
if (file !== undefined && file !== null) {
return file;
}
if (this.ctx.config.project.resolveUnknownPathsOnDisk) {
fileLog.debug(`File ${path} not found in context, trying to load from disk.`);
if (fs_1.default.existsSync(path)) {
return this.addFile(new flowr_file_1.FlowrTextFile(path));
}
}
return undefined;
}
resolveRequest(r) {
if (r.request === 'text') {
return { r };
}
const file = this.resolveFile(r.content);
(0, assert_1.guard)(file !== undefined && file !== null, `File ${r.content} not found in context.`);
const content = file.content();
return {
r: {
request: 'text',
content: typeof content === 'string' ? content : '',
},
path: file.path()
};
}
/**
* Get all requests that have been added to this context.
* This is a convenience method that calls {@link FlowrAnalyzerLoadingOrderContext.getLoadingOrder}.
*/
computeLoadingOrder() {
return this.loadingOrder.getLoadingOrder();
}
getFilesByRole(role) {
return this.byRole[role];
}
getAllFiles() {
return [...this.files.values(), ...this.inlineFiles];
}
getFileByPath(path) {
return this.files.get(path) ?? this.inlineFiles.find(f => f.path() === path);
}
getFileCount() {
return this.files.size + this.inlineFiles.length;
}
}
exports.FlowrAnalyzerFilesContext = FlowrAnalyzerFilesContext;
//# sourceMappingURL=flowr-analyzer-files-context.js.map