eslint-plugin-no-mistakes 0.98.0 → 0.100.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.
Files changed (81) hide show
  1. package/README.md +24 -1
  2. package/package.json +20 -8
  3. package/src/adjacent-annotation.js +72 -0
  4. package/src/index.js +1 -0
  5. package/src/rules/async-call-disposition.js +41 -21
  6. package/src/rules/async-consumer-cache-proof.js +146 -0
  7. package/src/rules/async-consumer-copy-spellings.js +82 -0
  8. package/src/rules/async-consumer-imported-proof.js +96 -0
  9. package/src/rules/async-consumer-local-proof.js +86 -0
  10. package/src/rules/async-consumer-proof.js +113 -0
  11. package/src/rules/async-consumer-property-carrier.js +136 -0
  12. package/src/rules/async-consumer-property-proof.js +192 -0
  13. package/src/rules/async-consumer-receivers.js +161 -0
  14. package/src/rules/async-consumer-typed-proof.js +96 -0
  15. package/src/rules/async-disposition-callbacks.js +140 -0
  16. package/src/rules/async-disposition-consumers.js +86 -22
  17. package/src/rules/async-disposition-expressions.js +80 -62
  18. package/src/rules/async-disposition-flow.js +23 -1
  19. package/src/rules/async-disposition-observers.js +95 -0
  20. package/src/rules/async-handoff-data.js +168 -0
  21. package/src/rules/async-handoff-return.js +38 -0
  22. package/src/rules/async-handoff-safety.js +184 -0
  23. package/src/rules/async-handoff-sibling.js +41 -0
  24. package/src/rules/async-handoff-snapshots.js +33 -0
  25. package/src/rules/async-observer-mutations.js +143 -0
  26. package/src/rules/async-promise-handoff.js +60 -0
  27. package/src/rules/async-schema.js +23 -1
  28. package/src/rules/async-source-candidates.js +35 -0
  29. package/src/rules/async-source-declarations.js +114 -0
  30. package/src/rules/async-source-files.js +42 -0
  31. package/src/rules/async-source-owner.js +75 -0
  32. package/src/rules/async-source-session.js +91 -0
  33. package/src/rules/async-source-slots.js +58 -0
  34. package/src/rules/async-source-store.js +45 -0
  35. package/src/rules/async-source-ts-declarations.js +184 -0
  36. package/src/rules/async-source-ts-expressions.js +201 -0
  37. package/src/rules/async-source-ts-parser.js +94 -0
  38. package/src/rules/async-source-ts-references.js +82 -0
  39. package/src/rules/async-source-ts-scopes.js +153 -0
  40. package/src/rules/async-source-ts-statements.js +102 -0
  41. package/src/rules/async-source-ts-templates.js +20 -0
  42. package/src/rules/async-source-ts-types.js +49 -0
  43. package/src/rules/async-source-types.js +172 -0
  44. package/src/rules/async-target-bindings.js +22 -0
  45. package/src/rules/async-target-sources.js +27 -0
  46. package/src/rules/async-targets.js +3 -22
  47. package/src/rules/no-import-only-test-files.js +45 -7
  48. package/src/rules/no-vitest-sequential.js +9 -1
  49. package/src/rules/playwright-no-swallowed-visibility.js +166 -0
  50. package/src/rules/playwright-test-timeout-bindings.js +2 -0
  51. package/src/rules/postgres-executor-calls.js +36 -3
  52. package/src/rules/postgres-executor.js +12 -4
  53. package/src/rules/postgres-no-correlated-sql-builder-append.js +19 -65
  54. package/src/rules/postgres-no-correlated-sql-builder-invocation.js +55 -0
  55. package/src/rules/postgres-no-correlated-sql-builder.js +12 -8
  56. package/src/rules/postgres-no-executor-adapter-binding.js +1 -0
  57. package/src/rules/postgres-no-executor-adapter-collection.js +123 -0
  58. package/src/rules/postgres-no-executor-adapter-escape.js +163 -0
  59. package/src/rules/postgres-no-executor-adapter-mutation.js +155 -0
  60. package/src/rules/postgres-no-executor-adapter-option-local.js +78 -0
  61. package/src/rules/postgres-no-executor-adapter-option-ts.js +173 -0
  62. package/src/rules/postgres-no-executor-adapter-options.js +46 -0
  63. package/src/rules/postgres-no-executor-adapter-ref.js +15 -0
  64. package/src/rules/postgres-no-executor-adapter-source.js +100 -10
  65. package/src/rules/postgres-no-executor-adapter-target.js +62 -19
  66. package/src/rules/postgres-no-executor-adapter.js +7 -2
  67. package/src/rules/postgres-no-manual-transaction.js +5 -3
  68. package/src/rules/postgres-no-nonliteral-sql-registry-key.js +2 -10
  69. package/src/rules/postgres-scoped-executors.js +20 -52
  70. package/src/rules/postgres-scoped-lookup.js +32 -0
  71. package/src/rules/postgres-scoped-parameters.js +83 -0
  72. package/src/rules/postgres-transaction-statements.js +3 -3
  73. package/src/rules/react-no-nullish-react-node.js +53 -27
  74. package/src/rules/test-message-expect.js +12 -6
  75. package/src/rules/test-message-runner.js +106 -0
  76. package/src/rules/test-no-error-message-matching.js +3 -15
  77. package/src/rules/test-no-skips-bindings.js +28 -5
  78. package/src/rules/vitest-mock-test-file-naming.js +36 -3
  79. package/src/rules/vitest-timeout-cap.js +15 -0
  80. package/src/rules/vitest-timeout-reasons.js +19 -0
  81. package/src/rules/vitest-timeout-values.js +5 -1
