@agjs/tsforge 0.1.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 (216) hide show
  1. package/bin/tsforge.js +2 -0
  2. package/package.json +35 -0
  3. package/src/agent/agent.constants.ts +382 -0
  4. package/src/agent/agent.types.ts +34 -0
  5. package/src/agent/index.ts +4 -0
  6. package/src/agent/model-agent.ts +297 -0
  7. package/src/agent/tool-repair.ts +194 -0
  8. package/src/agent/tools.ts +190 -0
  9. package/src/browser/checks.ts +96 -0
  10. package/src/browser/index.ts +8 -0
  11. package/src/browser/oracle.ts +303 -0
  12. package/src/classify.ts +48 -0
  13. package/src/cli.ts +1333 -0
  14. package/src/config/config.constants.ts +9 -0
  15. package/src/config/flags.ts +32 -0
  16. package/src/config/index.ts +8 -0
  17. package/src/config/tsforge-config.ts +301 -0
  18. package/src/constitution/baseline.ts +257 -0
  19. package/src/detect-gate.ts +498 -0
  20. package/src/eval/eval.types.ts +36 -0
  21. package/src/eval/index.ts +3 -0
  22. package/src/eval/judge.ts +62 -0
  23. package/src/eval/score.ts +39 -0
  24. package/src/files/create.ts +22 -0
  25. package/src/files/edit.ts +193 -0
  26. package/src/files/files.constants.ts +11 -0
  27. package/src/files/files.types.ts +81 -0
  28. package/src/files/hashline-format.ts +110 -0
  29. package/src/files/hashline.ts +689 -0
  30. package/src/files/index.ts +19 -0
  31. package/src/index.ts +8 -0
  32. package/src/inference/index.ts +6 -0
  33. package/src/inference/inference.constants.ts +34 -0
  34. package/src/inference/inference.types.ts +123 -0
  35. package/src/inference/openai-compatible.ts +113 -0
  36. package/src/inference/stream-guard.ts +161 -0
  37. package/src/inference/stream.ts +370 -0
  38. package/src/inference/transport.ts +78 -0
  39. package/src/inference/wire.ts +0 -0
  40. package/src/lib/fs/fs.ts +126 -0
  41. package/src/lib/fs/fs.types.ts +5 -0
  42. package/src/lib/fs/index.ts +3 -0
  43. package/src/lib/fs/process.ts +146 -0
  44. package/src/lib/guards/guards.ts +9 -0
  45. package/src/lib/guards/index.ts +1 -0
  46. package/src/lib/json/index.ts +1 -0
  47. package/src/lib/json/json.ts +12 -0
  48. package/src/lib/scope/index.ts +2 -0
  49. package/src/lib/scope/scope.constants.ts +3 -0
  50. package/src/lib/scope/scope.ts +40 -0
  51. package/src/loop/astgrep-fix.ts +228 -0
  52. package/src/loop/feedback/feedback.ts +138 -0
  53. package/src/loop/feedback/index.ts +8 -0
  54. package/src/loop/feedback/meta-rule-docs.ts +41 -0
  55. package/src/loop/feedback/meta-rule-feedback.ts +61 -0
  56. package/src/loop/feedback/rule-docs.generated.json +112 -0
  57. package/src/loop/feedback/rule-docs.ts +342 -0
  58. package/src/loop/index.ts +19 -0
  59. package/src/loop/loop.constants.ts +68 -0
  60. package/src/loop/loop.types.ts +99 -0
  61. package/src/loop/prompt/index.ts +2 -0
  62. package/src/loop/prompt/project-map.ts +69 -0
  63. package/src/loop/prompt/prompt.ts +107 -0
  64. package/src/loop/quality.ts +174 -0
  65. package/src/loop/rule-docs.generated.json +367 -0
  66. package/src/loop/run-spec.ts +88 -0
  67. package/src/loop/run.ts +400 -0
  68. package/src/loop/session.ts +1410 -0
  69. package/src/loop/tools/add-dependency.ts +71 -0
  70. package/src/loop/tools/condense.ts +498 -0
  71. package/src/loop/tools/edit-hashline.ts +80 -0
  72. package/src/loop/tools/execute-tool.ts +80 -0
  73. package/src/loop/tools/file-ops.ts +323 -0
  74. package/src/loop/tools/index.ts +2 -0
  75. package/src/loop/tools/lsp-ops.ts +222 -0
  76. package/src/loop/tools/scaffold-routes.ts +68 -0
  77. package/src/loop/tools/scaffold-ui.ts +62 -0
  78. package/src/loop/tools/scaffold-web.ts +35 -0
  79. package/src/loop/tools/tool-context.ts +126 -0
  80. package/src/loop/ttsr-defaults.ts +53 -0
  81. package/src/loop/ttsr.ts +322 -0
  82. package/src/loop/turn.ts +856 -0
  83. package/src/lsp/index.ts +2 -0
  84. package/src/lsp/lsp.types.ts +56 -0
  85. package/src/lsp/service.ts +500 -0
  86. package/src/meta-rules/context.ts +195 -0
  87. package/src/meta-rules/index.ts +9 -0
  88. package/src/meta-rules/meta-rules.types.ts +47 -0
  89. package/src/meta-rules/parsers/package-json-parser.ts +51 -0
  90. package/src/meta-rules/registry.ts +37 -0
  91. package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
  92. package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
  93. package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
  94. package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
  95. package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
  96. package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
  97. package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
  98. package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
  99. package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
  100. package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
  101. package/src/meta-rules/runner.ts +64 -0
  102. package/src/models-config.ts +196 -0
  103. package/src/render/ansi.ts +289 -0
  104. package/src/render/banner.ts +113 -0
  105. package/src/render/box.ts +134 -0
  106. package/src/render/index.ts +7 -0
  107. package/src/render/markdown.ts +123 -0
  108. package/src/render/render.types.ts +21 -0
  109. package/src/render/stream-markdown.ts +128 -0
  110. package/src/render/style.ts +26 -0
  111. package/src/rule-packs/bullmq/index.ts +39 -0
  112. package/src/rule-packs/bullmq/rules/index.ts +7 -0
  113. package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
  114. package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
  115. package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
  116. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
  117. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
  118. package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
  119. package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
  120. package/src/rule-packs/bullmq/utils.ts +334 -0
  121. package/src/rule-packs/code-flow/index.ts +25 -0
  122. package/src/rule-packs/code-flow/rules/index.ts +3 -0
  123. package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
  124. package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
  125. package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
  126. package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
  127. package/src/rule-packs/comment-hygiene/index.ts +25 -0
  128. package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
  129. package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
  130. package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
  131. package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
  132. package/src/rule-packs/create-rule.ts +9 -0
  133. package/src/rule-packs/drizzle/index.ts +41 -0
  134. package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
  135. package/src/rule-packs/drizzle/rules/index.ts +8 -0
  136. package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
  137. package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
  138. package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
  139. package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
  140. package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
  141. package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
  142. package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
  143. package/src/rule-packs/drizzle/utils.ts +115 -0
  144. package/src/rule-packs/elysia/index.ts +43 -0
  145. package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
  146. package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
  147. package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
  148. package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
  149. package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
  150. package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
  151. package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
  152. package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
  153. package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
  154. package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
  155. package/src/rule-packs/env-access/index.ts +23 -0
  156. package/src/rule-packs/env-access/rules/index.ts +2 -0
  157. package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
  158. package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
  159. package/src/rule-packs/i18n-keys/index.ts +19 -0
  160. package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
  161. package/src/rule-packs/index.ts +139 -0
  162. package/src/rule-packs/jwt-cookies/index.ts +25 -0
  163. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
  164. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
  165. package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
  166. package/src/rule-packs/jwt-cookies/utils.ts +188 -0
  167. package/src/rule-packs/oauth-security/index.ts +25 -0
  168. package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
  169. package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
  170. package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
  171. package/src/rule-packs/oauth-security/utils.ts +127 -0
  172. package/src/rule-packs/react-component-architecture/index.ts +35 -0
  173. package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
  174. package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
  175. package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
  176. package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
  177. package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
  178. package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
  179. package/src/rule-packs/react-component-architecture/utils.ts +47 -0
  180. package/src/rule-packs/rule-packs.types.ts +18 -0
  181. package/src/rule-packs/structured-logging/index.ts +26 -0
  182. package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
  183. package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
  184. package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
  185. package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
  186. package/src/rule-packs/tanstack-query/index.ts +20 -0
  187. package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
  188. package/src/rule-packs/test-conventions/index.ts +23 -0
  189. package/src/rule-packs/test-conventions/rules/index.ts +2 -0
  190. package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
  191. package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
  192. package/src/rule-packs/utils.ts +142 -0
  193. package/src/session-store.ts +359 -0
  194. package/src/spec/generate-tests.ts +213 -0
  195. package/src/spec/index.ts +5 -0
  196. package/src/spec/parse.ts +152 -0
  197. package/src/spec/review-tests.ts +162 -0
  198. package/src/spec/spec.constants.ts +13 -0
  199. package/src/spec/spec.types.ts +79 -0
  200. package/src/stack-detection/detect.ts +246 -0
  201. package/src/stack-detection/index.ts +3 -0
  202. package/src/stack-detection/packs.ts +174 -0
  203. package/src/stack-detection/stack-detection.types.ts +47 -0
  204. package/src/validate/accept.ts +49 -0
  205. package/src/validate/errors.ts +35 -0
  206. package/src/validate/index.ts +12 -0
  207. package/src/validate/parse.ts +148 -0
  208. package/src/validate/run-tests.ts +59 -0
  209. package/src/validate/validate.ts +40 -0
  210. package/src/validate/validate.types.ts +52 -0
  211. package/src/web-components.ts +638 -0
  212. package/src/web-coverage.ts +89 -0
  213. package/src/web-routes.ts +151 -0
  214. package/src/web-templates.ts +1011 -0
  215. package/strict.eslint.config.mjs +84 -0
  216. package/strict.web.eslint.config.mjs +185 -0
