eslint-plugin-sonarjs
Version:
190 lines (189 loc) • 7.62 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/
*/
// 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`));
}