UNPKG

eslint-plugin-sonarjs

Version:
313 lines (312 loc) 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.extractTestAssertion = extractTestAssertion; const ast_js_1 = require("./ast.js"); const assertions_chai_js_1 = require("./assertions-chai.js"); const assertions_cypress_js_1 = require("./assertions-cypress.js"); const module_js_1 = require("./module.js"); const JEST_LIKE_MODULES = ['vitest', 'bun:test', '@jest/globals']; // Jest-like test runners which expose global methods that can be used in assertions const JEST_LIKE_GLOBAL_MODULES = ['jest']; const JASMINE_MODULES = ['jasmine']; const JASMINE_GLOBAL_MODULES = ['jasmine', 'jasmine-core', 'jasmine-node', 'karma-jasmine']; const PLAYWRIGHT_MODULES = ['@playwright/test']; const CHAI_LIKE_GLOBAL_MODULES = [ 'chai', 'chai/register-assert', 'chai/register-expect', 'chai/register-should', 'cypress', ]; const NODE_ASSERT_MODULES = ['assert', 'node:assert', 'assert/strict', 'node:assert/strict']; /** * Maps common Jest matcher names to their corresponding assertion predicates. * Also covers Jest-compatible assertion libraries like Vitest, Bun's test runner, etc. * https://jestjs.io/docs/expect */ const JEST_PREDICATES_MAPPING = { toBeTruthy: 'truthy', toBeFalsy: 'falsy', toBeDefined: 'defined', toBeUndefined: 'undefined', toBeNull: 'null', }; const ASSERTION_LIBRARIES = [ { imports: JEST_LIKE_MODULES, dependencies: JEST_LIKE_GLOBAL_MODULES, extract: (_context, node) => extractExpectAssertion(node, 'jest-like'), }, { imports: JASMINE_MODULES, dependencies: JASMINE_GLOBAL_MODULES, extract: (_context, node) => extractExpectAssertion(node, 'jasmine'), }, { imports: PLAYWRIGHT_MODULES, dependencies: PLAYWRIGHT_MODULES, extract: (_context, node) => extractExpectAssertion(node, 'playwright'), }, { imports: CHAI_LIKE_GLOBAL_MODULES, dependencies: CHAI_LIKE_GLOBAL_MODULES, extract: (context, node) => (0, assertions_chai_js_1.extractChaiAssertion)(context, node, true) ?? (0, assertions_cypress_js_1.extractCypressChainAssertion)(node), }, ]; function extractTestAssertion(context, node) { // Explicit imports in the current file are more precise than project-wide dependency signals. const importedAssertion = extractAssertionFromLibraries(context, node, library => (0, module_js_1.importsModule)(context, library.imports)); if (importedAssertion) { return importedAssertion; } const dependencyAssertion = extractAssertionFromLibraries(context, node, library => (0, module_js_1.importsOrDependsOnModule)(context, library.imports, library.dependencies)); if (dependencyAssertion) { return dependencyAssertion; } // covers Node.js assert if (node.type === 'CallExpression' && (0, module_js_1.importsModule)(context, NODE_ASSERT_MODULES)) { return extractNodeJSAssertion(context, node); } return null; } function extractAssertionFromLibraries(context, node, isAvailable) { for (const library of ASSERTION_LIBRARIES) { if (!isAvailable(library)) { continue; } const assertion = library.extract(context, node); if (assertion) { return assertion; } } return null; } function extractExpectAssertion(expectCall, style) { if (expectCall.type !== 'CallExpression') { return null; } if (!(0, ast_js_1.isMethodCall)(expectCall)) { return null; } const matcher = expectCall.callee.property; const chain = extractExpectChain(expectCall.callee.object); if (!chain) { return null; } const actual = getArgumentAtIndex(chain.expectCall, 0); if (!actual) { return null; } const predicate = JEST_PREDICATES_MAPPING[matcher.name]; if (predicate !== undefined && expectCall.arguments.length === 0) { return { style, kind: 'predicate', predicate, actual, negated: chain.negated, node: expectCall, reportNode: actual, }; } const comparison = getJestComparison(matcher.name); if (comparison && expectCall.arguments.length === 1) { const expected = getArgumentAtIndex(expectCall, 0); if (!expected) { return null; } return { style, kind: 'comparison', comparison, actual, expected, negated: chain.negated, node: expectCall, reportNode: matcher, }; } return null; } function getJestComparison(name) { switch (name) { case 'toBe': return 'strict'; case 'toEqual': case 'toStrictEqual': return 'deep'; default: return null; } } function extractExpectChain(node) { let current = node; let negated = false; while (current.type === 'MemberExpression' && !current.computed) { // covers cases like `expect(value).not.toBeTruthy()` where the presence of `.not` negates the assertion // also guards against invalid chains like `expect(value).toBeTruthy.not` if (negated || !(0, ast_js_1.isIdentifier)(current.property, 'not')) { return null; } negated = true; current = current.object; } // guards against invalid expect chains // like `expect(value).toBeTruthy` (missing call) if (current.type !== 'CallExpression' || current.arguments.length !== 1 || !(0, ast_js_1.isIdentifier)(current.callee, 'expect')) { return null; } return { expectCall: current, negated }; } function extractNodeJSAssertion(context, node) { const assertCall = getNodeJSAssertCall(context, node); if (!assertCall) { return null; } if (assertCall.method === 'assert' || assertCall.method === 'ok') { const actual = getArgumentAtIndex(node, 0); if (!actual) { return null; } return { style: 'node-assert', kind: 'predicate', predicate: 'truthy', actual, negated: false, node, reportNode: actual, }; } const actual = getArgumentAtIndex(node, 0); const expected = getArgumentAtIndex(node, 1); if (!actual || !expected) { return null; } return { style: 'node-assert', kind: 'comparison', comparison: getNodeJSComparison(assertCall.method), actual, expected, negated: assertCall.negated, node, reportNode: assertCall.reportNode, }; } function getNodeJSComparison(method) { switch (method) { case 'deepStrictEqual': case 'notDeepStrictEqual': case 'deepEqual': case 'notDeepEqual': return 'deep'; case 'looseDeepEqual': case 'looseNotDeepEqual': return 'loose'; default: return 'strict'; } } function getNodeJSAssertCall(context, node) { // fully qualified assert method like `assert.strictEqual()` const fqn = (0, module_js_1.getFullyQualifiedName)(context, node.callee); const fqnMethod = getNodeJSAssertMethodFromFqn(fqn); if (fqnMethod) { return { // `negated` is derived from the raw method name, before normalization renames // e.g. `notDeepEqual` to `looseNotDeepEqual` for a non-strict import, which would // otherwise no longer start with `not`. method: normalizeNodeJSAssertMethod(fqnMethod, isStrictNodeJSAssertFqn(fqn)), negated: fqnMethod.startsWith('not'), reportNode: node.callee, }; } // `assert.xxx()` where `assert`'s origin isn't traceable via its fully qualified name, e.g. a // test-framework-provided `assert` parameter (`QUnit.test('...', assert => { assert.ok(x); })`). // Whether such an `assert` is the strict or non-strict variant can't be determined here, so // `deepEqual`/`notDeepEqual` — whose comparison kind depends on that — are left unresolved // rather than guessing non-strict; the unambiguous methods are still recognized. if ((0, ast_js_1.isMethodCall)(node) && (0, ast_js_1.isIdentifier)(node.callee.object, 'assert')) { const method = getNodeJSAssertMethodFromName(node.callee.property.name); if (method === 'deepEqual' || method === 'notDeepEqual') { return null; } if (method) { return { method: normalizeNodeJSAssertMethod(method, false), negated: method.startsWith('not'), reportNode: node.callee.property, }; } } return null; } function normalizeNodeJSAssertMethod(method, isStrictAssert) { if (method === 'deepEqual') { return isStrictAssert ? method : 'looseDeepEqual'; } if (method === 'notDeepEqual') { return isStrictAssert ? method : 'looseNotDeepEqual'; } return method; } function isStrictNodeJSAssertFqn(fqn) { return fqn?.startsWith('assert.strict.') ?? false; } function getNodeJSAssertMethodFromFqn(fqn) { switch (fqn) { case 'assert': case 'assert.strict': return 'assert'; case 'assert.ok': case 'assert.strict.ok': return 'ok'; case 'assert.deepEqual': case 'assert.strict.deepEqual': return 'deepEqual'; case 'assert.notDeepEqual': case 'assert.strict.notDeepEqual': return 'notDeepEqual'; case 'assert.strictEqual': case 'assert.strict.strictEqual': return 'strictEqual'; case 'assert.notStrictEqual': case 'assert.strict.notStrictEqual': return 'notStrictEqual'; case 'assert.deepStrictEqual': case 'assert.strict.deepStrictEqual': return 'deepStrictEqual'; case 'assert.notDeepStrictEqual': case 'assert.strict.notDeepStrictEqual': return 'notDeepStrictEqual'; default: return null; } } /** * covers cases like `const { strictEqual } = require('assert'); strictEqual(...)` or `import { strictEqual } from 'assert'; strictEqual(...)` */ function getNodeJSAssertMethodFromName(name) { switch (name) { case 'ok': case 'deepEqual': case 'notDeepEqual': case 'strictEqual': case 'notStrictEqual': case 'deepStrictEqual': case 'notDeepStrictEqual': return name; default: return null; } } function getArgumentAtIndex(node, index) { const argument = node.arguments[index]; if (!argument || argument.type === 'SpreadElement') { return null; } return argument; }