@@ -0,0 +1,130 @@
1
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
2
+ import type { TSESTree } from "@typescript-eslint/utils";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import {
6
+ analyzeBullmqImports,
7
+ collectQueueDefinitions,
8
+ findObjectProperty,
9
+ getCallReceiverKey,
10
+ getOptionsObjectArg,
11
+ isQueueAddCall,
12
+ isQueueLikeReceiverName,
13
+ type BullmqImports,
14
+ type QueueDefinition,
15
+ } from "../utils";
16
+
17
+ export const RULE_NAME = "queue-options-must-set-removeoncomplete";
18
+
19
+ type RuleOptions = [];
20
+ type MessageIds = "missingRemoveOnComplete";
21
+
22
+ const optionSchema: JSONSchema4 = {
23
+ type: "object",
24
+ additionalProperties: false,
25
+ properties: {},
26
+ };
27
+
28
+ export const queueOptionsMustSetRemoveOnCompleteRule = createRule<
29
+ RuleOptions,
30
+ MessageIds
31
+ >({
32
+ name: RULE_NAME,
33
+ meta: {
34
+ type: "problem",
35
+ docs: {
36
+ description:
37
+ "Every `<queue>.add(...)` must configure `removeOnComplete` (per-call or via `defaultJobOptions`) so completed jobs don't accumulate in Redis.",
38
+ },
39
+ schema: [optionSchema],
40
+ messages: {
41
+ missingRemoveOnComplete:
42
+ "Job has no `removeOnComplete` configuration — completed jobs accumulate in Redis indefinitely. Set it per-call or via `defaultJobOptions` on the Queue.",
43
+ },
44
+ },
45
+ defaultOptions: [],
46
+ create(context) {
47
+ let imports: BullmqImports = {
48
+ hasBullmqImport: false,
49
+ workerLocalNames: new Set(),
50
+ queueLocalNames: new Set(),
51
+ queueEventsLocalNames: new Set(),
52
+ };
53
+ let knownQueues = new Map<string, QueueDefinition>();
54
+
55
+ return {
56
+ Program(program) {
57
+ imports = analyzeBullmqImports(program);
58
+ knownQueues = collectQueueDefinitions(program, imports);
59
+ },
60
+ CallExpression(node) {
61
+ if (!isQueueAddCall(node)) {
62
+ return;
63
+ }
64
+
65
+ if (!receiverIsQueueLike(node, knownQueues)) {
66
+ return;
67
+ }
68
+
69
+ if (callHasOption(node, "removeOnComplete")) {
70
+ return;
71
+ }
72
+
73
+ if (queueDefaultsHaveOption(node, knownQueues, "removeOnComplete")) {
74
+ return;
75
+ }
76
+
77
+ context.report({ node, messageId: "missingRemoveOnComplete" });
78
+ },
79
+ };
80
+ },
81
+ });
82
+
83
+ function receiverIsQueueLike(
84
+ call: TSESTree.CallExpression,
85
+ knownQueues: ReadonlyMap<string, QueueDefinition>
86
+ ): boolean {
87
+ const key = getCallReceiverKey(call);
88
+
89
+ if (!key) {
90
+ return false;
91
+ }
92
+
93
+ if (knownQueues.has(key)) {
94
+ return true;
95
+ }
96
+
97
+ const last = key.includes(".") ? (key.split(".").pop() ?? key) : key;
98
+
99
+ return isQueueLikeReceiverName(last);
100
+ }
101
+
102
+ function callHasOption(call: TSESTree.CallExpression, name: string): boolean {
103
+ const opts = getOptionsObjectArg(call, 2);
104
+
105
+ if (!opts) {
106
+ return false;
107
+ }
108
+
109
+ return findObjectProperty(opts, name) !== null;
110
+ }
111
+
112
+ function queueDefaultsHaveOption(
113
+ call: TSESTree.CallExpression,
114
+ knownQueues: ReadonlyMap<string, QueueDefinition>,
115
+ name: string
116
+ ): boolean {
117
+ const key = getCallReceiverKey(call);
118
+
119
+ if (!key) {
120
+ return false;
121
+ }
122
+
123
+ const def = knownQueues.get(key);
124
+
125
+ if (!def?.defaultJobOptions) {
126
+ return false;
127
+ }
128
+
129
+ return findObjectProperty(def.defaultJobOptions, name) !== null;
130
+ }
@@ -0,0 +1,130 @@
1
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
2
+ import type { TSESTree } from "@typescript-eslint/utils";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import {
6
+ analyzeBullmqImports,
7
+ collectQueueDefinitions,
8
+ findObjectProperty,
9
+ getCallReceiverKey,
10
+ getOptionsObjectArg,
11
+ isQueueAddCall,
12
+ isQueueLikeReceiverName,
13
+ type BullmqImports,
14
+ type QueueDefinition,
15
+ } from "../utils";
16
+
17
+ export const RULE_NAME = "queue-options-must-set-removeonfail";
18
+
19
+ type RuleOptions = [];
20
+ type MessageIds = "missingRemoveOnFail";
21
+
22
+ const optionSchema: JSONSchema4 = {
23
+ type: "object",
24
+ additionalProperties: false,
25
+ properties: {},
26
+ };
27
+
28
+ export const queueOptionsMustSetRemoveOnFailRule = createRule<
29
+ RuleOptions,
30
+ MessageIds
31
+ >({
32
+ name: RULE_NAME,
33
+ meta: {
34
+ type: "problem",
35
+ docs: {
36
+ description:
37
+ "Every `<queue>.add(...)` must configure `removeOnFail` (per-call or via `defaultJobOptions`) so failed jobs don't accumulate in Redis.",
38
+ },
39
+ schema: [optionSchema],
40
+ messages: {
41
+ missingRemoveOnFail:
42
+ "Job has no `removeOnFail` configuration — failed jobs accumulate in Redis indefinitely. Set it per-call or via `defaultJobOptions` on the Queue.",
43
+ },
44
+ },
45
+ defaultOptions: [],
46
+ create(context) {
47
+ let imports: BullmqImports = {
48
+ hasBullmqImport: false,
49
+ workerLocalNames: new Set(),
50
+ queueLocalNames: new Set(),
51
+ queueEventsLocalNames: new Set(),
52
+ };
53
+ let knownQueues = new Map<string, QueueDefinition>();
54
+
55
+ return {
56
+ Program(program) {
57
+ imports = analyzeBullmqImports(program);
58
+ knownQueues = collectQueueDefinitions(program, imports);
59
+ },
60
+ CallExpression(node) {
61
+ if (!isQueueAddCall(node)) {
62
+ return;
63
+ }
64
+
65
+ if (!receiverIsQueueLike(node, knownQueues)) {
66
+ return;
67
+ }
68
+
69
+ if (callHasOption(node, "removeOnFail")) {
70
+ return;
71
+ }
72
+
73
+ if (queueDefaultsHaveOption(node, knownQueues, "removeOnFail")) {
74
+ return;
75
+ }
76
+
77
+ context.report({ node, messageId: "missingRemoveOnFail" });
78
+ },
79
+ };
80
+ },
81
+ });
82
+
83
+ function receiverIsQueueLike(
84
+ call: TSESTree.CallExpression,
85
+ knownQueues: ReadonlyMap<string, QueueDefinition>
86
+ ): boolean {
87
+ const key = getCallReceiverKey(call);
88
+
89
+ if (!key) {
90
+ return false;
91
+ }
92
+
93
+ if (knownQueues.has(key)) {
94
+ return true;
95
+ }
96
+
97
+ const last = key.includes(".") ? (key.split(".").pop() ?? key) : key;
98
+
99
+ return isQueueLikeReceiverName(last);
100
+ }
101
+
102
+ function callHasOption(call: TSESTree.CallExpression, name: string): boolean {
103
+ const opts = getOptionsObjectArg(call, 2);
104
+
105
+ if (!opts) {
106
+ return false;
107
+ }
108
+
109
+ return findObjectProperty(opts, name) !== null;
110
+ }
111
+
112
+ function queueDefaultsHaveOption(
113
+ call: TSESTree.CallExpression,
114
+ knownQueues: ReadonlyMap<string, QueueDefinition>,
115
+ name: string
116
+ ): boolean {
117
+ const key = getCallReceiverKey(call);
118
+
119
+ if (!key) {
120
+ return false;
121
+ }
122
+
123
+ const def = knownQueues.get(key);
124
+
125
+ if (!def?.defaultJobOptions) {
126
+ return false;
127
+ }
128
+
129
+ return findObjectProperty(def.defaultJobOptions, name) !== null;
130
+ }
@@ -0,0 +1,182 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import {
6
+ analyzeBullmqImports,
7
+ isNewWorker,
8
+ walkSome,
9
+ type BullmqImports,
10
+ } from "../utils";
11
+
12
+ export const RULE_NAME = "worker-must-implement-close";
13
+
14
+ export interface WorkerMustImplementCloseOptions {
15
+ readonly closeMethodNames?: readonly string[];
16
+ }
17
+
18
+ type RuleOptions = [WorkerMustImplementCloseOptions];
19
+ type MessageIds = "missingClose";
20
+
21
+ const DEFAULT_CLOSE_METHODS: readonly string[] = [
22
+ "close",
23
+ "shutdown",
24
+ "dispose",
25
+ "onModuleDestroy",
26
+ ];
27
+
28
+ const optionSchema: JSONSchema4 = {
29
+ type: "object",
30
+ additionalProperties: false,
31
+ properties: {
32
+ closeMethodNames: {
33
+ type: "array",
34
+ items: { type: "string" },
35
+ uniqueItems: true,
36
+ minItems: 1,
37
+ },
38
+ },
39
+ };
40
+
41
+ export const workerMustImplementCloseRule = createRule<RuleOptions, MessageIds>(
42
+ {
43
+ name: RULE_NAME,
44
+ meta: {
45
+ type: "problem",
46
+ docs: {
47
+ description:
48
+ "Classes that own a `new Worker(...)` instance must declare a close-equivalent method for graceful shutdown.",
49
+ },
50
+ schema: [optionSchema],
51
+ messages: {
52
+ missingClose:
53
+ "Class '{{name}}' owns a `new Worker(...)` instance but does not declare a close method ({{methods}}). BullMQ workers must be explicitly closed during graceful shutdown — otherwise jobs in flight are abandoned and Redis connections leak.",
54
+ },
55
+ },
56
+ defaultOptions: [{ closeMethodNames: [...DEFAULT_CLOSE_METHODS] }],
57
+ create(context, [options]) {
58
+ const closeMethods = new Set(
59
+ options.closeMethodNames ?? DEFAULT_CLOSE_METHODS
60
+ );
61
+
62
+ let imports: BullmqImports = {
63
+ hasBullmqImport: false,
64
+ workerLocalNames: new Set(),
65
+ queueLocalNames: new Set(),
66
+ queueEventsLocalNames: new Set(),
67
+ };
68
+
69
+ return {
70
+ Program(program) {
71
+ imports = analyzeBullmqImports(program);
72
+ },
73
+ ClassDeclaration(node) {
74
+ if (!imports.hasBullmqImport) {
75
+ return;
76
+ }
77
+
78
+ if (!classOwnsWorker(node, imports)) {
79
+ return;
80
+ }
81
+
82
+ if (classDeclaresAnyMethod(node, closeMethods)) {
83
+ return;
84
+ }
85
+
86
+ const className = node.id?.name ?? "<anonymous>";
87
+ const methodList = [...closeMethods]
88
+ .map((m) => `\`${m}\``)
89
+ .join(", ");
90
+
91
+ context.report({
92
+ node: node.id ?? node,
93
+ messageId: "missingClose",
94
+ data: { name: className, methods: methodList },
95
+ });
96
+ },
97
+ };
98
+ },
99
+ }
100
+ );
101
+
102
+ function classOwnsWorker(
103
+ cls: TSESTree.ClassDeclaration,
104
+ imports: BullmqImports
105
+ ): boolean {
106
+ for (const member of cls.body.body) {
107
+ if (
108
+ member.type === AST_NODE_TYPES.PropertyDefinition &&
109
+ member.value &&
110
+ isNewWorker(member.value, imports)
111
+ ) {
112
+ return true;
113
+ }
114
+
115
+ if (member.type === AST_NODE_TYPES.MethodDefinition) {
116
+ const fnBody = member.value.body;
117
+
118
+ if (!fnBody) {
119
+ continue;
120
+ }
121
+
122
+ const found = walkSome(fnBody, (n) => {
123
+ if (n.type !== AST_NODE_TYPES.AssignmentExpression) {
124
+ return false;
125
+ }
126
+
127
+ if (
128
+ n.left.type !== AST_NODE_TYPES.MemberExpression ||
129
+ n.left.object.type !== AST_NODE_TYPES.ThisExpression
130
+ ) {
131
+ return false;
132
+ }
133
+
134
+ return isNewWorker(n.right, imports);
135
+ });
136
+
137
+ if (found) {
138
+ return true;
139
+ }
140
+ }
141
+ }
142
+
143
+ return false;
144
+ }
145
+
146
+ function classDeclaresAnyMethod(
147
+ cls: TSESTree.ClassDeclaration,
148
+ methodNames: ReadonlySet<string>
149
+ ): boolean {
150
+ for (const member of cls.body.body) {
151
+ if (member.type !== AST_NODE_TYPES.MethodDefinition) {
152
+ continue;
153
+ }
154
+
155
+ if (member.kind === "constructor") {
156
+ continue;
157
+ }
158
+
159
+ const name = getMethodName(member);
160
+
161
+ if (name && methodNames.has(name)) {
162
+ return true;
163
+ }
164
+ }
165
+
166
+ return false;
167
+ }
168
+
169
+ function getMethodName(method: TSESTree.MethodDefinition): string | null {
170
+ if (method.key.type === AST_NODE_TYPES.Identifier) {
171
+ return method.key.name;
172
+ }
173
+
174
+ if (
175
+ method.key.type === AST_NODE_TYPES.Literal &&
176
+ typeof method.key.value === "string"
177
+ ) {
178
+ return method.key.value;
179
+ }
180
+
181
+ return null;
182
+ }
@@ -0,0 +1,140 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import {
6
+ analyzeBullmqImports,
7
+ collectWorkerDefinitions,
8
+ getReceiverKey,
9
+ walkAll,
10
+ type BullmqImports,
11
+ type WorkerDefinition,
12
+ } from "../utils";
13
+
14
+ export const RULE_NAME = "worker-must-listen-failed";
15
+
16
+ export interface WorkerMustListenFailedOptions {
17
+ readonly requiredEvents?: readonly string[];
18
+ }
19
+
20
+ type RuleOptions = [WorkerMustListenFailedOptions];
21
+ type MessageIds = "missingListener";
22
+
23
+ const DEFAULT_REQUIRED_EVENTS: readonly string[] = ["failed"];
24
+
25
+ const optionSchema: JSONSchema4 = {
26
+ type: "object",
27
+ additionalProperties: false,
28
+ properties: {
29
+ requiredEvents: {
30
+ type: "array",
31
+ items: { type: "string" },
32
+ uniqueItems: true,
33
+ minItems: 1,
34
+ },
35
+ },
36
+ };
37
+
38
+ export const workerMustListenFailedRule = createRule<RuleOptions, MessageIds>({
39
+ name: RULE_NAME,
40
+ meta: {
41
+ type: "problem",
42
+ docs: {
43
+ description:
44
+ "Every `new Worker(...)` must register listeners for required events (default `failed`) — BullMQ failures are silent unless explicitly subscribed.",
45
+ },
46
+ schema: [optionSchema],
47
+ messages: {
48
+ missingListener:
49
+ "Worker assigned to `{{name}}` has no `.on('{{event}}', ...)` listener — BullMQ failures are silent unless explicitly subscribed.",
50
+ },
51
+ },
52
+ defaultOptions: [{ requiredEvents: [...DEFAULT_REQUIRED_EVENTS] }],
53
+ create(context, [options]) {
54
+ const requiredEvents = options.requiredEvents ?? DEFAULT_REQUIRED_EVENTS;
55
+
56
+ let imports: BullmqImports = {
57
+ hasBullmqImport: false,
58
+ workerLocalNames: new Set(),
59
+ queueLocalNames: new Set(),
60
+ queueEventsLocalNames: new Set(),
61
+ };
62
+ let workers: WorkerDefinition[] = [];
63
+ let listenerKeyEventPairs = new Set<string>();
64
+
65
+ return {
66
+ Program(program) {
67
+ imports = analyzeBullmqImports(program);
68
+
69
+ if (!imports.hasBullmqImport) {
70
+ return;
71
+ }
72
+
73
+ workers = collectWorkerDefinitions(program, imports);
74
+ listenerKeyEventPairs = collectOnListeners(program);
75
+ },
76
+ "Program:exit"() {
77
+ if (!imports.hasBullmqImport) {
78
+ return;
79
+ }
80
+
81
+ for (const worker of workers) {
82
+ const bindingKey = worker.bindingKey;
83
+
84
+ if (!bindingKey) {
85
+ continue;
86
+ }
87
+
88
+ for (const event of requiredEvents) {
89
+ const key = `${bindingKey}::${event}`;
90
+
91
+ if (!listenerKeyEventPairs.has(key)) {
92
+ context.report({
93
+ node: worker.node,
94
+ messageId: "missingListener",
95
+ data: { name: bindingKey, event },
96
+ });
97
+ }
98
+ }
99
+ }
100
+ },
101
+ };
102
+ },
103
+ });
104
+
105
+ function collectOnListeners(program: TSESTree.Program): Set<string> {
106
+ const pairs = new Set<string>();
107
+
108
+ walkAll(program, (node) => {
109
+ if (node.type !== AST_NODE_TYPES.CallExpression) {
110
+ return;
111
+ }
112
+
113
+ if (
114
+ node.callee.type !== AST_NODE_TYPES.MemberExpression ||
115
+ node.callee.property.type !== AST_NODE_TYPES.Identifier ||
116
+ node.callee.property.name !== "on"
117
+ ) {
118
+ return;
119
+ }
120
+
121
+ const receiverKey = getReceiverKey(node.callee.object);
122
+
123
+ if (!receiverKey) {
124
+ return;
125
+ }
126
+
127
+ const eventArg = node.arguments[0];
128
+
129
+ if (
130
+ eventArg?.type !== AST_NODE_TYPES.Literal ||
131
+ typeof eventArg.value !== "string"
132
+ ) {
133
+ return;
134
+ }
135
+
136
+ pairs.add(`${receiverKey}::${eventArg.value}`);
137
+ });
138
+
139
+ return pairs;
140
+ }