eslint-plugin-bullmq 0.1.0 → 0.1.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1 (2026-10-22)
4
+
5
+ - README: TypeScript setup and the supported ESLint versions.
6
+ - Release smoke test now covers ESLint 8.57 (flat and legacy), 9, 10 and `@typescript-eslint/parser`. CI adds Windows.
7
+
3
8
  ## 0.1.0 (2026-10-22)
4
9
 
5
10
  - Rules: `require-error-listener`, `worker-connection-max-retries`, `valid-job-id`, `no-jobid-dedupe-with-remove-on-complete`.
package/README.md CHANGED
@@ -49,7 +49,11 @@ Starting a new project? [create-bullmq-app](https://github.com/Krrishkumar125/cr
49
49
  a worker that already passes these rules. Webhook-driven queues: see also
50
50
  [eslint-plugin-webhook-safety](https://github.com/Krrishkumar125/eslint-plugin-webhook-safety).
51
51
 
52
- Requires ESLint ≥ 8.57 and Node ≥ 20. Not affiliated with Taskforce.sh / BullMQ.
52
+ TypeScript: ESLint only lints `.ts` files that some config object matches, so combine it with `typescript-eslint`:
53
+ `export default [...tseslint.configs.recommended, bullmq.configs.recommended];`
54
+
55
+ Requires ESLint 8.57+, 9 or 10 and Node ≥ 20. Every release is smoke-tested from the packed tarball against ESLint
56
+ 8.57 (flat and legacy `.eslintrc`), 9, 10 and `@typescript-eslint/parser`, and the rules' tests run on Linux and Windows. Not affiliated with Taskforce.sh / BullMQ.
53
57
 
54
58
  ## License
55
59
 
package/dist/index.cjs CHANGED
@@ -397,7 +397,7 @@ var strictRules = Object.fromEntries(
397
397
  Object.keys(recommendedRules).map((k) => [k, "error"])
398
398
  );
399
399
  var plugin = {
400
- meta: { name: "eslint-plugin-bullmq", version: "0.1.0" },
400
+ meta: { name: "eslint-plugin-bullmq", version: "0.1.1" },
401
401
  rules,
402
402
  configs: {}
403
403
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["/workspace/eslint-plugin-bullmq/dist/index.cjs","../src/utils.ts","../src/rules/no-jobid-dedupe-with-remove-on-complete.ts","../src/rules/require-error-listener.ts","../src/rules/valid-job-id.ts","../src/rules/worker-connection-max-retries.ts","../src/index.ts"],"names":[],"mappings":"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","file":"/workspace/eslint-plugin-bullmq/dist/index.cjs","sourcesContent":[null,"import type { Rule, Scope } from 'eslint';\nimport type * as ESTree from 'estree';\n\nexport type Node = ESTree.Node & { parent?: Node };\n\nconst REPO = 'https://github.com/Krrishkumar125/eslint-plugin-bullmq';\nexport const docsUrl = (name: string): string => `${REPO}/blob/main/docs/rules/${name}.md`;\n\nfunction moduleName(node: Node | null | undefined): string | undefined {\n if (\n node?.type === 'CallExpression' &&\n node.callee.type === 'Identifier' &&\n node.callee.name === 'require' &&\n node.arguments[0]?.type === 'Literal' &&\n typeof node.arguments[0].value === 'string'\n ) {\n return node.arguments[0].value;\n }\n return undefined;\n}\n\nexport interface Imports {\n /** local name -> imported name */\n named: Map<string, string>;\n namespaces: Set<string>;\n}\n\n/** Collect bindings imported (ESM or CommonJS) from the given module. */\nexport function collectImports(program: ESTree.Program, source: string): Imports {\n const named = new Map<string, string>();\n const namespaces = new Set<string>();\n const visitDeclarator = (d: ESTree.VariableDeclarator): void => {\n if (moduleName(d.init as Node) !== source) return;\n if (d.id.type === 'Identifier') namespaces.add(d.id.name);\n if (d.id.type === 'ObjectPattern') {\n for (const p of d.id.properties) {\n if (p.type === 'Property' && p.key.type === 'Identifier' && p.value.type === 'Identifier') {\n named.set(p.value.name, p.key.name);\n }\n }\n }\n };\n for (const stmt of program.body) {\n if (stmt.type === 'ImportDeclaration' && stmt.source.value === source) {\n for (const s of stmt.specifiers) {\n if (s.type === 'ImportSpecifier') {\n const imported =\n s.imported.type === 'Identifier' ? s.imported.name : String(s.imported.value);\n named.set(s.local.name, imported);\n } else if (s.type === 'ImportDefaultSpecifier') {\n named.set(s.local.name, 'default');\n } else {\n namespaces.add(s.local.name);\n }\n }\n } else if (stmt.type === 'VariableDeclaration') {\n stmt.declarations.forEach(visitDeclarator);\n }\n }\n return { named, namespaces };\n}\n\n/** Which exported class a `new X(...)` refers to, given the imports of one module. */\nexport function resolveClass(node: ESTree.NewExpression, imports: Imports): string | undefined {\n const callee = node.callee;\n if (callee.type === 'Identifier') return imports.named.get(callee.name);\n if (\n callee.type === 'MemberExpression' &&\n callee.object.type === 'Identifier' &&\n imports.namespaces.has(callee.object.name) &&\n !callee.computed &&\n callee.property.type === 'Identifier'\n ) {\n return callee.property.name;\n }\n return undefined;\n}\n\nexport function findVariable(\n context: Rule.RuleContext,\n node: ESTree.Identifier,\n): Scope.Variable | undefined {\n let scope: Scope.Scope | null = context.sourceCode.getScope(node);\n while (scope) {\n const v = scope.set.get(node.name);\n if (v) return v;\n scope = scope.upper;\n }\n return undefined;\n}\n\nexport function propKey(p: ESTree.Property | ESTree.SpreadElement): string | undefined {\n if (p.type !== 'Property' || p.computed) return undefined;\n if (p.key.type === 'Identifier') return p.key.name;\n if (p.key.type === 'Literal' && typeof p.key.value === 'string') return p.key.value;\n return undefined;\n}\n\nexport function getProp(obj: ESTree.ObjectExpression, key: string): ESTree.Property | undefined {\n return obj.properties.find((p): p is ESTree.Property => propKey(p) === key);\n}\n\nexport const hasSpread = (obj: ESTree.ObjectExpression): boolean =>\n obj.properties.some((p) => p.type === 'SpreadElement');\n\n/** Is this object literal a BullMQ job-options object (`queue.add(n, d, opts)` or `{ opts: {...} }`)? */\nexport function isJobOptions(obj: Node): boolean {\n const parent = obj.parent;\n if (!parent) return false;\n if (parent.type === 'CallExpression' && parent.arguments[2] === obj) {\n const c = parent.callee;\n return (\n c.type === 'MemberExpression' &&\n !c.computed &&\n c.property.type === 'Identifier' &&\n c.property.name === 'add'\n );\n }\n return parent.type === 'Property' && parent.value === obj && propKey(parent) === 'opts';\n}\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport { docsUrl, getProp, isJobOptions, type Node } from '../utils.js';\n\nconst removesImmediately = (p: ESTree.Property | undefined): boolean =>\n p?.value.type === 'Literal' && (p.value.value === true || p.value.value === 0);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Warn when jobId deduplication is combined with removeOnComplete: true, which lets duplicates run again',\n recommended: true,\n url: docsUrl('no-jobid-dedupe-with-remove-on-complete'),\n },\n schema: [],\n messages: {\n conflict:\n 'removeOnComplete: true deletes the job as soon as it finishes, so a later add() with the same jobId runs again. The jobId only dedupes while the job is still waiting or active. Keep completed jobs for your dedupe window with removeOnComplete: { age: <seconds> }, or use deduplication: { id, ttl }.',\n conflictDefault:\n \"This queue's defaultJobOptions has removeOnComplete: true, so this jobId only dedupes while the job is still waiting or active. A redelivery after completion runs again. Use removeOnComplete: { age: <seconds> } or deduplication: { id, ttl }.\",\n },\n },\n create(context) {\n const removingQueues = new Set<string>();\n return {\n VariableDeclarator(node) {\n const init = node.init;\n if (\n node.id.type !== 'Identifier' ||\n init?.type !== 'NewExpression' ||\n init.callee.type !== 'Identifier' ||\n init.callee.name !== 'Queue'\n ) {\n return;\n }\n const opts = init.arguments[1];\n const defaults =\n opts?.type === 'ObjectExpression' ? getProp(opts, 'defaultJobOptions')?.value : undefined;\n if (\n defaults?.type === 'ObjectExpression' &&\n removesImmediately(getProp(defaults, 'removeOnComplete'))\n ) {\n removingQueues.add(node.id.name);\n }\n },\n ObjectExpression(node) {\n if (!isJobOptions(node as Node) || !getProp(node, 'jobId')) return;\n const remove = getProp(node, 'removeOnComplete');\n if (removesImmediately(remove)) {\n context.report({ node: remove as ESTree.Property, messageId: 'conflict' });\n return;\n }\n const call = (node as Node).parent;\n if (\n !remove &&\n call?.type === 'CallExpression' &&\n call.callee.type === 'MemberExpression' &&\n call.callee.object.type === 'Identifier' &&\n removingQueues.has(call.callee.object.name)\n ) {\n context.report({ node: call, messageId: 'conflictDefault' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport {\n collectImports,\n docsUrl,\n findVariable,\n resolveClass,\n type Imports,\n type Node,\n} from '../utils.js';\n\nconst CHECKED = new Set(['Worker', 'Queue']);\nconst LISTEN = new Set(['on', 'once', 'addListener', 'prependListener']);\n\nfunction isErrorListener(member: Node | undefined, object: Node): boolean {\n if (member?.type !== 'MemberExpression' || member.object !== object) return false;\n const prop = member.property;\n const method =\n !member.computed && prop.type === 'Identifier'\n ? prop.name\n : prop.type === 'Literal'\n ? prop.value\n : undefined;\n if (typeof method !== 'string' || !LISTEN.has(method)) return false;\n const call = member.parent;\n const arg = call?.type === 'CallExpression' ? call.arguments[0] : undefined;\n return arg?.type === 'Literal' && arg.value === 'error';\n}\n\n/** The identifier is handed to code we cannot see, which may attach the listener. */\nfunction escapes(id: Node): boolean {\n const p = id.parent;\n if (!p) return false;\n switch (p.type) {\n case 'CallExpression':\n case 'NewExpression':\n return p.arguments.includes(id as ESTree.Expression);\n case 'ReturnStatement':\n case 'ArrayExpression':\n case 'ExportSpecifier':\n case 'ExportDefaultDeclaration':\n case 'SpreadElement':\n return true;\n case 'Property':\n return p.value === id;\n case 'AssignmentExpression':\n return p.right === id;\n case 'ArrowFunctionExpression':\n return p.body === id;\n default:\n return false;\n }\n}\n\n/** `x.close()` / `x.disconnect()`: a short-lived instance (admin query, health check, test). */\nfunction isClosed(id: Node): boolean {\n const m = id.parent;\n return (\n m?.type === 'MemberExpression' &&\n m.object === id &&\n !m.computed &&\n m.property.type === 'Identifier' &&\n (m.property.name === 'close' || m.property.name === 'disconnect') &&\n m.parent?.type === 'CallExpression'\n );\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: \"Require an 'error' listener on BullMQ Worker and Queue instances\",\n recommended: true,\n url: docsUrl('require-error-listener'),\n },\n schema: [],\n messages: {\n missing:\n \"This {{kind}} has no 'error' listener. Without one, a Redis or processor error can crash the process or silently stop the {{kind}}. Add {{kind}}.on('error', handler).\",\n },\n },\n create(context) {\n let imports: Imports;\n return {\n Program(program) {\n imports = collectImports(program, 'bullmq');\n },\n NewExpression(node) {\n const kind = resolveClass(node, imports);\n if (!kind || !CHECKED.has(kind)) return;\n const n = node as Node;\n const parent = n.parent;\n let id: ESTree.Identifier | undefined;\n if (parent?.type === 'MemberExpression') {\n if (!isErrorListener(parent, n))\n context.report({ node, messageId: 'missing', data: { kind } });\n return;\n }\n if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {\n if (parent.parent?.parent?.type === 'ExportNamedDeclaration') return;\n id = parent.id;\n } else if (parent?.type === 'AssignmentExpression' && parent.left.type === 'Identifier') {\n id = parent.left;\n } else if (parent?.type !== 'ExpressionStatement') {\n return; // returned, passed along, stored on `this`, etc.\n }\n if (id) {\n const variable = findVariable(context, id);\n const ok = variable?.references.some((ref) => {\n const ident = ref.identifier as Node;\n return isErrorListener(ident.parent, ident) || escapes(ident) || isClosed(ident);\n });\n if (ok || !variable) return;\n }\n context.report({ node, messageId: 'missing', data: { kind } });\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport { docsUrl, isJobOptions, propKey, type Node } from '../utils.js';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n hasSuggestions: true,\n docs: {\n description: \"Disallow BullMQ custom job ids that contain ':' or are integers\",\n recommended: true,\n url: docsUrl('valid-job-id'),\n },\n schema: [],\n messages: {\n colon:\n \"BullMQ uses ':' as its Redis key separator, so a custom jobId containing ':' is rejected. Use another separator such as '-'.\",\n integer:\n 'BullMQ rejects integer custom job ids (they collide with its auto-generated ids). Prefix it, e.g. `order-42`.',\n replaceColon: \"Replace ':' with '-'\",\n },\n },\n create(context) {\n return {\n Property(node) {\n if (propKey(node) !== 'jobId' || !isJobOptions((node as Node).parent as Node)) return;\n const v = node.value as Node;\n if (v.type === 'Literal') {\n if (typeof v.value === 'number' && Number.isInteger(v.value)) {\n context.report({ node: v, messageId: 'integer' });\n } else if (typeof v.value === 'string') {\n if (/^\\d+$/.test(v.value)) context.report({ node: v, messageId: 'integer' });\n else if (v.value.includes(':')) {\n const raw = context.sourceCode.getText(v);\n context.report({\n node: v,\n messageId: 'colon',\n suggest: [\n {\n messageId: 'replaceColon',\n fix: (f) => f.replaceText(v, raw.replace(/:/g, '-')),\n },\n ],\n });\n }\n }\n } else if (\n v.type === 'TemplateLiteral' &&\n v.quasis.some((q) => q.value.raw.includes(':'))\n ) {\n context.report({\n node: v,\n messageId: 'colon',\n suggest: [\n {\n messageId: 'replaceColon',\n fix: (f) =>\n v.quasis\n .filter((q) => q.value.raw.includes(':'))\n .map((q) => f.replaceText(q, context.sourceCode.getText(q).replace(/:/g, '-'))),\n },\n ],\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport {\n collectImports,\n docsUrl,\n findVariable,\n getProp,\n hasSpread,\n resolveClass,\n type Imports,\n type Node,\n} from '../utils.js';\n\nconst BLOCKING = new Map([\n ['Worker', 2],\n ['QueueEvents', 1],\n]);\nconst REDIS_FALLBACK = /^(IO)?Redis$|^Cluster$/;\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n hasSuggestions: true,\n docs: {\n description:\n 'Require `maxRetriesPerRequest: null` on ioredis connections used by BullMQ Workers and QueueEvents',\n recommended: true,\n url: docsUrl('worker-connection-max-retries'),\n },\n schema: [],\n messages: {\n instance:\n \"This ioredis connection is used by a BullMQ {{kind}} but does not set maxRetriesPerRequest: null. BullMQ throws 'maxRetriesPerRequest must be null' when the {{kind}} starts; set it so blocking commands survive Redis reconnects.\",\n explicit:\n 'BullMQ Workers need maxRetriesPerRequest: null; a finite value makes blocking commands fail during Redis reconnects.',\n addNull: 'Add maxRetriesPerRequest: null',\n setNull: 'Set maxRetriesPerRequest to null',\n },\n },\n create(context) {\n let bull: Imports;\n let ioredis: Imports;\n const reported = new Set<ESTree.Node>();\n\n const isRedis = (n: ESTree.NewExpression): boolean =>\n resolveClass(n, ioredis) !== undefined ||\n (n.callee.type === 'Identifier' && REDIS_FALLBACK.test(n.callee.name));\n\n function checkObject(obj: ESTree.ObjectExpression): 'ok' | 'missing' {\n const prop = getProp(obj, 'maxRetriesPerRequest');\n if (!prop) return hasSpread(obj) ? 'ok' : 'missing';\n const v = prop.value;\n if (v.type === 'Literal' && v.value === null) return 'ok';\n if (v.type === 'Literal' || (v.type === 'UnaryExpression' && v.argument.type === 'Literal')) {\n if (!reported.has(prop)) {\n reported.add(prop);\n context.report({\n node: prop,\n messageId: 'explicit',\n suggest: [{ messageId: 'setNull', fix: (f) => f.replaceText(v, 'null') }],\n });\n }\n }\n return 'ok';\n }\n\n function checkInstance(n: ESTree.NewExpression, kind: string): void {\n if (reported.has(n)) return;\n const args = n.arguments as ESTree.Node[];\n const last = args[args.length - 1];\n if (last?.type === 'ObjectExpression') {\n if (checkObject(last) === 'ok') return;\n } else if (\n args.some(\n (a) =>\n a.type !== 'Literal' &&\n a.type !== 'TemplateLiteral' &&\n a.type !== 'Identifier' &&\n a.type !== 'MemberExpression',\n )\n ) {\n return; // options built dynamically\n }\n reported.add(n);\n const prop = 'maxRetriesPerRequest: null';\n context.report({\n node: n,\n messageId: 'instance',\n data: { kind },\n suggest: [\n {\n messageId: 'addNull',\n fix: (fixer) => {\n if (last?.type === 'ObjectExpression') {\n const lastProp = last.properties[last.properties.length - 1];\n return lastProp\n ? fixer.insertTextAfter(lastProp, `, ${prop}`)\n : fixer.replaceText(last, `{ ${prop} }`);\n }\n if (last) return fixer.insertTextAfter(last, `, { ${prop} }`);\n const end = (n.range?.[1] ?? 1) - 1;\n return fixer.insertTextBeforeRange([end, end], `{ ${prop} }`);\n },\n },\n ],\n });\n }\n\n return {\n Program(program) {\n bull = collectImports(program, 'bullmq');\n ioredis = collectImports(program, 'ioredis');\n },\n NewExpression(node) {\n const kind = resolveClass(node, bull);\n const index = kind ? BLOCKING.get(kind) : undefined;\n if (!kind || index === undefined) return;\n const opts = node.arguments[index] as Node | undefined;\n if (opts?.type !== 'ObjectExpression') return;\n const conn = getProp(opts, 'connection')?.value as Node | undefined;\n if (!conn) return;\n if (conn.type === 'ObjectExpression') {\n checkObject(conn);\n } else if (conn.type === 'NewExpression' && isRedis(conn)) {\n checkInstance(conn, kind);\n } else if (conn.type === 'Identifier') {\n const def = findVariable(context, conn)?.defs[0];\n const init =\n def?.node.type === 'VariableDeclarator' ? (def.node.init as Node | null) : null;\n if (init?.type === 'NewExpression' && isRedis(init)) checkInstance(init, kind);\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { ESLint, Linter, Rule } from 'eslint';\nimport noDedupeRemove from './rules/no-jobid-dedupe-with-remove-on-complete.js';\nimport requireErrorListener from './rules/require-error-listener.js';\nimport validJobId from './rules/valid-job-id.js';\nimport workerConnectionMaxRetries from './rules/worker-connection-max-retries.js';\n\nconst rules: Record<string, Rule.RuleModule> = {\n 'require-error-listener': requireErrorListener,\n 'worker-connection-max-retries': workerConnectionMaxRetries,\n 'valid-job-id': validJobId,\n 'no-jobid-dedupe-with-remove-on-complete': noDedupeRemove,\n};\n\nconst recommendedRules: Linter.RulesRecord = {\n 'bullmq/require-error-listener': 'error',\n 'bullmq/worker-connection-max-retries': 'error',\n 'bullmq/valid-job-id': 'error',\n 'bullmq/no-jobid-dedupe-with-remove-on-complete': 'warn',\n};\nconst strictRules: Linter.RulesRecord = Object.fromEntries(\n Object.keys(recommendedRules).map((k) => [k, 'error']),\n);\n\nconst plugin = {\n meta: { name: 'eslint-plugin-bullmq', version: '0.1.0' },\n rules,\n configs: {} as {\n recommended: Linter.Config & { rules: Linter.RulesRecord };\n strict: Linter.Config & { rules: Linter.RulesRecord };\n 'legacy-recommended': { plugins: string[]; rules: Linter.RulesRecord };\n 'legacy-strict': { plugins: string[]; rules: Linter.RulesRecord };\n },\n} satisfies ESLint.Plugin;\n\nplugin.configs.recommended = {\n name: 'bullmq/recommended',\n plugins: { bullmq: plugin as ESLint.Plugin },\n rules: recommendedRules,\n};\nplugin.configs.strict = {\n name: 'bullmq/strict',\n plugins: { bullmq: plugin as ESLint.Plugin },\n rules: strictRules,\n};\nplugin.configs['legacy-recommended'] = { plugins: ['bullmq'], rules: recommendedRules };\nplugin.configs['legacy-strict'] = { plugins: ['bullmq'], rules: strictRules };\n\nexport default plugin;\n"]}
1
+ {"version":3,"sources":["/workspace/eslint-plugin-bullmq/dist/index.cjs","../src/utils.ts","../src/rules/no-jobid-dedupe-with-remove-on-complete.ts","../src/rules/require-error-listener.ts","../src/rules/valid-job-id.ts","../src/rules/worker-connection-max-retries.ts","../src/index.ts"],"names":[],"mappings":"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","file":"/workspace/eslint-plugin-bullmq/dist/index.cjs","sourcesContent":[null,"import type { Rule, Scope } from 'eslint';\nimport type * as ESTree from 'estree';\n\nexport type Node = ESTree.Node & { parent?: Node };\n\nconst REPO = 'https://github.com/Krrishkumar125/eslint-plugin-bullmq';\nexport const docsUrl = (name: string): string => `${REPO}/blob/main/docs/rules/${name}.md`;\n\nfunction moduleName(node: Node | null | undefined): string | undefined {\n if (\n node?.type === 'CallExpression' &&\n node.callee.type === 'Identifier' &&\n node.callee.name === 'require' &&\n node.arguments[0]?.type === 'Literal' &&\n typeof node.arguments[0].value === 'string'\n ) {\n return node.arguments[0].value;\n }\n return undefined;\n}\n\nexport interface Imports {\n /** local name -> imported name */\n named: Map<string, string>;\n namespaces: Set<string>;\n}\n\n/** Collect bindings imported (ESM or CommonJS) from the given module. */\nexport function collectImports(program: ESTree.Program, source: string): Imports {\n const named = new Map<string, string>();\n const namespaces = new Set<string>();\n const visitDeclarator = (d: ESTree.VariableDeclarator): void => {\n if (moduleName(d.init as Node) !== source) return;\n if (d.id.type === 'Identifier') namespaces.add(d.id.name);\n if (d.id.type === 'ObjectPattern') {\n for (const p of d.id.properties) {\n if (p.type === 'Property' && p.key.type === 'Identifier' && p.value.type === 'Identifier') {\n named.set(p.value.name, p.key.name);\n }\n }\n }\n };\n for (const stmt of program.body) {\n if (stmt.type === 'ImportDeclaration' && stmt.source.value === source) {\n for (const s of stmt.specifiers) {\n if (s.type === 'ImportSpecifier') {\n const imported =\n s.imported.type === 'Identifier' ? s.imported.name : String(s.imported.value);\n named.set(s.local.name, imported);\n } else if (s.type === 'ImportDefaultSpecifier') {\n named.set(s.local.name, 'default');\n } else {\n namespaces.add(s.local.name);\n }\n }\n } else if (stmt.type === 'VariableDeclaration') {\n stmt.declarations.forEach(visitDeclarator);\n }\n }\n return { named, namespaces };\n}\n\n/** Which exported class a `new X(...)` refers to, given the imports of one module. */\nexport function resolveClass(node: ESTree.NewExpression, imports: Imports): string | undefined {\n const callee = node.callee;\n if (callee.type === 'Identifier') return imports.named.get(callee.name);\n if (\n callee.type === 'MemberExpression' &&\n callee.object.type === 'Identifier' &&\n imports.namespaces.has(callee.object.name) &&\n !callee.computed &&\n callee.property.type === 'Identifier'\n ) {\n return callee.property.name;\n }\n return undefined;\n}\n\nexport function findVariable(\n context: Rule.RuleContext,\n node: ESTree.Identifier,\n): Scope.Variable | undefined {\n let scope: Scope.Scope | null = context.sourceCode.getScope(node);\n while (scope) {\n const v = scope.set.get(node.name);\n if (v) return v;\n scope = scope.upper;\n }\n return undefined;\n}\n\nexport function propKey(p: ESTree.Property | ESTree.SpreadElement): string | undefined {\n if (p.type !== 'Property' || p.computed) return undefined;\n if (p.key.type === 'Identifier') return p.key.name;\n if (p.key.type === 'Literal' && typeof p.key.value === 'string') return p.key.value;\n return undefined;\n}\n\nexport function getProp(obj: ESTree.ObjectExpression, key: string): ESTree.Property | undefined {\n return obj.properties.find((p): p is ESTree.Property => propKey(p) === key);\n}\n\nexport const hasSpread = (obj: ESTree.ObjectExpression): boolean =>\n obj.properties.some((p) => p.type === 'SpreadElement');\n\n/** Is this object literal a BullMQ job-options object (`queue.add(n, d, opts)` or `{ opts: {...} }`)? */\nexport function isJobOptions(obj: Node): boolean {\n const parent = obj.parent;\n if (!parent) return false;\n if (parent.type === 'CallExpression' && parent.arguments[2] === obj) {\n const c = parent.callee;\n return (\n c.type === 'MemberExpression' &&\n !c.computed &&\n c.property.type === 'Identifier' &&\n c.property.name === 'add'\n );\n }\n return parent.type === 'Property' && parent.value === obj && propKey(parent) === 'opts';\n}\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport { docsUrl, getProp, isJobOptions, type Node } from '../utils.js';\n\nconst removesImmediately = (p: ESTree.Property | undefined): boolean =>\n p?.value.type === 'Literal' && (p.value.value === true || p.value.value === 0);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Warn when jobId deduplication is combined with removeOnComplete: true, which lets duplicates run again',\n recommended: true,\n url: docsUrl('no-jobid-dedupe-with-remove-on-complete'),\n },\n schema: [],\n messages: {\n conflict:\n 'removeOnComplete: true deletes the job as soon as it finishes, so a later add() with the same jobId runs again. The jobId only dedupes while the job is still waiting or active. Keep completed jobs for your dedupe window with removeOnComplete: { age: <seconds> }, or use deduplication: { id, ttl }.',\n conflictDefault:\n \"This queue's defaultJobOptions has removeOnComplete: true, so this jobId only dedupes while the job is still waiting or active. A redelivery after completion runs again. Use removeOnComplete: { age: <seconds> } or deduplication: { id, ttl }.\",\n },\n },\n create(context) {\n const removingQueues = new Set<string>();\n return {\n VariableDeclarator(node) {\n const init = node.init;\n if (\n node.id.type !== 'Identifier' ||\n init?.type !== 'NewExpression' ||\n init.callee.type !== 'Identifier' ||\n init.callee.name !== 'Queue'\n ) {\n return;\n }\n const opts = init.arguments[1];\n const defaults =\n opts?.type === 'ObjectExpression' ? getProp(opts, 'defaultJobOptions')?.value : undefined;\n if (\n defaults?.type === 'ObjectExpression' &&\n removesImmediately(getProp(defaults, 'removeOnComplete'))\n ) {\n removingQueues.add(node.id.name);\n }\n },\n ObjectExpression(node) {\n if (!isJobOptions(node as Node) || !getProp(node, 'jobId')) return;\n const remove = getProp(node, 'removeOnComplete');\n if (removesImmediately(remove)) {\n context.report({ node: remove as ESTree.Property, messageId: 'conflict' });\n return;\n }\n const call = (node as Node).parent;\n if (\n !remove &&\n call?.type === 'CallExpression' &&\n call.callee.type === 'MemberExpression' &&\n call.callee.object.type === 'Identifier' &&\n removingQueues.has(call.callee.object.name)\n ) {\n context.report({ node: call, messageId: 'conflictDefault' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport {\n collectImports,\n docsUrl,\n findVariable,\n resolveClass,\n type Imports,\n type Node,\n} from '../utils.js';\n\nconst CHECKED = new Set(['Worker', 'Queue']);\nconst LISTEN = new Set(['on', 'once', 'addListener', 'prependListener']);\n\nfunction isErrorListener(member: Node | undefined, object: Node): boolean {\n if (member?.type !== 'MemberExpression' || member.object !== object) return false;\n const prop = member.property;\n const method =\n !member.computed && prop.type === 'Identifier'\n ? prop.name\n : prop.type === 'Literal'\n ? prop.value\n : undefined;\n if (typeof method !== 'string' || !LISTEN.has(method)) return false;\n const call = member.parent;\n const arg = call?.type === 'CallExpression' ? call.arguments[0] : undefined;\n return arg?.type === 'Literal' && arg.value === 'error';\n}\n\n/** The identifier is handed to code we cannot see, which may attach the listener. */\nfunction escapes(id: Node): boolean {\n const p = id.parent;\n if (!p) return false;\n switch (p.type) {\n case 'CallExpression':\n case 'NewExpression':\n return p.arguments.includes(id as ESTree.Expression);\n case 'ReturnStatement':\n case 'ArrayExpression':\n case 'ExportSpecifier':\n case 'ExportDefaultDeclaration':\n case 'SpreadElement':\n return true;\n case 'Property':\n return p.value === id;\n case 'AssignmentExpression':\n return p.right === id;\n case 'ArrowFunctionExpression':\n return p.body === id;\n default:\n return false;\n }\n}\n\n/** `x.close()` / `x.disconnect()`: a short-lived instance (admin query, health check, test). */\nfunction isClosed(id: Node): boolean {\n const m = id.parent;\n return (\n m?.type === 'MemberExpression' &&\n m.object === id &&\n !m.computed &&\n m.property.type === 'Identifier' &&\n (m.property.name === 'close' || m.property.name === 'disconnect') &&\n m.parent?.type === 'CallExpression'\n );\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: \"Require an 'error' listener on BullMQ Worker and Queue instances\",\n recommended: true,\n url: docsUrl('require-error-listener'),\n },\n schema: [],\n messages: {\n missing:\n \"This {{kind}} has no 'error' listener. Without one, a Redis or processor error can crash the process or silently stop the {{kind}}. Add {{kind}}.on('error', handler).\",\n },\n },\n create(context) {\n let imports: Imports;\n return {\n Program(program) {\n imports = collectImports(program, 'bullmq');\n },\n NewExpression(node) {\n const kind = resolveClass(node, imports);\n if (!kind || !CHECKED.has(kind)) return;\n const n = node as Node;\n const parent = n.parent;\n let id: ESTree.Identifier | undefined;\n if (parent?.type === 'MemberExpression') {\n if (!isErrorListener(parent, n))\n context.report({ node, messageId: 'missing', data: { kind } });\n return;\n }\n if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {\n if (parent.parent?.parent?.type === 'ExportNamedDeclaration') return;\n id = parent.id;\n } else if (parent?.type === 'AssignmentExpression' && parent.left.type === 'Identifier') {\n id = parent.left;\n } else if (parent?.type !== 'ExpressionStatement') {\n return; // returned, passed along, stored on `this`, etc.\n }\n if (id) {\n const variable = findVariable(context, id);\n const ok = variable?.references.some((ref) => {\n const ident = ref.identifier as Node;\n return isErrorListener(ident.parent, ident) || escapes(ident) || isClosed(ident);\n });\n if (ok || !variable) return;\n }\n context.report({ node, messageId: 'missing', data: { kind } });\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport { docsUrl, isJobOptions, propKey, type Node } from '../utils.js';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n hasSuggestions: true,\n docs: {\n description: \"Disallow BullMQ custom job ids that contain ':' or are integers\",\n recommended: true,\n url: docsUrl('valid-job-id'),\n },\n schema: [],\n messages: {\n colon:\n \"BullMQ uses ':' as its Redis key separator, so a custom jobId containing ':' is rejected. Use another separator such as '-'.\",\n integer:\n 'BullMQ rejects integer custom job ids (they collide with its auto-generated ids). Prefix it, e.g. `order-42`.',\n replaceColon: \"Replace ':' with '-'\",\n },\n },\n create(context) {\n return {\n Property(node) {\n if (propKey(node) !== 'jobId' || !isJobOptions((node as Node).parent as Node)) return;\n const v = node.value as Node;\n if (v.type === 'Literal') {\n if (typeof v.value === 'number' && Number.isInteger(v.value)) {\n context.report({ node: v, messageId: 'integer' });\n } else if (typeof v.value === 'string') {\n if (/^\\d+$/.test(v.value)) context.report({ node: v, messageId: 'integer' });\n else if (v.value.includes(':')) {\n const raw = context.sourceCode.getText(v);\n context.report({\n node: v,\n messageId: 'colon',\n suggest: [\n {\n messageId: 'replaceColon',\n fix: (f) => f.replaceText(v, raw.replace(/:/g, '-')),\n },\n ],\n });\n }\n }\n } else if (\n v.type === 'TemplateLiteral' &&\n v.quasis.some((q) => q.value.raw.includes(':'))\n ) {\n context.report({\n node: v,\n messageId: 'colon',\n suggest: [\n {\n messageId: 'replaceColon',\n fix: (f) =>\n v.quasis\n .filter((q) => q.value.raw.includes(':'))\n .map((q) => f.replaceText(q, context.sourceCode.getText(q).replace(/:/g, '-'))),\n },\n ],\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport {\n collectImports,\n docsUrl,\n findVariable,\n getProp,\n hasSpread,\n resolveClass,\n type Imports,\n type Node,\n} from '../utils.js';\n\nconst BLOCKING = new Map([\n ['Worker', 2],\n ['QueueEvents', 1],\n]);\nconst REDIS_FALLBACK = /^(IO)?Redis$|^Cluster$/;\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n hasSuggestions: true,\n docs: {\n description:\n 'Require `maxRetriesPerRequest: null` on ioredis connections used by BullMQ Workers and QueueEvents',\n recommended: true,\n url: docsUrl('worker-connection-max-retries'),\n },\n schema: [],\n messages: {\n instance:\n \"This ioredis connection is used by a BullMQ {{kind}} but does not set maxRetriesPerRequest: null. BullMQ throws 'maxRetriesPerRequest must be null' when the {{kind}} starts; set it so blocking commands survive Redis reconnects.\",\n explicit:\n 'BullMQ Workers need maxRetriesPerRequest: null; a finite value makes blocking commands fail during Redis reconnects.',\n addNull: 'Add maxRetriesPerRequest: null',\n setNull: 'Set maxRetriesPerRequest to null',\n },\n },\n create(context) {\n let bull: Imports;\n let ioredis: Imports;\n const reported = new Set<ESTree.Node>();\n\n const isRedis = (n: ESTree.NewExpression): boolean =>\n resolveClass(n, ioredis) !== undefined ||\n (n.callee.type === 'Identifier' && REDIS_FALLBACK.test(n.callee.name));\n\n function checkObject(obj: ESTree.ObjectExpression): 'ok' | 'missing' {\n const prop = getProp(obj, 'maxRetriesPerRequest');\n if (!prop) return hasSpread(obj) ? 'ok' : 'missing';\n const v = prop.value;\n if (v.type === 'Literal' && v.value === null) return 'ok';\n if (v.type === 'Literal' || (v.type === 'UnaryExpression' && v.argument.type === 'Literal')) {\n if (!reported.has(prop)) {\n reported.add(prop);\n context.report({\n node: prop,\n messageId: 'explicit',\n suggest: [{ messageId: 'setNull', fix: (f) => f.replaceText(v, 'null') }],\n });\n }\n }\n return 'ok';\n }\n\n function checkInstance(n: ESTree.NewExpression, kind: string): void {\n if (reported.has(n)) return;\n const args = n.arguments as ESTree.Node[];\n const last = args[args.length - 1];\n if (last?.type === 'ObjectExpression') {\n if (checkObject(last) === 'ok') return;\n } else if (\n args.some(\n (a) =>\n a.type !== 'Literal' &&\n a.type !== 'TemplateLiteral' &&\n a.type !== 'Identifier' &&\n a.type !== 'MemberExpression',\n )\n ) {\n return; // options built dynamically\n }\n reported.add(n);\n const prop = 'maxRetriesPerRequest: null';\n context.report({\n node: n,\n messageId: 'instance',\n data: { kind },\n suggest: [\n {\n messageId: 'addNull',\n fix: (fixer) => {\n if (last?.type === 'ObjectExpression') {\n const lastProp = last.properties[last.properties.length - 1];\n return lastProp\n ? fixer.insertTextAfter(lastProp, `, ${prop}`)\n : fixer.replaceText(last, `{ ${prop} }`);\n }\n if (last) return fixer.insertTextAfter(last, `, { ${prop} }`);\n const end = (n.range?.[1] ?? 1) - 1;\n return fixer.insertTextBeforeRange([end, end], `{ ${prop} }`);\n },\n },\n ],\n });\n }\n\n return {\n Program(program) {\n bull = collectImports(program, 'bullmq');\n ioredis = collectImports(program, 'ioredis');\n },\n NewExpression(node) {\n const kind = resolveClass(node, bull);\n const index = kind ? BLOCKING.get(kind) : undefined;\n if (!kind || index === undefined) return;\n const opts = node.arguments[index] as Node | undefined;\n if (opts?.type !== 'ObjectExpression') return;\n const conn = getProp(opts, 'connection')?.value as Node | undefined;\n if (!conn) return;\n if (conn.type === 'ObjectExpression') {\n checkObject(conn);\n } else if (conn.type === 'NewExpression' && isRedis(conn)) {\n checkInstance(conn, kind);\n } else if (conn.type === 'Identifier') {\n const def = findVariable(context, conn)?.defs[0];\n const init =\n def?.node.type === 'VariableDeclarator' ? (def.node.init as Node | null) : null;\n if (init?.type === 'NewExpression' && isRedis(init)) checkInstance(init, kind);\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { ESLint, Linter, Rule } from 'eslint';\nimport noDedupeRemove from './rules/no-jobid-dedupe-with-remove-on-complete.js';\nimport requireErrorListener from './rules/require-error-listener.js';\nimport validJobId from './rules/valid-job-id.js';\nimport workerConnectionMaxRetries from './rules/worker-connection-max-retries.js';\n\nconst rules: Record<string, Rule.RuleModule> = {\n 'require-error-listener': requireErrorListener,\n 'worker-connection-max-retries': workerConnectionMaxRetries,\n 'valid-job-id': validJobId,\n 'no-jobid-dedupe-with-remove-on-complete': noDedupeRemove,\n};\n\nconst recommendedRules: Linter.RulesRecord = {\n 'bullmq/require-error-listener': 'error',\n 'bullmq/worker-connection-max-retries': 'error',\n 'bullmq/valid-job-id': 'error',\n 'bullmq/no-jobid-dedupe-with-remove-on-complete': 'warn',\n};\nconst strictRules: Linter.RulesRecord = Object.fromEntries(\n Object.keys(recommendedRules).map((k) => [k, 'error']),\n);\n\nconst plugin = {\n meta: { name: 'eslint-plugin-bullmq', version: '0.1.1' },\n rules,\n configs: {} as {\n recommended: Linter.Config & { rules: Linter.RulesRecord };\n strict: Linter.Config & { rules: Linter.RulesRecord };\n 'legacy-recommended': { plugins: string[]; rules: Linter.RulesRecord };\n 'legacy-strict': { plugins: string[]; rules: Linter.RulesRecord };\n },\n} satisfies ESLint.Plugin;\n\nplugin.configs.recommended = {\n name: 'bullmq/recommended',\n plugins: { bullmq: plugin as ESLint.Plugin },\n rules: recommendedRules,\n};\nplugin.configs.strict = {\n name: 'bullmq/strict',\n plugins: { bullmq: plugin as ESLint.Plugin },\n rules: strictRules,\n};\nplugin.configs['legacy-recommended'] = { plugins: ['bullmq'], rules: recommendedRules };\nplugin.configs['legacy-strict'] = { plugins: ['bullmq'], rules: strictRules };\n\nexport default plugin;\n"]}
package/dist/index.js CHANGED
@@ -397,7 +397,7 @@ var strictRules = Object.fromEntries(
397
397
  Object.keys(recommendedRules).map((k) => [k, "error"])
398
398
  );
399
399
  var plugin = {
400
- meta: { name: "eslint-plugin-bullmq", version: "0.1.0" },
400
+ meta: { name: "eslint-plugin-bullmq", version: "0.1.1" },
401
401
  rules,
402
402
  configs: {}
403
403
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils.ts","../src/rules/no-jobid-dedupe-with-remove-on-complete.ts","../src/rules/require-error-listener.ts","../src/rules/valid-job-id.ts","../src/rules/worker-connection-max-retries.ts","../src/index.ts"],"sourcesContent":["import type { Rule, Scope } from 'eslint';\nimport type * as ESTree from 'estree';\n\nexport type Node = ESTree.Node & { parent?: Node };\n\nconst REPO = 'https://github.com/Krrishkumar125/eslint-plugin-bullmq';\nexport const docsUrl = (name: string): string => `${REPO}/blob/main/docs/rules/${name}.md`;\n\nfunction moduleName(node: Node | null | undefined): string | undefined {\n if (\n node?.type === 'CallExpression' &&\n node.callee.type === 'Identifier' &&\n node.callee.name === 'require' &&\n node.arguments[0]?.type === 'Literal' &&\n typeof node.arguments[0].value === 'string'\n ) {\n return node.arguments[0].value;\n }\n return undefined;\n}\n\nexport interface Imports {\n /** local name -> imported name */\n named: Map<string, string>;\n namespaces: Set<string>;\n}\n\n/** Collect bindings imported (ESM or CommonJS) from the given module. */\nexport function collectImports(program: ESTree.Program, source: string): Imports {\n const named = new Map<string, string>();\n const namespaces = new Set<string>();\n const visitDeclarator = (d: ESTree.VariableDeclarator): void => {\n if (moduleName(d.init as Node) !== source) return;\n if (d.id.type === 'Identifier') namespaces.add(d.id.name);\n if (d.id.type === 'ObjectPattern') {\n for (const p of d.id.properties) {\n if (p.type === 'Property' && p.key.type === 'Identifier' && p.value.type === 'Identifier') {\n named.set(p.value.name, p.key.name);\n }\n }\n }\n };\n for (const stmt of program.body) {\n if (stmt.type === 'ImportDeclaration' && stmt.source.value === source) {\n for (const s of stmt.specifiers) {\n if (s.type === 'ImportSpecifier') {\n const imported =\n s.imported.type === 'Identifier' ? s.imported.name : String(s.imported.value);\n named.set(s.local.name, imported);\n } else if (s.type === 'ImportDefaultSpecifier') {\n named.set(s.local.name, 'default');\n } else {\n namespaces.add(s.local.name);\n }\n }\n } else if (stmt.type === 'VariableDeclaration') {\n stmt.declarations.forEach(visitDeclarator);\n }\n }\n return { named, namespaces };\n}\n\n/** Which exported class a `new X(...)` refers to, given the imports of one module. */\nexport function resolveClass(node: ESTree.NewExpression, imports: Imports): string | undefined {\n const callee = node.callee;\n if (callee.type === 'Identifier') return imports.named.get(callee.name);\n if (\n callee.type === 'MemberExpression' &&\n callee.object.type === 'Identifier' &&\n imports.namespaces.has(callee.object.name) &&\n !callee.computed &&\n callee.property.type === 'Identifier'\n ) {\n return callee.property.name;\n }\n return undefined;\n}\n\nexport function findVariable(\n context: Rule.RuleContext,\n node: ESTree.Identifier,\n): Scope.Variable | undefined {\n let scope: Scope.Scope | null = context.sourceCode.getScope(node);\n while (scope) {\n const v = scope.set.get(node.name);\n if (v) return v;\n scope = scope.upper;\n }\n return undefined;\n}\n\nexport function propKey(p: ESTree.Property | ESTree.SpreadElement): string | undefined {\n if (p.type !== 'Property' || p.computed) return undefined;\n if (p.key.type === 'Identifier') return p.key.name;\n if (p.key.type === 'Literal' && typeof p.key.value === 'string') return p.key.value;\n return undefined;\n}\n\nexport function getProp(obj: ESTree.ObjectExpression, key: string): ESTree.Property | undefined {\n return obj.properties.find((p): p is ESTree.Property => propKey(p) === key);\n}\n\nexport const hasSpread = (obj: ESTree.ObjectExpression): boolean =>\n obj.properties.some((p) => p.type === 'SpreadElement');\n\n/** Is this object literal a BullMQ job-options object (`queue.add(n, d, opts)` or `{ opts: {...} }`)? */\nexport function isJobOptions(obj: Node): boolean {\n const parent = obj.parent;\n if (!parent) return false;\n if (parent.type === 'CallExpression' && parent.arguments[2] === obj) {\n const c = parent.callee;\n return (\n c.type === 'MemberExpression' &&\n !c.computed &&\n c.property.type === 'Identifier' &&\n c.property.name === 'add'\n );\n }\n return parent.type === 'Property' && parent.value === obj && propKey(parent) === 'opts';\n}\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport { docsUrl, getProp, isJobOptions, type Node } from '../utils.js';\n\nconst removesImmediately = (p: ESTree.Property | undefined): boolean =>\n p?.value.type === 'Literal' && (p.value.value === true || p.value.value === 0);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Warn when jobId deduplication is combined with removeOnComplete: true, which lets duplicates run again',\n recommended: true,\n url: docsUrl('no-jobid-dedupe-with-remove-on-complete'),\n },\n schema: [],\n messages: {\n conflict:\n 'removeOnComplete: true deletes the job as soon as it finishes, so a later add() with the same jobId runs again. The jobId only dedupes while the job is still waiting or active. Keep completed jobs for your dedupe window with removeOnComplete: { age: <seconds> }, or use deduplication: { id, ttl }.',\n conflictDefault:\n \"This queue's defaultJobOptions has removeOnComplete: true, so this jobId only dedupes while the job is still waiting or active. A redelivery after completion runs again. Use removeOnComplete: { age: <seconds> } or deduplication: { id, ttl }.\",\n },\n },\n create(context) {\n const removingQueues = new Set<string>();\n return {\n VariableDeclarator(node) {\n const init = node.init;\n if (\n node.id.type !== 'Identifier' ||\n init?.type !== 'NewExpression' ||\n init.callee.type !== 'Identifier' ||\n init.callee.name !== 'Queue'\n ) {\n return;\n }\n const opts = init.arguments[1];\n const defaults =\n opts?.type === 'ObjectExpression' ? getProp(opts, 'defaultJobOptions')?.value : undefined;\n if (\n defaults?.type === 'ObjectExpression' &&\n removesImmediately(getProp(defaults, 'removeOnComplete'))\n ) {\n removingQueues.add(node.id.name);\n }\n },\n ObjectExpression(node) {\n if (!isJobOptions(node as Node) || !getProp(node, 'jobId')) return;\n const remove = getProp(node, 'removeOnComplete');\n if (removesImmediately(remove)) {\n context.report({ node: remove as ESTree.Property, messageId: 'conflict' });\n return;\n }\n const call = (node as Node).parent;\n if (\n !remove &&\n call?.type === 'CallExpression' &&\n call.callee.type === 'MemberExpression' &&\n call.callee.object.type === 'Identifier' &&\n removingQueues.has(call.callee.object.name)\n ) {\n context.report({ node: call, messageId: 'conflictDefault' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport {\n collectImports,\n docsUrl,\n findVariable,\n resolveClass,\n type Imports,\n type Node,\n} from '../utils.js';\n\nconst CHECKED = new Set(['Worker', 'Queue']);\nconst LISTEN = new Set(['on', 'once', 'addListener', 'prependListener']);\n\nfunction isErrorListener(member: Node | undefined, object: Node): boolean {\n if (member?.type !== 'MemberExpression' || member.object !== object) return false;\n const prop = member.property;\n const method =\n !member.computed && prop.type === 'Identifier'\n ? prop.name\n : prop.type === 'Literal'\n ? prop.value\n : undefined;\n if (typeof method !== 'string' || !LISTEN.has(method)) return false;\n const call = member.parent;\n const arg = call?.type === 'CallExpression' ? call.arguments[0] : undefined;\n return arg?.type === 'Literal' && arg.value === 'error';\n}\n\n/** The identifier is handed to code we cannot see, which may attach the listener. */\nfunction escapes(id: Node): boolean {\n const p = id.parent;\n if (!p) return false;\n switch (p.type) {\n case 'CallExpression':\n case 'NewExpression':\n return p.arguments.includes(id as ESTree.Expression);\n case 'ReturnStatement':\n case 'ArrayExpression':\n case 'ExportSpecifier':\n case 'ExportDefaultDeclaration':\n case 'SpreadElement':\n return true;\n case 'Property':\n return p.value === id;\n case 'AssignmentExpression':\n return p.right === id;\n case 'ArrowFunctionExpression':\n return p.body === id;\n default:\n return false;\n }\n}\n\n/** `x.close()` / `x.disconnect()`: a short-lived instance (admin query, health check, test). */\nfunction isClosed(id: Node): boolean {\n const m = id.parent;\n return (\n m?.type === 'MemberExpression' &&\n m.object === id &&\n !m.computed &&\n m.property.type === 'Identifier' &&\n (m.property.name === 'close' || m.property.name === 'disconnect') &&\n m.parent?.type === 'CallExpression'\n );\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: \"Require an 'error' listener on BullMQ Worker and Queue instances\",\n recommended: true,\n url: docsUrl('require-error-listener'),\n },\n schema: [],\n messages: {\n missing:\n \"This {{kind}} has no 'error' listener. Without one, a Redis or processor error can crash the process or silently stop the {{kind}}. Add {{kind}}.on('error', handler).\",\n },\n },\n create(context) {\n let imports: Imports;\n return {\n Program(program) {\n imports = collectImports(program, 'bullmq');\n },\n NewExpression(node) {\n const kind = resolveClass(node, imports);\n if (!kind || !CHECKED.has(kind)) return;\n const n = node as Node;\n const parent = n.parent;\n let id: ESTree.Identifier | undefined;\n if (parent?.type === 'MemberExpression') {\n if (!isErrorListener(parent, n))\n context.report({ node, messageId: 'missing', data: { kind } });\n return;\n }\n if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {\n if (parent.parent?.parent?.type === 'ExportNamedDeclaration') return;\n id = parent.id;\n } else if (parent?.type === 'AssignmentExpression' && parent.left.type === 'Identifier') {\n id = parent.left;\n } else if (parent?.type !== 'ExpressionStatement') {\n return; // returned, passed along, stored on `this`, etc.\n }\n if (id) {\n const variable = findVariable(context, id);\n const ok = variable?.references.some((ref) => {\n const ident = ref.identifier as Node;\n return isErrorListener(ident.parent, ident) || escapes(ident) || isClosed(ident);\n });\n if (ok || !variable) return;\n }\n context.report({ node, messageId: 'missing', data: { kind } });\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport { docsUrl, isJobOptions, propKey, type Node } from '../utils.js';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n hasSuggestions: true,\n docs: {\n description: \"Disallow BullMQ custom job ids that contain ':' or are integers\",\n recommended: true,\n url: docsUrl('valid-job-id'),\n },\n schema: [],\n messages: {\n colon:\n \"BullMQ uses ':' as its Redis key separator, so a custom jobId containing ':' is rejected. Use another separator such as '-'.\",\n integer:\n 'BullMQ rejects integer custom job ids (they collide with its auto-generated ids). Prefix it, e.g. `order-42`.',\n replaceColon: \"Replace ':' with '-'\",\n },\n },\n create(context) {\n return {\n Property(node) {\n if (propKey(node) !== 'jobId' || !isJobOptions((node as Node).parent as Node)) return;\n const v = node.value as Node;\n if (v.type === 'Literal') {\n if (typeof v.value === 'number' && Number.isInteger(v.value)) {\n context.report({ node: v, messageId: 'integer' });\n } else if (typeof v.value === 'string') {\n if (/^\\d+$/.test(v.value)) context.report({ node: v, messageId: 'integer' });\n else if (v.value.includes(':')) {\n const raw = context.sourceCode.getText(v);\n context.report({\n node: v,\n messageId: 'colon',\n suggest: [\n {\n messageId: 'replaceColon',\n fix: (f) => f.replaceText(v, raw.replace(/:/g, '-')),\n },\n ],\n });\n }\n }\n } else if (\n v.type === 'TemplateLiteral' &&\n v.quasis.some((q) => q.value.raw.includes(':'))\n ) {\n context.report({\n node: v,\n messageId: 'colon',\n suggest: [\n {\n messageId: 'replaceColon',\n fix: (f) =>\n v.quasis\n .filter((q) => q.value.raw.includes(':'))\n .map((q) => f.replaceText(q, context.sourceCode.getText(q).replace(/:/g, '-'))),\n },\n ],\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport {\n collectImports,\n docsUrl,\n findVariable,\n getProp,\n hasSpread,\n resolveClass,\n type Imports,\n type Node,\n} from '../utils.js';\n\nconst BLOCKING = new Map([\n ['Worker', 2],\n ['QueueEvents', 1],\n]);\nconst REDIS_FALLBACK = /^(IO)?Redis$|^Cluster$/;\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n hasSuggestions: true,\n docs: {\n description:\n 'Require `maxRetriesPerRequest: null` on ioredis connections used by BullMQ Workers and QueueEvents',\n recommended: true,\n url: docsUrl('worker-connection-max-retries'),\n },\n schema: [],\n messages: {\n instance:\n \"This ioredis connection is used by a BullMQ {{kind}} but does not set maxRetriesPerRequest: null. BullMQ throws 'maxRetriesPerRequest must be null' when the {{kind}} starts; set it so blocking commands survive Redis reconnects.\",\n explicit:\n 'BullMQ Workers need maxRetriesPerRequest: null; a finite value makes blocking commands fail during Redis reconnects.',\n addNull: 'Add maxRetriesPerRequest: null',\n setNull: 'Set maxRetriesPerRequest to null',\n },\n },\n create(context) {\n let bull: Imports;\n let ioredis: Imports;\n const reported = new Set<ESTree.Node>();\n\n const isRedis = (n: ESTree.NewExpression): boolean =>\n resolveClass(n, ioredis) !== undefined ||\n (n.callee.type === 'Identifier' && REDIS_FALLBACK.test(n.callee.name));\n\n function checkObject(obj: ESTree.ObjectExpression): 'ok' | 'missing' {\n const prop = getProp(obj, 'maxRetriesPerRequest');\n if (!prop) return hasSpread(obj) ? 'ok' : 'missing';\n const v = prop.value;\n if (v.type === 'Literal' && v.value === null) return 'ok';\n if (v.type === 'Literal' || (v.type === 'UnaryExpression' && v.argument.type === 'Literal')) {\n if (!reported.has(prop)) {\n reported.add(prop);\n context.report({\n node: prop,\n messageId: 'explicit',\n suggest: [{ messageId: 'setNull', fix: (f) => f.replaceText(v, 'null') }],\n });\n }\n }\n return 'ok';\n }\n\n function checkInstance(n: ESTree.NewExpression, kind: string): void {\n if (reported.has(n)) return;\n const args = n.arguments as ESTree.Node[];\n const last = args[args.length - 1];\n if (last?.type === 'ObjectExpression') {\n if (checkObject(last) === 'ok') return;\n } else if (\n args.some(\n (a) =>\n a.type !== 'Literal' &&\n a.type !== 'TemplateLiteral' &&\n a.type !== 'Identifier' &&\n a.type !== 'MemberExpression',\n )\n ) {\n return; // options built dynamically\n }\n reported.add(n);\n const prop = 'maxRetriesPerRequest: null';\n context.report({\n node: n,\n messageId: 'instance',\n data: { kind },\n suggest: [\n {\n messageId: 'addNull',\n fix: (fixer) => {\n if (last?.type === 'ObjectExpression') {\n const lastProp = last.properties[last.properties.length - 1];\n return lastProp\n ? fixer.insertTextAfter(lastProp, `, ${prop}`)\n : fixer.replaceText(last, `{ ${prop} }`);\n }\n if (last) return fixer.insertTextAfter(last, `, { ${prop} }`);\n const end = (n.range?.[1] ?? 1) - 1;\n return fixer.insertTextBeforeRange([end, end], `{ ${prop} }`);\n },\n },\n ],\n });\n }\n\n return {\n Program(program) {\n bull = collectImports(program, 'bullmq');\n ioredis = collectImports(program, 'ioredis');\n },\n NewExpression(node) {\n const kind = resolveClass(node, bull);\n const index = kind ? BLOCKING.get(kind) : undefined;\n if (!kind || index === undefined) return;\n const opts = node.arguments[index] as Node | undefined;\n if (opts?.type !== 'ObjectExpression') return;\n const conn = getProp(opts, 'connection')?.value as Node | undefined;\n if (!conn) return;\n if (conn.type === 'ObjectExpression') {\n checkObject(conn);\n } else if (conn.type === 'NewExpression' && isRedis(conn)) {\n checkInstance(conn, kind);\n } else if (conn.type === 'Identifier') {\n const def = findVariable(context, conn)?.defs[0];\n const init =\n def?.node.type === 'VariableDeclarator' ? (def.node.init as Node | null) : null;\n if (init?.type === 'NewExpression' && isRedis(init)) checkInstance(init, kind);\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { ESLint, Linter, Rule } from 'eslint';\nimport noDedupeRemove from './rules/no-jobid-dedupe-with-remove-on-complete.js';\nimport requireErrorListener from './rules/require-error-listener.js';\nimport validJobId from './rules/valid-job-id.js';\nimport workerConnectionMaxRetries from './rules/worker-connection-max-retries.js';\n\nconst rules: Record<string, Rule.RuleModule> = {\n 'require-error-listener': requireErrorListener,\n 'worker-connection-max-retries': workerConnectionMaxRetries,\n 'valid-job-id': validJobId,\n 'no-jobid-dedupe-with-remove-on-complete': noDedupeRemove,\n};\n\nconst recommendedRules: Linter.RulesRecord = {\n 'bullmq/require-error-listener': 'error',\n 'bullmq/worker-connection-max-retries': 'error',\n 'bullmq/valid-job-id': 'error',\n 'bullmq/no-jobid-dedupe-with-remove-on-complete': 'warn',\n};\nconst strictRules: Linter.RulesRecord = Object.fromEntries(\n Object.keys(recommendedRules).map((k) => [k, 'error']),\n);\n\nconst plugin = {\n meta: { name: 'eslint-plugin-bullmq', version: '0.1.0' },\n rules,\n configs: {} as {\n recommended: Linter.Config & { rules: Linter.RulesRecord };\n strict: Linter.Config & { rules: Linter.RulesRecord };\n 'legacy-recommended': { plugins: string[]; rules: Linter.RulesRecord };\n 'legacy-strict': { plugins: string[]; rules: Linter.RulesRecord };\n },\n} satisfies ESLint.Plugin;\n\nplugin.configs.recommended = {\n name: 'bullmq/recommended',\n plugins: { bullmq: plugin as ESLint.Plugin },\n rules: recommendedRules,\n};\nplugin.configs.strict = {\n name: 'bullmq/strict',\n plugins: { bullmq: plugin as ESLint.Plugin },\n rules: strictRules,\n};\nplugin.configs['legacy-recommended'] = { plugins: ['bullmq'], rules: recommendedRules };\nplugin.configs['legacy-strict'] = { plugins: ['bullmq'], rules: strictRules };\n\nexport default plugin;\n"],"mappings":"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","names":["rule","rule","rule"]}
1
+ {"version":3,"sources":["../src/utils.ts","../src/rules/no-jobid-dedupe-with-remove-on-complete.ts","../src/rules/require-error-listener.ts","../src/rules/valid-job-id.ts","../src/rules/worker-connection-max-retries.ts","../src/index.ts"],"sourcesContent":["import type { Rule, Scope } from 'eslint';\nimport type * as ESTree from 'estree';\n\nexport type Node = ESTree.Node & { parent?: Node };\n\nconst REPO = 'https://github.com/Krrishkumar125/eslint-plugin-bullmq';\nexport const docsUrl = (name: string): string => `${REPO}/blob/main/docs/rules/${name}.md`;\n\nfunction moduleName(node: Node | null | undefined): string | undefined {\n if (\n node?.type === 'CallExpression' &&\n node.callee.type === 'Identifier' &&\n node.callee.name === 'require' &&\n node.arguments[0]?.type === 'Literal' &&\n typeof node.arguments[0].value === 'string'\n ) {\n return node.arguments[0].value;\n }\n return undefined;\n}\n\nexport interface Imports {\n /** local name -> imported name */\n named: Map<string, string>;\n namespaces: Set<string>;\n}\n\n/** Collect bindings imported (ESM or CommonJS) from the given module. */\nexport function collectImports(program: ESTree.Program, source: string): Imports {\n const named = new Map<string, string>();\n const namespaces = new Set<string>();\n const visitDeclarator = (d: ESTree.VariableDeclarator): void => {\n if (moduleName(d.init as Node) !== source) return;\n if (d.id.type === 'Identifier') namespaces.add(d.id.name);\n if (d.id.type === 'ObjectPattern') {\n for (const p of d.id.properties) {\n if (p.type === 'Property' && p.key.type === 'Identifier' && p.value.type === 'Identifier') {\n named.set(p.value.name, p.key.name);\n }\n }\n }\n };\n for (const stmt of program.body) {\n if (stmt.type === 'ImportDeclaration' && stmt.source.value === source) {\n for (const s of stmt.specifiers) {\n if (s.type === 'ImportSpecifier') {\n const imported =\n s.imported.type === 'Identifier' ? s.imported.name : String(s.imported.value);\n named.set(s.local.name, imported);\n } else if (s.type === 'ImportDefaultSpecifier') {\n named.set(s.local.name, 'default');\n } else {\n namespaces.add(s.local.name);\n }\n }\n } else if (stmt.type === 'VariableDeclaration') {\n stmt.declarations.forEach(visitDeclarator);\n }\n }\n return { named, namespaces };\n}\n\n/** Which exported class a `new X(...)` refers to, given the imports of one module. */\nexport function resolveClass(node: ESTree.NewExpression, imports: Imports): string | undefined {\n const callee = node.callee;\n if (callee.type === 'Identifier') return imports.named.get(callee.name);\n if (\n callee.type === 'MemberExpression' &&\n callee.object.type === 'Identifier' &&\n imports.namespaces.has(callee.object.name) &&\n !callee.computed &&\n callee.property.type === 'Identifier'\n ) {\n return callee.property.name;\n }\n return undefined;\n}\n\nexport function findVariable(\n context: Rule.RuleContext,\n node: ESTree.Identifier,\n): Scope.Variable | undefined {\n let scope: Scope.Scope | null = context.sourceCode.getScope(node);\n while (scope) {\n const v = scope.set.get(node.name);\n if (v) return v;\n scope = scope.upper;\n }\n return undefined;\n}\n\nexport function propKey(p: ESTree.Property | ESTree.SpreadElement): string | undefined {\n if (p.type !== 'Property' || p.computed) return undefined;\n if (p.key.type === 'Identifier') return p.key.name;\n if (p.key.type === 'Literal' && typeof p.key.value === 'string') return p.key.value;\n return undefined;\n}\n\nexport function getProp(obj: ESTree.ObjectExpression, key: string): ESTree.Property | undefined {\n return obj.properties.find((p): p is ESTree.Property => propKey(p) === key);\n}\n\nexport const hasSpread = (obj: ESTree.ObjectExpression): boolean =>\n obj.properties.some((p) => p.type === 'SpreadElement');\n\n/** Is this object literal a BullMQ job-options object (`queue.add(n, d, opts)` or `{ opts: {...} }`)? */\nexport function isJobOptions(obj: Node): boolean {\n const parent = obj.parent;\n if (!parent) return false;\n if (parent.type === 'CallExpression' && parent.arguments[2] === obj) {\n const c = parent.callee;\n return (\n c.type === 'MemberExpression' &&\n !c.computed &&\n c.property.type === 'Identifier' &&\n c.property.name === 'add'\n );\n }\n return parent.type === 'Property' && parent.value === obj && propKey(parent) === 'opts';\n}\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport { docsUrl, getProp, isJobOptions, type Node } from '../utils.js';\n\nconst removesImmediately = (p: ESTree.Property | undefined): boolean =>\n p?.value.type === 'Literal' && (p.value.value === true || p.value.value === 0);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Warn when jobId deduplication is combined with removeOnComplete: true, which lets duplicates run again',\n recommended: true,\n url: docsUrl('no-jobid-dedupe-with-remove-on-complete'),\n },\n schema: [],\n messages: {\n conflict:\n 'removeOnComplete: true deletes the job as soon as it finishes, so a later add() with the same jobId runs again. The jobId only dedupes while the job is still waiting or active. Keep completed jobs for your dedupe window with removeOnComplete: { age: <seconds> }, or use deduplication: { id, ttl }.',\n conflictDefault:\n \"This queue's defaultJobOptions has removeOnComplete: true, so this jobId only dedupes while the job is still waiting or active. A redelivery after completion runs again. Use removeOnComplete: { age: <seconds> } or deduplication: { id, ttl }.\",\n },\n },\n create(context) {\n const removingQueues = new Set<string>();\n return {\n VariableDeclarator(node) {\n const init = node.init;\n if (\n node.id.type !== 'Identifier' ||\n init?.type !== 'NewExpression' ||\n init.callee.type !== 'Identifier' ||\n init.callee.name !== 'Queue'\n ) {\n return;\n }\n const opts = init.arguments[1];\n const defaults =\n opts?.type === 'ObjectExpression' ? getProp(opts, 'defaultJobOptions')?.value : undefined;\n if (\n defaults?.type === 'ObjectExpression' &&\n removesImmediately(getProp(defaults, 'removeOnComplete'))\n ) {\n removingQueues.add(node.id.name);\n }\n },\n ObjectExpression(node) {\n if (!isJobOptions(node as Node) || !getProp(node, 'jobId')) return;\n const remove = getProp(node, 'removeOnComplete');\n if (removesImmediately(remove)) {\n context.report({ node: remove as ESTree.Property, messageId: 'conflict' });\n return;\n }\n const call = (node as Node).parent;\n if (\n !remove &&\n call?.type === 'CallExpression' &&\n call.callee.type === 'MemberExpression' &&\n call.callee.object.type === 'Identifier' &&\n removingQueues.has(call.callee.object.name)\n ) {\n context.report({ node: call, messageId: 'conflictDefault' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport {\n collectImports,\n docsUrl,\n findVariable,\n resolveClass,\n type Imports,\n type Node,\n} from '../utils.js';\n\nconst CHECKED = new Set(['Worker', 'Queue']);\nconst LISTEN = new Set(['on', 'once', 'addListener', 'prependListener']);\n\nfunction isErrorListener(member: Node | undefined, object: Node): boolean {\n if (member?.type !== 'MemberExpression' || member.object !== object) return false;\n const prop = member.property;\n const method =\n !member.computed && prop.type === 'Identifier'\n ? prop.name\n : prop.type === 'Literal'\n ? prop.value\n : undefined;\n if (typeof method !== 'string' || !LISTEN.has(method)) return false;\n const call = member.parent;\n const arg = call?.type === 'CallExpression' ? call.arguments[0] : undefined;\n return arg?.type === 'Literal' && arg.value === 'error';\n}\n\n/** The identifier is handed to code we cannot see, which may attach the listener. */\nfunction escapes(id: Node): boolean {\n const p = id.parent;\n if (!p) return false;\n switch (p.type) {\n case 'CallExpression':\n case 'NewExpression':\n return p.arguments.includes(id as ESTree.Expression);\n case 'ReturnStatement':\n case 'ArrayExpression':\n case 'ExportSpecifier':\n case 'ExportDefaultDeclaration':\n case 'SpreadElement':\n return true;\n case 'Property':\n return p.value === id;\n case 'AssignmentExpression':\n return p.right === id;\n case 'ArrowFunctionExpression':\n return p.body === id;\n default:\n return false;\n }\n}\n\n/** `x.close()` / `x.disconnect()`: a short-lived instance (admin query, health check, test). */\nfunction isClosed(id: Node): boolean {\n const m = id.parent;\n return (\n m?.type === 'MemberExpression' &&\n m.object === id &&\n !m.computed &&\n m.property.type === 'Identifier' &&\n (m.property.name === 'close' || m.property.name === 'disconnect') &&\n m.parent?.type === 'CallExpression'\n );\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: \"Require an 'error' listener on BullMQ Worker and Queue instances\",\n recommended: true,\n url: docsUrl('require-error-listener'),\n },\n schema: [],\n messages: {\n missing:\n \"This {{kind}} has no 'error' listener. Without one, a Redis or processor error can crash the process or silently stop the {{kind}}. Add {{kind}}.on('error', handler).\",\n },\n },\n create(context) {\n let imports: Imports;\n return {\n Program(program) {\n imports = collectImports(program, 'bullmq');\n },\n NewExpression(node) {\n const kind = resolveClass(node, imports);\n if (!kind || !CHECKED.has(kind)) return;\n const n = node as Node;\n const parent = n.parent;\n let id: ESTree.Identifier | undefined;\n if (parent?.type === 'MemberExpression') {\n if (!isErrorListener(parent, n))\n context.report({ node, messageId: 'missing', data: { kind } });\n return;\n }\n if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {\n if (parent.parent?.parent?.type === 'ExportNamedDeclaration') return;\n id = parent.id;\n } else if (parent?.type === 'AssignmentExpression' && parent.left.type === 'Identifier') {\n id = parent.left;\n } else if (parent?.type !== 'ExpressionStatement') {\n return; // returned, passed along, stored on `this`, etc.\n }\n if (id) {\n const variable = findVariable(context, id);\n const ok = variable?.references.some((ref) => {\n const ident = ref.identifier as Node;\n return isErrorListener(ident.parent, ident) || escapes(ident) || isClosed(ident);\n });\n if (ok || !variable) return;\n }\n context.report({ node, messageId: 'missing', data: { kind } });\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport { docsUrl, isJobOptions, propKey, type Node } from '../utils.js';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n hasSuggestions: true,\n docs: {\n description: \"Disallow BullMQ custom job ids that contain ':' or are integers\",\n recommended: true,\n url: docsUrl('valid-job-id'),\n },\n schema: [],\n messages: {\n colon:\n \"BullMQ uses ':' as its Redis key separator, so a custom jobId containing ':' is rejected. Use another separator such as '-'.\",\n integer:\n 'BullMQ rejects integer custom job ids (they collide with its auto-generated ids). Prefix it, e.g. `order-42`.',\n replaceColon: \"Replace ':' with '-'\",\n },\n },\n create(context) {\n return {\n Property(node) {\n if (propKey(node) !== 'jobId' || !isJobOptions((node as Node).parent as Node)) return;\n const v = node.value as Node;\n if (v.type === 'Literal') {\n if (typeof v.value === 'number' && Number.isInteger(v.value)) {\n context.report({ node: v, messageId: 'integer' });\n } else if (typeof v.value === 'string') {\n if (/^\\d+$/.test(v.value)) context.report({ node: v, messageId: 'integer' });\n else if (v.value.includes(':')) {\n const raw = context.sourceCode.getText(v);\n context.report({\n node: v,\n messageId: 'colon',\n suggest: [\n {\n messageId: 'replaceColon',\n fix: (f) => f.replaceText(v, raw.replace(/:/g, '-')),\n },\n ],\n });\n }\n }\n } else if (\n v.type === 'TemplateLiteral' &&\n v.quasis.some((q) => q.value.raw.includes(':'))\n ) {\n context.report({\n node: v,\n messageId: 'colon',\n suggest: [\n {\n messageId: 'replaceColon',\n fix: (f) =>\n v.quasis\n .filter((q) => q.value.raw.includes(':'))\n .map((q) => f.replaceText(q, context.sourceCode.getText(q).replace(/:/g, '-'))),\n },\n ],\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport {\n collectImports,\n docsUrl,\n findVariable,\n getProp,\n hasSpread,\n resolveClass,\n type Imports,\n type Node,\n} from '../utils.js';\n\nconst BLOCKING = new Map([\n ['Worker', 2],\n ['QueueEvents', 1],\n]);\nconst REDIS_FALLBACK = /^(IO)?Redis$|^Cluster$/;\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n hasSuggestions: true,\n docs: {\n description:\n 'Require `maxRetriesPerRequest: null` on ioredis connections used by BullMQ Workers and QueueEvents',\n recommended: true,\n url: docsUrl('worker-connection-max-retries'),\n },\n schema: [],\n messages: {\n instance:\n \"This ioredis connection is used by a BullMQ {{kind}} but does not set maxRetriesPerRequest: null. BullMQ throws 'maxRetriesPerRequest must be null' when the {{kind}} starts; set it so blocking commands survive Redis reconnects.\",\n explicit:\n 'BullMQ Workers need maxRetriesPerRequest: null; a finite value makes blocking commands fail during Redis reconnects.',\n addNull: 'Add maxRetriesPerRequest: null',\n setNull: 'Set maxRetriesPerRequest to null',\n },\n },\n create(context) {\n let bull: Imports;\n let ioredis: Imports;\n const reported = new Set<ESTree.Node>();\n\n const isRedis = (n: ESTree.NewExpression): boolean =>\n resolveClass(n, ioredis) !== undefined ||\n (n.callee.type === 'Identifier' && REDIS_FALLBACK.test(n.callee.name));\n\n function checkObject(obj: ESTree.ObjectExpression): 'ok' | 'missing' {\n const prop = getProp(obj, 'maxRetriesPerRequest');\n if (!prop) return hasSpread(obj) ? 'ok' : 'missing';\n const v = prop.value;\n if (v.type === 'Literal' && v.value === null) return 'ok';\n if (v.type === 'Literal' || (v.type === 'UnaryExpression' && v.argument.type === 'Literal')) {\n if (!reported.has(prop)) {\n reported.add(prop);\n context.report({\n node: prop,\n messageId: 'explicit',\n suggest: [{ messageId: 'setNull', fix: (f) => f.replaceText(v, 'null') }],\n });\n }\n }\n return 'ok';\n }\n\n function checkInstance(n: ESTree.NewExpression, kind: string): void {\n if (reported.has(n)) return;\n const args = n.arguments as ESTree.Node[];\n const last = args[args.length - 1];\n if (last?.type === 'ObjectExpression') {\n if (checkObject(last) === 'ok') return;\n } else if (\n args.some(\n (a) =>\n a.type !== 'Literal' &&\n a.type !== 'TemplateLiteral' &&\n a.type !== 'Identifier' &&\n a.type !== 'MemberExpression',\n )\n ) {\n return; // options built dynamically\n }\n reported.add(n);\n const prop = 'maxRetriesPerRequest: null';\n context.report({\n node: n,\n messageId: 'instance',\n data: { kind },\n suggest: [\n {\n messageId: 'addNull',\n fix: (fixer) => {\n if (last?.type === 'ObjectExpression') {\n const lastProp = last.properties[last.properties.length - 1];\n return lastProp\n ? fixer.insertTextAfter(lastProp, `, ${prop}`)\n : fixer.replaceText(last, `{ ${prop} }`);\n }\n if (last) return fixer.insertTextAfter(last, `, { ${prop} }`);\n const end = (n.range?.[1] ?? 1) - 1;\n return fixer.insertTextBeforeRange([end, end], `{ ${prop} }`);\n },\n },\n ],\n });\n }\n\n return {\n Program(program) {\n bull = collectImports(program, 'bullmq');\n ioredis = collectImports(program, 'ioredis');\n },\n NewExpression(node) {\n const kind = resolveClass(node, bull);\n const index = kind ? BLOCKING.get(kind) : undefined;\n if (!kind || index === undefined) return;\n const opts = node.arguments[index] as Node | undefined;\n if (opts?.type !== 'ObjectExpression') return;\n const conn = getProp(opts, 'connection')?.value as Node | undefined;\n if (!conn) return;\n if (conn.type === 'ObjectExpression') {\n checkObject(conn);\n } else if (conn.type === 'NewExpression' && isRedis(conn)) {\n checkInstance(conn, kind);\n } else if (conn.type === 'Identifier') {\n const def = findVariable(context, conn)?.defs[0];\n const init =\n def?.node.type === 'VariableDeclarator' ? (def.node.init as Node | null) : null;\n if (init?.type === 'NewExpression' && isRedis(init)) checkInstance(init, kind);\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { ESLint, Linter, Rule } from 'eslint';\nimport noDedupeRemove from './rules/no-jobid-dedupe-with-remove-on-complete.js';\nimport requireErrorListener from './rules/require-error-listener.js';\nimport validJobId from './rules/valid-job-id.js';\nimport workerConnectionMaxRetries from './rules/worker-connection-max-retries.js';\n\nconst rules: Record<string, Rule.RuleModule> = {\n 'require-error-listener': requireErrorListener,\n 'worker-connection-max-retries': workerConnectionMaxRetries,\n 'valid-job-id': validJobId,\n 'no-jobid-dedupe-with-remove-on-complete': noDedupeRemove,\n};\n\nconst recommendedRules: Linter.RulesRecord = {\n 'bullmq/require-error-listener': 'error',\n 'bullmq/worker-connection-max-retries': 'error',\n 'bullmq/valid-job-id': 'error',\n 'bullmq/no-jobid-dedupe-with-remove-on-complete': 'warn',\n};\nconst strictRules: Linter.RulesRecord = Object.fromEntries(\n Object.keys(recommendedRules).map((k) => [k, 'error']),\n);\n\nconst plugin = {\n meta: { name: 'eslint-plugin-bullmq', version: '0.1.1' },\n rules,\n configs: {} as {\n recommended: Linter.Config & { rules: Linter.RulesRecord };\n strict: Linter.Config & { rules: Linter.RulesRecord };\n 'legacy-recommended': { plugins: string[]; rules: Linter.RulesRecord };\n 'legacy-strict': { plugins: string[]; rules: Linter.RulesRecord };\n },\n} satisfies ESLint.Plugin;\n\nplugin.configs.recommended = {\n name: 'bullmq/recommended',\n plugins: { bullmq: plugin as ESLint.Plugin },\n rules: recommendedRules,\n};\nplugin.configs.strict = {\n name: 'bullmq/strict',\n plugins: { bullmq: plugin as ESLint.Plugin },\n rules: strictRules,\n};\nplugin.configs['legacy-recommended'] = { plugins: ['bullmq'], rules: recommendedRules };\nplugin.configs['legacy-strict'] = { plugins: ['bullmq'], rules: strictRules };\n\nexport default plugin;\n"],"mappings":"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","names":["rule","rule","rule"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-bullmq",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "ESLint rules for BullMQ production pitfalls from the official going-to-production guide: missing error listeners, worker connections without maxRetriesPerRequest: null, invalid custom job ids, and jobId dedupe that removeOnComplete silently disables.",
5
5
  "license": "MIT",
6
6
  "author": "Krrish Kumar (https://github.com/Krrishkumar125)",