diff --git a/README.md b/README.md index fad4d39e7..bfff60cf9 100644 --- a/README.md +++ b/README.md @@ -317,61 +317,69 @@ Automatically fixable by the Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions). -| Name                          | Description | 💼 | ⚠️ | 🔧 | 💡 | -| :--------------------------------------------------------------------------- | :------------------------------------------------------------------------ | :-- | :-- | :-- | :-- | -| [consistent-test-it](docs/rules/consistent-test-it.md) | Enforce `test` and `it` usage conventions | | | 🔧 | | -| [expect-expect](docs/rules/expect-expect.md) | Enforce assertion to be made in a test body | | ✅ | | | -| [max-expects](docs/rules/max-expects.md) | Enforces a maximum number assertion calls in a test body | | | | | -| [max-nested-describe](docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls | | | | | -| [no-alias-methods](docs/rules/no-alias-methods.md) | Disallow alias methods | ✅ | 🎨 | 🔧 | | -| [no-commented-out-tests](docs/rules/no-commented-out-tests.md) | Disallow commented out tests | | ✅ | | | -| [no-conditional-expect](docs/rules/no-conditional-expect.md) | Disallow calling `expect` conditionally | ✅ | | | | -| [no-conditional-in-test](docs/rules/no-conditional-in-test.md) | Disallow conditional logic in tests | | | | | -| [no-confusing-set-timeout](docs/rules/no-confusing-set-timeout.md) | Disallow confusing usages of jest.setTimeout | | | | | -| [no-deprecated-functions](docs/rules/no-deprecated-functions.md) | Disallow use of deprecated functions | ✅ | | 🔧 | | -| [no-disabled-tests](docs/rules/no-disabled-tests.md) | Disallow disabled tests | | ✅ | | | -| [no-done-callback](docs/rules/no-done-callback.md) | Disallow using a callback in asynchronous tests and hooks | ✅ | | | 💡 | -| [no-duplicate-hooks](docs/rules/no-duplicate-hooks.md) | Disallow duplicate setup and teardown hooks | | | | | -| [no-export](docs/rules/no-export.md) | Disallow using `exports` in files containing tests | ✅ | | | | -| [no-focused-tests](docs/rules/no-focused-tests.md) | Disallow focused tests | ✅ | | | 💡 | -| [no-hooks](docs/rules/no-hooks.md) | Disallow setup and teardown hooks | | | | | -| [no-identical-title](docs/rules/no-identical-title.md) | Disallow identical titles | ✅ | | | | -| [no-interpolation-in-snapshots](docs/rules/no-interpolation-in-snapshots.md) | Disallow string interpolation inside snapshots | ✅ | | | | -| [no-jasmine-globals](docs/rules/no-jasmine-globals.md) | Disallow Jasmine globals | ✅ | | 🔧 | | -| [no-large-snapshots](docs/rules/no-large-snapshots.md) | Disallow large snapshots | | | | | -| [no-mocks-import](docs/rules/no-mocks-import.md) | Disallow manually importing from `__mocks__` | ✅ | | | | -| [no-restricted-jest-methods](docs/rules/no-restricted-jest-methods.md) | Disallow specific `jest.` methods | | | | | -| [no-restricted-matchers](docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers | | | | | -| [no-standalone-expect](docs/rules/no-standalone-expect.md) | Disallow using `expect` outside of `it` or `test` blocks | ✅ | | | | -| [no-test-prefixes](docs/rules/no-test-prefixes.md) | Require using `.only` and `.skip` over `f` and `x` | ✅ | | 🔧 | | -| [no-test-return-statement](docs/rules/no-test-return-statement.md) | Disallow explicitly returning from tests | | | | | -| [no-untyped-mock-factory](docs/rules/no-untyped-mock-factory.md) | Disallow using `jest.mock()` factories without an explicit type parameter | | | 🔧 | | -| [prefer-called-with](docs/rules/prefer-called-with.md) | Suggest using `toBeCalledWith()` or `toHaveBeenCalledWith()` | | | | | -| [prefer-comparison-matcher](docs/rules/prefer-comparison-matcher.md) | Suggest using the built-in comparison matchers | | | 🔧 | | -| [prefer-each](docs/rules/prefer-each.md) | Prefer using `.each` rather than manual loops | | | | | -| [prefer-equality-matcher](docs/rules/prefer-equality-matcher.md) | Suggest using the built-in equality matchers | | | | 💡 | -| [prefer-expect-assertions](docs/rules/prefer-expect-assertions.md) | Suggest using `expect.assertions()` OR `expect.hasAssertions()` | | | | 💡 | -| [prefer-expect-resolves](docs/rules/prefer-expect-resolves.md) | Prefer `await expect(...).resolves` over `expect(await ...)` syntax | | | 🔧 | | -| [prefer-hooks-in-order](docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order | | | | | -| [prefer-hooks-on-top](docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | | | | -| [prefer-importing-jest-globals](docs/rules/prefer-importing-jest-globals.md) | Prefer importing Jest globals | | | 🔧 | | -| [prefer-jest-mocked](docs/rules/prefer-jest-mocked.md) | Prefer `jest.mocked()` over `fn as jest.Mock` | | | 🔧 | | -| [prefer-lowercase-title](docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | | 🔧 | | -| [prefer-mock-promise-shorthand](docs/rules/prefer-mock-promise-shorthand.md) | Prefer mock resolved/rejected shorthands for promises | | | 🔧 | | -| [prefer-snapshot-hint](docs/rules/prefer-snapshot-hint.md) | Prefer including a hint with external snapshots | | | | | -| [prefer-spy-on](docs/rules/prefer-spy-on.md) | Suggest using `jest.spyOn()` | | | 🔧 | | -| [prefer-strict-equal](docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | | | 💡 | -| [prefer-to-be](docs/rules/prefer-to-be.md) | Suggest using `toBe()` for primitive literals | 🎨 | | 🔧 | | -| [prefer-to-contain](docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | 🎨 | | 🔧 | | -| [prefer-to-have-length](docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` | 🎨 | | 🔧 | | -| [prefer-todo](docs/rules/prefer-todo.md) | Suggest using `test.todo` | | | 🔧 | | -| [require-hook](docs/rules/require-hook.md) | Require setup and teardown code to be within a hook | | | | | -| [require-to-throw-message](docs/rules/require-to-throw-message.md) | Require a message for `toThrow()` | | | | | -| [require-top-level-describe](docs/rules/require-top-level-describe.md) | Require test cases and hooks to be inside a `describe` block | | | | | -| [valid-describe-callback](docs/rules/valid-describe-callback.md) | Enforce valid `describe()` callback | ✅ | | | | -| [valid-expect](docs/rules/valid-expect.md) | Enforce valid `expect()` usage | ✅ | | 🔧 | | -| [valid-expect-in-promise](docs/rules/valid-expect-in-promise.md) | Require promises that have expectations in their chain to be valid | ✅ | | | | -| [valid-title](docs/rules/valid-title.md) | Enforce valid titles | ✅ | | 🔧 | | +| Name                              | Description | 💼 | ⚠️ | 🔧 | 💡 | +| :----------------------------------------------------------------------------------- | :------------------------------------------------------------------------ | :-- | :-- | :-- | :-- | +| [consistent-test-it](docs/rules/consistent-test-it.md) | Enforce `test` and `it` usage conventions | | | 🔧 | | +| [expect-expect](docs/rules/expect-expect.md) | Enforce assertion to be made in a test body | | ✅ | | | +| [max-expects](docs/rules/max-expects.md) | Enforces a maximum number assertion calls in a test body | | | | | +| [max-nested-describe](docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls | | | | | +| [no-alias-methods](docs/rules/no-alias-methods.md) | Disallow alias methods | ✅ | 🎨 | 🔧 | | +| [no-commented-out-tests](docs/rules/no-commented-out-tests.md) | Disallow commented out tests | | ✅ | | | +| [no-conditional-expect](docs/rules/no-conditional-expect.md) | Disallow calling `expect` conditionally | ✅ | | | | +| [no-conditional-in-test](docs/rules/no-conditional-in-test.md) | Disallow conditional logic in tests | | | | | +| [no-confusing-set-timeout](docs/rules/no-confusing-set-timeout.md) | Disallow confusing usages of jest.setTimeout | | | | | +| [no-deprecated-functions](docs/rules/no-deprecated-functions.md) | Disallow use of deprecated functions | ✅ | | 🔧 | | +| [no-disabled-tests](docs/rules/no-disabled-tests.md) | Disallow disabled tests | | ✅ | | | +| [no-done-callback](docs/rules/no-done-callback.md) | Disallow using a callback in asynchronous tests and hooks | ✅ | | | 💡 | +| [no-duplicate-hooks](docs/rules/no-duplicate-hooks.md) | Disallow duplicate setup and teardown hooks | | | | | +| [no-export](docs/rules/no-export.md) | Disallow using `exports` in files containing tests | ✅ | | | | +| [no-focused-tests](docs/rules/no-focused-tests.md) | Disallow focused tests | ✅ | | | 💡 | +| [no-hooks](docs/rules/no-hooks.md) | Disallow setup and teardown hooks | | | | | +| [no-identical-title](docs/rules/no-identical-title.md) | Disallow identical titles | ✅ | | | | +| [no-interpolation-in-snapshots](docs/rules/no-interpolation-in-snapshots.md) | Disallow string interpolation inside snapshots | ✅ | | | | +| [no-jasmine-globals](docs/rules/no-jasmine-globals.md) | Disallow Jasmine globals | ✅ | | 🔧 | | +| [no-large-snapshots](docs/rules/no-large-snapshots.md) | Disallow large snapshots | | | | | +| [no-mocks-import](docs/rules/no-mocks-import.md) | Disallow manually importing from `__mocks__` | ✅ | | | | +| [no-restricted-jest-methods](docs/rules/no-restricted-jest-methods.md) | Disallow specific `jest.` methods | | | | | +| [no-restricted-matchers](docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers | | | | | +| [no-standalone-expect](docs/rules/no-standalone-expect.md) | Disallow using `expect` outside of `it` or `test` blocks | ✅ | | | | +| [no-test-prefixes](docs/rules/no-test-prefixes.md) | Require using `.only` and `.skip` over `f` and `x` | ✅ | | 🔧 | | +| [no-test-return-statement](docs/rules/no-test-return-statement.md) | Disallow explicitly returning from tests | | | | | +| [no-untyped-mock-factory](docs/rules/no-untyped-mock-factory.md) | Disallow using `jest.mock()` factories without an explicit type parameter | | | 🔧 | | +| [padding-around-after-all-blocks](docs/rules/padding-around-after-all-blocks.md) | Enforce padding around `afterAll` blocks | | | 🔧 | | +| [padding-around-after-each-blocks](docs/rules/padding-around-after-each-blocks.md) | Enforce padding around `afterEach` blocks | | | 🔧 | | +| [padding-around-all](docs/rules/padding-around-all.md) | Enforce padding around Jest functions | | | 🔧 | | +| [padding-around-before-all-blocks](docs/rules/padding-around-before-all-blocks.md) | Enforce padding around `beforeAll` blocks | | | 🔧 | | +| [padding-around-before-each-blocks](docs/rules/padding-around-before-each-blocks.md) | Enforce padding around `beforeEach` blocks | | | 🔧 | | +| [padding-around-describe-blocks](docs/rules/padding-around-describe-blocks.md) | Enforce padding around `describe` blocks | | | 🔧 | | +| [padding-around-expect-groups](docs/rules/padding-around-expect-groups.md) | Enforce padding around `expect` groups | | | 🔧 | | +| [padding-around-test-blocks](docs/rules/padding-around-test-blocks.md) | Enforce padding around afterAll blocks | | | 🔧 | | +| [prefer-called-with](docs/rules/prefer-called-with.md) | Suggest using `toBeCalledWith()` or `toHaveBeenCalledWith()` | | | | | +| [prefer-comparison-matcher](docs/rules/prefer-comparison-matcher.md) | Suggest using the built-in comparison matchers | | | 🔧 | | +| [prefer-each](docs/rules/prefer-each.md) | Prefer using `.each` rather than manual loops | | | | | +| [prefer-equality-matcher](docs/rules/prefer-equality-matcher.md) | Suggest using the built-in equality matchers | | | | 💡 | +| [prefer-expect-assertions](docs/rules/prefer-expect-assertions.md) | Suggest using `expect.assertions()` OR `expect.hasAssertions()` | | | | 💡 | +| [prefer-expect-resolves](docs/rules/prefer-expect-resolves.md) | Prefer `await expect(...).resolves` over `expect(await ...)` syntax | | | 🔧 | | +| [prefer-hooks-in-order](docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order | | | | | +| [prefer-hooks-on-top](docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | | | | +| [prefer-importing-jest-globals](docs/rules/prefer-importing-jest-globals.md) | Prefer importing Jest globals | | | 🔧 | | +| [prefer-jest-mocked](docs/rules/prefer-jest-mocked.md) | Prefer `jest.mocked()` over `fn as jest.Mock` | | | 🔧 | | +| [prefer-lowercase-title](docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | | 🔧 | | +| [prefer-mock-promise-shorthand](docs/rules/prefer-mock-promise-shorthand.md) | Prefer mock resolved/rejected shorthands for promises | | | 🔧 | | +| [prefer-snapshot-hint](docs/rules/prefer-snapshot-hint.md) | Prefer including a hint with external snapshots | | | | | +| [prefer-spy-on](docs/rules/prefer-spy-on.md) | Suggest using `jest.spyOn()` | | | 🔧 | | +| [prefer-strict-equal](docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | | | 💡 | +| [prefer-to-be](docs/rules/prefer-to-be.md) | Suggest using `toBe()` for primitive literals | 🎨 | | 🔧 | | +| [prefer-to-contain](docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | 🎨 | | 🔧 | | +| [prefer-to-have-length](docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` | 🎨 | | 🔧 | | +| [prefer-todo](docs/rules/prefer-todo.md) | Suggest using `test.todo` | | | 🔧 | | +| [require-hook](docs/rules/require-hook.md) | Require setup and teardown code to be within a hook | | | | | +| [require-to-throw-message](docs/rules/require-to-throw-message.md) | Require a message for `toThrow()` | | | | | +| [require-top-level-describe](docs/rules/require-top-level-describe.md) | Require test cases and hooks to be inside a `describe` block | | | | | +| [valid-describe-callback](docs/rules/valid-describe-callback.md) | Enforce valid `describe()` callback | ✅ | | | | +| [valid-expect](docs/rules/valid-expect.md) | Enforce valid `expect()` usage | ✅ | | 🔧 | | +| [valid-expect-in-promise](docs/rules/valid-expect-in-promise.md) | Require promises that have expectations in their chain to be valid | ✅ | | | | +| [valid-title](docs/rules/valid-title.md) | Enforce valid titles | ✅ | | 🔧 | | ### Requires Type Checking diff --git a/docs/rules/padding-around-after-all-blocks.md b/docs/rules/padding-around-after-all-blocks.md new file mode 100644 index 000000000..f1ce1cc07 --- /dev/null +++ b/docs/rules/padding-around-after-all-blocks.md @@ -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). + + + +## 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', () => {}); +``` diff --git a/docs/rules/padding-around-after-each-blocks.md b/docs/rules/padding-around-after-each-blocks.md new file mode 100644 index 000000000..6fa1f32e4 --- /dev/null +++ b/docs/rules/padding-around-after-each-blocks.md @@ -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). + + + +## 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', () => {}); +``` diff --git a/docs/rules/padding-around-all.md b/docs/rules/padding-around-all.md new file mode 100644 index 000000000..83ca83244 --- /dev/null +++ b/docs/rules/padding-around-all.md @@ -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). + + + +## 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) diff --git a/docs/rules/padding-around-before-all-blocks.md b/docs/rules/padding-around-before-all-blocks.md new file mode 100644 index 000000000..7caca4dfe --- /dev/null +++ b/docs/rules/padding-around-before-all-blocks.md @@ -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). + + + +## 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', () => {}); +``` diff --git a/docs/rules/padding-around-before-each-blocks.md b/docs/rules/padding-around-before-each-blocks.md new file mode 100644 index 000000000..4101e12f7 --- /dev/null +++ b/docs/rules/padding-around-before-each-blocks.md @@ -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). + + + +## 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', () => {}); +``` diff --git a/docs/rules/padding-around-describe-blocks.md b/docs/rules/padding-around-describe-blocks.md new file mode 100644 index 000000000..a11f6ce7c --- /dev/null +++ b/docs/rules/padding-around-describe-blocks.md @@ -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). + + + +## 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 +}); +``` diff --git a/docs/rules/padding-around-expect-groups.md b/docs/rules/padding-around-expect-groups.md new file mode 100644 index 000000000..06e671c30 --- /dev/null +++ b/docs/rules/padding-around-expect-groups.md @@ -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). + + + +## 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); +}); +``` diff --git a/docs/rules/padding-around-test-blocks.md b/docs/rules/padding-around-test-blocks.md new file mode 100644 index 000000000..e76f95957 --- /dev/null +++ b/docs/rules/padding-around-test-blocks.md @@ -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). + + + +## 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', () => {}); +``` diff --git a/src/__tests__/__snapshots__/rules.test.ts.snap b/src/__tests__/__snapshots__/rules.test.ts.snap index e5a158bc3..d27e91fcb 100644 --- a/src/__tests__/__snapshots__/rules.test.ts.snap +++ b/src/__tests__/__snapshots__/rules.test.ts.snap @@ -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", diff --git a/src/__tests__/rules.test.ts b/src/__tests__/rules.test.ts index 469b95472..36a1f0407 100644 --- a/src/__tests__/rules.test.ts +++ b/src/__tests__/rules.test.ts @@ -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) diff --git a/src/rules/__tests__/padding-around-after-all-blocks.test.ts b/src/rules/__tests__/padding-around-after-all-blocks.test.ts new file mode 100644 index 000000000..7a3ab574d --- /dev/null +++ b/src/rules/__tests__/padding-around-after-all-blocks.test.ts @@ -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 }), + ), +}); diff --git a/src/rules/__tests__/padding-around-after-each-blocks.test.ts b/src/rules/__tests__/padding-around-after-each-blocks.test.ts new file mode 100644 index 000000000..8dbb13dc8 --- /dev/null +++ b/src/rules/__tests__/padding-around-after-each-blocks.test.ts @@ -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 }), + ), +}); diff --git a/src/rules/__tests__/padding-around-all.test.ts b/src/rules/__tests__/padding-around-all.test.ts new file mode 100644 index 000000000..7b53fcdc9 --- /dev/null +++ b/src/rules/__tests__/padding-around-all.test.ts @@ -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, + }, + ], + }, + ], +}); diff --git a/src/rules/__tests__/padding-around-before-all-blocks.test.ts b/src/rules/__tests__/padding-around-before-all-blocks.test.ts new file mode 100644 index 000000000..7b143c049 --- /dev/null +++ b/src/rules/__tests__/padding-around-before-all-blocks.test.ts @@ -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 }), + ), +}); diff --git a/src/rules/__tests__/padding-around-before-each-blocks.test.ts b/src/rules/__tests__/padding-around-before-each-blocks.test.ts new file mode 100644 index 000000000..d7459a826 --- /dev/null +++ b/src/rules/__tests__/padding-around-before-each-blocks.test.ts @@ -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 }), + ), +}); diff --git a/src/rules/__tests__/padding-around-describe-blocks.test.ts b/src/rules/__tests__/padding-around-describe-blocks.test.ts new file mode 100644 index 000000000..25ab569e4 --- /dev/null +++ b/src/rules/__tests__/padding-around-describe-blocks.test.ts @@ -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 }), + ), +}); diff --git a/src/rules/__tests__/padding-around-expect-groups.test.ts b/src/rules/__tests__/padding-around-expect-groups.test.ts new file mode 100644 index 000000000..6dfc9b721 --- /dev/null +++ b/src/rules/__tests__/padding-around-expect-groups.test.ts @@ -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 }), + ), +}); diff --git a/src/rules/__tests__/padding-around-test-blocks.test.ts b/src/rules/__tests__/padding-around-test-blocks.test.ts new file mode 100644 index 000000000..5d0f8f704 --- /dev/null +++ b/src/rules/__tests__/padding-around-test-blocks.test.ts @@ -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 }), + ), +}); diff --git a/src/rules/padding-around-after-all-blocks.ts b/src/rules/padding-around-after-all-blocks.ts new file mode 100644 index 000000000..5436d8362 --- /dev/null +++ b/src/rules/padding-around-after-all-blocks.ts @@ -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, +); diff --git a/src/rules/padding-around-after-each-blocks.ts b/src/rules/padding-around-after-each-blocks.ts new file mode 100644 index 000000000..361bbc096 --- /dev/null +++ b/src/rules/padding-around-after-each-blocks.ts @@ -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, +); diff --git a/src/rules/padding-around-all.ts b/src/rules/padding-around-all.ts new file mode 100644 index 000000000..4a19def37 --- /dev/null +++ b/src/rules/padding-around-all.ts @@ -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, + ], +); diff --git a/src/rules/padding-around-before-all-blocks.ts b/src/rules/padding-around-before-all-blocks.ts new file mode 100644 index 000000000..88ac37abe --- /dev/null +++ b/src/rules/padding-around-before-all-blocks.ts @@ -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, +); diff --git a/src/rules/padding-around-before-each-blocks.ts b/src/rules/padding-around-before-each-blocks.ts new file mode 100644 index 000000000..49989cf79 --- /dev/null +++ b/src/rules/padding-around-before-each-blocks.ts @@ -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, +); diff --git a/src/rules/padding-around-describe-blocks.ts b/src/rules/padding-around-describe-blocks.ts new file mode 100644 index 000000000..e75b5c9dc --- /dev/null +++ b/src/rules/padding-around-describe-blocks.ts @@ -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, +); diff --git a/src/rules/padding-around-expect-groups.ts b/src/rules/padding-around-expect-groups.ts new file mode 100644 index 000000000..968475fca --- /dev/null +++ b/src/rules/padding-around-expect-groups.ts @@ -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, +); diff --git a/src/rules/padding-around-test-blocks.ts b/src/rules/padding-around-test-blocks.ts new file mode 100644 index 000000000..0350b77ea --- /dev/null +++ b/src/rules/padding-around-test-blocks.ts @@ -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, +); diff --git a/src/rules/utils/__tests__/ast-utils.test.ts b/src/rules/utils/__tests__/ast-utils.test.ts new file mode 100644 index 000000000..0b9ccea38 --- /dev/null +++ b/src/rules/utils/__tests__/ast-utils.test.ts @@ -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); + }, + ); +}); diff --git a/src/rules/utils/ast-utils.ts b/src/rules/utils/ast-utils.ts new file mode 100644 index 000000000..3a9e3e0f4 --- /dev/null +++ b/src/rules/utils/ast-utils.ts @@ -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; +}; diff --git a/src/rules/utils/padding.ts b/src/rules/utils/padding.ts new file mode 100644 index 000000000..edd071b05 --- /dev/null +++ b/src/rules/utils/padding.ts @@ -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, + }; + }, + }); +};