Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jest-community/eslint-plugin-jest
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v28.7.0
Choose a base ref
...
head repository: jest-community/eslint-plugin-jest
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v28.8.0
Choose a head ref
  • 4 commits
  • 35 files changed
  • 3 contributors

Commits on Aug 5, 2024

  1. chore(deps): lock file maintenance

    renovate[bot] committed Aug 5, 2024
    Copy the full SHA
    c88f741 View commit details

Commits on Aug 6, 2024

  1. chore(deps): update yarn to v3.8.4 (#1638)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Aug 6, 2024
    Copy the full SHA
    11ef4fc View commit details

Commits on Aug 7, 2024

  1. feat: import formatting rules from eslint-plugin-jest-formatting (#…

    …1563)
    
    * feat: naively copy and paste `eslint-plugin-jest-formatting` rules
    
    * refactor: remove unneeded exports
    
    * refactor: give each rule a dedicated file
    
    * refactor: rename and convert test files to typescript
    
    * refactor: use `import` instead of `require` and clean up other imports
    
    * refactor: use `createRule`
    
    * fix: pass in the correct `name` for each padding rule
    
    * refactor: use `@typescript-eslint` types and constants
    
    * fix: address some initial typescript errors
    
    * refactor: use `messageId` instead of `message`
    
    * test: use flat compat `RuleTester`
    
    * test: specify full errors instead of counts
    
    * test: add basic file for `padding-around-all`
    
    * docs: add entries for new padding rules
    
    * refactor: address remaining TypeScript issues
    
    * refactor: address deprecated `context` method usage
    
    * test: remove comments
    
    * test: cleanup cases
    
    * test: add more cases for coverage
    
    * refactor: remove unneeded checks
    G-Rath authored Aug 7, 2024
    Copy the full SHA
    74078ee View commit details
  2. chore(release): 28.8.0 [skip ci]

    # [28.8.0](v28.7.0...v28.8.0) (2024-08-07)
    
    ### Features
    
    * import formatting rules from `eslint-plugin-jest-formatting` ([#1563](#1563)) ([74078ee](74078ee))
    semantic-release-bot committed Aug 7, 2024
    Copy the full SHA
    e1410ae View commit details
Showing with 2,486 additions and 329 deletions.
  1. +157 −157 .yarn/releases/{yarn-3.8.3.cjs → yarn-3.8.4.cjs}
  2. +1 −1 .yarnrc.yml
  3. +7 −0 CHANGELOG.md
  4. +63 −55 README.md
  5. +32 −0 docs/rules/padding-around-after-all-blocks.md
  6. +36 −0 docs/rules/padding-around-after-each-blocks.md
  7. +18 −0 docs/rules/padding-around-all.md
  8. +35 −0 docs/rules/padding-around-before-all-blocks.md
  9. +36 −0 docs/rules/padding-around-before-each-blocks.md
  10. +40 −0 docs/rules/padding-around-describe-blocks.md
  11. +42 −0 docs/rules/padding-around-expect-groups.md
  12. +46 −0 docs/rules/padding-around-test-blocks.md
  13. +2 −2 package.json
  14. +16 −0 src/__tests__/__snapshots__/rules.test.ts.snap
  15. +1 −1 src/__tests__/rules.test.ts
  16. +96 −0 src/rules/__tests__/padding-around-after-all-blocks.test.ts
  17. +94 −0 src/rules/__tests__/padding-around-after-each-blocks.test.ts
  18. +229 −0 src/rules/__tests__/padding-around-all.test.ts
  19. +96 −0 src/rules/__tests__/padding-around-before-all-blocks.test.ts
  20. +96 −0 src/rules/__tests__/padding-around-before-each-blocks.test.ts
  21. +133 −0 src/rules/__tests__/padding-around-describe-blocks.test.ts
  22. +198 −0 src/rules/__tests__/padding-around-expect-groups.test.ts
  23. +143 −0 src/rules/__tests__/padding-around-test-blocks.test.ts
  24. +20 −0 src/rules/padding-around-after-all-blocks.ts
  25. +20 −0 src/rules/padding-around-after-each-blocks.ts
  26. +22 −0 src/rules/padding-around-all.ts
  27. +20 −0 src/rules/padding-around-before-all-blocks.ts
  28. +20 −0 src/rules/padding-around-before-each-blocks.ts
  29. +28 −0 src/rules/padding-around-describe-blocks.ts
  30. +25 −0 src/rules/padding-around-expect-groups.ts
  31. +32 −0 src/rules/padding-around-test-blocks.ts
  32. +104 −0 src/rules/utils/__tests__/ast-utils.test.ts
  33. +84 −0 src/rules/utils/ast-utils.ts
  34. +379 −0 src/rules/utils/padding.ts
  35. +115 −113 yarn.lock
314 changes: 157 additions & 157 deletions .yarn/releases/yarn-3.8.3.cjs → .yarn/releases/yarn-3.8.4.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -6,4 +6,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: '@yarnpkg/plugin-interactive-tools'

yarnPath: .yarn/releases/yarn-3.8.3.cjs
yarnPath: .yarn/releases/yarn-3.8.4.cjs
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# [28.8.0](https://github.com/jest-community/eslint-plugin-jest/compare/v28.7.0...v28.8.0) (2024-08-07)


### Features

* import formatting rules from `eslint-plugin-jest-formatting` ([#1563](https://github.com/jest-community/eslint-plugin-jest/issues/1563)) ([74078ee](https://github.com/jest-community/eslint-plugin-jest/commit/74078ee13dd7c7d257d514809dadc5593a214e74))

# [28.7.0](https://github.com/jest-community/eslint-plugin-jest/compare/v28.6.0...v28.7.0) (2024-08-03)


118 changes: 63 additions & 55 deletions README.md

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions docs/rules/padding-around-after-all-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Enforce padding around `afterAll` blocks (`padding-around-after-all-blocks`)

🔧 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 -->

## Rule Details

This rule enforces a line of padding before _and_ after 1 or more `afterAll`
statements.

Note that it doesn't add/enforce a padding line if it's the last statement in
its scope.

Examples of **incorrect** code for this rule:

```js
const someText = 'abc';
afterAll(() => {});
describe('someText', () => {});
```

Examples of **correct** code for this rule:

```js
const someText = 'abc';

afterAll(() => {});

describe('someText', () => {});
```
36 changes: 36 additions & 0 deletions docs/rules/padding-around-after-each-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Enforce padding around `afterEach` blocks (`padding-around-after-each-blocks`)

🔧 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 -->

## Rule Details

This rule enforces a line of padding before _and_ after 1 or more `afterEach`
statements.

Note that it doesn't add/enforce a padding line if it's the last statement in
its scope.

Examples of **incorrect** code for this rule:

```js
const something = 123;
afterEach(() => {
// more stuff
});
describe('foo', () => {});
```

Examples of **correct** code for this rule:

```js
const something = 123;

afterEach(() => {
// more stuff
});

describe('foo', () => {});
```
18 changes: 18 additions & 0 deletions docs/rules/padding-around-all.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Enforce padding around Jest functions (`padding-around-all`)

🔧 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 -->

## Rule Details

This is a meta rule that simply enables all of the following rules:

- [padding-around-after-all-blocks](padding-around-after-all-blocks.md)
- [padding-around-after-each-blocks](padding-around-after-each-blocks.md)
- [padding-around-before-all-blocks](padding-around-before-all-blocks.md)
- [padding-around-before-each-blocks](padding-around-before-each-blocks.md)
- [padding-around-expect-groups](padding-around-expect-groups.md)
- [padding-around-describe-blocks](padding-around-describe-blocks.md)
- [padding-around-test-blocks](padding-around-test-blocks.md)
35 changes: 35 additions & 0 deletions docs/rules/padding-around-before-all-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Enforce padding around `beforeAll` blocks (`padding-around-before-all-blocks`)

🔧 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 -->

## Rule Details

This rule enforces a line of padding before _and_ after `beforeAll` statements.

Note that it doesn't add/enforce a padding line if it's the last statement in
its scope.

Examples of **incorrect** code for this rule:

```js
const something = 123;
beforeAll(() => {
// more stuff
});
describe('foo', () => {});
```

Examples of **correct** code for this rule:

```js
const something = 123;

beforeAll(() => {
// more stuff
});

describe('foo', () => {});
```
36 changes: 36 additions & 0 deletions docs/rules/padding-around-before-each-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Enforce padding around `beforeEach` blocks (`padding-around-before-each-blocks`)

🔧 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 -->

## Rule Details

This rule enforces a line of padding before _and_ after 1 or more `beforeEach`
statements

Note that it doesn't add/enforce a padding line if it's the last statement in
its scope

Examples of **incorrect** code for this rule:

```js
const something = 123;
beforeEach(() => {
// more stuff
});
describe('foo', () => {});
```

Examples of **correct** code for this rule:

```js
const something = 123;

beforeEach(() => {
// more stuff
});

describe('foo', () => {});
```
40 changes: 40 additions & 0 deletions docs/rules/padding-around-describe-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Enforce padding around `describe` blocks (`padding-around-describe-blocks`)

🔧 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 -->

## Rule Details

This rule enforces a line of padding before _and_ after 1 or more `describe`
statements

Note that it doesn't add/enforce a padding line if it's the last statement in
its scope

Examples of **incorrect** code for this rule:

```js
const thing = 123;
describe('foo', () => {
// stuff
});
describe('bar', () => {
// more stuff
});
```

Examples of **correct** code for this rule:

```js
const thing = 123;

describe('foo', () => {
// stuff
});

describe('bar', () => {
// more stuff
});
```
42 changes: 42 additions & 0 deletions docs/rules/padding-around-expect-groups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Enforce padding around `expect` groups (`padding-around-expect-groups`)

🔧 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 -->

## Rule Details

This rule enforces a line of padding before _and_ after 1 or more `expect`
statements

Note that it doesn't add/enforce a padding line if it's the last statement in
its scope and it doesn't add/enforce padding between two or more adjacent
`expect` statements.

Examples of **incorrect** code for this rule:

```js
test('thing one', () => {
let abc = 123;
expect(abc).toEqual(123);
expect(123).toEqual(abc);
abc = 456;
expect(abc).toEqual(456);
});
```

Examples of **correct** code for this rule:

```js
test('thing one', () => {
let abc = 123;

expect(abc).toEqual(123);
expect(123).toEqual(abc);

abc = 456;

expect(abc).toEqual(456);
});
```
46 changes: 46 additions & 0 deletions docs/rules/padding-around-test-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Enforce padding around afterAll blocks (`padding-around-test-blocks`)

🔧 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 -->

## Rule Details

This rule enforces a line of padding before _and_ after 1 or more `test`/`it`
statements

Note that it doesn't add/enforce a padding line if it's the last statement in
its scope

Examples of **incorrect** code for this rule:

```js
const thing = 123;
test('foo', () => {});
test('bar', () => {});
```

```js
const thing = 123;
it('foo', () => {});
it('bar', () => {});
```

Examples of **correct** code for this rule:

```js
const thing = 123;

test('foo', () => {});

test('bar', () => {});
```

```js
const thing = 123;

it('foo', () => {});

it('bar', () => {});
```
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-jest",
"version": "28.7.0",
"version": "28.8.0",
"description": "ESLint rules for Jest",
"keywords": [
"eslint",
@@ -118,7 +118,7 @@
"optional": true
}
},
"packageManager": "yarn@3.8.3",
"packageManager": "yarn@3.8.4",
"engines": {
"node": "^16.10.0 || ^18.12.0 || >=20.0.0"
},
16 changes: 16 additions & 0 deletions src/__tests__/__snapshots__/rules.test.ts.snap
Original file line number Diff line number Diff line change
@@ -37,6 +37,14 @@ exports[`rules should export configs that refer to actual rules 1`] = `
"jest/no-test-prefixes": "error",
"jest/no-test-return-statement": "error",
"jest/no-untyped-mock-factory": "error",
"jest/padding-around-after-all-blocks": "error",
"jest/padding-around-after-each-blocks": "error",
"jest/padding-around-all": "error",
"jest/padding-around-before-all-blocks": "error",
"jest/padding-around-before-each-blocks": "error",
"jest/padding-around-describe-blocks": "error",
"jest/padding-around-expect-groups": "error",
"jest/padding-around-test-blocks": "error",
"jest/prefer-called-with": "error",
"jest/prefer-comparison-matcher": "error",
"jest/prefer-each": "error",
@@ -120,6 +128,14 @@ exports[`rules should export configs that refer to actual rules 1`] = `
"jest/no-test-prefixes": "error",
"jest/no-test-return-statement": "error",
"jest/no-untyped-mock-factory": "error",
"jest/padding-around-after-all-blocks": "error",
"jest/padding-around-after-each-blocks": "error",
"jest/padding-around-all": "error",
"jest/padding-around-before-all-blocks": "error",
"jest/padding-around-before-each-blocks": "error",
"jest/padding-around-describe-blocks": "error",
"jest/padding-around-expect-groups": "error",
"jest/padding-around-test-blocks": "error",
"jest/prefer-called-with": "error",
"jest/prefer-comparison-matcher": "error",
"jest/prefer-each": "error",
2 changes: 1 addition & 1 deletion src/__tests__/rules.test.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { existsSync } from 'fs';
import { resolve } from 'path';
import plugin from '../';

const numberOfRules = 54;
const numberOfRules = 62;
const ruleNames = Object.keys(plugin.rules);
const deprecatedRules = Object.entries(plugin.rules)
.filter(([, rule]) => rule.meta.deprecated)
96 changes: 96 additions & 0 deletions src/rules/__tests__/padding-around-after-all-blocks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { TSESLint } from '@typescript-eslint/utils';
import rule from '../padding-around-after-all-blocks';
import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils';

const ruleTester = new RuleTester({
parser: espreeParser,
parserOptions: {
ecmaVersion: 6,
},
});

const testCase = {
code: `
const someText = 'abc';
afterAll(() => {
});
describe('someText', () => {
const something = 'abc';
// A comment
afterAll(() => {
// stuff
});
afterAll(() => {
// other stuff
});
});
describe('someText', () => {
const something = 'abc';
afterAll(() => {
// stuff
});
});
`,
output: `
const someText = 'abc';
afterAll(() => {
});
describe('someText', () => {
const something = 'abc';
// A comment
afterAll(() => {
// stuff
});
afterAll(() => {
// other stuff
});
});
describe('someText', () => {
const something = 'abc';
afterAll(() => {
// stuff
});
});
`,
errors: [
{
messageId: 'missingPadding',
line: 3,
column: 1,
},
{
messageId: 'missingPadding',
line: 5,
column: 1,
},
{
messageId: 'missingPadding',
line: 8,
column: 3,
},
{
messageId: 'missingPadding',
line: 11,
column: 3,
},
{
messageId: 'missingPadding',
line: 18,
column: 3,
},
],
} satisfies TSESLint.InvalidTestCase<'missingPadding', never>;

ruleTester.run('padding-around-after-all-blocks', rule, {
valid: [testCase.output],
invalid: ['src/component.test.jsx', 'src/component.test.js'].map(
filename => ({ ...testCase, filename }),
),
});
94 changes: 94 additions & 0 deletions src/rules/__tests__/padding-around-after-each-blocks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { TSESLint } from '@typescript-eslint/utils';
import rule from '../padding-around-after-each-blocks';
import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils';

const ruleTester = new RuleTester({
parser: espreeParser,
parserOptions: {
ecmaVersion: 6,
},
});

const testCase = {
code: `
const someText = 'abc';
afterEach(() => {
});
describe('someText', () => {
const something = 'abc';
// A comment
afterEach(() => {
// stuff
});
afterEach(() => {
// other stuff
});
});
describe('someText', () => {
const something = 'abc';
afterEach(() => {
// stuff
});
});
`,
output: `
const someText = 'abc';
afterEach(() => {
});
describe('someText', () => {
const something = 'abc';
// A comment
afterEach(() => {
// stuff
});
afterEach(() => {
// other stuff
});
});
describe('someText', () => {
const something = 'abc';
afterEach(() => {
// stuff
});
});
`,
errors: [
{
messageId: 'missingPadding',
line: 3,
column: 1,
},
{
messageId: 'missingPadding',
line: 5,
column: 1,
},
{
messageId: 'missingPadding',
line: 8,
column: 3,
},
{
messageId: 'missingPadding',
line: 11,
column: 3,
},
{
messageId: 'missingPadding',
line: 17,
column: 3,
},
],
} satisfies TSESLint.InvalidTestCase<'missingPadding', never>;

ruleTester.run('padding-around-after-each-blocks', rule, {
valid: [testCase.output],
invalid: ['src/component.test.jsx', 'src/component.test.js'].map(
filename => ({ ...testCase, filename }),
),
});
229 changes: 229 additions & 0 deletions src/rules/__tests__/padding-around-all.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import type { TSESLint } from '@typescript-eslint/utils';
import dedent from 'dedent';
import rule from '../padding-around-all';
import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils';

const ruleTester = new RuleTester({
parser: espreeParser,
parserOptions: {
ecmaVersion: 6,
},
});

// todo: these should be more fulsome
const testCase = {
code: `
const someText = 'abc';
afterAll(() => {
});
describe('someText', () => {
const something = 'abc';
// A comment
afterAll(() => {
// stuff
});
afterAll(() => {
// other stuff
});
});
describe('someText', () => {
const something = 'abc';
afterAll(() => {
// stuff
});
});
`,
output: `
const someText = 'abc';
afterAll(() => {
});
describe('someText', () => {
const something = 'abc';
// A comment
afterAll(() => {
// stuff
});
afterAll(() => {
// other stuff
});
});
describe('someText', () => {
const something = 'abc';
afterAll(() => {
// stuff
});
});
`,
errors: [
{
messageId: 'missingPadding',
line: 3,
column: 1,
},
{
messageId: 'missingPadding',
line: 5,
column: 1,
},
{
messageId: 'missingPadding',
line: 8,
column: 3,
},
{
messageId: 'missingPadding',
line: 11,
column: 3,
},
{
messageId: 'missingPadding',
line: 18,
column: 3,
},
],
} satisfies TSESLint.InvalidTestCase<'missingPadding', never>;

ruleTester.run('padding-around-all', rule, {
valid: [
testCase.output,
dedent`
xyz:
afterEach(() => {});
`,
],
invalid: [
...['src/component.test.jsx', 'src/component.test.js'].map(filename => ({
...testCase,
filename,
})),
{
code: dedent`
const someText = 'abc'
;afterEach(() => {})
`,
output: dedent`
const someText = 'abc'
;afterEach(() => {})
`,
errors: [
{
messageId: 'missingPadding',
line: 2,
column: 2,
},
],
},
{
code: dedent`
const someText = 'abc';
xyz:
afterEach(() => {});
`,
output: dedent`
const someText = 'abc';
xyz:
afterEach(() => {});
`,
errors: [
{
messageId: 'missingPadding',
line: 2,
column: 1,
},
],
},
{
code: dedent`
const expr = 'Papayas';
beforeEach(() => {});
it('does something?', () => {
switch (expr) {
case 'Oranges':
expect(expr).toBe('Oranges');
break;
case 'Mangoes':
case 'Papayas':
const v = 1;
expect(v).toBe(1);
console.log('Mangoes and papayas are $2.79 a pound.');
// Expected output: "Mangoes and papayas are $2.79 a pound."
break;
default:
console.log(\`Sorry, we are out of $\{expr}.\`);
}
});
`,
output: dedent`
const expr = 'Papayas';
beforeEach(() => {});
it('does something?', () => {
switch (expr) {
case 'Oranges':
expect(expr).toBe('Oranges');
break;
case 'Mangoes':
case 'Papayas':
const v = 1;
expect(v).toBe(1);
console.log('Mangoes and papayas are $2.79 a pound.');
// Expected output: "Mangoes and papayas are $2.79 a pound."
break;
default:
console.log(\`Sorry, we are out of $\{expr}.\`);
}
});
`,
errors: [
{
messageId: 'missingPadding',
line: 2,
column: 1,
endLine: 2,
endColumn: 22,
},
{
messageId: 'missingPadding',
line: 3,
column: 1,
endLine: 18,
endColumn: 4,
},
{
messageId: 'missingPadding',
line: 7,
column: 7,
endLine: 7,
endColumn: 13,
},
{
messageId: 'missingPadding',
line: 11,
column: 7,
endLine: 11,
endColumn: 25,
},
{
messageId: 'missingPadding',
line: 12,
column: 7,
endLine: 12,
endColumn: 61,
},
],
},
],
});
96 changes: 96 additions & 0 deletions src/rules/__tests__/padding-around-before-all-blocks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { TSESLint } from '@typescript-eslint/utils';
import rule from '../padding-around-before-all-blocks';
import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils';

const ruleTester = new RuleTester({
parser: espreeParser,
parserOptions: {
ecmaVersion: 6,
},
});

const testCase = {
code: `
const someText = 'abc';
beforeAll(() => {
});
describe('someText', () => {
const something = 'abc';
// A comment
beforeAll(() => {
// stuff
});
beforeAll(() => {
// other stuff
});
});
describe('someText', () => {
const something = 'abc';
beforeAll(() => {
// stuff
});
});
`,
output: `
const someText = 'abc';
beforeAll(() => {
});
describe('someText', () => {
const something = 'abc';
// A comment
beforeAll(() => {
// stuff
});
beforeAll(() => {
// other stuff
});
});
describe('someText', () => {
const something = 'abc';
beforeAll(() => {
// stuff
});
});
`,
errors: [
{
messageId: 'missingPadding',
line: 3,
column: 1,
},
{
messageId: 'missingPadding',
line: 5,
column: 1,
},
{
messageId: 'missingPadding',
line: 8,
column: 3,
},
{
messageId: 'missingPadding',
line: 11,
column: 3,
},
{
messageId: 'missingPadding',
line: 18,
column: 3,
},
],
} satisfies TSESLint.InvalidTestCase<'missingPadding', never>;

ruleTester.run('padding-around-before-all-blocks', rule, {
valid: [testCase.output],
invalid: ['src/component.test.jsx', 'src/component.test.js'].map(
filename => ({ ...testCase, filename }),
),
});
96 changes: 96 additions & 0 deletions src/rules/__tests__/padding-around-before-each-blocks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { TSESLint } from '@typescript-eslint/utils';
import rule from '../padding-around-before-each-blocks';
import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils';

const ruleTester = new RuleTester({
parser: espreeParser,
parserOptions: {
ecmaVersion: 6,
},
});

const testCase = {
code: `
const someText = 'abc';
beforeEach(() => {
});
describe('someText', () => {
const something = 'abc';
// A comment
beforeEach(() => {
// stuff
});
beforeEach(() => {
// other stuff
});
});
describe('someText', () => {
const something = 'abc';
beforeEach(() => {
// stuff
});
});
`,
output: `
const someText = 'abc';
beforeEach(() => {
});
describe('someText', () => {
const something = 'abc';
// A comment
beforeEach(() => {
// stuff
});
beforeEach(() => {
// other stuff
});
});
describe('someText', () => {
const something = 'abc';
beforeEach(() => {
// stuff
});
});
`,
errors: [
{
messageId: 'missingPadding',
line: 3,
column: 1,
},
{
messageId: 'missingPadding',
line: 5,
column: 1,
},
{
messageId: 'missingPadding',
line: 8,
column: 3,
},
{
messageId: 'missingPadding',
line: 11,
column: 3,
},
{
messageId: 'missingPadding',
line: 18,
column: 3,
},
],
} satisfies TSESLint.InvalidTestCase<'missingPadding', never>;

ruleTester.run('padding-around-before-each-blocks', rule, {
valid: [testCase.output],
invalid: ['src/component.test.jsx', 'src/component.test.js'].map(
filename => ({ ...testCase, filename }),
),
});
133 changes: 133 additions & 0 deletions src/rules/__tests__/padding-around-describe-blocks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type { TSESLint } from '@typescript-eslint/utils';
import rule from '../padding-around-describe-blocks';
import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils';

const ruleTester = new RuleTester({
parser: espreeParser,
parserOptions: {
ecmaVersion: 6,
},
});

const testCase = {
code: `
foo();
bar();
const someText = 'abc';
const someObject = {
one: 1,
two: 2,
};
// A comment before describe
describe('someText', () => {
describe('some condition', () => {
});
describe('some other condition', () => {
});
});
xdescribe('someObject', () => {
// Another comment
describe('some condition', () => {
const anotherThing = 500;
describe('yet another condition', () => { // A comment over here!
});
});
});fdescribe('weird', () => {});
describe.skip('skip me', () => {});
const BOOP = "boop";
describe
.skip('skip me too', () => {
// stuff
});
`,
output: `
foo();
bar();
const someText = 'abc';
const someObject = {
one: 1,
two: 2,
};
// A comment before describe
describe('someText', () => {
describe('some condition', () => {
});
describe('some other condition', () => {
});
});
xdescribe('someObject', () => {
// Another comment
describe('some condition', () => {
const anotherThing = 500;
describe('yet another condition', () => { // A comment over here!
});
});
});
fdescribe('weird', () => {});
describe.skip('skip me', () => {});
const BOOP = "boop";
describe
.skip('skip me too', () => {
// stuff
});
`,
errors: [
{
messageId: 'missingPadding',
line: 11,
column: 1,
},
{
messageId: 'missingPadding',
line: 14,
column: 3,
},
{
messageId: 'missingPadding',
line: 17,
column: 1,
},
{
messageId: 'missingPadding',
line: 21,
column: 5,
},
{
messageId: 'missingPadding',
line: 24,
column: 4,
},
{
messageId: 'missingPadding',
line: 25,
column: 1,
},
{
messageId: 'missingPadding',
line: 26,
column: 1,
},
{
messageId: 'missingPadding',
line: 27,
column: 1,
},
],
} satisfies TSESLint.InvalidTestCase<'missingPadding', never>;

ruleTester.run('padding-around-describe-blocks', rule, {
valid: [testCase.output],
invalid: ['src/component.test.jsx', 'src/component.test.js'].map(
filename => ({ ...testCase, filename }),
),
});
198 changes: 198 additions & 0 deletions src/rules/__tests__/padding-around-expect-groups.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import type { TSESLint } from '@typescript-eslint/utils';
import rule from '../padding-around-expect-groups';
import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils';

const ruleTester = new RuleTester({
parser: espreeParser,
parserOptions: {
ecmaVersion: 2017,
},
});

const testCase = {
code: `
foo();
bar();
const someText = 'abc';
const someObject = {
one: 1,
two: 2,
};
test('thing one', () => {
let abc = 123;
expect(abc).toEqual(123);
expect(123).toEqual(abc); // Line comment
abc = 456;
expect(abc).toEqual(456);
});
test('thing one', () => {
const abc = 123;
expect(abc).toEqual(123);
const xyz = 987;
expect(123).toEqual(abc); // Line comment
});
describe('someText', () => {
describe('some condition', () => {
test('foo', () => {
const xyz = 987;
// Comment
expect(xyz).toEqual(987);
expect(1)
.toEqual(1);
expect(true).toEqual(true);
});
});
});
test('awaited expect', async () => {
const abc = 123;
const hasAPromise = () => Promise.resolve('foo');
await expect(hasAPromise()).resolves.toEqual('foo');
expect(abc).toEqual(123);
const efg = 456;
expect(123).toEqual(abc);
await expect(hasAPromise()).resolves.toEqual('foo');
const hij = 789;
await expect(hasAPromise()).resolves.toEqual('foo');
await expect(hasAPromise()).resolves.toEqual('foo');
const somethingElseAsync = () => Promise.resolve('bar');
await somethingElseAsync();
await expect(hasAPromise()).resolves.toEqual('foo');
});
`,
output: `
foo();
bar();
const someText = 'abc';
const someObject = {
one: 1,
two: 2,
};
test('thing one', () => {
let abc = 123;
expect(abc).toEqual(123);
expect(123).toEqual(abc); // Line comment
abc = 456;
expect(abc).toEqual(456);
});
test('thing one', () => {
const abc = 123;
expect(abc).toEqual(123);
const xyz = 987;
expect(123).toEqual(abc); // Line comment
});
describe('someText', () => {
describe('some condition', () => {
test('foo', () => {
const xyz = 987;
// Comment
expect(xyz).toEqual(987);
expect(1)
.toEqual(1);
expect(true).toEqual(true);
});
});
});
test('awaited expect', async () => {
const abc = 123;
const hasAPromise = () => Promise.resolve('foo');
await expect(hasAPromise()).resolves.toEqual('foo');
expect(abc).toEqual(123);
const efg = 456;
expect(123).toEqual(abc);
await expect(hasAPromise()).resolves.toEqual('foo');
const hij = 789;
await expect(hasAPromise()).resolves.toEqual('foo');
await expect(hasAPromise()).resolves.toEqual('foo');
const somethingElseAsync = () => Promise.resolve('bar');
await somethingElseAsync();
await expect(hasAPromise()).resolves.toEqual('foo');
});
`,
errors: [
{
messageId: 'missingPadding',
line: 13,
column: 3,
},
{
messageId: 'missingPadding',
line: 15,
column: 3,
},
{
messageId: 'missingPadding',
line: 16,
column: 3,
},
{
messageId: 'missingPadding',
line: 21,
column: 3,
},
{
messageId: 'missingPadding',
line: 24,
column: 3,
},
{
messageId: 'missingPadding',
line: 32,
column: 7,
},
{
messageId: 'missingPadding',
line: 43,
column: 3,
},
{
messageId: 'missingPadding',
line: 47,
column: 3,
},
{
messageId: 'missingPadding',
line: 51,
column: 3,
},
{
messageId: 'missingPadding',
line: 56,
column: 3,
},
],
} satisfies TSESLint.InvalidTestCase<'missingPadding', never>;

ruleTester.run('padding-around-expect-groups', rule, {
valid: [testCase.output],
invalid: ['src/component.test.jsx', 'src/component.test.js'].map(
filename => ({ ...testCase, filename }),
),
});
143 changes: 143 additions & 0 deletions src/rules/__tests__/padding-around-test-blocks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import type { TSESLint } from '@typescript-eslint/utils';
import rule from '../padding-around-test-blocks';
import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils';

const ruleTester = new RuleTester({
parser: espreeParser,
parserOptions: {
ecmaVersion: 6,
},
});

const testCase = {
code: `
const foo = 'bar';
const bar = 'baz';
it('foo', () => {
// stuff
});
fit('bar', () => {
// stuff
});
test('foo foo', () => {});
test('bar bar', () => {});
// Nesting
describe('other bar', () => {
const thing = 123;
test('is another bar w/ test', () => {
});
// With a comment
it('is another bar w/ it', () => {
});
test.skip('skipping', () => {}); // Another comment
it.skip('skipping too', () => {});
});xtest('weird', () => {});
test
.skip('skippy skip', () => {});
xit('bar foo', () => {});
`,
output: `
const foo = 'bar';
const bar = 'baz';
it('foo', () => {
// stuff
});
fit('bar', () => {
// stuff
});
test('foo foo', () => {});
test('bar bar', () => {});
// Nesting
describe('other bar', () => {
const thing = 123;
test('is another bar w/ test', () => {
});
// With a comment
it('is another bar w/ it', () => {
});
test.skip('skipping', () => {}); // Another comment
it.skip('skipping too', () => {});
});
xtest('weird', () => {});
test
.skip('skippy skip', () => {});
xit('bar foo', () => {});
`,
errors: [
{
messageId: 'missingPadding',
line: 4,
column: 1,
},
{
messageId: 'missingPadding',
line: 7,
column: 1,
},
{
messageId: 'missingPadding',
line: 10,
column: 1,
},
{
messageId: 'missingPadding',
line: 11,
column: 1,
},
{
messageId: 'missingPadding',
line: 16,
column: 3,
},
{
messageId: 'missingPadding',
line: 19,
column: 3,
},
{
messageId: 'missingPadding',
line: 21,
column: 3,
},
{
messageId: 'missingPadding',
line: 22,
column: 3,
},
{
messageId: 'missingPadding',
line: 23,
column: 4,
},
{
messageId: 'missingPadding',
line: 24,
column: 1,
},
{
messageId: 'missingPadding',
line: 26,
column: 1,
},
],
} satisfies TSESLint.InvalidTestCase<'missingPadding', never>;

ruleTester.run('padding-around-test-blocks', rule, {
valid: [testCase.output],
invalid: ['src/component.test.jsx', 'src/component.test.js'].map(
filename => ({ ...testCase, filename }),
),
});
20 changes: 20 additions & 0 deletions src/rules/padding-around-after-all-blocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PaddingType, StatementType, createPaddingRule } from './utils/padding';

export const config = [
{
paddingType: PaddingType.Always,
prevStatementType: StatementType.Any,
nextStatementType: StatementType.AfterAllToken,
},
{
paddingType: PaddingType.Always,
prevStatementType: StatementType.AfterAllToken,
nextStatementType: StatementType.Any,
},
];

export default createPaddingRule(
__filename,
'Enforce padding around `afterAll` blocks',
config,
);
20 changes: 20 additions & 0 deletions src/rules/padding-around-after-each-blocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PaddingType, StatementType, createPaddingRule } from './utils/padding';

export const config = [
{
paddingType: PaddingType.Always,
prevStatementType: StatementType.Any,
nextStatementType: StatementType.AfterEachToken,
},
{
paddingType: PaddingType.Always,
prevStatementType: StatementType.AfterEachToken,
nextStatementType: StatementType.Any,
},
];

export default createPaddingRule(
__filename,
'Enforce padding around `afterEach` blocks',
config,
);
22 changes: 22 additions & 0 deletions src/rules/padding-around-all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { config as paddingAroundAfterAllBlocksConfig } from './padding-around-after-all-blocks';
import { config as paddingAroundAfterEachBlocksConfig } from './padding-around-after-each-blocks';
import { config as paddingAroundBeforeAllBlocksConfig } from './padding-around-before-all-blocks';
import { config as paddingAroundBeforeEachBlocksConfig } from './padding-around-before-each-blocks';
import { config as paddingAroundDescribeBlocksConfig } from './padding-around-describe-blocks';
import { config as paddingAroundExpectGroupsConfig } from './padding-around-expect-groups';
import { config as paddingAroundTestBlocksConfig } from './padding-around-test-blocks';
import { createPaddingRule } from './utils/padding';

export default createPaddingRule(
__filename,
'Enforce padding around Jest functions',
[
...paddingAroundAfterAllBlocksConfig,
...paddingAroundAfterEachBlocksConfig,
...paddingAroundBeforeAllBlocksConfig,
...paddingAroundBeforeEachBlocksConfig,
...paddingAroundDescribeBlocksConfig,
...paddingAroundExpectGroupsConfig,
...paddingAroundTestBlocksConfig,
],
);
20 changes: 20 additions & 0 deletions src/rules/padding-around-before-all-blocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PaddingType, StatementType, createPaddingRule } from './utils/padding';

export const config = [
{
paddingType: PaddingType.Always,
prevStatementType: StatementType.Any,
nextStatementType: StatementType.BeforeAllToken,
},
{
paddingType: PaddingType.Always,
prevStatementType: StatementType.BeforeAllToken,
nextStatementType: StatementType.Any,
},
];

export default createPaddingRule(
__filename,
'Enforce padding around `beforeAll` blocks',
config,
);
20 changes: 20 additions & 0 deletions src/rules/padding-around-before-each-blocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PaddingType, StatementType, createPaddingRule } from './utils/padding';

