Skip to content

Fix consistent-spacing-between-blocks when using timeout() modifier #379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .c8rc.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"all": true,
"lines": 98.4,
"statements": 98.4,
"lines": 98.34,
"statements": 98.34,
"functions": 99.6,
"branches": 95,
"branches": 94.8,
"check-coverage": true,
"extension": [".js"],
"instrument": false,
Expand Down
6 changes: 6 additions & 0 deletions source/ast/node-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,9 @@ export type Literal = NodeType<'Literal'>;
export function isLiteral(node: Except<Rule.Node, 'parent'>): node is Literal {
return node.type === 'Literal';
}

export type Program = NodeType<'Program'>;

export function isProgram(node: Except<Rule.Node, 'parent'>): node is Program {
return node.type === 'Program';
}
44 changes: 41 additions & 3 deletions source/rules/consistent-spacing-between-blocks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,28 @@ ruleTester.run('consistent-spacing-between-mocha-calls', consistentSpacingBetwee

`it('does something outside a describe block', () => {});

afterEach(() => {});`
afterEach(() => {});`,
{
code: `describe('foo', () => {
it('bar', () => {}).timeout(42);
});`
},
{
code: `describe('foo', () => {
it('bar', () => {}).timeout(42);

it('baz', () => {}).timeout(42);
});`
},
{
code: `describe('foo', () => {
it('bar', () => {})
.timeout(42);

it('baz', () => {})
.timeout(42);
});`
}
],

invalid: [
Expand Down Expand Up @@ -120,10 +141,27 @@ ruleTester.run('consistent-spacing-between-mocha-calls', consistentSpacingBetwee
}
]
},
{
code: "describe('Same line blocks', () => {" +
"it('block one', () => {})\n.timeout(42);" +
"it('block two', () => {});" +
'});',
output: "describe('Same line blocks', () => {" +
"it('block one', () => {})\n.timeout(42);" +
'\n\n' +
"it('block two', () => {});" +
'});',
errors: [
{
message: 'Expected line break before this statement.',
type: 'CallExpression'
}
]
},

{
code: 'describe();describe();',
output: 'describe();\n\ndescribe();',
code: 'describe("", () => {});describe("", () => {});',
output: 'describe("", () => {});\n\ndescribe("", () => {});',
errors: [
{
message: 'Expected line break before this statement.',
Expand Down
61 changes: 52 additions & 9 deletions source/rules/consistent-spacing-between-blocks.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,51 @@
import type { Rule } from 'eslint';
import type { Except } from 'type-fest';
import { createMochaVisitors, type VisitorContext } from '../ast/mocha-visitors.js';
import {
type AnyFunction,
isBlockStatement,
isFunction,
isMemberExpression,
isProgram,
type Program
} from '../ast/node-types.js';
import { getLastOrThrow } from '../list.js';

const minimumAmountOfLinesBetweenNeeded = 2;

function isFirstStatementInScope(node: Readonly<Rule.Node>): boolean {
// @ts-expect-error -- ok in this case
return node.parent.parent.body[0] === node.parent; // eslint-disable-line @typescript-eslint/no-unsafe-member-access -- ok in this case
function containsNode(nodeA: Except<Rule.Node, 'parent'>, nodeB: Except<Rule.Node, 'parent'>): boolean {
const { range: rangeA } = nodeA;
const { range: rangeB } = nodeB;
if (rangeA === undefined || rangeB === undefined) {
return false;
}

return rangeB[1] <= rangeA[1] && rangeB[0] >= rangeA[0];
}

function isFirstStatementInScope(scopeNode: Layer['scopeNode'], node: Rule.Node): boolean {
if (isBlockStatement(scopeNode) || isProgram(scopeNode)) {
const [firstNode] = scopeNode.body;
if (firstNode !== undefined) {
return containsNode(firstNode, node);
}
}

return containsNode(scopeNode, node);
}

type Layer = {
entities: VisitorContext[];
scopeNode: AnyFunction['body'] | Program;
};

function getParentWhileMemberExpression(node: Rule.Node): Rule.Node {
if (isMemberExpression(node.parent)) {
return getParentWhileMemberExpression(node.parent);
}
return node;
}

export const consistentSpacingBetweenBlocksRule: Readonly<Rule.RuleModule> = {
meta: {
type: 'suggestion',
Expand All @@ -26,7 +59,7 @@ export const consistentSpacingBetweenBlocksRule: Readonly<Rule.RuleModule> = {
},

create(context) {
const layers: [Layer, ...Layer[]] = [{ entities: [] }];
const layers: Layer[] = [];
const { sourceCode } = context;

function addEntityToCurrentLayer(visitorContext: Readonly<VisitorContext>): void {
Expand All @@ -39,15 +72,15 @@ export const consistentSpacingBetweenBlocksRule: Readonly<Rule.RuleModule> = {
const currentLayer = getLastOrThrow(layers);

for (const entity of currentLayer.entities) {
const { node } = entity;
const node = getParentWhileMemberExpression(entity.node);
const beforeToken = sourceCode.getTokenBefore(node);

if (!isFirstStatementInScope(node) && beforeToken !== null) {
if (!isFirstStatementInScope(currentLayer.scopeNode, node) && beforeToken !== null) {
const linesBetween = (node.loc?.start.line ?? 0) - (beforeToken.loc.end.line);

if (linesBetween < minimumAmountOfLinesBetweenNeeded) {
context.report({
node,
node: entity.node,
message: 'Expected line break before this statement.',
fix(fixer) {
return fixer.insertTextAfter(
Expand All @@ -64,14 +97,24 @@ export const consistentSpacingBetweenBlocksRule: Readonly<Rule.RuleModule> = {
return createMochaVisitors(context, {
suite(visitorContext) {
addEntityToCurrentLayer(visitorContext);
layers.push({ entities: [] });
},

'suite:exit'() {
suiteCallback(visitorContext) {
const { node } = visitorContext;
if (isFunction(node)) {
layers.push({ entities: [], scopeNode: node.body });
}
},

'suiteCallback:exit'() {
checkCurrentLayer();
layers.pop();
},

Program(node) {
layers.push({ entities: [], scopeNode: node });
},

'Program:exit'() {
checkCurrentLayer();
},
Expand Down