UNPKG

eslint-plugin-sonarjs

Version:
397 lines (396 loc) 15.5 kB
"use strict"; /* * SonarQube JavaScript Plugin * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * You can redistribute and/or modify this program under the terms of * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. * * 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 var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.rule = void 0; const reaching_definitions_js_1 = require("../helpers/reaching-definitions.js"); const generate_meta_js_1 = require("../helpers/generate-meta.js"); const collection_js_1 = require("../helpers/collection.js"); const meta = __importStar(require("./generated-meta.js")); exports.rule = { meta: (0, generate_meta_js_1.generateMeta)(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, reaching_definitions_js_1.reachingDefinitions)(reachingDefsMap); for (const defs of reachingDefsMap.values()) { checkSegment(defs); } reachingDefsMap.clear(); variableUsages.clear(); while (codePathStack.length > 0) { codePathStack.pop(); } }, // CodePath events onCodePathSegmentStart: (segment) => { reachingDefsMap.set(segment.id, new reaching_definitions_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 = (0, collection_js_1.last)(codePathStack).assignmentStack.pop(); for (const r of assignment.rhs) { processReference(r); } for (const r of assignment.lhs) { processReference(r); } } function pushAssignmentContext(node) { (0, collection_js_1.last)(codePathStack).assignmentStack.push(new AssignmentContext(node)); } function checkSegment(reachingDefs) { const assignedValuesMap = new Map(reachingDefs.in); for (const ref of reachingDefs.references) { const variable = ref.resolved; if (!variable || !ref.isWrite() || !shouldReport(ref)) { continue; } const lhsValues = assignedValuesMap.get(variable); const rhsValues = (0, reaching_definitions_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) && !isInForLoopInit(ref) && !isDeclarationInsideLoopBody(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 = (0, collection_js_1.last)(codePathStack).assignmentStack; if (assignmentStack.length > 0) { const assignment = (0, collection_js_1.last)(assignmentStack); assignment.add(ref); } else { for (const segment of currentCodePathSegments) { 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 reaching_definitions_js_1.ReachingDefinitions(segment); reachingDefsMap.set(segment.id, defs); } return defs; } function updateVariableUsages(variable) { const codePathId = (0, collection_js_1.last)(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 isSelfAssignement(ref) { const lhs = ref.resolved; if (ref.writeExpr?.type === 'Identifier') { const rhs = (0, reaching_definitions_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?.type === 'AssignmentExpression' && node.operator !== '='; } return false; } function isDefaultParameter(ref) { if (ref.identifier.type !== 'Identifier') { return false; } const parent = ref.identifier.parent; return parent?.type === 'AssignmentPattern'; } /** * Checks if an assignment is part of a for-loop initialization. * For-loop initializations (e.g., `for (i = 0; ...)`) are idiomatic patterns * that should not be flagged as redundant, even if the variable already holds * the same value. */ function isInForLoopInit(ref) { const writeExpr = ref.writeExpr; if (!writeExpr) { return false; } let current = writeExpr; while (current) { const parent = current.parent; if (!parent) { break; } if (parent.type === 'ForStatement' && parent.init === current) { return true; } // Stop at function boundaries if (parent.type === 'FunctionDeclaration' || parent.type === 'FunctionExpression' || parent.type === 'ArrowFunctionExpression') { break; } current = parent; } return false; } /** * Checks if a variable declaration with initialization is inside a loop body. * Variable declarations inside loop bodies (e.g., `while (cond) { var x = 0; }`) * should not be flagged as redundant because each iteration reinitializes the variable. * For `let`/`const`, each iteration creates a new binding. * For `var`, the initialization still runs each iteration and is idiomatic. */ function isDeclarationInsideLoopBody(ref) { const variable = ref.resolved; if (!variable) { return false; } // Find the variable declaration that corresponds to this write reference const varDef = variable.defs.find(def => def.type === 'Variable' && def.node.init && def.name === ref.identifier); if (!varDef) { return false; } // Get the VariableDeclaration node (parent of VariableDeclarator) const varDeclarator = varDef.node; const varDeclaration = varDeclarator.parent; if (varDeclaration?.type !== 'VariableDeclaration') { return false; } // Traverse up to find if this declaration is inside a loop body let current = varDeclaration; while (current) { const parent = current.parent; if (!parent) { break; } // Check if we're inside a loop body if ((parent.type === 'ForStatement' && parent.body === current) || (parent.type === 'ForInStatement' && parent.body === current) || (parent.type === 'ForOfStatement' && parent.body === current) || (parent.type === 'WhileStatement' && parent.body === current) || (parent.type === 'DoWhileStatement' && parent.body === current)) { return true; } // Stop at function boundaries if (parent.type === 'FunctionDeclaration' || parent.type === 'FunctionExpression' || parent.type === 'ArrowFunctionExpression') { break; } current = parent; } return false; }