eslint-plugin-sonarjs
Version:
140 lines (139 loc) • 6.29 kB
JavaScript
"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/
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.isSortReturnedByComparedNormalizer = isSortReturnedByComparedNormalizer;
const ancestor_js_1 = require("../../helpers/ancestor.js");
const ast_js_1 = require("../../helpers/ast.js");
const helpers_js_1 = require("./helpers.js");
/**
* False-positive shapes:
* function normalize(values: string[]) {
* return values.map(String).sort();
* }
* JSON.stringify(normalize(a)) === JSON.stringify(normalize(b))
*
* const normalize = (values: string[]) => values.toSorted();
* JSON.stringify(normalize(a)) !== JSON.stringify(normalize(b))
*
* function normalize(values: number[]) {
* return values.sort();
* }
* JSON.stringify(normalize(before)) == JSON.stringify(normalize(after))
*
* What can vary:
* - function declaration vs local function expression / arrow function
* - `sort()` vs `toSorted()` inside the helper
* - the equality operator used around the JSON.stringify comparison
*
* The inner sort call is acceptable when the helper is only used as a
* normalizer inside matching JSON.stringify equality/inequality comparisons.
*/
function isSortReturnedByComparedNormalizer(call, ruleContext) {
const callInfo = (0, helpers_js_1.getComparatorlessSortCallInfo)(call, ruleContext);
const functionVariable = getEnclosingReturnedFunctionVariable(call, ruleContext);
return (callInfo !== null &&
(0, helpers_js_1.isPrimitiveSortReceiver)(callInfo.receiver, ruleContext) &&
functionVariable !== null &&
isUsedOnlyInJsonStringifyComparisons(functionVariable, ruleContext));
}
function getEnclosingReturnedFunctionVariable(call, ruleContext) {
let currentNode = (0, ancestor_js_1.getNodeParent)(call);
while (currentNode) {
if (currentNode.type === 'FunctionDeclaration' ||
currentNode.type === 'FunctionExpression' ||
currentNode.type === 'ArrowFunctionExpression') {
if (!isTrivialSingleReturnNormalizer(currentNode, call)) {
return null;
}
return getLocalFunctionVariable(currentNode, ruleContext);
}
currentNode = (0, ancestor_js_1.getNodeParent)(currentNode);
}
return null;
}
function isTrivialSingleReturnNormalizer(functionNode, returnedCall) {
if (functionNode.params.length !== 1) {
return false;
}
if (isReturnedExpressionBody(functionNode, returnedCall)) {
return true;
}
if (functionNode.body.type !== 'BlockStatement' || functionNode.body.body.length !== 1) {
return false;
}
const [statement] = functionNode.body.body;
return statement.type === 'ReturnStatement' && statement.argument === returnedCall;
}
function isReturnedExpressionBody(functionNode, childNode) {
return (functionNode.type === 'ArrowFunctionExpression' &&
functionNode.body.type !== 'BlockStatement' &&
functionNode.body === childNode);
}
function getLocalFunctionVariable(functionNode, { context }) {
const parent = (0, ancestor_js_1.getNodeParent)(functionNode);
if (parent?.type === 'ExportNamedDeclaration' || parent?.type === 'ExportDefaultDeclaration') {
return null;
}
if (functionNode.type === 'FunctionDeclaration') {
if (functionNode.id === null) {
return null;
}
return (0, ast_js_1.getVariableFromName)(context, functionNode.id.name, functionNode) ?? null;
}
if (parent?.type === 'VariableDeclarator' && (0, ast_js_1.isIdentifier)(parent.id)) {
const variableDeclaration = (0, ancestor_js_1.getNodeParent)(parent);
if (variableDeclaration?.type === 'VariableDeclaration') {
const declarationParent = (0, ancestor_js_1.getNodeParent)(variableDeclaration);
if (declarationParent?.type === 'ExportNamedDeclaration' ||
declarationParent?.type === 'ExportDefaultDeclaration') {
return null;
}
}
return (0, ast_js_1.getVariableFromName)(context, parent.id.name, parent) ?? null;
}
return null;
}
function isUsedOnlyInJsonStringifyComparisons(functionVariable, ruleContext) {
const reads = functionVariable.references.filter(reference => reference.isRead());
return (reads.length > 0 &&
reads.every(reference => {
const parent = (0, ancestor_js_1.getNodeParent)(reference.identifier);
return (parent?.type === 'CallExpression' &&
parent.callee === reference.identifier &&
isSingleArgumentJsonStringifyFunctionCallComparison(parent, functionVariable, ruleContext));
}));
}
function isSingleArgumentJsonStringifyFunctionCallComparison(call, functionVariable, { context }) {
if (call.arguments.length !== 1 || call.arguments[0].type === 'SpreadElement') {
return false;
}
const parent = (0, ancestor_js_1.getNodeParent)(call);
if (!(0, helpers_js_1.isJsonStringifyCall)(parent) || parent.arguments[0] !== call) {
return false;
}
const sibling = (0, helpers_js_1.getEqualityComparisonSibling)(parent);
if (!(0, helpers_js_1.isJsonStringifyCall)(sibling)) {
return false;
}
const siblingArgument = sibling.arguments[0];
return (siblingArgument.type === 'CallExpression' &&
siblingArgument.arguments.length === 1 &&
siblingArgument.arguments[0].type !== 'SpreadElement' &&
siblingArgument.callee.type === 'Identifier' &&
(0, ast_js_1.getVariableFromName)(context, siblingArgument.callee.name, siblingArgument) === functionVariable);
}