@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
702 lines • 47.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.FlowrConfig = exports.FlowrDefaultPlugins = exports.DefaultAssumedRVersion = exports.DropPathsOption = exports.InferWorkingDirectory = exports.VersionSelection = exports.VariableResolve = void 0;
exports.isSigDbEnabled = isSigDbEnabled;
exports.resolveAssumedRVersion = resolveAssumedRVersion;
exports.globalConfigFilePath = globalConfigFilePath;
exports.persistSigDbPathToGlobalConfig = persistSigDbPathToGlobalConfig;
const objects_1 = require("./util/objects");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const os_1 = __importDefault(require("os"));
const log_1 = require("./util/log");
const files_1 = require("./util/files");
const joi_1 = __importDefault(require("joi"));
const project_kind_1 = require("./project/context/project-kind");
const object_path_1 = __importDefault(require("object-path"));
const gas_1 = require("./gas");
const input_types_1 = require("./queries/catalog/input-sources-query/input-types");
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 a constrained dependency (e.g. `Imports: quantmod (>= 0.4-9)`) is resolved to a concrete version
* against the signature database (see `solver.sigdb.versionSelection`).
*/
var VersionSelection;
(function (VersionSelection) {
/** Resolve to the newest version satisfying the constraint (default; preserves the historic behavior). */
VersionSelection["Newest"] = "newest";
/** Resolve to the oldest version satisfying the constraint. */
VersionSelection["Oldest"] = "oldest";
/** Resolve to the version installed on the analyzing system (needs R; falls back to `newest` when unavailable). */
VersionSelection["System"] = "system";
})(VersionSelection || (exports.VersionSelection = VersionSelection = {}));
/**
* 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 = {}));
/** Whether library exports should be resolved from a signature database (`solver.sigdb.enabled`). */
function isSigDbEnabled(config) {
return config?.solver.sigdb.enabled === true;
}
/**
* R version assumed for analysis when `solver.sigdb.assumedRVersion` is `"auto"` and none could be detected.
* Kept in step with the newest base-R release in the bundled sigdb (see `newestRVersion` in the generated
* base-package cache) so the default hits the precomputed base-package fast path.
*/
exports.DefaultAssumedRVersion = '4.5.3';
/**
* The R version analysis should assume when resolving versioned (base-R) exports (see `solver.sigdb.assumedRVersion`):
* an explicit pin wins, otherwise a real `detected` version (from the engine's `rVersion()`, ignoring `none`/`unknown`),
* otherwise {@link DefaultAssumedRVersion}. Pure and synchronous, so a resolver can call it per lookup.
*/
function resolveAssumedRVersion(config, detected) {
const assumed = config?.solver.sigdb.assumedRVersion;
if (assumed && assumed !== 'auto') {
return assumed;
}
if (detected && detected !== 'none' && detected !== 'unknown') {
return detected;
}
return exports.DefaultAssumedRVersion;
}
const defaultEngineConfigs = {
'tree-sitter': { type: 'tree-sitter' },
'r-shell': { type: 'r-shell' }
};
exports.FlowrDefaultPlugins = [
'file:description',
'versions:description',
'versions:sigdb',
'versions:library',
'versions:namespace',
'versions:renv',
'versions:rv',
'versions:uvr',
'versions:packrat',
'versions:session-info',
'loading-order:description',
'loading-order:implicit-sources',
'loading-order:rprofile',
'loading-order:included-files',
'meta:description',
'meta:rproject',
'meta:uvr',
'file-roles:vignette',
'file-roles:test',
'file-roles:inst',
'file:rmd',
'file:qmd',
'file:rnw',
'file:ipynb',
'file:namespace',
'file:news',
'file:rda',
'file:license',
'file:virtualenv',
'file:rproject',
'file:uvr',
'file:rprofile',
];
/** deep-merge two config overwrites with `own` winning; objects merge, arrays/scalars replace (matching {@link specialize}'s array-as-leaf treatment) */
function mergeOverwrite(parent, own) {
const result = { ...parent };
for (const [key, value] of Object.entries(own)) {
const prev = parent[key];
result[key] = (0, objects_1.isPlainObject)(prev) && (0, objects_1.isPlainObject)(value) ? mergeOverwrite(prev, value) : value;
}
return result;
}
/** The effective overwrite for `kind`, following {@link SpecializeConfigEntry.inherit} (cycle-guarded; own keys win, `inherit` stripped), or `undefined` when nothing is overwritten. */
function resolveSpecialization(specializeConfig, kind, seen = new Set()) {
const entry = specializeConfig?.[kind];
if (!entry || seen.has(kind)) {
return undefined;
}
seen.add(kind);
const { inherit, ...own } = entry;
const parent = inherit !== undefined ? resolveSpecialization(specializeConfig, inherit, seen) : undefined;
const resolved = parent !== undefined ? mergeOverwrite(parent, own) : own;
return Object.keys(resolved).length > 0 ? resolved : undefined;
}
/**
* Applies `overwrite` to `current`, key by key, keeping every value that differs from `base`: only a value nobody
* configured is left to the overwrite. An array is replaced, never appended to.
*/
function specialize(current, base, overwrite) {
if (overwrite === undefined) {
return current;
}
else if ((0, objects_1.isPlainObject)(current) && (0, objects_1.isPlainObject)(overwrite)) {
const result = { ...current };
for (const [key, value] of Object.entries(overwrite)) {
result[key] = specialize(current[key], (0, objects_1.isPlainObject)(base) ? base[key] : undefined, value);
}
return result;
}
return JSON.stringify(current) === JSON.stringify(base) ? overwrite : current;
}
/**
* Merge a user config onto the defaults. Unlike {@link deepMergeObject}, an array in the user config replaces the
* default one instead of being appended to it, so options such as `defaultPlugins` can be reduced and not just extended.
*/
function mergeConfigOntoDefaults(base, addon) {
const merge = (b, a) => {
if (a === undefined) {
return b;
}
if (!(0, objects_1.isPlainObject)(a) || !(0, objects_1.isPlainObject)(b)) {
return a;
}
const out = { ...b };
for (const [key, value] of Object.entries(a)) {
out[key] = merge(out[key], value);
}
return out;
};
return merge(base, addon);
}
/** The shortest start of `key` that none of its `siblings` share, which is how a path stays short and readable. */
function shortestPrefix(key, siblings = []) {
for (let length = 1; length < key.length; length++) {
const prefix = key.slice(0, length);
if (!siblings.some(other => other !== key && other.startsWith(prefix))) {
return prefix;
}
}
return key;
}
/**
* The full path a possibly shortened one names, walking `within` a segment at a time: a segment that names a
* key outright is that key, otherwise it has to start exactly one of them.
*/
function expandPath(path, within) {
const full = [];
let at = within;
for (const segment of path.split('.')) {
if (at === null || typeof at !== 'object') {
return undefined;
}
const keys = Object.keys(at);
const key = keys.includes(segment) ? segment : keys.filter(other => other.startsWith(segment));
if (typeof key !== 'string' && key.length !== 1) {
return undefined;
}
const found = typeof key === 'string' ? key : key[0];
full.push(found);
at = at[found];
}
return full.length > 0 ? full.join('.') : undefined;
}
exports.FlowrConfig = {
name: 'FlowrConfig',
/**
* The default configuration for flowR, used when no config file is found or when a config file is missing some options.
* You can use this as a base for your own config and only specify the options you want to change.
*/
default() {
return {
ignoreSourceCalls: false,
ignoreLoadCalls: false,
semantics: {
environment: {
overwriteBuiltIns: {
loadDefaults: true,
definitions: []
}
}
},
defaultPlugins: exports.FlowrDefaultPlugins,
repl: {
quickStats: false,
dfProcessorHeat: false,
hints: true,
plugins: ['flowr:default'],
autoUseFileProtocol: true,
queryStats: true,
showPlugins: false,
},
project: {
resolveUnknownPathsOnDisk: true,
failOnInaccessiblePath: false
},
linter: {
disabledRules: []
},
specializeConfig: {
[project_kind_1.ProjectKind.Package]: { solver: { resolveSource: { assumeFilesExist: true } } },
[project_kind_1.ProjectKind.Project]: { solver: { resolveSource: { assumeFilesExist: true } } },
[project_kind_1.ProjectKind.ShinyApp]: {
/* shiny evaluates global.R before the supporting files in R/, and the app itself last */
project: { implicitSources: ['global.R', 'R/*.R', 'ui.R', 'server.R', 'app.R'] },
solver: { resolveSource: { assumeFilesExist: true } }
},
[project_kind_1.ProjectKind.Script]: { inherit: project_kind_1.ProjectKind.Unknown },
[project_kind_1.ProjectKind.Notebook]: { inherit: project_kind_1.ProjectKind.Unknown },
[project_kind_1.ProjectKind.Unknown]: { linter: { disabledRules: ['software-has-license', 'software-has-tests'] } }
},
engines: [],
defaultEngine: 'tree-sitter',
solver: {
variables: VariableResolve.Alias,
evalStrings: true,
trackEnvironments: true,
sigdb: { enabled: true, loadProjectDependencies: true, eagerlyLoad: false, eagerlyLoadExports: false, assumedRVersion: 'auto', linkBaseR: false, linkDescriptionDependencies: false, linkBaseRCalls: false, linkPackageCalls: false, warmInBackground: false, additionalPaths: [], autoSync: false, versionSelection: VersionSelection.Newest, versionOverrides: {}, installedLibrary: { enabled: false, paths: [], useEnvironment: true, useProjectLibrary: true, maxDepth: 3, packages: [] } },
versionManagement: { linkedVersionGroups: [] },
resolveSource: {
dropPaths: DropPathsOption.No,
ignoreCapitalization: true,
inferWorkingDirectory: InferWorkingDirectory.ActiveScript,
searchPath: [],
repeatedSourceLimit: 2,
assumeFilesExist: false
},
instrument: {
dataflowExtractors: undefined
},
slicer: {
threshold: 50,
autoExtend: false
}
},
abstractInterpretation: {
wideningThreshold: 4,
dataFrame: {
maxColNames: 50,
readLoadedData: {
readExternalFiles: true,
maxReadLines: 1e6
}
}
},
incremental: {
alwaysIncremental: false,
parsing: {
activated: false,
heuristics: {
activated: true,
mtime: true,
linesFrom: 500,
bytesFrom: 50_000,
alwaysWithEdits: false,
minFiles: 1,
}
}
},
gas: {
thresholds: {
memory: { problematic: 0.7, critical: 0.9 },
timeMs: { problematic: 100_000, critical: 120_000 }
},
features: {}
}
};
},
/**
* The Joi schema for validating a config file, use this to validate your config file before using it. You can also use this to generate documentation for the config file format.
*/
Schema: joi_1.default.object({
logLevel: joi_1.default.string().valid(...Object.keys(log_1.LogLevelNames)).optional().description('flowR\'s global minimum log level, applied when the config is loaded.'),
ignoreSourceCalls: joi_1.default.boolean().optional().description('Whether source calls should be ignored, causing {@link processSourceCall}\'s behavior to be skipped.'),
ignoreLoadCalls: joi_1.default.boolean().optional().description('Whether load calls should be ignored, causing {@link processLoadCall}\'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.'),
defaultPlugins: joi_1.default.array().items(joi_1.default.alternatives().try(joi_1.default.string(), joi_1.default.array().ordered(joi_1.default.string(), joi_1.default.array().items(joi_1.default.any())).length(2))).optional().description('The default plugins to load when creating a new instance of FlowrAnalyzer'),
repl: joi_1.default.object({
quickStats: joi_1.default.boolean().optional().description('Whether to show quick stats in the REPL after each evaluation.'),
dfProcessorHeat: joi_1.default.boolean().optional().description('This instruments the dataflow processors to count how often each processor is called.'),
hints: joi_1.default.boolean().optional().description('Whether to show dim inline hints on the empty prompt (automatically disabled on non-interactive terminals).'),
plugins: joi_1.default.array().items(joi_1.default.alternatives().try(joi_1.default.string(), joi_1.default.array().ordered(joi_1.default.string(), joi_1.default.array().items(joi_1.default.any())).length(2))).optional().description('The plugins to load in REPL mode'),
autoUseFileProtocol: joi_1.default.boolean().optional().description('Prepend the file protocol to a repl input that looks like a path, instead of only warning about it.'),
queryStats: joi_1.default.boolean().optional().description('Whether `:query` closes with the line stating how long the queries took (`:query*` never prints it).'),
showPlugins: joi_1.default.boolean().optional().description('Whether `:version` grays out the plugins that did not activate during the last analysis.')
}).description('Configuration options for the REPL.'),
project: joi_1.default.object({
resolveUnknownPathsOnDisk: joi_1.default.boolean().optional().description('Whether to resolve unknown paths loaded by the r project disk when trying to source/analyze files.'),
failOnInaccessiblePath: joi_1.default.boolean().optional().description('Whether a directory that cannot be traversed during file discovery (e.g. due to permissions) aborts the analysis; when false (the default) such paths are logged and skipped.'),
basePackages: joi_1.default.array().items(joi_1.default.string()).optional().description('The packages considered part of R itself (base and recommended); if unset, flowR uses its built-in list.'),
implicitSources: joi_1.default.array().items(joi_1.default.string()).optional().description('Files a framework loads on its own, without any source() call (e.g. global.R in a shiny app), in the order they are loaded; flowR orders the matching project files accordingly and analyzes them as one program. Entries are case-insensitive globs matched against the file path, a plain name matches any file with that name, and entries matching no project file are warned about. Usually set per project kind via specializeConfig.'),
useProjectType: joi_1.default.string().valid(...Object.values(project_kind_1.ProjectKind)).optional().description('Overwrite the project kind flowR would otherwise infer from the analyzed files, e.g. when auto-detection guesses wrong.'),
discovery: joi_1.default.object({
full: joi_1.default.boolean().optional().description('Collect every file below the project root (greedy) instead of only the files the detected project kind needs (default false).'),
perKind: joi_1.default.object().pattern(joi_1.default.string().valid(...Object.values(project_kind_1.ProjectKind)), joi_1.default.object({
include: joi_1.default.array().items(joi_1.default.string()).optional(),
exclude: joi_1.default.array().items(joi_1.default.string()).optional()
})).optional().description('Per-kind include/exclude glob overrides layered on the default scoping.'),
ignore: joi_1.default.array().items(joi_1.default.string()).optional().description('Case-insensitive globs that drop matching files from the intelligent discovery, regardless of kind (e.g. .Renviron to ignore environment files).')
}).optional().description('Scoping options for the default project discovery.'),
classification: joi_1.default.object({
shinyDescriptionTypes: joi_1.default.array().items(joi_1.default.string()).optional().description('DESCRIPTION Type: values that mark a shiny app.'),
shinyEntryFiles: joi_1.default.array().items(joi_1.default.string()).optional().description('File names a shiny app is assembled from.'),
shinyUsagePattern: joi_1.default.string().optional().description('Regex source evidencing shiny usage in an entry file.'),
notebookExtensions: joi_1.default.array().items(joi_1.default.string()).optional().description('File extensions marking a notebook.')
}).optional().description('Overrides for the signals flowR uses to classify the project kind.')
}).description('Project specific configuration options.'),
linter: joi_1.default.object({
disabledRules: joi_1.default.array().items(joi_1.default.string()).description('Linting rule names excluded from the default rule set (a rule requested explicitly via a linter query still runs). Usually set per project kind via specializeConfig.')
}).description('Linter configuration options.'),
inputSources: joi_1.default.object({
pure: joi_1.default.array().items(joi_1.default.string()).optional().description('Functions that only pass the constantness of their arguments on.'),
...Object.fromEntries(Object.values(input_types_1.InputType).map(t => [t, joi_1.default.array().items(joi_1.default.string()).optional().description(`Functions whose result is a '${t}' input.`)])),
linkedObjects: joi_1.default.array().items(joi_1.default.object()).optional().description('Objects a framework provides without a definition in the code, e.g. shiny\'s input.'),
linkedEntryPoints: joi_1.default.array().items(joi_1.default.object()).optional().description('Calls that hand a function to a framework, which binds its objects to the parameters by position.')
}).optional().description('Further frameworks the input-sources analysis should know about; entries are added to flowR\'s defaults.'),
specializeConfig: joi_1.default.object().pattern(joi_1.default.string().valid(...Object.values(project_kind_1.ProjectKind)), joi_1.default.object({
inherit: joi_1.default.string().valid(...Object.values(project_kind_1.ProjectKind)).optional().description('Inherit another kind\'s overwrite first (merged before this entry\'s own keys, which win).')
}).unknown(true)).optional()
.description('Overwrite (parts of) the configuration depending on the project kind flowR detects, e.g. to give a shiny app its implicit sources.'),
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.'))).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?'),
trackEnvironments: joi_1.default.boolean().optional().description('Track user-created environments (new.env, assign/get/local with envir=, dollar-assign, attach). When false, all envir-style calls fall through conservatively.'),
sigdb: joi_1.default.object({
enabled: joi_1.default.boolean().optional().description('Resolve library()/use() exports from a signature database (default true); when false no database is consulted.'),
loadProjectDependencies: joi_1.default.boolean().optional().description('Load the project\'s declared dependencies from its metadata files (DESCRIPTION Imports/Depends, rproject.toml, uvr.toml, renv.lock, rv.lock, uvr.lock) (default true); when false these files are not read for dependencies.'),
eagerlyLoad: joi_1.default.boolean().optional().description('Parse the database up front rather than on the first package load (default false, ignored if disabled).'),
eagerlyLoadExports: joi_1.default.boolean().optional().description('Add a vertex for every export on load rather than on demand (default false); keeps the dataflow graph small.'),
assumedRVersion: joi_1.default.string().optional().description('R version assumed when resolving versioned (base-R) exports: a pin like "4.5" or "auto" to detect the installed R (default "auto").'),
linkBaseR: joi_1.default.boolean().optional().description('Eagerly attach base-R namespaces so bare base calls resolve without library() (default false).'),
linkBaseRCalls: joi_1.default.boolean().optional().description('Add a lightweight Reads edge from a bare base-R call to its signature-database function vertex (default false; base-R qualification is edge-free otherwise).'),
linkPackageCalls: joi_1.default.boolean().optional().description('Add a lightweight Reads edge from a resolved package call to its signature-database function vertex (default false).'),
linkDescriptionDependencies: joi_1.default.boolean().optional().description('Eagerly attach the namespaces of the project\'s declared DESCRIPTION dependencies (Imports/Depends) so their exports resolve without an explicit library() (default false).'),
warmInBackground: joi_1.default.boolean().optional().description('Decompress the hot shards (base + most-downloaded) in a background task on startup so the first library() lookup is warm (default false; for long-running servers/REPLs).'),
additionalPaths: joi_1.default.array().items(joi_1.default.string()).optional().description('Extra directories or bundle/manifest files searched for signature databases (alongside the shipped default and $FLOWR_SIGDB_DIR); a downloaded full-history bundle placed here is mounted automatically.'),
downloadRepo: joi_1.default.string().optional().description('GitHub owner/repo the full-history bundle is downloaded from via ":signature download" (default "flowr-analysis/flowr", release tag "sigdb-v<flowR-version>").'),
autoSync: joi_1.default.boolean().optional().description('On startup, re-download shards whose committed sigdb.remote.json hash no longer matches the cache, in the background (default false; opt-in network sync after a git pull).'),
installedLibrary: joi_1.default.object({
enabled: joi_1.default.boolean().required().description('Recover packages no signature database knows from their installed copy (default false).'),
paths: joi_1.default.array().items(joi_1.default.string()).optional().description('Library directories to search; when empty they are discovered from the environment and the project.'),
useEnvironment: joi_1.default.boolean().optional().description('Search the libraries R_LIBS_USER/R_LIBS/R_LIBS_SITE name (default true).'),
useProjectLibrary: joi_1.default.boolean().optional().description('Search a project-local renv/packrat library (default true).'),
maxDepth: joi_1.default.number().optional().description('How far to descend into the nested layout of a project-local library (default 3).'),
packages: joi_1.default.array().items(joi_1.default.string()).optional().description('Only recover packages whose name matches one of these regular expressions; empty means any.')
}).optional().description('Recovering packages no signature database knows from the copy installed on this machine.'),
versionSelection: joi_1.default.string().valid(...Object.values(VersionSelection)).optional().description('When a project constrains a dependency, resolve to the newest (default), oldest, or system-installed version satisfying it; system needs R and falls back to newest. Base-R packages always resolve against the assumed R version.'),
versionOverrides: joi_1.default.object().pattern(joi_1.default.string(), joi_1.default.string()).optional().description('Force an exact version for specific packages (name -> version), overriding both the project constraint and the versionSelection policy (default {}).')
}).description('Resolving library exports from a signature database.'),
versionManagement: joi_1.default.object({
linkedVersionGroups: joi_1.default.array().items(joi_1.default.array().items(joi_1.default.string())).optional().description('Groups of packages that must resolve to the same version; version guessing intersects each group so its members stay mutually compatible (default []).')
}).description('Policies for reasoning about dependency versions.'),
instrument: joi_1.default.object({
dataflowExtractors: joi_1.default.any().optional().description('These keys are only intended for use within code, allowing to instrument the dataflow analyzer!')
}),
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'),
assumeFilesExist: joi_1.default.boolean().optional().description('Assume a sourced file is always there, making what it defines certain instead of conditional on the source call.')
}).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.'),
autoExtend: joi_1.default.boolean().optional().description('If set, the slicer will gain an additional post-pass.')
}).optional().description('The configuration for the slicer.')
}).description('How to resolve constants, constraints, cells, ...'),
abstractInterpretation: joi_1.default.object({
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.'),
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.'),
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.'),
incremental: joi_1.default.object({
alwaysIncremental: joi_1.default.boolean().description('Always take the incremental path, regardless of heuristics.'),
parsing: joi_1.default.object({
activated: joi_1.default.boolean().description('If set, incremental parsing will be used.'),
heuristics: joi_1.default.object({
activated: joi_1.default.boolean().optional().description('If set, the heuristics for incremental parsing will be used.'),
mtime: joi_1.default.boolean().optional().description('Skip reparsing entirely if the file\'s modification time is unchanged since the last parse.'),
linesFrom: joi_1.default.number().min(0).optional().description('Only consider incremental parsing for files with at least this many lines.'),
bytesFrom: joi_1.default.number().min(0).optional().description('Only consider incremental parsing for files with at least this many bytes.'),
alwaysWithEdits: joi_1.default.boolean().optional().description('Always take the incremental path whenever there is a computed edit region, regardless of the other thresholds.'),
minFiles: joi_1.default.number().min(1).optional().description('Only apply these heuristics once the project has at least this many files loaded.'),
}),
}),
}),
gas: joi_1.default.object({
thresholds: joi_1.default.object({
memory: joi_1.default.object({
problematic: joi_1.default.number().min(0).max(1).optional().description('Heap fraction (0-1) above which Problematic is returned, for every feature without an entry of its own.'),
critical: joi_1.default.number().min(0).max(1).optional().description('Heap fraction (0-1) above which Critical is returned, for every feature without an entry of its own.')
}).pattern(joi_1.default.string(), joi_1.default.object({
problematic: joi_1.default.number().min(0).max(1).optional().description('Heap fraction (0-1) above which Problematic is returned for this feature.'),
critical: joi_1.default.number().min(0).max(1).optional().description('Heap fraction (0-1) above which Critical is returned for this feature.')
})).optional().description('Heap-usage fraction thresholds (0-1), either shared or given per feature key (with `default` covering the rest).'),
timeMs: joi_1.default.object({
problematic: joi_1.default.number().min(0).optional().description('Elapsed ms above which Problematic is returned, for every feature without an entry of its own.'),
critical: joi_1.default.number().min(0).optional().description('Elapsed ms above which Critical is returned, for every feature without an entry of its own.')
}).pattern(joi_1.default.string(), joi_1.default.object({
problematic: joi_1.default.number().min(0).optional().description('Elapsed ms above which Problematic is returned for this feature.'),
critical: joi_1.default.number().min(0).optional().description('Elapsed ms above which Critical is returned for this feature.')
})).optional().description('Elapsed analysis time thresholds in milliseconds, either shared or given per feature key (with `default` covering the rest).')
}).optional().description('Thresholds for all gas checks (scaled by per-feature factor), boundable per feature.'),
features: joi_1.default.object().pattern(joi_1.default.string(), joi_1.default.number().min(0).optional()).optional().description('Per-feature sensitivity factors. 0 or absent disables gas checking for that feature. A factor of 2 makes the feature twice as sensitive. Recognised keys: `source`, `side-effect-linking`, `linter`, `slicer`.'),
heapProvider: joi_1.default.function().optional().description('Custom heap statistics source (programmatic configs only), overriding the built-in v8/performance.memory detection.')
}).optional().description(`Resource-usage guard (gas) configuration. All feature factors default to 0 (disabled). See ${gas_1.GasWikiRef}.`)
}).description('The configuration file format for flowR.'),
/**
* Parses the given JSON string as a flowR config file, returning the resulting config object if the parsing and validation were successful, or `undefined` if there was an error.
*/
parse(jsonString) {
try {
const parsed = JSON.parse(jsonString);
const validate = exports.FlowrConfig.Schema.validate(parsed);
if (!validate.error) {
// assign default values to all config options except for the specified ones
return mergeConfigOntoDefaults(exports.FlowrConfig.default(), 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.
*/
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
amend(config, amendmentFunc) {
const newConfig = exports.FlowrConfig.clone(config);
return amendmentFunc(newConfig) ?? newConfig;
},
/**
* Clones the given flowr config object.
*/
clone(config) {
return (0, objects_1.deepClonePreserveUnclonable)(config);
},
/**
* Loads the flowr config from the given file or the default locations.
* Please note that you can also use this without a path parameter to
* infer the config from flowR's default locations.
* This is mostly useful for user-facing features.
*/
fromFile(configFile, configWorkingDirectory = process.cwd()) {
let config;
try {
config = loadConfigFromFile(configFile, configWorkingDirectory);
}
catch (e) {
log_1.log.error(`Failed to load config: ${e.message}`);
config = exports.FlowrConfig.default();
}
if (config.logLevel !== undefined) {
(0, log_1.setLogLevel)(config.logLevel);
}
return config;
},
/**
* Resolves the configuration for the given {@link ProjectKind} by applying the matching
* {@link FlowrConfig.specializeConfig} entry to every key it names. What you configured wins over the entry, which in
* turn wins over flowR's default; a value that differs from the default counts as configured.
* Returns `config` itself if the kind has no entry, so callers can use this freely.
*/
forKind(config, kind) {
const overwrite = exports.FlowrConfig.specializationFor(config, kind);
return overwrite ? specialize(config, exports.FlowrConfig.default(), overwrite) : config;
},
/** The overwrite {@link FlowrConfig.forKind} applies for `kind` (with {@link SpecializeConfigEntry.inherit} resolved), or `undefined`. */
specializationFor(config, kind) {
return resolveSpecialization(config.specializeConfig, kind);
},
/**
* Gets the configuration for the given engine type from the config.
*/
getForEngine(config, engine) {
const engines = config.engines;
if (engines.length > 0) {
return engines.find(e => e.type === engine);
}
else {
return defaultEngineConfigs[engine];
}
},
/**
* Every key `config` changed against the {@link FlowrConfig.default|default}, as `a.b.c=<json>` lines.
* A whole configuration is a long document while an edit to it is usually one line, which is what makes
* this the form to hand around: a link, a command line, a bug report.
* @see {@link FlowrConfig.applyPaths} - to read them back
* @example
* ```ts
* FlowrConfig.changedPaths(config); // ['solver.sigdb.enabled=false']
* ```
*/
changedPaths(config) {
const found = [];
const walk = (current, base, path, siblings) => {
if (current !== null && base !== null && typeof current === 'object' && typeof base === 'object'
&& !Array.isArray(current) && !Array.isArray(base)) {
const keys = [...new Set([...Object.keys(current), ...Object.keys(base)])];
for (const key of keys) {
walk(current[key], base[key], [...path, key], [...siblings, keys]);
}
}
else if (JSON.stringify(current) !== JSON.stringify(base)) {
const short = path.map((key, at) => shortestPrefix(key, siblings[at]));
found.push(`${short.join('.')}=${JSON.stringify(current)}`);
}
};
walk(config, exports.FlowrConfig.default(), [], []);
return found;
},
/**
* The inverse of {@link FlowrConfig.changedPaths}: the default configuration with those keys set again.
* A line that names no known key, or whose value is not readable, is skipped rather than guessed at.
*/
applyPaths(paths, base = exports.FlowrConfig.default()) {
const config = structuredClone(base);
for (const line of paths) {
const at = line.indexOf('=');
const key = at < 0 ? undefined : expandPath(line.slice(0, at), config);
if (key === undefined) {
continue;
}
try {
object_path_1.default.set(config, key, JSON.parse(line.slice(at + 1)));
}
catch { /* a value nobody can read is one to leave alone */ }
}
return config;
},
/**
* Returns a new config object with the given value set at the given key, where the key is a dot-separated path to the value in the config object.
* @see {@link setInConfigInPlace} for a version that modifies the config object in place instead of returning a new one.
* @example
* ```ts
* const config = FlowrConfig.default();
* const newConfig = FlowrConfig.setInConfig(config, 'solver.variables', VariableResolve.Builtin);
* console.log(config.solver.variables); // Output: "alias"
* console.log(newConfig.solver.variables); // Output: "builtin"
* ```
*/
setInConfig(config, key, value) {
const clone = exports.FlowrConfig.clone(config);
object_path_1.default.set(clone, key, value);
return clone;
},
/**
* Modifies the given config object in place by setting the given value at the given key, where the key is a dot-separated path to the value in the config object.
* @see {@link setInConfig} for a version that returns a new config object instead of modifying the given one in place.
*/
setInConfigInPlace(config, key, value) {
object_path_1.default.set(config, key, value);
},
};
/** Path to the user-global `flowr.json`, read as a fallback when no project config is found. */
function globalConfigFilePath() {
const base = process.env.FLOWR_CONFIG_HOME
?? (process.env.XDG_CONFIG_HOME ? path_1.default.join(process.env.XDG_CONFIG_HOME, 'flowr') : undefined)
?? (process.env.APPDATA ? path_1.default.join(process.env.APPDATA, 'flowr') : undefined)
?? path_1.default.join(os_1.default.homedir?.() || os_1.default.tmpdir(), '.config', 'flowr');
return path_1.default.join(base, 'flowr.json');
}
/** Persist `dbPath` into `solver.sigdb.additionalPaths` in the global config (creating it, preserving the user's raw JSON); idempotent, returns the file written. */
function persistSigDbPathToGlobalConfig(dbPath) {
const file = globalConfigFilePath();
let raw = {};
try {
raw = fs_1.default.existsSync(file) ? JSON.parse(fs_1.default.readFileSync(file, 'utf-8')) : {};
}
catch (e) {
log_1.log.warn(`Could not read global config ${file}, recreating it: ${e.message}`);
}
const current = object_path_1.default.get(raw, 'solver.sigdb.additionalPaths');
const paths = Array.isArray(current) ? current : [];
object_path_1.default.set(raw, 'solver.sigdb.additionalPaths', [...new Set([...paths, dbPath])]);
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
fs_1.default.writeFileSync(file, JSON.stringify(raw, null, '\t') + '\n');
return file;
}
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 = exports.FlowrConfig.parse(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 = exports.FlowrConfig.parse(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));
}
const global = globalConfigFilePath();
if (fs_1.default.existsSync(global)) {
const ret = exports.FlowrConfig.parse(fs_1.default.readFileSync(global, { encoding: 'utf-8' }));
if (ret) {
log_1.log.info(`Using global config ${global}`);
return ret;
}
}
log_1.log.info('Using default config');
return exports.FlowrConfig.default();
}
//# sourceMappingURL=config.js.map