eslint-plugin-sonarjs
Version:
SonarJS rules for ESLint
145 lines (144 loc) • 5.38 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/S1515/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");
const message = 'Make sure this function is not called after the loop completes.';
const loopLike = 'WhileStatement,DoWhileStatement,ForStatement,ForOfStatement,ForInStatement';
const functionLike = 'FunctionDeclaration,FunctionExpression,ArrowFunctionExpression';
const allowedCallbacks = [
'replace',
'forEach',
'filter',
'map',
'find',
'findIndex',
'every',
'some',
'reduce',
'reduceRight',
'sort',
'each',
];
exports.rule = {
meta: (0, index_js_1.generateMeta)(meta_js_1.meta, undefined, true),
create(context) {
function getLocalEnclosingLoop(node) {
return (0, index_js_1.findFirstMatchingAncestor)(node, n => loopLike.includes(n.type));
}
return {
[functionLike]: (node) => {
const loopNode = getLocalEnclosingLoop(node);
if (loopNode &&
!isIIEF(node, context) &&
!isAllowedCallbacks(context, node) &&
context.sourceCode.getScope(node).through.some(ref => !isSafe(ref, loopNode))) {
(0, index_js_1.report)(context, {
message,
loc: (0, index_js_1.getMainFunctionTokenLocation)(node, (0, index_js_1.getParent)(context, node), context),
}, [(0, index_js_1.toSecondaryLocation)(getMainLoopToken(loopNode, context))]);
}
},
};
},
};
function isIIEF(node, context) {
const parent = (0, index_js_1.getParent)(context, node);
return (parent &&
((parent.type === 'CallExpression' && parent.callee === node) ||
(parent.type === 'MemberExpression' && parent.object === node)));
}
function isAllowedCallbacks(context, node) {
const parent = (0, index_js_1.getParent)(context, node);
if (parent && parent.type === 'CallExpression') {
const callee = parent.callee;
if (callee.type === 'MemberExpression') {
return (callee.property.type === 'Identifier' && allowedCallbacks.includes(callee.property.name));
}
}
return false;
}
function isSafe(ref, loopNode) {
const variable = ref.resolved;
if (variable) {
const definition = variable.defs[0];
const declaration = definition?.parent;
const kind = declaration && declaration.type === 'VariableDeclaration' ? declaration.kind : '';
if (kind !== 'let' && kind !== 'const') {
return hasConstValue(variable, loopNode);
}
}
return true;
}
function hasConstValue(variable, loopNode) {
for (const ref of variable.references) {
if (ref.isWrite()) {
//Check if write is in the scope of the loop
if (ref.from.type === 'block' && ref.from.block === loopNode.body) {
return false;
}
const refRange = ref.identifier.range;
const range = getLoopTestRange(loopNode);
//Check if value change in the header of the loop
if (refRange && range && refRange[0] >= range[0] && refRange[1] <= range[1]) {
return false;
}
}
}
return true;
}
function getLoopTestRange(loopNode) {
const bodyRange = loopNode.body.range;
if (bodyRange) {
switch (loopNode.type) {
case 'ForStatement':
if (loopNode.test?.range) {
return [loopNode.test.range[0], bodyRange[0]];
}
break;
case 'WhileStatement':
case 'DoWhileStatement':
return loopNode.test.range;
case 'ForOfStatement':
case 'ForInStatement': {
const leftRange = loopNode.range;
if (leftRange) {
return [leftRange[0], bodyRange[0]];
}
}
}
}
return undefined;
}
function getMainLoopToken(loop, context) {
const sourceCode = context.sourceCode;
let token;
switch (loop.type) {
case 'WhileStatement':
case 'DoWhileStatement':
token = sourceCode.getTokenBefore(loop.test, t => t.type === 'Keyword' && t.value === 'while');
break;
case 'ForStatement':
case 'ForOfStatement':
default:
token = sourceCode.getFirstToken(loop, t => t.type === 'Keyword' && t.value === 'for');
}
return token;
}
;