UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

702 lines 47.8 kB
"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