package/README.md CHANGED
@@ -9,6 +9,24 @@ callers, fetches, or selectors behind aliases and dynamic values. See
9
9
  npm install --save-dev eslint-plugin-no-mistakes
10
10
  ```
11
11
 
12
+ For `async-call-disposition` to inspect imported callback implementations when
13
+ the linter does not provide a TypeScript program (including Oxlint's
14
+ `--type-aware` mode), also install `typescript` in your project:
15
+
16
+ ```sh
17
+ npm install --save-dev typescript
18
+ ```
19
+
20
+ `typescript` and `@typescript-eslint/parser` are optional peers, but only
21
+ `typescript` is required for the cross-file proof. When the ESLint parser is
22
+ absent, the proof parses supported syntax with `ts.createSourceFile`; unsupported
23
+ syntax stays unproven. This fallback requires TypeScript 5 or
24
+ later for Bundler module resolution. With older TypeScript or missing or
25
+ incompatible tooling, calls that rely on this cross-file proof are reported as
26
+ unproven. Same-file proofs and explicitly configured awaiting-consumer
27
+ contracts continue to work. When ESLint provides a typed TypeScript program,
28
+ the rule continues to use that program for its typed proof.
29
+
12
30
  Configure the plugin in a flat config and scope rules with `files` globs. For
13
31
  example, this checks test files for skipped or exclusive tests while allowing
14
32
  `todo`:
@@ -32,7 +50,9 @@ for the complete list.
32
50
  To cap long Vitest timeouts, enable `no-mistakes/vitest-timeout-cap` in the
33
51
  test and Vitest config files selected by your flat-config globs. It defaults to
34
52
  5,000 ms for configured test and hook timeouts and 30,000 ms for per-test and
35
- hook overrides. See the [rule documentation](../../docs/eslint-rules/vitest-timeout-cap.md)
53
+ hook overrides. Overrides above the default cap require an adjacent
54
+ `// no-mistakes-timeout-reason: <specific reason>` comment.
55
+ See the [rule documentation](../../docs/eslint-rules/vitest-timeout-cap.md)
36
56
  for options and examples.
37
57
 
38
58
  `gh-api-argument-safety` is an opt-in rule for structured endpoint fields and explicit API methods.
@@ -47,6 +67,9 @@ See [options and examples](../../docs/eslint-rules/no-comment-phrases.md).
47
67
  `forbidden-member-chains` is an opt-in rule for configured static member-access chains.
48
68
  See [options and examples](../../docs/eslint-rules/forbidden-member-chains.md).
49
69
 
