eslint-plugin-jest 29.15.5 → 29.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,3 +41,65 @@ expect('hello world').toBe('hello sunshine');
41
41
 
42
42
  expect(new Promise(r => r(0))).rejects.toThrow('oh noes!');
43
43
  ```
44
+
45
+ ## Options
46
+
47
+ ```json
48
+ {
49
+ "jest/valid-expect-with-promise": [
50
+ "error",
51
+ {
52
+ "checkThenables": false
53
+ }
54
+ ]
55
+ }
56
+ ```
57
+
58
+ ### `checkThenables`
59
+
60
+ Default: `false`
61
+
62
+ A
63
+ ["Thenable"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables)
64
+ value is an object which has a `then` method, such as a `Promise`. Other
65
+ Thenables include TypeScript's built-in `PromiseLike` interface and any custom
66
+ object that happens to have a `.then()`.
67
+
68
+ The `checkThenables` option triggers `valid-expect-with-promise` to also
69
+ consider all values that satisfy the Thenable shape (a `.then()` method that
70
+ takes two callback parameters), not just Promises. This can be useful if your
71
+ code works with older `Promise` polyfills instead of the native `Promise` class,
72
+ or in environments where the global `Promise` is not declared by TypeScript's
73
+ default libraries (such as with `noLib`).
74
+
75
+ Examples of **incorrect** code when `checkThenables` is `true`:
76
+
77
+ ```ts
78
+ declare function createPromiseLike(): PromiseLike<string>;
79
+
80
+ expect(createPromiseLike()).toBe('hello sunshine');
81
+
82
+ interface MyThenable {
83
+ then(onFulfilled: () => void, onRejected: () => void): MyThenable;
84
+ }
85
+
86
+ declare function createMyThenable(): MyThenable;
87
+
88
+ expect(createMyThenable()).toBe('hello sunshine');
89
+ ```
90
+
91
+ Examples of **correct** code when `checkThenables` is `true`:
92
+
93
+ ```ts
94
+ declare function createPromiseLike(): PromiseLike<string>;
95
+
96
+ await expect(createPromiseLike()).resolves.toBe('hello sunshine');
97
+
98
+ interface MyThenable {
99
+ then(onFulfilled: () => void, onRejected: () => void): MyThenable;
100
+ }
101
+
102
+ declare function createMyThenable(): MyThenable;
103
+
104
+ await expect(createMyThenable()).resolves.toBe('hello sunshine');
105
+ ```
@@ -4,8 +4,16 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.isBuiltinSymbolLike = isBuiltinSymbolLike;
7
+ exports.isThenableType = isThenableType;
7
8
  let SymbolFlags;
8
9
  let TypeFlags;
10
+ function loadTypeScriptFlags() {
11
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
12
+ ({
13
+ TypeFlags,
14
+ SymbolFlags
15
+ } = require('typescript'));
16
+ }
9
17
  function isSymbolFromDefaultLibrary(program, symbol) {
10
18
  /* istanbul ignore next */
11
19
  const declarations = symbol.getDeclarations() ?? [];
@@ -22,11 +30,7 @@ function isSymbolFromDefaultLibrary(program, symbol) {
22
30
  return false;
23
31
  }
24
32
  function isBuiltinSymbolLike(program, type, symbolName) {
25
- // eslint-disable-next-line @typescript-eslint/no-require-imports
26
- ({
27
- TypeFlags,
28
- SymbolFlags
29
- } = require('typescript'));
33
+ loadTypeScriptFlags();
30
34
  return isBuiltinSymbolLikeRecurser(program, type, subType => {
31
35
  const symbol = subType.getSymbol();
32
36
  if (!symbol) {
@@ -39,6 +43,33 @@ function isBuiltinSymbolLike(program, type, symbolName) {
39
43
  return null;
40
44
  });
41
45
  }
46
+
47
+ /**
48
+ * Checks if the given type is thenable: it has a `then` method that accepts
49
+ * both a fulfillment and a rejection callback, mirroring the check performed
50
+ * by typescript-eslint's `no-floating-promises` rule.
51
+ */
52
+ function isThenableType(program, node, type) {
53
+ loadTypeScriptFlags();
54
+ const checker = program.getTypeChecker();
55
+ return isBuiltinSymbolLikeRecurser(program, type, subType => {
56
+ const then = subType.getProperty('then');
57
+ if (!then) {
58
+ return false;
59
+ }
60
+ return unionParts(checker.getTypeOfSymbolAtLocation(then, node)).some(thenType => thenType.getCallSignatures().some(signature => {
61
+ const [onFulfilled, onRejected] = signature.getParameters();
62
+ return onFulfilled !== undefined && onRejected !== undefined && isFunctionParam(checker, node, onFulfilled) && isFunctionParam(checker, node, onRejected);
63
+ }));
64
+ });
65
+ }
66
+ function isFunctionParam(checker, node, param) {
67
+ const paramType = checker.getApparentType(checker.getTypeOfSymbolAtLocation(param, node));
68
+ return unionParts(paramType).some(subType => subType.getCallSignatures().length > 0);
69
+ }
70
+ function unionParts(type) {
71
+ return type.isUnion() ? type.types : [type];
72
+ }
42
73
  function isBuiltinSymbolLikeRecurser(program, type, predicate) {
43
74
  if (type.isIntersection()) {
44
75
  return type.types.some(t => isBuiltinSymbolLikeRecurser(program, t, predicate));
@@ -18,10 +18,22 @@ var _default = exports.default = (0, _utils2.createRule)({
18
18
  unneededRejectResolve: 'Subject is not a promise so {{ modifier }} is not needed'
19
19
  },
20
20
  type: 'suggestion',
21
- schema: []
21
+ schema: [{
22
+ type: 'object',
23
+ properties: {
24
+ checkThenables: {
25
+ type: 'boolean'
26
+ }
27
+ },
28
+ additionalProperties: false
29
+ }]
22
30
  },
23
- defaultOptions: [],
24
- create(context) {
31
+ defaultOptions: [{
32
+ checkThenables: false
33
+ }],
34
+ create(context, [{
35
+ checkThenables
36
+ }]) {
25
37
  const services = _utils.ESLintUtils.getParserServices(context);
26
38
  return {
27
39
  CallExpression(node) {
@@ -30,7 +42,12 @@ var _default = exports.default = (0, _utils2.createRule)({
30
42
  return;
31
43
  }
32
44
  const [argument] = jestFnCall.head.node.parent.arguments;
33
- const isPromiseLike = (0, _utils2.isBuiltinSymbolLike)(services.program, services.getTypeAtLocation(argument), 'Promise');
45
+ const argumentType = services.getTypeAtLocation(argument);
46
+
47
+ // `isBuiltinSymbolLike` only recognises the `Promise` declared in
48
+ // TypeScript's default libraries, so optionally check for any thenable
49
+ // (e.g. custom promise types in `noLib` environments, or polyfills)
50
+ const isPromiseLike = (0, _utils2.isBuiltinSymbolLike)(services.program, argumentType, 'Promise') || checkThenables === true && (0, _utils2.isThenableType)(services.program, services.esTreeNodeToTSNodeMap.get(argument), argumentType);
34
51
  const promiseModifier = jestFnCall.modifiers.find(nod => (0, _utils2.getAccessorValue)(nod) !== 'not');
35
52
  if (isPromiseLike && !promiseModifier) {
36
53
  context.report({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-jest",
3
- "version": "29.15.5",
3
+ "version": "29.16.0",
4
4
  "description": "ESLint rules for Jest",
5
5
  "keywords": [
6
6
  "eslint",
@@ -138,7 +138,7 @@
138
138
  "optional": true
139
139
  }
140
140
  },
141
- "packageManager": "yarn@4.17.0",
141
+ "packageManager": "yarn@4.17.1",
142
142
  "engines": {
143
143
  "node": "^20.12.0 || ^22.0.0 || >=24.0.0"
144
144
  }