From 9e0e2fa966b43c1099d11b2424acb1590c241c03 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sat, 16 May 2020 21:58:08 +1200 Subject: [PATCH] feat(valid-expect): support `minArgs` & `maxArgs` options (#584) --- docs/rules/valid-expect.md | 22 +++- src/rules/__tests__/valid-expect.test.ts | 150 ++++++++++++++++++++++- src/rules/valid-expect.ts | 61 ++++++--- 3 files changed, 208 insertions(+), 25 deletions(-) diff --git a/docs/rules/valid-expect.md b/docs/rules/valid-expect.md index b01504e83..cb8512dd7 100644 --- a/docs/rules/valid-expect.md +++ b/docs/rules/valid-expect.md @@ -30,7 +30,7 @@ This rule is enabled by default. ## Options -```js +```json5 { type: 'object', properties: { @@ -38,6 +38,14 @@ This rule is enabled by default. type: 'boolean', default: false, }, + minArgs: { + type: 'number', + minimum: 1, + }, + maxArgs: { + type: 'number', + minimum: 1, + }, }, additionalProperties: false, } @@ -70,6 +78,18 @@ test('test1', async () => { test('test2', () => expect(Promise.resolve(2)).resolves.toBe(2)); ``` +### `minArgs` & `maxArgs` + +Enforces the minimum and maximum number of arguments that `expect` can take, and +is required to take. + +Both of these properties have a default value of `1`, which is the number of +arguments supported by vanilla `expect`. + +This is useful when you're using libraries that increase the number of arguments +supported by `expect`, such as +[`jest-expect-message`](https://www.npmjs.com/package/jest-expect-message). + ### Default configuration The following patterns are considered warnings: diff --git a/src/rules/__tests__/valid-expect.test.ts b/src/rules/__tests__/valid-expect.test.ts index becf990af..ea016eb94 100644 --- a/src/rules/__tests__/valid-expect.test.ts +++ b/src/rules/__tests__/valid-expect.test.ts @@ -73,6 +73,18 @@ ruleTester.run('valid-expect', rule, { return expect(functionReturningAPromise()).resolves.toEqual(1).then(() => expect(Promise.resolve(2)).resolves.toBe(1)); });`, }, + { + code: 'expect(1).toBe(2);', + options: [{ maxArgs: 2 }], + }, + { + code: 'expect(1, "1 !== 2").toBe(2);', + options: [{ maxArgs: 2 }], + }, + { + code: 'expect(1, "1 !== 2").toBe(2);', + options: [{ maxArgs: 2, minArgs: 2 }], + }, ], invalid: [ /* @@ -97,22 +109,144 @@ ruleTester.run('valid-expect', rule, { 'test("valid-expect", async () => { await expect(Promise.reject(2)).not.resolves.toBeDefined().then(() => console.log("valid-case")).catch(() => console.log("another valid case")); });', 'test("valid-expect", async () => { await expect(Promise.reject(2)).not.resolves.toBeDefined().then(() => { expect(someMock).toHaveBeenCalledTimes(1); }); });', */ + { + code: 'expect().toBe(2);', + options: [{ minArgs: undefined, maxArgs: undefined }], + errors: [ + { + messageId: 'notEnoughArgs', + data: { + s: '', + amount: 1, + }, + }, + ], + }, + { code: 'expect().toBe(true);', errors: [ - { endColumn: 8, column: 7, messageId: 'incorrectNumberOfArguments' }, + { + endColumn: 8, + column: 7, + messageId: 'notEnoughArgs', + data: { + s: '', + amount: 1, + }, + }, ], }, { code: 'expect().toEqual("something");', errors: [ - { endColumn: 8, column: 7, messageId: 'incorrectNumberOfArguments' }, + { + endColumn: 8, + column: 7, + messageId: 'notEnoughArgs', + data: { + s: '', + amount: 1, + }, + }, + ], + }, + { + code: 'expect("something", "else").toEqual("something");', + errors: [ + { + endColumn: 26, + column: 21, + messageId: 'tooManyArgs', + data: { + s: '', + amount: 1, + }, + }, + ], + }, + { + code: 'expect("something", "else", "entirely").toEqual("something");', + options: [{ maxArgs: 2 }], + errors: [ + { + endColumn: 38, + column: 29, + messageId: 'tooManyArgs', + data: { + s: 's', + amount: 2, + }, + }, + ], + }, + { + code: 'expect("something", "else", "entirely").toEqual("something");', + options: [{ maxArgs: 2, minArgs: 2 }], + errors: [ + { + endColumn: 38, + column: 29, + messageId: 'tooManyArgs', + data: { + s: 's', + amount: 2, + }, + }, + ], + }, + { + code: 'expect("something", "else", "entirely").toEqual("something");', + options: [{ maxArgs: 2, minArgs: 1 }], + errors: [ + { + endColumn: 38, + column: 29, + messageId: 'tooManyArgs', + data: { + s: 's', + amount: 2, + }, + }, + ], + }, + { + code: 'expect("something").toEqual("something");', + options: [{ minArgs: 2 }], + errors: [ + { + endColumn: 8, + column: 7, + messageId: 'notEnoughArgs', + data: { + s: 's', + amount: 2, + }, + }, ], }, { code: 'expect("something", "else").toEqual("something");', + options: [{ maxArgs: 1, minArgs: 3 }], errors: [ - { endColumn: 26, column: 21, messageId: 'incorrectNumberOfArguments' }, + { + endColumn: 8, + column: 7, + messageId: 'notEnoughArgs', + data: { + s: 's', + amount: 3, + }, + }, + { + endColumn: 26, + column: 21, + messageId: 'tooManyArgs', + data: { + s: '', + amount: 1, + }, + }, ], }, { @@ -123,7 +257,15 @@ ruleTester.run('valid-expect', rule, { code: 'expect();', errors: [ { endColumn: 9, column: 1, messageId: 'matcherNotFound' }, - { endColumn: 8, column: 7, messageId: 'incorrectNumberOfArguments' }, + { + endColumn: 8, + column: 7, + messageId: 'notEnoughArgs', + data: { + s: '', + amount: 1, + }, + }, ], }, { diff --git a/src/rules/valid-expect.ts b/src/rules/valid-expect.ts index ff7cd2ecc..45e5d7b39 100644 --- a/src/rules/valid-expect.ts +++ b/src/rules/valid-expect.ts @@ -100,14 +100,18 @@ const promiseArrayExceptionKey = ({ start, end }: TSESTree.SourceLocation) => `${start.line}:${start.column}-${end.line}:${end.column}`; type MessageIds = - | 'incorrectNumberOfArguments' + | 'tooManyArgs' + | 'notEnoughArgs' | 'modifierUnknown' | 'matcherNotFound' | 'matcherNotCalled' | 'asyncMustBeAwaited' | 'promisesWithAsyncAssertionsMustBeAwaited'; -export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({ +export default createRule< + [{ alwaysAwait?: boolean; minArgs?: number; maxArgs?: number }], + MessageIds +>({ name: __filename, meta: { docs: { @@ -116,7 +120,8 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({ recommended: 'error', }, messages: { - incorrectNumberOfArguments: 'Expect takes one and only one argument.', + tooManyArgs: 'Expect takes at most {{ amount }} argument{{ s }}.', + notEnoughArgs: 'Expect requires at least {{ amount }} argument{{ s }}.', modifierUnknown: 'Expect has no modifier named "{{ modifierName }}".', matcherNotFound: 'Expect must have a corresponding matcher call.', matcherNotCalled: 'Matchers must be called to assert.', @@ -133,13 +138,21 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({ type: 'boolean', default: false, }, + minArgs: { + type: 'number', + minimum: 1, + }, + maxArgs: { + type: 'number', + minimum: 1, + }, }, additionalProperties: false, }, ], }, - defaultOptions: [{ alwaysAwait: false }], - create(context, [{ alwaysAwait }]) { + defaultOptions: [{ alwaysAwait: false, minArgs: 1, maxArgs: 1 }], + create(context, [{ alwaysAwait, minArgs = 1, maxArgs = 1 }]) { // Context state const arrayExceptions = new Set(); @@ -164,10 +177,10 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({ const { expect, modifier, matcher } = parseExpectCall(node); - if (expect.arguments.length !== 1) { + if (expect.arguments.length < minArgs) { const expectLength = getAccessorValue(expect.callee).length; - let loc: TSESTree.SourceLocation = { + const loc: TSESTree.SourceLocation = { start: { column: node.loc.start.column + expectLength, line: node.loc.start.line, @@ -178,21 +191,29 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({ }, }; - if (expect.arguments.length !== 0) { - const { start } = expect.arguments[1].loc; - const { end } = expect.arguments[node.arguments.length - 1].loc; - - loc = { - start, - end: { - column: end.column - 1, - line: end.line, - }, - }; - } + context.report({ + messageId: 'notEnoughArgs', + data: { amount: minArgs, s: minArgs === 1 ? '' : 's' }, + node, + loc, + }); + } + + if (expect.arguments.length > maxArgs) { + const { start } = expect.arguments[maxArgs].loc; + const { end } = expect.arguments[node.arguments.length - 1].loc; + + const loc = { + start, + end: { + column: end.column - 1, + line: end.line, + }, + }; context.report({ - messageId: 'incorrectNumberOfArguments', + messageId: 'tooManyArgs', + data: { amount: maxArgs, s: maxArgs === 1 ? '' : 's' }, node, loc, });