eslint-plugin-sonarjs
Version: 
SonarJS rules for ESLint
281 lines (280 loc) • 10.8 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/S4165/javascript
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");
exports.rule = {
    meta: (0, index_js_1.generateMeta)(meta_js_1.meta, {
        messages: {
            reviewAssignment: 'Review this redundant assignment: "{{symbol}}" already holds the assigned value along all execution paths.',
        },
    }),
    create(context) {
        const codePathStack = [];
        const reachingDefsMap = new Map();
        // map from Variable to CodePath ids where variable is used
        const variableUsages = new Map();
        const codePathSegments = [];
        let currentCodePathSegments = [];
        return {
            ':matches(AssignmentExpression, VariableDeclarator[init])': (node) => {
                pushAssignmentContext(node);
            },
            ':matches(AssignmentExpression, VariableDeclarator[init]):exit': () => {
                popAssignmentContext();
            },
            Identifier: (node) => {
                if (isEnumConstant(node)) {
                    return;
                }
                checkIdentifierUsage(node);
            },
            'Program:exit': () => {
                (0, index_js_1.reachingDefinitions)(reachingDefsMap);
                reachingDefsMap.forEach(defs => {
                    checkSegment(defs);
                });
                reachingDefsMap.clear();
                variableUsages.clear();
                while (codePathStack.length > 0) {
                    codePathStack.pop();
                }
            },
            // CodePath events
            onCodePathSegmentStart: (segment) => {
                reachingDefsMap.set(segment.id, new index_js_1.ReachingDefinitions(segment));
                currentCodePathSegments.push(segment);
            },
            onCodePathStart: codePath => {
                pushContext(new CodePathContext(codePath));
                codePathSegments.push(currentCodePathSegments);
                currentCodePathSegments = [];
            },
            onCodePathEnd: () => {
                popContext();
                currentCodePathSegments = codePathSegments.pop() || [];
            },
            onCodePathSegmentEnd() {
                currentCodePathSegments.pop();
            },
        };
        function popAssignmentContext() {
            const assignment = peek(codePathStack).assignmentStack.pop();
            assignment.rhs.forEach(r => processReference(r));
            assignment.lhs.forEach(r => processReference(r));
        }
        function pushAssignmentContext(node) {
            peek(codePathStack).assignmentStack.push(new AssignmentContext(node));
        }
        function checkSegment(reachingDefs) {
            const assignedValuesMap = new Map(reachingDefs.in);
            reachingDefs.references.forEach(ref => {
                const variable = ref.resolved;
                if (!variable || !ref.isWrite() || !shouldReport(ref)) {
                    return;
                }
                const lhsValues = assignedValuesMap.get(variable);
                const rhsValues = (0, index_js_1.resolveAssignedValues)(variable, ref.writeExpr, assignedValuesMap, ref.from);
                if (lhsValues?.type === 'AssignedValues' && lhsValues?.size === 1) {
                    const [lhsVal] = [...lhsValues];
                    checkRedundantAssignement(ref, ref.writeExpr, lhsVal, rhsValues, variable.name);
                }
                assignedValuesMap.set(variable, rhsValues);
            });
        }
        function checkRedundantAssignement({ resolved: variable }, node, lhsVal, rhsValues, name) {
            if (rhsValues.type === 'UnknownValue' || rhsValues.size !== 1) {
                return;
            }
            const [rhsVal] = [...rhsValues];
            if (!isWrittenOnlyOnce(variable) && lhsVal === rhsVal) {
                context.report({
                    node: node,
                    messageId: 'reviewAssignment',
                    data: {
                        symbol: name,
                    },
                });
            }
        }
        // to avoid raising on code like:
        // while (cond) {  let x = 42; }
        function isWrittenOnlyOnce(variable) {
            return variable.references.filter(ref => ref.isWrite()).length === 1;
        }
        function shouldReport(ref) {
            const variable = ref.resolved;
            return variable && shouldReportReference(ref) && !variableUsedOutsideOfCodePath(variable);
        }
        function shouldReportReference(ref) {
            const variable = ref.resolved;
            return (variable &&
                !isDefaultParameter(ref) &&
                !variable.name.startsWith('_') &&
                !isCompoundAssignment(ref.writeExpr) &&
                !isSelfAssignement(ref) &&
                !variable.defs.some(def => def.type === 'Parameter' || (def.type === 'Variable' && !def.node.init)));
        }
        function isEnumConstant(node) {
            return context.sourceCode.getAncestors(node).some(n => n.type === 'TSEnumDeclaration');
        }
        function variableUsedOutsideOfCodePath(variable) {
            return variableUsages.get(variable).size > 1;
        }
        function checkIdentifierUsage(node) {
            const { ref, variable } = resolveReference(node);
            if (ref) {
                processReference(ref);
            }
            if (variable) {
                updateVariableUsages(variable);
            }
        }
        function processReference(ref) {
            const assignmentStack = peek(codePathStack).assignmentStack;
            if (assignmentStack.length > 0) {
                const assignment = peek(assignmentStack);
                assignment.add(ref);
            }
            else {
                currentCodePathSegments.forEach(segment => {
                    const reachingDefs = reachingDefsForSegment(segment);
                    reachingDefs.add(ref);
                });
            }
        }
        function reachingDefsForSegment(segment) {
            let defs;
            if (reachingDefsMap.has(segment.id)) {
                defs = reachingDefsMap.get(segment.id);
            }
            else {
                defs = new index_js_1.ReachingDefinitions(segment);
                reachingDefsMap.set(segment.id, defs);
            }
            return defs;
        }
        function updateVariableUsages(variable) {
            const codePathId = peek(codePathStack).codePath.id;
            if (variableUsages.has(variable)) {
                variableUsages.get(variable).add(codePathId);
            }
            else {
                variableUsages.set(variable, new Set([codePathId]));
            }
        }
        function pushContext(codePathContext) {
            codePathStack.push(codePathContext);
        }
        function popContext() {
            codePathStack.pop();
        }
        function resolveReferenceRecursively(node, scope) {
            if (scope === null) {
                return { ref: null, variable: null };
            }
            const ref = scope.references.find(r => r.identifier === node);
            if (ref) {
                return { ref, variable: ref.resolved };
            }
            else {
                // if it's not a reference, it can be just declaration without initializer
                const variable = scope.variables.find(v => v.defs.find(def => def.name === node));
                if (variable) {
                    return { ref: null, variable };
                }
                // in theory we only need 1-level recursion, only for switch expression, which is likely a bug in eslint
                // generic recursion is used for safety & readability
                return resolveReferenceRecursively(node, scope.upper);
            }
        }
        function resolveReference(node) {
            return resolveReferenceRecursively(node, context.sourceCode.getScope(node));
        }
    },
};
class CodePathContext {
    constructor(codePath) {
        this.reachingDefinitionsMap = new Map();
        this.reachingDefinitionsStack = [];
        this.segments = new Map();
        this.assignmentStack = [];
        this.codePath = codePath;
    }
}
class AssignmentContext {
    constructor(node) {
        this.lhs = new Set();
        this.rhs = new Set();
        this.node = node;
    }
    isRhs(node) {
        return this.node.type === 'AssignmentExpression'
            ? this.node.right === node
            : this.node.init === node;
    }
    isLhs(node) {
        return this.node.type === 'AssignmentExpression'
            ? this.node.left === node
            : this.node.id === node;
    }
    add(ref) {
        let parent = ref.identifier;
        while (parent) {
            if (this.isLhs(parent)) {
                this.lhs.add(ref);
                break;
            }
            if (this.isRhs(parent)) {
                this.rhs.add(ref);
                break;
            }
            parent = parent.parent;
        }
        if (parent === null) {
            throw new Error('failed to find assignment lhs/rhs');
        }
    }
}
function peek(arr) {
    return arr[arr.length - 1];
}
function isSelfAssignement(ref) {
    const lhs = ref.resolved;
    if (ref.writeExpr?.type === 'Identifier') {
        const rhs = (0, index_js_1.getVariableFromIdentifier)(ref.writeExpr, ref.from);
        return lhs === rhs;
    }
    return false;
}
function isCompoundAssignment(writeExpr) {
    if (writeExpr?.hasOwnProperty('parent')) {
        const node = writeExpr.parent;
        return node && node.type === 'AssignmentExpression' && node.operator !== '=';
    }
    return false;
}
function isDefaultParameter(ref) {
    if (ref.identifier.type !== 'Identifier') {
        return false;
    }
    const parent = ref.identifier.parent;
    return parent && parent.type === 'AssignmentPattern';
}