@noctcore/eslint-plugin-code-quality
v0.4.2
Published
Portable code-quality, comment-hygiene and test-discipline ESLint rules: guard clauses, deterministic time, no stray process.exit, and tests that cannot pass vacuously.
Maintainers
Readme
@noctcore/eslint-plugin-code-quality
Portable code-quality, comment-hygiene and test-discipline rules: catches vacuous tests, focused or untracked skipped tests, and comments that rot. Every rule keys off code and generic file-path globs, never a specific repo layout.
Docs: noctcore.github.io/eslint-plugins/packages/code-quality
Not a good fit for a team that writes explanatory step-by-step comments on purpose.
Requirements
- ESLint 9 or newer, flat config (
eslint.config.js) only. configs.recommendedregisters the plugin and sets rule severities, nothing else. It sets nofilesand no parser, so it applies to whatever files the rest of your config lints. To lint TypeScript, add afilespattern and@typescript-eslint/parser, as in the quick start.
Install
bun add -D @noctcore/eslint-plugin-code-quality @typescript-eslint/parser # or npm i -D / pnpm add -DQuick start
// eslint.config.js
import tsParser from '@typescript-eslint/parser';
import codeQuality from '@noctcore/eslint-plugin-code-quality';
export default [
{
...codeQuality.configs.recommended,
files: ['**/*.{ts,tsx}'],
languageOptions: { parser: tsParser },
},
// Optional: tune a preset rule's options.
{
files: ['**/*.{ts,tsx}'],
rules: {
'noctcore-code-quality/no-process-exit': ['error', { allowIn: ['**/scripts/**'] }],
},
},
];Opt-in rules
Some rules are exported but left out of recommended. interface-prefix-i and
no-template-trim-empty-ternary are house style, not correctness. The test-discipline rules in the
second block need a per-project fact before they are precise, such as which files are unit tests,
so each one takes its options from your project.
// eslint.config.js
export default [
// ...the quick start's entries, then:
{
files: ['**/*.{ts,tsx}'],
languageOptions: { parser: tsParser },
rules: {
'noctcore-code-quality/interface-prefix-i': 'error',
'noctcore-code-quality/no-template-trim-empty-ternary': 'error',
},
},
{
files: ['**/*.{test,spec}.{ts,tsx}'],
languageOptions: { parser: tsParser },
rules: {
'noctcore-code-quality/no-message-only-throw-assertion': ['error', {
assertionHelpers: ['^expectRejectsDomainError$'],
}],
'noctcore-code-quality/no-sleep-in-unit-tests': ['error', {
integrationMarkers: ['.integration.', '/e2e/'],
}],
'noctcore-code-quality/no-real-clock-in-unit-tests': ['error', {
clockModules: ['**/common/clock'],
}],
'noctcore-code-quality/typed-mock-over-double-cast': ['error', {
allowTargets: ['PrismaService'],
}],
},
},
];Rules
✅ on in recommended (error) · 🔘 opt-in: not on in recommended, enable it yourself · ⚙️ needs options · 🔧 --fix · 💡 suggestions · 💭 needs type info · ❌ deprecated
| Rule | Description | Preset | ⚙️ | 🔧 | 💡 | 💭 |
| --- | --- | :-: | :-: | :-: | :-: | :-: |
| fake-timers-must-be-restored | A test file that calls useFakeTimers() must also call useRealTimers(), so fake timers do not leak into later tests. | ✅ | | | | |
| interface-prefix-i | Interface names must be prefixed with I followed by an uppercase letter. Module/global augmentations are exempt. | 🔘 | | | | |
| no-bare-date-now | Disallow bare Date.now() / new Date() in business logic. Read wall-clock time through a shared clock util (nowMs() / now()) so time is mockable. | ✅ | | | | |
| no-conditional-expect | Disallow expect() inside a branch, catch or loop that may not run: a skipped assertion lets a broken test pass. | ✅ | | | | |
| no-elided-code-comments | Disallow comments that stand in for elided code ('// ... existing code ...', '// rest of the function unchanged', '// your code here'). They are what an agent leaves when it rewrites a file from an abbreviated draft, and the code they replaced has usually been deleted. | ✅ | | | | |
| no-focused-tests | Ban focused tests (it.only / describe.only / test.only, fdescribe / fit / ddescribe) so a focused test never silently lands in CI. | ✅ | | | | |
| no-historical-comments | Disallow comments that frame code relative to what it used to do or to a past incident ('before the fix', 'after the refactor', 'we used to', 'no longer'). Source comments describe the current invariant; history belongs in the commit message or PR description, where it does not rot when the code changes again. | ✅ | | | | |
| no-message-only-throw-assertion | Disallow toThrow() with no argument or with only a message: any error passes, including a TypeError from a broken mock. Pin the error class. | 🔘 | | | | |
| no-narration-comments | Disallow narrative comments like 'Here we...', 'Now we...', 'First, we...'. These read as step-by-step prose and add no information a future reader cannot get from the code itself. Often a tell that the comment was generated by an agent describing its own changes. | ✅ | | | | |
| no-pr-reference-comments | Disallow PR/issue references in comments. They belong in commit messages and PR descriptions, where they do not rot when the repo moves, the issue tracker migrates, or the numbering changes. | ✅ | | | | |
| no-process-exit | Disallow process.exit() outside bootstrap/shutdown paths and standalone CLIs. Application and service code must throw or reject so the lifecycle can shut down gracefully. | ✅ | | | | |
| no-real-clock-in-unit-tests | Unit tests must not read the real clock (Date.now(), new Date()) unless the file fakes it; an offset from now (Date.now() + 60_000) is allowed. | 🔘 | | | | |
| no-real-network-in-unit-tests | Unit tests must not perform real network I/O: mock the HTTP client, or move the test to an integration suite. | ✅ | | | | |
| no-sleep-in-unit-tests | Unit tests must not sleep on the real clock (new Promise((r) => setTimeout(r, n)), timers/promises): fake the timers or wait for the condition. | 🔘 | | | | |
| no-swallowed-assertion | Disallow assertions inside a try whose catch neither rethrows nor asserts, and .catch() handlers that swallow an expect(...).rejects/.resolves failure: the assertion fails, the error is dropped, and the test passes. | ✅ | | | | |
| no-template-trim-empty-ternary | Disallow inline <template>.trim() === '' ? fallback : <template>.trim() patterns. Extract to a named utility so the expression is built once and is unit-testable in one place. | 🔘 | | | | |
| no-vacuous-expect | Disallow vacuous expects (typeof checks, literal tautologies, a sole toBeDefined/toBeTruthy, a sole presence check on the render root): a test must assert behaviour that a real regression would break. | ✅ | | | | |
| prefer-early-return | Prefer guard clauses (early return) over wrapping the whole function body in a multi-statement if without an else. | ✅ | | | | |
| skipped-tests-need-tracking | Skipped tests (.skip / .fixme / xit / xdescribe, and unconditional node:test { skip } / { todo } / t.skip()) must carry a tracking marker (an issue URL or TODO(@owner)) on or above the line, so the debt has an owner instead of rotting silently. | ✅ | | | | |
| typed-mock-over-double-cast | Disallow an object literal of jest.fn() / vi.fn() mocks cast as unknown as T: the double cast switches type checking off, so a mock of a renamed or removed method keeps passing. | 🔘 | | | | |
Severity policy
Every rule is error or off, never warn: a warning is a rule nobody obeys. See the
severity policy.
Credits
no-vacuous-expect, no-conditional-expect, fake-timers-must-be-restored and
no-real-network-in-unit-tests are ported from tsforge
(MIT). See THIRD_PARTY_NOTICES.md.
