UNPKG

eslint-plugin-sonarjs

Version:
225 lines (224 loc) 10.9 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/ */ // https://sonarsource.github.io/rspec/#/rspec/S5906/javascript var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.rule = void 0; const semver_1 = require("semver"); const assertions_js_1 = require("../helpers/assertions.js"); const ast_js_1 = require("../helpers/ast.js"); const dependencies_js_1 = require("../helpers/dependency-manifests/dependencies.js"); const module_js_1 = require("../helpers/module.js"); const generate_meta_js_1 = require("../helpers/generate-meta.js"); const assertion_suggestions_js_1 = require("./assertion-suggestions.js"); const assertion_utils_js_1 = require("./assertion-utils.js"); const chai_suggestions_js_1 = require("./chai-suggestions.js"); const cypress_suggestions_js_1 = require("./cypress-suggestions.js"); const expect_chain_js_1 = require("./expect-chain.js"); const meta = __importStar(require("./generated-meta.js")); const playwright_suggestions_js_1 = require("./playwright-suggestions.js"); const messages = { preferSpecificAssertion: 'Prefer "{{assertion}}" over this generic assertion; dedicated matchers read better and report clearer failures.', preferSpecificLengthAssertion: 'Prefer "{{assertion}}" over this generic assertion for better reporting; it works on any object with a numeric length property.', quickfix: 'Replace with {{assertion}}.', }; const PLAYWRIGHT_MODULES = ['@playwright/test']; const JEST_LIKE_MODULES = ['vitest', 'bun:test', '@jest/globals']; const JEST_LIKE_GLOBAL_MODULES = ['jest']; const JASMINE_MODULES = ['jasmine']; const JASMINE_GLOBAL_MODULES = ['jasmine', 'jasmine-core', 'jasmine-node', 'karma-jasmine']; const JASMINE_SIZE_DEPENDENCIES = ['jasmine-core', 'jasmine']; const JASMINE_TO_HAVE_SIZE_MIN_VERSION = '3.6.0'; exports.rule = { meta: (0, generate_meta_js_1.generateMeta)(meta, { messages, hasSuggestions: true }), create(context) { const sourceCode = context.sourceCode; const hasPlaywright = (0, module_js_1.importsOrDependsOnModule)(context, PLAYWRIGHT_MODULES, PLAYWRIGHT_MODULES); const jasmineSupportsToHaveSize = supportsJasmineToHaveSize(context); const hasAmbiguousJasmineJestGlobalExpect = hasAmbiguousJasmineJestGlobalExpectSignal(context); const playwrightLocators = new Set(); function report(node, suggestion) { const replacementText = suggestion.replacement; const suggest = replacementText ? [ { messageId: 'quickfix', data: { assertion: suggestion.assertion }, fix: (fixer) => fixer.replaceText(node, replacementText), }, ] : undefined; context.report({ node: node.callee, messageId: suggestion.messageId ?? 'preferSpecificAssertion', data: { assertion: suggestion.assertion }, ...(suggest ? { suggest } : {}), }); } return { ...(hasPlaywright ? (0, assertion_utils_js_1.trackPlaywrightLocators)(context, playwrightLocators) : {}), CallExpression(node) { if (node.type !== 'CallExpression') { return; } if (hasPlaywright) { const playwrightSuggestion = (0, playwright_suggestions_js_1.getPlaywrightLocatorSuggestion)(context, node, sourceCode, playwrightLocators); if (playwrightSuggestion) { report(node, playwrightSuggestion); return; } } const assertion = (0, assertions_js_1.extractTestAssertion)(context, node); if (assertion?.kind !== 'comparison') { return; } const suggestion = getSuggestion(context, assertion.style, node, sourceCode, jasmineSupportsToHaveSize, hasAmbiguousJasmineJestGlobalExpect); if (suggestion) { report(node, suggestion); } }, }; }, }; function getSuggestion(context, style, node, sourceCode, jasmineSupportsToHaveSize, hasAmbiguousJasmineJestGlobalExpect) { switch (style) { case 'jest-like': return getExpectLikeSuggestion(node, sourceCode, 'jest', true, hasAmbiguousJasmineJestGlobalExpect); case 'playwright': return getExpectLikeSuggestion(node, sourceCode, 'jest'); case 'jasmine': return getExpectLikeSuggestion(node, sourceCode, 'jasmine', jasmineSupportsToHaveSize); case 'chai-bdd': return (0, chai_suggestions_js_1.getChaiBddSuggestion)(node, sourceCode); case 'chai-assert': return (0, chai_suggestions_js_1.getChaiAssertSuggestion)(context, node, sourceCode); case 'cypress': return (0, cypress_suggestions_js_1.getCypressSuggestion)(node, sourceCode); default: return null; } } function getExpectLikeSuggestion(node, sourceCode, family, jasmineSupportsToHaveSize = true, skipLengthEqualitySuggestions = false) { if (!(0, ast_js_1.isMethodCall)(node) || !['toBe', 'toEqual', 'toStrictEqual'].includes(node.callee.property.name)) { return null; } const chain = (0, expect_chain_js_1.getExpectChain)(node.callee.object); if (!chain || node.arguments.length !== 1) { return null; } const { actual, negated } = chain; const expected = node.arguments[0]; const actualText = sourceCode.getText(actual); const expectedText = sourceCode.getText(expected); const prefix = `expect(${actualText})${negated ? '.not' : ''}`; if ((0, assertion_utils_js_1.isNullLiteral)(expected)) { return (0, assertion_utils_js_1.replacement)(`${prefix}.toBeNull()`, node, sourceCode); } if ((0, assertion_utils_js_1.isUndefinedExpression)(expected)) { return (0, assertion_utils_js_1.replacement)(negated ? `expect(${actualText}).toBeDefined()` : `${prefix}.toBeUndefined()`, node, sourceCode); } if ((0, assertion_utils_js_1.isNaNExpression)(expected)) { if (family === 'jasmine' && node.callee.property.name === 'toBe') { return null; } return (0, assertion_utils_js_1.replacement)(`${prefix}.toBeNaN()`, node, sourceCode); } if ((0, assertion_utils_js_1.isLengthAccess)(actual)) { if (skipLengthEqualitySuggestions) { return null; } if (family === 'jasmine' && !jasmineSupportsToHaveSize) { return null; } const lengthMatcher = family === 'jasmine' ? 'toHaveSize' : 'toHaveLength'; return (0, assertion_utils_js_1.replacement)(`expect(${sourceCode.getText(actual.object)}).${negated ? 'not.' : ''}${lengthMatcher}(${expectedText})`, node, sourceCode, 'preferSpecificLengthAssertion'); } const booleanExpected = (0, assertion_utils_js_1.getBooleanValue)(expected); if (booleanExpected === undefined) { return null; } if (skipLengthEqualitySuggestions && isLengthEqualityComparison(actual)) { return null; } return (0, assertion_suggestions_js_1.getBooleanExpressionSuggestion)(actual, booleanExpected !== negated, family, sourceCode, node); } function supportsJasmineToHaveSize(context) { const dependencies = (0, dependencies_js_1.getDependenciesSanitizePaths)(context); for (const dependency of JASMINE_SIZE_DEPENDENCIES) { const versionSignal = dependencies.get(dependency); if (!versionSignal || versionSignal === 'latest' || versionSignal === '*') { continue; } try { const version = (0, semver_1.minVersion)(versionSignal); return version !== null && version.compare(JASMINE_TO_HAVE_SIZE_MIN_VERSION) >= 0; } catch { return false; } } return false; } // For bare global expect(), mixed Jasmine/Jest dependency signals are ambiguous: // there is no import/FQN evidence to choose between toHaveSize() and toHaveLength(). function hasAmbiguousJasmineJestGlobalExpectSignal(context) { if ((0, module_js_1.importsModule)(context, JEST_LIKE_MODULES) || (0, module_js_1.importsModule)(context, JASMINE_MODULES)) { return false; } const dependencies = (0, dependencies_js_1.getDependenciesSanitizePaths)(context); return (JEST_LIKE_GLOBAL_MODULES.some(dependency => dependencies.has(dependency)) && JASMINE_GLOBAL_MODULES.some(dependency => dependencies.has(dependency))); } function isLengthEqualityComparison(node) { return (node.type === 'BinaryExpression' && ['===', '!=='].includes(node.operator) && ((0, assertion_utils_js_1.isLengthAccess)(node.left) || (0, assertion_utils_js_1.isLengthAccess)(node.right))); }