export const config = [
{
paddingType: PaddingType.Always,
prevStatementType: StatementType.Any,
nextStatementType: StatementType.BeforeEachToken,
},
{
paddingType: PaddingType.Always,
prevStatementType: StatementType.BeforeEachToken,
nextStatementType: StatementType.Any,
},
];

export default createPaddingRule(
__filename,
'Enforce padding around `beforeEach` blocks',
config,
);
28 changes: 28 additions & 0 deletions src/rules/padding-around-describe-blocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { PaddingType, StatementType, createPaddingRule } from './utils/padding';

export const config = [
{
paddingType: PaddingType.Always,
prevStatementType: StatementType.Any,
nextStatementType: [
StatementType.DescribeToken,
StatementType.FdescribeToken,
StatementType.XdescribeToken,
],
},
{
paddingType: PaddingType.Always,
prevStatementType: [
StatementType.DescribeToken,
StatementType.FdescribeToken,
StatementType.XdescribeToken,
],
nextStatementType: StatementType.Any,
},
];

export default createPaddingRule(
__filename,
'Enforce padding around `describe` blocks',
config,
);
25 changes: 25 additions & 0 deletions src/rules/padding-around-expect-groups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { PaddingType, StatementType, createPaddingRule } from './utils/padding';

export const config = [
{
paddingType: PaddingType.Always,
prevStatementType: StatementType.Any,
nextStatementType: StatementType.ExpectToken,
},
{
paddingType: PaddingType.Always,
prevStatementType: StatementType.ExpectToken,
nextStatementType: StatementType.Any,
},
{
paddingType: PaddingType.Any,
prevStatementType: StatementType.ExpectToken,
nextStatementType: StatementType.ExpectToken,
},
];

