Skip to content

Commit 07071ad

Browse files
committed
Fix consistent-spacing-between-blocks when using timeout() modifier
1 parent d7f28cb commit 07071ad

File tree

4 files changed

+102
-15
lines changed

4 files changed

+102
-15
lines changed

.c8rc.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"all": true,
3-
"lines": 98.4,
4-
"statements": 98.4,
3+
"lines": 98.34,
4+
"statements": 98.34,
55
"functions": 99.6,
6-
"branches": 95,
6+
"branches": 94.8,
77
"check-coverage": true,
88
"extension": [".js"],
99
"instrument": false,

source/ast/node-types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,9 @@ export type Literal = NodeType<'Literal'>;
8686
export function isLiteral(node: Except<Rule.Node, 'parent'>): node is Literal {
8787
return node.type === 'Literal';
8888
}
89+
90+
export type Program = NodeType<'Program'>;
91+
92+
export function isProgram(node: Except<Rule.Node, 'parent'>): node is Program {
93+
return node.type === 'Program';
94+
}

source/rules/consistent-spacing-between-blocks.test.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,28 @@ ruleTester.run('consistent-spacing-between-mocha-calls', consistentSpacingBetwee
4141

4242
`it('does something outside a describe block', () => {});
4343
44-
afterEach(() => {});`
44+
afterEach(() => {});`,
45+
{
46+
code: `describe('foo', () => {
47+
it('bar', () => {}).timeout(42);
48+
});`
49+
},
50+
{
51+
code: `describe('foo', () => {
52+
it('bar', () => {}).timeout(42);
53+
54+
it('baz', () => {}).timeout(42);
55+
});`
56+
},
57+
{
58+
code: `describe('foo', () => {
59+
it('bar', () => {})
60+
.timeout(42);
61+
62+
it('baz', () => {})
63+
.timeout(42);
64+
});`
65+
}
4566
],
4667

4768
invalid: [
@@ -120,10 +141,27 @@ ruleTester.run('consistent-spacing-between-mocha-calls', consistentSpacingBetwee
120141
}
121142
]
122143
},
144+
{
145+
code: "describe('Same line blocks', () => {" +
146+
"it('block one', () => {})\n.timeout(42);" +
147+
"it('block two', () => {});" +
148+
'});',
149+
output: "describe('Same line blocks', () => {" +
150+
"it('block one', () => {})\n.timeout(42);" +
151+
'\n\n' +
152+
"it('block two', () => {});" +
153+
'});',
154+
errors: [
155+
{
156+
message: 'Expected line break before this statement.',
157+
type: 'CallExpression'
158+
}
159+
]
160+
},
123161

124162
{
125-
code: 'describe();describe();',
126-
output: 'describe();\n\ndescribe();',
163+
code: 'describe("", () => {});describe("", () => {});',
164+
output: 'describe("", () => {});\n\ndescribe("", () => {});',
127165
errors: [
128166
{
129167
message: 'Expected line break before this statement.',

source/rules/consistent-spacing-between-blocks.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,51 @@
11
import type { Rule } from 'eslint';
2+
import type { Except } from 'type-fest';
23
import { createMochaVisitors, type VisitorContext } from '../ast/mocha-visitors.js';
4+
import {
5+
type AnyFunction,
6+
isBlockStatement,
7+
isFunction,
8+
isMemberExpression,
9+
isProgram,
10+
type Program
11+
} from '../ast/node-types.js';
312
import { getLastOrThrow } from '../list.js';
413

514
const minimumAmountOfLinesBetweenNeeded = 2;
615

7-
function isFirstStatementInScope(node: Readonly<Rule.Node>): boolean {
8-
// @ts-expect-error -- ok in this case
9-
return node.parent.parent.body[0] === node.parent; // eslint-disable-line @typescript-eslint/no-unsafe-member-access -- ok in this case
16+
function containsNode(nodeA: Except<Rule.Node, 'parent'>, nodeB: Except<Rule.Node, 'parent'>): boolean {
17+
const { range: rangeA } = nodeA;
18+
const { range: rangeB } = nodeB;
19+
if (rangeA === undefined || rangeB === undefined) {
20+
return false;
21+
}
22+
23+
return rangeB[1] <= rangeA[1] && rangeB[0] >= rangeA[0];
24+
}
25+
26+
function isFirstStatementInScope(scopeNode: Layer['scopeNode'], node: Rule.Node): boolean {
27+
if (isBlockStatement(scopeNode) || isProgram(scopeNode)) {
28+
const [firstNode] = scopeNode.body;
29+
if (firstNode !== undefined) {
30+
return containsNode(firstNode, node);
31+
}
32+
}
33+
34+
return containsNode(scopeNode, node);
1035
}
1136

1237
type Layer = {
1338
entities: VisitorContext[];
39+
scopeNode: AnyFunction['body'] | Program;
1440
};
1541

42+
function getParentWhileMemberExpression(node: Rule.Node): Rule.Node {
43+
if (isMemberExpression(node.parent)) {
44+
return getParentWhileMemberExpression(node.parent);
45+
}
46+
return node;
47+
}
48+
1649
export const consistentSpacingBetweenBlocksRule: Readonly<Rule.RuleModule> = {
1750
meta: {
1851
type: 'suggestion',
@@ -26,7 +59,7 @@ export const consistentSpacingBetweenBlocksRule: Readonly<Rule.RuleModule> = {
2659
},
2760

2861
create(context) {
29-
const layers: [Layer, ...Layer[]] = [{ entities: [] }];
62+
const layers: Layer[] = [];
3063
const { sourceCode } = context;
3164

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

4174
for (const entity of currentLayer.entities) {
42-
const { node } = entity;
75+
const node = getParentWhileMemberExpression(entity.node);
4376
const beforeToken = sourceCode.getTokenBefore(node);
4477

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

4881
if (linesBetween < minimumAmountOfLinesBetweenNeeded) {
4982
context.report({
50-
node,
83+
node: entity.node,
5184
message: 'Expected line break before this statement.',
5285
fix(fixer) {
5386
return fixer.insertTextAfter(
@@ -64,14 +97,24 @@ export const consistentSpacingBetweenBlocksRule: Readonly<Rule.RuleModule> = {
6497
return createMochaVisitors(context, {
6598
suite(visitorContext) {
6699
addEntityToCurrentLayer(visitorContext);
67-
layers.push({ entities: [] });
68100
},
69101

70-
'suite:exit'() {
102+
suiteCallback(visitorContext) {
103+
const { node } = visitorContext;
104+
if (isFunction(node)) {
105+
layers.push({ entities: [], scopeNode: node.body });
106+
}
107+
},
108+
109+
'suiteCallback:exit'() {
71110
checkCurrentLayer();
72111
layers.pop();
73112
},
74113

114+
Program(node) {
115+
layers.push({ entities: [], scopeNode: node });
116+
},
117+
75118
'Program:exit'() {
76119
checkCurrentLayer();
77120
},

0 commit comments

Comments
 (0)