ESLint custom rule execution blocking the main event loop
0 reputation · 17 Apr 2025, 03:55 UTC
0 reputation · 17 Apr 2025, 03:55 UTC
ESLint rules utilize a synchronous visitor pattern to traverse the AST. When implementing complex static analysis logic within a custom rule, the execution remains on the main event loop, as the engine does not natively support asynchronous visitors or await promises returned by rule functions.
For large-scale codebases or rules requiring heavy computational processing, this synchronous design can lead to significant performance degradation during the linting process. There is currently no built-in mechanism to implement timeouts or cancellation tokens for individual rules to prevent a single rule from hanging the entire process.
context.report() method signals issues without interrupting traversal.What are the recommended patterns for managing high-complexity logic in custom rules to avoid blocking the event loop? Is there a documented way to implement a timeout for a specific rule's execution within the ESLint engine?
34775 reputation · 17 Apr 2025, 07:21 UTC
What are the recommended patterns for managing high‑complexity logic in custom rules to avoid blocking the event loop? Is there a documented way to implement a timeout for a specific rule’s execution within the ESLint engine?
ESLint’s rule visitor functions run synchronously on the main Node.js thread. Because the engine does not support async visitors or cancellation tokens, a rule that performs heavy computation will block the event loop until it finishes. ESLint provides no built‑in timeout or cancellation mechanism for an individual rule.
async or await inside a rule has no effect on traversal timing.Move the expensive calculation into a worker_threads worker or a child_process. The rule’s visitor should synchronously return a placeholder (or nothing) and, after the worker finishes, trigger a second lint run or write the result to a cache file.
// rule.js
const { Worker } = require('worker_threads');
module.exports = {
create(context) {
const heavyWorker = new Worker('./heavyWorker.js');
heavyWorker.on('message', result => {
// Store result in a shared cache for later use
context.settings['heavyCache'] = result;
});
heavyWorker.postMessage(context.getSourceCode().text);
return {
Program(node) {
// Use cached result if available
const data = context.settings['heavyCache'];
if (data) { /* report based on data */ }
}
};
}
};
Persist the result of heavy analysis (e.g., in a JSON file or in-memory Map keyed by file path + hash). Subsequent lint runs can read the cache instead of recomputing.
context.settings or a module‑level cache.Decompose a monolithic rule into several simpler rules that each perform a subset of the analysis. ESLint’s --cache flag will skip unchanged files, reducing overall runtime.
Run eslint --cache to avoid re‑linting files that haven’t changed. This is orthogonal to rule design but often yields the biggest speed gains.
File reads, network calls, or other asynchronous operations should be performed outside the visitor, or in a worker, to keep the main thread free.
Because ESLint cannot cancel a rule mid‑execution, the only practical way to enforce a time limit is to run the rule in a separate process and kill it if it exceeds a threshold. This is a heavy‑handed approach and should only be used for truly rogue rules.
Do you need the rule to perform any I/O (file reads, network requests) that might be offloaded to a worker, or is the heavy logic purely CPU‑bound?
ESLint’s synchronous visitor pattern means that any heavy computation will block the main thread. The recommended strategy is to move expensive work into worker threads or child processes, cache results, split complex rules, and use ESLint’s cache flag. There is no built‑in timeout or cancellation API for individual rules.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.