export default createPaddingRule(
__filename,
'Enforce padding around `expect` groups',
config,
);
32 changes: 32 additions & 0 deletions src/rules/padding-around-test-blocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { PaddingType, StatementType, createPaddingRule } from './utils/padding';

export const config = [
{
paddingType: PaddingType.Always,
prevStatementType: StatementType.Any,
nextStatementType: [
StatementType.TestToken,
StatementType.ItToken,
StatementType.FitToken,
StatementType.XitToken,
StatementType.XtestToken,
],
},
{
paddingType: PaddingType.Always,
prevStatementType: [
StatementType.TestToken,
StatementType.ItToken,
StatementType.FitToken,
StatementType.XitToken,
StatementType.XtestToken,
],
nextStatementType: StatementType.Any,
},
];

export default createPaddingRule(
__filename,
'Enforce padding around afterAll blocks',
config,
);
104 changes: 104 additions & 0 deletions src/rules/utils/__tests__/ast-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
AST_NODE_TYPES,
AST_TOKEN_TYPES,
type TSESTree,
} from '@typescript-eslint/utils';
import {
areTokensOnSameLine,
isTokenASemicolon,
isValidParent,
} from '../ast-utils';

describe('isValidParent', () => {
test.each`
type | expected
${AST_NODE_TYPES.Program} | ${true}
${AST_NODE_TYPES.BlockStatement} | ${true}
${AST_NODE_TYPES.SwitchCase} | ${true}
${AST_NODE_TYPES.SwitchStatement} | ${true}
${AST_NODE_TYPES.Identifier} | ${false}
`('returns $expected for parent value of $type', ({ type, expected }) => {
expect(isValidParent(type)).toBe(expected);
});
});

