@eagleoutice/flowr
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
230 lines • 12.8 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.flowrConfigFileSchema = exports.defaultConfigOptions = exports.DropPathsOption = exports.InferWorkingDirectory = exports.VariableResolve = void 0;
exports.parseConfig = parseConfig;
exports.amendConfig = amendConfig;
exports.cloneConfig = cloneConfig;
exports.getConfig = getConfig;
exports.getEngineConfig = getEngineConfig;
exports.isOverPointerAnalysisThreshold = isOverPointerAnalysisThreshold;
const objects_1 = require("./util/objects");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const log_1 = require("./util/log");
const files_1 = require("./util/files");
const joi_1 = __importDefault(require("joi"));
var VariableResolve;
(function (VariableResolve) {
/** Don't resolve constants at all */
VariableResolve["Disabled"] = "disabled";
/** Use alias tracking to resolve */
VariableResolve["Alias"] = "alias";
/** Only resolve directly assigned builtin constants */
VariableResolve["Builtin"] = "builtin";
})(VariableResolve || (exports.VariableResolve = VariableResolve = {}));
/**
* How to infer the working directory from a script
*/
var InferWorkingDirectory;
(function (InferWorkingDirectory) {
/** Don't infer the working directory */
InferWorkingDirectory["No"] = "no";
/** Infer the working directory from the main script */
InferWorkingDirectory["MainScript"] = "main-script";
/** Infer the working directory from the active script */
InferWorkingDirectory["ActiveScript"] = "active-script";
/** Infer the working directory from any script */
InferWorkingDirectory["AnyScript"] = "any-script";
})(InferWorkingDirectory || (exports.InferWorkingDirectory = InferWorkingDirectory = {}));
/**
* How to handle fixed strings in a source path
*/
var DropPathsOption;
(function (DropPathsOption) {
/** Don't drop any parts of the sourced path */
DropPathsOption["No"] = "no";
/** try to drop everything but the filename */
DropPathsOption["Once"] = "once";
/** try to drop every folder of the path */
DropPathsOption["All"] = "all";
})(DropPathsOption || (exports.DropPathsOption = DropPathsOption = {}));
const defaultEngineConfigs = {
'tree-sitter': { type: 'tree-sitter' },
'r-shell': { type: 'r-shell' }
};
exports.defaultConfigOptions = {
ignoreSourceCalls: false,
semantics: {
environment: {
overwriteBuiltIns: {
loadDefaults: true,
definitions: []
}
}
},
engines: [],
defaultEngine: 'tree-sitter',
solver: {
variables: VariableResolve.Alias,
evalStrings: true,
pointerTracking: false,
resolveSource: {
dropPaths: DropPathsOption.No,
ignoreCapitalization: true,
inferWorkingDirectory: InferWorkingDirectory.ActiveScript,
searchPath: [],
repeatedSourceLimit: 2
},
slicer: {
threshold: 50
}
},
abstractInterpretation: {
dataFrame: {
maxColNames: 50,
wideningThreshold: 4,
readLoadedData: {
readExternalFiles: true,
maxReadLines: 1e6
}
}
}
};
exports.flowrConfigFileSchema = joi_1.default.object({
ignoreSourceCalls: joi_1.default.boolean().optional().description('Whether source calls should be ignored, causing {@link processSourceCall}\'s behavior to be skipped.'),
semantics: joi_1.default.object({
environment: joi_1.default.object({
overwriteBuiltIns: joi_1.default.object({
loadDefaults: joi_1.default.boolean().optional().description('Should the default configuration still be loaded?'),
definitions: joi_1.default.array().items(joi_1.default.object()).optional().description('The definitions to load/overwrite.')
}).optional().description('Do you want to overwrite (parts) of the builtin definition?')
}).optional().description('Semantics regarding how to handle the R environment.')
}).description('Configure language semantics and how flowR handles them.'),
engines: joi_1.default.array().items(joi_1.default.alternatives(joi_1.default.object({
type: joi_1.default.string().required().valid('tree-sitter').description('Use the tree sitter engine.'),
wasmPath: joi_1.default.string().optional().description('The path to the tree-sitter-r WASM binary to use. If this is undefined, this uses the default path.'),
treeSitterWasmPath: joi_1.default.string().optional().description('The path to the tree-sitter WASM binary to use. If this is undefined, this uses the default path.'),
lax: joi_1.default.boolean().optional().description('Whether to use the lax parser for parsing R code (allowing for syntax errors). If this is undefined, the strict parser will be used.')
}).description('The configuration for the tree sitter engine.'), joi_1.default.object({
type: joi_1.default.string().required().valid('r-shell').description('Use the R shell engine.'),
rPath: joi_1.default.string().optional().description('The path to the R executable to use. If this is undefined, this uses the default path.')
}).description('The configuration for the R shell engine.'))).min(1).description('The engine or set of engines to use for interacting with R code. An empty array means all available engines will be used.'),
defaultEngine: joi_1.default.string().optional().valid('tree-sitter', 'r-shell').description('The default engine to use for interacting with R code. If this is undefined, an arbitrary engine from the specified list will be used.'),
solver: joi_1.default.object({
variables: joi_1.default.string().valid(...Object.values(VariableResolve)).description('How to resolve variables and their values.'),
evalStrings: joi_1.default.boolean().description('Should we include eval(parse(text="...")) calls in the dataflow graph?'),
pointerTracking: joi_1.default.alternatives(joi_1.default.boolean(), joi_1.default.object({
maxIndexCount: joi_1.default.number().required().description('The maximum number of indices tracked per object with the pointer analysis.')
})).description('Whether to track pointers in the dataflow graph, if not, the graph will be over-approximated wrt. containers and accesses.'),
resolveSource: joi_1.default.object({
dropPaths: joi_1.default.string().valid(...Object.values(DropPathsOption)).description('Allow to drop the first or all parts of the sourced path, if it is relative.'),
ignoreCapitalization: joi_1.default.boolean().description('Search for filenames matching in the lowercase.'),
inferWorkingDirectory: joi_1.default.string().valid(...Object.values(InferWorkingDirectory)).description('Try to infer the working directory from the main or any script to analyze.'),
searchPath: joi_1.default.array().items(joi_1.default.string()).description('Additionally search in these paths.'),
repeatedSourceLimit: joi_1.default.number().optional().description('How often the same file can be sourced within a single run? Please be aware: in case of cyclic sources this may not reach a fixpoint so give this a sensible limit.'),
applyReplacements: joi_1.default.array().items(joi_1.default.object()).description('Provide name replacements for loaded files')
}).optional().description('If lax source calls are active, flowR searches for sourced files much more freely, based on the configurations you give it. This option is only in effect if `ignoreSourceCalls` is set to false.'),
slicer: joi_1.default.object({
threshold: joi_1.default.number().optional().description('The maximum number of iterations to perform on a single function call during slicing.')
}).optional().description('The configuration for the slicer.')
}).description('How to resolve constants, constraints, cells, ...'),
abstractInterpretation: joi_1.default.object({
dataFrame: joi_1.default.object({
maxColNames: joi_1.default.number().min(0).description('The maximum number of columns names to infer for data frames before over-approximating the column names to top.'),
wideningThreshold: joi_1.default.number().min(1).description('The threshold for the number of visitations of a node at which widening should be performed to ensure the termination of the fixpoint iteration.'),
readLoadedData: joi_1.default.object({
readExternalFiles: joi_1.default.boolean().description('Whether data frame shapes should be extracted from loaded external files, such as CSV files.'),
maxReadLines: joi_1.default.number().min(1).description('The maximum number of lines to read when extracting data frame shapes from loaded files, such as CSV files.')
}).description('Configuration options for reading data frame shapes from loaded external data files, such as CSV files.')
}).description('The configuration of the shape inference for data frames.')
}).description('The configuration options for abstract interpretation.')
}).description('The configuration file format for flowR.');
function parseConfig(jsonString) {
try {
const parsed = JSON.parse(jsonString);
const validate = exports.flowrConfigFileSchema.validate(parsed);
if (!validate.error) {
// assign default values to all config options except for the specified ones
return (0, objects_1.deepMergeObject)(exports.defaultConfigOptions, parsed);
}
else {
log_1.log.error(`Failed to validate config ${jsonString}: ${validate.error.message}`);
return undefined;
}
}
catch (e) {
log_1.log.error(`Failed to parse config ${jsonString}: ${e.message}`);
}
}
/**
* Creates a new flowr config that has the updated values.
*/
function amendConfig(config, amendmentFunc) {
return amendmentFunc(cloneConfig(config));
}
function cloneConfig(config) {
return JSON.parse(JSON.stringify(config));
}
function getConfig(configFile, configWorkingDirectory = process.cwd()) {
try {
return loadConfigFromFile(configFile, configWorkingDirectory);
}
catch (e) {
log_1.log.error(`Failed to load config: ${e.message}`);
return exports.defaultConfigOptions;
}
}
function getEngineConfig(config, engine) {
const engines = config.engines;
if (!engines.length) {
return defaultEngineConfigs[engine];
}
else {
return engines.find(e => e.type == engine);
}
}
function getPointerAnalysisThreshold(config) {
const pointerTracking = config.solver.pointerTracking;
if (typeof pointerTracking === 'object') {
return pointerTracking.maxIndexCount;
}
else {
return pointerTracking ? 'unlimited' : 'disabled';
}
}
function isOverPointerAnalysisThreshold(config, count) {
const threshold = getPointerAnalysisThreshold(config);
return threshold !== 'unlimited' && (threshold === 'disabled' || count > threshold);
}
function loadConfigFromFile(configFile, workingDirectory) {
if (configFile !== undefined) {
if (path_1.default.isAbsolute(configFile) && fs_1.default.existsSync(configFile)) {
log_1.log.trace(`Found config at ${configFile} (absolute)`);
const ret = parseConfig(fs_1.default.readFileSync(configFile, { encoding: 'utf-8' }));
if (ret) {
log_1.log.info(`Using config ${JSON.stringify(ret)}`);
return ret;
}
}
let searchPath = path_1.default.resolve(workingDirectory);
do {
const configPath = path_1.default.join(searchPath, configFile);
if (fs_1.default.existsSync(configPath)) {
log_1.log.trace(`Found config at ${configPath}`);
const ret = parseConfig(fs_1.default.readFileSync(configPath, { encoding: 'utf-8' }));
if (ret) {
log_1.log.info(`Using config ${JSON.stringify(ret)}`);
return ret;
}
}
// move up to parent directory
searchPath = (0, files_1.getParentDirectory)(searchPath);
} while (fs_1.default.existsSync(searchPath));
}
log_1.log.info(`Using default config ${JSON.stringify(exports.defaultConfigOptions)}`);
return exports.defaultConfigOptions;
}
//# sourceMappingURL=config.js.map