UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

262 lines 12.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CriteriaParseError = exports.SlicingCriteria = exports.SlicingCriterion = void 0; const range_1 = require("../../util/range"); const log_1 = require("../../util/log"); const node_id_1 = require("../../r-bridge/lang-4.x/ast/model/processing/node-id"); const static_slicer_1 = require("../static/static-slicer"); const type_1 = require("../../r-bridge/lang-4.x/ast/model/type"); /** * The helper object associated with {@link SlicingCriterion} which makes it easy * to parse, validate and resolve slicing criteria. */ exports.SlicingCriterion = { name: 'SlicingCriterion', /** * Checks whether a value has a valid slicing criterion syntax. * This does not check whether the slicing criterion exists (represents a valid node ID). * @see {@link SlicingCriterion.parse} to parse a slicing criterion to a node ID */ isValid(criterion) { if (typeof criterion !== 'string') { return false; } else if (criterion.startsWith('$')) { return criterion.length > 1; } /* the file filter is optional on every form, so validate what remains once it is split off */ const split = splitFileFilter(criterion); return split !== undefined && /^-?\d+([:~]\d+|\^|@.+)$/.test(split.rest); }, /** * Resolves a slicing criterion to the corresponding node id. * @see {@link SlicingCriterion.tryParse} for a version that does not throw an error */ parse(criterion, idMap) { const resolved = exports.SlicingCriterion.tryParse(criterion, idMap); if (resolved === undefined) { throw new CriteriaParseError(`invalid slicing criterion ${criterion}`); } return resolved; }, /** * Tries to resolve a slicing criterion to an id, but does not throw an error if it fails. * The formats and what each of them resolves to are documented in the * {@link https://github.com/flowr-analysis/flowr/wiki/Terminology#slicing-criterion|wiki}. * @see {@link SlicingCriterion.parse} for the version that throws an error */ tryParse(criterion, idMap) { criterion = criterion.toString(); // in case it's a number if (criterion.startsWith('$')) { return node_id_1.NodeId.normalize(criterion.slice(1)); } const split = splitFileFilter(criterion); if (split === undefined) { return undefined; // a malformed file filter must not silently resolve without it } const { rest: base, file } = split; if (base.includes('@')) { const at = base.indexOf('@'); const line = parseLineNumber(base.slice(0, Math.max(0, at)), idMap, file); const name = base.slice(Math.max(0, at + 1)); if (line === undefined || name.length === 0) { return undefined; } // an optional `[n]` prefix picks the n-th occurrence within the line (`2@[2]a`, `2@[-1]a` for the last one) const nth = /^\[(-?\d+)](.+)$/.exec(name); return nth ? nthOccurrenceToId(line, nth[2], idMap, parseInt(nth[1]), file) : nthOccurrenceToId(line, name, idMap, 1, file); } else if (base.includes(':')) { const location = parseLocation(base, ':', idMap, file); return location && locationToId(location, idMap, file); } else if (base.includes('~')) { const location = parseLocation(base, '~', idMap, file); return location && fuzzyLocationToId(location, idMap, file); } else if (base.endsWith('^')) { const line = parseLineNumber(base.slice(0, -1), idMap, file); return line === undefined ? undefined : topLevelStatementToId(line, idMap, file); } }, /** * Converts a node id to a slicing criterion in the form of `$id` */ fromId(id) { return `$${id}`; } }; /** * The helper object associated with {@link SlicingCriteria} which makes it easy to parse, validate and resolve slicing criteria. */ exports.SlicingCriteria = { name: 'SlicingCriteria', /** * Decodes all slicing criteria to their corresponding node ids * @throws CriteriaParseError if any of the criteria can not be resolved * @see {@link SlicingCriteria.convertAll} */ decodeAll(criteria, decorated) { return criteria.map(l => ({ criterion: l, id: exports.SlicingCriterion.parse(l, decorated) })); }, /** * Converts all criteria to their id in the AST if possible, this keeps the original criterion if it can not be resolved. * @see {@link SlicingCriteria.decodeAll} */ convertAll(criteria, decorated) { return criteria.map(l => exports.SlicingCriterion.tryParse(l, decorated) ?? l); } }; /** * Thrown if the given slicing criteria can not be found */ class CriteriaParseError extends Error { constructor(message) { super(message); this.name = 'CriteriaParseError'; } } exports.CriteriaParseError = CriteriaParseError; function locationToId(location, dataflowIdMap, file) { let candidate; for (const [id, nodeInfo] of dataflowIdMap.entries()) { if (nodeInfo.location === undefined || nodeInfo.location[0] !== location[0] || nodeInfo.location[1] !== location[1] || !matchesFile(nodeInfo, file)) { continue; // only consider those with position information } (0, log_1.expensiveTrace)(static_slicer_1.slicerLogger, () => `can resolve id ${id} (${JSON.stringify(nodeInfo.location)}) for location ${JSON.stringify(location)}`); // function calls have the same location as the symbol they refer to, so we need to prefer the function call if (candidate !== undefined && nodeInfo.type !== type_1.RType.FunctionCall || nodeInfo.type === type_1.RType.Argument || nodeInfo.type === type_1.RType.ExpressionList) { continue; } candidate = nodeInfo; } return candidate?.info.id; } /** * Resolves a `line~column` criterion: unlike {@link locationToId}, which wants a node *starting* exactly there, * this accepts any node whose source range *contains* the position and returns the innermost of them (the same * matching the search API performs for `fuzzy` with `innermostOnly`). */ function fuzzyLocationToId(location, dataflowIdMap, file) { const potentials = [...dataflowIdMap.values()].filter(nodeInfo => // arguments and expression lists only wrap their content, sharing its range, so they never say more than it nodeInfo.type !== type_1.RType.Argument && nodeInfo.type !== type_1.RType.ExpressionList && matchesFile(nodeInfo, file)); /* a call shares its range with the symbol naming it, so keep both (`treatChildAsInner: false`) and let the * preference below decide, rather than always landing on the symbol */ const candidates = range_1.SourceRange.innermostNodes(range_1.SourceRange.nodesContaining(potentials, location[0], location[1]), false); // prefer the call over the symbol it refers to, exactly as locationToId does return (candidates.find(n => n.type === type_1.RType.FunctionCall) ?? candidates[0])?.info.id; } /** Walks up to the statement `node` belongs to: the outermost node still below the root of its file. */ function enclosingTopLevelStatement(node, idMap) { let current = node; for (;;) { const parent = current.info.parent !== undefined ? idMap.get(current.info.parent) : undefined; if (parent === undefined || parent.info.parent === undefined) { return current; } current = parent; } } /** * Resolves a `line^` criterion: the top-level statement covering the line, which is what has to be excised to * remove that line from the program. Unlike {@link fuzzyLocationToId} this widens rather than narrows, so an * inner sub-expression on the line never stands in for the statement carrying it. */ function topLevelStatementToId(line, idMap, file) { const potentials = [...idMap.values()].filter(nodeInfo => matchesFile(nodeInfo, file)); let best; let bestRange; for (const node of range_1.SourceRange.nodesContaining(potentials, line)) { const statement = enclosingTopLevelStatement(node, idMap); const range = range_1.SourceRange.fromNode(statement); /* several statements may cover the line (`a <- 1; b <- 2`), the one starting first wins */ if (range !== undefined && (bestRange === undefined || range_1.SourceRange.compare(range, bestRange) < 0)) { best = statement; bestRange = range; } } return best?.info.id; } /** * Splits the optional trailing `(file-regex)` off a criterion (e.g. `2@x(tmp/.*)`), which restricts it to nodes * originating from a matching file. The regex may contain escaped parentheses (`3^(a\(b\)\.R)`). * Returns `undefined` if the regex is malformed. */ function splitFileFilter(criterion) { const match = /^([^()]*)\(((?:\\.|[^()])*)\)$/.exec(criterion); if (match === null) { return { rest: criterion, file: undefined }; } try { return { rest: match[1], file: new RegExp(match[2]) }; } catch { return undefined; } } /** Whether the node stems from a file matching the criterion's `(file-regex)` suffix (if any). */ function matchesFile(nodeInfo, file) { return file === undefined || file.test(nodeInfo.info.file ?? ''); } /** * The last line covered by the given AST (restricted to `file`), used to resolve lines counted from the end. */ function lastLineOf(dataflowIdMap, file) { let last = 0; for (const nodeInfo of dataflowIdMap.values()) { const range = range_1.SourceRange.fromNode(nodeInfo); if (range !== undefined && range[2] > last && matchesFile(nodeInfo, file)) { last = range[2]; } } return last; } /** * Parses the line part of a criterion, resolving a negative line (`-1` being the last line) against the AST. * Returns `undefined` if it is not a number at all (e.g. `x@y`), so the criterion can be rejected as invalid. */ function parseLineNumber(text, dataflowIdMap, file) { if (!/^-?\d+$/.test(text)) { return undefined; } const line = parseInt(text); return line < 0 ? lastLineOf(dataflowIdMap, file) + line + 1 : line; } /** Parses a `line<sep>column` criterion, rejecting anything that is not a pair of numbers. */ function parseLocation(criterion, separator, dataflowIdMap, file) { const parts = criterion.split(separator); if (parts.length !== 2 || !/^\d+$/.test(parts[1])) { return undefined; } const line = parseLineNumber(parts[0], dataflowIdMap, file); return line === undefined ? undefined : [line, parseInt(parts[1])]; } /** * Resolves the `nth` occurrence of `name` in the given line (as written by the optional `[n]` prefix of * `line@[n]name`); `n` is 1-based and may be negative to count from the end of the line (`-1` being the last). * Occurrences are counted by column, as a function call and the symbol it refers to share a position and are * thus one and the same occurrence. */ function nthOccurrenceToId(line, name, dataflowIdMap, nth, file) { const byColumn = new Map(); for (const nodeInfo of dataflowIdMap.values()) { if (nodeInfo.location === undefined || nodeInfo.location[0] !== line || nodeInfo.lexeme !== name || !matchesFile(nodeInfo, file)) { continue; } if (nodeInfo.type === type_1.RType.Argument || nodeInfo.type === type_1.RType.ExpressionList) { continue; } const column = nodeInfo.location[1]; // function calls have the same location as the symbol they refer to, so we need to prefer the function call if (!byColumn.has(column) || nodeInfo.type === type_1.RType.FunctionCall) { byColumn.set(column, nodeInfo); } } const columns = [...byColumn.keys()].sort((a, b) => a - b); const index = nth < 0 ? columns.length + nth : nth - 1; return index >= 0 && index < columns.length ? byColumn.get(columns[index])?.info.id : undefined; } //# sourceMappingURL=parse.js.map