describe('isTokenASemicolon', () => {
test.each`
type | value | expected
${AST_TOKEN_TYPES.Punctuator} | ${';'} | ${true}
${AST_TOKEN_TYPES.Punctuator} | ${'.'} | ${false}
${AST_TOKEN_TYPES.String} | ${';'} | ${false}
`('returns $expected for $type and $value', ({ type, value, expected }) => {
const token: TSESTree.Token = {
type,
value,
range: [0, 1],
loc: {
start: {
line: 0,
column: 0,
},
end: {
line: 0,
column: 1,
},
},
};

expect(isTokenASemicolon(token)).toBe(expected);
});
});

describe('areTokensOnSameLine', () => {
const makeNode = (line: number): TSESTree.Node => {
return {
type: AST_NODE_TYPES.Identifier,
name: 'describe',
loc: {
start: {
line,
column: 10,
},
end: {
line,
column: 10,
},
},
} as TSESTree.Node;
};

const makeToken = (line: number): TSESTree.Token => {
return {
type: AST_TOKEN_TYPES.Punctuator,
value: ';',
range: [0, 1],
loc: {
start: {
line,
column: 10,
},
end: {
line,
column: 10,
},
},
};
};

test.each`
left | right | expected
${makeNode(1)} | ${makeNode(1)} | ${true}
${makeNode(1)} | ${makeToken(1)} | ${true}
${makeToken(1)} | ${makeNode(1)} | ${true}
${makeToken(1)} | ${makeToken(1)} | ${true}
${makeNode(1)} | ${makeNode(2)} | ${false}
${makeNode(1)} | ${makeToken(2)} | ${false}
${makeToken(1)} | ${makeNode(2)} | ${false}
${makeToken(1)} | ${makeToken(2)} | ${false}
`(
'returns $expected for left node/token ending on $left.loc.end.line and right node/token starting on $right.loc.start.line',
({ left, right, expected }) => {
expect(areTokensOnSameLine(left, right)).toBe(expected);
},
);
});
84 changes: 84 additions & 0 deletions src/rules/utils/ast-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
AST_NODE_TYPES,
AST_TOKEN_TYPES,
type TSESLint,
type TSESTree,
} from '@typescript-eslint/utils';

