diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c52141aa..dbcad78f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# [23.5.0](https://github.com/jest-community/eslint-plugin-jest/compare/v23.4.0...v23.5.0) (2020-01-12) + +### Features + +- **expect-expect:** support glob patterns for assertFunctionNames + ([#509](https://github.com/jest-community/eslint-plugin-jest/issues/509)) + ([295ca9a](https://github.com/jest-community/eslint-plugin-jest/commit/295ca9a6969c77fadaa1a42d76e89cae992520a6)) +- **valid-expect:** refactor `valid-expect` linting messages + ([#501](https://github.com/jest-community/eslint-plugin-jest/issues/501)) + ([7338362](https://github.com/jest-community/eslint-plugin-jest/commit/7338362420eb4970f99be2016bb4ded5732797e3)) + # [23.4.0](https://github.com/jest-community/eslint-plugin-jest/compare/v23.3.0...v23.4.0) (2020-01-10) ### Features diff --git a/docs/rules/expect-expect.md b/docs/rules/expect-expect.md index 258114cad..43441a279 100644 --- a/docs/rules/expect-expect.md +++ b/docs/rules/expect-expect.md @@ -42,7 +42,9 @@ it('should work with callbacks/async', () => { ### `assertFunctionNames` -This array option whitelists the assertion function names to look for. +This array option whitelists the assertion function names to look for. Function +names can be a glob pattern like `request.*.expect` (see +[micromatch](https://github.com/micromatch/micromatch) for syntax) Examples of **incorrect** code for the `{ "assertFunctionNames": ["expect"] }` option: @@ -78,10 +80,10 @@ test('returns sum', () => Examples of **correct** code for working with the HTTP assertions library [SuperTest](https://www.npmjs.com/package/supertest) with the -`{ "assertFunctionNames": ["expect", "request.get.expect"] }` option: +`{ "assertFunctionNames": ["expect", "request.*.expect"] }` option: ```js -/* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "request.get.expect"] }] */ +/* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "request.*.expect"] }] */ const request = require('supertest'); const express = require('express'); diff --git a/package.json b/package.json index 3aa6d5f66..0657c310b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-jest", - "version": "23.4.0", + "version": "23.5.0", "description": "Eslint rules for Jest", "repository": "jest-community/eslint-plugin-jest", "license": "MIT", @@ -37,7 +37,8 @@ "typecheck": "tsc -p ." }, "dependencies": { - "@typescript-eslint/experimental-utils": "^2.5.0" + "@typescript-eslint/experimental-utils": "^2.5.0", + "micromatch": "^4.0.2" }, "devDependencies": { "@babel/cli": "^7.4.4", @@ -50,6 +51,7 @@ "@semantic-release/git": "^7.0.17", "@types/eslint": "^6.1.3", "@types/jest": "^24.0.15", + "@types/micromatch": "^4.0.0", "@types/node": "^12.6.6", "@typescript-eslint/eslint-plugin": "^2.5.0", "@typescript-eslint/parser": "^2.5.0", @@ -60,7 +62,7 @@ "eslint-plugin-eslint-comments": "^3.1.2", "eslint-plugin-eslint-plugin": "^2.0.0", "eslint-plugin-import": "^2.18.0", - "eslint-plugin-node": "^10.0.0", + "eslint-plugin-node": "^11.0.0", "eslint-plugin-prettier": "^3.0.0", "husky": "^3.0.9", "jest": "^24.9.0", diff --git a/renovate.json b/renovate.json index ed37e9bba..f6d03fb49 100644 --- a/renovate.json +++ b/renovate.json @@ -3,5 +3,6 @@ "config:base" ], "lockFileMaintenance": { "enabled": true }, - "rangeStrategy": "replace" + "rangeStrategy": "replace", + "ignorePresets": ["group:semantic-releaseMonorepo"] } diff --git a/src/rules/__tests__/expect-expect.test.ts b/src/rules/__tests__/expect-expect.test.ts index 59f0bc2a3..852bb393a 100644 --- a/src/rules/__tests__/expect-expect.test.ts +++ b/src/rules/__tests__/expect-expect.test.ts @@ -54,6 +54,22 @@ ruleTester.run('expect-expect', rule, { });`, options: [{ assertFunctionNames: ['tester.foo.expect'] }], }, + { + code: `test('wildcard chained function', () => { + class Foo { + expect(k) { + return k; + } + } + let tester = { + foo: function() { + return new Foo() + } + } + tester.foo().expect(123); + });`, + options: [{ assertFunctionNames: ['tester.*.expect'] }], + }, { code: `test('verifies recursive expect method call', () => { class Foo { diff --git a/src/rules/__tests__/valid-expect.test.ts b/src/rules/__tests__/valid-expect.test.ts index b75b9b0eb..becf990af 100644 --- a/src/rules/__tests__/valid-expect.test.ts +++ b/src/rules/__tests__/valid-expect.test.ts @@ -99,25 +99,31 @@ ruleTester.run('valid-expect', rule, { */ { code: 'expect().toBe(true);', - errors: [{ endColumn: 8, column: 7, messageId: 'noArgs' }], + errors: [ + { endColumn: 8, column: 7, messageId: 'incorrectNumberOfArguments' }, + ], }, { code: 'expect().toEqual("something");', - errors: [{ endColumn: 8, column: 7, messageId: 'noArgs' }], + errors: [ + { endColumn: 8, column: 7, messageId: 'incorrectNumberOfArguments' }, + ], }, { code: 'expect("something", "else").toEqual("something");', - errors: [{ endColumn: 26, column: 21, messageId: 'multipleArgs' }], + errors: [ + { endColumn: 26, column: 21, messageId: 'incorrectNumberOfArguments' }, + ], }, { code: 'expect("something");', - errors: [{ endColumn: 20, column: 1, messageId: 'noAssertions' }], + errors: [{ endColumn: 20, column: 1, messageId: 'matcherNotFound' }], }, { code: 'expect();', errors: [ - { endColumn: 9, column: 1, messageId: 'noAssertions' }, - { endColumn: 8, column: 7, messageId: 'noArgs' }, + { endColumn: 9, column: 1, messageId: 'matcherNotFound' }, + { endColumn: 8, column: 7, messageId: 'incorrectNumberOfArguments' }, ], }, { @@ -126,8 +132,7 @@ ruleTester.run('valid-expect', rule, { { endColumn: 25, column: 14, - messageId: 'matcherOnPropertyNotCalled', - data: { propertyName: 'toBeDefined' }, + messageId: 'matcherNotCalled', }, ], }, @@ -137,8 +142,7 @@ ruleTester.run('valid-expect', rule, { { endColumn: 29, column: 18, - messageId: 'matcherOnPropertyNotCalled', - data: { propertyName: 'toBeDefined' }, + messageId: 'matcherNotCalled', }, ], }, @@ -148,8 +152,8 @@ ruleTester.run('valid-expect', rule, { { endColumn: 18, column: 14, - messageId: 'invalidProperty', - data: { propertyName: 'nope' }, + messageId: 'modifierUnknown', + data: { modifierName: 'nope' }, }, ], }, @@ -159,8 +163,7 @@ ruleTester.run('valid-expect', rule, { { endColumn: 22, column: 14, - messageId: 'propertyWithoutMatcher', - data: { propertyName: 'resolves' }, + messageId: 'matcherNotFound', }, ], }, @@ -170,8 +173,7 @@ ruleTester.run('valid-expect', rule, { { endColumn: 21, column: 14, - messageId: 'propertyWithoutMatcher', - data: { propertyName: 'rejects' }, + messageId: 'matcherNotFound', }, ], }, @@ -181,8 +183,7 @@ ruleTester.run('valid-expect', rule, { { endColumn: 17, column: 14, - messageId: 'propertyWithoutMatcher', - data: { propertyName: 'not' }, + messageId: 'matcherNotFound', }, ], }, @@ -538,8 +539,7 @@ ruleTester.run('valid-expect', rule, { { column: 37, endColumn: 41, - messageId: 'matcherOnPropertyNotCalled', - data: { propertyName: 'toBe' }, + messageId: 'matcherNotCalled', }, ], }, @@ -582,7 +582,7 @@ ruleTester.run('valid-expect', rule, { code: `test("valid-expect", async () => { await expect(Promise.resolve(1)); });`, - errors: [{ endColumn: 41, column: 15, messageId: 'noAssertions' }], + errors: [{ endColumn: 41, column: 15, messageId: 'matcherNotFound' }], }, ], }); diff --git a/src/rules/expect-expect.ts b/src/rules/expect-expect.ts index 5dd5374ca..258b6b4ee 100644 --- a/src/rules/expect-expect.ts +++ b/src/rules/expect-expect.ts @@ -7,6 +7,7 @@ import { AST_NODE_TYPES, TSESTree, } from '@typescript-eslint/experimental-utils'; +import micromatch from 'micromatch'; import { TestCaseName, createRule, @@ -74,7 +75,7 @@ export default createRule< const name = getNodeName(node.callee); if (name === TestCaseName.it || name === TestCaseName.test) { unchecked.push(node); - } else if (name && assertFunctionNames.includes(name)) { + } else if (name && micromatch.isMatch(name, assertFunctionNames)) { // Return early in case of nested `it` statements. checkCallExpressionUsed(context.getAncestors()); } diff --git a/src/rules/valid-expect.ts b/src/rules/valid-expect.ts index c5a587981..ff7cd2ecc 100644 --- a/src/rules/valid-expect.ts +++ b/src/rules/valid-expect.ts @@ -100,12 +100,10 @@ const promiseArrayExceptionKey = ({ start, end }: TSESTree.SourceLocation) => `${start.line}:${start.column}-${end.line}:${end.column}`; type MessageIds = - | 'multipleArgs' - | 'noArgs' - | 'noAssertions' - | 'invalidProperty' - | 'propertyWithoutMatcher' - | 'matcherOnPropertyNotCalled' + | 'incorrectNumberOfArguments' + | 'modifierUnknown' + | 'matcherNotFound' + | 'matcherNotCalled' | 'asyncMustBeAwaited' | 'promisesWithAsyncAssertionsMustBeAwaited'; @@ -118,13 +116,10 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({ recommended: 'error', }, messages: { - multipleArgs: 'More than one argument was passed to expect().', - noArgs: 'No arguments were passed to expect().', - noAssertions: 'No assertion was called on expect().', - invalidProperty: - '"{{ propertyName }}" is not a valid property of expect.', - propertyWithoutMatcher: '"{{ propertyName }}" needs to call a matcher.', - matcherOnPropertyNotCalled: '"{{ propertyName }}" was not called.', + incorrectNumberOfArguments: 'Expect takes one and only one argument.', + modifierUnknown: 'Expect has no modifier named "{{ modifierName }}".', + matcherNotFound: 'Expect must have a corresponding matcher call.', + matcherNotCalled: 'Matchers must be called to assert.', asyncMustBeAwaited: 'Async assertions must be awaited{{ orReturned }}.', promisesWithAsyncAssertionsMustBeAwaited: 'Promises which return async assertions must be awaited{{ orReturned }}.', @@ -169,37 +164,37 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({ const { expect, modifier, matcher } = parseExpectCall(node); - if (expect.arguments.length > 1) { - const secondArgumentLocStart = expect.arguments[1].loc.start; - const lastArgumentLocEnd = - expect.arguments[node.arguments.length - 1].loc.end; + if (expect.arguments.length !== 1) { + const expectLength = getAccessorValue(expect.callee).length; - context.report({ - loc: { - end: { - column: lastArgumentLocEnd.column - 1, - line: lastArgumentLocEnd.line, - }, - start: secondArgumentLocStart, + let loc: TSESTree.SourceLocation = { + start: { + column: node.loc.start.column + expectLength, + line: node.loc.start.line, }, - messageId: 'multipleArgs', - node, - }); - } else if (expect.arguments.length === 0) { - const expectLength = getAccessorValue(expect.callee).length; - context.report({ - loc: { + end: { + column: node.loc.start.column + expectLength + 1, + line: node.loc.start.line, + }, + }; + + if (expect.arguments.length !== 0) { + const { start } = expect.arguments[1].loc; + const { end } = expect.arguments[node.arguments.length - 1].loc; + + loc = { + start, end: { - column: node.loc.start.column + expectLength + 1, - line: node.loc.start.line, - }, - start: { - column: node.loc.start.column + expectLength, - line: node.loc.start.line, + column: end.column - 1, + line: end.line, }, - }, - messageId: 'noArgs', + }; + } + + context.report({ + messageId: 'incorrectNumberOfArguments', node, + loc, }); } @@ -207,8 +202,7 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({ if (!matcher) { if (modifier) { context.report({ - data: { propertyName: modifier.name }, // todo: rename to 'modifierName' - messageId: 'propertyWithoutMatcher', // todo: rename to 'modifierWithoutMatcher' + messageId: 'matcherNotFound', node: modifier.node.property, }); } @@ -218,8 +212,8 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({ if (matcher.node.parent && isExpectMember(matcher.node.parent)) { context.report({ - messageId: 'invalidProperty', // todo: rename to 'invalidModifier' - data: { propertyName: matcher.name }, // todo: rename to 'matcherName' (or modifierName?) + messageId: 'modifierUnknown', + data: { modifierName: matcher.name }, node: matcher.node.property, }); @@ -228,8 +222,7 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({ if (!matcher.arguments) { context.report({ - data: { propertyName: matcher.name }, // todo: rename to 'matcherName' - messageId: 'matcherOnPropertyNotCalled', // todo: rename to 'matcherNotCalled' + messageId: 'matcherNotCalled', node: matcher.node.property, }); } @@ -287,7 +280,7 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({ // nothing called on "expect()" 'CallExpression:exit'(node: TSESTree.CallExpression) { if (isExpectCall(node) && isNoAssertionsParentNode(node.parent)) { - context.report({ messageId: 'noAssertions', node }); + context.report({ messageId: 'matcherNotFound', node }); } }, }; diff --git a/yarn.lock b/yarn.lock index 791f0cdf5..4f5248f4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1221,6 +1221,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/braces@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.0.tgz#7da1c0d44ff1c7eb660a36ec078ea61ba7eb42cb" + integrity sha512-TbH79tcyi9FHwbyboOKeRachRq63mSuWYXOflsNO9ZyE5ClQ/JaozNKl+aWUq87qPNsXasXxi2AbgfwIJ+8GQw== + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -1290,6 +1295,13 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== +"@types/micromatch@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.0.tgz#c57e0b11518c930465ce923f44342e1bb4bef309" + integrity sha512-bavSCssCRRlbUI639WG0Y30AOowkI5CdxyyrC5eVbsb0BJIbgS5ROfwlwDYHsOmgS59iYlre9sstIA5wfVNKBA== + dependencies: + "@types/braces" "*" + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -3029,12 +3041,12 @@ eslint-module-utils@^2.4.1: debug "^2.6.9" pkg-dir "^2.0.0" -eslint-plugin-es@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-2.0.0.tgz#0f5f5da5f18aa21989feebe8a73eadefb3432976" - integrity sha512-f6fceVtg27BR02EYnBhgWLFQfK6bN4Ll0nQFrBHOlCsAyxeZkn0NHns5O0YZOPrV1B3ramd6cgFwaoFLcSkwEQ== +eslint-plugin-es@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.0.tgz#98cb1bc8ab0aa807977855e11ad9d1c9422d014b" + integrity sha512-6/Jb/J/ZvSebydwbBJO1R9E5ky7YeElfK56Veh7e4QGFHCXoIXGH9HhVz+ibJLM3XJ1XjP+T7rKBLUa/Y7eIng== dependencies: - eslint-utils "^1.4.2" + eslint-utils "^2.0.0" regexpp "^3.0.0" eslint-plugin-eslint-comments@^3.1.2: @@ -3068,13 +3080,13 @@ eslint-plugin-import@^2.18.0: read-pkg-up "^2.0.0" resolve "^1.12.0" -eslint-plugin-node@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-10.0.0.tgz#fd1adbc7a300cf7eb6ac55cf4b0b6fc6e577f5a6" - integrity sha512-1CSyM/QCjs6PXaT18+zuAXsjXGIGo5Rw630rSKwokSs2jrYURQc4R5JZpoanNCqwNmepg+0eZ9L7YiRUJb8jiQ== +eslint-plugin-node@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.0.0.tgz#365944bb0804c5d1d501182a9bc41a0ffefed726" + integrity sha512-chUs/NVID+sknFiJzxoN9lM7uKSOEta8GC8365hw1nDfwIPIjjpRSwwPvQanWv8dt/pDe9EV4anmVSwdiSndNg== dependencies: - eslint-plugin-es "^2.0.0" - eslint-utils "^1.4.2" + eslint-plugin-es "^3.0.0" + eslint-utils "^2.0.0" ignore "^5.1.1" minimatch "^3.0.4" resolve "^1.10.1" @@ -3103,13 +3115,20 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.4.2, eslint-utils@^1.4.3: +eslint-utils@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== dependencies: eslint-visitor-keys "^1.1.0" +eslint-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd" + integrity sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA== + dependencies: + eslint-visitor-keys "^1.1.0" + eslint-visitor-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"