eslint-plugin-sonarjs
Version:
SonarJS rules for ESLint
163 lines (162 loc) • 7.45 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/S3516/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, undefined, true),
create(context) {
const functionContextStack = [];
const codePathSegments = [];
let currentCodePathSegments = [];
const checkOnFunctionExit = (node) => checkInvariantReturnStatements(node, functionContextStack[functionContextStack.length - 1]);
function checkInvariantReturnStatements(node, functionContext) {
if (!functionContext || hasDifferentReturnTypes(functionContext, currentCodePathSegments)) {
return;
}
const returnedValues = functionContext.returnStatements.map(returnStatement => returnStatement.argument);
if (areAllSameValue(returnedValues, context.sourceCode.getScope(node))) {
(0, index_js_1.report)(context, {
message: `Refactor this function to not always return the same value.`,
loc: (0, index_js_1.getMainFunctionTokenLocation)(node, (0, index_js_1.getParent)(context, node), context),
}, returnedValues.map(node => (0, index_js_1.toSecondaryLocation)(node, 'Returned value.')), returnedValues.length);
}
}
return {
onCodePathStart(codePath) {
functionContextStack.push({
codePath,
containsReturnWithoutValue: false,
returnStatements: [],
});
codePathSegments.push(currentCodePathSegments);
currentCodePathSegments = [];
},
onCodePathEnd() {
functionContextStack.pop();
currentCodePathSegments = codePathSegments.pop() || [];
},
onCodePathSegmentStart: (segment) => {
currentCodePathSegments.push(segment);
},
onCodePathSegmentEnd() {
currentCodePathSegments.pop();
},
ReturnStatement(node) {
const currentContext = functionContextStack[functionContextStack.length - 1];
if (currentContext) {
const returnStatement = node;
currentContext.containsReturnWithoutValue =
currentContext.containsReturnWithoutValue || !returnStatement.argument;
currentContext.returnStatements.push(returnStatement);
}
},
'FunctionDeclaration:exit': checkOnFunctionExit,
'FunctionExpression:exit': checkOnFunctionExit,
'ArrowFunctionExpression:exit': checkOnFunctionExit,
};
},
};
function hasDifferentReturnTypes(functionContext, currentSegments) {
// As this method is called at the exit point of a function definition, the current
// segments are the ones leading to the exit point at the end of the function. If they
// are reachable, it means there is an implicit return.
const hasImplicitReturn = currentSegments.some(segment => segment.reachable);
return (hasImplicitReturn ||
functionContext.containsReturnWithoutValue ||
functionContext.returnStatements.length <= 1 ||
functionContext.codePath.thrownSegments.length > 0);
}
function areAllSameValue(returnedValues, scope) {
const firstReturnedValue = returnedValues[0];
const firstValue = getLiteralValue(firstReturnedValue, scope);
if (firstValue !== undefined) {
return returnedValues
.slice(1)
.every(returnedValue => getLiteralValue(returnedValue, scope) === firstValue);
}
else if (firstReturnedValue.type === 'Identifier') {
const singleWriteVariable = getSingleWriteDefinition(firstReturnedValue.name, scope);
if (singleWriteVariable) {
const readReferenceIdentifiers = singleWriteVariable.variable.references
.slice(1)
.map(ref => ref.identifier);
return returnedValues.every(returnedValue => readReferenceIdentifiers.includes(returnedValue));
}
}
return false;
}
function getSingleWriteDefinition(variableName, scope) {
const variable = scope.set.get(variableName);
if (variable) {
const references = variable.references.slice(1);
if (!references.some(ref => ref.isWrite() || isPossibleObjectUpdate(ref))) {
let initExpression = null;
if (variable.defs.length === 1 && variable.defs[0].type === 'Variable') {
initExpression = variable.defs[0].node.init;
}
return { variable, initExpression };
}
}
return null;
}
function isPossibleObjectUpdate(ref) {
const expressionStatement = (0, index_js_1.findFirstMatchingAncestor)(ref.identifier, n => n.type === 'ExpressionStatement' || index_js_1.FUNCTION_NODES.includes(n.type));
// To avoid FP, we consider method calls as write operations, since we do not know whether they will
// update the object state or not.
return (expressionStatement &&
expressionStatement.type === 'ExpressionStatement' &&
((0, index_js_1.isElementWrite)(expressionStatement, ref) ||
expressionStatement.expression.type === 'CallExpression'));
}
function getLiteralValue(returnedValue, scope) {
if (returnedValue.type === 'Literal') {
return returnedValue.value;
}
else if (returnedValue.type === 'UnaryExpression') {
const innerReturnedValue = getLiteralValue(returnedValue.argument, scope);
return innerReturnedValue !== undefined
? evaluateUnaryLiteralExpression(returnedValue.operator, innerReturnedValue)
: undefined;
}
else if (returnedValue.type === 'Identifier') {
const singleWriteVariable = getSingleWriteDefinition(returnedValue.name, scope);
if (singleWriteVariable?.initExpression) {
return getLiteralValue(singleWriteVariable.initExpression, scope);
}
}
return undefined;
}
function evaluateUnaryLiteralExpression(operator, innerReturnedValue) {
switch (operator) {
case '-':
return -Number(innerReturnedValue);
case '+':
return Number(innerReturnedValue);
case '~':
return ~Number(innerReturnedValue);
case '!':
return !Boolean(innerReturnedValue);
case 'typeof':
return typeof innerReturnedValue;
default:
return undefined;
}
}
;