@noctcore/eslint-plugin-code-quality 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ # `noctcore-code-quality/interface-prefix-i`
2
+
3
+ > Interface names must be `I` + an uppercase letter (`IUserProfile`). **Opinionated — not in `recommended`.**
4
+
5
+ ## Why
6
+
7
+ An `I`-prefixed interface reads as an interface at a glance and never collides with a value of the same
8
+ name. This is a house-style naming convention, not a correctness rule — some teams find the prefix
9
+ noisy. It is therefore **excluded from the `recommended` preset**; enable it explicitly if your team
10
+ wants it.
11
+
12
+ ## What it flags
13
+
14
+ A `TSInterfaceDeclaration` whose name is **not** `I` followed by an uppercase letter
15
+ (`/^I[A-Z]/`). `Input` fails (the letter after `I` is lowercase); `IUserProfile` passes.
16
+
17
+ Interfaces inside an ambient `declare module` / `declare global` block are **exempt** — their names are
18
+ dictated by the module being augmented (`Register`, `Window`).
19
+
20
+ Report-only: a rename touches every reference, which a single-file fixer cannot do safely.
21
+
22
+ ```ts
23
+ // ✗ interface UserProfile { id: string; }
24
+ // ✗ interface Input { value: string; }
25
+
26
+ // ✓ interface IUserProfile { id: string; }
27
+ // ✓ declare global { interface Window { electron: unknown; } } // augmentation
28
+ ```
29
+
30
+ ## Options
31
+
32
+ None.
33
+
34
+ ## Enabling it
35
+
36
+ Not part of `recommended`. Turn it on directly:
37
+
38
+ ```js
39
+ 'noctcore-code-quality/interface-prefix-i': 'error'
40
+ ```
41
+
42
+ ## When not to use it
43
+
44
+ If your codebase does not use the `I` interface-prefix convention (most don't) — leave it off.
@@ -0,0 +1,44 @@
1
+ # `noctcore-code-quality/no-bare-date-now`
2
+
3
+ > Read wall-clock time through a mockable `clock` util, not bare `Date.now()` / `new Date()`.
4
+
5
+ ## Why
6
+
7
+ Business logic that reads `Date.now()` or `new Date()` directly is hard to test: every time-dependent
8
+ branch depends on the real clock. Routing wall-clock reads through a shared `clock` util
9
+ (`nowMs()` / `now()`) gives time-dependent code one seam you can freeze or advance in tests.
10
+
11
+ ## What it flags
12
+
13
+ - `Date.now()` → suggests `nowMs()`.
14
+ - Zero-argument `new Date()` → suggests `now()`.
15
+
16
+ `new Date(value)` with an argument is a **parse** of an explicit instant, not a bare clock read, and
17
+ is never flagged. Files covered by `allowIn` are skipped entirely.
18
+
19
+ ```ts
20
+ // ✗ bare clock reads in business logic
21
+ const start = Date.now();
22
+ const created = new Date();
23
+
24
+ // ✓ through the clock seam
25
+ const start = nowMs();
26
+ const created = now();
27
+
28
+ // ✓ parsing an explicit instant
29
+ const at = new Date('2026-01-01T00:00:00Z');
30
+ ```
31
+
32
+ ## Options
33
+
34
+ | Option | Type | Default | Meaning |
35
+ | --- | --- | --- | --- |
36
+ | `allowIn` | `string[]` (globs) | `["**/clock.ts", "**/clock/**"]` | File-path globs (infra / the clock util itself) where bare `Date` is allowed. |
37
+
38
+ ```js
39
+ 'noctcore-code-quality/no-bare-date-now': ['error', { allowIn: ['**/clock.ts', '**/*.timing.ts'] }]
40
+ ```
41
+
42
+ ## When not to use it
43
+
44
+ If your project has no clock abstraction and does not intend to add one.
@@ -0,0 +1,33 @@
1
+ # `noctcore-code-quality/no-focused-tests`
2
+
3
+ > Ban `.only` / `fdescribe` / `fit` so a focused test never silently lands in CI.
4
+
5
+ ## Why
6
+
7
+ A focused test (`it.only`, `fdescribe`, …) silences the rest of the suite. Committed by accident it
8
+ turns a green CI run into a lie — the suite "passes" because almost none of it ran. This rule makes a
9
+ focused test a lint error so it can't merge.
10
+
11
+ ## What it flags
12
+
13
+ - `.only` resolved back to a `it` / `describe` / `test` runner, including chained modifiers
14
+ (`test.concurrent.only`, `describe.concurrent.only`).
15
+ - The Jest/Jasmine focused-call forms `fdescribe(...)`, `fit(...)`, `ddescribe(...)` — but only as a
16
+ bare-identifier callee, so an unrelated `obj.fit(...)` method is not flagged.
17
+
18
+ ```ts
19
+ // ✗ it.only('runs', () => {});
20
+ // ✗ test.concurrent.only('case', () => {});
21
+ // ✗ fdescribe('suite', () => {});
22
+
23
+ // ✓ it('runs', () => {});
24
+ // ✓ layout.fit('contain'); // not a test runner
25
+ ```
26
+
27
+ ## Options
28
+
29
+ None.
30
+
31
+ ## When not to use it
32
+
33
+ You almost always want this on. Disable only in throwaway scratch suites.
@@ -0,0 +1,31 @@
1
+ # `noctcore-code-quality/no-historical-comments`
2
+
3
+ > Comments describe the current invariant, not what the code used to do.
4
+
5
+ ## Why
6
+
7
+ A comment that frames code against its past — "before the fix", "we used to", "no longer" — rots the
8
+ moment the code changes again, and it forces the reader to reconstruct a history they don't need. The
9
+ history belongs in the commit message or PR description, where it is durable and out of the way.
10
+ This is also a common tell that a comment was generated to narrate a change rather than to explain the code.
11
+
12
+ ## What it flags
13
+
14
+ Line and block comments (JSDoc `/** … */` blocks are exempt) matching narrow past-framing phrases:
15
+ `before/after the fix`, `before/after the refactor`, `we/this used to`, `used to be`, `no longer`,
16
+ `kept for backwards/legacy/compat`, `was/were a bug/footgun`, and `historical(ly)`.
17
+
18
+ ```ts
19
+ // ✗ We used to read process.env directly here.
20
+ // ✗ Before the fix this collapsed to {}.
21
+
22
+ // ✓ Caps concurrent connections to avoid pool exhaustion.
23
+ ```
24
+
25
+ ## Options
26
+
27
+ None.
28
+
29
+ ## When not to use it
30
+
31
+ If you deliberately keep inline change-history in comments.
@@ -0,0 +1,34 @@
1
+ # `noctcore-code-quality/no-narration-comments`
2
+
3
+ > Ban step-by-step "Now we… / First we…" narration comments.
4
+
5
+ ## Why
6
+
7
+ Comments like "Here we attach the user" or "First, we parse the cookies" restate what the next line
8
+ of code already says. They add no information a reader can't get from the code and are a frequent tell
9
+ of a comment generated to narrate a change. Describe the **why** when a comment is warranted, or
10
+ delete it.
11
+
12
+ ## What it flags
13
+
14
+ Line and block comments (JSDoc `/** … */` blocks are exempt) that **begin** with a narration
15
+ construction: `here we`, `now we`, `first[,] we`, `then[,] we`, `next[,] we`, `finally[,] we`,
16
+ `let's`, `let me`.
17
+
18
+ A bare leading word ("Next attempt…", "First run…") is fine — only the "we"/"let's" narration form matches.
19
+
20
+ ```ts
21
+ // ✗ Now we attach the user to the socket.
22
+ // ✗ Let's validate the session token.
23
+
24
+ // ✓ WHY: Prisma reuses the pooled connection across requests.
25
+ // ✓ call next() to continue the middleware chain
26
+ ```
27
+
28
+ ## Options
29
+
30
+ None.
31
+
32
+ ## When not to use it
33
+
34
+ If your team writes tutorial-style narrated code on purpose.
@@ -0,0 +1,36 @@
1
+ # `noctcore-code-quality/no-pr-reference-comments`
2
+
3
+ > PR/issue references belong in commit messages, not in source comments.
4
+
5
+ ## Why
6
+
7
+ `// fixes #123` or a link to a pull request rots the moment the repo moves, the issue tracker
8
+ migrates, or the numbering changes — and it drags the reader out of the code to chase context that a
9
+ `git blame` already carries. The git log and PR description are the canonical, durable home for
10
+ repo-history references.
11
+
12
+ ## What it flags
13
+
14
+ Comments containing:
15
+
16
+ - a GitHub PR/issue URL (`https://github.com/owner/repo/pull/42`, `.../issues/42`);
17
+ - an action reference (`see`/`closes`/`fixes`/`resolves`/`refs` `#123`);
18
+ - a `PR #123` / `PR 123` reference;
19
+ - a bare `#123` at a word boundary.
20
+
21
+ ```ts
22
+ // ✗ fixes #123
23
+ // ✗ See https://github.com/noctcore/eslint-plugins/pull/42 for context.
24
+ // ✗ workaround (#88)
25
+
26
+ // ✓ Trust-proxy depth for single-host Traefik.
27
+ const channel = "#general"; // ✓ not a comment
28
+ ```
29
+
30
+ ## Options
31
+
32
+ None.
33
+
34
+ ## When not to use it
35
+
36
+ If your workflow relies on inline issue links in code.
@@ -0,0 +1,38 @@
1
+ # `noctcore-code-quality/no-process-exit`
2
+
3
+ > `process.exit()` belongs to bootstrap/shutdown and CLIs — not application code.
4
+
5
+ ## Why
6
+
7
+ `process.exit()` kills the whole process immediately, skipping pending I/O, `finally` blocks, and
8
+ graceful-shutdown handlers. In request-scoped, service, or renderer code that is a footgun: a single
9
+ call can take the entire process down mid-flight. Those layers should `throw` or reject and let the
10
+ lifecycle decide how to tear down. The legitimate exit sites are standalone scripts, CLI entrypoints,
11
+ and config files.
12
+
13
+ ## What it flags
14
+
15
+ Any `process.exit(...)` call in a file **not** covered by `allowIn`. Both the dot form
16
+ (`process.exit()`) and the computed form (`process['exit']()`) are caught so a computed callee
17
+ cannot bypass the rule.
18
+
19
+ ## Options
20
+
21
+ | Option | Type | Default | Meaning |
22
+ | --- | --- | --- | --- |
23
+ | `allowIn` | `string[]` (globs) | see below | File-path globs where `process.exit()` is allowed. |
24
+
25
+ The globs are matched against the file path (a leading `**/` may match zero leading directories, so
26
+ the same pattern works for absolute and project-relative paths). Default:
27
+
28
+ ```jsonc
29
+ ["**/scripts/**", "**/bin/**", "**/cli/**", "**/*.config.{ts,js,mjs,cjs,cts,mts}"]
30
+ ```
31
+
32
+ ```js
33
+ 'noctcore-code-quality/no-process-exit': ['error', { allowIn: ['**/scripts/**', '**/*.cli.ts'] }]
34
+ ```
35
+
36
+ ## When not to use it
37
+
38
+ Standalone CLI projects where every file is legitimately an entrypoint.
@@ -0,0 +1,44 @@
1
+ # `noctcore-code-quality/no-template-trim-empty-ternary`
2
+
3
+ > Extract the inline `` `…`.trim() === '' ? fallback : `…`.trim() `` pattern to a named util. **Niche — not in `recommended`.**
4
+
5
+ ## Why
6
+
7
+ The inline shape `` `${a} ${b}`.trim() === '' ? fallback : `${a} ${b}`.trim() `` builds the same
8
+ template-plus-`trim()` expression **twice** and buries a small piece of display logic where it can't be
9
+ unit-tested on its own. Extracting it to a named helper (e.g. `buildDisplayName(...)`) builds the
10
+ expression once and gives it one testable home.
11
+
12
+ This is a very specific pattern, so the rule is **excluded from the `recommended` preset**. Enable it
13
+ explicitly if the shape recurs in your codebase.
14
+
15
+ ## What it flags
16
+
17
+ A `ConditionalExpression` whose test is a `===` / `!==` comparison between an **empty-string literal**
18
+ and a **`.trim()` call on a template literal**, in either order.
19
+
20
+ ```ts
21
+ // ✗
22
+ const name = `${first} ${last}`.trim() === '' ? email : `${first} ${last}`.trim();
23
+ const name = '' !== `${a}`.trim() ? `${a}`.trim() : fallback;
24
+
25
+ // ✓
26
+ const name = buildDisplayName({ first, last, fallback: email });
27
+ const trimmed = value.trim() === '' ? fallback : value; // not a template literal
28
+ ```
29
+
30
+ ## Options
31
+
32
+ None.
33
+
34
+ ## Enabling it
35
+
36
+ Not part of `recommended`. Turn it on directly:
37
+
38
+ ```js
39
+ 'noctcore-code-quality/no-template-trim-empty-ternary': 'error'
40
+ ```
41
+
42
+ ## When not to use it
43
+
44
+ If this pattern doesn't appear in your codebase, there's nothing to gain from enabling it.
@@ -0,0 +1,47 @@
1
+ # `noctcore-code-quality/prefer-early-return`
2
+
3
+ > Prefer a guard clause over wrapping the whole function body in an `if`.
4
+
5
+ ## Why
6
+
7
+ When a function's entire body is wrapped in a single `if` with no `else`, the happy path is
8
+ needlessly indented and the reader has to hold the condition in their head to the end. Inverting
9
+ the condition into an early return keeps the meaningful work at the top level and reads top-to-bottom.
10
+
11
+ ## What it flags
12
+
13
+ The **last** statement of a function block that is an `if` with:
14
+
15
+ - no `else` branch, and
16
+ - a block consequent holding **two or more** statements.
17
+
18
+ A single-statement `if`, an `if/else`, or an `if` that is not the final statement is left alone —
19
+ those are not body-wraps.
20
+
21
+ ```ts
22
+ // ✗ the whole body is wrapped
23
+ function handle(x) {
24
+ if (x) {
25
+ doA();
26
+ doB();
27
+ }
28
+ }
29
+
30
+ // ✓ guard clause
31
+ function handle(x) {
32
+ if (!x) {
33
+ return;
34
+ }
35
+ doA();
36
+ doB();
37
+ }
38
+ ```
39
+
40
+ ## Options
41
+
42
+ None.
43
+
44
+ ## When not to use it
45
+
46
+ If you prefer the wrapped style, or lint a codebase where trailing single-`if` bodies are idiomatic,
47
+ disable it.
@@ -0,0 +1,50 @@
1
+ # `noctcore-code-quality/skipped-tests-need-tracking`
2
+
3
+ > A skipped test must carry a tracking marker (issue URL or `TODO(@owner)`) so the debt has an owner.
4
+
5
+ ## Why
6
+
7
+ `.skip` / `.fixme` / `xit` / `xdescribe` are escape hatches. Left unowned they rot into permanent dark
8
+ zones — nobody remembers why the test is off or who is meant to turn it back on. Requiring a tracking
9
+ marker (an issue URL or a `TODO(@owner)`) within a few lines of the skip keeps a human attached to the
10
+ debt.
11
+
12
+ `.only` is deliberately **not** covered here — [`no-focused-tests`](./no-focused-tests.md) bans it
13
+ outright, so it can never legitimately appear with or without tracking.
14
+
15
+ ## What it flags
16
+
17
+ Any line matching a skip form — `it.skip(` / `test.skip(` / `describe.skip(`, the `.fixme(` variants,
18
+ `xit(`, `xdescribe(`, `xtest(` — with **no** tracking marker on that line or within the `lookback`
19
+ window above it.
20
+
21
+ Faithful to the original text-scanning implementation, the source is scanned line by line, so a marker
22
+ in a trailing comment, a preceding comment, or anywhere in the lookback window is honoured.
23
+
24
+ ```ts
25
+ // ✓ tracked
26
+ // TODO(@alice): flaky under CI
27
+ it.skip('later', () => {});
28
+
29
+ it.skip('later', () => {}); // https://github.com/org/repo/issues/1
30
+
31
+ // ✗ untracked
32
+ it.skip('later', () => {});
33
+ xdescribe('later', () => {});
34
+ ```
35
+
36
+ ## Options
37
+
38
+ | Option | Type | Default | Meaning |
39
+ | --- | --- | --- | --- |
40
+ | `markers` | `string[]` (regex sources) | `["https?://\\S+", "TODO\\(@?\\S+\\)"]` | Any pattern that, if found in the lookback window, satisfies the tracking requirement. Compiled with the `u` flag. |
41
+ | `lookback` | `integer` (≥0) | `30` | How many lines above the skip a marker may live and still count. |
42
+
43
+ ```js
44
+ // Require a Jira-style key instead of a URL / TODO.
45
+ 'noctcore-code-quality/skipped-tests-need-tracking': ['error', { markers: ['[A-Z]+-\\d+'] }]
46
+ ```
47
+
48
+ ## When not to use it
49
+
50
+ If your project already enforces skip-tracking another way, or never skips tests.
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@noctcore/eslint-plugin-code-quality",
3
+ "version": "0.1.0",
4
+ "description": "Portable code-quality, comment-hygiene, and test-discipline ESLint rules.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "docs",
21
+ "README.md"
22
+ ],
23
+ "sideEffects": false,
24
+ "keywords": [
25
+ "eslint",
26
+ "eslintplugin",
27
+ "eslint-plugin",
28
+ "code-quality",
29
+ "noctcore"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "provenance": true
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/noctcore/eslint-plugins.git",
38
+ "directory": "packages/eslint-plugin-code-quality"
39
+ },
40
+ "homepage": "https://github.com/noctcore/eslint-plugins/tree/main/packages/eslint-plugin-code-quality",
41
+ "bugs": "https://github.com/noctcore/eslint-plugins/issues",
42
+ "scripts": {
43
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run"
46
+ },
47
+ "dependencies": {
48
+ "@noctcore/eslint-utils": "^0.1.0",
49
+ "@typescript-eslint/utils": "^8.61.1"
50
+ },
51
+ "peerDependencies": {
52
+ "eslint": ">=9.0.0",
53
+ "typescript": ">=5.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@noctcore/eslint-test-utils": "workspace:*",
57
+ "@types/node": "^22.0.0",
58
+ "@typescript-eslint/parser": "^8.61.1",
59
+ "@typescript-eslint/rule-tester": "^8.61.1",
60
+ "tsup": "^8.5.1",
61
+ "typescript": "^5.6.0",
62
+ "vitest": "^3"
63
+ }
64
+ }