@sarj/eslint-plugin 2.6.0 → 2.8.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.
package/README.md CHANGED
@@ -12,6 +12,131 @@ import sarj from "@sarj/eslint-plugin";
12
12
  export default [...sarj.configs.recommended];
13
13
  ```
14
14
 
15
- Each rule's source under `lib/rules/` carries its own `meta.docs.description` + `meta.messages` — read the file for full rationale.
15
+ Each rule's source under `src/rules/` carries its own `@fileoverview` rationale plus `meta.docs.description` + `meta.messages` — read the file for the full reasoning, including the false positives it deliberately does not fire on.
16
16
 
17
17
  Presets: `recommended` (warn-first), `strict` (every rule at error), `style-guide` (formatting/naming subset).
18
+
19
+ ## Options
20
+
21
+ ### Declare your logger (`loggerNames` / `logFunctions`)
22
+
23
+ Three rules decide whether a call writes to a log sink: `no-log-only-catch`,
24
+ `no-sentinel-return-on-catch`, `no-secret-in-log`. Out of the box they recognise
25
+ a log method on a logger *receiver* (`console.error`, `logger.warn`,
26
+ `this.logger.info`). A structured logger is usually a free *function* taking a
27
+ meta object, which has no receiver — declare it once and all of them see it:
28
+
29
+ ```js
30
+ const logging = { logFunctions: ["logEvent"], loggerNames: ["obs"] };
31
+
32
+ rules: {
33
+ "@sarj/no-log-only-catch": ["error", logging],
34
+ "@sarj/no-sentinel-return-on-catch": ["error", logging],
35
+ "@sarj/no-secret-in-log": ["error", logging],
36
+ }
37
+ ```
38
+
39
+ - `logFunctions` — free functions (or methods) that log: `logEvent("x", { err })`.
40
+ - `loggerNames` — extra logger *receiver* names, added to the built-in set.
41
+
42
+ This suppresses "swallows the error without logging it" on a correctly-logged
43
+ degraded return, and — importantly — makes `no-secret-in-log` inspect those
44
+ calls, which it could not do before: `logEvent("slack.auth", { botToken })` was
45
+ previously never examined.
46
+
47
+ ### `zod-naming-convention`: `convention`
48
+
49
+ `"either"` (default) accepts both the `Z`-prefix (`ZUser`) and the `Schema`
50
+ suffix (`userSchema`) — the two conventions `require-zod-form-validation`
51
+ already recognises. Set `"prefix"` or `"suffix"` to pin one:
52
+
53
+ ```js
54
+ "@sarj/zod-naming-convention": ["error", { convention: "suffix" }]
55
+ ```
56
+
57
+ ### `prefer-string-literal-union`: `ignoreFields`
58
+
59
+ Field names whose value set is owned by a vendor and genuinely open (a Slack
60
+ `event.subtype`, a Resend `bounceType`). Narrowing to a union you don't control
61
+ would be wrong, not better:
62
+
63
+ ```js
64
+ "@sarj/prefer-string-literal-union": ["warn", { ignoreFields: ["subtype", "bounceType"] }]
65
+ ```
66
+
67
+ ### `no-enum`: `ignoreFiles`
68
+
69
+ Glob patterns whose files opt out (generated code already opts out by default).
70
+
71
+ ## Configurable rules
72
+
73
+ Most rules take no options. These three do, because they encode a codebase's
74
+ architecture rather than a language fact — the defaults describe one convention
75
+ and every repo gets to name its own.
76
+
77
+ | Rule | Option | Default | Effect |
78
+ |---|---|---|---|
79
+ | `no-raw-fetch-outside-clients` | `allow` | client/test path patterns | Files exempt from the "no bare `fetch`" rule |
80
+ | `no-dynamic-sql` | `methods` | `["prepare", "exec", "query"]` | Statement-taking methods to inspect |
81
+ | `no-storage-in-stateless-modules` | `modules` | `[]` (rule off) | Directories declared stateless |
82
+ | `no-storage-in-stateless-modules` | `methods` | `["prepare", "put", "getWithMetadata"]` | Storage methods to flag |
83
+
84
+ Every option value is a **regular-expression source matched against the absolute
85
+ filename**, not a glob — so it can express both path separators. Supplying an
86
+ option **replaces** the default rather than extending it.
87
+
88
+ `no-storage-in-stateless-modules` is a **no-op until `modules` is set**. The
89
+ method names alone (`put`, `prepare`) carry no type information, so the rule is
90
+ only meaningful once it is pointed at the directories a team has actually
91
+ declared stateless.
92
+
93
+ ```js
94
+ // eslint.config.mjs
95
+ import sarj from "@sarj/eslint-plugin";
96
+
97
+ export default [
98
+ ...sarj.configs.strict,
99
+ {
100
+ rules: {
101
+ // This repo keeps its HTTP layer in `lib/api/`, not `clients/`.
102
+ "@sarj/no-raw-fetch-outside-clients": [
103
+ "error",
104
+ { allow: ["[\\\\/]lib[\\\\/]api[\\\\/]", "\\.test\\.", "\\.spec\\."] },
105
+ ],
106
+ // Declare which modules must stay stateless.
107
+ "@sarj/no-storage-in-stateless-modules": [
108
+ "error",
109
+ { modules: ["[\\\\/]engineer-digest[\\\\/]"] },
110
+ ],
111
+ },
112
+ },
113
+ ];
114
+ ```
115
+
116
+ Tiering: `no-dynamic-sql` is in both presets (an injection guard with a low
117
+ false-positive rate, relevant to any repo touching SQL). The two architectural
118
+ rules are **`strict`-only**, since they need per-repo configuration to say
119
+ anything useful.
120
+
121
+ ## Ported from `sarj_python_lint`
122
+
123
+ Several rules are ports of the Python linter's SARJ rules, retuned for TypeScript. The false-positive tuning documented in the Python docstrings is ported with them — that tuning is the valuable part.
124
+
125
+ | TypeScript rule | Python | What it prevents |
126
+ | --- | --- | --- |
127
+ | `prefer-constant-time-secret-compare` | SARJ011 | Byte-by-byte secret recovery through the timing of a short-circuiting `===` on a token / signature / HMAC. On Workers the fix is `crypto.subtle.timingSafeEqual` over equal-length digests. |
128
+ | `no-secret-in-log` | SARJ012 | Credentials persisted into log sinks. |
129
+ | `store-insert-requires-on-conflict` | SARJ018 | Duplicate rows — or unique-constraint failures that re-trigger the handler — when a cron re-runs or a queue message is redelivered. |
130
+ | `no-select-star` | SARJ021 | An implicit row contract that changes silently when a column is added or reordered. |
131
+ | `no-offset-pagination` | SARJ025 | O(N)-per-page scans, and rows repeated or skipped when the offset window shifts under concurrent inserts. |
132
+ | `no-repeated-string-literal` | SARJ024 | Copies of a structured literal (SQL, column lists, prompt templates) drifting apart when only one is edited. |
133
+ | `no-positional-tuple-return` | SARJ026 | Call sites re-inventing — and disagreeing on — the field names of a positional tuple return. |
134
+ | `no-sleep-in-test-body` | SARJ031 | Tests that assert on wall-clock time and flake under CI load. |
135
+ | `no-fat-try-blocks` | SARJ007 | Over-broad `catch` handlers swallowing unrelated failures. |
136
+ | `no-cors-wildcard-with-credentials` | SARJ008 | Credentialed cross-origin requests from any origin. |
137
+ | `single-public-export` | SARJ022 | Modules with no single obvious entry point. |
138
+ | `prefer-string-literal-union` | SARJ006 | An open `string` where a closed set is intended. |
139
+
140
+ Shared helpers live in `src/rules/_*.ts` (`_secret_names.ts`, `_sql.ts`, `_logging.ts`, `_paths.ts`, `_tailwind.ts`) so related rules cannot diverge on what counts as a secret, a SQL statement, a logging call, or a test file.
141
+
142
+ Deliberately **not** ported: `no-unreachable-after-terminal` (SARJ010) is already covered by `allowUnreachableCode: false` in `@sarj/tsconfig` plus ESLint core `no-unreachable`; `no-aggregation-in-store-query` (SARJ020) assumes a Postgres-OLTP / columnar-mirror split that D1 does not have; `no-query-with-many-joins` (SARJ019), `stepdown` (SARJ023), `prefer-class-row`, `prefer-struct-over-namedtuple`, `prefer-timedelta-for-durations`, and `no-fstring-in-log` have no TypeScript defect class or target API; `prefer-str-enum` is covered by `prefer-string-literal-union` + `no-enum`.