eslint-plugin-sonarjs
Version:
266 lines (265 loc) • 10.9 kB
JavaScript
;
/*
* 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/S1244/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 generate_meta_js_1 = require("../helpers/generate-meta.js");
const ast_js_1 = require("../helpers/ast.js");
const equivalence_js_1 = require("../helpers/equivalence.js");
const assertions_js_1 = require("../helpers/assertions.js");
const numbers_js_1 = require("../helpers/numbers.js");
const meta = __importStar(require("./generated-meta.js"));
const equalityOperators = new Set(['===', '!==', '==', '!=']);
const arithmeticOperators = new Set(['+', '-', '*', '%', '**']);
const constantArithmeticOperators = new Set(['+', '-', '*', '/', '%', '**']);
const indirectEqualityOperators = new Set(['<=', '>=']);
const indirectInequalityOperators = new Set(['<', '>']);
exports.rule = {
meta: (0, generate_meta_js_1.generateMeta)(meta, {
messages: {
noExactFloatEquality: 'Do not check floating point equality or inequality with exact values, use a range instead.',
},
}),
create(context) {
function report(node) {
context.report({
messageId: 'noExactFloatEquality',
node,
});
}
function isFloatingPointSensitive(node) {
return isFloatingPointExpression(context, node, new Set());
}
function shouldReportComparison(node) {
return (equalityOperators.has(node.operator) &&
(isFloatingPointSensitive(node.left) || isFloatingPointSensitive(node.right)));
}
return {
BinaryExpression(node) {
const binaryExpression = node;
if (shouldReportComparison(binaryExpression)) {
report(binaryExpression);
}
},
LogicalExpression(node) {
const logicalExpression = node;
if (isIndirectExactComparison(context, logicalExpression, isFloatingPointSensitive)) {
report(logicalExpression);
}
},
CallExpression(node) {
const assertion = (0, assertions_js_1.extractTestAssertion)(context, node);
if (assertion?.kind === 'comparison' &&
(isFloatingPointSensitive(assertion.actual) ||
isFloatingPointSensitive(assertion.expected))) {
report(assertion.reportNode);
}
},
SwitchCase(node) {
const { test } = node;
if (test && isFloatingPointSensitive(test)) {
report(test);
}
},
};
},
};
function isFloatingPointLiteral(node) {
const raw = String(node.raw).replaceAll('_', '').toLowerCase();
if (!raw.includes('.') && !raw.includes('e')) {
return false;
}
if (!/^\d*(?:\.\d*)?(?:e[+-]?\d+)?$/u.test(raw)) {
return false;
}
return !(0, numbers_js_1.isExactlyRepresentableAsBinaryFraction)(raw);
}
function isFractionProducingDivision(node) {
const leftValue = numericLiteralValue(node.left);
const rightValue = numericLiteralValue(node.right);
if (leftValue === null || rightValue === null || rightValue === 0) {
return false;
}
const result = leftValue / rightValue;
return (Number.isInteger(leftValue) &&
!Number.isInteger(result) &&
!(0, numbers_js_1.isExactlyRepresentableIntegerDivision)(leftValue, rightValue));
}
function numericLiteralValue(node) {
if ((0, ast_js_1.isNumberLiteral)(node)) {
return node.value;
}
if (node.type === 'UnaryExpression' &&
(node.operator === '+' || node.operator === '-') &&
(0, ast_js_1.isNumberLiteral)(node.argument)) {
return node.operator === '-' ? -node.argument.value : node.argument.value;
}
return null;
}
function numericExpressionValue(node) {
const literalValue = numericLiteralValue(node);
if (literalValue !== null) {
return literalValue;
}
if (node.type !== 'BinaryExpression' || !constantArithmeticOperators.has(node.operator)) {
return null;
}
const leftValue = numericExpressionValue(node.left);
const rightValue = numericExpressionValue(node.right);
if (leftValue === null || rightValue === null) {
return null;
}
switch (node.operator) {
case '+':
return leftValue + rightValue;
case '-':
return leftValue - rightValue;
case '*':
return leftValue * rightValue;
case '/':
return rightValue === 0 ? null : leftValue / rightValue;
case '%':
return rightValue === 0 ? null : leftValue % rightValue;
case '**':
return leftValue ** rightValue;
default:
return null;
}
}
function isFloatingPointConst(context, node, visitedVariables) {
const variable = (0, ast_js_1.getVariableFromName)(context, node.name, node);
if (!variable || visitedVariables.has(variable)) {
return false;
}
const definition = variable.defs[0];
if (definition?.type !== 'Variable' || definition.node.type !== 'VariableDeclarator') {
return false;
}
const declaration = definition.parent;
if (declaration?.type !== 'VariableDeclaration' ||
declaration.kind !== 'const' ||
definition.node.id.type !== 'Identifier' ||
!definition.node.init) {
return false;
}
visitedVariables.add(variable);
return isFloatingPointExpression(context, definition.node.init, visitedVariables);
}
function isFloatingPointExpression(context, node, visitedVariables) {
if ((0, ast_js_1.isNumberLiteral)(node)) {
return isFloatingPointLiteral(node);
}
if (node.type === 'UnaryExpression') {
return (['+', '-'].includes(node.operator) &&
isFloatingPointExpression(context, node.argument, visitedVariables));
}
if (node.type === 'BinaryExpression') {
const numericValue = numericExpressionValue(node);
if (numericValue !== null && Number.isSafeInteger(numericValue)) {
return false;
}
if (node.operator === '/') {
return (isFloatingPointExpression(context, node.left, visitedVariables) ||
isFloatingPointExpression(context, node.right, visitedVariables) ||
isFractionProducingDivision(node));
}
return (arithmeticOperators.has(node.operator) &&
(isFloatingPointExpression(context, node.left, visitedVariables) ||
isFloatingPointExpression(context, node.right, visitedVariables)));
}
return (0, ast_js_1.isIdentifier)(node) && isFloatingPointConst(context, node, visitedVariables);
}
function isIndirectExactComparison(context, node, isFloatingPointSensitive) {
const acceptedOperators = acceptedIndirectOperators(node.operator);
const { left: leftComparison, right: rightComparison } = node;
if (!acceptedOperators ||
!isAcceptedComparison(leftComparison, acceptedOperators) ||
!isAcceptedComparison(rightComparison, acceptedOperators)) {
return false;
}
return comparisonOrientations(leftComparison).some(left => comparisonOrientations(rightComparison).some(right => left.isAbove !== right.isAbove &&
(0, equivalence_js_1.areEquivalent)(left.expression, right.expression, context.sourceCode) &&
(0, equivalence_js_1.areEquivalent)(left.threshold, right.threshold, context.sourceCode) &&
(isFloatingPointSensitive(left.expression) || isFloatingPointSensitive(left.threshold))));
}
function acceptedIndirectOperators(operator) {
// && of two <=/>= comparisons collapses to equality; || of two strict </> comparisons
// asserts inequality. Keep operator sets, this mapping, and comparisonOrientations in sync.
if (operator === '&&') {
return indirectEqualityOperators;
}
if (operator === '||') {
return indirectInequalityOperators;
}
return null;
}
function isAcceptedComparison(node, acceptedOperators) {
return node.type === 'BinaryExpression' && acceptedOperators.has(node.operator);
}
function comparisonOrientations(node) {
const { left, right } = node;
switch (node.operator) {
case '<':
case '<=':
return [
{ expression: left, threshold: right, isAbove: false },
{ expression: right, threshold: left, isAbove: true },
];
case '>':
case '>=':
return [
{ expression: left, threshold: right, isAbove: true },
{ expression: right, threshold: left, isAbove: false },
];
default:
return [];
}
}