forked from LeenkxTeam/Kmake
258 lines
7.5 KiB
JavaScript
258 lines
7.5 KiB
JavaScript
// Copyright 2025 the V8 project authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
/**
|
|
* @fileoverview Analyzer.
|
|
* Mutator that acquires additional information used in subsequent mutators.
|
|
* This mutator doesn't change the structure, but only marks nodes with
|
|
* meta information.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const babelTypes = require('@babel/types');
|
|
|
|
const common = require('./common.js');
|
|
const mutator = require('./mutator.js');
|
|
|
|
// Additional resource needed if the AsyncIterator ID is found in the code.
|
|
const ASYNC_ITERATOR_ID = 'AsyncIterator'
|
|
const ASYNC_ITERATOR_RESOURCE = 'async_iterator.js'
|
|
|
|
// State for keeping track of loops and loop bodies during traversal.
|
|
const LoopState = Object.freeze({
|
|
LOOP: Symbol("loop"),
|
|
BLOCK: Symbol("block"),
|
|
});
|
|
|
|
// We consider loops with more iterations than this as large. This can be used
|
|
// to reduce some mutations in such loops.
|
|
const LARGE_LOOP_THRESHOLD = 1000;
|
|
|
|
/**
|
|
* Combine multiple enter/exit visitor pairs. Also single enter or exit
|
|
* methods are possible.
|
|
*/
|
|
function compound(...visitors) {
|
|
const enters = [];
|
|
const exits = [];
|
|
for (const visitor of visitors) {
|
|
if ("enter" in visitor) enters.push(visitor.enter);
|
|
if ("exit" in visitor) exits.push(visitor.exit);
|
|
}
|
|
return {
|
|
enter: enters,
|
|
// Exit in reverse order so that the corresponding enter/exit pairs
|
|
// operate on a stack.
|
|
exit: exits.reverse(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns true for numeric values with or without an unary minus compound.
|
|
*/
|
|
function isNumber(node) {
|
|
return (
|
|
(
|
|
// Direct valid number literal.
|
|
babelTypes.isLiteral(node) &&
|
|
!isNaN(node.value)
|
|
) ||
|
|
(
|
|
// A number literal wrapped with an unary minus.
|
|
babelTypes.isUnaryExpression(node) &&
|
|
node.operator === '-' &&
|
|
babelTypes.isLiteral(node.argument) &&
|
|
!isNaN(node.argument.value)
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Extracts the numeric value from a node. We assume it is a number as
|
|
* determined by the function above.
|
|
*/
|
|
function getNumber(node) {
|
|
// Direct valid number literal.
|
|
if (babelTypes.isLiteral(node)) {
|
|
assert(!isNaN(node.value));
|
|
return node.value;
|
|
}
|
|
// A number literal wrapped with an unary minus.
|
|
assert(babelTypes.isUnaryExpression(node));
|
|
assert(node.operator === '-');
|
|
assert(babelTypes.isLiteral(node.argument));
|
|
assert(!isNaN(node.argument.value));
|
|
return -node.argument.value;
|
|
}
|
|
|
|
/**
|
|
* Resolve two nodes in any order as [identifier, number] if possible,
|
|
* [undefined, undefined] otherwise.
|
|
*/
|
|
function idAndNumber(node1, node2) {
|
|
if(!node1 || !node2) {
|
|
return [undefined, undefined];
|
|
}
|
|
if (babelTypes.isIdentifier(node1) && isNumber(node2)) {
|
|
return [node1.name, getNumber(node2)];
|
|
}
|
|
if (isNumber(node1) && babelTypes.isIdentifier(node2)) {
|
|
return [node2.name, getNumber(node1)];
|
|
}
|
|
return [undefined, undefined];
|
|
}
|
|
|
|
/**
|
|
* Return true if we approximate this node as a large for-loop. We
|
|
* heuristically look at simple loop initializers and conditions only.
|
|
*
|
|
* As this is only an approximation, it will determine some large loops
|
|
* as small and vice versa. Hence the result of this should only be used
|
|
* for soft logical changes during fuzzing, e.g. behind a probability.
|
|
*/
|
|
function approximateLargeForLoop(node) {
|
|
assert(babelTypes.isForStatement(node));
|
|
|
|
// The initializer must be a single declaration of the form:
|
|
// `{var|let|const} {decl}`.
|
|
if (!node.init ||
|
|
!babelTypes.isVariableDeclaration(node.init) ||
|
|
node.init.declarations.length != 1) {
|
|
return false;
|
|
}
|
|
|
|
// The declaration must assign a number to an identifier: `id = num`.
|
|
const decl = node.init.declarations[0];
|
|
if (!decl ||
|
|
!babelTypes.isVariableDeclarator(decl) ||
|
|
!babelTypes.isIdentifier(decl.id) ||
|
|
!decl.init ||
|
|
!isNumber(decl.init)) {
|
|
return false;
|
|
}
|
|
|
|
// The test part must be a single binary expression. We don't care which
|
|
// operator, e.g. `A < B`.
|
|
const initID = decl.id.name;
|
|
const initNumber = getNumber(decl.init);
|
|
if (!node.test || !babelTypes.isBinaryExpression(node.test)) {
|
|
return false;
|
|
}
|
|
|
|
// The two operants must be an identifier and a number in any order. E.g.:
|
|
// `id < num`.
|
|
const [testID, testNumber] = idAndNumber(node.test.left, node.test.right);
|
|
if (testID === undefined || testNumber === undefined) {
|
|
return false;
|
|
}
|
|
|
|
// If we test the same identifier we initialized and if the numeric
|
|
// difference is large enough, we approximate this as large. We ignore
|
|
// increments.
|
|
return (
|
|
initID === testID &&
|
|
Math.abs(initNumber - testNumber) >= LARGE_LOOP_THRESHOLD);
|
|
}
|
|
|
|
|
|
class ContextAnalyzer extends mutator.Mutator {
|
|
constructor() {
|
|
super();
|
|
this.loopStack = [];
|
|
}
|
|
|
|
/**
|
|
* Return true if the traversal is currently within a loop test part.
|
|
*/
|
|
isLoopTest() {
|
|
return this.loopStack.at(-1) === LoopState.LOOP;
|
|
}
|
|
|
|
get visitor() {
|
|
const thisMutator = this;
|
|
const loopStack = this.loopStack;
|
|
|
|
// Maintain the state if we're in a loop.
|
|
const loopStatement = {
|
|
enter(path) {
|
|
loopStack.push(LoopState.LOOP);
|
|
},
|
|
exit(path) {
|
|
assert(loopStack.pop() === LoopState.LOOP);
|
|
}
|
|
};
|
|
|
|
// Mark functions that contain empty infinite loops.
|
|
const emptyInfiniteLoop = {
|
|
enter(path) {
|
|
if (common.isInfiniteLoop(path.node) &&
|
|
path.node.body &&
|
|
babelTypes.isBlockStatement(path.node.body) &&
|
|
!path.node.body.body.length) {
|
|
const fun = path.findParent((p) => p.isFunctionDeclaration());
|
|
if (fun && fun.node && fun.node.id &&
|
|
babelTypes.isIdentifier(fun.node.id) &&
|
|
common.isFunctionIdentifier(fun.node.id.name)) {
|
|
thisMutator.context.infiniteFunctions.add(fun.node.id.name);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
const largeForLoop = {
|
|
enter(path) {
|
|
if (approximateLargeForLoop(path.node)) {
|
|
common.setLargeLoop(path.node);
|
|
}
|
|
},
|
|
};
|
|
|
|
return {
|
|
Identifier(path) {
|
|
if (path.node.name === ASYNC_ITERATOR_ID) {
|
|
thisMutator.context.extraResources.add(ASYNC_ITERATOR_RESOURCE);
|
|
}
|
|
if (thisMutator.isLoopTest() &&
|
|
common.isVariableIdentifier(path.node.name)) {
|
|
thisMutator.context.loopVariables.add(path.node.name);
|
|
}
|
|
},
|
|
FunctionDeclaration(path) {
|
|
if (path.node.id && babelTypes.isIdentifier(path.node.id)) {
|
|
if (common.isFunctionIdentifier(path.node.id.name) &&
|
|
path.node.async) {
|
|
thisMutator.context.asyncFunctions.add(path.node.id.name);
|
|
}
|
|
}
|
|
},
|
|
WhileStatement: compound(loopStatement, emptyInfiniteLoop),
|
|
DoWhileStatement: compound(loopStatement, emptyInfiniteLoop),
|
|
ForStatement: compound(loopStatement, largeForLoop),
|
|
BlockStatement: {
|
|
enter(path) {
|
|
loopStack.push(LoopState.BLOCK);
|
|
},
|
|
exit(path) {
|
|
assert(loopStack.pop() === LoopState.BLOCK);
|
|
}
|
|
},
|
|
// Mark nodes containing a yield expression up to the encolsing
|
|
// generator function declaration.
|
|
YieldExpression(path) {
|
|
for (let parent = path.parentPath;
|
|
parent && !parent.isFunctionDeclaration();
|
|
parent = parent.parentPath) {
|
|
common.setContainsYield(parent.node);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
ContextAnalyzer: ContextAnalyzer,
|
|
};
|