mirror of
https://git.proxmox.com/git/pve-eslint
synced 2025-08-13 17:22:29 +00:00
206 lines
7.5 KiB
JavaScript
206 lines
7.5 KiB
JavaScript
"use strict";
|
|
|
|
const evk = require("eslint-visitor-keys");
|
|
const recast = require("recast");
|
|
const espree = require("espree");
|
|
const assert = require("assert");
|
|
|
|
/**
|
|
* Determines whether an AST node could be an expression, based on the type
|
|
* @param {ASTNode} node The node
|
|
* @returns {boolean} `true` if the node could be an expression
|
|
*/
|
|
function isMaybeExpression(node) {
|
|
return node.type.endsWith("Expression") ||
|
|
node.type === "Identifier" ||
|
|
node.type === "MetaProperty" ||
|
|
node.type.endsWith("Literal");
|
|
}
|
|
|
|
/**
|
|
* Determines whether an AST node is a statement
|
|
* @param {ASTNode} node The node
|
|
* @returns {boolean} `true` if the node is a statement
|
|
*/
|
|
function isStatement(node) {
|
|
return node.type.endsWith("Statement") || node.type.endsWith("Declaration");
|
|
}
|
|
|
|
/**
|
|
* Given "bad" source text (e.g. an code sample that causes a rule to crash), tries to return a smaller
|
|
* piece of source text which is also "bad", to make it easier for a human to figure out where the
|
|
* problem is.
|
|
* @param {Object} options Options to process
|
|
* @param {string} options.sourceText Initial piece of "bad" source text
|
|
* @param {function(string): boolean} options.predicate A predicate that returns `true` for bad source text and `false` for good source text
|
|
* @param {Parser} [options.parser] The parser used to parse the source text. Defaults to a modified
|
|
* version of espree that uses recent parser options.
|
|
* @param {Object} [options.visitorKeys] The visitor keys of the AST. Defaults to eslint-visitor-keys.
|
|
* @returns {string} Another piece of "bad" source text, which may or may not be smaller than the original source text.
|
|
*/
|
|
function reduceBadExampleSize({
|
|
sourceText,
|
|
predicate,
|
|
parser = {
|
|
parse: (code, options) =>
|
|
espree.parse(code, {
|
|
...options,
|
|
loc: true,
|
|
range: true,
|
|
raw: true,
|
|
tokens: true,
|
|
comment: true,
|
|
eslintVisitorKeys: true,
|
|
eslintScopeManager: true,
|
|
ecmaVersion: espree.latestEcmaVersion,
|
|
sourceType: "script"
|
|
})
|
|
},
|
|
visitorKeys = evk.KEYS
|
|
}) {
|
|
let counter = 0;
|
|
|
|
/**
|
|
* Returns a new unique identifier
|
|
* @returns {string} A name for a new identifier
|
|
*/
|
|
function generateNewIdentifierName() {
|
|
return `$${(counter++)}`;
|
|
}
|
|
|
|
/**
|
|
* Determines whether a source text sample is "bad"
|
|
* @param {string} updatedSourceText The sample
|
|
* @returns {boolean} `true` if the sample is "bad"
|
|
*/
|
|
function reproducesBadCase(updatedSourceText) {
|
|
try {
|
|
parser.parse(updatedSourceText);
|
|
} catch {
|
|
return false;
|
|
}
|
|
|
|
return predicate(updatedSourceText);
|
|
}
|
|
|
|
assert(reproducesBadCase(sourceText), "Original source text should reproduce issue");
|
|
const parseResult = recast.parse(sourceText, { parser });
|
|
|
|
/**
|
|
* Recursively removes descendant subtrees of the given AST node and replaces
|
|
* them with simplified variants to produce a simplified AST which is still considered "bad".
|
|
* @param {ASTNode} node An AST node to prune. May be mutated by this call, but the
|
|
* resulting AST will still produce "bad" source code.
|
|
* @returns {void}
|
|
*/
|
|
function pruneIrrelevantSubtrees(node) {
|
|
for (const key of visitorKeys[node.type]) {
|
|
if (Array.isArray(node[key])) {
|
|
for (let index = node[key].length - 1; index >= 0; index--) {
|
|
const [childNode] = node[key].splice(index, 1);
|
|
|
|
if (!reproducesBadCase(recast.print(parseResult).code)) {
|
|
node[key].splice(index, 0, childNode);
|
|
if (childNode) {
|
|
pruneIrrelevantSubtrees(childNode);
|
|
}
|
|
}
|
|
}
|
|
} else if (typeof node[key] === "object" && node[key] !== null) {
|
|
|
|
const childNode = node[key];
|
|
|
|
if (isMaybeExpression(childNode)) {
|
|
node[key] = { type: "Identifier", name: generateNewIdentifierName(), range: childNode.range };
|
|
if (!reproducesBadCase(recast.print(parseResult).code)) {
|
|
node[key] = childNode;
|
|
pruneIrrelevantSubtrees(childNode);
|
|
}
|
|
} else if (isStatement(childNode)) {
|
|
node[key] = { type: "EmptyStatement", range: childNode.range };
|
|
if (!reproducesBadCase(recast.print(parseResult).code)) {
|
|
node[key] = childNode;
|
|
pruneIrrelevantSubtrees(childNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively tries to extract a descendant node from the AST that is "bad" on its own
|
|
* @param {ASTNode} node A node which produces "bad" source code
|
|
* @returns {ASTNode} A descendent of `node` which is also bad
|
|
*/
|
|
function extractRelevantChild(node) {
|
|
const childNodes = visitorKeys[node.type]
|
|
.flatMap(key => (Array.isArray(node[key]) ? node[key] : [node[key]]));
|
|
|
|
for (const childNode of childNodes) {
|
|
if (!childNode) {
|
|
continue;
|
|
}
|
|
|
|
if (isMaybeExpression(childNode)) {
|
|
if (reproducesBadCase(recast.print(childNode).code)) {
|
|
return extractRelevantChild(childNode);
|
|
}
|
|
|
|
} else if (isStatement(childNode)) {
|
|
if (reproducesBadCase(recast.print(childNode).code)) {
|
|
return extractRelevantChild(childNode);
|
|
}
|
|
} else {
|
|
const childResult = extractRelevantChild(childNode);
|
|
|
|
if (reproducesBadCase(recast.print(childResult).code)) {
|
|
return childResult;
|
|
}
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Removes and simplifies comments from the source text
|
|
* @param {string} text A piece of "bad" source text
|
|
* @returns {string} A piece of "bad" source text with fewer and/or simpler comments.
|
|
*/
|
|
function removeIrrelevantComments(text) {
|
|
const ast = parser.parse(text);
|
|
|
|
if (ast.comments) {
|
|
for (const comment of ast.comments) {
|
|
for (const potentialSimplification of [
|
|
|
|
// Try deleting the comment
|
|
`${text.slice(0, comment.range[0])}${text.slice(comment.range[1])}`,
|
|
|
|
// Try replacing the comment with a space
|
|
`${text.slice(0, comment.range[0])} ${text.slice(comment.range[1])}`,
|
|
|
|
// Try deleting the contents of the comment
|
|
text.slice(0, comment.range[0] + 2) + text.slice(comment.type === "Block" ? comment.range[1] - 2 : comment.range[1])
|
|
]) {
|
|
if (reproducesBadCase(potentialSimplification)) {
|
|
return removeIrrelevantComments(potentialSimplification);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
pruneIrrelevantSubtrees(parseResult.program);
|
|
const relevantChild = recast.print(extractRelevantChild(parseResult.program)).code;
|
|
|
|
assert(reproducesBadCase(relevantChild), "Extracted relevant source text should reproduce issue");
|
|
const result = removeIrrelevantComments(relevantChild);
|
|
|
|
assert(reproducesBadCase(result), "Source text with irrelevant comments removed should reproduce issue");
|
|
return result;
|
|
}
|
|
|
|
module.exports = reduceBadExampleSize;
|