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';
}
;