@noctcore/eslint-plugin-contracts 0.5.0 → 0.6.1
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 +16 -4
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +16 -4
- package/docs/rules/env-var-schema-parity.md +1 -1
- package/docs/rules/fetch-must-check-ok.md +44 -20
- package/docs/rules/money-must-be-decimal.md +25 -5
- package/docs/rules/no-direct-process-env.md +9 -3
- package/docs/rules/no-error-stringify.md +8 -7
- package/docs/rules/require-error-cause.md +6 -5
- package/docs/rules/require-registered-keys.md +4 -5
- package/docs/rules/require-schema-parse-at-boundary.md +6 -6
- package/docs/rules/restrict-throw-to-taxonomy.md +6 -4
- package/docs/rules/schema-enum-field-consistency.md +8 -4
- package/docs/rules/translation-key-exists.md +1 -1
- package/docs/rules/wire-message-naming.md +8 -6
- package/docs/rules/zod-schema-naming.md +6 -5
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -639,6 +639,7 @@ var DEFAULT_FIELD_PATTERNS = [
|
|
|
639
639
|
"balance"
|
|
640
640
|
];
|
|
641
641
|
var DEFAULT_ALLOWED_FILES = [];
|
|
642
|
+
var DEFAULT_MINOR_UNIT_PATTERNS = [];
|
|
642
643
|
var optionSchema3 = {
|
|
643
644
|
type: "object",
|
|
644
645
|
additionalProperties: false,
|
|
@@ -654,6 +655,11 @@ var optionSchema3 = {
|
|
|
654
655
|
type: "array",
|
|
655
656
|
items: { type: "string" },
|
|
656
657
|
uniqueItems: true
|
|
658
|
+
},
|
|
659
|
+
minorUnitPatterns: {
|
|
660
|
+
type: "array",
|
|
661
|
+
items: { type: "string", minLength: 1 },
|
|
662
|
+
uniqueItems: true
|
|
657
663
|
}
|
|
658
664
|
}
|
|
659
665
|
};
|
|
@@ -692,7 +698,8 @@ var moneyMustBeDecimalRule = createRule({
|
|
|
692
698
|
{
|
|
693
699
|
decimalType: DEFAULT_DECIMAL_TYPE,
|
|
694
700
|
fieldPatterns: [...DEFAULT_FIELD_PATTERNS],
|
|
695
|
-
allowedFiles: []
|
|
701
|
+
allowedFiles: [],
|
|
702
|
+
minorUnitPatterns: []
|
|
696
703
|
}
|
|
697
704
|
],
|
|
698
705
|
create(context, [options]) {
|
|
@@ -703,6 +710,11 @@ var moneyMustBeDecimalRule = createRule({
|
|
|
703
710
|
}
|
|
704
711
|
const fieldPatterns = options.fieldPatterns ?? DEFAULT_FIELD_PATTERNS;
|
|
705
712
|
const moneyPattern = new RegExp(`(${fieldPatterns.join("|")})`, "i");
|
|
713
|
+
const minorUnitPatterns = options.minorUnitPatterns ?? DEFAULT_MINOR_UNIT_PATTERNS;
|
|
714
|
+
const minorUnitPattern = minorUnitPatterns.length > 0 ? new RegExp(`(${minorUnitPatterns.join("|")})`, "i") : null;
|
|
715
|
+
function isDecimalMoneyName(name) {
|
|
716
|
+
return moneyPattern.test(name) && !(minorUnitPattern?.test(name) ?? false);
|
|
717
|
+
}
|
|
706
718
|
function report(node) {
|
|
707
719
|
context.report({ node, messageId: "moneyMustBeDecimal", data: { decimalType } });
|
|
708
720
|
}
|
|
@@ -713,7 +725,7 @@ var moneyMustBeDecimalRule = createRule({
|
|
|
713
725
|
return;
|
|
714
726
|
}
|
|
715
727
|
const name = staticName(node.key);
|
|
716
|
-
if (name !== void 0 &&
|
|
728
|
+
if (name !== void 0 && isDecimalMoneyName(name) && isNumberAnnotation(node.typeAnnotation)) {
|
|
717
729
|
report(node);
|
|
718
730
|
}
|
|
719
731
|
},
|
|
@@ -723,7 +735,7 @@ var moneyMustBeDecimalRule = createRule({
|
|
|
723
735
|
return;
|
|
724
736
|
}
|
|
725
737
|
const name = node.id.name;
|
|
726
|
-
if (
|
|
738
|
+
if (isDecimalMoneyName(name) && isNumberAnnotation(node.id.typeAnnotation)) {
|
|
727
739
|
report(node);
|
|
728
740
|
}
|
|
729
741
|
}
|
|
@@ -2294,7 +2306,7 @@ var rules = {
|
|
|
2294
2306
|
|
|
2295
2307
|
// src/index.ts
|
|
2296
2308
|
var NAMESPACE = "noctcore-contracts";
|
|
2297
|
-
var VERSION = "0.
|
|
2309
|
+
var VERSION = "0.6.1";
|
|
2298
2310
|
var plugin = {
|
|
2299
2311
|
meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
|
|
2300
2312
|
rules,
|
package/dist/index.d.cts
CHANGED
|
@@ -51,6 +51,12 @@ interface MoneyMustBeDecimalOptions {
|
|
|
51
51
|
readonly fieldPatterns?: readonly string[];
|
|
52
52
|
/** Path-suffix allowlist of files to skip entirely. */
|
|
53
53
|
readonly allowedFiles?: readonly string[];
|
|
54
|
+
/**
|
|
55
|
+
* Regex fragments (case-insensitive) naming fields that hold an integer count
|
|
56
|
+
* of minor units (cents), where `number` is the correct type. A money-named
|
|
57
|
+
* field matching one of these is not reported. Empty by default.
|
|
58
|
+
*/
|
|
59
|
+
readonly minorUnitPatterns?: readonly string[];
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
interface NoDirectProcessEnvOptions {
|
package/dist/index.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ interface MoneyMustBeDecimalOptions {
|
|
|
51
51
|
readonly fieldPatterns?: readonly string[];
|
|
52
52
|
/** Path-suffix allowlist of files to skip entirely. */
|
|
53
53
|
readonly allowedFiles?: readonly string[];
|
|
54
|
+
/**
|
|
55
|
+
* Regex fragments (case-insensitive) naming fields that hold an integer count
|
|
56
|
+
* of minor units (cents), where `number` is the correct type. A money-named
|
|
57
|
+
* field matching one of these is not reported. Empty by default.
|
|
58
|
+
*/
|
|
59
|
+
readonly minorUnitPatterns?: readonly string[];
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
interface NoDirectProcessEnvOptions {
|
package/dist/index.js
CHANGED
|
@@ -595,6 +595,7 @@ var DEFAULT_FIELD_PATTERNS = [
|
|
|
595
595
|
"balance"
|
|
596
596
|
];
|
|
597
597
|
var DEFAULT_ALLOWED_FILES = [];
|
|
598
|
+
var DEFAULT_MINOR_UNIT_PATTERNS = [];
|
|
598
599
|
var optionSchema3 = {
|
|
599
600
|
type: "object",
|
|
600
601
|
additionalProperties: false,
|
|
@@ -610,6 +611,11 @@ var optionSchema3 = {
|
|
|
610
611
|
type: "array",
|
|
611
612
|
items: { type: "string" },
|
|
612
613
|
uniqueItems: true
|
|
614
|
+
},
|
|
615
|
+
minorUnitPatterns: {
|
|
616
|
+
type: "array",
|
|
617
|
+
items: { type: "string", minLength: 1 },
|
|
618
|
+
uniqueItems: true
|
|
613
619
|
}
|
|
614
620
|
}
|
|
615
621
|
};
|
|
@@ -648,7 +654,8 @@ var moneyMustBeDecimalRule = createRule({
|
|
|
648
654
|
{
|
|
649
655
|
decimalType: DEFAULT_DECIMAL_TYPE,
|
|
650
656
|
fieldPatterns: [...DEFAULT_FIELD_PATTERNS],
|
|
651
|
-
allowedFiles: []
|
|
657
|
+
allowedFiles: [],
|
|
658
|
+
minorUnitPatterns: []
|
|
652
659
|
}
|
|
653
660
|
],
|
|
654
661
|
create(context, [options]) {
|
|
@@ -659,6 +666,11 @@ var moneyMustBeDecimalRule = createRule({
|
|
|
659
666
|
}
|
|
660
667
|
const fieldPatterns = options.fieldPatterns ?? DEFAULT_FIELD_PATTERNS;
|
|
661
668
|
const moneyPattern = new RegExp(`(${fieldPatterns.join("|")})`, "i");
|
|
669
|
+
const minorUnitPatterns = options.minorUnitPatterns ?? DEFAULT_MINOR_UNIT_PATTERNS;
|
|
670
|
+
const minorUnitPattern = minorUnitPatterns.length > 0 ? new RegExp(`(${minorUnitPatterns.join("|")})`, "i") : null;
|
|
671
|
+
function isDecimalMoneyName(name) {
|
|
672
|
+
return moneyPattern.test(name) && !(minorUnitPattern?.test(name) ?? false);
|
|
673
|
+
}
|
|
662
674
|
function report(node) {
|
|
663
675
|
context.report({ node, messageId: "moneyMustBeDecimal", data: { decimalType } });
|
|
664
676
|
}
|
|
@@ -669,7 +681,7 @@ var moneyMustBeDecimalRule = createRule({
|
|
|
669
681
|
return;
|
|
670
682
|
}
|
|
671
683
|
const name = staticName(node.key);
|
|
672
|
-
if (name !== void 0 &&
|
|
684
|
+
if (name !== void 0 && isDecimalMoneyName(name) && isNumberAnnotation(node.typeAnnotation)) {
|
|
673
685
|
report(node);
|
|
674
686
|
}
|
|
675
687
|
},
|
|
@@ -679,7 +691,7 @@ var moneyMustBeDecimalRule = createRule({
|
|
|
679
691
|
return;
|
|
680
692
|
}
|
|
681
693
|
const name = node.id.name;
|
|
682
|
-
if (
|
|
694
|
+
if (isDecimalMoneyName(name) && isNumberAnnotation(node.id.typeAnnotation)) {
|
|
683
695
|
report(node);
|
|
684
696
|
}
|
|
685
697
|
}
|
|
@@ -2250,7 +2262,7 @@ var rules = {
|
|
|
2250
2262
|
|
|
2251
2263
|
// src/index.ts
|
|
2252
2264
|
var NAMESPACE = "noctcore-contracts";
|
|
2253
|
-
var VERSION = "0.
|
|
2265
|
+
var VERSION = "0.6.1";
|
|
2254
2266
|
var plugin = {
|
|
2255
2267
|
meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
|
|
2256
2268
|
rules,
|
|
@@ -15,7 +15,7 @@ declaration in lockstep.
|
|
|
15
15
|
A static `process.env.FOO` or `import.meta.env.FOO` access whose key `FOO` is not declared in the
|
|
16
16
|
configured schema file:
|
|
17
17
|
|
|
18
|
-
```ts
|
|
18
|
+
```ts prose reason="the rule reads the schema file named in its options from disk"
|
|
19
19
|
// schema (.env.example) declares DATABASE_URL, PORT, NODE_ENV
|
|
20
20
|
|
|
21
21
|
// ✗
|
|
@@ -15,27 +15,37 @@ status the server actually sent. Check the response first, and the failure is na
|
|
|
15
15
|
|
|
16
16
|
A `.json()` read on a response bound from a configured fetch callee when no check governs it.
|
|
17
17
|
|
|
18
|
-
```ts
|
|
19
|
-
//
|
|
18
|
+
```ts bad reports=4
|
|
19
|
+
// no check at all
|
|
20
20
|
export async function loadUser(id: string) {
|
|
21
21
|
const res = await fetch(`/api/users/${id}`);
|
|
22
22
|
return res.json();
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// the check comes after the body is already parsed
|
|
26
|
+
export async function loadOrder(id: string) {
|
|
27
|
+
const res = await fetch(`/api/orders/${id}`);
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
if (!res.ok) throw new Error('failed');
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
28
32
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
// a single-code guard lets every other error through
|
|
34
|
+
export async function findUser(id: string) {
|
|
35
|
+
const res = await fetch(`/api/users/${id}`);
|
|
36
|
+
if (res.status === 404) return null;
|
|
37
|
+
return res.json();
|
|
38
|
+
}
|
|
32
39
|
|
|
33
|
-
//
|
|
34
|
-
|
|
40
|
+
// `||` runs the parse exactly on the failure path
|
|
41
|
+
export async function loadFlags() {
|
|
42
|
+
const res = await fetch('/api/flags');
|
|
43
|
+
return res.ok || res.json();
|
|
44
|
+
}
|
|
35
45
|
```
|
|
36
46
|
|
|
37
|
-
```ts
|
|
38
|
-
//
|
|
47
|
+
```ts good
|
|
48
|
+
// guard clause
|
|
39
49
|
export async function loadUser(id: string) {
|
|
40
50
|
const res = await fetch(`/api/users/${id}`);
|
|
41
51
|
if (!res.ok) {
|
|
@@ -44,16 +54,30 @@ export async function loadUser(id: string) {
|
|
|
44
54
|
return res.json();
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
//
|
|
48
|
-
|
|
57
|
+
// the parse sits in the branch the check permits
|
|
58
|
+
export async function findUser(id: string) {
|
|
59
|
+
const res = await fetch(`/api/users/${id}`);
|
|
60
|
+
return res.ok ? res.json() : null;
|
|
61
|
+
}
|
|
49
62
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
// a status split at the success/error boundary, or a switch on success codes
|
|
64
|
+
export async function loadOrder(id: string) {
|
|
65
|
+
const res = await fetch(`/api/orders/${id}`);
|
|
66
|
+
if (res.status >= 400) return null;
|
|
67
|
+
return res.json();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function createOrder() {
|
|
71
|
+
const res = await fetch('/api/orders', { method: 'POST' });
|
|
72
|
+
switch (res.status) { case 200: case 201: return res.json(); default: return null; }
|
|
73
|
+
}
|
|
53
74
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
// an assertion helper (`assert`, `invariant`, `ensure*`, `expect*`)
|
|
76
|
+
export async function loadFlags() {
|
|
77
|
+
const res = await fetch('/api/flags');
|
|
78
|
+
invariant(res.ok, 'user request failed');
|
|
79
|
+
return res.json();
|
|
80
|
+
}
|
|
57
81
|
```
|
|
58
82
|
|
|
59
83
|
Three response shapes are tracked: `const res = await fetch(...)` then `res.json()` in the same block,
|
|
@@ -20,12 +20,12 @@ Conservative on purpose. Untyped declarations and numeric-literal initializers (
|
|
|
20
20
|
**not** flagged — those are usually counters/accumulators. Interface and type-literal members
|
|
21
21
|
(`{ amount: number }`) are **out of scope** so non-money type members do not regress.
|
|
22
22
|
|
|
23
|
-
```ts
|
|
24
|
-
// ✗
|
|
23
|
+
```ts bad reports=2
|
|
25
24
|
class Invoice { total: number; }
|
|
26
25
|
const amount: number = 5;
|
|
26
|
+
```
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
```ts good
|
|
29
29
|
class Invoice { total: Decimal; }
|
|
30
30
|
const count: number = 3; // not a money name
|
|
31
31
|
interface Payment { amount: number; } // type member, out of scope
|
|
@@ -38,6 +38,7 @@ interface Payment { amount: number; } // type member, out of scope
|
|
|
38
38
|
| `decimalType` | `string` | `'Decimal'` | Name of the money type to require; appears in the report message. |
|
|
39
39
|
| `fieldPatterns` | `string[]` | `['amount', 'price', 'cost', 'total', 'balance']` | Case-insensitive regex fragments identifying money-named fields (OR-combined). |
|
|
40
40
|
| `allowedFiles` | `string[]` | `[]` | Path-suffix allowlist of files skipped entirely (e.g. `apps/api/src/legacy/totals.ts`). |
|
|
41
|
+
| `minorUnitPatterns` | `string[]` | `[]` | Case-insensitive regex fragments naming fields that hold integer minor units on purpose and are therefore not reported. Checked before `fieldPatterns`. |
|
|
41
42
|
|
|
42
43
|
```js
|
|
43
44
|
'noctcore-contracts/money-must-be-decimal': ['error', {
|
|
@@ -46,7 +47,26 @@ interface Payment { amount: number; } // type member, out of scope
|
|
|
46
47
|
}]
|
|
47
48
|
```
|
|
48
49
|
|
|
50
|
+
## Talking to a payment API
|
|
51
|
+
|
|
52
|
+
Stripe and most payment APIs deal in integer minor units: `amount` is a number of cents, and that
|
|
53
|
+
is correct at that boundary. Without `minorUnitPatterns` this rule flags every one of those
|
|
54
|
+
fields, which is how a project ends up turning the rule off entirely and losing it everywhere
|
|
55
|
+
else.
|
|
56
|
+
|
|
57
|
+
Name the boundary fields instead, so the rule keeps policing the rest of the codebase:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{ "minorUnitPatterns": ["amountInCents", "unitAmount", "^amount$"] }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Prefer patterns that are specific to the boundary. A blanket `amount` would exempt every field
|
|
64
|
+
whose name contains it, including the internal totals this rule exists to protect.
|
|
65
|
+
|
|
49
66
|
## When not to use it
|
|
50
67
|
|
|
51
|
-
If your project
|
|
52
|
-
|
|
68
|
+
If your project has no dedicated Decimal money type, this rule does not fit.
|
|
69
|
+
|
|
70
|
+
If it represents money as integer minor units **everywhere**, by choice, the rule can still be
|
|
71
|
+
useful with `minorUnitPatterns` covering that convention, but at that point it is asserting a
|
|
72
|
+
naming convention rather than a type, and you may not want it.
|
|
@@ -15,19 +15,25 @@ Any `process.env` access, in every position — property read (`process.env.X`),
|
|
|
15
15
|
(`process.env[X]`), destructure (`const { X } = process.env`), or the bare value passed / returned /
|
|
16
16
|
assigned (`log(process.env)`, `return process.env`). Computed `process['env']` cannot bypass it.
|
|
17
17
|
|
|
18
|
-
```ts
|
|
19
|
-
// ✗
|
|
18
|
+
```ts bad reports=3
|
|
20
19
|
const isProd = process.env.NODE_ENV === 'production';
|
|
21
20
|
const { DATABASE_URL } = process.env;
|
|
22
21
|
const env = process['env'];
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```ts good
|
|
25
|
+
import { config } from '@/config';
|
|
23
26
|
|
|
24
|
-
// ✓
|
|
25
27
|
const isProd = config.isProduction;
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
Files matched by the `allowedFiles` glob allowlist are skipped entirely, so bootstrap entrypoints,
|
|
29
31
|
config files, and tests may still read `process.env` directly.
|
|
30
32
|
|
|
33
|
+
```ts good filename=vite.config.ts relocation
|
|
34
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
35
|
+
```
|
|
36
|
+
|
|
31
37
|
## Options
|
|
32
38
|
|
|
33
39
|
| Option | Type | Default | Meaning |
|
|
@@ -9,7 +9,7 @@ discarding `error.cause`, the stack, and any custom fields. The value that reach
|
|
|
9
9
|
sentence with no chain to the underlying failure. The guarded extractor idiom preserves the object for
|
|
10
10
|
structured loggers and stays legal:
|
|
11
11
|
|
|
12
|
-
```ts
|
|
12
|
+
```ts good
|
|
13
13
|
error instanceof Error ? error.message : String(error)
|
|
14
14
|
```
|
|
15
15
|
|
|
@@ -18,14 +18,15 @@ error instanceof Error ? error.message : String(error)
|
|
|
18
18
|
Only the three unambiguous cause-chain-dropping forms, and only when the operand is a known error
|
|
19
19
|
identifier (default `error`, `err`, `e`, `cause`):
|
|
20
20
|
|
|
21
|
-
```ts
|
|
22
|
-
// ✗
|
|
21
|
+
```ts bad reports=4
|
|
23
22
|
logger.error(`request failed: ${error}`);
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
23
|
+
const a = err.toString();
|
|
24
|
+
const b = error + "";
|
|
25
|
+
const c = "" + e;
|
|
26
|
+
```
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
```ts good
|
|
29
|
+
// the guarded idiom (bare String(error) is intentionally NOT policed)
|
|
29
30
|
const msg = error instanceof Error ? error.message : String(error);
|
|
30
31
|
const m = `${error.message}`;
|
|
31
32
|
```
|
|
@@ -9,7 +9,7 @@ forward the caught error as `cause`, the underlying failure — its message, sta
|
|
|
9
9
|
cause — is gone. The stack you see points only at the re-throw site. `Error`'s standard `cause`
|
|
10
10
|
option (and every `*Error` subclass that forwards it) preserves the chain:
|
|
11
11
|
|
|
12
|
-
```ts
|
|
12
|
+
```ts good
|
|
13
13
|
try {
|
|
14
14
|
await db.query(sql);
|
|
15
15
|
} catch (err) {
|
|
@@ -34,14 +34,15 @@ Deliberately conservative:
|
|
|
34
34
|
A `throw` nested in a closure declared inside the catch is still flagged: the binding is genuinely in
|
|
35
35
|
scope there. Nested `try/catch` uses the nearest binding.
|
|
36
36
|
|
|
37
|
-
```ts
|
|
38
|
-
//
|
|
37
|
+
```ts bad reports=2
|
|
38
|
+
// drops the cause: autofixes to `new Error('failed', { cause: err })`
|
|
39
39
|
try { work(); } catch (err) { throw new Error('failed'); }
|
|
40
40
|
|
|
41
|
-
//
|
|
41
|
+
// merges into an existing options object
|
|
42
42
|
try { work(); } catch (err) { throw new HttpError('failed', { status: 500 }); }
|
|
43
|
+
```
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
```ts good
|
|
45
46
|
try { work(); } catch (err) { throw new Error('failed', { cause: err }); }
|
|
46
47
|
try { work(); } catch (err) { throw err; }
|
|
47
48
|
```
|
|
@@ -14,14 +14,13 @@ compile error instead of a runtime miss.
|
|
|
14
14
|
|
|
15
15
|
A **raw string literal** in the configured key position of a configured sink call:
|
|
16
16
|
|
|
17
|
-
```ts
|
|
18
|
-
// with sinks: [{ callee: 'localStorage.getItem', argIndex: 0 }, { callee: 'emitter.on', argIndex: 0 }]
|
|
19
|
-
|
|
20
|
-
// ✗
|
|
17
|
+
```ts bad reports=2 options={"sinks":[{"callee":"localStorage.getItem","argIndex":0},{"callee":"emitter.on","argIndex":0}]}
|
|
21
18
|
localStorage.getItem('user-profile');
|
|
22
19
|
emitter.on('task-done', handler);
|
|
20
|
+
```
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
```ts good options={"sinks":[{"callee":"localStorage.getItem","argIndex":0},{"callee":"emitter.on","argIndex":0}]}
|
|
23
|
+
// imported constant
|
|
25
24
|
import { USER_PROFILE_KEY, TASK_DONE } from '@/keys';
|
|
26
25
|
localStorage.getItem(USER_PROFILE_KEY);
|
|
27
26
|
emitter.on(TASK_DONE, handler);
|
|
@@ -10,7 +10,7 @@ promise the runtime never checks: a field the server dropped is now `undefined`
|
|
|
10
10
|
`string`, and the corruption surfaces far from the boundary. Parsing with a runtime schema
|
|
11
11
|
(zod/valibot) validates the shape at the edge and fails loudly there:
|
|
12
12
|
|
|
13
|
-
```ts
|
|
13
|
+
```ts good
|
|
14
14
|
const user = UserSchema.parse(await res.json()); // validated
|
|
15
15
|
```
|
|
16
16
|
|
|
@@ -19,15 +19,15 @@ const user = UserSchema.parse(await res.json()); // validated
|
|
|
19
19
|
This is a **conservative syntactic slice** of a concept that is fully general only with type
|
|
20
20
|
information. It flags a cast applied **directly** to a call site that is unmistakably a boundary read:
|
|
21
21
|
|
|
22
|
-
```ts
|
|
23
|
-
// ✗
|
|
22
|
+
```ts bad reports=3
|
|
24
23
|
const user = JSON.parse(raw) as User;
|
|
25
24
|
const users = JSON.parse(raw) as User[];
|
|
26
|
-
const
|
|
25
|
+
const fetched = (await res.json()) as User;
|
|
26
|
+
```
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
```ts good
|
|
29
29
|
const user = UserSchema.parse(JSON.parse(raw));
|
|
30
|
-
const
|
|
30
|
+
const fetched = UserSchema.parse(await res.json());
|
|
31
31
|
const data = JSON.parse(raw) as unknown; // safe widening, not a shape claim
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -19,15 +19,17 @@ Conservative on the ambiguous forms. A bare identifier (`throw err` — the re-t
|
|
|
19
19
|
(`throw ctx.error`), and a call (`throw makeError()`) are all left alone: a syntactic rule cannot know
|
|
20
20
|
whether they resolve to an Error, and re-throwing a caught error is the most common `throw` there is.
|
|
21
21
|
|
|
22
|
-
```ts
|
|
23
|
-
//
|
|
22
|
+
```ts bad reports=3
|
|
23
|
+
// built-in not in the taxonomy
|
|
24
24
|
throw new TypeError('bad');
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// bare values
|
|
27
27
|
throw 'boom';
|
|
28
28
|
throw { code: 500 };
|
|
29
|
+
```
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
```ts good
|
|
32
|
+
// default allow is ['Error']
|
|
31
33
|
throw new Error('boom');
|
|
32
34
|
try { work(); } catch (err) { throw err; }
|
|
33
35
|
```
|
|
@@ -26,8 +26,8 @@ Per file, purely syntactic, no type information. The rule collects every propert
|
|
|
26
26
|
Every **string** occurrence of a key that is **enum** elsewhere in the file is reported. No autofix:
|
|
27
27
|
the right fix may be a data migration (the stored column was free text), not a schema edit.
|
|
28
28
|
|
|
29
|
-
```ts
|
|
30
|
-
//
|
|
29
|
+
```ts bad reports=1
|
|
30
|
+
// the output widens what both inputs narrow
|
|
31
31
|
export const statusSchema = z.enum(['OPEN', 'CLOSED']);
|
|
32
32
|
|
|
33
33
|
export const ticketCreateInput = z.object({ status: statusSchema.default('OPEN') });
|
|
@@ -38,8 +38,12 @@ export const ticketOutput = z.object({
|
|
|
38
38
|
});
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
```ts
|
|
42
|
-
//
|
|
41
|
+
```ts good
|
|
42
|
+
// the output reuses the enum
|
|
43
|
+
export const statusSchema = z.enum(['OPEN', 'CLOSED']);
|
|
44
|
+
|
|
45
|
+
export const ticketCreateInput = z.object({ status: statusSchema.default('OPEN') });
|
|
46
|
+
export const ticketUpdateInput = z.object({ status: statusSchema.optional() });
|
|
43
47
|
export const ticketOutput = z.object({
|
|
44
48
|
id: z.string(),
|
|
45
49
|
status: statusSchema.nullable(),
|
|
@@ -14,7 +14,7 @@ the ones that cannot resolve.
|
|
|
14
14
|
|
|
15
15
|
A static key that no configured catalog of the namespace in scope contains:
|
|
16
16
|
|
|
17
|
-
```tsx
|
|
17
|
+
```tsx prose reason="the rule reads the translation catalogs named in its options from disk"
|
|
18
18
|
// catalogs: common = { actions: { save, cancel } }, portal = { tasks: { title } }
|
|
19
19
|
|
|
20
20
|
const { t } = useTranslation(); // default namespace: common
|
|
@@ -16,15 +16,17 @@ whose zod object declares a `type: z.literal('…')` property, the literal must
|
|
|
16
16
|
`kebab(constName minus the role suffix)`. A mismatch is reported and **autofixed** to the expected
|
|
17
17
|
value.
|
|
18
18
|
|
|
19
|
-
```ts
|
|
20
|
-
//
|
|
19
|
+
```ts bad reports=2
|
|
20
|
+
// camelCase discriminant: autofixes to 'task-completed'
|
|
21
21
|
export const TaskCompletedEvent = z.object({ type: z.literal('taskCompleted') });
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// wrong value: autofixes to 'run-task'
|
|
24
24
|
export const RunTaskCommand = z.object({ type: z.literal('run') });
|
|
25
|
+
```
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
```ts good
|
|
27
28
|
export const TaskCompletedEvent = z.object({ type: z.literal('task-completed') });
|
|
29
|
+
export const RunTaskCommand = z.object({ type: z.literal('run-task') });
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
Consts without a role suffix, and role-suffixed consts without a `type` literal, are ignored.
|
|
@@ -39,8 +41,8 @@ Consts without a role suffix, and role-suffixed consts without a `type` literal,
|
|
|
39
41
|
'noctcore-contracts/wire-message-naming': ['error', { roleSuffixes: ['Message'] }]
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
```ts
|
|
43
|
-
// with roleSuffixes: ['Message']
|
|
44
|
+
```ts bad options={"roleSuffixes":["Message"]}
|
|
45
|
+
// with roleSuffixes: ['Message'], autofixes to 'task-done'
|
|
44
46
|
export const TaskDoneMessage = z.object({ type: z.literal('done') });
|
|
45
47
|
```
|
|
46
48
|
|
|
@@ -17,14 +17,15 @@ For every `export const` whose initializer is rooted at the `z` identifier (`z.o
|
|
|
17
17
|
- a correctly-named `FooSchema` must have a sibling `export type Foo` (a `type` alias or
|
|
18
18
|
`interface`) — otherwise `missingType`.
|
|
19
19
|
|
|
20
|
-
```ts
|
|
21
|
-
//
|
|
20
|
+
```ts bad reports=2
|
|
21
|
+
// not suffixed `Schema`
|
|
22
22
|
export const Task = z.object({});
|
|
23
23
|
|
|
24
|
-
//
|
|
24
|
+
// no sibling inferred type
|
|
25
25
|
export const TaskSchema = z.object({});
|
|
26
|
+
```
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
```ts good
|
|
28
29
|
export const TaskSchema = z.object({ id: z.string() });
|
|
29
30
|
export type Task = z.infer<typeof TaskSchema>;
|
|
30
31
|
```
|
|
@@ -43,7 +44,7 @@ suffixes to carve them out:
|
|
|
43
44
|
'noctcore-contracts/zod-schema-naming': ['error', { roleSuffixes: ['Event', 'Command', 'Query'] }]
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
```ts
|
|
47
|
+
```ts good options={"roleSuffixes":["Event","Command","Query"]} reconfigured
|
|
47
48
|
// carved out only when 'Command' is listed in roleSuffixes
|
|
48
49
|
export const RunTaskCommand = z.object({ type: z.literal('run-task') });
|
|
49
50
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noctcore/eslint-plugin-contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "ESLint rules for shared contract, config, error-handling, and money-precision conventions (zod schema naming, wire discriminants, no-direct-process-env, decimal money).",
|
|
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": {
|