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}`)] : []);
}