export const isTokenASemicolon = (token: TSESTree.Token): boolean =>
token.value === ';' && token.type === AST_TOKEN_TYPES.Punctuator;

export const areTokensOnSameLine = (
left: TSESTree.Node | TSESTree.Token,
right: TSESTree.Node | TSESTree.Token,
): boolean => left.loc.end.line === right.loc.start.line;

// We'll only verify nodes with these parent types
const STATEMENT_LIST_PARENTS = new Set([
AST_NODE_TYPES.Program,
AST_NODE_TYPES.BlockStatement,
AST_NODE_TYPES.SwitchCase,
AST_NODE_TYPES.SwitchStatement,
]);

export const isValidParent = (parentType: AST_NODE_TYPES): boolean => {
return STATEMENT_LIST_PARENTS.has(parentType);
};

/**
* Gets the actual last token.
*
* If a semicolon is semicolon-less style's semicolon, this ignores it.
* For example:
*
* foo()
* ;[1, 2, 3].forEach(bar)
*/
export const getActualLastToken = (
sourceCode: TSESLint.SourceCode,
node: TSESTree.Node,
): TSESTree.Token => {
const semiToken = sourceCode.getLastToken(node)!;
const prevToken = sourceCode.getTokenBefore(semiToken)!;
const nextToken = sourceCode.getTokenAfter(semiToken);
const isSemicolonLessStyle = Boolean(
prevToken &&
nextToken &&
prevToken.range[0] >= node.range[0] &&
isTokenASemicolon(semiToken) &&
semiToken.loc.start.line !== prevToken.loc.end.line &&
semiToken.loc.end.line === nextToken.loc.start.line,
);

return isSemicolonLessStyle ? prevToken : semiToken;
};

