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>
104 lines
2.8 KiB
JavaScript
104 lines
2.8 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* no-elapsed-assertion
|
|
*
|
|
* Flag assert*() calls whose argument reads a property named
|
|
* /^(elapsed|duration|took|ms)$/ or compares such an identifier.
|
|
* Timing assertions are flaky and should not be in the test suite.
|
|
*/
|
|
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
const rule = {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description:
|
|
'Disallow timing assertions (elapsed, duration, took, ms) in assert calls',
|
|
category: 'Best Practices',
|
|
},
|
|
schema: [],
|
|
messages: {
|
|
noElapsedAssertion:
|
|
'Timing assertion detected: assert*() on a timing property (elapsed/duration/took/ms). Timing assertions are flaky — assert on observable behavior instead.',
|
|
},
|
|
},
|
|
create(context) {
|
|
const TIMING_PROPS = /^(elapsed|duration|took|ms)$/;
|
|
|
|
function containsTimingRef(node) {
|
|
if (!node) return false;
|
|
|
|
// foo.elapsed, foo.duration, foo.took, foo.ms
|
|
if (
|
|
node.type === 'MemberExpression' &&
|
|
node.property.type === 'Identifier' &&
|
|
TIMING_PROPS.test(node.property.name)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// Identifier directly: elapsed, duration, took, ms
|
|
if (node.type === 'Identifier' && TIMING_PROPS.test(node.name)) {
|
|
return true;
|
|
}
|
|
|
|
// Binary expression: elapsed > 100, duration <= 500, etc.
|
|
if (node.type === 'BinaryExpression') {
|
|
return containsTimingRef(node.left) || containsTimingRef(node.right);
|
|
}
|
|
|
|
// Logical expression: elapsed && elapsed > 0
|
|
if (node.type === 'LogicalExpression') {
|
|
return containsTimingRef(node.left) || containsTimingRef(node.right);
|
|
}
|
|
|
|
// UnaryExpression: !elapsed
|
|
if (node.type === 'UnaryExpression') {
|
|
return containsTimingRef(node.argument);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function isAssertCall(node) {
|
|
if (node.callee.type === 'Identifier') {
|
|
return /^assert/.test(node.callee.name);
|
|
}
|
|
if (
|
|
node.callee.type === 'MemberExpression' &&
|
|
node.callee.object.type === 'Identifier' &&
|
|
node.callee.object.name === 'assert'
|
|
) {
|
|
return true;
|
|
}
|
|
// assert.strict.* or assert/strict
|
|
if (
|
|
node.callee.type === 'MemberExpression' &&
|
|
node.callee.object.type === 'MemberExpression' &&
|
|
node.callee.object.object.type === 'Identifier' &&
|
|
node.callee.object.object.name === 'assert'
|
|
) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return {
|
|
CallExpression(node) {
|
|
if (!isAssertCall(node)) return;
|
|
|
|
// Check all arguments for timing refs
|
|
for (const arg of node.arguments) {
|
|
if (containsTimingRef(arg)) {
|
|
context.report({ node, messageId: 'noElapsedAssertion' });
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
module.exports = rule;
|