Skip to content

Commit

Permalink
New: no-invalid-names rule
Browse files Browse the repository at this point in the history
  • Loading branch information
platinumazure committed Jan 3, 2023
1 parent 0f88ebe commit a991f9d
Show file tree
Hide file tree
Showing 4 changed files with 440 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -49,6 +49,7 @@ For more details on how to extend your configuration from a plugin configuration
| [no-hooks-from-ancestor-modules](docs/rules/no-hooks-from-ancestor-modules.md) | disallow the use of hooks from ancestor modules || | |
| [no-identical-names](docs/rules/no-identical-names.md) | disallow identical test and module names || | |
| [no-init](docs/rules/no-init.md) | disallow use of QUnit.init || | |
| [no-invalid-names](docs/rules/no-invalid-names.md) | disallow invalid and missing test names | | 🔧 | |
| [no-jsdump](docs/rules/no-jsdump.md) | disallow use of QUnit.jsDump || | |
| [no-loose-assertions](docs/rules/no-loose-assertions.md) | disallow the use of assert.equal/assert.ok/assert.notEqual/assert.notOk | | | |
| [no-negated-ok](docs/rules/no-negated-ok.md) | disallow negation in assert.ok/assert.notOk || 🔧 | |
Expand Down
65 changes: 65 additions & 0 deletions docs/rules/no-invalid-names.md
@@ -0,0 +1,65 @@
# Disallow invalid and missing test names (`qunit/no-invalid-names`)

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

QUnit tests can be difficult to debug without useful module and test names. The purpose
of this rule is to ensure that module and test names are present and valid.

## Rule Details

The following patterns are considered warnings:

```js
// Missing names
module(function () {});
test(function () {});

// Empty or space-only names
module("", function () {});
test("", function () {});
module(" ", function () {});
test(" ", function () {});

// Leading and trailing spaces
module(' Foo Bar unit ', function () {});
test(' it does foo ', function () {});

// Non-string names
module(["foo"], function () {});
test(["foo"], function () {});
module(1, function () {});
test(1, function () {});

// Names starting or ending with QUnit delimiters (>, :)
module('>Foo Bar unit', function () {});
test('>it does foo', function () {});
module('Foo Bar unit>', function () {});
test('it does foo>', function () {});
module(':Foo Bar unit', function () {});
test(':it does foo', function () {});
module('Foo Bar unit:', function () {});
test('it does foo:', function () {});
```

The following patterns are not considered warnings:

```js
// Valid strings
module("Foo Bar", function () {});
test("Foo Bar", function () {});

// Templates are okay since those are strings
module(`Foo Bar ${foo}`, function () {});
test(`Foo Bar ${foo}`, function () {});

// Can't check variables
module(foo, function () {});
test(foo, function () {});
```

## When Not to Use It

This rule is mostly stylistic, but can cause problems in the case of QUnit delimiters
at the start and end of test names.
142 changes: 142 additions & 0 deletions lib/rules/no-invalid-names.js
@@ -0,0 +1,142 @@
/**
* @fileoverview Disallow invalid and missing test names.
* @author Kevin Partington
*/
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const utils = require("../utils");

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow invalid and missing test names",
category: "Best Practices",
url: "https://github.com/platinumazure/eslint-plugin-qunit/blob/master/docs/rules/no-invalid-names.md"
},
fixable: "code",
messages: {
moduleNameEmpty: "Module name is empty.",
moduleNameInvalidType: "Module name \"{{ name }}\" is invalid type: {{ type }}.",
moduleNameMissing: "Module name is missing.",
moduleNameOuterQUnitDelimiters: "Module name \"{{ name }}\" has leading and/or trailing QUnit delimiter: (> or :).",
moduleNameOuterSpaces: "Module name has leading and/or trailing spaces.",
testNameEmpty: "Test name is empty.",
testNameInvalidType: "Test name \"{{ name }}\" is invalid type: {{ type }}.",
testNameMissing: "Test name is missing.",
testNameOuterQUnitDelimiters: "Test name \"{{ name }}\" has leading and/or trailing QUnit delimiter (> or :).",
testNameOuterSpaces: "Test name has leading and/or trailing spaces."
},
schema: []
},

create: function (context) {
const sourceCode = context.getSourceCode();

const FUNCTION_TYPES = new Set(["FunctionExpression", "ArrowFunctionExpression"]);
const INVALID_NAME_AST_TYPES = new Set([
"ArrayExpression",
"ObjectExpression",
"ThisExpression",
"UnaryExpression",
"UpdateExpression",
"BinaryExpression",
"AssignmentExpression",
"LogicalExpression"
]);
const QUNIT_NAME_DELIMITERS = [">", ":"];

/**
* Check name for starting or ending with QUnit delimiters.
* @param {string} name The test or module name to check.
* @returns {boolean} True if the name starts or ends with a QUnit name delimiter, false otherwise.
*/
function nameHasOuterQUnitDelimiters(name) {
return QUNIT_NAME_DELIMITERS.some(delimiter =>
name.startsWith(delimiter) || name.endsWith(delimiter)
);
}

/**
* Check the name argument of a module or test CallExpression.
* @param {ASTNode} firstArg The first argument of the test/module call.
* @param {"test"|"module"} objectType Whether this is a test or module call.
* @param {ASTNode} calleeForMissingName The callee, used as report location if the test/module name is missing.
* @returns {void}
*/
function checkNameArgument(firstArg, objectType, calleeForMissingName) {
if (!firstArg || FUNCTION_TYPES.has(firstArg.type)) {
context.report({
node: calleeForMissingName,
messageId: `${objectType}NameMissing`
});
} else if (INVALID_NAME_AST_TYPES.has(firstArg.type)) {
context.report({
node: firstArg,
messageId: `${objectType}NameInvalidType`,
data: {
type: firstArg.type,
name: sourceCode.getText(firstArg)
}
});
} else if (firstArg.type === "Literal") {
if (typeof firstArg.value !== "string") {
context.report({
node: firstArg,
messageId: `${objectType}NameInvalidType`,
data: {
type: typeof firstArg.value,
name: sourceCode.getText(firstArg)
}
});
} else if (firstArg.value.trim().length === 0) {
context.report({
node: firstArg,
messageId: `${objectType}NameEmpty`
});
} else if (firstArg.value.trim() !== firstArg.value) {
const trimmedValue = firstArg.value.trim();

const raw = firstArg.raw;
const startDelimiter = raw[0];
const endDelimiter = raw[raw.length - 1];

context.report({
node: firstArg,
messageId: `${objectType}NameOuterSpaces`,
fix: fixer => fixer.replaceText(
firstArg,
`${startDelimiter}${trimmedValue}${endDelimiter}`
)
});
} else if (nameHasOuterQUnitDelimiters(firstArg.value)) {
context.report({
node: firstArg,
messageId: `${objectType}NameOuterQUnitDelimiters`,
data: { name: firstArg.value }
});
}
}
}

return {
"CallExpression": function (node) {
/* istanbul ignore else: Correctly does nothing */
if (utils.isTest(node.callee)) {
checkNameArgument(node.arguments[0], "test", node.callee);
} else if (utils.isModule(node.callee)) {
checkNameArgument(node.arguments[0], "module", node.callee);
}
}
};
}
};

0 comments on commit a991f9d

Please sign in to comment.