UNPKG

eslint-plugin-sonarjs

Version:
280 lines (279 loc) 13.5 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.getBooleanExpressionSuggestion = getBooleanExpressionSuggestion; exports.chaiShouldReceiver = chaiShouldReceiver; const ast_js_1 = require("../helpers/ast.js"); const assertion_utils_js_1 = require("./assertion-utils.js"); const STRICT_EQUALITY_OPERATORS = new Set(['===', '!==']); const STRING_LIKE_IDENTIFIER = /(?:text|string|message|content|html)$/i; const NUMERIC_IDENTIFIER = /(?:amount|count|delta|depth|diff|duration|elapsed|height|index|length|level|limit|number|price|score|size|total|width)$/i; const DATE_LIKE_IDENTIFIER = /(?:date|time|timestamp)$/i; function getBooleanExpressionSuggestion(actual, positive, family, sourceCode, node, assertObject = 'assert', extraArguments = '') { if (actual.type === 'BinaryExpression') { return getBinaryExpressionSuggestion(actual, positive, family, sourceCode, node, assertObject, extraArguments); } return getIncludesSuggestion(actual, positive, family, sourceCode, node, assertObject, extraArguments); } function chaiShouldReceiver(source, node) { return isSafeChaiShouldReceiver(node) ? source : `(${source})`; } const SAFE_CHAI_SHOULD_RECEIVER_TYPES = new Set([ 'Identifier', 'MemberExpression', 'CallExpression', 'ThisExpression', ]); function isSafeChaiShouldReceiver(node) { return SAFE_CHAI_SHOULD_RECEIVER_TYPES.has(node.type); } function getBinaryExpressionSuggestion(actual, positive, family, sourceCode, node, assertObject, extraArguments) { const left = sourceCode.getText(actual.left); const right = sourceCode.getText(actual.right); const suggestionContext = { family, sourceCode, node, assertObject, extraArguments }; if (STRICT_EQUALITY_OPERATORS.has(actual.operator)) { const same = actual.operator === '===' ? positive : !positive; const specificSuggestion = buildSpecificEqualitySuggestion(actual, same, suggestionContext); if (specificSuggestion !== undefined) { return specificSuggestion; } return buildEqualitySuggestion(actual.left, right, same, suggestionContext); } if (actual.operator === 'instanceof') { return buildInstanceofSuggestion(left, actual.left, right, positive, suggestionContext); } const comparison = isNumericComparison(actual) ? getNumericComparison(actual.operator, positive) : null; if (comparison) { return buildNumericComparisonSuggestion(left, actual.left, right, comparison, suggestionContext); } return null; } function buildSpecificEqualitySuggestion(actual, same, context) { const leftNullish = getNullishKind(actual.left); if (leftNullish) { return buildNullishEqualitySuggestion(actual.right, leftNullish, same, context); } const rightNullish = getNullishKind(actual.right); if (rightNullish) { return buildNullishEqualitySuggestion(actual.left, rightNullish, same, context); } if ((0, assertion_utils_js_1.isLengthAccess)(actual.left)) { return buildLengthEqualitySuggestion(actual.left.object, context.sourceCode.getText(actual.right), same, context); } if ((0, assertion_utils_js_1.isLengthAccess)(actual.right)) { return buildLengthEqualitySuggestion(actual.right.object, context.sourceCode.getText(actual.left), same, context); } return undefined; } function getNullishKind(node) { if ((0, assertion_utils_js_1.isNullLiteral)(node)) { return 'null'; } if ((0, assertion_utils_js_1.isUndefinedExpression)(node)) { return 'undefined'; } return null; } function buildNullishEqualitySuggestion(actualNode, nullish, same, context) { const { family, sourceCode, node, assertObject, extraArguments } = context; const actual = sourceCode.getText(actualNode); if (family === 'jest' || family === 'jasmine') { if (nullish === 'null') { return (0, assertion_utils_js_1.replacement)(`expect(${actual})${negation(same)}.toBeNull()`, node); } return (0, assertion_utils_js_1.replacement)(same ? `expect(${actual}).toBeUndefined()` : `expect(${actual}).toBeDefined()`, node); } if (family === 'assert') { const method = getAssertNullishMethod(nullish, same); return (0, assertion_utils_js_1.replacement)(`${assertObject}.${method}(${actual}${extraArguments})`, node); } if (family === 'chai-should') { return (0, assertion_utils_js_1.replacement)(`${chaiShouldReceiver(actual, actualNode)}.should${negation(same)}.be.${nullish}`, node); } return (0, assertion_utils_js_1.replacement)(`expect(${actual}${extraArguments}).to${negation(same)}.be.${nullish}`, node); } function getAssertNullishMethod(nullish, same) { if (nullish === 'null') { return same ? 'isNull' : 'isNotNull'; } return same ? 'isUndefined' : 'isDefined'; } function buildLengthEqualitySuggestion(actualNode, expected, same, context) { const { family, sourceCode, node, assertObject, extraArguments } = context; const actual = sourceCode.getText(actualNode); if (family === 'jest' || family === 'jasmine') { const matcher = family === 'jasmine' ? 'toHaveSize' : 'toHaveLength'; return (0, assertion_utils_js_1.replacement)(`expect(${actual})${negation(same)}.${matcher}(${expected})`, node, undefined, 'preferSpecificLengthAssertion'); } if (family === 'assert') { return same ? (0, assertion_utils_js_1.replacement)(`${assertObject}.lengthOf(${actual}, ${expected}${extraArguments})`, node, undefined, 'preferSpecificLengthAssertion') : null; } if (family === 'chai-should') { return (0, assertion_utils_js_1.replacement)(`${chaiShouldReceiver(actual, actualNode)}.should${negation(same)}.have.lengthOf(${expected})`, node, undefined, 'preferSpecificLengthAssertion'); } return (0, assertion_utils_js_1.replacement)(`expect(${actual}${extraArguments}).to${negation(same)}.have.lengthOf(${expected})`, node, undefined, 'preferSpecificLengthAssertion'); } function buildEqualitySuggestion(leftNode, right, same, context) { const { family, sourceCode, node, assertObject, extraArguments } = context; const left = sourceCode.getText(leftNode); if (family === 'jest' || family === 'jasmine') { return (0, assertion_utils_js_1.replacement)(`expect(${left})${negation(same)}.toBe(${right})`, node); } if (family === 'assert') { return (0, assertion_utils_js_1.replacement)(`${assertObject}.${same ? 'strictEqual' : 'notStrictEqual'}(${left}, ${right}${extraArguments})`, node); } if (family === 'chai-should') { return (0, assertion_utils_js_1.replacement)(`${chaiShouldReceiver(left, leftNode)}.should${negation(same)}.equal(${right})`, node); } return (0, assertion_utils_js_1.replacement)(`expect(${left}${extraArguments}).to${negation(same)}.equal(${right})`, node); } function buildInstanceofSuggestion(left, leftNode, right, positive, context) { const { family, node, assertObject, extraArguments } = context; if (family === 'jest' || family === 'jasmine') { return (0, assertion_utils_js_1.replacement)(`expect(${left})${negation(positive)}.toBeInstanceOf(${right})`, node); } if (family === 'assert') { return (0, assertion_utils_js_1.replacement)(`${assertObject}.${positive ? 'instanceOf' : 'notInstanceOf'}(${left}, ${right}${extraArguments})`, node); } if (family === 'chai-should') { return (0, assertion_utils_js_1.replacement)(`${chaiShouldReceiver(left, leftNode)}.should${negation(positive)}.be.instanceOf(${right})`, node); } return (0, assertion_utils_js_1.replacement)(`expect(${left}${extraArguments}).to${negation(positive)}.be.instanceOf(${right})`, node); } function buildNumericComparisonSuggestion(left, leftNode, right, comparison, context) { const { family, node, assertObject, extraArguments } = context; if (family === 'jest' || family === 'jasmine') { return (0, assertion_utils_js_1.replacement)(`expect(${left}).${comparison.jest}(${right})`, node); } if (family === 'assert') { return (0, assertion_utils_js_1.replacement)(`${assertObject}.${comparison.assert}(${left}, ${right}${extraArguments})`, node); } if (family === 'chai-should') { return (0, assertion_utils_js_1.replacement)(`${chaiShouldReceiver(left, leftNode)}.should.be.${comparison.chai}(${right})`, node); } return (0, assertion_utils_js_1.replacement)(`expect(${left}${extraArguments}).to.be.${comparison.chai}(${right})`, node); } function getIncludesSuggestion(actual, positive, family, sourceCode, node, assertObject, extraArguments) { if (actual.type !== 'CallExpression' || !(0, ast_js_1.isMethodCall)(actual) || !(0, ast_js_1.isIdentifier)(actual.callee.property, 'includes') || actual.arguments.length !== 1 || !isTrustedStringReceiver(actual.callee.object)) { return null; } const receiver = sourceCode.getText(actual.callee.object); const needle = sourceCode.getText(actual.arguments[0]); if (family === 'jest' || family === 'jasmine') { return (0, assertion_utils_js_1.replacement)(`expect(${receiver})${negation(positive)}.toContain(${needle})`, node); } if (family === 'assert') { return (0, assertion_utils_js_1.replacement)(`${assertObject}.${positive ? 'include' : 'notInclude'}(${receiver}, ${needle}${extraArguments})`, node); } if (family === 'chai-should') { return (0, assertion_utils_js_1.replacement)(`${chaiShouldReceiver(receiver, actual.callee.object)}.should${negation(positive)}.include(${needle})`, node); } return (0, assertion_utils_js_1.replacement)(`expect(${receiver}${extraArguments}).to${negation(positive)}.include(${needle})`, node); } function negation(positive) { return positive ? '' : '.not'; } function isTrustedStringReceiver(node) { if (node.type === 'Literal') { return typeof node.value === 'string'; } if (node.type === 'TemplateLiteral') { return node.expressions.length === 0; } return (0, ast_js_1.isIdentifier)(node) && STRING_LIKE_IDENTIFIER.test(node.name); } function isNumericComparison(node) { return (isKnownNumericOperand(node.left) && isKnownNumericOperand(node.right) && !isKnownNonNumericOperand(node.left) && !isKnownNonNumericOperand(node.right)); } function isKnownNumericOperand(node) { if (node.type === 'Literal') { return typeof node.value === 'number' || typeof node.value === 'bigint'; } if (node.type === 'UnaryExpression' && ['+', '-'].includes(node.operator)) { return isKnownNumericOperand(node.argument); } if ((0, ast_js_1.isIdentifier)(node)) { return NUMERIC_IDENTIFIER.test(node.name); } if (node.type === 'MemberExpression' && !node.computed && (0, ast_js_1.isIdentifier)(node.property)) { return NUMERIC_IDENTIFIER.test(node.property.name); } return false; } function isKnownNonNumericOperand(node) { if (node.type === 'Literal') { return typeof node.value === 'string'; } if (node.type === 'TemplateLiteral') { return node.expressions.length === 0; } if (node.type === 'NewExpression' && (0, ast_js_1.isIdentifier)(node.callee, 'Date')) { return true; } if (node.type === 'CallExpression' && (0, ast_js_1.isIdentifier)(node.callee, 'Date')) { return true; } if ((0, ast_js_1.isIdentifier)(node)) { return STRING_LIKE_IDENTIFIER.test(node.name) || DATE_LIKE_IDENTIFIER.test(node.name); } if (node.type === 'MemberExpression' && !node.computed && (0, ast_js_1.isIdentifier)(node.property)) { return (STRING_LIKE_IDENTIFIER.test(node.property.name) || DATE_LIKE_IDENTIFIER.test(node.property.name)); } return false; } function getNumericComparison(operator, positive) { const effective = positive ? operator : invertComparison(operator); switch (effective) { case '>': return { jest: 'toBeGreaterThan', chai: 'above', assert: 'isAbove' }; case '>=': return { jest: 'toBeGreaterThanOrEqual', chai: 'at.least', assert: 'isAtLeast' }; case '<': return { jest: 'toBeLessThan', chai: 'below', assert: 'isBelow' }; case '<=': return { jest: 'toBeLessThanOrEqual', chai: 'at.most', assert: 'isAtMost' }; default: return null; } } function invertComparison(operator) { switch (operator) { case '>': return '<='; case '>=': return '<'; case '<': return '>='; case '<=': return '>'; default: return null; } }