eslint-plugin-sonarjs
Version:
163 lines (162 loc) • 6.57 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.allSortLike = void 0;
exports.isJsonStringifyCall = isJsonStringifyCall;
exports.getEqualityComparisonSibling = getEqualityComparisonSibling;
exports.getChainedMethodCall = getChainedMethodCall;
exports.isStringMapCall = isStringMapCall;
exports.getJoinSeparator = getJoinSeparator;
exports.getComparatorlessSortCallInfo = getComparatorlessSortCallInfo;
exports.isPrimitiveSortReceiver = isPrimitiveSortReceiver;
const ancestor_js_1 = require("../../helpers/ancestor.js");
const collection_js_1 = require("../../helpers/collection.js");
const ast_js_1 = require("../../helpers/ast.js");
const type_js_1 = require("../../helpers/type.js");
/**
* Names of array sorting methods covered by S2871.
*
* This includes mutating sort-like methods such as `sort` and copying
* sort-like methods such as `toSorted`.
*/
exports.allSortLike = new Set([...collection_js_1.sortLike, ...collection_js_1.copyingSortLike]);
const equalityOperators = new Set(['==', '!=', '===', '!==']);
/**
* Checks whether a node is a direct `JSON.stringify(<expr>)` call.
*
* The match is intentionally narrow: exactly one argument, non-computed member
* access, and the global-looking `JSON` identifier as receiver. It does not
* match calls with replacer/space arguments, `JSON['stringify'](...)`, or
* custom objects exposing a `stringify` method.
*/
function isJsonStringifyCall(node) {
return (node !== null && (0, ast_js_1.isStaticMethodCall)(node, 'JSON', 'stringify') && node.arguments.length === 1);
}
/**
* Returns the other side of an equality or inequality comparison.
*
* If `node` is not directly inside `==`, `!=`, `===`, or `!==`, there is no
* comparison sibling to inspect and the function returns `null`.
*/
function getEqualityComparisonSibling(node) {
const parent = (0, ancestor_js_1.getNodeParent)(node);
if (parent.type !== 'BinaryExpression' || !equalityOperators.has(parent.operator)) {
return null;
}
return parent.left === node ? parent.right : parent.left;
}
/**
* Returns a method call chained directly after an expression.
*
* For `object` representing `value.sort()`, asking for `map` matches
* `value.sort().map(...)` and returns the `map(...)` call expression. Computed
* member access such as `value.sort()['map'](...)` is intentionally ignored.
*/
function getChainedMethodCall(object, methodName) {
const member = (0, ancestor_js_1.getNodeParent)(object);
if (member.type !== 'MemberExpression' ||
member.object !== object ||
member.computed ||
!(0, ast_js_1.isIdentifier)(member.property, methodName)) {
return null;
}
const call = (0, ancestor_js_1.getNodeParent)(member);
if (call.type !== 'CallExpression' || call.callee !== member) {
return null;
}
return call;
}
/**
* Checks whether a call expression is `array.map(String)`.
*
* This recognizes the explicit conversion to strings that makes subsequent
* default sorting intentional in the false-positive patterns handled by S2871.
* Computed access such as `array[map](String)` is intentionally excluded.
*/
function isStringMapCall(call) {
return (call.arguments.length === 1 &&
(0, ast_js_1.isIdentifier)(call.arguments[0], 'String') &&
call.callee.type === 'MemberExpression' &&
!call.callee.computed &&
(0, ast_js_1.isIdentifier)(call.callee.property, 'map'));
}
/**
* Extracts the separator from a `join` call when it is statically known.
*
* `join()` is treated as `join(',')`, matching JavaScript's default separator.
* A single string literal argument is returned as-is. Other calls, such as
* `join(separator)` or `join(',', extra)`, return `null` because the comparison
* pattern can no longer prove both sides serialize identically.
*/
function getJoinSeparator(call) {
if (call.arguments.length === 0) {
return ',';
}
if (call.arguments.length === 1 &&
call.arguments[0].type === 'Literal' &&
typeof call.arguments[0].value === 'string') {
return call.arguments[0].value;
}
return null;
}
/**
* Extracts information from a comparator-less `sort()` or `toSorted()` call.
*
* The call must have no arguments, use a known sort-like method name, and be
* invoked on an array-like receiver according to TypeScript type information.
*/
function getComparatorlessSortCallInfo(node, { sourceCode, services }) {
if (node.type !== 'CallExpression') {
return null;
}
const callExpr = node;
if (callExpr.arguments.length !== 0) {
return null;
}
if (callExpr.callee.type !== 'MemberExpression') {
return null;
}
const callee = callExpr.callee;
const text = sourceCode.getText(callee.property);
if (!exports.allSortLike.has(text)) {
return null;
}
const receiverType = (0, type_js_1.getTypeFromTreeNode)(callee.object, services);
if (!(0, type_js_1.isArrayLikeType)(receiverType, services)) {
return null;
}
return {
methodName: text,
receiver: callee.object,
};
}
/**
* Checks whether a sort receiver is an array of primitive values with safe
* normalization semantics for the S2871 false-positive patterns.
*
* Only number, bigint, string, and boolean arrays are accepted. Arrays of
* objects, unknown values, unions, or custom wrappers still require S2871 to
* report comparator-less sorting.
*/
function isPrimitiveSortReceiver(receiver, { services }) {
const receiverType = (0, type_js_1.getTypeFromTreeNode)(receiver, services);
return ((0, type_js_1.isNumberArray)(receiverType, services) ||
(0, type_js_1.isBigIntArray)(receiverType, services) ||
(0, type_js_1.isStringArray)(receiverType, services) ||
(0, type_js_1.isBooleanArray)(receiverType, services));
}