@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
123 lines • 6.75 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PasteLikeCalls = exports.StringFns = void 0;
exports.foldStringCall = foldStringCall;
exports.resolveAsStringFn = resolveAsStringFn;
const type_1 = require("../../../r-bridge/lang-4.x/ast/model/type");
const identifier_1 = require("../../environments/identifier");
const r_value_1 = require("../values/r-value");
const string_constants_1 = require("../values/string/string-constants");
const interval_constants_1 = require("../values/intervals/interval-constants");
const match_arguments_1 = require("./match-arguments");
const resolve_helper_1 = require("../../environments/resolve-helper");
/** everything after the last separator, with trailing separators dropped first (`a/b/` is `b`, `/` is the empty string) */
function basename(path) {
const trimmed = path.replace(/\/+$/, '');
return trimmed.slice(trimmed.lastIndexOf('/') + 1);
}
/** everything before the last separator, `.` if there is none and `/` if only the root remains */
function dirname(path) {
if (path === '') {
return '';
}
const trimmed = path.length > 1 ? path.replace(/(?<=.)\/+$/, '') : path;
const cut = trimmed.lastIndexOf('/');
if (cut < 0) {
return '.';
}
const head = trimmed.slice(0, cut).replace(/\/+$/, '');
return head === '' ? '/' : head;
}
/**
* Every string built-in the value solver folds, the joining ones included: `paste` is an entry like `toupper`
* is, and both are reached through {@link resolveAsStringFn}. Teaching flowR one more is a line here plus the
* matching `evalHandler` in the built-in configuration -- a test checks that the two agree.
*
* An entry with a `...` parameter joins what it collected, which is why its separator is just another
* parameter with a default. The rest take a fixed number of arguments under the names R documents.
*/
exports.StringFns = {
/* the joining calls, which differ only in their separator and what the argument overriding it is called */
paste: { pkg: "base" /* PkgName.Base */, params: ['...', 'sep'], defaults: { sep: ' ' }, ignored: ['collapse'], fold: (parts, sep) => parts.join(sep) },
paste0: { pkg: "base" /* PkgName.Base */, params: ['...', 'sep'], defaults: { sep: '' }, ignored: ['collapse'], fold: (parts, sep) => parts.join(sep) },
'file.path': { pkg: "base" /* PkgName.Base */, params: ['...', 'fsep'], defaults: { fsep: '/' }, fold: (parts, fsep) => parts.join(fsep) },
/* the project root stays implicit, so the fold yields the path below it */
here: { pkg: "here" /* PkgName.Here */, params: ['...'], fold: (parts) => parts.length > 0 ? parts.join('/') : '.' },
/* the path splits, which only ever treat `/` as a separator, so a Windows path keeps its non-`\` parts */
basename: { pkg: "base" /* PkgName.Base */, params: ['path'], fold: basename },
dirname: { pkg: "base" /* PkgName.Base */, params: ['path'], fold: dirname },
/* whole-string transformations */
toupper: { pkg: "base" /* PkgName.Base */, params: ['x'], fold: (s) => s.toUpperCase() },
tolower: { pkg: "base" /* PkgName.Base */, params: ['x'], fold: (s) => s.toLowerCase() },
trimws: { pkg: "base" /* PkgName.Base */, params: ['x'], fold: (s) => s.trim() },
/** R counts characters, so we count code points rather than UTF-16 units */
nchar: { pkg: "base" /* PkgName.Base */, params: ['x'], fold: (s) => [...s].length }
};
/** the entries that join what they are handed, the ones a name at construction time may be built from */
exports.PasteLikeCalls = new Set(Object.entries(exports.StringFns).filter(([, fn]) => fn.params.includes('...')).map(([name]) => name));
/**
* Folds a named {@link StringFns} call to its result, resolving each argument with `resolveArg`. `undefined`
* when the name is not one of them, an argument does not resolve, or the call does not match what the entry
* declares. Shared by the value solver ({@link resolveAsStringFn}) and construction-time name resolution.
*/
function foldStringCall(node, resolveArg) {
const known = exports.StringFns[identifier_1.Identifier.getName(node.functionName.content)];
if (known === undefined) {
return undefined;
}
/* `dplyr::paste` is not `base::paste`; a bare call has already been resolved */
const ns = identifier_1.Identifier.getNamespace(node.functionName.content);
if (ns !== undefined && ns !== known.pkg) {
return undefined;
}
const matched = (0, match_arguments_1.matchCallArguments)(node, known.params, known.ignored);
if (matched === undefined) {
return undefined;
}
/* a fold that reads the characters cannot run on source text with an escape still in it (`\t` is two chars
* there); joining does not read them, so the entries collecting a `...` are exempt */
const literal = !known.params.includes('...');
const args = [];
for (const [at, slot] of matched.entries()) {
if (Array.isArray(slot)) {
const parts = slot.map(resolveArg);
if (parts.includes(undefined)) {
return undefined;
}
args.push(parts);
continue;
}
if (slot === undefined) {
const fallback = known.defaults?.[known.params[at]];
if (fallback === undefined) {
break; // the parameters R gives a default are the trailing ones, so the fold sees a shorter prefix
}
args.push(fallback);
continue;
}
// the argument *was* given, so failing to resolve it means we do not know the result, defaults do not apply
const value = resolveArg(slot);
if (value === undefined || (literal && value.includes('\\'))) {
return undefined;
}
args.push(value);
}
return args.length > 0 ? known.fold(...args) : undefined;
}
/**
* Resolves any call of a {@link StringFns} entry to a {@link Value}, with its arguments in any order R accepts:
* a join like `paste0("cfg_", k)` when every part resolves to a single string constant, and a transformation
* like `basename(p)` when its argument does. Anything that does not resolve stays `Top`.
*/
function resolveAsStringFn(args) {
const node = args.node;
if (node.type !== type_1.RType.FunctionCall || !node.named) {
return r_value_1.Top;
}
const folded = foldStringCall(node, arg => resolve_helper_1.Resolve.toSingleString(arg.info.id, args));
if (folded === undefined) {
return r_value_1.Top;
}
return typeof folded === 'number' ? (0, interval_constants_1.intervalFrom)(folded, folded) : (0, string_constants_1.stringFrom)(folded);
}
//# sourceMappingURL=resolve-strings.js.map