eslint-plugin-sonarjs
Version:
184 lines (183 loc) • 8.38 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/S8960/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 location_js_1 = require("../helpers/location.js");
const module_js_1 = require("../helpers/module.js");
const mocha_style_test_frameworks_js_1 = require("../helpers/mocha-style-test-frameworks.js");
const meta = __importStar(require("./generated-meta.js"));
const messages = {
singleTestCompletionStyle: 'Use a single completion style for this test callback: remove the completion callback or stop declaring the callback `async`.',
singleHookCompletionStyle: 'Use a single completion style for this hook callback: remove the completion callback or stop declaring the callback `async`.',
};
const JEST_IMPORTS = ['jest', '@jest/globals'];
const JEST_DEPENDENCIES = ['jest'];
const MOCHA_MODULES = ['mocha'];
const JASMINE_MODULES = ['jasmine', 'jasmine-core', 'jasmine-node', 'karma-jasmine'];
const UNSUPPORTED_GLOBAL_FRAMEWORK_IMPORTS = ['vitest', '@playwright/test', 'cypress', 'node:test'];
const UNSUPPORTED_GLOBAL_FRAMEWORK_DEPENDENCIES = ['vitest', '@playwright/test', 'cypress'];
const TEST_FUNCTION_NAMES = new Set(['it', 'test', 'specify', 'fit', 'xit', 'xtest']);
const HOOK_FUNCTION_NAMES = new Set([
'before',
'after',
'beforeEach',
'afterEach',
'beforeAll',
'afterAll',
]);
const SUPPORTED_CONSTRUCT_NAMES = new Set([...TEST_FUNCTION_NAMES, ...HOOK_FUNCTION_NAMES]);
const SUPPORTED_MODULE_FQNS = ['jest', '@jest.globals', 'mocha', 'jasmine'];
const COMMON_TEST_MODIFIERS = new Set(['only']);
const JEST_TEST_MODIFIERS = new Set(['concurrent', 'failing']);
exports.rule = {
meta: (0, generate_meta_js_1.generateMeta)(meta, { messages }),
create(context) {
const activeFrameworks = {
jest: (0, module_js_1.importsOrDependsOnModule)(context, JEST_IMPORTS, JEST_DEPENDENCIES),
mocha: (0, module_js_1.importsOrDependsOnModule)(context, MOCHA_MODULES, MOCHA_MODULES),
jasmine: (0, module_js_1.importsOrDependsOnModule)(context, JASMINE_MODULES, JASMINE_MODULES),
};
if (!Object.values(activeFrameworks).some(Boolean)) {
return {};
}
const allowGlobalConstructs = !(0, module_js_1.importsOrDependsOnModule)(context, UNSUPPORTED_GLOBAL_FRAMEWORK_IMPORTS, UNSUPPORTED_GLOBAL_FRAMEWORK_DEPENDENCIES);
return {
CallExpression(node) {
const callback = getCallback(node);
if (!callback?.async) {
return;
}
const completionCallback = getCompletionCallbackParameter(callback);
if (completionCallback === undefined) {
return;
}
const constructKind = getConstructKind(context, node, activeFrameworks.jest, allowGlobalConstructs);
if (constructKind === undefined) {
return;
}
const asyncToken = context.sourceCode.getFirstToken(callback);
if (asyncToken === null) {
return;
}
const messageId = constructKind === 'hook' ? 'singleHookCompletionStyle' : 'singleTestCompletionStyle';
(0, location_js_1.report)(context, {
loc: asyncToken.loc,
messageId,
message: messages[messageId],
}, [(0, location_js_1.toSecondaryLocation)(completionCallback, 'Completion callback parameter.')]);
},
};
},
};
function getCallback(node) {
return node.arguments.find(isCallback);
}
function isCallback(argument) {
return argument.type === 'FunctionExpression' || argument.type === 'ArrowFunctionExpression';
}
function getCompletionCallbackParameter(callback) {
const [firstParameter] = callback.params;
return (0, ast_js_1.isIdentifier)(firstParameter) ? firstParameter : undefined;
}
function getConstructKind(context, node, jestActive, allowGlobalConstructs) {
const calleeParts = (0, mocha_style_test_frameworks_js_1.getMochaCalleeParts)(node.callee);
if (calleeParts === undefined) {
return undefined;
}
const construct = getSupportedConstruct(context, calleeParts.base, calleeParts.modifiers, allowGlobalConstructs);
if (construct === undefined) {
return undefined;
}
if (HOOK_FUNCTION_NAMES.has(construct.name)) {
return construct.modifiers.length === 0 ? 'hook' : undefined;
}
return construct.modifiers.every(modifier => isConcreteTestModifier(modifier, jestActive))
? 'test'
: undefined;
}
function getSupportedConstruct(context, base, modifiers, allowGlobalConstructs) {
const fqn = (0, module_js_1.getFullyQualifiedName)(context, base);
const importedName = getConstructNameFromFullyQualifiedName(fqn);
if (importedName !== undefined) {
return { name: importedName, modifiers };
}
if (modifiers.length > 0 &&
SUPPORTED_CONSTRUCT_NAMES.has(modifiers[0]) &&
isSupportedModule(fqn)) {
return { name: modifiers[0], modifiers: modifiers.slice(1) };
}
const variable = (0, ast_js_1.getVariableFromScope)(context.sourceCode.getScope(base), base.name);
if (allowGlobalConstructs &&
(variable === undefined || variable.defs.length === 0) &&
SUPPORTED_CONSTRUCT_NAMES.has(base.name)) {
return { name: base.name, modifiers };
}
return undefined;
}
function getConstructNameFromFullyQualifiedName(fqn) {
if (fqn === null) {
return undefined;
}
const moduleName = SUPPORTED_MODULE_FQNS.find(candidate => fqn.startsWith(`${candidate}.`));
const constructName = moduleName === undefined ? undefined : fqn.slice(moduleName.length + 1);
return constructName !== undefined && SUPPORTED_CONSTRUCT_NAMES.has(constructName)
? constructName
: undefined;
}
function isSupportedModule(fqn) {
return fqn !== null && SUPPORTED_MODULE_FQNS.includes(fqn);
}
function isConcreteTestModifier(modifier, jestActive) {
return COMMON_TEST_MODIFIERS.has(modifier) || (jestActive && JEST_TEST_MODIFIERS.has(modifier));
}