70
+ `playwright-no-swallowed-visibility` is an opt-in rule for Playwright state checks whose rejection becomes a boolean literal.
71
+ See [options and examples](../../docs/eslint-rules/playwright-no-swallowed-visibility.md).
72
+
50
73
  `no-object-mutation` is an opt-in rule for shared object writes and mutating calls.
51
74
  See [options and examples](../../docs/eslint-rules/no-object-mutation.md).
52
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-no-mistakes",
3
- "version": "0.98.0",
3
+ "version": "0.100.0",
4
4
  "description": "ESLint and Oxlint rules that keep code static enough for no-mistakes analyzers and coding agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,16 +21,28 @@
21
21
  "test": "vitest run --coverage"
22
22
  },
23
23
  "devDependencies": {
24
- "@typescript-eslint/parser": "^8.71.1",
25
- "@vitest/coverage-v8": "^5.0.4",
26
- "eslint": "^10.12.0",
27
- "oxlint": "^1.87.0",
28
- "vitest": "^5.0.4"
24
+ "@typescript-eslint/parser": "^8.72.0",
25
+ "@vitest/coverage-v8": "^5.0.5",
26
+ "eslint": "^10.13.0",
27
+ "oxlint": "^1.89.0",
28
+ "typescript": "^6.0.3",
29
+ "vitest": "^5.0.5"
29
30
  },
30
31
  "peerDependencies": {
31
- "eslint": ">=9"
32
+ "@typescript-eslint/parser": ">=8",
33
+ "eslint": ">=9",
34
+ "typescript": ">=5"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "@typescript-eslint/parser": {
38
+ "optional": true
39
+ },
40
+ "typescript": {
41
+ "optional": true
42
+ }
32
43
  },
33
44
  "engines": {
34
45
  "node": "^20.19.0 || ^22.13.0 || >=24"
35
- }
46
+ },
47
+ "//": "typescript stays on 6.0.3. TypeScript 7 makes the typescript-eslint parser throw on load. https://github.com/typescript-eslint/typescript-eslint/issues/12518 Related: https://github.com/typescript-eslint/typescript-eslint/issues/10940"
36
48
  }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+