/**
* Gets padding line sequences between the given 2 statements.
* Comments are separators of the padding line sequences.
*/
export const getPaddingLineSequences = (
prevNode: TSESTree.Node,
nextNode: TSESTree.Node,
sourceCode: TSESLint.SourceCode,
): TSESTree.Token[][] => {
const pairs: TSESTree.Token[][] = [];
const includeComments = true;
let prevToken = getActualLastToken(sourceCode, prevNode);

if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) {
do {
const token = sourceCode.getTokenAfter(prevToken, {
includeComments,
}) as TSESTree.Token;

if (token.loc.start.line - prevToken.loc.end.line >= 2) {
pairs.push([prevToken, token]);
}

prevToken = token;
} while (prevToken.range[0] < nextNode.range[0]);
}

return pairs;
};
379 changes: 379 additions & 0 deletions src/rules/utils/padding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,379 @@
/**
* Require/fix newlines around jest functions
*
* Based on eslint/padding-line-between-statements by Toru Nagashima
* See: https://github.com/eslint/eslint/blob/master/lib/rules/padding-line-between-statements.js
*
* Some helpers borrowed from eslint ast-utils by Gyandeep Singh
* See: https://github.com/eslint/eslint/blob/master/lib/rules/utils/ast-utils.js
*/

import {
AST_NODE_TYPES,
AST_TOKEN_TYPES,
type TSESLint,
type TSESTree,
} from '@typescript-eslint/utils';
import * as astUtils from './ast-utils';
import { createRule, getSourceCode } from './misc';

