Skip to content

Commit

Permalink
Merge pull request #365 from lo1tuma/rework
Browse files Browse the repository at this point in the history
Implement new resolve algorithm
  • Loading branch information
lo1tuma authored Jan 23, 2025
2 parents 4168eca + fad8b0e commit 4a02c4c
Show file tree
Hide file tree
Showing 77 changed files with 3,340 additions and 1,870 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,20 @@ This plugin supports the following settings, which are used by multiple rules:
```json
{
"rules": {
"mocha/no-skipped-tests": "error",
"mocha/no-pending-tests": "error",
"mocha/no-exclusive-tests": "error"
},
"settings": {
"mocha/additionalCustomNames": [
{
"name": "describeModule",
"type": "suite",
"interfaces": ["BDD"]
"interface": "BDD"
},
{
"name": "testModule",
"type": "testCase",
"interfaces": ["TDD"]
"interface": "TDD"
}
]
}
Expand Down Expand Up @@ -87,6 +87,8 @@ This plugin supports the following settings, which are used by multiple rules:
.describeModule.modifier("example", function(n) { ... });
```

- `interface`: This allows to select either `TDD`, `BDD` (default) or `exports`. When using `exports` mocha variables are resolved from named `import` statements instead of global variables.

## Configs

### `recommended`
Expand Down Expand Up @@ -122,7 +124,7 @@ See [Configuring Eslint](http://eslint.org/docs/user-guide/configuring) on [esli
| [consistent-spacing-between-blocks](docs/rules/consistent-spacing-between-blocks.md) | Require consistent spacing between blocks || | | 🔧 |
| [handle-done-callback](docs/rules/handle-done-callback.md) | Enforces handling of callbacks for async tests || | | |
| [max-top-level-suites](docs/rules/max-top-level-suites.md) | Enforce the number of top-level suites in a single file || | | |
| [no-async-describe](docs/rules/no-async-describe.md) | Disallow async functions passed to describe || | | 🔧 |
| [no-async-describe](docs/rules/no-async-describe.md) | Disallow async functions passed to a suite || | | 🔧 |
| [no-empty-description](docs/rules/no-empty-description.md) | Disallow empty test descriptions || | | |
| [no-exclusive-tests](docs/rules/no-exclusive-tests.md) | Disallow exclusive tests | || | |
| [no-exports](docs/rules/no-exports.md) | Disallow exports from test files || | | |
Expand All @@ -136,8 +138,7 @@ See [Configuring Eslint](http://eslint.org/docs/user-guide/configuring) on [esli
| [no-return-and-callback](docs/rules/no-return-and-callback.md) | Disallow returning in a test or hook function that uses a callback || | | |
| [no-return-from-async](docs/rules/no-return-from-async.md) | Disallow returning from an async test or hook | | || |
| [no-setup-in-describe](docs/rules/no-setup-in-describe.md) | Disallow setup in describe blocks || | | |
| [no-sibling-hooks](docs/rules/no-sibling-hooks.md) | Disallow duplicate uses of a hook at the same level inside a describe || | | |
| [no-skipped-tests](docs/rules/no-skipped-tests.md) | Disallow skipped tests | || | |
| [no-sibling-hooks](docs/rules/no-sibling-hooks.md) | Disallow duplicate uses of a hook at the same level inside a suite || | | |
| [no-synchronous-tests](docs/rules/no-synchronous-tests.md) | Disallow synchronous tests | | || |
| [no-top-level-hooks](docs/rules/no-top-level-hooks.md) | Disallow top-level hooks | || | |
| [prefer-arrow-callback](docs/rules/prefer-arrow-callback.md) | Require using arrow functions for callbacks | | || 🔧 |
Expand Down
3 changes: 1 addition & 2 deletions cspell.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"Toru",
"Nagashima",
"rambda",
"Afanasyev",
"Melnikow"
"quuux"
]
}
4 changes: 2 additions & 2 deletions docs/rules/handle-done-callback.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ before(function (done) {

This rule supports the following options:

- `ignoreSkipped`: When set to `true` skipped test cases won’t be checked. Defaults to `false`.
- `ignorePending`: When set to `true` skipped test cases won’t be checked. Defaults to `false`.

```json
{
"rules": {
"mocha/handle-done-callback": ["error", { "ignoreSkipped": true }]
"mocha/handle-done-callback": ["error", { "ignorePending": true }]
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/rules/no-async-describe.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Disallow async functions passed to describe (`mocha/no-async-describe`)
# Disallow async functions passed to a suite (`mocha/no-async-describe`)

💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/lo1tuma/eslint-plugin-mocha#configs).

Expand Down
24 changes: 23 additions & 1 deletion docs/rules/no-pending-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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

Mocha allows specification of pending tests, which represent tests that aren't yet implemented, but are intended to be implemented eventually. These are designated like a normal mocha test, but with only the first argument provided (no callback for the actual implementation). For example: `it('unimplemented test');`
Mocha allows specification of pending tests, which represent tests that aren't yet implemented or explicitly skipped, but are intended to be implemented eventually. These are designated like a normal mocha test, but with only the first argument provided (no callback for the actual implementation). For example: `it('unimplemented test');` or `it.skip('foo', function () {});`

This rule allows you to raise ESLint warnings or errors on pending tests. This can be useful, for example, for reminding developers that pending tests exist in the repository, so they're more likely to get implemented.

Expand All @@ -18,6 +18,18 @@ The following patterns are considered warnings:
it('foo');
specify('foo');
test('foo');

describe.skip('foo', function () {});
it.skip('foo', function () {});
describe['skip']('bar', function () {});
it['skip']('bar', function () {});
xdescribe('baz', function () {});
xit('baz', function () {});

suite.skip('foo', function () {});
test.skip('foo', function () {});
suite['skip']('bar', function () {});
test['skip']('bar', function () {});
```

These patterns are not considered warnings:
Expand All @@ -26,6 +38,16 @@ These patterns are not considered warnings:
it('foo', function () {});
specify('foo', function () {});
test('foo', function () {});

describe('foo', function () {});
it('foo', function () {});
describe.only('bar', function () {});
it.only('bar', function () {});

suite('foo', function () {});
test('foo', function () {});
suite.only('bar', function () {});
test.only('bar', function () {});
```

## When Not To Use It
Expand Down
2 changes: 1 addition & 1 deletion docs/rules/no-sibling-hooks.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Disallow duplicate uses of a hook at the same level inside a describe (`mocha/no-sibling-hooks`)
# Disallow duplicate uses of a hook at the same level inside a suite (`mocha/no-sibling-hooks`)

💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/lo1tuma/eslint-plugin-mocha#configs).

Expand Down
55 changes: 0 additions & 55 deletions docs/rules/no-skipped-tests.md

This file was deleted.

3 changes: 1 addition & 2 deletions docs/rules/valid-suite-description.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ This rule enforces the suite descriptions to follow the desired format.
## Rule Details

By default, the regular expression is not configured and would be required if rule is enabled.
By default, the rule supports "describe", "context" and "suite" suite function names, but it can be configured to look for different suite names via rule configuration.

Example of a custom rule configuration:

```js
rules: {
"mocha/valid-suite-description": ["warn", "^[A-Z]"]
"mocha/valid-suite-description": ["warn", { pattern: "^[A-Z]", message: 'custom error message' }]
},
```

Expand Down
10 changes: 2 additions & 8 deletions docs/rules/valid-test-description.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,22 @@ This rule enforces the test descriptions to follow the desired format.

## Rule Details

By default, the regular expression is configured to be "^should" which requires test descriptions to start with "should".
By default, the rule supports "it", "specify" and "test" test function names, but it can be configured to look for different test names via rule configuration.
By default, the regular expression is configured to be `"^should"` which requires test descriptions to start with "should".

## Options

Example of a custom rule configuration:

```js
rules: {
"mocha/valid-test-description": ["warn", "mypattern$", ["it", "specify", "test", "mytestname"], "custom error message"]
},
// OR
rules: {
"mocha/valid-test-description": ["warn", { pattern: "mypattern$", testNames: ["it", "specify", "test", "mytestname"], message: 'custom error message' }]
"mocha/valid-test-description": ["warn", { pattern: "mypattern$", message: 'custom error message' }]
},
```

where:

- `warn` is a rule error level (see [Configuring Rules](http://eslint.org/docs/user-guide/configuring#configuring-rules))
- `mypattern$` is a custom regular expression pattern to match test names against
- `["it", "specify", "test", "mytestname"]` is an array of custom test names
- `custom error message` a custom error message to describe your pattern

The following patterns are considered warnings (with the default rule configuration):
Expand Down
5 changes: 3 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ export default [
{
files: ['test/index.js', '**/*.test.js', '**/*Spec.js', 'benchmarks/**/*.bench.js'],
rules: {
'max-statements': ['error', { max: 15 }],
'max-nested-callbacks': ['error', { max: 8 }]
'max-statements': ['error', { max: 50 }],
'max-nested-callbacks': ['error', { max: 8 }],
'no-magic-numbers': 'off'
}
},
{
Expand Down
4 changes: 0 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { noReturnAndCallbackRule } from './lib/rules/no-return-and-callback.js';
import { noReturnFromAsyncRule } from './lib/rules/no-return-from-async.js';
import { noSetupInDescribeRule } from './lib/rules/no-setup-in-describe.js';
import { noSiblingHooksRule } from './lib/rules/no-sibling-hooks.js';
import { noSkippedTestsRule } from './lib/rules/no-skipped-tests.js';
import { noSynchronousTestsRule } from './lib/rules/no-synchronous-tests.js';
import { noTopLevelHooksRule } from './lib/rules/no-top-level-hooks.js';
import { preferArrowCallbackRule } from './lib/rules/prefer-arrow-callback.js';
Expand All @@ -41,7 +40,6 @@ const allRules = {
'mocha/no-return-from-async': 'error',
'mocha/no-setup-in-describe': 'error',
'mocha/no-sibling-hooks': 'error',
'mocha/no-skipped-tests': 'error',
'mocha/no-synchronous-tests': 'error',
'mocha/no-top-level-hooks': 'error',
'mocha/prefer-arrow-callback': 'error',
Expand All @@ -68,7 +66,6 @@ const recommendedRules = {
'mocha/no-return-from-async': 'off',
'mocha/no-setup-in-describe': 'error',
'mocha/no-sibling-hooks': 'error',
'mocha/no-skipped-tests': 'warn',
'mocha/no-synchronous-tests': 'off',
'mocha/no-top-level-hooks': 'warn',
'mocha/prefer-arrow-callback': 'off',
Expand Down Expand Up @@ -96,7 +93,6 @@ const mochaPlugin = {
'no-return-from-async': noReturnFromAsyncRule,
'no-setup-in-describe': noSetupInDescribeRule,
'no-sibling-hooks': noSiblingHooksRule,
'no-skipped-tests': noSkippedTestsRule,
'no-synchronous-tests': noSynchronousTestsRule,
'no-top-level-hooks': noTopLevelHooksRule,
'prefer-arrow-callback': preferArrowCallbackRule,
Expand Down
104 changes: 104 additions & 0 deletions lib/ast/alias-references.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { findVariable } from '@eslint-community/eslint-utils';
import { flatMapWithArgs, mapWithArgs } from '../list.js';
import { extractMemberExpressionPath, isConstantPath } from './member-expression.js';
import { findParentNodeAndPathForIdentifier } from './resolved-reference.js';

function isAliasConstAssignment(node) {
if (node.type === 'VariableDeclarator' && node.parent.type === 'VariableDeclaration') {
return node.parent.kind === 'const';
}
return false;
}

function extractIdentifiersFromObjectPattern(node, currentPath) {
return node.properties.flatMap((property) => {
if (property.value.type === 'Identifier') {
return [{ identifier: property.value, path: [...currentPath, property.key.name] }];
}
if (property.value.type === 'ObjectPattern') {
return extractIdentifiersFromObjectPattern(property.value, [...currentPath, property.key.name]);
}

return [];
});
}

function getDeclaredIdentifiers(sourceCode, node) {
const path = extractMemberExpressionPath(sourceCode, node.init);

if (node.id.type === 'Identifier') {
return [{ identifier: node.id, fullPath: path, leftHandSidePath: [], rightHandSidePath: path }];
}

if (node.id.type === 'ObjectPattern') {
const allPatternIdentifiers = extractIdentifiersFromObjectPattern(node.id, []);
return allPatternIdentifiers.map((patternIdentifiers) => {
return {
identifier: patternIdentifiers.identifier,
fullPath: [...path, ...patternIdentifiers.path],
leftHandSidePath: patternIdentifiers.path,
rightHandSidePath: path
};
});
}

return [];
}

function extendPath(parentReference, originalPath, identifierPath) {
const extendedPath = [...parentReference.resolvedPath, ...originalPath, ...identifierPath.slice(1)];
if (isConstantPath(identifierPath) && identifierPath.length === 1 && identifierPath.at(0)?.endsWith('()')) {
extendedPath[extendedPath.length - 1] += '()';
}
return extendedPath;
}

function aliasReferenceToResolvedReference(sourceCode, parentReference, originalPath, reference) {
const { node, path } = findParentNodeAndPathForIdentifier(sourceCode, reference.identifier);
return { node, path, resolvedPath: extendPath(parentReference, originalPath, path) };
}

function isNonInitReference(reference) {
return reference.init !== true;
}

// eslint-disable-next-line max-statements -- no good idea how to split this function
function resolveAliasReferencesRecursively(sourceCode, reference) {
const { node } = reference;
const result = [reference];

if (isAliasConstAssignment(node)) {
const declaratedIdentifiers = getDeclaredIdentifiers(sourceCode, node);

for (const { identifier, leftHandSidePath: identifierPath } of declaratedIdentifiers) {
const aliasedVariable = findVariable(sourceCode.getScope(node), identifier.name);

if (aliasedVariable) {
const aliasReferencesWithoutInit = aliasedVariable.references.filter(isNonInitReference);
const aliasedResolvedReferences = mapWithArgs(
aliasReferencesWithoutInit,
aliasReferenceToResolvedReference,
sourceCode,
reference,
identifierPath
);

result.push(
...flatMapWithArgs(aliasedResolvedReferences, resolveAliasReferencesRecursively, sourceCode)
);
}
}
}

return result;
}

function hasConstantResolvedPath(resolvedReference) {
return isConstantPath(resolvedReference.resolvedPath);
}

export function resolveAliasedReferences(sourceCode, originalResolvedReferences) {
return flatMapWithArgs(originalResolvedReferences, resolveAliasReferencesRecursively, sourceCode).filter(
hasConstantResolvedPath
);
}
Loading

0 comments on commit 4a02c4c

Please sign in to comment.