eslint-plugin-sonarjs
Version:
352 lines (351 loc) • 13 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/
*/
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;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.hasSupportedTestFramework = hasSupportedTestFramework;
exports.isTypeLevelAssertion = isTypeLevelAssertion;
exports.isIncompleteShouldAccess = isIncompleteShouldAccess;
exports.isChaiShouldTSAssertion = isChaiShouldTSAssertion;
exports.isGlobalExpectAssertion = isGlobalExpectAssertion;
exports.isNodeAssertAssertion = isNodeAssertAssertion;
exports.isGlobalTSExpectAssertion = isGlobalTSExpectAssertion;
exports.isNodeAssertTSAssertion = isNodeAssertTSAssertion;
const typescript_1 = __importDefault(require("typescript"));
const Playwright = __importStar(require("./playwright.js"));
const Vitest = __importStar(require("./vitest.js"));
const ancestor_js_1 = require("../ancestor.js");
const module_js_1 = require("../module.js");
const module_ts_js_1 = require("../module-ts.js");
const SUPPORTED_TEST_FRAMEWORK_IMPORTS = [
'@jest/globals',
'@playwright/test',
'chai',
'cypress',
'jasmine',
'jest',
'mocha',
'bun:test',
'node:test',
'sinon',
'supertest',
'uvu',
'vitest',
];
const SUPPORTED_TEST_FRAMEWORK_DEPENDENCIES = [
'@jest/globals',
'@playwright/test',
'chai',
'cypress',
'jasmine',
'jasmine-core',
'jasmine-node',
'jest',
'karma-jasmine',
'mocha',
'sinon',
'supertest',
'uvu',
'vitest',
];
// Known global `expect*(...)` entry points: the universal `expect`, Jasmine's
// `expectAsync`, rxjs marble testing's `expectObservable`/`expectSubscriptions`,
// and vitest's `expectTypeOf`.
// Matched by exact name (not an `expect`-prefix) so unrelated identifiers such as
// `expectation(...)` or `expected(...)` in production code are not treated as
// assertions.
const GLOBAL_EXPECT_NAMES = new Set([
'expect',
'expectAsync',
'expectObservable',
'expectSubscriptions',
'expectTypeOf',
]);
const CHAI_NON_TERMINAL_PROPERTY_NAMES = new Set([
'all',
'also',
'and',
'any',
'at',
'be',
'been',
'but',
'deep',
'does',
'have',
'has',
'is',
'itself',
'nested',
'not',
'of',
'ordered',
'own',
'same',
'still',
'that',
'to',
'which',
'with',
]);
const CHAI_TERMINAL_PROPERTY_NAMES = new Set([
'Arguments',
'NaN',
'arguments',
'empty',
'exist',
'extensible',
'false',
'finite',
'frozen',
'null',
'ok',
'sealed',
'true',
'undefined',
]);
function hasSupportedTestFramework(context) {
return (0, module_js_1.importsOrDependsOnModule)(context, SUPPORTED_TEST_FRAMEWORK_IMPORTS, SUPPORTED_TEST_FRAMEWORK_DEPENDENCIES);
}
// All FQN roots whose calls are compile-time-only type checks: Vitest's
// `expectTypeOf`/`assertType` and the standalone `expect-type` package's
// `expectTypeOf` (which Vitest uses internally and may be imported directly).
const TYPE_LEVEL_ASSERTION_ROOTS = [...Vitest.TYPE_LEVEL_ROOTS, 'expect-type.expectTypeOf'];
const NODE_ASSERT_MODULES = ['assert', 'assert/strict', 'node:assert', 'node:assert/strict'];
/**
* Whether `node` is a compile-time-only type check that must not be flagged for
* placement outside a test case.
*/
function isTypeLevelAssertion(context, node) {
if (node.type !== 'CallExpression') {
return false;
}
const fqn = (0, module_js_1.getFullyQualifiedName)(context, node);
return (fqn !== null &&
TYPE_LEVEL_ASSERTION_ROOTS.some(root => fqn === root || fqn.startsWith(`${root}.`)));
}
/**
* Incomplete Chai `foo.should` property chains are not assertions on their own.
* We exclude them so S2699 does not treat an unfinished `should` chain as an assertion
* and miss the "Add at least one assertion to this test case." issue.
*/
function isIncompleteShouldAccess(context, node) {
if (!isShouldMember(node)) {
return false;
}
return !isCompleteESTreeShouldPropertyChain(context, node);
}
/**
* Whether a typed Chai `should` property access ends in a complete assertion.
*/
function isChaiShouldTSAssertion(node) {
return (isTSShouldAccess(node) &&
(0, module_ts_js_1.importsModuleTS)(node.getSourceFile(), ['chai']) &&
isCompleteTSShouldPropertyChain(node));
}
function isTSShouldAccess(node) {
return typescript_1.default.isPropertyAccessExpression(node) && node.name.text === 'should';
}
/**
* Checks if the node matches the pattern expectX(...).method() where:
* - expectX is one of the known global expect entry points ({@link GLOBAL_EXPECT_NAMES})
* or a Playwright `expect(...)` entry point
* - method is a chained property access with a method call (e.g., .toBe(), .toEqual(), .not.toBe())
*
* This mirrors the TypeScript isGlobalExpectExpression function logic.
*/
function isGlobalExpectAssertion(context, node) {
if (node.callee.type !== 'MemberExpression') {
return false;
}
// Walk up the chain of member expressions to find the innermost call expression
// This handles: expect(...).toBe() as well as expect(...).not.toBe()
// Also handles: expectObservable(...).toBe(...), expectSubscriptions(...).toBe(...), etc.
let current = node.callee.object;
while (current.type === 'MemberExpression') {
current = current.object;
}
if (current.type !== 'CallExpression') {
return false;
}
const innerCall = current;
return ((innerCall.callee.type === 'Identifier' && GLOBAL_EXPECT_NAMES.has(innerCall.callee.name)) ||
Playwright.isExpectFqn((0, module_js_1.getFullyQualifiedName)(context, innerCall.callee)));
}
/**
* Whether `node` is a call rooted at the node `assert` module — `assert(...)` itself or any of its
* methods. Pure-AST counterpart of {@link isNodeAssertTSAssertion}.
*/
function isNodeAssertAssertion(context, node) {
if (node.type !== 'CallExpression') {
return false;
}
if (!(0, module_js_1.importsModule)(context, NODE_ASSERT_MODULES)) {
return false;
}
const fullyQualifiedName = (0, module_js_1.getFullyQualifiedName)(context, node);
return fullyQualifiedName?.split('.')[0] === 'assert';
}
function isShouldMember(node) {
return (node.type === 'MemberExpression' &&
!node.computed &&
node.property.type === 'Identifier' &&
node.property.name === 'should');
}
function isCompleteESTreeShouldPropertyChain(context, node) {
let current = node;
let parent = (0, ancestor_js_1.getParent)(context, node);
while (isESTreeChaiShouldChainContinuation(parent, current)) {
const grandparent = (0, ancestor_js_1.getParent)(context, parent);
if (isESTreeCallOnNode(grandparent, parent)) {
return true;
}
if (CHAI_TERMINAL_PROPERTY_NAMES.has(parent.property.name)) {
if (!isESTreeChaiShouldChainContinuation(grandparent, parent)) {
return true;
}
current = parent;
parent = grandparent;
continue;
}
if (!CHAI_NON_TERMINAL_PROPERTY_NAMES.has(parent.property.name) &&
!isESTreeChaiShouldChainContinuation(grandparent, parent)) {
return false;
}
current = parent;
parent = grandparent;
}
return false;
}
function isCompleteTSShouldPropertyChain(node) {
let current = node;
let parent = node.parent;
while (isTSChaiShouldChainContinuation(parent, current)) {
const grandparent = parent.parent;
if (isTSCallOnNode(grandparent, parent)) {
return true;
}
if (CHAI_TERMINAL_PROPERTY_NAMES.has(parent.name.text)) {
if (!isTSChaiShouldChainContinuation(grandparent, parent)) {
return true;
}
current = parent;
parent = grandparent;
continue;
}
if (!CHAI_NON_TERMINAL_PROPERTY_NAMES.has(parent.name.text) &&
!isTSChaiShouldChainContinuation(grandparent, parent)) {
return false;
}
current = parent;
parent = grandparent;
}
return false;
}
function isESTreeCallOnNode(parent, node) {
return parent?.type === 'CallExpression' && parent.callee === node;
}
function isTSCallOnNode(parent, node) {
return parent !== undefined && typescript_1.default.isCallExpression(parent) && parent.expression === node;
}
function isESTreeChaiShouldChainContinuation(parent, node) {
return (parent?.type === 'MemberExpression' &&
parent.object === node &&
!parent.computed &&
parent.property.type === 'Identifier');
}
function isTSChaiShouldChainContinuation(parent, node) {
return (parent !== undefined && typescript_1.default.isPropertyAccessExpression(parent) && parent.expression === node);
}
/**
* Type-checker-aware counterpart of {@link isGlobalExpectAssertion}: a resolved global
* `expect*(...)` chain. Node `assert` is deliberately not covered here — it is its own catalog
* entry, {@link isNodeAssertTSAssertion}.
*/
function isGlobalTSExpectAssertion(services, node) {
if (node.kind !== typescript_1.default.SyntaxKind.CallExpression) {
return false;
}
const callExpressionNode = node;
return isGlobalExpectExpression(services, callExpressionNode);
}
function isGlobalExpectExpression(services, node) {
if (node.expression.kind !== typescript_1.default.SyntaxKind.PropertyAccessExpression) {
return false;
}
// Walk up the chain of property accesses to find the innermost call expression
// This handles: expect(...).toHaveBeenCalled() as well as expect(...).not.toHaveBeenCalled()
// Also handles: expectObservable(...).toBe(...), expectSubscriptions(...).toBe(...), etc.
let current = node.expression.expression;
while (current.kind === typescript_1.default.SyntaxKind.PropertyAccessExpression) {
current = current.expression;
}
if (current.kind !== typescript_1.default.SyntaxKind.CallExpression) {
return false;
}
const innerCallExpression = current;
return ((innerCallExpression.expression.kind === typescript_1.default.SyntaxKind.Identifier &&
GLOBAL_EXPECT_NAMES.has(innerCallExpression.expression.text)) ||
Playwright.isExpectFqn((0, module_ts_js_1.getFullyQualifiedNameTS)(services, innerCallExpression.expression)));
}
/**
* Type-checker-aware counterpart of {@link isNodeAssertAssertion}, also accepting the
* `assert/strict` entry point.
*/
function isNodeAssertTSAssertion(services, node) {
if (!(0, module_ts_js_1.importsModuleTS)(node.getSourceFile(), NODE_ASSERT_MODULES)) {
return false;
}
const fqn = (0, module_ts_js_1.getFullyQualifiedNameTS)(services, node);
const root = fqn?.split('.')[0];
return root === 'assert' || root === 'assert/strict';
}