// Statement types we'll respond to
export const enum StatementType {
Any,
AfterAllToken,
AfterEachToken,
BeforeAllToken,
BeforeEachToken,
DescribeToken,
ExpectToken,
FdescribeToken,
FitToken,
ItToken,
TestToken,
XdescribeToken,
XitToken,
XtestToken,
}

type StatementTypes = StatementType | StatementType[];

type StatementTester = (
node: TSESTree.Node,
sourceCode: TSESLint.SourceCode,
) => boolean;

// Padding type to apply between statements
export const enum PaddingType {
Any,
Always,
}

// A configuration object for padding type and the two statement types
interface Config {
paddingType: PaddingType;
prevStatementType: StatementTypes;
nextStatementType: StatementTypes;
}

interface ScopeInfo {
prevNode: TSESTree.Node | null;
enter: () => void;
exit: () => void;
}

interface PaddingContext {
ruleContext: TSESLint.RuleContext<'missingPadding', unknown[]>;
sourceCode: TSESLint.SourceCode;
scopeInfo: ScopeInfo;
configs: Config[];
}

type PaddingTester = (
prevNode: TSESTree.Node,
nextNode: TSESTree.Node,
paddingContext: PaddingContext,
) => void;

// Tracks position in scope and prevNode. Used to compare current and prev node
// and then to walk back up to the parent scope or down into the next one.
// And so on...
interface Scope {
upper: Scope | null;
prevNode: TSESTree.Node | null;
}

// Creates a StatementTester to test an ExpressionStatement's first token name
const createTokenTester = (tokenName: string): StatementTester => {
return (node: TSESTree.Node, sourceCode: TSESLint.SourceCode): boolean => {
let activeNode = node;

if (activeNode.type === AST_NODE_TYPES.ExpressionStatement) {
// In the case of `await`, we actually care about its argument
if (activeNode.expression.type === AST_NODE_TYPES.AwaitExpression) {
activeNode = activeNode.expression.argument;
}

const token = sourceCode.getFirstToken(activeNode);

return (
token?.type === AST_TOKEN_TYPES.Identifier && token.value === tokenName
);
}

return false;
};
};

// A mapping of StatementType to StatementTester for... testing statements
const statementTesters: { [T in StatementType]: StatementTester } = {
[StatementType.Any]: () => true,
[StatementType.AfterAllToken]: createTokenTester('afterAll'),
[StatementType.AfterEachToken]: createTokenTester('afterEach'),
[StatementType.BeforeAllToken]: createTokenTester('beforeAll'),
[StatementType.BeforeEachToken]: createTokenTester('beforeEach'),
[StatementType.DescribeToken]: createTokenTester('describe'),
[StatementType.ExpectToken]: createTokenTester('expect'),
[StatementType.FdescribeToken]: createTokenTester('fdescribe'),
[StatementType.FitToken]: createTokenTester('fit'),
[StatementType.ItToken]: createTokenTester('it'),
[StatementType.TestToken]: createTokenTester('test'),
[StatementType.XdescribeToken]: createTokenTester('xdescribe'),
[StatementType.XitToken]: createTokenTester('xit'),
[StatementType.XtestToken]: createTokenTester('xtest'),
};

