UNPKG

eslint-plugin-sonarjs

Version:
140 lines (139 loc) 6.29 kB
"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); }