| 2568 | // receivers, so the candidate set stays small. |
| 2569 | const taskCache = new Map<string, boolean>(); |
| 2570 | const isCeleryTask = (node: Node): boolean => { |
| 2571 | let v = taskCache.get(node.id); |
| 2572 | if (v !== undefined) return v; |
| 2573 | v = false; |
| 2574 | if (node.kind === 'function' && CELERY_PY_EXT.test(node.filePath)) { |
| 2575 | const content = ctx.readFile(node.filePath); |
| 2576 | if (content) { |
| 2577 | const lines = content.split('\n'); |
| 2578 | // startLine is the `def` line (decorators sit ABOVE it). Walk upward, stopping at the |
| 2579 | // previous declaration so a non-task def can never inherit the prior def's decorator. |
| 2580 | const stop = Math.max(0, node.startLine - 1 - CELERY_DECORATOR_LOOKBACK); |
| 2581 | for (let i = node.startLine - 2; i >= stop; i--) { |
| 2582 | const t = (lines[i] ?? '').trim(); |
| 2583 | if (/^(?:async\s+def|def|class)\b/.test(t)) break; // previous decl → stop |
| 2584 | if (CELERY_TASK_DECORATOR_RE.test(t)) { v = true; break; } |
| 2585 | } |
| 2586 | } |
| 2587 | } |
| 2588 | taskCache.set(node.id, v); |
| 2589 | return v; |
| 2590 | }; |
| 2591 | |
| 2592 | const resolve = (name: string, dispatchFile: string): Node | null => { |
| 2593 | const cands = ctx.getNodesByName(name).filter((n) => n.kind === 'function' && isCeleryTask(n)); |