UNPKG

eslint-plugin-sonarjs

Version:
190 lines (189 loc) • 7.62 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/S9027/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 ast_js_1 = require("../helpers/ast.js"); const generate_meta_js_1 = require("../helpers/generate-meta.js"); const module_js_1 = require("../helpers/module.js"); const testing_library_js_1 = require("../helpers/testing-library.js"); const meta = __importStar(require("./generated-meta.js")); const messages = { presence: 'A presence assertion should use a `getBy*` query so a missing element produces Testing Library diagnostics.', absence: 'An absence assertion should use a `queryBy*` so it can observe a missing element.', }; const PRESENCE_MATCHERS = new Set(['toBeInTheDocument', 'toBeTruthy', 'toBeDefined']); const ABSENCE_MATCHERS = new Set(['toBeNull', 'toBeFalsy']); exports.rule = { meta: (0, generate_meta_js_1.generateMeta)(meta, { messages, fixable: 'code' }), create(context) { return { CallExpression(node) { if (node.type !== 'CallExpression') { return; } const assertion = getAssertion(node); if (!assertion) { return; } const query = getQueryCall(assertion.actual); if (!query || isInTestingLibraryWaitFor(context, node)) { return; } const queryMethod = getQueryMethod(query); if (!queryMethod || query.callee.type !== 'MemberExpression' || query.callee.object.type === 'Super' || !isDirectTestingLibraryScreen(context, query.callee.object)) { return; } const isGetQuery = queryMethod.name.startsWith('get'); if (assertion.presence === isGetQuery) { return; } const replacement = isGetQuery ? queryMethod.name.replace(/^get/, 'query') : queryMethod.name.replace(/^query/, 'get'); context.report({ node: queryMethod, messageId: assertion.presence ? 'presence' : 'absence', fix: fixer => fixer.replaceText(queryMethod, replacement), }); }, }; }, }; function getAssertion(node) { if (node.callee.type !== 'MemberExpression' || node.callee.computed || !(0, ast_js_1.isIdentifier)(node.callee.property) || node.arguments.length !== 0) { return null; } const matcher = node.callee.property.name; let object = node.callee.object; let negated = false; if (object.type === 'MemberExpression' && !object.computed && (0, ast_js_1.isIdentifier)(object.property, 'not')) { negated = true; object = object.object; } if (object.type !== 'CallExpression' || object.arguments.length !== 1 || !(0, ast_js_1.isIdentifier)(object.callee, 'expect')) { return null; } const [actual] = object.arguments; if (!actual || actual.type === 'SpreadElement') { return null; } if (PRESENCE_MATCHERS.has(matcher)) { return { actual, presence: !negated }; } if (ABSENCE_MATCHERS.has(matcher)) { return { actual, presence: negated }; } return null; } function getQueryCall(actual) { actual = (0, ast_js_1.unwrapTypeScriptExpression)(actual); if (actual.type === 'CallExpression') { return actual; } if (actual.type !== 'MemberExpression' || actual.object.type !== 'CallExpression') { return null; } const queryMethod = getQueryMethod(actual.object); if (!queryMethod?.name.includes('AllBy')) { return null; } const isArrayIndex = actual.computed && ((0, ast_js_1.isIdentifier)(actual.property) || (actual.property.type === 'Literal' && typeof actual.property.value === 'number' && Number.isInteger(actual.property.value) && actual.property.value >= 0)); const isLength = !actual.computed && (0, ast_js_1.isIdentifier)(actual.property, 'length'); return isArrayIndex || isLength ? actual.object : null; } function getQueryMethod(query) { if (!(0, ast_js_1.isDotNotation)(query.callee) || !/^(get|query)(All)?By[A-Z]/.test(query.callee.property.name)) { return null; } return query.callee.property; } function isDirectTestingLibraryScreen(context, receiver) { return (0, testing_library_js_1.isDirectTestingLibraryBinding)(context, receiver, 'screen', { allowNamespaceImport: false, allowSubpathImport: false, }); } function isInTestingLibraryWaitFor(context, node) { let current = node; while (current.parent) { const parent = current.parent; if ((current.type === 'ArrowFunctionExpression' || current.type === 'FunctionExpression') && parent.type === 'CallExpression' && parent.arguments.includes(current) && isTestingLibraryWaitFor(context, parent)) { return true; } current = parent; } return false; } function isTestingLibraryWaitFor(context, node) { const fqn = (0, module_js_1.getFullyQualifiedName)(context, node.callee); return (fqn != null && testing_library_js_1.TESTING_LIBRARY_MODULES.some(module => fqn === `${module.replace('/', '.')}.waitFor`)); }