2025-04-17 07:44:37 -04:00

224 lines
5.7 KiB
JavaScript

/**
* @fileoverview Helpers to debug for code path analysis.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const debug = require("debug")("eslint:code-path");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Gets id of a given segment.
* @param {CodePathSegment} segment A segment to get.
* @returns {string} Id of the segment.
*/
/* c8 ignore next */
// eslint-disable-next-line jsdoc/require-jsdoc -- Ignoring
function getId(segment) {
return segment.id + (segment.reachable ? "" : "!");
}
/**
* Get string for the given node and operation.
* @param {ASTNode} node The node to convert.
* @param {"enter" | "exit" | undefined} label The operation label.
* @returns {string} The string representation.
*/
function nodeToString(node, label) {
const suffix = label ? `:${label}` : "";
switch (node.type) {
case "Identifier":
return `${node.type}${suffix} (${node.name})`;
case "Literal":
return `${node.type}${suffix} (${node.value})`;
default:
return `${node.type}${suffix}`;
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
/**
* A flag that debug dumping is enabled or not.
* @type {boolean}
*/
enabled: debug.enabled,
/**
* Dumps given objects.
* @param {...any} args objects to dump.
* @returns {void}
*/
dump: debug,
/**
* Dumps the current analyzing state.
* @param {ASTNode} node A node to dump.
* @param {CodePathState} state A state to dump.
* @param {boolean} leaving A flag whether or not it's leaving
* @returns {void}
*/
dumpState: !debug.enabled
? debug
: /* c8 ignore next */ function (node, state, leaving) {
for (let i = 0; i < state.currentSegments.length; ++i) {
const segInternal = state.currentSegments[i].internal;
if (leaving) {
const last = segInternal.nodes.length - 1;
if (
last >= 0 &&
segInternal.nodes[last] ===
nodeToString(node, "enter")
) {
segInternal.nodes[last] = nodeToString(
node,
void 0,
);
} else {
segInternal.nodes.push(nodeToString(node, "exit"));
}
} else {
segInternal.nodes.push(nodeToString(node, "enter"));
}
}
debug(
[
`${state.currentSegments.map(getId).join(",")})`,
`${node.type}${leaving ? ":exit" : ""}`,
].join(" "),
);
},
/**
* Dumps a DOT code of a given code path.
* The DOT code can be visualized with Graphvis.
* @param {CodePath} codePath A code path to dump.
* @returns {void}
* @see http://www.graphviz.org
* @see http://www.webgraphviz.com
*/
dumpDot: !debug.enabled
? debug
: /* c8 ignore next */ function (codePath) {
let text =
"\n" +
"digraph {\n" +
'node[shape=box,style="rounded,filled",fillcolor=white];\n' +
'initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];\n';
if (codePath.returnedSegments.length > 0) {
text +=
'final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];\n';
}
if (codePath.thrownSegments.length > 0) {
text +=
'thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize=true];\n';
}
const traceMap = Object.create(null);
const arrows = this.makeDotArrows(codePath, traceMap);
// eslint-disable-next-line guard-for-in -- Want ability to traverse prototype
for (const id in traceMap) {
const segment = traceMap[id];
text += `${id}[`;
if (segment.reachable) {
text += 'label="';
} else {
text +=
'style="rounded,dashed,filled",fillcolor="#FF9800",label="<<unreachable>>\\n';
}
if (segment.internal.nodes.length > 0) {
text += segment.internal.nodes.join("\\n");
} else {
text += "????";
}
text += '"];\n';
}
text += `${arrows}\n`;
text += "}";
debug("DOT", text);
},
/**
* Makes a DOT code of a given code path.
* The DOT code can be visualized with Graphvis.
* @param {CodePath} codePath A code path to make DOT.
* @param {Object} traceMap Optional. A map to check whether or not segments had been done.
* @returns {string} A DOT code of the code path.
*/
makeDotArrows(codePath, traceMap) {
const stack = [[codePath.initialSegment, 0]];
const done = traceMap || Object.create(null);
let lastId = codePath.initialSegment.id;
let text = `initial->${codePath.initialSegment.id}`;
while (stack.length > 0) {
const item = stack.pop();
const segment = item[0];
const index = item[1];
if (done[segment.id] && index === 0) {
continue;
}
done[segment.id] = segment;
const nextSegment = segment.allNextSegments[index];
if (!nextSegment) {
continue;
}
if (lastId === segment.id) {
text += `->${nextSegment.id}`;
} else {
text += `;\n${segment.id}->${nextSegment.id}`;
}
lastId = nextSegment.id;
stack.unshift([segment, 1 + index]);
stack.push([nextSegment, 0]);
}
codePath.returnedSegments.forEach(finalSegment => {
if (lastId === finalSegment.id) {
text += "->final";
} else {
text += `;\n${finalSegment.id}->final`;
}
lastId = null;
});
codePath.thrownSegments.forEach(finalSegment => {
if (lastId === finalSegment.id) {
text += "->thrown";
} else {
text += `;\n${finalSegment.id}->thrown`;
}
lastId = null;
});
return `${text};`;
},
};