@noctcore/eslint-plugin-code-quality 0.2.0 → 0.2.2
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.
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/docs/rules/fake-timers-must-be-restored.md +3 -5
- package/docs/rules/interface-prefix-i.md +7 -5
- package/docs/rules/no-bare-date-now.md +8 -4
- package/docs/rules/no-conditional-expect.md +2 -4
- package/docs/rules/no-focused-tests.md +8 -6
- package/docs/rules/no-historical-comments.md +6 -4
- package/docs/rules/no-narration-comments.md +7 -5
- package/docs/rules/no-pr-reference-comments.md +8 -6
- package/docs/rules/no-process-exit.md +18 -0
- package/docs/rules/no-real-network-in-unit-tests.md +10 -5
- package/docs/rules/no-template-trim-empty-ternary.md +4 -4
- package/docs/rules/no-vacuous-expect.md +2 -4
- package/docs/rules/prefer-early-return.md +5 -3
- package/docs/rules/skipped-tests-need-tracking.md +8 -6
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1411,7 +1411,7 @@ var rules = {
|
|
|
1411
1411
|
|
|
1412
1412
|
// src/index.ts
|
|
1413
1413
|
var NAMESPACE = "noctcore-code-quality";
|
|
1414
|
-
var VERSION = "0.2.
|
|
1414
|
+
var VERSION = "0.2.2";
|
|
1415
1415
|
var plugin = {
|
|
1416
1416
|
meta: { name: "@noctcore/eslint-plugin-code-quality", version: VERSION },
|
|
1417
1417
|
rules,
|
package/dist/index.js
CHANGED
|
@@ -1373,7 +1373,7 @@ var rules = {
|
|
|
1373
1373
|
|
|
1374
1374
|
// src/index.ts
|
|
1375
1375
|
var NAMESPACE = "noctcore-code-quality";
|
|
1376
|
-
var VERSION = "0.2.
|
|
1376
|
+
var VERSION = "0.2.2";
|
|
1377
1377
|
var plugin = {
|
|
1378
1378
|
meta: { name: "@noctcore/eslint-plugin-code-quality", version: VERSION },
|
|
1379
1379
|
rules,
|
|
@@ -22,7 +22,7 @@ the file runs.
|
|
|
22
22
|
A contract suite often owns the restore: the spec installs fake timers inside a helper it hands to
|
|
23
23
|
the suite, and the suite restores them in its own `afterEach`.
|
|
24
24
|
|
|
25
|
-
```ts
|
|
25
|
+
```ts prose reason="two files: the rule reads the imported suite from disk"
|
|
26
26
|
// provider.contract-suite.ts
|
|
27
27
|
export function runProviderContract(subject: Subject): void {
|
|
28
28
|
describe('provider contract', () => {
|
|
@@ -51,8 +51,7 @@ re-exported, never called, does not count.
|
|
|
51
51
|
For a suite the rule cannot resolve (a path alias or a workspace package), name it in
|
|
52
52
|
`sharedSuiteModules`.
|
|
53
53
|
|
|
54
|
-
```ts
|
|
55
|
-
// Bad
|
|
54
|
+
```ts bad filename=src/session.test.ts
|
|
56
55
|
beforeEach(() => {
|
|
57
56
|
vi.useFakeTimers();
|
|
58
57
|
});
|
|
@@ -63,8 +62,7 @@ it('expires the session', () => {
|
|
|
63
62
|
});
|
|
64
63
|
```
|
|
65
64
|
|
|
66
|
-
```ts
|
|
67
|
-
// Good
|
|
65
|
+
```ts good filename=src/session.test.ts
|
|
68
66
|
beforeEach(() => {
|
|
69
67
|
vi.useFakeTimers();
|
|
70
68
|
});
|
|
@@ -19,12 +19,14 @@ dictated by the module being augmented (`Register`, `Window`).
|
|
|
19
19
|
|
|
20
20
|
Report-only: a rename touches every reference, which a single-file fixer cannot do safely.
|
|
21
21
|
|
|
22
|
-
```ts
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
```ts bad reports=2
|
|
23
|
+
interface UserProfile { id: string; }
|
|
24
|
+
interface Input { value: string; }
|
|
25
|
+
```
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
```ts good
|
|
28
|
+
interface IUserProfile { id: string; }
|
|
29
|
+
declare global { interface Window { electron: unknown; } } // augmentation
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
## Options
|
|
@@ -16,16 +16,20 @@ branch depends on the real clock. Routing wall-clock reads through a shared `clo
|
|
|
16
16
|
`new Date(value)` with an argument is a **parse** of an explicit instant, not a bare clock read, and
|
|
17
17
|
is never flagged. Files covered by `allowIn` are skipped entirely.
|
|
18
18
|
|
|
19
|
-
```ts
|
|
20
|
-
//
|
|
19
|
+
```ts bad reports=2
|
|
20
|
+
// bare clock reads in business logic
|
|
21
21
|
const start = Date.now();
|
|
22
22
|
const created = new Date();
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```ts good
|
|
26
|
+
import { now, nowMs } from './clock';
|
|
23
27
|
|
|
24
|
-
//
|
|
28
|
+
// through the clock seam
|
|
25
29
|
const start = nowMs();
|
|
26
30
|
const created = now();
|
|
27
31
|
|
|
28
|
-
//
|
|
32
|
+
// parsing an explicit instant
|
|
29
33
|
const at = new Date('2026-01-01T00:00:00Z');
|
|
30
34
|
```
|
|
31
35
|
|
|
@@ -26,8 +26,7 @@ loop or `if` that generates tests does not flag the expects inside those tests.
|
|
|
26
26
|
A test that calls `expect.assertions(n)` or `expect.hasAssertions()` is exempt: a skipped branch
|
|
27
27
|
already fails it.
|
|
28
28
|
|
|
29
|
-
```ts
|
|
30
|
-
// Bad
|
|
29
|
+
```ts bad filename=src/parse.test.ts reports=2
|
|
31
30
|
it('rejects bad input', async () => {
|
|
32
31
|
try {
|
|
33
32
|
await parse('');
|
|
@@ -44,8 +43,7 @@ it('returns the value', () => {
|
|
|
44
43
|
});
|
|
45
44
|
```
|
|
46
45
|
|
|
47
|
-
```ts
|
|
48
|
-
// Good
|
|
46
|
+
```ts good filename=src/parse.test.ts
|
|
49
47
|
it('rejects bad input', async () => {
|
|
50
48
|
await expect(parse('')).rejects.toBeInstanceOf(ParseError);
|
|
51
49
|
});
|
|
@@ -15,13 +15,15 @@ focused test a lint error so it can't merge.
|
|
|
15
15
|
- The Jest/Jasmine focused-call forms `fdescribe(...)`, `fit(...)`, `ddescribe(...)` — but only as a
|
|
16
16
|
bare-identifier callee, so an unrelated `obj.fit(...)` method is not flagged.
|
|
17
17
|
|
|
18
|
-
```ts
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
```ts bad filename=src/example.test.ts reports=3
|
|
19
|
+
it.only('runs', () => {});
|
|
20
|
+
test.concurrent.only('case', () => {});
|
|
21
|
+
fdescribe('suite', () => {});
|
|
22
|
+
```
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
```ts good filename=src/example.test.ts
|
|
25
|
+
it('runs', () => {});
|
|
26
|
+
layout.fit('contain'); // not a test runner
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
## Options
|
|
@@ -15,11 +15,13 @@ Line and block comments (JSDoc `/** … */` blocks are exempt) matching narrow p
|
|
|
15
15
|
`before/after the fix`, `before/after the refactor`, `we/this used to`, `used to be`, `no longer`,
|
|
16
16
|
`kept for backwards/legacy/compat`, `was/were a bug/footgun`, and `historical(ly)`.
|
|
17
17
|
|
|
18
|
-
```ts
|
|
19
|
-
//
|
|
20
|
-
//
|
|
18
|
+
```ts bad reports=2
|
|
19
|
+
// We used to read process.env directly here.
|
|
20
|
+
// Before the fix this collapsed to {}.
|
|
21
|
+
```
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
```ts good
|
|
24
|
+
// Caps concurrent connections to avoid pool exhaustion.
|
|
23
25
|
```
|
|
24
26
|
|
|
25
27
|
## Options
|
|
@@ -17,12 +17,14 @@ construction: `here we`, `now we`, `first[,] we`, `then[,] we`, `next[,] we`, `f
|
|
|
17
17
|
|
|
18
18
|
A bare leading word ("Next attempt…", "First run…") is fine — only the "we"/"let's" narration form matches.
|
|
19
19
|
|
|
20
|
-
```ts
|
|
21
|
-
//
|
|
22
|
-
//
|
|
20
|
+
```ts bad reports=2
|
|
21
|
+
// Now we attach the user to the socket.
|
|
22
|
+
// Let's validate the session token.
|
|
23
|
+
```
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
//
|
|
25
|
+
```ts good
|
|
26
|
+
// WHY: Prisma reuses the pooled connection across requests.
|
|
27
|
+
// call next() to continue the middleware chain
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
## Options
|
|
@@ -18,13 +18,15 @@ Comments containing:
|
|
|
18
18
|
- a `PR #123` / `PR 123` reference;
|
|
19
19
|
- a bare `#123` at a word boundary.
|
|
20
20
|
|
|
21
|
-
```ts
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
21
|
+
```ts bad reports=3
|
|
22
|
+
// fixes #123
|
|
23
|
+
// See https://github.com/noctcore/eslint-plugins/pull/42 for context.
|
|
24
|
+
// workaround (#88)
|
|
25
|
+
```
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
```ts good
|
|
28
|
+
// Trust-proxy depth for single-host Traefik.
|
|
29
|
+
const channel = "#general"; // the string is not a comment
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
## Options
|
|
@@ -16,6 +16,24 @@ Any `process.exit(...)` call in a file **not** covered by `allowIn`. Both the do
|
|
|
16
16
|
(`process.exit()`) and the computed form (`process['exit']()`) are caught so a computed callee
|
|
17
17
|
cannot bypass the rule.
|
|
18
18
|
|
|
19
|
+
```ts bad filename=src/orders/orders.service.ts
|
|
20
|
+
if (!order) {
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```ts good filename=src/orders/orders.service.ts
|
|
26
|
+
if (!order) {
|
|
27
|
+
throw new Error('order not found');
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Moving the exit to a CLI entrypoint is the other fix:
|
|
32
|
+
|
|
33
|
+
```ts good filename=scripts/migrate.ts relocation
|
|
34
|
+
main().catch(() => process.exit(1));
|
|
35
|
+
```
|
|
36
|
+
|
|
19
37
|
## Options
|
|
20
38
|
|
|
21
39
|
| Option | Type | Default | Meaning |
|
|
@@ -29,22 +29,27 @@ It does not flag:
|
|
|
29
29
|
- a client whose module the file mocks (`vi.mock('axios')`, `jest.mock('node-fetch')`);
|
|
30
30
|
- a method that only shares a name (`repository.fetch(1)`, `store.get('k')`).
|
|
31
31
|
|
|
32
|
-
```ts
|
|
33
|
-
// Bad: src/api/client.test.ts
|
|
32
|
+
```ts bad filename=src/api/client.test.ts
|
|
34
33
|
it('loads the profile', async () => {
|
|
35
34
|
const res = await fetch('https://api.example.com/me');
|
|
36
35
|
expect(res.status).toBe(200);
|
|
37
36
|
});
|
|
38
37
|
```
|
|
39
38
|
|
|
40
|
-
```ts
|
|
41
|
-
// Good: src/api/client.test.ts
|
|
39
|
+
```ts good filename=src/api/client.test.ts
|
|
42
40
|
it('loads the profile', async () => {
|
|
43
41
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('{"id":1}')));
|
|
44
42
|
expect(await loadProfile()).toEqual({ id: 1 });
|
|
45
43
|
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A test that needs the real server moves to an integration suite:
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
```ts good filename=src/api/client.integration.test.ts relocation
|
|
49
|
+
it('loads the profile', async () => {
|
|
50
|
+
const res = await fetch('https://api.example.com/me');
|
|
51
|
+
expect(res.status).toBe(200);
|
|
52
|
+
});
|
|
48
53
|
```
|
|
49
54
|
|
|
50
55
|
## Options
|
|
@@ -17,12 +17,12 @@ explicitly if the shape recurs in your codebase.
|
|
|
17
17
|
A `ConditionalExpression` whose test is a `===` / `!==` comparison between an **empty-string literal**
|
|
18
18
|
and a **`.trim()` call on a template literal**, in either order.
|
|
19
19
|
|
|
20
|
-
```ts
|
|
21
|
-
// ✗
|
|
20
|
+
```ts bad reports=2
|
|
22
21
|
const name = `${first} ${last}`.trim() === '' ? email : `${first} ${last}`.trim();
|
|
23
|
-
const
|
|
22
|
+
const label = '' !== `${a}`.trim() ? `${a}`.trim() : fallback;
|
|
23
|
+
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
```ts good
|
|
26
26
|
const name = buildDisplayName({ first, last, fallback: email });
|
|
27
27
|
const trimmed = value.trim() === '' ? fallback : value; // not a template literal
|
|
28
28
|
```
|
|
@@ -25,8 +25,7 @@ The rule matches on method names, not on a runner import, so it covers Jest, Vit
|
|
|
25
25
|
A test counts every `expect(...)` matcher plus any call matching `assertionCallees`, so a weak
|
|
26
26
|
`expect` next to `assert.equal(...)`, `expectValidUser(...)` or supertest's `.expect(200)` is fine.
|
|
27
27
|
|
|
28
|
-
```ts
|
|
29
|
-
// Bad
|
|
28
|
+
```ts bad filename=src/token.test.ts reports=3
|
|
30
29
|
it('should be defined', () => {
|
|
31
30
|
expect(service).toBeDefined();
|
|
32
31
|
});
|
|
@@ -40,8 +39,7 @@ it('works', () => {
|
|
|
40
39
|
});
|
|
41
40
|
```
|
|
42
41
|
|
|
43
|
-
```ts
|
|
44
|
-
// Good
|
|
42
|
+
```ts good filename=src/token.test.ts
|
|
45
43
|
it('issues a signed token for the user', () => {
|
|
46
44
|
const token = issueToken({ userId: 'u-1' });
|
|
47
45
|
expect(verify(token)).toEqual({ userId: 'u-1' });
|
|
@@ -18,16 +18,18 @@ The **last** statement of a function block that is an `if` with:
|
|
|
18
18
|
A single-statement `if`, an `if/else`, or an `if` that is not the final statement is left alone —
|
|
19
19
|
those are not body-wraps.
|
|
20
20
|
|
|
21
|
-
```ts
|
|
22
|
-
//
|
|
21
|
+
```ts bad
|
|
22
|
+
// the whole body is wrapped
|
|
23
23
|
function handle(x) {
|
|
24
24
|
if (x) {
|
|
25
25
|
doA();
|
|
26
26
|
doB();
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
```
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
```ts good
|
|
32
|
+
// guard clause
|
|
31
33
|
function handle(x) {
|
|
32
34
|
if (!x) {
|
|
33
35
|
return;
|
|
@@ -21,16 +21,18 @@ window above it.
|
|
|
21
21
|
Faithful to the original text-scanning implementation, the source is scanned line by line, so a marker
|
|
22
22
|
in a trailing comment, a preceding comment, or anywhere in the lookback window is honoured.
|
|
23
23
|
|
|
24
|
-
```ts
|
|
25
|
-
//
|
|
24
|
+
```ts bad filename=src/example.test.ts reports=2
|
|
25
|
+
// untracked
|
|
26
|
+
it.skip('later', () => {});
|
|
27
|
+
xdescribe('later', () => {});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ts good filename=src/example.test.ts
|
|
31
|
+
// tracked
|
|
26
32
|
// TODO(@alice): flaky under CI
|
|
27
33
|
it.skip('later', () => {});
|
|
28
34
|
|
|
29
35
|
it.skip('later', () => {}); // https://github.com/org/repo/issues/1
|
|
30
|
-
|
|
31
|
-
// ✗ untracked
|
|
32
|
-
it.skip('later', () => {});
|
|
33
|
-
xdescribe('later', () => {});
|
|
34
36
|
```
|
|
35
37
|
|
|
36
38
|
## Options
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noctcore/eslint-plugin-code-quality",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Portable code-quality, comment-hygiene, and test-discipline ESLint rules.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"test": "vitest run"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@noctcore/eslint-utils": "^0.1.
|
|
49
|
+
"@noctcore/eslint-utils": "^0.1.1",
|
|
50
50
|
"@typescript-eslint/utils": "^8.61.1"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|