eslint-plugin-sonarjs
Version:
SonarJS rules for ESLint
190 lines (189 loc) • 7.14 kB
JavaScript
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
// https://sonarsource.github.io/rspec/#/rspec/S2589
Object.defineProperty(exports, "__esModule", { value: true });
exports.rule = void 0;
const index_js_1 = require("../helpers/index.js");
const meta_js_1 = require("./meta.js");
const message = 'This always evaluates to {{value}}. Consider refactoring this code.';
exports.rule = {
meta: (0, index_js_1.generateMeta)(meta_js_1.meta, {
messages: {
refactorBooleanExpression: message,
},
}, true),
create(context) {
const truthyMap = new Map();
const falsyMap = new Map();
function isInsideJSX(node) {
const ancestors = context.sourceCode.getAncestors(node);
return !!ancestors.find(ancestor => ancestor.type === 'JSXExpressionContainer');
}
return {
IfStatement: (node) => {
const { test } = node;
if (test.type === 'Literal' && typeof test.value === 'boolean') {
reportIssue(test, undefined, context, test.value);
}
},
':statement': (node) => {
const { parent } = node;
if ((0, index_js_1.isIfStatement)(parent)) {
// we visit 'consequent' and 'alternate' and not if-statement directly in order to get scope for 'consequent'
const currentScope = context.sourceCode.getScope(node);
if (parent.consequent === node) {
const { truthy, falsy } = collectKnownIdentifiers(parent.test);
truthyMap.set(parent.consequent, transformAndFilter(truthy, currentScope));
falsyMap.set(parent.consequent, transformAndFilter(falsy, currentScope));
}
else if (parent.alternate === node && (0, index_js_1.isIdentifier)(parent.test)) {
falsyMap.set(parent.alternate, transformAndFilter([parent.test], currentScope));
}
}
},
':statement:exit': (node) => {
const stmt = node;
truthyMap.delete(stmt);
falsyMap.delete(stmt);
},
Identifier: (node) => {
const id = node;
const symbol = getSymbol(id, context.sourceCode.getScope(node));
const { parent } = node;
if (!symbol || !parent || (isInsideJSX(node) && isLogicalAndRhs(id, parent))) {
return;
}
if (!isLogicalAnd(parent) &&
!isLogicalOrLhs(id, parent) &&
!(0, index_js_1.isIfStatement)(parent) &&
!isLogicalNegation(parent)) {
return;
}
const checkIfKnownAndReport = (map, truthy) => {
map.forEach(references => {
const ref = references.find(ref => ref.resolved === symbol);
if (ref) {
reportIssue(id, ref, context, truthy);
}
});
};
checkIfKnownAndReport(truthyMap, true);
checkIfKnownAndReport(falsyMap, false);
},
Program: () => {
truthyMap.clear();
falsyMap.clear();
},
};
},
};
function collectKnownIdentifiers(expression) {
const truthy = [];
const falsy = [];
const checkExpr = (expr) => {
if ((0, index_js_1.isIdentifier)(expr)) {
truthy.push(expr);
}
else if (isLogicalNegation(expr)) {
if ((0, index_js_1.isIdentifier)(expr.argument)) {
falsy.push(expr.argument);
}
else if (isLogicalNegation(expr.argument) && (0, index_js_1.isIdentifier)(expr.argument.argument)) {
truthy.push(expr.argument.argument);
}
}
};
let current = expression;
checkExpr(current);
while (isLogicalAnd(current)) {
checkExpr(current.right);
current = current.left;
}
checkExpr(current);
return { truthy, falsy };
}
function isLogicalAnd(expression) {
return expression.type === 'LogicalExpression' && expression.operator === '&&';
}
function isLogicalOrLhs(id, expression) {
return (expression.type === 'LogicalExpression' &&
expression.operator === '||' &&
expression.left === id);
}
function isLogicalAndRhs(id, expression) {
return (expression.parent?.type !== 'LogicalExpression' &&
expression.type === 'LogicalExpression' &&
expression.operator === '&&' &&
expression.right === id);
}
function isLogicalNegation(expression) {
return expression.type === 'UnaryExpression' && expression.operator === '!';
}
function isDefined(x) {
return x != null;
}
function getSymbol(id, scope) {
const ref = scope.references.find(r => r.identifier === id);
if (ref) {
return ref.resolved;
}
return null;
}
function getFunctionScope(scope) {
if (scope.type === 'function') {
return scope;
}
else if (!scope.upper) {
return null;
}
return getFunctionScope(scope.upper);
}
function mightBeWritten(symbol, currentScope) {
return symbol.references
.filter(ref => ref.isWrite())
.find(ref => {
const refScope = ref.from;
let cur = refScope;
while (cur) {
if (cur === currentScope) {
return true;
}
cur = cur.upper;
}
const currentFunc = getFunctionScope(currentScope);
const refFunc = getFunctionScope(refScope);
return refFunc !== currentFunc;
});
}
function transformAndFilter(ids, currentScope) {
return ids
.map(id => currentScope.upper?.references.find(r => r.identifier === id))
.filter(isDefined)
.filter(ref => isDefined(ref.resolved))
.filter(ref => !mightBeWritten(ref.resolved, currentScope));
}
function reportIssue(id, ref, context, truthy) {
const value = truthy ? 'truthy' : 'falsy';
(0, index_js_1.report)(context, {
message,
data: {
value,
},
node: id,
}, ref?.identifier ? [(0, index_js_1.toSecondaryLocation)(ref.identifier, `Evaluated here to be ${value}`)] : []);
}
;