UNPKG

eslint-plugin-sonarjs

Version:
302 lines (301 loc) 11.4 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/S8780/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 generate_meta_js_1 = require("../helpers/generate-meta.js"); const ast_js_1 = require("../helpers/ast.js"); const expect_call_chain_js_1 = require("../helpers/expect-call-chain.js"); const mocha_js_1 = require("../helpers/mocha.js"); const module_js_1 = require("../helpers/module.js"); const meta = __importStar(require("./generated-meta.js")); const messages = { awaitOrReturn: 'Await or return this async assertion.', }; const JEST_MODULES = ['jest', '@jest/globals']; const VITEST_MODULES = ['vitest']; const JASMINE_MODULES = ['jasmine']; const JASMINE_DEPENDENCIES = ['jasmine', 'jasmine-core', 'jasmine-node', 'karma-jasmine']; const PLAYWRIGHT_MODULES = ['@playwright/test']; const TEST_FUNCTION_NAMES = new Set([ 'it', 'it.only', 'it.skip', 'test', 'test.only', 'test.skip', 'specify', 'specify.only', 'specify.skip', 'vitest.it', 'vitest.it.only', 'vitest.it.skip', 'vitest.test', 'vitest.test.only', 'vitest.test.skip', '@playwright.test.test', '@playwright.test.test.only', '@playwright.test.test.skip', ]); const PLAYWRIGHT_ASYNC_MATCHERS = new Set([ 'toBeAttached', 'toBeChecked', 'toBeDisabled', 'toBeEditable', 'toBeEmpty', 'toBeEnabled', 'toBeFocused', 'toBeHidden', 'toBeInViewport', 'toBeOK', 'toBeVisible', 'toContainClass', 'toContainText', 'toHaveAccessibleDescription', 'toHaveAccessibleName', 'toHaveAttribute', 'toHaveClass', 'toHaveCount', 'toHaveCSS', 'toHaveId', 'toHaveJSProperty', 'toHaveRole', 'toHaveScreenshot', 'toHaveText', 'toHaveTitle', 'toHaveURL', 'toHaveValue', 'toHaveValues', 'toMatchAriaSnapshot', ]); exports.rule = { meta: (0, generate_meta_js_1.generateMeta)(meta, { messages }), create(context) { const frameworks = { jest: (0, module_js_1.importsOrDependsOnModule)(context, JEST_MODULES, ['jest']), vitest: (0, module_js_1.importsOrDependsOnModule)(context, VITEST_MODULES, VITEST_MODULES), jasmine: (0, module_js_1.importsOrDependsOnModule)(context, JASMINE_MODULES, JASMINE_DEPENDENCIES), playwright: (0, module_js_1.importsOrDependsOnModule)(context, PLAYWRIGHT_MODULES, PLAYWRIGHT_MODULES), }; if (!Object.values(frameworks).some(Boolean)) { return {}; } return { 'CallExpression:exit'(node) { const call = node; const callback = extractSupportedTestCallback(context, call); if (!callback) { return; } if (callback.body.type !== 'BlockStatement') { return; } if (usesDoneCallback(callback)) { return; } forEachExpressionStatement(callback.body, statement => { const assertionNode = getAsyncAssertionNode(context, statement.expression, frameworks); if (assertionNode) { context.report({ node: assertionNode, messageId: 'awaitOrReturn', }); } }); }, }; }, }; function extractSupportedTestCallback(context, call) { const mochaTestCase = (0, mocha_js_1.extractTestCase)(call); if (mochaTestCase) { return isFunctionArgument(mochaTestCase.callback) ? mochaTestCase.callback : null; } const name = (0, module_js_1.getFullyQualifiedName)(context, call.callee) ?? getDottedName(call.callee); if (!name || !TEST_FUNCTION_NAMES.has(name)) { return null; } for (const argument of call.arguments) { if (isFunctionArgument(argument)) { return argument; } } return null; } function isFunctionArgument(node) { return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression'; } /** * Jest/Jasmine/Vitest pass a `done` callback whenever the test function declares a parameter * (arity-based, regardless of its name). Mixing that with an awaited/returned assertion makes * the runtime throw ("cannot both take a 'done' callback and return something"), so the fix this * rule suggests doesn't apply to such tests. Playwright's fixtures parameter is always a * destructuring pattern, never a plain identifier, so it isn't mistaken for a done callback. */ function usesDoneCallback(callback) { const [firstParam] = callback.params; return firstParam?.type === 'Identifier'; } function forEachExpressionStatement(node, callback) { switch (node.type) { case 'ExpressionStatement': callback(node); return; case 'BlockStatement': node.body.forEach(child => forEachExpressionStatement(child, callback)); return; case 'IfStatement': forEachExpressionStatement(node.consequent, callback); if (node.alternate) { forEachExpressionStatement(node.alternate, callback); } return; case 'ForStatement': case 'ForInStatement': case 'ForOfStatement': case 'WhileStatement': case 'DoWhileStatement': case 'LabeledStatement': case 'WithStatement': forEachExpressionStatement(node.body, callback); return; case 'SwitchStatement': node.cases.forEach(switchCase => switchCase.consequent.forEach(child => forEachExpressionStatement(child, callback))); return; case 'TryStatement': forEachExpressionStatement(node.block, callback); if (node.handler) { forEachExpressionStatement(node.handler.body, callback); } if (node.finalizer) { forEachExpressionStatement(node.finalizer, callback); } return; } } function getAsyncAssertionNode(context, expression, frameworks) { const unwrapped = (0, expect_call_chain_js_1.unwrapChainExpression)(expression); if (unwrapped.type === 'AwaitExpression') { return null; } if (unwrapped.type !== 'CallExpression') { return null; } return (getJestOrVitestAsyncNode(context, unwrapped, frameworks) ?? getJasmineAsyncNode(context, unwrapped, frameworks) ?? getPlaywrightAsyncNode(context, unwrapped, frameworks)); } function getJestOrVitestAsyncNode(context, call, frameworks) { if (!frameworks.jest && !frameworks.vitest) { return null; } const { segments } = (0, expect_call_chain_js_1.collectCallChain)(call); const asyncSegment = segments.find(segment => segment.name === 'resolves' || segment.name === 'rejects'); if (!asyncSegment) { return null; } const root = (0, expect_call_chain_js_1.getRootCall)(call); if (!root || !isExpectCall(context, root, frameworks, false)) { return null; } return asyncSegment.node; } function getJasmineAsyncNode(context, call, frameworks) { if (!frameworks.jasmine) { return null; } const root = (0, expect_call_chain_js_1.getRootCall)(call); if (root && isNamedCall(context, root, 'expectAsync', ['jasmine.expectAsync'])) { return root.callee; } return null; } function getPlaywrightAsyncNode(context, call, frameworks) { if (!frameworks.playwright || call.callee.type !== 'MemberExpression' || call.callee.computed) { return null; } const matcher = call.callee.property; if (!(0, ast_js_1.isIdentifier)(matcher) || !PLAYWRIGHT_ASYNC_MATCHERS.has(matcher.name)) { return null; } const root = (0, expect_call_chain_js_1.getRootCall)(call); if (root && isExpectCall(context, root, frameworks, true)) { return matcher; } return null; } function isExpectCall(context, call, frameworks, playwrightOnly) { const fqn = (0, module_js_1.getFullyQualifiedName)(context, call.callee); if (fqn) { return playwrightOnly ? fqn === '@playwright.test.expect' : fqn === 'expect' || fqn === '@jest.globals.expect' || fqn === 'vitest.expect' || (frameworks.jest && (0, ast_js_1.isIdentifier)(call.callee, 'expect')) || (frameworks.vitest && (0, ast_js_1.isIdentifier)(call.callee, 'expect')); } return !playwrightOnly && (0, ast_js_1.isIdentifier)(call.callee, 'expect'); } function isNamedCall(context, call, globalName, importedNames) { const fqn = (0, module_js_1.getFullyQualifiedName)(context, call.callee); return importedNames.includes(fqn ?? '') || (0, ast_js_1.isIdentifier)(call.callee, globalName); } function getDottedName(node) { if (node.type === 'Identifier') { return node.name; } if (node.type !== 'MemberExpression' || node.computed || !(0, ast_js_1.isIdentifier)(node.property)) { return null; } const objectName = getDottedName(node.object); return objectName ? `${objectName}.${node.property.name}` : null; }