You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
147 lines
4.4 KiB
147 lines
4.4 KiB
2 years ago
|
import { compareRangeCovs } from "./compare";
|
||
|
import { RangeCov } from "./types";
|
||
|
|
||
|
interface ReadonlyRangeTree {
|
||
|
readonly start: number;
|
||
|
readonly end: number;
|
||
|
readonly count: number;
|
||
|
readonly children: ReadonlyRangeTree[];
|
||
|
}
|
||
|
|
||
|
export function emitForest(trees: ReadonlyArray<ReadonlyRangeTree>): string {
|
||
|
return emitForestLines(trees).join("\n");
|
||
|
}
|
||
|
|
||
|
export function emitForestLines(trees: ReadonlyArray<ReadonlyRangeTree>): string[] {
|
||
|
const colMap: Map<number, number> = getColMap(trees);
|
||
|
const header: string = emitOffsets(colMap);
|
||
|
return [header, ...trees.map(tree => emitTree(tree, colMap).join("\n"))];
|
||
|
}
|
||
|
|
||
|
function getColMap(trees: Iterable<ReadonlyRangeTree>): Map<number, number> {
|
||
|
const eventSet: Set<number> = new Set();
|
||
|
for (const tree of trees) {
|
||
|
const stack: ReadonlyRangeTree[] = [tree];
|
||
|
while (stack.length > 0) {
|
||
|
const cur: ReadonlyRangeTree = stack.pop()!;
|
||
|
eventSet.add(cur.start);
|
||
|
eventSet.add(cur.end);
|
||
|
for (const child of cur.children) {
|
||
|
stack.push(child);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
const events: number[] = [...eventSet];
|
||
|
events.sort((a, b) => a - b);
|
||
|
let maxDigits: number = 1;
|
||
|
for (const event of events) {
|
||
|
maxDigits = Math.max(maxDigits, event.toString(10).length);
|
||
|
}
|
||
|
const colWidth: number = maxDigits + 3;
|
||
|
const colMap: Map<number, number> = new Map();
|
||
|
for (const [i, event] of events.entries()) {
|
||
|
colMap.set(event, i * colWidth);
|
||
|
}
|
||
|
return colMap;
|
||
|
}
|
||
|
|
||
|
function emitTree(tree: ReadonlyRangeTree, colMap: Map<number, number>): string[] {
|
||
|
const layers: ReadonlyRangeTree[][] = [];
|
||
|
let nextLayer: ReadonlyRangeTree[] = [tree];
|
||
|
while (nextLayer.length > 0) {
|
||
|
const layer: ReadonlyRangeTree[] = nextLayer;
|
||
|
layers.push(layer);
|
||
|
nextLayer = [];
|
||
|
for (const node of layer) {
|
||
|
for (const child of node.children) {
|
||
|
nextLayer.push(child);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return layers.map(layer => emitTreeLayer(layer, colMap));
|
||
|
}
|
||
|
|
||
|
export function parseFunctionRanges(text: string, offsetMap: Map<number, number>): RangeCov[] {
|
||
|
const result: RangeCov[] = [];
|
||
|
for (const line of text.split("\n")) {
|
||
|
for (const range of parseTreeLayer(line, offsetMap)) {
|
||
|
result.push(range);
|
||
|
}
|
||
|
}
|
||
|
result.sort(compareRangeCovs);
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* @param layer Sorted list of disjoint trees.
|
||
|
* @param colMap
|
||
|
*/
|
||
|
function emitTreeLayer(layer: ReadonlyRangeTree[], colMap: Map<number, number>): string {
|
||
|
const line: string[] = [];
|
||
|
let curIdx: number = 0;
|
||
|
for (const {start, end, count} of layer) {
|
||
|
const startIdx: number = colMap.get(start)!;
|
||
|
const endIdx: number = colMap.get(end)!;
|
||
|
if (startIdx > curIdx) {
|
||
|
line.push(" ".repeat(startIdx - curIdx));
|
||
|
}
|
||
|
line.push(emitRange(count, endIdx - startIdx));
|
||
|
curIdx = endIdx;
|
||
|
}
|
||
|
return line.join("");
|
||
|
}
|
||
|
|
||
|
function parseTreeLayer(text: string, offsetMap: Map<number, number>): RangeCov[] {
|
||
|
const result: RangeCov[] = [];
|
||
|
const regex: RegExp = /\[(\d+)-*\)/gs;
|
||
|
while (true) {
|
||
|
const match: RegExpMatchArray | null = regex.exec(text);
|
||
|
if (match === null) {
|
||
|
break;
|
||
|
}
|
||
|
const startIdx: number = match.index!;
|
||
|
const endIdx: number = startIdx + match[0].length;
|
||
|
const count: number = parseInt(match[1], 10);
|
||
|
const startOffset: number | undefined = offsetMap.get(startIdx);
|
||
|
const endOffset: number | undefined = offsetMap.get(endIdx);
|
||
|
if (startOffset === undefined || endOffset === undefined) {
|
||
|
throw new Error(`Invalid offsets for: ${JSON.stringify(text)}`);
|
||
|
}
|
||
|
result.push({startOffset, endOffset, count});
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
function emitRange(count: number, len: number): string {
|
||
|
const rangeStart: string = `[${count.toString(10)}`;
|
||
|
const rangeEnd: string = ")";
|
||
|
const hyphensLen: number = len - (rangeStart.length + rangeEnd.length);
|
||
|
const hyphens: string = "-".repeat(Math.max(0, hyphensLen));
|
||
|
return `${rangeStart}${hyphens}${rangeEnd}`;
|
||
|
}
|
||
|
|
||
|
function emitOffsets(colMap: Map<number, number>): string {
|
||
|
let line: string = "";
|
||
|
for (const [event, col] of colMap) {
|
||
|
if (line.length < col) {
|
||
|
line += " ".repeat(col - line.length);
|
||
|
}
|
||
|
line += event.toString(10);
|
||
|
}
|
||
|
return line;
|
||
|
}
|
||
|
|
||
|
export function parseOffsets(text: string): Map<number, number> {
|
||
|
const result: Map<number, number> = new Map();
|
||
|
const regex: RegExp = /\d+/gs;
|
||
|
while (true) {
|
||
|
const match: RegExpExecArray | null = regex.exec(text);
|
||
|
if (match === null) {
|
||
|
break;
|
||
|
}
|
||
|
result.set(match.index, parseInt(match[0], 10));
|
||
|
}
|
||
|
return result;
|
||
|
}
|