/**
* Check and report statements for `PaddingType.Always` configuration.
* This autofix inserts a blank line between the given 2 statements.
* If the `prevNode` has trailing comments, it inserts a blank line after the
* trailing comments.
*/
const paddingAlwaysTester = (
prevNode: TSESTree.Node,
nextNode: TSESTree.Node,
paddingContext: PaddingContext,
): void => {
const { sourceCode, ruleContext } = paddingContext;
const paddingLines = astUtils.getPaddingLineSequences(
prevNode,
nextNode,
sourceCode,
);

// We've got some padding lines. Great.
if (paddingLines.length > 0) {
return;
}

// Missing padding line
ruleContext.report({
node: nextNode,
messageId: 'missingPadding',
fix(fixer: TSESLint.RuleFixer) {
let prevToken = astUtils.getActualLastToken(sourceCode, prevNode);
const nextToken = (sourceCode.getFirstTokenBetween(prevToken, nextNode, {
includeComments: true,
/**
* Skip the trailing comments of the previous node.
* This inserts a blank line after the last trailing comment.
*
* For example:
*
* foo(); // trailing comment.
* // comment.
* bar();
*
* Get fixed to:
*
* foo(); // trailing comment.
*
* // comment.
* bar();
*/
filter(token: TSESTree.Token): boolean {
if (astUtils.areTokensOnSameLine(prevToken, token)) {
prevToken = token;

return false;
}

return true;
},
}) || nextNode) as TSESTree.Token;

const insertText = astUtils.areTokensOnSameLine(prevToken, nextToken)
? '\n\n'
: '\n';

return fixer.insertTextAfter(prevToken, insertText);
},
});
};

// A mapping of PaddingType to PaddingTester
const paddingTesters: { [T in PaddingType]: PaddingTester } = {
[PaddingType.Any]: () => true,
[PaddingType.Always]: paddingAlwaysTester,
};

const createScopeInfo = (): ScopeInfo => {
let scope: Scope | null = null;

// todo: explore seeing if we can refactor to a more TypeScript friendly structure
return {
get prevNode() {
return scope!.prevNode;
},
set prevNode(node) {
scope!.prevNode = node;
},
enter() {
scope = { upper: scope, prevNode: null };
},
exit() {
scope = scope!.upper;
},
};
};

/**
* Check whether the given node matches the statement type
*/
const nodeMatchesType = (
node: TSESTree.Node,
statementType: StatementTypes,
paddingContext: PaddingContext,
): boolean => {
let innerStatementNode = node;
const { sourceCode } = paddingContext;

// Dig into LabeledStatement body until it's not that anymore
while (innerStatementNode.type === AST_NODE_TYPES.LabeledStatement) {
innerStatementNode = innerStatementNode.body;
}

// If it's an array recursively check if any of the statement types match
// the node
if (Array.isArray(statementType)) {
return statementType.some(type =>
nodeMatchesType(innerStatementNode, type, paddingContext),
);
}

return statementTesters[statementType](innerStatementNode, sourceCode);
};

/**
* Executes matching padding tester for last matched padding config for given
* nodes
*/
const testPadding = (
prevNode: TSESTree.Node,
nextNode: TSESTree.Node,
paddingContext: PaddingContext,
): void => {
const { configs } = paddingContext;

const testType = (type: PaddingType) =>
paddingTesters[type](prevNode, nextNode, paddingContext);

for (let i = configs.length - 1; i >= 0; --i) {
const {
prevStatementType: prevType,
nextStatementType: nextType,
paddingType,
} = configs[i];

if (
nodeMatchesType(prevNode, prevType, paddingContext) &&
nodeMatchesType(nextNode, nextType, paddingContext)
) {
return testType(paddingType);
}
}

// There were no matching padding rules for the prevNode, nextNode,
// paddingType combination... so we'll use PaddingType.Any which is always ok
return testType(PaddingType.Any);
};

/**
* Verify padding lines between the given node and the previous node.
*/
const verifyNode = (
node: TSESTree.Node,
paddingContext: PaddingContext,
): void => {
const { scopeInfo } = paddingContext;

// NOTE: ESLint types use ESTree which provides a Node type, however
// ESTree.Node doesn't support the parent property which is added by
// ESLint during traversal. Our best bet is to ignore the property access
// here as it's the only place that it's checked.

if (!astUtils.isValidParent((node as any).parent.type)) {
return;
}

if (scopeInfo.prevNode) {
testPadding(scopeInfo.prevNode, node, paddingContext);
}

scopeInfo.prevNode = node;
};

/**
* Creates an ESLint rule for a given set of padding Config objects.
*
* The algorithm is approximately this:
*
* For each 'scope' in the program
* - Enter the scope (store the parent scope and previous node)
* - For each statement in the scope
* - Check the current node and previous node against the Config objects
* - If the current node and previous node match a Config, check the padding.
* Otherwise, ignore it.
* - If the padding is missing (and required), report and fix
* - Store the current node as the previous
* - Repeat
* - Exit scope (return to parent scope and clear previous node)
*
* The items we're looking for with this rule are ExpressionStatement nodes
* where the first token is an Identifier with a name matching one of the Jest
* functions. It's not foolproof, of course, but it's probably good enough for
* almost all cases.
*
* The Config objects specify a padding type, a previous statement type, and a
* next statement type. Wildcard statement types and padding types are
* supported. The current node and previous node are checked against the
* statement types. If they match then the specified padding type is
* tested/enforced.
*
* See src/index.ts for examples of Config usage.
*/
export const createPaddingRule = (
name: string,
description: string,
configs: Config[],
deprecated = false,
) => {
return createRule({
name,
meta: {
docs: { description },
fixable: 'whitespace',
deprecated,
messages: {
missingPadding: 'Expected blank line before this statement.',
},
schema: [],
type: 'suggestion',
},
defaultOptions: [],
create(context) {
const paddingContext = {
ruleContext: context,
sourceCode: getSourceCode(context),
scopeInfo: createScopeInfo(),
configs,
};

const { scopeInfo } = paddingContext;

return {
Program: scopeInfo.enter,
'Program:exit': scopeInfo.enter,
BlockStatement: scopeInfo.enter,
'BlockStatement:exit': scopeInfo.exit,
SwitchStatement: scopeInfo.enter,
'SwitchStatement:exit': scopeInfo.exit,
':statement': (node: TSESTree.Node) => verifyNode(node, paddingContext),
SwitchCase(node: TSESTree.Node) {
verifyNode(node, paddingContext);
scopeInfo.enter();
},
'SwitchCase:exit': scopeInfo.exit,
};
},
});
};
228 changes: 115 additions & 113 deletions yarn.lock

Large diffs are not rendered by default.