Skip to content
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