eslint-plugin-sonarjs
Version:
105 lines (104 loc) • 4.39 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/
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.isSortUsedInStringJoinComparison = isSortUsedInStringJoinComparison;
const ast_js_1 = require("../../helpers/ast.js");
const type_js_1 = require("../../helpers/type.js");
const helpers_js_1 = require("./helpers.js");
/**
* False-positive shapes:
* a.slice().sort().map(String).join(',') === b.slice().sort().map(String).join(',')
* a.toSorted().map(String).join(',') !== b.toSorted().map(String).join(',')
* a.sort().map(String).join() == b.sort().map(String).join()
*
* What can vary:
* - `sort()` vs `toSorted()`, as long as both sides use the same sort family
* - the equality operator (`==`, `!=`, `===`, `!==`)
* - the join separator, as long as both sides use the same safe one
*
* Here the sort call is only part of a normalization pipeline used to compare
* two numeric arrays after converting both sides to the same canonical string.
*/
function isSortUsedInStringJoinComparison(call, ruleContext) {
const chainRoot = getEnclosingJoinCall(call);
if (chainRoot === null) {
return false;
}
const callInfo = getStringJoinSortChainInfo(chainRoot, ruleContext);
const siblingInfo = getStringJoinSortChainInfo((0, helpers_js_1.getEqualityComparisonSibling)(chainRoot), ruleContext);
return (callInfo !== null &&
siblingInfo !== null &&
callInfo.methodName === siblingInfo.methodName &&
callInfo.joinSeparator === siblingInfo.joinSeparator &&
isSafeNumericJoinSeparator(callInfo.joinSeparator) &&
isNumericSortReceiver(callInfo.receiver, ruleContext) &&
isNumericSortReceiver(siblingInfo.receiver, ruleContext));
}
function isSafeNumericJoinSeparator(separator) {
return separator.length > 0 && !/[0-9+\-.eEn]/.test(separator);
}
function isNumericSortReceiver(receiver, ruleContext) {
const receiverType = (0, type_js_1.getTypeFromTreeNode)(receiver, ruleContext.services);
return ((0, type_js_1.isNumberArray)(receiverType, ruleContext.services) ||
(0, type_js_1.isBigIntArray)(receiverType, ruleContext.services));
}
/**
* Climbs the `sort() -> map(...) -> join(...)` chain from a comparator-less
* sort call to the enclosing `join(...)` call — the root of the serialization
* pipeline and the operand of the comparison.
*
* No shape validation happens here on purpose: `getStringJoinSortChainInfo` is
* the single place that decides whether a `join(...)` call is an accepted
* normalization chain, so both comparison sides are validated identically.
*/
function getEnclosingJoinCall(sortCall) {
const mapCall = (0, helpers_js_1.getChainedMethodCall)(sortCall, 'map');
if (mapCall === null) {
return null;
}
return (0, helpers_js_1.getChainedMethodCall)(mapCall, 'join');
}
function getStringJoinSortChainInfo(node, ruleContext) {
if (node?.type !== 'CallExpression') {
return null;
}
if (node.callee.type !== 'MemberExpression' ||
node.callee.computed ||
!(0, ast_js_1.isIdentifier)(node.callee.property, 'join')) {
return null;
}
const joinSeparator = (0, helpers_js_1.getJoinSeparator)(node);
if (joinSeparator === null) {
return null;
}
const mapCall = node.callee.object;
if (mapCall.type !== 'CallExpression' || !(0, helpers_js_1.isStringMapCall)(mapCall)) {
return null;
}
if (mapCall.callee.type !== 'MemberExpression') {
return null;
}
const sortInfo = (0, helpers_js_1.getComparatorlessSortCallInfo)(mapCall.callee.object, ruleContext);
if (sortInfo === null) {
return null;
}
return {
...sortInfo,
joinSeparator,
};
}