Files
msd-core/eslint-rules/no-magic-sleep-in-tests.cjs
Tom Boucher 33afb4f6eb chore(#452): add ESLint 9 flat-config harness with three custom AST rules (#460)
Install eslint@9 + typescript-eslint@8 + globals@16 + eslint-plugin-n@17 +
eslint-plugin-no-only-tests@3 + typescript as devDependencies.

eslint.config.mjs (flat config):
- Global ignores: node_modules, dist, .worktrees, .claude, coverage, the
  12 generated get-shit-done/bin/lib/*.cjs files
- Block for get-shit-done/bin/**/*.cjs + scripts/**/*.cjs: js.recommended +
  eslint-plugin-n + local plugin; generic quality rules (no-var, prefer-const,
  no-unused-vars, no-empty, n/no-process-exit)
- Block for tests/**/*.test.cjs: no-only-tests (error), local timing rules,
  no-restricted-syntax timing bans

eslint-rules/ local plugin (three AST rules, all at warn pending cleanup):
- no-source-grep: flag readFileSync on source .cjs/.js/.ts + text methods
- no-magic-sleep-in-tests: flag Atomics.wait and await-new-Promise(setTimeout)
- no-elapsed-assertion: flag assert*() on timing props (elapsed/duration/took/ms)

tsconfig.lint.json: allowJs + checkJs + noEmit for future type-aware passes.

tests/eslint-rules.test.cjs: 15 RuleTester unit tests (all pass, 0 fail).

package.json: add lint/lint:fix scripts; remove lint:tests (subsumed by ESLint
local/no-source-grep). Rules that produced pre-existing errors downgraded to
warn: no-useless-escape, no-unsafe-finally, no-regex-spaces, no-control-regex,
no-irregular-whitespace. ESLint exits 0 (warnings ok).

.github/workflows/test.yml lint-tests job: add npm ci + ESLint step; remove
"Lint — no source-grep tests" step (now covered by ESLint); bump timeout 3→5
min. .gitignore: add node_modules/.cache/eslint/ entry.

eslint --fix auto-cleaned: no-regex-spaces in tests, prefer-const in state.cjs,
redundant eslint-disable-next-line comments.

Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:54:49 -04:00

81 lines
2.5 KiB
JavaScript

'use strict';
/**
* no-magic-sleep-in-tests
*
* In *.test.cjs files, flag:
* - Atomics.wait used as a sleep
* - raw setTimeout used for synchronization (i.e., awaited or used without a callback driven by logic)
*/
/** @type {import('eslint').Rule.RuleModule} */
const rule = {
meta: {
type: 'problem',
docs: {
description:
'Disallow Atomics.wait sleeps and raw setTimeout synchronization in test files',
category: 'Best Practices',
},
schema: [],
messages: {
atomicsWaitSleep:
'Atomics.wait() used as a sleep in tests. Use a proper async wait pattern instead.',
setTimeoutSync:
'Raw setTimeout used for synchronization in tests. Use proper async patterns (promises, events, polling) instead.',
},
},
create(context) {
const filename = context.getFilename();
// Only applies in test files
if (!filename.endsWith('.test.cjs')) return {};
return {
CallExpression(node) {
// Atomics.wait(...)
if (
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'Atomics' &&
node.callee.property.name === 'wait'
) {
context.report({ node, messageId: 'atomicsWaitSleep' });
}
// setTimeout(...) used for synchronization:
// - await new Promise(resolve => setTimeout(resolve, N))
// - setTimeout(() => resolve(...), N) pattern
// - setTimeout(cb, N) where N is a numeric literal (magic delay)
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'setTimeout'
) {
const args = node.arguments;
// setTimeout(something, numericLiteral) — magic delay
if (
args.length >= 2 &&
args[1].type === 'Literal' &&
typeof args[1].value === 'number'
) {
// Check if it's in an await expression or a Promise constructor
const parent = node.parent;
const grandParent = parent && parent.parent;
const isInPromise =
(grandParent &&
grandParent.type === 'NewExpression' &&
grandParent.callee.type === 'Identifier' &&
grandParent.callee.name === 'Promise') ||
(parent && parent.type === 'AwaitExpression');
if (isInPromise) {
context.report({ node, messageId: 'setTimeoutSync' });
}
}
}
},
};
},
};
module.exports = rule;