3
+ // Parentheses required by a parent construct are not grouping around the target.
4
+ function syntaxParen(sourceCode, node) {
5
+ // ESTree wraps an optional registration without introducing any syntax.
6
+ while (node.parent?.type === "ChainExpression") node = node.parent;
7
+ const parent = node.parent;
8
+ if (!parent) return null;
9
+ if (["CallExpression", "NewExpression"].includes(parent.type)) {
10
+ return sourceCode.getTokenAfter(
11
+ parent.typeArguments || parent.typeParameters || parent.callee,
12
+ {
13
+ filter: (token) => token.value === "(",
14
+ },
15
+ );
16
+ }
17
+ if (
18
+ [
19
+ "IfStatement",
20
+ "WhileStatement",
21
+ "SwitchStatement",
22
+ "WithStatement",
23
+ "ImportExpression",
24
+ ].includes(parent.type)
25
+ )
26
+ return sourceCode.getTokenAfter(sourceCode.getFirstToken(parent));
27
+ if (parent.type === "DoWhileStatement")
28
+ return sourceCode.getTokenAfter(sourceCode.getTokenAfter(parent.body));
29
+ return null;
30
+ }
31
+
32
+ // Only comments, whitespace and grouping may separate an annotation and its target.
33
+ // A comment trailing another statement belongs to that statement, never the target.
34
+ function adjacentAnnotations(sourceCode, node, marker) {
35
+ const comments = [];
36
+ let left = node;
37
+ let right = node;
38
+ const boundary = syntaxParen(sourceCode, node);
39
+ for (;;) {
40
+ const previous = sourceCode.getTokenBefore(left, { includeComments: true });
41
+ if (!previous || left.loc.start.line - previous.loc.end.line > 1) break;
42
+ if (["Line", "Block"].includes(previous.type)) {
43
+ comments.push(previous);
44
+ left = previous;
45
+ continue;
46
+ }
47
+ const closing = sourceCode.getTokenAfter(right);
48
+ if (
49
+ previous.value !== "(" ||
50
+ closing?.value !== ")" ||
51
+ previous.range[0] === boundary?.range[0]
52
+ )
53
+ break;
54
+ left = previous;
55
+ right = closing;
56
+ }
57
+ const result = [];
58
+ for (const comment of comments) {
59
+ const previous = sourceCode.getTokenBefore(comment);
60
+ if (
61
+ previous?.loc.end.line === comment.loc.start.line &&
62
+ !["(", "[", "{", ",", ":"].includes(previous.value)
63
+ )
64
+ break;
65
+ for (const text of comment.value.split(/\r?\n/)) {
66
+ const normalized = text.replace(/^\s*\*?\s?/, "");
67
+ if (normalized.startsWith(marker)) result.push(normalized.slice(marker.length).trim());
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+ module.exports = { adjacentAnnotations };
package/src/index.js CHANGED
@@ -34,6 +34,7 @@ const rules = {
34
34
  "playwright-no-empty": require("./rules/playwright-no-empty"),
35
35
  "playwright-no-hoisted-unique-token": require("./rules/playwright-no-hoisted-unique-token"),
36
36
  "playwright-no-raw-scroll-pagination": require("./rules/playwright-no-raw-scroll-pagination"),
37
+ "playwright-no-swallowed-visibility": require("./rules/playwright-no-swallowed-visibility"),
37
38
  "playwright-prefer-get-by-test-id": require("./rules/playwright-prefer-get-by-test-id"),
38
39
  "playwright-require-exported-component-attribute": require("./rules/playwright-require-exported-component-attribute"),
39
40
  "playwright-require-interactive-test-id": require("./rules/playwright-require-interactive-test-id"),
@@ -5,37 +5,36 @@ const { ruleSuppression } = require("../rule-suppression");
5
5
  const {
6
6
  collectedPromise,
7
7
  isPromiseAllCall,
8
- retainedCollection,
9
8
  retainedPromise,
10
9
  } = require("./async-disposition-bindings");
11
10
  const {
12
11
  directlyDisposed,
12
+ mappedArgumentDisposed,
13
13
  observedExpression,
14
14
  promiseAllDisposed,
15
15
  mappedPromise,
16
16
  } = require("./async-disposition-expressions");
17
17
  const { dispositionOptionsSchema } = require("./async-schema");
18
18
  const { createTargetMatcher } = require("./async-targets");
19
+ const { promiseChainFrom } = require("./async-disposition-observers");
19
20
  const { createAwaitingConsumers } = require("./async-disposition-consumers");
21
+ const { createPromiseHandoff } = require("./async-promise-handoff");
22
+ const { possibleValueType } = require("./async-source-candidates");
23
+ const { sourcesUnavailable, withSourceOwner } = require("./async-source-owner");
20
24
 
21
- function isDisposed(node, context, returnsThroughConsumer, seen = new Set()) {
25
+ function isDisposed(node, context, consumers, seen = new Set(), catchProven = true) {
22
26
  if (seen.has(node)) return false;
23
27
  seen.add(node);
24
28
  const observed = observedExpression(node, context);
25
- const disposed = (value) => isDisposed(value, context, returnsThroughConsumer, seen);
29
+ const retainedCatch =
30
+ catchProven && (observed === node || promiseChainFrom(observed, node, context));
31
+ const disposed = (value) => isDisposed(value, context, consumers, seen, retainedCatch);
26
32
  return (
27
- directlyDisposed(node, context, returnsThroughConsumer) ||
28
- directlyDisposed(observed, context, returnsThroughConsumer) ||
29
- promiseAllDisposed(observed, context, returnsThroughConsumer) ||
33
+ directlyDisposed(node, context, consumers) ||
34
+ directlyDisposed(observed, context, consumers, catchProven ? node : null) ||
35
+ promiseAllDisposed(observed, context, consumers) ||
30
36
  retainedPromise(observed, context, disposed) ||
31
- mappedPromise(observed, (value) =>
32
- retainedCollection(
33
- value,
34
- context,
35
- (aggregate) => isPromiseAllCall(aggregate, context),
36
- disposed,
37
- ),
38
- ) ||
37
+ mappedPromise(observed, context, consumers, disposed) ||
39
38
  collectedPromise(
40
39
  observed,
41
40
  context,
@@ -46,7 +45,7 @@ function isDisposed(node, context, returnsThroughConsumer, seen = new Set()) {
46
45
  );
47
46
  }
48
47
 
49
- module.exports = rule(
48
+ const dispositionRule = rule(
50
49
  {
51
50
  type: "problem",
52
51
  docs: {
@@ -63,7 +62,24 @@ module.exports = rule(
63
62
  },
64
63
  (context) => {
65
64
  const matcher = createTargetMatcher(context);
66
- const consumers = createAwaitingConsumers(context);
65
+ const sourceFacts = {
66
+ typed: new WeakMap(),
67
+ sources: sourcesUnavailable(context) ? false : null,
68
+ targets: matcher.targets,
69
+ };
70
+ const { promiseSlotHandoff, promiseTypedTargetCall } = createPromiseHandoff(
71
+ context,
72
+ sourceFacts,
73
+ );
74
+ const consumers = createAwaitingConsumers(
75
+ context,
76
+ (fn, argument) =>
77
+ mappedArgumentDisposed(fn, argument, context, consumers, (value) =>
78
+ isDisposed(value, context, consumers),
79
+ ),
80
+ sourceFacts,
81
+ promiseSlotHandoff,
82
+ );
67
83
  const suppressed = ruleSuppression(context, "async-call-disposition");
68
84
  if (!matcher.hasTargets) return {};
69
85
 
@@ -78,12 +94,14 @@ module.exports = rule(
78
94
  ]),
79
95
  ),
80
96
  CallExpression(node) {
81
- if (
82
- !matcher.isTargetCall(node) ||
83
- suppressed(node) ||
84
- isDisposed(node, context, consumers.returnsThroughConsumer)
85
- )
97
+ if (suppressed(node)) return;
98
+ const target = matcher.isTargetCall(node);
99
+ // Unrelated calls cannot need a callback-body or imported type proof.
100
+ if (!target && (sourceFacts.sources === false || !possibleValueType(node.callee, context)))
86
101
  return;
102
+ // Disposed calls need no cross-file type lookup, including local/configured JS consumers.
103
+ if (isDisposed(node, context, consumers)) return;
104
+ if (!target && !promiseTypedTargetCall(node)) return;
87
105
  context.report({
88
106
  node,
89
107
  messageId: "disposition",
@@ -103,3 +121,5 @@ module.exports = rule(
103
121
  };
104
122
  },
105
123
  );
124
+
125
+ module.exports = withSourceOwner(dispositionRule);
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+
3
+ const { resolveVariable, resolveRuntimeVariable } = require("./async-target-bindings");
4
+ const { propertiesWritten } = require("./async-disposition-mocks");
5
+
6
+ function sameKey(first, key, context) {
7
+ if (key?.type === "Literal")
8
+ return typeof key.value === "string" && first?.type === "Literal" && first.value === key.value;
9
+ return (
10
+ first?.type === "Identifier" &&
11
+ key?.type === "Identifier" &&
12
+ resolveVariable(first, context) === resolveVariable(key, context)
13
+ );
14
+ }
15
+
16
+ function stableKey(key, fn, param, context) {
17
+ if (key?.type === "Literal") return typeof key.value === "string";
18
+ if (key?.type !== "Identifier") return false;
19
+ const variable = resolveVariable(key, context);
20
+ if (!variable) return false;
21
+ return (
22
+ fn.params.some((item) => item !== param && resolveVariable(item, context) === variable) &&
23
+ !variable.references.some((ref) => ref.isWrite())
24
+ );
25
+ }
26
+
27
+ function builtinMap(node, context) {
28
+ if (resolveVariable(node, context)?.defs.length || propertiesWritten(node, context)) return false;
29
+ // Global-holder writes can replace Map without mentioning its bare identifier.
30
+ const references = context.sourceCode.scopeManager.scopes
31
+ .flatMap((scope) => scope.references)
32
+ .filter((ref) => !(ref.isTypeReference && !ref.isValueReference));
33
+ if (
34
+ references.some(
35
+ (ref) =>
36
+ ["eval", "Function"].includes(ref.identifier.name) &&
37
+ !resolveRuntimeVariable(ref.identifier, context)?.defs.length,
38
+ )
39
+ )
40
+ return false;
41
+ const binding = resolveRuntimeVariable(node, context);
42
+ // Do not infer built-in semantics after the constructor/prototype escapes to
43
+ // reflective APIs or unknown consumers, whose effects are not modeled here.
44
+ if (
45
+ references.some(
46
+ (ref) =>
47
+ ref.identifier.name === "Map" &&
48
+ resolveRuntimeVariable(ref.identifier, context) === binding &&
49
+ !(
50
+ ref.identifier.parent.type === "NewExpression" &&
51
+ ref.identifier.parent.callee === ref.identifier
52
+ ),
53
+ )
54
+ )
55
+ return false;
56
+ const holders = ["globalThis", "global", "window", "self"];
57
+ return !references.some((ref) => {
58
+ if (!holders.includes(ref.identifier.name)) return false;
59
+ if (resolveRuntimeVariable(ref.identifier, context)?.defs.length) return false;
60
+ if (propertiesWritten(ref.identifier, context)) return true;
61
+ const member = ref.identifier.parent;
62
+ if (member.type !== "MemberExpression" || member.object !== ref.identifier) return true;
63
+ const name = member.computed ? member.property.value : member.property.name;
64
+ if (["eval", "Function"].includes(name)) return true;
65
+ if (name !== "Map" && typeof name === "string") return false;
66
+ return member.parent.type !== "NewExpression" || member.parent.callee !== member;
67
+ });
68
+ }
69
+
70
+ function mapCall(node, method, cache, key, context, count) {
71
+ const member = node?.callee;
72
+ const first = node?.arguments?.[0];
73
+ return (
74
+ node?.type === "CallExpression" &&
75
+ !node.optional &&
76
+ node.arguments.length === count &&
77
+ member?.type === "MemberExpression" &&
78
+ !member.computed &&
79
+ !member.optional &&
80
+ member.property.name === method &&
81
+ member.object.type === "Identifier" &&
82
+ resolveVariable(member.object, context) === cache &&
83
+ sameKey(first, key, context)
84
+ );
85
+ }
86
+
87
+ // This is deliberately a complete body grammar: any intervening effect, alias,
88
+ // escape, overwrite, early return, or exception may strand the stored promise.
89
+ function cachedBodyAwaited(fn, param, context) {
90
+ if (!fn.async || fn.generator || fn.params.some((item) => item.type !== "Identifier"))
91
+ return false;
92
+ const body = fn.body?.type === "BlockStatement" ? fn.body.body : [];
93
+ if (body.length !== 3) return false;
94
+ const [declaration, guard, returned] = body;
95
+ const entry = declaration.declarations?.[0];
96
+ const init = entry?.init;
97
+ if (
98
+ declaration.type !== "VariableDeclaration" ||
99
+ declaration.kind !== "const" ||
100
+ declaration.declarations.length !== 1 ||
101
+ entry.id.type !== "Identifier" ||
102
+ init?.type !== "NewExpression" ||
103
+ init.callee.type !== "Identifier" ||
104
+ init.callee.name !== "Map" ||
105
+ init.arguments.length !== 0 ||
106
+ !builtinMap(init.callee, context)
107
+ )
108
+ return false;
109
+ const cache = resolveVariable(entry.id, context);
110
+ const test = guard.test;
111
+ const inserted =
112
+ guard.consequent?.type === "BlockStatement"
113
+ ? guard.consequent.body.length === 1 && guard.consequent.body[0]
114
+ : guard.consequent;
115
+ const set = inserted?.expression;
116
+ const key = set?.arguments?.[0];
117
+ if (
118
+ !stableKey(key, fn, param, context) ||
119
+ guard.type !== "IfStatement" ||
120
+ guard.alternate ||
121
+ test?.type !== "UnaryExpression" ||
122
+ test.operator !== "!" ||
123
+ inserted?.type !== "ExpressionStatement" ||
124
+ !mapCall(test.argument, "has", cache, key, context, 1) ||
125
+ !mapCall(set, "set", cache, key, context, 2) ||
126
+ returned.type !== "ReturnStatement" ||
127
+ returned.argument?.type !== "AwaitExpression" ||
128
+ !mapCall(returned.argument.argument, "get", cache, key, context, 1)
129
+ )
130
+ return false;
131
+ const invocation = set.arguments[1];
132
+ const parameter = resolveVariable(param, context);
133
+ const reads = parameter?.references.filter((ref) => !ref.init) || [];
134
+ return (
135
+ invocation?.type === "CallExpression" &&
136
+ !invocation.optional &&
137
+ invocation.arguments.length === 0 &&
138
+ invocation.callee.type === "Identifier" &&
139
+ resolveVariable(invocation.callee, context) === parameter &&
140
+ reads.length === 1 &&
141
+ !reads[0].isWrite() &&
142
+ reads[0].identifier === invocation.callee
143
+ );
144
+ }
145
+
146
+ module.exports = { cachedBodyAwaited };
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ const { unwrapTransparentParent } = require("./async-ast");
4
+ const { followConst } = require("./async-consumer-property-carrier");
5
+
6
+ const copyOperators = new Set(["??", "||"]);
7
+
8
+ // Only the left operand is the supplied callback. The right-hand default is not.
9
+ function unwrapCopyLogical(node) {
10
+ const selected = unwrapTransparentParent(node);
11
+ const parent = selected.parent;
12
+ if (
13
+ parent?.type === "LogicalExpression" &&
14
+ copyOperators.has(parent.operator) &&
15
+ parent.left === selected
16
+ )
17
+ return unwrapTransparentParent(parent);
18
+ return selected;
19
+ }
20
+
21
+ function logicalCopyLeft(value) {
22
+ if (value?.type !== "LogicalExpression" || !copyOperators.has(value.operator)) return null;
23
+ return value.left;
24
+ }
25
+
26
+ function plainProperty(entry) {
27
+ return entry.type === "Property" && !entry.computed && !entry.method && entry.kind === "init";
28
+ }
29
+
30
+ // A key before the spread is overwritten by the source object. A key after it
31
+ // replaces the copied callback, so that order stays unproven.
32
+ function propertyKey(key) {
33
+ return key.type === "Identifier" ? key.name : key.value;
34
+ }
35
+
36
+ function patternBinding(value) {
37
+ const node = value?.type === "AssignmentPattern" ? value.left : value;
38
+ return node?.type === "Identifier" ? node : null;
39
+ }
40
+
41
+ function spreadPreserves(object, spread, property) {
42
+ if (object?.type !== "ObjectExpression") return false;
43
+ const index = object.properties.indexOf(spread);
44
+ const keyOf = (entry) => propertyKey(entry.key);
45
+ return (
46
+ object.properties.slice(0, index).every(plainProperty) &&
47
+ object.properties
48
+ .slice(index + 1)
49
+ .every((entry) => plainProperty(entry) && keyOf(entry) !== property)
50
+ );
51
+ }
52
+
53
+ // Shorthand `const { name }` and `const { name = defaultImpl }` only. A rename,
54
+ // rest, or computed key can hide the callback, so those patterns stay unproven.
55
+ function destructuredBinding(declarator, property) {
56
+ if (declarator.parent.kind !== "const") return false;
57
+ let binding = null;
58
+ for (const entry of declarator.id.properties) {
59
+ if (entry.type !== "Property" || entry.computed || entry.kind !== "init") return false;
60
+ const name = propertyKey(entry.key);
61
+ const id = patternBinding(entry.value);
62
+ if (id?.name !== name) return false;
63
+ if (name === property) binding = id;
64
+ }
65
+ return binding ?? "safe";
66
+ }
67
+
68
+ // "safe" is a read of some other field. false is an unproven pattern.
69
+ function destructureMode(value, property, context, shape, api, seen) {
70
+ const declarator = value.parent;
71
+ if (
72
+ declarator?.type !== "VariableDeclarator" ||
73
+ declarator.init !== value ||
74
+ declarator.id.type !== "ObjectPattern"
75
+ )
76
+ return null;
77
+ const binding = destructuredBinding(declarator, property);
78
+ if (binding === "safe" || binding === false) return binding;
79
+ return followConst(binding, null, context, shape, api, seen);
80
+ }
81
+
82
+ module.exports = { destructureMode, logicalCopyLeft, spreadPreserves, unwrapCopyLogical };
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ const { createImportedSources } = require("./async-source-store");
3
+ const { unwrapExpression } = require("./async-ast");
4
+ const { resolveVariable } = require("./async-target-bindings");
5
+ const { propertiesWritten } = require("./async-disposition-mocks");
6
+ const { localFunction } = require("./async-consumer-local-proof");
7
+
8
+ function exportedFunction(filename, name, sources, seen = new Set()) {
9
+ if (seen.size >= 32 || seen.has(filename)) return null;
10
+ seen.add(filename);
11
+ const source = sources.load(filename);
12
+ if (!source) return null;
13
+ const matches = [];
14
+ for (const statement of source.ast.body) {
15
+ if (statement.type !== "ExportNamedDeclaration" || statement.exportKind === "type") continue;
16
+ const declaration = statement.declaration;
17
+ const ids =
18
+ declaration?.type === "FunctionDeclaration"
19
+ ? [declaration.id]
20
+ : declaration?.type === "VariableDeclaration"
21
+ ? declaration.declarations.map((item) => item.id)
22
+ : [];
23
+ for (const id of ids) if (id?.name === name) matches.push({ local: id });
24
+ for (const item of statement.specifiers)
25
+ if (item.exportKind !== "type" && (item.exported.name ?? item.exported.value) === name)
26
+ matches.push({ local: item.local, source: statement.source });
27
+ }
28
+ if (matches.length !== 1) return null;
29
+ const { local, source: reexport } = matches[0];
30
+ if (reexport) {
31
+ const target = sources.resolve(reexport.value, filename);
32
+ return target ? exportedFunction(target, local.name, sources, seen) : null;
33
+ }
34
+ const fn = localFunction(local, source.context);
35
+ const variable = resolveVariable(local, source.context);
36
+ // Exporting preserves live binding identity; any other non-call use may mutate it.
37
+ if (
38
+ !fn ||
39
+ variable.references.some((ref) => {
40
+ if (ref.init) return false;
41
+ const parent = ref.identifier.parent;
42
+ return (
43
+ parent.type !== "ExportSpecifier" &&
44
+ !(parent.type === "CallExpression" && parent.callee === ref.identifier)
45
+ );
46
+ })
47
+ )
48
+ return null;
49
+ return { fn, context: source.context };
50
+ }
51
+
52
+ function importedAwaited(call, index, context, state, property, proveBody) {
53
+ const program = context.sourceCode.parserServices?.program;
54
+ if (program?.getCompilerOptions().noResolve) return false;
55
+ if (program && property === null)
56
+ return require("./async-consumer-typed-proof").importedAwaited(
57
+ call,
58
+ index,
59
+ context,
60
+ state.typed,
61
+ );
62
+
63
+ const resolved = importedFunction(unwrapExpression(call.callee), context, state);
64
+ if (!resolved) return false;
65
+ const mode = proveBody(resolved.fn, index, resolved.context, property);
66
+ return mode?.returned ? { ...mode, async: Boolean(resolved.fn.async) } : mode;
67
+ }
68
+
69
+ function importedFunction(callee, context, state) {
70
+ const binding = resolveVariable(callee, context);
71
+ if (
72
+ binding?.defs.length !== 1 ||
73
+ binding.defs[0].type !== "ImportBinding" ||
74
+ binding.references.some((ref) => ref.isWrite()) ||
75
+ propertiesWritten(callee, context, new Set(), null, true)
76
+ )
77
+ return false;
78
+ const def = binding.defs[0];
79
+ if (
80
+ def.node.type !== "ImportSpecifier" ||
81
+ def.parent.importKind === "type" ||
82
+ def.node.importKind === "type"
83
+ )
84
+ return false;
85
+ // Cache unavailable peers too; a new lint context retries without persisting state.
86
+ if (state.sources === null) state.sources = createImportedSources(context) || false;
87
+ const sources = state.sources;
88
+ if (!sources) return false;
89
+ const filename = context.filename;
90
+ const target = sources.resolve(def.parent.source.value, filename);
91
+ const resolved =
92
+ target && exportedFunction(target, def.node.imported.name ?? def.node.imported.value, sources);
93
+ return resolved;
94
+ }
95
+
96
+ module.exports = { importedAwaited, importedFunction, createImportedSources };