@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
361 lines • 13 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SourceLocation = exports.SourceRange = exports.SourcePosition = void 0;
const assert_1 = require("./assert");
/**
* Utility functions for {@link SourcePosition|source positions}.
*/
exports.SourcePosition = {
name: 'SourcePosition',
/**
* Formats a {@link SourcePosition|source position} as a human-readable string.
*/
format(pos) {
if (pos === undefined) {
return '??.??';
}
else {
return `${pos[0]}.${pos[1]}`;
}
},
/**
* Creates a source position from the given line and column numbers.
* @param line - line number (starts with 1)
* @param column - column number (starts with 1)
*/
from(line, column) {
return [Number(line), Number(column)];
},
/**
* returns an invalid source position
*/
invalid() {
return [-1, -1];
},
/**
* Returns the line of a source position
*/
getLine(pos) {
return pos[0];
},
/**
* Returns the column of a source position
*/
getColumn(pos) {
return pos[1];
},
};
/**
* Utility functions for {@link SourceRange|source ranges}.
*/
exports.SourceRange = {
name: 'SourceRange',
/**
* Prints a {@link SourceRange|range} as a human-readable string.
*/
format(range) {
if (range === undefined) {
return '??-??';
}
else if (range[0] === range[2]) {
if (range[1] === range[3]) {
return `${range[0]}.${range[1]}`;
}
else {
return `${range[0]}.${range[1]}-${range[3]}`;
}
}
return `${range[0]}.${range[1]}-${range[2]}.${range[3]}`;
},
/**
* Returns the start position of a source range.
*/
getStart(range) {
return [range[0], range[1]];
},
/**
* Returns the start line of a source range.
*/
getStartLine(range) {
return range[0];
},
/**
* Returns the end position of a source range.
*/
getEnd(range) {
return [range[2], range[3]];
},
/**
* Returns the end line of a source range.
*/
getEndLine(range) {
return range[2];
},
/**
* Creates a source range from the given line and column numbers.
* @param sl - start line
* @param sc - start column
* @param el - end line
* @param ec - end column
*/
from(sl, sc, el = sl, ec = sc) {
return [Number(sl), Number(sc), Number(el), Number(ec)];
},
/**
* returns an invalid source range
*/
invalid() {
return [-1, -1, -1, -1];
},
/**
* Merges multiple source ranges into a single source range that spans from the earliest start to the latest end.
* If you are interested in reducing a set of ranges to those none of the others contains, see {@link combineRanges}.
* @throws if no ranges are provided
*/
merge(rs) {
const rsSafe = rs.filter(assert_1.isNotUndefined);
(0, assert_1.guard)(rsSafe.length > 0, 'Cannot merge no ranges');
return rsSafe.reduce(([sl, sc, el, ec], [nsl, nsc, nel, nec]) => [
...(sl < nsl || (sl === nsl && sc < nsc) ? [sl, sc] : [nsl, nsc]),
...(el > nel || (el === nel && ec > nec) ? [el, ec] : [nel, nec])
], rsSafe[0]);
},
/**
* @returns true iff `r1` starts and ends before `r2` starts (i.e., if `r1` and `r2` do not overlap and `r1` comes before `r2`
*/
startsCompletelyBefore([, , r1el, r1ec], [r2sl, r2sc, ,]) {
return r1el < r2sl || (r1el === r2sl && r1ec < r2sc);
},
/** Checks if the two ranges overlap, i.e. whether neither of them lies completely before the other. */
overlap(r1, r2) {
return !exports.SourceRange.startsCompletelyBefore(r1, r2) && !exports.SourceRange.startsCompletelyBefore(r2, r1);
},
/**
* Calculates the component-wise sum of two ranges.
*/
add([r1sl, r1sc, r1el, r1ec], [r2sl, r2sc, r2el, r2ec]) {
return [r1sl + r2sl, r1sc + r2sc, r1el + r2el, r1ec + r2ec];
},
/**
* Provides a comparator for {@link SourceRange}s that sorts them in ascending order.
* @returns a positive number if `r1` comes after `r2`, a negative number if `r1` comes before `r2`, and `0` if they are equal
*/
compare([r1sl, r1sc, ,], [r2sl, r2sc, ,]) {
if (r1sl === r2sl) {
return r1sc - r2sc;
}
else {
return r1sl - r2sl;
}
},
/**
* Checks if two ranges are equal (i.e., they start and end at the same position).
*/
equals([r1sl, r1sc, r1el, r1ec], [r2sl, r2sc, r2el, r2ec]) {
return r1sl === r2sl && r1sc === r2sc && r1el === r2el && r1ec === r2ec;
},
/**
* Checks if a given position (line, column) is contained within the range.
*/
containsPosition([sl, sc, el, ec], line, column) {
if (line < sl || line > el) {
return false;
}
else if (sl === el) {
return sc <= column && column <= ec;
}
else if (line === sl) {
return column >= sc;
}
else if (line === el) {
return column <= ec;
}
return true;
},
/**
* Checks if the first range is a subset of the second range.
*/
isSubsetOf([r1sl, r1sc, r1el, r1ec], [r2sl, r2sc, r2el, r2ec]) {
return (r1sl > r2sl || r1sl === r2sl && r1sc >= r2sc) && (r1el < r2el || r1el === r2el && r1ec <= r2ec);
},
/**
* Checks if the first range is a strict subset of the second range (i.e., it is a subset but not equal).
*/
isStrictSubsetOf(r1, r2) {
return exports.SourceRange.isSubsetOf(r1, r2) && !exports.SourceRange.equals(r1, r2);
},
/**
* Reduces the ranges to those no other one contains, keeping the first of any duplicates. Ranges that merely
* overlap are both kept, as neither contains the other. A non-empty input always yields a non-empty result.
* @see {@link SourceRange.merge} for merging multiple ranges into a single range.
*/
combineRanges(...ranges) {
return ranges.filter((range, i) => !ranges.some((other, j) => i !== j
&& (exports.SourceRange.isStrictSubsetOf(range, other) || (j < i && exports.SourceRange.equals(range, other)))));
},
fromNode(node) {
return node?.info.fullRange ?? node?.location;
},
/**
* "Fuzzy" position match, as opposed to requiring a node to *start* exactly at the position.
* @see {@link SourceRange.innermostNodes} to narrow the result down to the deepest matches
*/
nodesContaining(nodes, line, column) {
return nodes.filter(node => {
const range = exports.SourceRange.fromNode(node);
if (range === undefined) {
return false;
}
return column === undefined ? range[0] <= line && line <= range[2] : exports.SourceRange.containsPosition(range, line, column);
});
},
/**
* Collects all nodes satisfying the innermost condition: those containing no other of the given nodes.
*
* Nodes may share a range (a function call and the symbol naming it do), so `treatChildAsInner` decides that
* tie: with it, a node sharing its parent's range counts as the inner one and the parent drops out; without
* it, both are kept and the caller may pick between them (e.g. by node type).
*
* A non-empty input always yields a non-empty result: enclosure is a strict order so some node is always minimal,
* and a node carrying no range at all is kept rather than dropped.
* @see {@link SourceRange.nodesContaining} which this usually narrows down
*/
innermostNodes(nodes, treatChildAsInner = true) {
const result = [];
for (const node of nodes) {
const range = exports.SourceRange.fromNode(node);
if (!range) {
result.push(node);
continue;
}
let inner = false;
for (const other of nodes) {
if (other === node) {
continue;
}
const otherRange = exports.SourceRange.fromNode(other);
if (!otherRange) {
continue;
}
if (exports.SourceRange.isStrictSubsetOf(otherRange, range) ||
(treatChildAsInner &&
other.info.parent === node.info.id &&
exports.SourceRange.equals(otherRange, range))) {
inner = true;
break;
}
}
if (!inner) {
result.push(node);
}
}
return result.length > 0 ? result : nodes.slice();
}
};
/**
* Utility functions for {@link SourceLocation|source locations}.
*/
exports.SourceLocation = {
name: 'SourceLocation',
/**
* Formats a {@link SourceLocation|source location} as a human-readable string.
*/
format(location) {
if (location === undefined) {
return '??:??-??';
}
else if (location[4] !== undefined) {
return `${location[4]}:${exports.SourceRange.format(location)}`;
}
else {
return exports.SourceRange.format(location);
}
},
/** Returns the {@link SourceRange|source range} part of a {@link SourceLocation|source location}, file excluded. */
getRange([sl, sc, el, ec]) {
return [sl, sc, el, ec];
},
/**
* Returns the file part of a {@link SourceLocation|source location}, or `undefined` if no file is set.
*/
getFile(location) {
return location[4];
},
/**
* Creates a {@link SourceLocation|source location} from a {@link SourceRange|source range} and a file name.
*/
from(range, file) {
return file !== undefined ? [...range, file] : range;
},
/**
* The {@link SourceLocation|source location} of an AST node, file included.
* @returns undefined if the given range is undefined
* @see {@link SourceRange.fromNode} for getting the range from an AST node
*/
fromNode(node) {
const range = exports.SourceRange.fromNode(node);
return node !== undefined && range !== undefined ? exports.SourceLocation.from(range, node.info.file) : undefined;
},
/**
* Maps the file part of a {@link SourceLocation|source location} using the given mapper function.
*/
mapFile(loc, fileMapper) {
const range = exports.SourceLocation.getRange(loc);
const file = loc[4];
return exports.SourceLocation.from(range, fileMapper(file));
},
/**
* Checks if the first location is a subset of the second location.
* For this, they must be in the same file!
* @see {@link SourceRange.isSubsetOf}
*/
isSubsetOf(loc1, loc2) {
if (exports.SourceLocation.getFile(loc1) !== exports.SourceLocation.getFile(loc2)) {
return false;
}
return exports.SourceRange.isSubsetOf(exports.SourceLocation.getRange(loc1), exports.SourceLocation.getRange(loc2));
},
compare(loc1, loc2) {
const res = exports.SourceRange.compare(exports.SourceLocation.getRange(loc1), exports.SourceLocation.getRange(loc2));
if (res !== 0) {
return res;
}
const file1 = exports.SourceLocation.getFile(loc1);
const file2 = exports.SourceLocation.getFile(loc2);
if (file1 === file2) {
return 0;
}
else if (file1 === undefined) {
return -1;
}
else if (file2 === undefined) {
return 1;
}
else {
return file1 < file2 ? -1 : 1;
}
},
/**
* Returns an invalid source location (i.e., with an invalid range and no file).
*/
invalid() {
return exports.SourceRange.invalid();
},
/**
* Merges multiple source locations into a single source location that spans from the earliest start to the latest end.
* If the locations are from different files, `undefined` is returned.
* Files may be `undefined` themselves, but if there is at least one defined file, they must all be the same defined file for the merge to succeed.
*/
merge(locs) {
const locsSafe = locs.filter(assert_1.isNotUndefined);
if (locsSafe.length === 0) {
return undefined;
}
const firstFile = locsSafe.find(loc => loc[4] !== undefined)?.[4];
if (locsSafe.some(loc => loc[4] !== undefined && loc[4] !== firstFile)) {
return undefined;
}
return exports.SourceLocation.from(exports.SourceRange.merge(locsSafe.map(exports.SourceLocation.getRange)), firstFile);
}
};
//# sourceMappingURL=range.js.map