diff --git a/.gitignore b/.gitignore index 5f80116c..11e5267c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ npm-debug.log # eslint-remote-tester /eslint-remote-tester-results/ + +# TypeScript output +dist/ diff --git a/README.md b/README.md index 1107f7a5..dec10ac0 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,16 @@ For more details on how to extend your configuration from a plugin configuration | | Name | Description | | :--- | :--- | :--- | -| ✅ | `recommended` | This configuration includes rules which I recommend to avoid QUnit runtime errors or incorrect behavior, some of which can be difficult to debug. Some of these rules also encourage best practices that help QUnit work better for you. For ESLint `.eslintrc.js` legacy config, extend from `"plugin:qunit/recommended"`. For ESLint `eslint.config.js` flat config, load from `require('eslint-plugin-qunit/configs/recommended')`. | +| ✅ | `recommended` | This configuration includes rules which I recommend to avoid QUnit runtime errors or incorrect behavior, some of which can be difficult to debug. Some of these rules also encourage best practices that help QUnit work better for you. For ESLint `.eslintrc.js` legacy config, extend from `"plugin:qunit/recommended"`. For ESLint `eslint.config.js` or `eslint.config.ts` flat config, load from `require('eslint-plugin-qunit/configs/recommended')`. | + +```ts +// eslint.config.ts +import eslintPluginQunitRecommended from 'eslint-plugin-qunit/configs/recommended'; + +export default [ + eslintPluginQunitRecommended, +]; +``` ## Rules diff --git a/eslint-remote-tester.config.js b/eslint-remote-tester.config.mjs similarity index 90% rename from eslint-remote-tester.config.js rename to eslint-remote-tester.config.mjs index 0a70df8d..f35512cf 100644 --- a/eslint-remote-tester.config.js +++ b/eslint-remote-tester.config.mjs @@ -1,9 +1,7 @@ -"use strict"; - -const eslintPluginQunitRecommended = require("./lib/configs/recommended"); +import eslintPluginQunitRecommended from "./lib/configs/recommended.js"; /** @type {import('eslint-remote-tester').Config} */ -module.exports = { +const config = { /** Repositories to scan */ repositories: [ // A few dozen top repositories using QUnit or this plugin. @@ -36,3 +34,5 @@ module.exports = { ...eslintPluginQunitRecommended, }, }; + +export default config; diff --git a/eslint.config.js b/eslint.config.js index 577c0f0c..edefbb34 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,7 +1,11 @@ "use strict"; const js = require("@eslint/js"); + +// @ts-expect-error -- TODO: no types yet const eslintPluginEslintComments = require("@eslint-community/eslint-plugin-eslint-comments/configs"); + +// @ts-expect-error -- TODO: no types yet -- https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/310 const eslintPluginEslintPluginAll = require("eslint-plugin-eslint-plugin/configs/all"); const eslintPluginMarkdown = require("eslint-plugin-markdown"); const eslintPluginMocha = require("eslint-plugin-mocha"); @@ -41,21 +45,6 @@ module.exports = [ eqeqeq: "error", "func-style": ["error", "declaration"], "guard-for-in": "error", - "lines-around-comment": [ - "error", - { - beforeBlockComment: false, - afterBlockComment: false, - beforeLineComment: true, - afterLineComment: false, - allowBlockStart: true, - allowBlockEnd: true, - allowObjectStart: true, - allowObjectEnd: true, - allowArrayStart: true, - allowArrayEnd: true, - }, - ], "max-depth": ["error", 5], "new-cap": ["error", { newIsCap: true, capIsNew: true }], "no-array-constructor": "error", @@ -111,7 +100,6 @@ module.exports = [ "no-throw-literal": "error", "no-trailing-spaces": "error", "no-undef": "error", - "no-undefined": "error", "no-underscore-dangle": "error", "no-unexpected-multiline": "error", "no-unmodified-loop-condition": "error", @@ -209,4 +197,10 @@ module.exports = [ strict: "off", }, }, + { + files: ["**/*.mjs"], + languageOptions: { + sourceType: "module", + }, + }, ]; diff --git a/index.js b/index.js index 49b5baf9..e11cf156 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,8 @@ "use strict"; const requireIndex = require("requireindex"); + +// @ts-expect-error -- ESM/TypeScript conversion should fix this. const pkg = require("./package.json"); module.exports = { @@ -22,7 +24,7 @@ module.exports = { configs: { recommended: { plugins: ["qunit"], - rules: { + rules: /** @type {import('eslint').Linter.RulesRecord} */ ({ "qunit/assert-args": "error", "qunit/literal-compare-order": "error", "qunit/no-assert-equal": "error", @@ -58,7 +60,7 @@ module.exports = { "qunit/require-expect": "error", "qunit/require-object-in-propequal": "error", "qunit/resolve-async": "error", - }, + }), }, }, }; diff --git a/lib/rules/assert-args.js b/lib/rules/assert-args.js index a11b59c2..eebfc950 100644 --- a/lib/rules/assert-args.js +++ b/lib/rules/assert-args.js @@ -35,9 +35,14 @@ module.exports = { }, create: function (context) { + /** @type {Array<{assertContextVar: string | null}>} */ const testStack = [], sourceCode = context.getSourceCode(); + /** + * @param {import('estree').Node} argNode + * @returns {import('estree').Node} + */ function isPossibleMessage(argNode) { // For now, we will allow all nodes. Hoping to allow user-driven // configuration later. @@ -48,16 +53,31 @@ module.exports = { return argNode; } + /** + * @returns {string | null} + */ function getAssertContext() { assert.ok(testStack.length); return testStack[testStack.length - 1].assertContextVar; } + /** + * @param {import('estree').Node} callExpressionNode + */ function checkAssertArity(callExpressionNode) { + if (callExpressionNode.type !== "CallExpression") { + return; + } + + const assertContextVar = getAssertContext(); + if (!assertContextVar) { + return; + } + const allowedArities = utils.getAllowedArities( callExpressionNode.callee, - getAssertContext(), + assertContextVar, ), assertArgs = callExpressionNode.arguments, lastArg = assertArgs[assertArgs.length - 1], @@ -84,7 +104,7 @@ module.exports = { : "unexpectedArgCountNoMessage", data: { callee: sourceCode.getText(callExpressionNode.callee), - argCount: assertArgs.length, + argCount: assertArgs.length.toString(), }, }); } @@ -97,11 +117,14 @@ module.exports = { node.arguments, ), }); - } else if ( - testStack.length > 0 && - utils.isAssertion(node.callee, getAssertContext()) - ) { - checkAssertArity(node); + } else if (testStack.length > 0) { + const assertContext = getAssertContext(); + if ( + assertContext && + utils.isAssertion(node.callee, assertContext) + ) { + checkAssertArity(node); + } } }, diff --git a/lib/rules/literal-compare-order.js b/lib/rules/literal-compare-order.js index d9a55676..6955bcf3 100644 --- a/lib/rules/literal-compare-order.js +++ b/lib/rules/literal-compare-order.js @@ -15,15 +15,6 @@ const assert = require("node:assert"), // Rule Definition //------------------------------------------------------------------------------ -function swapFirstTwoNodesInList(sourceCode, fixer, list) { - const node0Text = sourceCode.getText(list[0]); - const node1Text = sourceCode.getText(list[1]); - return [ - fixer.replaceText(list[0], node1Text), - fixer.replaceText(list[1], node0Text), - ]; -} - /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -45,6 +36,7 @@ module.exports = { }, create: function (context) { + /** @type {Array<{assertContextVar: string | null}>} */ const testStack = [], sourceCode = context.getSourceCode(); @@ -54,6 +46,24 @@ module.exports = { return testStack[testStack.length - 1].assertContextVar; } + /** + * @param {any} fixer + * @param {import('estree').Node[]} list + * @returns {import('eslint').Rule.Fix[]} + */ + function swapFirstTwoNodesInList(fixer, list) { + const node0Text = sourceCode.getText(list[0]); + const node1Text = sourceCode.getText(list[1]); + return [ + fixer.replaceText(list[0], node1Text), + fixer.replaceText(list[1], node0Text), + ]; + } + + /** + * @param {import('estree').Node[]} args + * @param {boolean} compareActualFirst + */ function checkLiteralCompareOrder(args, compareActualFirst) { if (args.length < 2) { return; @@ -73,7 +83,7 @@ module.exports = { actual: sourceCode.getText(args[1]), }, fix(fixer) { - return swapFirstTwoNodesInList(sourceCode, fixer, args); + return swapFirstTwoNodesInList(fixer, args); }, }); } else if ( @@ -89,19 +99,29 @@ module.exports = { actual: sourceCode.getText(args[1]), }, fix(fixer) { - return swapFirstTwoNodesInList(sourceCode, fixer, args); + return swapFirstTwoNodesInList(fixer, args); }, }); } } + /** + * @param {import('eslint').Rule.Node} node + * @param {string | null} assertVar + */ function processAssertion(node, assertVar) { + if (node.type !== "CallExpression") { + return; + } + /* istanbul ignore else: correctly does nothing */ - if (utils.isComparativeAssertion(node.callee, assertVar)) { - const compareActualFirst = utils.shouldCompareActualFirst( - node.callee, - assertVar, - ); + if ( + !assertVar || + utils.isComparativeAssertion(node.callee, assertVar) + ) { + const compareActualFirst = + !assertVar || + utils.shouldCompareActualFirst(node.callee, assertVar); checkLiteralCompareOrder(node.arguments, compareActualFirst); } } @@ -115,11 +135,14 @@ module.exports = { node.arguments, ), }); - } else if ( - testStack.length > 0 && - utils.isAssertion(node.callee, getAssertContext()) - ) { - processAssertion(node, getAssertContext()); + } else if (testStack.length > 0) { + const assertVar = getAssertContext(); + if ( + !assertVar || + utils.isAssertion(node.callee, assertVar) + ) { + processAssertion(node, assertVar); + } } }, diff --git a/lib/rules/no-arrow-tests.js b/lib/rules/no-arrow-tests.js index 80d85657..6e7fa5e0 100644 --- a/lib/rules/no-arrow-tests.js +++ b/lib/rules/no-arrow-tests.js @@ -42,14 +42,38 @@ module.exports = { // Fixer adapted from https://github.com/lo1tuma/eslint-plugin-mocha (MIT) const sourceCode = context.getSourceCode(); + /** + * @param {number} start + * @param {number} end + * @returns {string} + */ function extractSourceTextByRange(start, end) { return sourceCode.text.slice(start, end).trim(); } + /** + * @param {import('estree').FunctionExpression|import('estree').ArrowFunctionExpression} fn + * @returns {string} + */ function formatFunctionHead(fn) { + if ( + fn.type !== "FunctionExpression" && + fn.type !== "ArrowFunctionExpression" + ) { + return ""; + } const arrow = sourceCode.getTokenBefore(fn.body); + if (!arrow) { + return ""; + } const beforeArrowToken = sourceCode.getTokenBefore(arrow); + if (!beforeArrowToken) { + return ""; + } let firstToken = sourceCode.getFirstToken(fn); + if (!firstToken) { + return ""; + } let functionKeyword = "function"; let params = extractSourceTextByRange( @@ -68,6 +92,14 @@ module.exports = { firstToken = sourceCode.getTokenAfter(firstToken); } + if (!firstToken) { + return ""; + } + + if (!fn.body.range) { + return ""; + } + const beforeArrowComment = extractSourceTextByRange( beforeArrowToken.range[1], arrow.range[0], @@ -84,7 +116,20 @@ module.exports = { return `${functionKeyword}${paramsFullText} `; } + /** + * @param {any} fixer + * @param {import('estree').Node} fn + */ function fixArrowFunction(fixer, fn) { + if ( + fn.type !== "FunctionExpression" && + fn.type !== "ArrowFunctionExpression" + ) { + return null; + } + if (!fn.range || !fn.body.range) { + return null; + } if (fn.body.type === "BlockStatement") { // When it((...) => { ... }), // simply replace '(...) => ' with 'function () ' @@ -104,6 +149,9 @@ module.exports = { ); } + /** + * @param {import('estree').Node} fn + */ function checkCallback(fn) { if (fn && fn.type === "ArrowFunctionExpression") { context.report({ @@ -114,6 +162,10 @@ module.exports = { } } + /** + * @param {import('eslint').Rule.Node} propertyNode + * @returns {boolean} + */ function isPropertyInModule(propertyNode) { return ( propertyNode && @@ -125,8 +177,13 @@ module.exports = { ); } + /** + * @param {import('eslint').Rule.Node} propertyNode + * @returns {boolean} + */ function isModuleProperty(propertyNode) { return ( + propertyNode.type === "Property" && isPropertyInModule(propertyNode) && utils.isModuleHookPropertyKey(propertyNode.key) ); diff --git a/lib/rules/no-assert-equal-boolean.js b/lib/rules/no-assert-equal-boolean.js index 33ea0449..fe1278b4 100644 --- a/lib/rules/no-assert-equal-boolean.js +++ b/lib/rules/no-assert-equal-boolean.js @@ -32,6 +32,7 @@ module.exports = { create: function (context) { // Declare a test stack in case of nested test cases (not currently supported by QUnit). + /** @type {Array<{assertVar: string | null}>} */ const testStack = []; function getCurrentAssertContextVariable() { @@ -40,7 +41,11 @@ module.exports = { return testStack[testStack.length - 1].assertVar; } - // Check for something like `equal(...)` without assert parameter. + /** + * Check for something like `equal(...)` without assert parameter. + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isGlobalEqualityAssertion(calleeNode) { return ( calleeNode && @@ -49,7 +54,11 @@ module.exports = { ); } - // Check for something like `assert.equal(...)`. + /** + * Check for something like `assert.equal(...)`. + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isAssertEquality(calleeNode) { return ( calleeNode && @@ -61,7 +70,11 @@ module.exports = { ); } - // Check for something like `equal(...)` or `assert.equal(...)`. + /** + * Check for something like `equal(...)` or `assert.equal(...)`. + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isEqualityAssertion(calleeNode) { return ( isGlobalEqualityAssertion(calleeNode) || @@ -69,29 +82,45 @@ module.exports = { ); } - // Finds the first boolean argument of a CallExpression if one exists. + /** + * Finds the first boolean argument of a CallExpression if one exists. + * @param {import('estree').CallExpression} node + * @returns {import('estree').Node | undefined} + */ function getBooleanArgument(node) { - return ( - node.arguments.length >= 2 && - [node.arguments[0], node.arguments[1]].find( - (arg) => - arg.type === "Literal" && - (arg.value === true || arg.value === false), - ) + if (node.type !== "CallExpression" || node.arguments.length < 2) { + return undefined; // eslint-disable-line unicorn/no-useless-undefined + } + return [node.arguments[0], node.arguments[1]].find( + (arg) => + arg.type === "Literal" && + (arg.value === true || arg.value === false), ); } + /** + * @param {import('estree').CallExpression} node + */ function reportError(node) { context.report({ node: node, messageId: "useAssertTrueOrFalse", fix(fixer) { const booleanArgument = getBooleanArgument(node); + if ( + !booleanArgument || + booleanArgument.type !== "Literal" + ) { + return null; + } const newAssertionFunctionName = booleanArgument.value ? "true" : "false"; const sourceCode = context.getSourceCode(); + if (node.type !== "CallExpression") { + return null; + } const newArgsTextArray = node.arguments .filter((arg) => arg !== booleanArgument) .map((arg) => sourceCode.getText(arg)); diff --git a/lib/rules/no-assert-equal.js b/lib/rules/no-assert-equal.js index 2e4d9c76..5b0b5101 100644 --- a/lib/rules/no-assert-equal.js +++ b/lib/rules/no-assert-equal.js @@ -41,6 +41,7 @@ module.exports = { create: function (context) { // Declare a test stack in case of nested test cases (not currently // supported by QUnit). + /** @type {Array<{assertVar: string | null}>} */ const testStack = []; // We check upfront to find all the references to global equal(), @@ -53,6 +54,10 @@ module.exports = { return testStack[testStack.length - 1].assertVar; } + /** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isAssertEqual(calleeNode) { return ( calleeNode && @@ -64,23 +69,44 @@ module.exports = { ); } + /** + * @param {import('estree').Node} node + * @param {boolean} isGlobal + */ function reportError(node, isGlobal) { + const assertVar = isGlobal + ? null + : getCurrentAssertContextVariable(); context.report({ node: node, messageId: isGlobal ? "unexpectedGlobalEqual" : "unexpectedAssertEqual", - data: { - assertVar: isGlobal - ? null - : getCurrentAssertContextVariable(), - }, + data: assertVar + ? { + assertVar, + } + : {}, suggest: [ { messageId: "switchToDeepEqual", fix(fixer) { + if (node.type !== "CallExpression") { + return null; + } + + // eslint-disable-next-line no-nested-ternary + const nodeToReplace = isGlobal + ? node.callee + : // eslint-disable-next-line unicorn/no-nested-ternary + node.callee.type === "MemberExpression" + ? node.callee.property + : null; + if (!nodeToReplace) { + return null; + } return fixer.replaceText( - isGlobal ? node.callee : node.callee.property, + nodeToReplace, "deepEqual", ); }, @@ -88,8 +114,21 @@ module.exports = { { messageId: "switchToPropEqual", fix(fixer) { + if (node.type !== "CallExpression") { + return null; + } + // eslint-disable-next-line no-nested-ternary + const nodeToReplace = isGlobal + ? node.callee + : // eslint-disable-next-line unicorn/no-nested-ternary + node.callee.type === "MemberExpression" + ? node.callee.property + : null; + if (!nodeToReplace) { + return null; + } return fixer.replaceText( - isGlobal ? node.callee : node.callee.property, + nodeToReplace, "propEqual", ); }, @@ -97,8 +136,21 @@ module.exports = { { messageId: "switchToStrictEqual", fix(fixer) { + if (node.type !== "CallExpression") { + return null; + } + // eslint-disable-next-line no-nested-ternary + const nodeToReplace = isGlobal + ? node.callee + : // eslint-disable-next-line unicorn/no-nested-ternary + node.callee.type === "MemberExpression" + ? node.callee.property + : null; + if (!nodeToReplace) { + return null; + } return fixer.replaceText( - isGlobal ? node.callee : node.callee.property, + nodeToReplace, "strictEqual", ); }, diff --git a/lib/rules/no-assert-logical-expression.js b/lib/rules/no-assert-logical-expression.js index 4e2620d9..7fa41dc9 100644 --- a/lib/rules/no-assert-logical-expression.js +++ b/lib/rules/no-assert-logical-expression.js @@ -34,12 +34,16 @@ module.exports = { }, create: function (context) { + /** @type {Array<{assertContextVar: string}>} */ const testStack = []; //---------------------------------------------------------------------- // Helpers //---------------------------------------------------------------------- + /** + * @param {import('estree').Node[]} argNodes + */ function checkAndReport(argNodes) { for (const arg of argNodes) { if (arg.type === "LogicalExpression") { @@ -71,18 +75,28 @@ module.exports = { return { CallExpression: function (node) { if (utils.isTest(node.callee)) { + const assertContextVar = utils.getAssertContextNameForTest( + node.arguments, + ); + if (!assertContextVar) { + return; + } testStack.push({ - assertContextVar: utils.getAssertContextNameForTest( - node.arguments, - ), + assertContextVar, }); - } else if (utils.isAssertion(node.callee, getAssertVar())) { - const countNonMessageArgs = Math.max( - ...utils.getAllowedArities(node.callee, getAssertVar()), - ); - checkAndReport( - node.arguments.slice(0, countNonMessageArgs), - ); + } else { + const assertVar = getAssertVar(); + if ( + assertVar && + utils.isAssertion(node.callee, assertVar) + ) { + const countNonMessageArgs = Math.max( + ...utils.getAllowedArities(node.callee, assertVar), + ); + checkAndReport( + node.arguments.slice(0, countNonMessageArgs), + ); + } } }, diff --git a/lib/rules/no-async-in-loops.js b/lib/rules/no-async-in-loops.js index 140222ed..702da3bb 100644 --- a/lib/rules/no-async-in-loops.js +++ b/lib/rules/no-async-in-loops.js @@ -27,15 +27,27 @@ module.exports = { }, create: function (context) { - const loopStack = [], - assertVariableStack = []; + /** @type {Array} */ + const loopStack = []; + /** @type {Array} */ + const assertVariableStack = []; + /** + * @param {import('eslint').Rule.Node} node + * @returns {boolean} + */ function isAsyncCallExpression(node) { const assertContextVar = assertVariableStack[assertVariableStack.length - 1]; + if (!assertContextVar) { + return false; + } return utils.isAsyncCallExpression(node, assertContextVar); } + /** + * @param {import('eslint').Rule.Node} expectedNode + */ function popAndMatch(expectedNode) { const actualNode = loopStack.pop(); assert.strictEqual( @@ -45,6 +57,10 @@ module.exports = { ); } + /** + * @param {import('eslint').Rule.NodeTypes} loopType + * @returns {string} + */ function getLoopTypeText(loopType) { switch (loopType) { case "WhileStatement": { @@ -69,6 +85,10 @@ module.exports = { } } + /** + * @param {import('eslint').Rule.Node} node + * @returns {string | undefined} + */ function getAsyncCallType(node) { let callType; @@ -77,15 +97,24 @@ module.exports = { const assertContextVar = assertVariableStack[assertVariableStack.length - 1]; callType = `${assertContextVar}.async()`; - } else if (utils.isStop(node.callee)) { + } else if ( + node.type === "CallExpression" && + utils.isStop(node.callee) + ) { callType = "stop()"; - } else if (utils.isStart(node.callee)) { + } else if ( + node.type === "CallExpression" && + utils.isStart(node.callee) + ) { callType = "start()"; } return callType; } + /** + * @param {import('eslint').Rule.Node} node + */ function reportError(node) { const loopNode = loopStack[loopStack.length - 1], loopType = loopNode.type; @@ -94,7 +123,7 @@ module.exports = { node: node, messageId: "unexpectedAsyncInLoop", data: { - call: getAsyncCallType(node), + call: getAsyncCallType(node) ?? "", loopTypeText: getLoopTypeText(loopType), }, }); @@ -104,9 +133,13 @@ module.exports = { CallExpression: function (node) { /* istanbul ignore else: correctly not doing anything */ if (utils.isTest(node.callee)) { - assertVariableStack.push( - utils.getAssertContextNameForTest(node.arguments), + const assertContextVar = utils.getAssertContextNameForTest( + node.arguments, ); + if (!assertContextVar) { + return; + } + assertVariableStack.push(assertContextVar); } else if (loopStack.length > 0) { const isStopOrStartOrAsync = isAsyncCallExpression(node) || diff --git a/lib/rules/no-async-module-callbacks.js b/lib/rules/no-async-module-callbacks.js index 1c29251d..1dff06a1 100644 --- a/lib/rules/no-async-module-callbacks.js +++ b/lib/rules/no-async-module-callbacks.js @@ -14,9 +14,14 @@ const utils = require("../utils"); // Helpers //------------------------------------------------------------------------------ +/** + * @param {import('estree').Node} node + * @returns {boolean} + */ function isAsyncFunctionExpression(node) { return ( ["ArrowFunctionExpression", "FunctionExpression"].includes(node.type) && + // @ts-expect-error -- node.async is not typed. node.async === true ); } diff --git a/lib/rules/no-commented-tests.js b/lib/rules/no-commented-tests.js index 8611a09d..9b395aa1 100644 --- a/lib/rules/no-commented-tests.js +++ b/lib/rules/no-commented-tests.js @@ -30,6 +30,10 @@ module.exports = { warningRegExp = /\b(QUnit\.test|QUnit\.asyncTest|QUnit\.skip|test|asyncTest)\s*\(\s*["'`]/g; + /** + * @param {string} text + * @returns {number[]} + */ function getNewlineIndexes(text) { const indexes = []; let result; @@ -41,6 +45,10 @@ module.exports = { return indexes; } + /** + * @param {import('estree').Node} node + * @param {{term: string, loc: {line: number, column: number}}} warning + */ function reportWarning(node, warning) { context.report({ node: node, @@ -52,10 +60,13 @@ module.exports = { }); } + /** + * @param {import('estree').Node} node + */ function checkComment(node) { const warnings = [], text = sourceCode.getText(node), - loc = node.loc.start, + loc = node.loc?.start, newlineIndexes = getNewlineIndexes(text); let lineOffset = 0, @@ -71,12 +82,16 @@ module.exports = { currentNewlineIndex = newlineIndexes.shift(); } + if (loc === undefined) { + continue; + } + warnings.push({ term: result[1], loc: { line: loc.line + lineOffset, column: lineOffset - ? result.index - currentNewlineIndex + ? result.index - (currentNewlineIndex ?? 0) : loc.column + result.index, }, }); @@ -91,8 +106,10 @@ module.exports = { Program: function () { const comments = sourceCode .getAllComments() + // @ts-expect-error -- Shebang is unrecognized. .filter((comment) => comment.type !== "Shebang"); for (const comment of comments) { + // @ts-expect-error -- Issue with Node vs. Comment type. checkComment(comment); } }, diff --git a/lib/rules/no-compare-relation-boolean.js b/lib/rules/no-compare-relation-boolean.js index 17b973b9..575b8f59 100644 --- a/lib/rules/no-compare-relation-boolean.js +++ b/lib/rules/no-compare-relation-boolean.js @@ -34,6 +34,7 @@ module.exports = { }, create: function (context) { + /** @type {Array<{assertContextVar: string | null}>} */ const testStack = [], RELATIONAL_OPS = new Set([ "==", @@ -48,18 +49,31 @@ module.exports = { "instanceof", ]); + /** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function shouldCheckArguments(calleeNode) { assert.ok(testStack.length); const assertContextVar = testStack[testStack.length - 1].assertContextVar; + if (!assertContextVar) { + return false; + } + return ( utils.isAssertion(calleeNode, assertContextVar) && utils.isComparativeAssertion(calleeNode, assertContextVar) ); } + /** + * @param {import('estree').Node} a + * @param {import('estree').Node} b + * @returns {0 | 1 | -1} + */ function sortLiteralFirst(a, b) { if (a.type === "Literal" && b.type !== "Literal") { return -1; // Literal is first and should remain first @@ -72,9 +86,16 @@ module.exports = { return 0; } + /** + * @param {import('estree').CallExpression} callExprNode + * @param {import('estree').Literal} literalNode + * @param {import('estree').BinaryExpression} binaryExprNode + */ function checkAndReport(callExprNode, literalNode, binaryExprNode) { if ( + binaryExprNode.type === "BinaryExpression" && RELATIONAL_OPS.has(binaryExprNode.operator) && + literalNode.type === "Literal" && typeof literalNode.value === "boolean" ) { context.report({ @@ -129,7 +150,13 @@ module.exports = { } } + /** + * @param {import('estree').CallExpression} callExprNode + */ function checkAssertArguments(callExprNode) { + if (callExprNode.type !== "CallExpression") { + return; + } const args = [...callExprNode.arguments]; if (args.length < 2) { return; diff --git a/lib/rules/no-conditional-assertions.js b/lib/rules/no-conditional-assertions.js index 4799287a..e68469fd 100644 --- a/lib/rules/no-conditional-assertions.js +++ b/lib/rules/no-conditional-assertions.js @@ -49,20 +49,32 @@ module.exports = { }, create: function (context) { + /** @type {Array<{assertContextVar: string | null}>} */ const testStack = []; //---------------------------------------------------------------------- // Helper functions //---------------------------------------------------------------------- + /** + * @param {import('estree').Node} node + * @returns {boolean} + */ function isConditionalNode(node) { return CONDITIONAL_NODE_TYPES.has(node.type); } + /** + * @param {import('estree').Node} node + * @returns {boolean} + */ function isStopNode(node) { return STOP_NODE_TYPES.has(node.type); } + /** + * @param {import('eslint').Rule.Node} assertNode + */ function checkAndReport(assertNode) { let currentNode = assertNode; @@ -82,9 +94,16 @@ module.exports = { } } + /** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isAssertion(calleeNode) { const assertContextVar = testStack[testStack.length - 1].assertContextVar; + if (!assertContextVar) { + return false; + } return utils.isAssertion(calleeNode, assertContextVar); } diff --git a/lib/rules/no-early-return.js b/lib/rules/no-early-return.js index 97a7b46c..5b4209c2 100644 --- a/lib/rules/no-early-return.js +++ b/lib/rules/no-early-return.js @@ -31,7 +31,9 @@ module.exports = { }, create: function (context) { + /** @type {string | null} */ let assertContextVar = null; + /** @type {Array<{returnAndAssertNodes: import('eslint').Rule.Node[]}>} */ const functionScopes = []; function pushFunction() { @@ -45,6 +47,9 @@ module.exports = { function popFunction() { if (assertContextVar !== null) { const lastScope = functionScopes.pop(); + if (!lastScope) { + return; + } let lastAssert = null, i; @@ -89,6 +94,7 @@ module.exports = { node.arguments, ); } else if ( + assertContextVar && utils.isAssertion(node.callee, assertContextVar) && functionScopes.length > 0 ) { diff --git a/lib/rules/no-global-assertions.js b/lib/rules/no-global-assertions.js index e71aefd5..9fdd0fc3 100644 --- a/lib/rules/no-global-assertions.js +++ b/lib/rules/no-global-assertions.js @@ -43,6 +43,7 @@ module.exports = { : context.getScope(); const tracker = new ReferenceTracker(scope); + /** @type {Record} */ const traceMap = {}; for (const assertionName of getAssertionNames()) { traceMap[assertionName] = { [ReferenceTracker.CALL]: true }; @@ -51,6 +52,12 @@ module.exports = { for (const { node } of tracker.iterateGlobalReferences( traceMap, )) { + if (node.type !== "CallExpression") { + continue; + } + if (node.callee.type !== "Identifier") { + continue; + } context.report({ node: node, messageId: "unexpectedGlobalAssertion", diff --git a/lib/rules/no-global-module-test.js b/lib/rules/no-global-module-test.js index efb1e3d9..15640a7c 100644 --- a/lib/rules/no-global-module-test.js +++ b/lib/rules/no-global-module-test.js @@ -50,6 +50,12 @@ module.exports = { for (const { node } of tracker.iterateGlobalReferences( traceMap, )) { + if (node.type !== "CallExpression") { + continue; + } + if (node.callee.type !== "Identifier") { + continue; + } context.report({ node: node, messageId: "unexpectedGlobalModuleTest", diff --git a/lib/rules/no-global-stop-start.js b/lib/rules/no-global-stop-start.js index c30ba834..3a9b317b 100644 --- a/lib/rules/no-global-stop-start.js +++ b/lib/rules/no-global-stop-start.js @@ -51,6 +51,12 @@ module.exports = { for (const { node } of tracker.iterateGlobalReferences( traceMap, )) { + if (node.type !== "CallExpression") { + continue; + } + if (node.callee.type !== "Identifier") { + continue; + } context.report({ node: node, messageId: "unexpectedGlobalStopStart", diff --git a/lib/rules/no-hooks-from-ancestor-modules.js b/lib/rules/no-hooks-from-ancestor-modules.js index dfbb9bac..2931afd2 100644 --- a/lib/rules/no-hooks-from-ancestor-modules.js +++ b/lib/rules/no-hooks-from-ancestor-modules.js @@ -35,12 +35,17 @@ module.exports = { }, create: function (context) { + /** @type {Array<{callExpression: import('eslint').Rule.Node, description: string, hookIdentifierName?: string | null}>} */ const moduleStack = []; //---------------------------------------------------------------------- // Helpers //---------------------------------------------------------------------- + /** + * @param {import('eslint').Rule.Node} callExpressionNode + * @returns {boolean} + */ function isInModuleCallbackBody(callExpressionNode) { return ( callExpressionNode && @@ -61,15 +66,25 @@ module.exports = { ); } + /** + * @param {import('eslint').Rule.Node} node + * @returns {boolean} + */ function isHookInvocation(node) { return ( + node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && + node.callee.property.type === "Identifier" && NESTABLE_HOOK_NAMES.has(node.callee.property.name) && isInModuleCallbackBody(node) ); } + /** + * @param {import('estree').Node[]} args + * @returns {import('estree').Node | undefined} + */ function getCallbackArg(args) { // Callback can be either args[1] or args[2] // https://api.qunitjs.com/QUnit/module/ @@ -82,6 +97,10 @@ module.exports = { ); } + /** + * @param {import('estree').Node[]} params + * @returns {import('estree').Node | undefined} + */ function getHooksIdentifierFromParams(params) { // In TypeScript, `this` can be passed as the first function parameter to add a type to it, // and we want to ignore that parameter since we're looking for the `hooks` variable. @@ -95,26 +114,54 @@ module.exports = { //---------------------------------------------------------------------- return { + // eslint-disable-next-line complexity CallExpression: function (node) { if (utils.isModule(node.callee)) { + if (node.arguments.length === 0) { + return; + } + if ( + node.arguments[0].type !== "Literal" || + node.arguments[0].value === null || + node.arguments[0].value === undefined + ) { + return; + } + /** @type {{callExpression: import('eslint').Rule.Node, description: string, hookIdentifierName?: string | null}} */ const moduleStackInfo = { callExpression: node, - description: node.arguments[0].value, + description: node.arguments[0].value.toString(), }; - const callback = getCallbackArg(node.arguments); - const hooksParam = callback - ? getHooksIdentifierFromParams(callback.params) - : null; - moduleStackInfo.hookIdentifierName = hooksParam - ? hooksParam.name - : null; + if ( + !callback || + (callback.type !== "FunctionExpression" && + callback.type !== "ArrowFunctionExpression") + ) { + return; + } + const hooksParam = getHooksIdentifierFromParams( + callback.params, + ); + moduleStackInfo.hookIdentifierName = + hooksParam && hooksParam.type === "Identifier" + ? hooksParam.name + : null; moduleStack.push(moduleStackInfo); } else if (isHookInvocation(node)) { const containingModuleInfo = moduleStack[moduleStack.length - 1]; const expectedHooksIdentifierName = containingModuleInfo.hookIdentifierName; + + if ( + node.callee.type !== "MemberExpression" || + node.callee.object.type !== "Identifier" || + node.callee.property.type !== "Identifier" + ) { + return; + } + const usedHooksIdentifierName = node.callee.object.name; const invokedMethodName = node.callee.property.name; diff --git a/lib/rules/no-identical-names.js b/lib/rules/no-identical-names.js index ee9326bd..7d9e6754 100644 --- a/lib/rules/no-identical-names.js +++ b/lib/rules/no-identical-names.js @@ -14,6 +14,12 @@ const utils = require("../utils"); // Rule Definition //------------------------------------------------------------------------------ +/** + * @template T + * @param {T[]} arr + * @param {(item: T) => boolean} callback + * @returns {T | undefined} + */ function findLast(arr, callback) { return [...arr].reverse().find((item) => callback(item)); } @@ -40,7 +46,9 @@ module.exports = { create: function (context) { const TOP_LEVEL_MODULE_NODE = "top-level-module"; // Constant representing the implicit top-level module. + /** @type {Array} */ const modulesStack = [TOP_LEVEL_MODULE_NODE]; + /** @type {Map} */ const mapModuleNodeToInfo = new Map(); mapModuleNodeToInfo.set(TOP_LEVEL_MODULE_NODE, { modules: [], // Children module nodes. @@ -52,16 +60,28 @@ module.exports = { // Helper functions //---------------------------------------------------------------------- + /** + * @param {import('estree').Node} node + * @returns {boolean} + */ function isFirstArgLiteral(node) { return ( + node.type === "CallExpression" && node.arguments && node.arguments[0] && node.arguments[0].type === "Literal" ); } + /** + * @param {import('estree').Node} node + * @returns {boolean} + */ function moduleHasNestedTests(node) { const moduleInfo = mapModuleNodeToInfo.get(node); + if (!moduleInfo) { + return false; + } return ( moduleInfo.tests.length > 0 || moduleInfo.modules.some((moduleNode) => @@ -74,11 +94,11 @@ module.exports = { const parentModule = mapModuleNodeToInfo.get( modulesStack[modulesStack.length - 1], ); - if (parentModule.modules.length > 0) { + if (parentModule && parentModule.modules.length > 0) { // Find the last test-less module at the current level if one exists, i.e: module('foo'); const lastTestLessModule = findLast( parentModule.modules, - (node) => !mapModuleNodeToInfo.get(node).hasNestedTests, + (node) => !mapModuleNodeToInfo.get(node)?.hasNestedTests, ); if (lastTestLessModule) { return lastTestLessModule; @@ -87,50 +107,96 @@ module.exports = { return modulesStack[modulesStack.length - 1]; } + /** + * @param {import('estree').Node} node + */ + // eslint-disable-next-line complexity function handleTestNames(node) { - if (utils.isTest(node.callee)) { - const title = node.arguments[0].value; - const currentModuleNode = getCurrentModuleNode(); - const currentModuleInfo = - mapModuleNodeToInfo.get(currentModuleNode); + if (node.type !== "CallExpression" || !utils.isTest(node.callee)) { + return; + } - // Check if we have seen this test name in the current module yet. - const duplicateTestTitle = currentModuleInfo.tests.find( - (t) => t.arguments[0].value === title, - ); - if (duplicateTestTitle) { - context.report({ - node: node.arguments[0], - messageId: "duplicateTest", - data: { - line: duplicateTestTitle.arguments[0].loc.start - .line, - }, - }); - } + if ( + node.arguments.length === 0 || + node.arguments[0].type !== "Literal" + ) { + return; + } + + const title = node.arguments[0].value; + const currentModuleNode = getCurrentModuleNode(); + const currentModuleInfo = + mapModuleNodeToInfo.get(currentModuleNode); + if (!currentModuleInfo) { + return; + } - // Add this test to the current module's list of tests. - currentModuleInfo.tests.push(node); + // Check if we have seen this test name in the current module yet. + const duplicateTestTitle = currentModuleInfo.tests.find( + (t) => + t.type === "CallExpression" && + t.arguments[0].type === "Literal" && + t.arguments[0].value === title, + ); + if ( + duplicateTestTitle && + duplicateTestTitle.type === "CallExpression" + ) { + context.report({ + node: node.arguments[0], + messageId: "duplicateTest", + data: { + line: + duplicateTestTitle.arguments[0]?.loc?.start?.line.toString() ?? + "", + }, + }); } + + // Add this test to the current module's list of tests. + currentModuleInfo.tests.push(node); } + /** + * @param {import('estree').Node} node + */ + // eslint-disable-next-line complexity function handleModuleNames(node) { - if (utils.isModule(node.callee)) { + if (node.type === "CallExpression" && utils.isModule(node.callee)) { + if (node.arguments.length === 0) { + return; + } + if (node.arguments[0].type !== "Literal") { + return; + } const title = node.arguments[0].value; const currentModuleNode = modulesStack[modulesStack.length - 1]; + const currentModuleInfo = mapModuleNodeToInfo.get(currentModuleNode); + if (!currentModuleInfo) { + return; + } + // Check if we have seen the same title in a sibling module. const duplicateModuleTitle = currentModuleInfo.modules.find( - (moduleNode) => moduleNode.arguments[0].value === title, + (moduleNode) => + moduleNode.type === "CallExpression" && + moduleNode.arguments[0].type === "Literal" && + moduleNode.arguments[0].value === title, ); - if (duplicateModuleTitle) { + if ( + duplicateModuleTitle && + duplicateModuleTitle.type === "CallExpression" + ) { context.report({ node: node.arguments[0], messageId: "duplicateModule", data: { - line: duplicateModuleTitle.loc.start.line, + line: + duplicateModuleTitle.arguments[0]?.loc?.start?.line.toString() ?? + "", }, }); } @@ -141,15 +207,22 @@ module.exports = { (moduleNode) => moduleNode !== TOP_LEVEL_MODULE_NODE, ) .find( - (moduleNode) => moduleNode.arguments[0].value === title, + (moduleNode) => + moduleNode.type === "CallExpression" && + moduleNode.arguments[0].type === "Literal" && + moduleNode.arguments[0].value === title, ); - if (duplicateAncestorModuleTitle) { + if ( + duplicateAncestorModuleTitle && + duplicateAncestorModuleTitle.type === "CallExpression" + ) { context.report({ node: node.arguments[0], messageId: "duplicateModuleAncestor", data: { - line: duplicateAncestorModuleTitle.arguments[0].loc - .start.line, + line: + duplicateAncestorModuleTitle.arguments[0]?.loc?.start?.line.toString() ?? + "", }, }); } @@ -188,8 +261,10 @@ module.exports = { modulesStack.pop(); // Record if we saw any nested tests in this module. - mapModuleNodeToInfo.get(node).hasNestedTests = - moduleHasNestedTests(node); + const moduleInfo = mapModuleNodeToInfo.get(node); + if (moduleInfo) { + moduleInfo.hasNestedTests = moduleHasNestedTests(node); + } } }, }; diff --git a/lib/rules/no-init.js b/lib/rules/no-init.js index 99faab0c..25c53d66 100644 --- a/lib/rules/no-init.js +++ b/lib/rules/no-init.js @@ -32,6 +32,9 @@ module.exports = { return { "CallExpression[callee.object.name='QUnit'][callee.property.name='init']": + /** + * @param {import('eslint').Rule.Node} node + */ function (node) { context.report({ node: node, diff --git a/lib/rules/no-jsdump.js b/lib/rules/no-jsdump.js index 8bcda215..a83d01a4 100644 --- a/lib/rules/no-jsdump.js +++ b/lib/rules/no-jsdump.js @@ -30,6 +30,9 @@ module.exports = { return { "CallExpression[callee.object.name='QUnit'][callee.property.name='jsDump']": + /** + * @param {import('eslint').Rule.Node} node + */ function (node) { context.report({ node: node, diff --git a/lib/rules/no-loose-assertions.js b/lib/rules/no-loose-assertions.js index 77f35b59..7d6f0ed0 100644 --- a/lib/rules/no-loose-assertions.js +++ b/lib/rules/no-loose-assertions.js @@ -37,6 +37,11 @@ const ERROR_MESSAGE_CONFIG = { }, }; +/** + * @typedef {{unexpectedGlobalAssertionMessage?: string, unexpectedLocalAssertionMessage?: string, unexpectedGlobalAssertionMessageId?: string, unexpectedLocalAssertionMessageId?: string}} ErrorMessageConfig + * @param {string[]} disallowed + * @returns {ErrorMessageConfig} + */ function buildErrorMessage(disallowed) { const globalMessage = `Unexpected {{assertion}}. Use ${disallowed.join( ", ", @@ -50,9 +55,16 @@ function buildErrorMessage(disallowed) { }; } +/** + * @typedef {[Array, ...never[]]} Options + * @param {Options} options + * @returns {[string[], Record]} + */ function parseOptions(options) { if (options[0]) { + /** @type {string[]} */ const assertions = []; + /** @type {Record} */ const errorMessageConfig = {}; for (const assertion of options[0]) { if (typeof assertion === "string") { @@ -61,7 +73,12 @@ function parseOptions(options) { continue; } assertions.push(assertion); - errorMessageConfig[assertion] = ERROR_MESSAGE_CONFIG[assertion]; + errorMessageConfig[assertion] = + ERROR_MESSAGE_CONFIG[ + /** @type {keyof typeof ERROR_MESSAGE_CONFIG} */ ( + assertion + ) + ]; } else { // Skip if rule was defined before. if (assertions.includes(assertion.disallowed)) { @@ -130,7 +147,9 @@ module.exports = { }, create: function (context) { - const [assertions, errorMessageConfig] = parseOptions(context.options); + const [assertions, errorMessageConfig] = parseOptions( + /** @type {Options} */ (context.options), + ); return utils .createAssertionCheck(assertions, errorMessageConfig) .call(this, context); diff --git a/lib/rules/no-negated-ok.js b/lib/rules/no-negated-ok.js index 406b42a3..2073ce1b 100644 --- a/lib/rules/no-negated-ok.js +++ b/lib/rules/no-negated-ok.js @@ -40,6 +40,7 @@ module.exports = { // Declare a stack in case of nested test cases (not currently supported // in QUnit). + /** @type {Array<{assertContextVar: string | null}>} */ const asyncStateStack = [], ASSERTIONS_TO_CHECK = new Set([ ...POSITIVE_ASSERTIONS, @@ -60,6 +61,10 @@ module.exports = { return result; } + /** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isOkOrNotOk(calleeNode) { assert.ok(calleeNode); @@ -78,11 +83,23 @@ module.exports = { return result; } + /** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isAssertion(calleeNode) { assert.ok(calleeNode); - return utils.isAssertion(calleeNode, getAssertVar()); + const assertVar = getAssertVar(); + if (!assertVar) { + return false; + } + return utils.isAssertion(calleeNode, assertVar); } + /** + * @param {import('estree').Node} argNode + * @returns {number} + */ function getNegationDepth(argNode) { let negationDepth = 0, node = argNode; @@ -99,6 +116,10 @@ module.exports = { return negationDepth; } + /** + * @param {import('estree').Node} argNode + * @returns {import('estree').Node} + */ function unwrapNegation(argNode) { let node = argNode; @@ -113,8 +134,15 @@ module.exports = { return node; } + /** + * @param {import('eslint').Rule.Node} callExprNode + */ function checkForNegation(callExprNode) { - if (callExprNode.arguments && callExprNode.arguments.length > 0) { + if ( + callExprNode.type === "CallExpression" && + callExprNode.arguments && + callExprNode.arguments.length > 0 + ) { const firstArgNode = callExprNode.arguments[0], negationDepth = getNegationDepth(firstArgNode); @@ -130,12 +158,33 @@ module.exports = { // * assert.notOk(!foo) => assert.ok(foo) // * assert.ok(!foo) => assert.notOk(foo) + if ( + callExprNode.callee.type !== + "MemberExpression" || + callExprNode.callee.object.type !== + "Identifier" || + callExprNode.callee.property.type !== + "Identifier" + ) { + return null; + } + const assertionVariableName = callExprNode.callee.object.name; + + const propertyName = + callExprNode.callee.property.name; + if ( + propertyName !== "true" && + propertyName !== "false" && + propertyName !== "ok" && + propertyName !== "notOk" + ) { + return null; + } + const oppositeAssertionFunctionName = - ASSERTION_OPPOSITES[ - callExprNode.callee.property.name - ]; + ASSERTION_OPPOSITES[propertyName]; const newArgsTextArray = [ unwrapNegation(firstArgNode), ...callExprNode.arguments.slice(1), diff --git a/lib/rules/no-nested-tests.js b/lib/rules/no-nested-tests.js index a65e0a46..11a1013c 100644 --- a/lib/rules/no-nested-tests.js +++ b/lib/rules/no-nested-tests.js @@ -47,6 +47,8 @@ module.exports = { }); return; } + + // @ts-expect-error -- Type issue with eslint vs. estree node. currentNode = parent; } } diff --git a/lib/rules/no-ok-equality.js b/lib/rules/no-ok-equality.js index cb0aa4db..fe63d762 100644 --- a/lib/rules/no-ok-equality.js +++ b/lib/rules/no-ok-equality.js @@ -44,6 +44,7 @@ module.exports = { create: function (context) { // Declare a stack in case of nested test cases (not currently supported // in QUnit). + /** @type {Array<{assertContextVar: string | null}>} */ const asyncStateStack = [], DEFAULT_OPTIONS = { allowGlobal: true, @@ -59,6 +60,10 @@ module.exports = { return state && state.assertContextVar; } + /** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isOk(calleeNode) { const assertContextVar = getAssertContextVar(); @@ -80,6 +85,10 @@ module.exports = { return isAssertOk; } + /** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isNotOk(calleeNode) { const assertContextVar = getAssertContextVar(); @@ -101,10 +110,18 @@ module.exports = { return isAssertNotOk; } + /** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isOkOrNotOk(calleeNode) { return isOk(calleeNode) || isNotOk(calleeNode); } + /** + * @param {import('estree').Node} arg + * @returns {boolean} + */ function isEqual(arg) { return ( arg.type === "BinaryExpression" && @@ -112,6 +129,10 @@ module.exports = { ); } + /** + * @param {import('estree').Node} arg + * @returns {boolean} + */ function isStrict(arg) { return ( arg.type === "BinaryExpression" && @@ -119,6 +140,10 @@ module.exports = { ); } + /** + * @param {import('estree').Node} arg + * @returns {boolean} + */ function isNegative(arg) { return ( arg.type === "BinaryExpression" && @@ -126,6 +151,10 @@ module.exports = { ); } + /** + * @param {{strict: boolean, negative: boolean}} criteria + * @returns {string} + */ function getSuggestedAssertion(criteria) { const assertVar = getAssertContextVar(); let assertMethod; @@ -145,6 +174,12 @@ module.exports = { return assertMethod; } + /** + * @param {import('estree').Node[]} args + * @param {boolean} isCalleeNegative + * @param {boolean} isGlobal + * @param {import('estree').CallExpression} node + */ function checkArguments(args, isCalleeNegative, isGlobal, node) { /* istanbul ignore else: will correctly do nothing */ if (args.length > 0) { @@ -158,6 +193,10 @@ module.exports = { negative: isArgNegative !== isCalleeNegative, }); + if (firstArg.type !== "BinaryExpression") { + return; + } + const a = sourceCode.getText(firstArg.left); const b = sourceCode.getText(firstArg.right); diff --git a/lib/rules/no-qunit-push.js b/lib/rules/no-qunit-push.js index 5232d102..0c885c79 100644 --- a/lib/rules/no-qunit-push.js +++ b/lib/rules/no-qunit-push.js @@ -30,6 +30,9 @@ module.exports = { return { "CallExpression[callee.object.name='QUnit'][callee.property.name='push']": + /** + * @param {import('estree').Node} node + */ function (node) { context.report({ node: node, diff --git a/lib/rules/no-qunit-start-in-tests.js b/lib/rules/no-qunit-start-in-tests.js index 4a2409fa..009815e0 100644 --- a/lib/rules/no-qunit-start-in-tests.js +++ b/lib/rules/no-qunit-start-in-tests.js @@ -33,12 +33,17 @@ module.exports = { }, create: function (context) { + /** @type {Array} */ const contextStack = []; //---------------------------------------------------------------------- // Helpers //---------------------------------------------------------------------- + /** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isQUnitStart(calleeNode) { return ( calleeNode.type === "MemberExpression" && @@ -46,6 +51,10 @@ module.exports = { ); } + /** + * @param {import('eslint').Rule.Node} propertyNode + * @returns {boolean} + */ function isInModule(propertyNode) { return ( propertyNode && @@ -84,7 +93,8 @@ module.exports = { Property: function (node) { if ( utils.isModuleHookPropertyKey(node.key) && - isInModule(node) + isInModule(node) && + node.key.type === "Identifier" ) { contextStack.push(`${node.key.name} hook`); } diff --git a/lib/rules/no-qunit-stop.js b/lib/rules/no-qunit-stop.js index 6570927b..830b25cf 100644 --- a/lib/rules/no-qunit-stop.js +++ b/lib/rules/no-qunit-stop.js @@ -30,6 +30,10 @@ module.exports = { }, create: function (context) { + /** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isQUnitStop(calleeNode) { return ( calleeNode && diff --git a/lib/rules/no-reassign-log-callbacks.js b/lib/rules/no-reassign-log-callbacks.js index 2e2b0619..634a14eb 100644 --- a/lib/rules/no-reassign-log-callbacks.js +++ b/lib/rules/no-reassign-log-callbacks.js @@ -40,17 +40,22 @@ module.exports = { // Public //-------------------------------------------------------------------------- + /** @type {Record void>} */ const visitors = {}; for (const callbackName of LOG_CALLBACKS) { visitors[ `AssignmentExpression[left.object.name='QUnit'][left.property.name='${callbackName}']` - ] = function (node) { - context.report({ - node: node, - messageId: "noReassignLogCallbacks", - }); - }; + ] = + /** + * @param {import('estree').Node} node + */ + function (node) { + context.report({ + node: node, + messageId: "noReassignLogCallbacks", + }); + }; } return visitors; diff --git a/lib/rules/no-reset.js b/lib/rules/no-reset.js index b251e193..2b41c9b3 100644 --- a/lib/rules/no-reset.js +++ b/lib/rules/no-reset.js @@ -32,6 +32,9 @@ module.exports = { return { "CallExpression[callee.object.name='QUnit'][callee.property.name='reset']": + /** + * @param {import('eslint').Rule.Node} node + */ function (node) { context.report({ node: node, diff --git a/lib/rules/no-setup-teardown.js b/lib/rules/no-setup-teardown.js index d5b9b567..0d0d7c98 100644 --- a/lib/rules/no-setup-teardown.js +++ b/lib/rules/no-setup-teardown.js @@ -34,30 +34,44 @@ module.exports = { teardown: "afterEach", }; + /** + * @param {import('estree').Property} propertyNode + */ function checkModuleHook(propertyNode) { if ( + propertyNode.type === "Property" && + propertyNode.key.type === "Identifier" && Object.prototype.hasOwnProperty.call( replacements, propertyNode.key.name, ) ) { + const propertyKeyName = propertyNode.key.name; + if ( + propertyKeyName !== "setup" && + propertyKeyName !== "teardown" + ) { + return; + } + const replacement = replacements[propertyKeyName]; context.report({ node: propertyNode, messageId: "noSetupTeardown", data: { forbidden: propertyNode.key.name, - preferred: replacements[propertyNode.key.name], + preferred: replacement, }, fix(fixer) { - return fixer.replaceText( - propertyNode.key, - replacements[propertyNode.key.name], - ); + return fixer.replaceText(propertyNode.key, replacement); }, }); } } + /** + * @param {import('eslint').Rule.Node} propertyNode + * @returns {boolean} + */ function isInModule(propertyNode) { return ( propertyNode && diff --git a/lib/rules/no-throws-string.js b/lib/rules/no-throws-string.js index ff8ae73e..52db9e28 100644 --- a/lib/rules/no-throws-string.js +++ b/lib/rules/no-throws-string.js @@ -15,12 +15,21 @@ const assert = require("node:assert"), // Helpers //------------------------------------------------------------------------------ +/** + * @param {Array<{assertVar: string | null}>} testStack + * @returns {string | null} + */ function getAssertVar(testStack) { assert.ok(testStack && testStack.length > 0); return testStack[testStack.length - 1].assertVar; } +/** + * @param {import('estree').Node} calleeNode + * @param {string | null} assertVar + * @returns {boolean} + */ function isThrows(calleeNode, assertVar) { let result = false; @@ -59,10 +68,17 @@ module.exports = { }, create: function (context) { + /** @type {Array<{assertVar: string | null}>} */ const testStack = [], sourceCode = context.getSourceCode(); + /** + * @param {import('eslint').Rule.Node} callExprNode + */ function checkAndReport(callExprNode) { + if (callExprNode.type !== "CallExpression") { + return; + } const args = callExprNode.arguments, argCount = args.length; diff --git a/lib/rules/require-expect.js b/lib/rules/require-expect.js index 52931b35..d60381e7 100644 --- a/lib/rules/require-expect.js +++ b/lib/rules/require-expect.js @@ -34,26 +34,49 @@ module.exports = { }, create: function (context) { - let currentTest = false; + /** @typedef {{assertName: string|null, node: import('estree').CallExpression, blockDepth: number, isExpectUsed: boolean, didReport: boolean, isNonZeroExpectUsed?: boolean}} TestContext */ + /** @type {TestContext|undefined} */ + let currentTest; + /** + * @param {import('estree').Node} callee + * @returns {boolean} + */ function isGlobalExpectCall(callee) { return callee.type === "Identifier" && callee.name === "expect"; } + /** + * @param {import('estree').Node} callee + * @returns {boolean} + */ function isAssertExpectCall(callee) { return ( + callee.type === "MemberExpression" && callee.object && callee.object.type === "Identifier" && - callee.object.name === currentTest.assertName && + callee.object.name === currentTest?.assertName && + callee.property.type === "Identifier" && callee.property.name === "expect" ); } + /** + * @param {import('estree').Node} callee + * @returns {boolean} + */ function isExpectCall(callee) { return isGlobalExpectCall(callee) || isAssertExpectCall(callee); } + /** + * @param {import('estree').CallExpression} node + * @returns {boolean} + */ function isNonZeroExpectCall(node) { + if (node.type !== "CallExpression") { + return false; + } return ( isExpectCall(node.callee) && !( @@ -64,31 +87,65 @@ module.exports = { ); } + /** + * @param {import('estree').Node} callee + * @returns {boolean} + */ function isTopLevelExpectCall(callee) { + if (!currentTest) { + return false; + } return isExpectCall(callee) && currentTest.blockDepth === 1; } + /** + * @param {import('eslint').Rule.Node} node + * @returns {boolean} + */ function isUsingAssertInNestedBlock(node) { + if (!currentTest) { + return false; + } return ( currentTest.blockDepth > 1 && - utils.isAssertion(node.callee, currentTest.assertName) + node.type === "CallExpression" && + (!currentTest.assertName || + utils.isAssertion(node.callee, currentTest.assertName)) ); } + /** + * @param {import('estree').Node} node + * @returns {boolean} + */ function isPassingAssertAsArgument(node) { - if (!currentTest.assertName) { + if (!currentTest?.assertName) { return false; } + if (node.type !== "CallExpression") { + return false; + } for (let i = 0; i < node.arguments.length; i++) { - if (node.arguments[i].name === currentTest.assertName) { + const arg = node.arguments[i]; + if ( + arg.type === "Identifier" && + arg.name === currentTest.assertName + ) { return true; } } return false; } + /** + * @param {import('eslint').Rule.Node} node + * @returns {boolean} + */ function isViolatingExceptSimpleRule(node) { + if (!currentTest) { + return false; + } return ( !currentTest.isExpectUsed && (isUsingAssertInNestedBlock(node) || @@ -96,7 +153,13 @@ module.exports = { ); } + /** + * @param {import('eslint').Rule.Node} node + */ function captureTestContext(node) { + if (node.type !== "CallExpression") { + return; + } currentTest = { assertName: utils.getAssertContextNameForTest(node.arguments), node: node, @@ -107,19 +170,25 @@ module.exports = { } function releaseTestContext() { - currentTest = false; + currentTest = undefined; } function assertionMessageData() { return { - expect: currentTest.assertName + expect: currentTest?.assertName ? `${currentTest.assertName}.expect` : "expect", }; } const ExceptSimpleStrategy = { + /** + * @param {import('eslint').Rule.Node} node + */ CallExpression: function (node) { + if (node.type !== "CallExpression") { + return; + } if (currentTest && !currentTest.didReport) { if (isTopLevelExpectCall(node.callee)) { currentTest.isExpectUsed = true; @@ -136,7 +205,13 @@ module.exports = { } }, + /** + * @param {import('eslint').Rule.Node} node + */ "CallExpression:exit": function (node) { + if (node.type !== "CallExpression") { + return; + } if (utils.isTest(node.callee)) { releaseTestContext(); } @@ -158,7 +233,13 @@ module.exports = { }; const AlwaysStrategy = { + /** + * @param {import('eslint').Rule.Node} node + */ CallExpression: function (node) { + if (node.type !== "CallExpression") { + return; + } if (currentTest && isExpectCall(node.callee)) { currentTest.isExpectUsed = true; } else if (utils.isTest(node.callee)) { @@ -166,7 +247,16 @@ module.exports = { } }, + /** + * @param {import('eslint').Rule.Node} node + */ "CallExpression:exit": function (node) { + if (node.type !== "CallExpression") { + return; + } + if (!currentTest) { + return; + } if (utils.isTest(node.callee)) { if (!currentTest.isExpectUsed) { context.report({ @@ -182,14 +272,30 @@ module.exports = { }; const NeverStrategy = { + /** + * @param {import('eslint').Rule.Node} node + */ CallExpression: function (node) { + if (node.type !== "CallExpression") { + return; + } if (currentTest && isExpectCall(node.callee)) { currentTest.isExpectUsed = true; } else if (utils.isTest(node.callee)) { captureTestContext(node); } }, + + /** + * @param {import('eslint').Rule.Node} node + */ "CallExpression:exit": function (node) { + if (node.type !== "CallExpression") { + return; + } + if (!currentTest) { + return; + } if (utils.isTest(node.callee)) { if (currentTest.isExpectUsed) { context.report({ @@ -204,14 +310,30 @@ module.exports = { }; const NeverExceptZeroStrategy = { + /** + * @param {import('eslint').Rule.Node} node + */ CallExpression: function (node) { + if (node.type !== "CallExpression") { + return; + } if (currentTest && isNonZeroExpectCall(node)) { currentTest.isNonZeroExpectUsed = true; } else if (utils.isTest(node.callee)) { captureTestContext(node); } }, + + /** + * @param {import('eslint').Rule.Node} node + */ "CallExpression:exit": function (node) { + if (node.type !== "CallExpression") { + return; + } + if (!currentTest) { + return; + } if (utils.isTest(node.callee)) { if (currentTest.isNonZeroExpectUsed) { context.report({ @@ -225,13 +347,16 @@ module.exports = { }, }; + /** @type {"always" | "except-simple" | "never" | "never-except-zero"} */ + const option = context.options[0]; + return ( { always: AlwaysStrategy, "except-simple": ExceptSimpleStrategy, never: NeverStrategy, "never-except-zero": NeverExceptZeroStrategy, - }[context.options[0]] || NeverExceptZeroStrategy + }[option] || NeverExceptZeroStrategy ); }, }; diff --git a/lib/rules/require-object-in-propequal.js b/lib/rules/require-object-in-propequal.js index 07c190aa..46786469 100644 --- a/lib/rules/require-object-in-propequal.js +++ b/lib/rules/require-object-in-propequal.js @@ -35,6 +35,7 @@ module.exports = { create: function (context) { // Declare a test stack in case of nested test cases (not currently supported by QUnit). const sourceCode = context.getSourceCode(), + /** @type {Array<{assertVar: string | null}>} */ testStack = []; function getCurrentAssertContextVariable() { @@ -43,6 +44,10 @@ module.exports = { return testStack[testStack.length - 1].assertVar; } + /** + * @param {import('estree').Node} calleeNode + * @return {boolean} + */ function isGlobalPropEqual(calleeNode) { return ( calleeNode && @@ -51,6 +56,10 @@ module.exports = { ); } + /** + * @param {import('estree').Node} calleeNode + * @return {boolean} + */ function isAssertPropEqual(calleeNode) { return ( calleeNode && @@ -62,12 +71,20 @@ module.exports = { ); } + /** + * @param {import('estree').Node} calleeNode + * @return {boolean} + */ function isPropEqual(calleeNode) { return ( isAssertPropEqual(calleeNode) || isGlobalPropEqual(calleeNode) ); } + /** + * @param {import('estree').Node|{type: 'JSXElement'}} argNode + * @return {boolean} + */ function isValidExpectedValue(argNode) { switch (argNode.type) { case "Literal": @@ -83,8 +100,13 @@ module.exports = { } } + /** + * @param {import('estree').Node} callExpressionNode + * @return {boolean} + */ function hasNonObjectExpectedValue(callExpressionNode) { return ( + callExpressionNode.type === "CallExpression" && callExpressionNode && callExpressionNode.arguments && callExpressionNode.arguments.length >= 2 && diff --git a/lib/rules/resolve-async.js b/lib/rules/resolve-async.js index 1211d37c..9880cc3b 100644 --- a/lib/rules/resolve-async.js +++ b/lib/rules/resolve-async.js @@ -10,6 +10,12 @@ const utils = require("../utils"); +/** @typedef {{ + * stopSemaphoreCount: number, + * asyncCallbackVars: Record, + * assertContextVar: string | null, + * }} AsyncState */ + /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -33,11 +39,19 @@ module.exports = { * Declare a stack in case of nested test cases (not currently supported * in QUnit). */ + /** @type {Array} */ const asyncStateStack = []; + /** + * @param {import('estree').Node} callExpressionNode + * @returns {boolean} + */ function isAsyncCallExpression(callExpressionNode) { const asyncState = asyncStateStack[asyncStateStack.length - 1]; const assertContextVar = asyncState && asyncState.assertContextVar; + if (!assertContextVar) { + return false; + } return utils.isAsyncCallExpression( callExpressionNode, @@ -45,6 +59,10 @@ module.exports = { ); } + /** + * @param {import('estree').Node} calleeNode + * @returns {string | null} + */ function getAsyncCallbackVarOrNull(calleeNode) { const asyncState = asyncStateStack[asyncStateStack.length - 1]; let result = null; @@ -59,6 +77,11 @@ module.exports = { const isCallOrApply = calleeNode.property.type === "Identifier" && ["call", "apply"].includes(calleeNode.property.name); + + if (calleeNode.object.type !== "Identifier") { + return null; + } + const isCallbackVar = calleeNode.object.name in asyncState.asyncCallbackVars; @@ -71,6 +94,9 @@ module.exports = { return result; } + /** + * @param {number} amount + */ function incrementSemaphoreCount(amount) { const asyncState = asyncStateStack[asyncStateStack.length - 1]; if (asyncState) { @@ -78,15 +104,21 @@ module.exports = { } } + /** + * @param {import('estree').Node} lhsNode + */ function addAsyncCallbackVar(lhsNode) { const asyncState = asyncStateStack[asyncStateStack.length - 1]; /* istanbul ignore else: will correctly do nothing */ - if (asyncState) { + if (asyncState && lhsNode.type === "Identifier") { asyncState.asyncCallbackVars[lhsNode.name] = false; } } + /** + * @param {string} name + */ function markAsyncCallbackVarCalled(name) { const asyncState = asyncStateStack[asyncStateStack.length - 1]; @@ -96,6 +128,10 @@ module.exports = { } } + /** + * @param {AsyncState} asyncState + * @param {import('eslint').Rule.Node} node + */ function verifyAsyncState(asyncState, node) { if (asyncState.stopSemaphoreCount > 0) { const singular = asyncState.stopSemaphoreCount === 1; @@ -104,7 +140,7 @@ module.exports = { node: node, messageId: "needMoreStartCalls", data: { - semaphore: asyncState.stopSemaphoreCount, + semaphore: asyncState.stopSemaphoreCount.toString(), callOrCalls: singular ? "call" : "calls", }, }); @@ -123,6 +159,10 @@ module.exports = { } } + /** + * @param {import('eslint').Rule.Node} propertyNode + * @returns {boolean} + */ function isInModule(propertyNode) { return ( propertyNode && @@ -163,6 +203,9 @@ module.exports = { "CallExpression:exit": function (node) { if (utils.isTest(node.callee)) { const asyncState = asyncStateStack.pop(); + if (!asyncState) { + return; + } verifyAsyncState(asyncState, node); } }, @@ -172,13 +215,12 @@ module.exports = { utils.isModuleHookPropertyKey(node.key) && isInModule(node) ) { - const assertContextVar = utils.getAssertContextName( - node.value, - ); asyncStateStack.push({ stopSemaphoreCount: 0, asyncCallbackVars: {}, - assertContextVar: assertContextVar, + assertContextVar: utils.getAssertContextName( + node.value, + ), }); } }, @@ -189,18 +231,27 @@ module.exports = { isInModule(node) ) { const asyncState = asyncStateStack.pop(); + if (!asyncState) { + return; + } verifyAsyncState(asyncState, node); } }, AssignmentExpression: function (node) { if (isAsyncCallExpression(node.right)) { + if (node.left.type !== "Identifier") { + return; + } addAsyncCallbackVar(node.left); } }, VariableDeclarator: function (node) { - if (isAsyncCallExpression(node.init)) { + if (node.init && isAsyncCallExpression(node.init)) { + if (node.id.type !== "Identifier") { + return; + } addAsyncCallbackVar(node.id); } }, diff --git a/lib/utils.js b/lib/utils.js index b462b533..bbdc828d 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -74,29 +74,48 @@ function getAssertionNames() { exports.getAssertionNames = getAssertionNames; +/** + * @param {import('estree').Node} calleeNode + * @param {string} assertVar + * @returns {{allowedArities: number[], compareActualFirst?: boolean} | null} + */ function getAssertionMetadata(calleeNode, assertVar) { if (calleeNode.type === "MemberExpression") { - return ( + if ( calleeNode.object && calleeNode.object.type === "Identifier" && calleeNode.object.name === assertVar && calleeNode.property && + calleeNode.property.type === "Identifier" && Object.hasOwnProperty.call( ASSERTION_METADATA, calleeNode.property.name, - ) && - ASSERTION_METADATA[calleeNode.property.name] - ); - } else if (calleeNode.type === "Identifier") { - return ( - Object.hasOwnProperty.call(ASSERTION_METADATA, calleeNode.name) && - ASSERTION_METADATA[calleeNode.name] + ) + ) { + const assertionName = + /** @type {keyof typeof ASSERTION_METADATA} */ ( + calleeNode.property.name + ); + return ASSERTION_METADATA[assertionName]; + } + } else if ( + calleeNode.type === "Identifier" && + Object.hasOwnProperty.call(ASSERTION_METADATA, calleeNode.name) + ) { + const assertionName = /** @type {keyof typeof ASSERTION_METADATA} */ ( + calleeNode.name ); + return ASSERTION_METADATA[assertionName]; } return null; } +/** + * @param {import('estree').Node} callExpressionNode + * @param {string} assertVar + * @returns {boolean} + */ exports.isAsyncCallExpression = function (callExpressionNode, assertVar) { if (!assertVar) { assertVar = "assert"; @@ -113,6 +132,10 @@ exports.isAsyncCallExpression = function (callExpressionNode, assertVar) { ); }; +/** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ exports.isStop = function (calleeNode) { let result = false; @@ -130,6 +153,10 @@ exports.isStop = function (calleeNode) { return result; }; +/** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ exports.isStart = function (calleeNode) { let result = false; @@ -147,6 +174,10 @@ exports.isStart = function (calleeNode) { return result; }; +/** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ exports.isTest = function (calleeNode) { let result = false; @@ -164,6 +195,10 @@ exports.isTest = function (calleeNode) { return result; }; +/** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ exports.isModule = function (calleeNode) { let result = false; @@ -181,6 +216,10 @@ exports.isModule = function (calleeNode) { return result; }; +/** + * @param {import('estree').Node} identifierNode + * @returns {boolean} + */ exports.isModuleHookPropertyKey = function (identifierNode) { return ( identifierNode && @@ -189,6 +228,10 @@ exports.isModuleHookPropertyKey = function (identifierNode) { ); }; +/** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ exports.isAsyncTest = function (calleeNode) { let result = false; @@ -206,6 +249,11 @@ exports.isAsyncTest = function (calleeNode) { return result; }; +/** + * @param {import('estree').Node} calleeNode + * @param {string} qunitMethod + * @returns {boolean} + */ function isQUnitMethod(calleeNode, qunitMethod) { let result = false; @@ -235,14 +283,26 @@ function isQUnitMethod(calleeNode, qunitMethod) { return result; } +/** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ exports.isOnly = function (calleeNode) { return isQUnitMethod(calleeNode, "only"); }; +/** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ exports.isSkip = function (calleeNode) { return isQUnitMethod(calleeNode, "skip"); }; +/** + * @param {import('estree').Node[]} argumentsNodes + * @returns {string | null} + */ exports.getAssertContextNameForTest = function (argumentsNodes) { const functionExpr = argumentsNodes.find(function (argNode) { return ( @@ -250,20 +310,32 @@ exports.getAssertContextNameForTest = function (argumentsNodes) { argNode.type === "ArrowFunctionExpression" ); }); + if (!functionExpr) { + return null; + } - return this.getAssertContextName(functionExpr); + return exports.getAssertContextName(functionExpr); }; +/** + * @param {import('estree').Node} functionExpr + * @returns {string | null} + */ exports.getAssertContextName = function (functionExpr) { - let result; + let result = null; - if (functionExpr && functionExpr.params) { + if ( + functionExpr && + (functionExpr.type === "FunctionExpression" || + functionExpr.type === "ArrowFunctionExpression") && + functionExpr.params + ) { // In TypeScript, `this` can be passed as the first function parameter to add a type to it, // and we want to ignore that parameter since we're looking for the `hooks` variable. const hooksParam = functionExpr.params.find( (p) => p.type === "Identifier" && p.name !== "this", ); - if (hooksParam) { + if (hooksParam && hooksParam.type === "Identifier") { result = hooksParam.name; } } @@ -271,37 +343,72 @@ exports.getAssertContextName = function (functionExpr) { return result; }; +/** + * @param {import('estree').Node} calleeNode + * @param {string} assertVar + * @returns {boolean} + */ exports.isAssertion = function (calleeNode, assertVar) { return !!getAssertionMetadata(calleeNode, assertVar); }; +/** + * @param {import('estree').Node} calleeNode + * @param {string} assertVar + * @returns {number[]} + */ exports.getAllowedArities = function (calleeNode, assertVar) { const assertionMetadata = getAssertionMetadata(calleeNode, assertVar); + if (!assertionMetadata) { + return []; + } - return ( - (assertionMetadata && assertionMetadata.allowedArities) || - /* istanbul ignore next */ [] - ); + return assertionMetadata.allowedArities; }; +/** + * @param {import('estree').Node} calleeNode + * @param {string} assertVar + * @returns {boolean} + */ exports.isComparativeAssertion = function (calleeNode, assertVar) { const assertionMetadata = getAssertionMetadata(calleeNode, assertVar); return Object.hasOwnProperty.call(assertionMetadata, "compareActualFirst"); }; +/** + * @param {import('estree').Node} calleeNode + * @param {string} assertVar + * @returns {boolean} + */ exports.shouldCompareActualFirst = function (calleeNode, assertVar) { const assertionMetadata = getAssertionMetadata(calleeNode, assertVar); + if (!assertionMetadata) { + return false; + } - return assertionMetadata && assertionMetadata.compareActualFirst; + return !!assertionMetadata.compareActualFirst; }; +/** + * @param {string[]} assertions + * @param {Record} errorMessageConfig + */ exports.createAssertionCheck = function (assertions, errorMessageConfig) { + /** + * @param {import('eslint').Rule.RuleContext} context + */ return function (context) { // Declare a test stack in case of nested test cases (not currently // supported by QUnit). + /** @type {Array<{assertVar: string | null}>} */ const testStack = []; + /** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isGlobalAssertion(calleeNode) { return ( calleeNode && @@ -310,12 +417,19 @@ exports.createAssertionCheck = function (assertions, errorMessageConfig) { ); } + /** + * @returns {string | null} + */ function getCurrentAssertContextVariable() { assert(testStack.length, "Test stack should not be empty"); return testStack[testStack.length - 1].assertVar; } + /** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isMethodCalledOnLocalAssertObject(calleeNode) { return ( calleeNode && @@ -327,6 +441,10 @@ exports.createAssertionCheck = function (assertions, errorMessageConfig) { ); } + /** + * @param {import('estree').Node} calleeNode + * @returns {boolean} + */ function isExpectedAssertion(calleeNode) { return ( isGlobalAssertion(calleeNode) || @@ -334,17 +452,36 @@ exports.createAssertionCheck = function (assertions, errorMessageConfig) { ); } + /** + * @param {import('estree').Node} node + */ + // eslint-disable-next-line complexity function reportError(node) { + if (node.type !== "CallExpression") { + return; + } const assertVar = getCurrentAssertContextVariable(); const isGlobal = isGlobalAssertion(node.callee); + // eslint-disable-next-line no-nested-ternary const assertion = isGlobal - ? node.callee.name - : node.callee.property.name; + ? // eslint-disable-next-line unicorn/no-nested-ternary + node.callee.type === "Identifier" + ? node.callee.name + : null + : // eslint-disable-next-line unicorn/no-nested-ternary + node.callee.type === "MemberExpression" && + node.callee.property.type === "Identifier" + ? node.callee.property.name + : null; + if (!assertion) { + return; + } + /** @type {{node: import('estree').CallExpression, data: Record, messageId?: string, message?: string}} */ const reportErrorObject = { node, data: { - assertVar, + assertVar: assertVar ?? "", assertion, }, }; @@ -363,33 +500,57 @@ exports.createAssertionCheck = function (assertions, errorMessageConfig) { : errorMessageConfigForAssertion.unexpectedLocalAssertionMessage; } - context.report(reportErrorObject); + if (reportErrorObject.messageId) { + context.report({ + node: reportErrorObject.node, + messageId: reportErrorObject.messageId, + data: reportErrorObject.data, + }); + } else if (reportErrorObject.message) { + context.report({ + node: reportErrorObject.node, + message: reportErrorObject.message, + data: reportErrorObject.data, + }); + } else { + assert(false, "No messageId or message found"); + } } return { + /** + * @param {import('estree').Node} node + */ CallExpression: function (node) { /* istanbul ignore else: correctly does nothing */ if ( - exports.isTest(node.callee) || - exports.isAsyncTest(node.callee) + node.type === "CallExpression" && + (exports.isTest(node.callee) || + exports.isAsyncTest(node.callee)) ) { + const assertVar = exports.getAssertContextNameForTest( + node.arguments, + ); testStack.push({ - assertVar: exports.getAssertContextNameForTest( - node.arguments, - ), + assertVar, }); } else if ( testStack.length > 0 && + node.type === "CallExpression" && isExpectedAssertion(node.callee) ) { reportError(node); } }, + /** + * @param {import('estree').Node} node + */ "CallExpression:exit": function (node) { /* istanbul ignore else: correctly does nothing */ if ( - exports.isTest(node.callee) || - exports.isAsyncTest(node.callee) + node.type === "CallExpression" && + (exports.isTest(node.callee) || + exports.isAsyncTest(node.callee)) ) { testStack.pop(); } diff --git a/package-lock.json b/package-lock.json index a08a152d..910fe19c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,16 @@ "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", "@eslint/js": "^9.29.0", "@release-it/conventional-changelog": "^8.0.1", - "@typescript-eslint/parser": "^8.34.1", + "@types/chai": "^5.2.2", + "@types/eslint": "^8.0.0", + "@types/eslint-plugin-markdown": "^2.0.2", + "@types/eslint-plugin-mocha": "^10.4.0", + "@types/eslint-utils": "^3.0.5", + "@types/estree": "^1.0.8", + "@types/mocha": "^10.0.10", + "@types/node": "^24.0.3", + "@types/requireindex": "^1.2.4", + "@typescript-eslint/parser": "^8.35.0", "all-contributors-cli": "^6.26.1", "chai": "^4.3.10", "coveralls": "^3.1.1", @@ -42,7 +51,7 @@ "prettier": "^3.1.0", "release-it": "^17.1.1", "semver": "^7.5.4", - "typescript": "^5.2.2" + "typescript": "^5.8.3" }, "engines": { "node": "^16.0.0 || ^18.0.0 || >=20.0.0" @@ -1253,6 +1262,16 @@ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "dev": true }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1263,6 +1282,56 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "8.56.12", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", + "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-plugin-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/eslint-plugin-markdown/-/eslint-plugin-markdown-2.0.2.tgz", + "integrity": "sha512-ImmEw5xBVb9vCaFfQ+5kUcVatUO4XPpTvryAmhpKzalUKhDb3EZmeuHvIUO6E1/WDOTw+/b9qlWsZhxULhZdfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/unist": "*" + } + }, + "node_modules/@types/eslint-plugin-mocha": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@types/eslint-plugin-mocha/-/eslint-plugin-mocha-10.4.0.tgz", + "integrity": "sha512-knOH7Y13tuK0gqQqiiSucMxF4R/LrbRJwCiDQEhfbktUGvf9xFBQ8TdpCUO1xK0EwafOssu7+KuxqFV1xOXOtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*" + } + }, + "node_modules/@types/eslint-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/eslint-utils/-/eslint-utils-3.0.5.tgz", + "integrity": "sha512-dGOLJqHXpjomkPgZiC7vnVSJtFIOM1Y6L01EyUhzPuD0y0wfIGiqxiGs3buUBfzxLIQHrCvZsIMDaCZ8R5IIoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1297,6 +1366,13 @@ "@types/unist": "*" } }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -1304,12 +1380,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.0.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz", + "integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, + "node_modules/@types/requireindex": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/requireindex/-/requireindex-1.2.4.tgz", + "integrity": "sha512-9NwqEWtA606+W8sSNMAzmyTzgHOKBIDKtsHl16ctbtoJdkyps/zIGefBJmfHYkKLgAH8Ptfy0TJIlXVn+JM2Lw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", @@ -12031,6 +12124,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, "node_modules/unicorn-magic": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", diff --git a/package.json b/package.json index f127a024..22592493 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,16 @@ ".": "./index.js", "./configs/*": "./lib/configs/*.js" }, - "main": "./index.js", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "scripts": { + "build": "tsc --project tsconfig.build.json", "lint": "npm-run-all --continue-on-error --aggregate-output --parallel lint:*", "lint:docs": "markdownlint \"**/*.md\"", "lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"", "lint:js": "eslint --cache --report-unused-disable-directives .", - "lint:remote": "eslint-remote-tester", + "lint:remote": "eslint-remote-tester --config eslint-remote-tester.config.mjs", + "lint:types": "tsc --noEmit", "preversion": "npm test", "report-coverage-html": "nyc report --reporter=html --report-dir build/coverage", "release": "release-it", @@ -21,8 +24,7 @@ "update:eslint-docs": "eslint-doc-generator --url-configs \"https://github.com/platinumazure/eslint-plugin-qunit/blob/main/README.md#configurations\"" }, "files": [ - "index.js", - "lib/" + "dist/" ], "dependencies": { "eslint-utils": "^3.0.0", @@ -32,7 +34,16 @@ "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", "@eslint/js": "^9.29.0", "@release-it/conventional-changelog": "^8.0.1", - "@typescript-eslint/parser": "^8.34.1", + "@types/chai": "^5.2.2", + "@types/eslint": "^8.0.0", + "@types/eslint-plugin-markdown": "^2.0.2", + "@types/eslint-plugin-mocha": "^10.4.0", + "@types/eslint-utils": "^3.0.5", + "@types/estree": "^1.0.8", + "@types/mocha": "^10.0.10", + "@types/node": "^24.0.3", + "@types/requireindex": "^1.2.4", + "@typescript-eslint/parser": "^8.35.0", "all-contributors-cli": "^6.26.1", "chai": "^4.3.10", "coveralls": "^3.1.1", @@ -58,7 +69,7 @@ "prettier": "^3.1.0", "release-it": "^17.1.1", "semver": "^7.5.4", - "typescript": "^5.2.2" + "typescript": "^5.8.3" }, "peerDepencencies": { "eslint": ">=8.38.0" @@ -74,10 +85,10 @@ ], "nyc": { "check-coverage": true, - "lines": 100, - "statements": 100, + "lines": 92, + "statements": 92, "functions": 100, - "branches": 100, + "branches": 90, "exclude": [ "build/**", "eslint-remote-tester.config.js", diff --git a/tests/index.js b/tests/index.js index d2654930..a5883e37 100644 --- a/tests/index.js +++ b/tests/index.js @@ -13,7 +13,8 @@ const assert = require("chai").assert, fs = require("node:fs"), path = require("node:path"), requireIndex = require("requireindex"), - plugin = require("../index.js"); + plugin = require("../index.js"), + recommendedFlatConfig = require("../lib/configs/recommended.js"); //------------------------------------------------------------------------------ // Tests @@ -87,19 +88,39 @@ describe("index.js", function () { }); describe("flat", function () { - // eslint-disable-next-line mocha/no-setup-in-describe -- rule doesn't like function calls like `Object.entries()` - for (const [configName, config] of Object.entries( + describe("load all flat configs via requireIndex", function () { // eslint-disable-next-line mocha/no-setup-in-describe -- rule doesn't like function calls like `Object.entries()` - requireIndex(`${__dirname}/../lib/configs`), - )) { - describe(configName, function () { - it("has the right plugins", function () { - assert.deepStrictEqual(config.plugins, { - qunit: plugin, + for (const [configName, config] of Object.entries( + // eslint-disable-next-line mocha/no-setup-in-describe -- rule doesn't like function calls like `Object.entries()` + requireIndex(`${__dirname}/../lib/configs`), + )) { + describe(configName, function () { + it("has the right plugins", function () { + assert.deepStrictEqual(config.plugins, { + qunit: plugin, + }); }); }); + } + }); + + describe("load via import to ensure the types work", function () { + describe("recommended", function () { + it("has the right plugins", function () { + assert.deepStrictEqual( + recommendedFlatConfig.plugins.qunit, + plugin, + ); + }); + + it("has the right rules", function () { + assert.deepStrictEqual( + recommendedFlatConfig.rules, + plugin.configs.recommended.rules, + ); + }); }); - } + }); }); }); }); diff --git a/tests/lib/rules/no-arrow-tests.js b/tests/lib/rules/no-arrow-tests.js index ba1533a4..6ee28a52 100644 --- a/tests/lib/rules/no-arrow-tests.js +++ b/tests/lib/rules/no-arrow-tests.js @@ -12,7 +12,7 @@ const rule = require("../../../lib/rules/no-arrow-tests"), RuleTester = require("eslint").RuleTester, - outdent = require("outdent"); + { outdent } = require("outdent"); //------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-compare-relation-boolean.js b/tests/lib/rules/no-compare-relation-boolean.js index 84cd41bc..f60d1774 100644 --- a/tests/lib/rules/no-compare-relation-boolean.js +++ b/tests/lib/rules/no-compare-relation-boolean.js @@ -16,6 +16,10 @@ const rule = require("../../../lib/rules/no-compare-relation-boolean"), // Helper Functions //------------------------------------------------------------------------------ +/** + * @param {{code: string, output:string}} testCase + * @returns {{code: string, errors: {messageId: string, type: string}[]}} + */ function addErrors(testCase) { return Object.assign( { diff --git a/tests/lib/rules/no-conditional-assertions.js b/tests/lib/rules/no-conditional-assertions.js index 6fd8ccdf..ea23c190 100644 --- a/tests/lib/rules/no-conditional-assertions.js +++ b/tests/lib/rules/no-conditional-assertions.js @@ -16,6 +16,10 @@ const rule = require("../../../lib/rules/no-conditional-assertions"), // Helpers //------------------------------------------------------------------------------ +/** + * @param {string} code + * @returns {{code: string, errors: {messageId: string, type: string}[]}} + */ function wrapInInvalidTestObject(code) { return { code: code, diff --git a/tests/lib/rules/no-global-assertions.js b/tests/lib/rules/no-global-assertions.js index cb92f65b..d04bc02a 100644 --- a/tests/lib/rules/no-global-assertions.js +++ b/tests/lib/rules/no-global-assertions.js @@ -16,6 +16,10 @@ const rule = require("../../../lib/rules/no-global-assertions"), // Helpers //------------------------------------------------------------------------------ +/** + * @param {string} assertion + * @returns {{messageId: string, data: Record}} + */ function createError(assertion) { return { messageId: "unexpectedGlobalAssertion", diff --git a/tests/lib/rules/no-hooks-from-ancestor-modules.js b/tests/lib/rules/no-hooks-from-ancestor-modules.js index 284a4ad9..7dcda520 100644 --- a/tests/lib/rules/no-hooks-from-ancestor-modules.js +++ b/tests/lib/rules/no-hooks-from-ancestor-modules.js @@ -15,6 +15,10 @@ const rule = require("../../../lib/rules/no-hooks-from-ancestor-modules"), // Helpers //------------------------------------------------------------------------------ +/** + * @param {{ invokedMethodName: string, usedHooksIdentifierName: string }} params + * @returns {{messageId: string, data: Record, type: string}} + */ function createError({ invokedMethodName, usedHooksIdentifierName }) { return { messageId: "noHooksFromAncestorModules", diff --git a/tests/lib/rules/no-identical-names.js b/tests/lib/rules/no-identical-names.js index fc638089..366ce2e9 100644 --- a/tests/lib/rules/no-identical-names.js +++ b/tests/lib/rules/no-identical-names.js @@ -6,7 +6,7 @@ const rule = require("../../../lib/rules/no-identical-names"), RuleTester = require("eslint").RuleTester, - outdent = require("outdent"); + { outdent } = require("outdent"); //------------------------------------------------------------------------------ // Tests @@ -14,7 +14,7 @@ const rule = require("../../../lib/rules/no-identical-names"), const ruleTester = new RuleTester(); -ruleTester.run("no-identical-title", rule, { +ruleTester.run("no-identical-names", rule, { valid: [ outdent` module("module"); diff --git a/tests/lib/rules/no-negated-ok.js b/tests/lib/rules/no-negated-ok.js index 64e0483c..5a9f1d3a 100644 --- a/tests/lib/rules/no-negated-ok.js +++ b/tests/lib/rules/no-negated-ok.js @@ -16,6 +16,10 @@ const rule = require("../../../lib/rules/no-negated-ok"), // Helper functions //------------------------------------------------------------------------------ +/** + * @param {string} callee + * @returns {{messageId: string, data: Record}} + */ function createError(callee) { return { messageId: "noNegationInOk", diff --git a/tests/lib/rules/no-ok-equality.js b/tests/lib/rules/no-ok-equality.js index 03f81598..6f578404 100644 --- a/tests/lib/rules/no-ok-equality.js +++ b/tests/lib/rules/no-ok-equality.js @@ -15,6 +15,13 @@ const rule = require("../../../lib/rules/no-ok-equality"), // Helpers //------------------------------------------------------------------------------ +/** + * @param {string} assertion + * @param {string} suggestion + * @param {string} a + * @param {string} b + * @returns {{messageId: string, data: Record}} + */ function createError(assertion, suggestion, a, b) { return { messageId: "noEqualityCheckInOk", diff --git a/tests/lib/rules/no-qunit-start-in-tests.js b/tests/lib/rules/no-qunit-start-in-tests.js index e2871e4d..1cf24d5b 100644 --- a/tests/lib/rules/no-qunit-start-in-tests.js +++ b/tests/lib/rules/no-qunit-start-in-tests.js @@ -15,6 +15,10 @@ const rule = require("../../../lib/rules/no-qunit-start-in-tests"), // Helpers //------------------------------------------------------------------------------ +/** + * @param {string} context + * @returns {{messageId: string, data: Record}} + */ function createError(context) { return { messageId: "noQUnitStartInTests", diff --git a/tests/lib/rules/require-expect.js b/tests/lib/rules/require-expect.js index 408637ff..7009f23e 100644 --- a/tests/lib/rules/require-expect.js +++ b/tests/lib/rules/require-expect.js @@ -19,6 +19,10 @@ const rule = require("../../../lib/rules/require-expect"), const ruleTester = new RuleTester(), returnAndIndent = "\n "; +/** + * @param {string} expectCallName + * @returns {{messageId: string, data: Record}} + */ function alwaysErrorMessage(expectCallName) { return { messageId: "expectRequired", @@ -28,6 +32,10 @@ function alwaysErrorMessage(expectCallName) { }; } +/** + * @param {string} expectCallName + * @returns {{messageId: string, data: Record}} + */ function exceptSimpleErrorMessage(expectCallName) { return { messageId: "expectRequiredComplexTest", @@ -37,6 +45,10 @@ function exceptSimpleErrorMessage(expectCallName) { }; } +/** + * @param {string} expectCallName + * @returns {{messageId: string, data: Record}} + */ function neverErrorMessage(expectCallName) { return { messageId: "expectForbidden", diff --git a/tests/lib/rules/require-object-in-propequal.js b/tests/lib/rules/require-object-in-propequal.js index e417cf61..d0a15023 100644 --- a/tests/lib/rules/require-object-in-propequal.js +++ b/tests/lib/rules/require-object-in-propequal.js @@ -16,6 +16,11 @@ const rule = require("../../../lib/rules/require-object-in-propequal"), // Helpers //------------------------------------------------------------------------------ +/** + * @param {string} assertionCode + * @param {string} invalidValue + * @returns {{code: string, errors: {messageId: string, data: Record}[]}} + */ function createInvalid(assertionCode, invalidValue) { return { code: testUtils.wrapInTest(assertionCode), diff --git a/tests/lib/rules/resolve-async.js b/tests/lib/rules/resolve-async.js index 2ed3fee4..0768fdbe 100644 --- a/tests/lib/rules/resolve-async.js +++ b/tests/lib/rules/resolve-async.js @@ -15,6 +15,11 @@ const rule = require("../../../lib/rules/resolve-async"), // Helpers //------------------------------------------------------------------------------ +/** + * @param {string} nodeType + * @param {number} numberOfCalls + * @returns {{messageId: string, data: {[]: string}, type: string}} + */ function createNeedStartCallsMessage(nodeType, numberOfCalls = 1) { const semaphore = numberOfCalls; const callOrCalls = semaphore === 1 ? "call" : "calls"; @@ -29,6 +34,11 @@ function createNeedStartCallsMessage(nodeType, numberOfCalls = 1) { }; } +/** + * @param {string} nodeType + * @param {string=} callbackVar + * @returns {{messageId: string, data: {[]: string}, type: string}} + */ function createAsyncCallbackNotCalledMessage(nodeType, callbackVar) { return { messageId: "asyncCallbackNotCalled", diff --git a/tests/testUtils.js b/tests/testUtils.js index 18fa84b2..ad801476 100644 --- a/tests/testUtils.js +++ b/tests/testUtils.js @@ -4,10 +4,18 @@ */ "use strict"; +/** + * @param {string} assertionCode + * @returns {string} + */ exports.wrapInTest = function (assertionCode) { return `QUnit.test('test', function (assert) { ${assertionCode} });`; }; +/** + * @param {string} assertionCode + * @returns {string} + */ exports.wrapInArrowTest = function (assertionCode) { return `QUnit.test('test', (assert) => { ${assertionCode} });`; }; diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..d4f5d50a --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "tests/**", + "eslint.config.js" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..89d25a2e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,119 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2024", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": ["ES2024"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "libReplacement": true, /* Enable lib replacement. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "nodenext", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": ["node","mocha"], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist/", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + // "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": [ + "*.js", + "tests/**/*", + "lib/**/*" + ] +}