Skip to content

Commit

Permalink
Use code path analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
mdjermanovic committed Aug 27, 2021
1 parent 05038f3 commit 7f86df8
Showing 1 changed file with 48 additions and 82 deletions.
130 changes: 48 additions & 82 deletions lib/rules/complexity.js
Expand Up @@ -74,116 +74,82 @@ module.exports = {
// Helpers
//--------------------------------------------------------------------------

// Using a stack to store complexity (handling nested functions)
const fns = [];
// Using a stack to store complexity per code path
const complexities = [];

/**
* When parsing a new function, store it in our function stack
* @returns {void}
* @private
*/
function startFunction() {
fns.push(1);
}

/**
* Evaluate the node at the end of function
* @param {ASTNode} node node to evaluate. If it is a `PropertyDefinition` node, its initializer is being evaluated.
* @returns {void}
* @private
*/
function endFunction(node) {
const complexity = fns.pop();

if (complexity > THRESHOLD) {
let evaluatedNode, name;

if (node.type === "PropertyDefinition") {
evaluatedNode = node.value;
name = "class field initializer";
} else {
evaluatedNode = node;
name = astUtils.getFunctionNameWithKind(node);
}

context.report({
node: evaluatedNode,
messageId: "complex",
data: {
name: upperCaseFirst(name),
complexity,
max: THRESHOLD
}
});
}
}

/**
* Increase the complexity of the function in context
* Increase the complexity of the code path in context
* @returns {void}
* @private
*/
function increaseComplexity() {
if (fns.length) {
fns[fns.length - 1]++;
}
}

/**
* Increase the switch complexity in context
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function increaseSwitchComplexity(node) {

// Avoiding `default`
if (node.test) {
increaseComplexity();
}
complexities[complexities.length - 1]++;
}

//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------

return {
FunctionDeclaration: startFunction,
FunctionExpression: startFunction,
ArrowFunctionExpression: startFunction,
"FunctionDeclaration:exit": endFunction,
"FunctionExpression:exit": endFunction,
"ArrowFunctionExpression:exit": endFunction,

/*
* Class field initializers are implicit functions. Therefore, they shouldn't contribute
* to the enclosing function's complexity, but their own complexity should be evaluated.
* We're using `*.key:exit` here in order to make sure that `startFunction()` is called
* before entering the `.value` node, and thus certainly before other listeners
* (e.g., if the initializer is `a || b`, due to a higher selector specificity
* `PropertyDefinition > *.value` would be called after `LogicalExpression`).
* We're passing the `PropertyDefinition` node instead of `PropertyDefinition.value` node
* to `endFunction(node)` in order to disambiguate between evaluating implicit initializer
* functions and "regular" functions, which may be the `.value` itself, e.g., `x = () => {};`.
*/
"PropertyDefinition[value] > *.key:exit": startFunction,
"PropertyDefinition[value]:exit": endFunction,

onCodePathStart() {

// The initial complexity is 1, representing one execution path in the CodePath
complexities.push(1);
},

// Each branching in the code adds 1 to the complexity
CatchClause: increaseComplexity,
ConditionalExpression: increaseComplexity,
LogicalExpression: increaseComplexity,
ForStatement: increaseComplexity,
ForInStatement: increaseComplexity,
ForOfStatement: increaseComplexity,
IfStatement: increaseComplexity,
SwitchCase: increaseSwitchComplexity,
WhileStatement: increaseComplexity,
DoWhileStatement: increaseComplexity,

// Avoid `default`
"SwitchCase[test]": increaseComplexity,

// Logical assignment operators have short-circuiting behavior
AssignmentExpression(node) {
if (astUtils.isLogicalAssignmentOperator(node.operator)) {
increaseComplexity();
}
},

onCodePathEnd(codePath, node) {
const complexity = complexities.pop();

/*
* This rule only evaluates complexity of functions, so "program" is excluded.
* Class field initializers are implicit functions. Therefore, they shouldn't contribute
* to the enclosing function's complexity, but their own complexity should be evaluated.
*/
if (
codePath.origin !== "function" &&
codePath.origin !== "class-field-initializer"
) {
return;
}


if (complexity > THRESHOLD) {
const name = codePath.origin === "class-field-initializer"
? "class field initializer"
: astUtils.getFunctionNameWithKind(node);

context.report({
node,
messageId: "complex",
data: {
name: upperCaseFirst(name),
complexity,
max: THRESHOLD
}
});
}
}
};

Expand Down

0 comments on commit 7f86df8

Please sign in to comment.