@jayyuen66/dsh-plugin-shared 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,1020 @@
1
+ import { createRequire } from "node:module";
2
+ import { defineConfig } from "oxlint";
3
+ import type { OxlintConfig, OxlintOverride } from "oxlint";
4
+
5
+ export type RuleTable = NonNullable<OxlintConfig["rules"]>;
6
+
7
+ /**
8
+ * 一条规则能被关掉的**全部**合法理由。写不进这六类的,就不该有关掉它的选项。
9
+ * 每类的判据来自 oxlint/eslint 官方文档、上游 recommended 集合的归属,加上本仓的实测。
10
+ */
11
+ export type OffReason =
12
+ /** 规则的判据与官方契约方向相反,开了等于判官方写法违规。 */
13
+ | "契约冲突"
14
+ /** 与本仓已启用的对侧规则互斥(开一侧就判另一侧死刑)。 */
15
+ | "对侧互斥"
16
+ /** 按它的意思改代码会把代码改坏,有实测的编译/运行后果。 */
17
+ | "实测否证"
18
+ /** 判据只在某种文体(用例文件 / 构建脚本)里成立,按文件种类收窄。 */
19
+ | "文体作用域"
20
+ /** 纯口味规则,上游不放进 recommended,开启只带来语义无关的重写。 */
21
+ | "上游不推荐"
22
+ /** 旧 `.oxlintrc.json` 继承下来、实测开了是 0 条 —— 没有存在的理由,直接作废。 */
23
+ | "零代价作废";
24
+
25
+ export interface OffJustification {
26
+ /** 关它的理由类别。 */
27
+ kind: OffReason;
28
+ /** 打开它在全仓产生的条数(口径见 OFF_JUSTIFICATIONS 的头注释)。 */
29
+ measured: number;
30
+ /** 一句话依据;配置加载只要求这个字段非空。 */
31
+ note: string;
32
+ }
33
+
34
+ /** `no-unsafe-call` / `-assignment` / `-return` / `-argument` 四条的依据与 member-access 同一条。 */
35
+ const NOTE_UNSAFE_SAME_AS_MEMBER_ACCESS = "同 no-unsafe-member-access";
36
+
37
+ /**
38
+ * 实测口径:临时把基线里每条 `off`(rules 与 override 层都算)翻成 `error` 再跑 15 个成员、
39
+ * 与基线取差数 `::error` 行,按路径分成 prod / test / scripts 三桶。`--deny=<rule>` 盖不住
40
+ * `off`,最早一轮用它在 13 个包目录上量出的数因此全偏小(`vitest/no-hooks` 用 `-D` 量出来是
41
+ * 0,实际是 181),整批作废重测。
42
+ *
43
+ * 键支持插件级通配(`vitest/*`),作用于该插件下没有更具体键的全部规则。
44
+ */
45
+ export const OFF_JUSTIFICATIONS: Record<string, OffJustification> = {
46
+ // ===== 文体作用域:vitest 的用例结构类判据只对 `*.test.ts` 成立 =====
47
+ // oxlint 的 `plugins` 与 `categories` 都是全局作用域,没有 per-files 的插件开关,所以
48
+ // 「只在测试文件里生效」只能这样实现:基线整族 off,再由 TEST_FILE_OVERRIDES 在用例文件上 on。
49
+ // 抄的是 eslint-plugin-vitest 官方 recommended 的 files 作用域,规则一条不丢。
50
+ // 实测挂在生产文件上的错用形态:danger-guard/host.ts 的模块级 `let strArrayWarned = false`
51
+ // 被 require-hook 判成「setup 没进 hook」(3 条)、注释里出现官方设置 API 名 `describe()`
52
+ // 被 no-commented-out-tests 判成「注释掉了用例」(3 条)、
53
+ // dir-prep-organize/src/client-entry.ts:437 同类 1 条。
54
+ "vitest/*": { kind: "文体作用域", measured: 7, note: "用例结构类判据在非用例文件上无从计算" },
55
+ "vitest/no-hooks": {
56
+ kind: "上游不推荐",
57
+ measured: 181,
58
+ note: "禁用 hook 本身;本仓按官方 vitest 用法大量使用 before*/after*",
59
+ },
60
+ "vitest/no-conditional-in-test": {
61
+ kind: "文体作用域",
62
+ measured: 523,
63
+ note: "平台/宿主版本分支是刻意的(macOS 与 Linux 的家目录形状、0.1.7 与 0.1.6 的 API 面)",
64
+ },
65
+ "vitest/max-expects": {
66
+ kind: "上游不推荐",
67
+ measured: 163,
68
+ note: "给单条用例的断言条数设上限属口味,不指向缺陷",
69
+ },
70
+ "vitest/prefer-expect-assertions": {
71
+ kind: "上游不推荐",
72
+ measured: 2902,
73
+ note: "要求每个 async 用例手写 expect.assertions(n),是与断言强度无关的计数仪式",
74
+ },
75
+ "vitest/require-test-timeout": {
76
+ kind: "上游不推荐",
77
+ measured: 2892,
78
+ note: "要求每条用例显式 timeout;本仓用泵式假时钟,超时由 13 份 vitest 配置统一给",
79
+ },
80
+ "vitest/prefer-importing-vitest-globals": {
81
+ kind: "实测否证",
82
+ measured: 0,
83
+ note: "autofix 方向是删掉显式 import 改回裸全局;13 份 vitest 配置都没开 globals:true,应用后当场 3605 处 TS2593、测试全废",
84
+ },
85
+ "vitest/no-importing-vitest-globals": {
86
+ kind: "实测否证",
87
+ measured: 133,
88
+ note: "与上一条同族、方向相反,两条同时开就是把两种写法都判死",
89
+ },
90
+ "vitest/prefer-import-in-mock": {
91
+ kind: "实测否证",
92
+ measured: 6,
93
+ note: "动态形态按完整模块面校验工厂返回值,而 @types/node 的 os、@types/react 都是 `export =` 形状、类型面上没有 default;实测三处 TS2769+TS2740/2741,react 那处无论怎么 spread 都补不出 default",
94
+ },
95
+ "vitest/prefer-lowercase-title": {
96
+ kind: "上游不推荐",
97
+ measured: 171,
98
+ note: "只在用例文件里生效:它带 autofix,实测一轮 oxlint --fix 静默小写化了 260 条标题(含 nO_VERIFY 这种坏名)。包级 titlePrefixes 按 owner 2026-09-28 的口径只留真正的技术名(GET/CSRF/组件名…):规格 ticket 号已从标题里删掉,中文开头的标题天然满足判据(无大小写)⇒ 免检面能收多小收多小。实测 oxlint 前缀一命中就把整条标题免检,所以陈旧条目由 scripts/assert-config-baseline.mjs 的 LINT_TITLE_PREFIX_UNUSED 点名,不靠人工巡检",
99
+ },
100
+ "vitest/prefer-called-times": {
101
+ kind: "对侧互斥",
102
+ measured: 0,
103
+ note: "探针实测(同文件四种写法各跑一次):toHaveBeenCalledTimes(1) 被 prefer-called-once 判死、toHaveBeenCalledOnce() 被本条判死、expect(spy.mock.calls).toHaveLength(1) 被 prefer-to-have-been-called-times 判死,只有 n>=2 能同时满足三条 ⇒ 三条同开等于「单次调用」这条断言无法书写(实测逼得 plugin-hot-reload 改用 assert.equal(spy.mock.calls.length, 1) 绕行)。留 prefer-called-once 与 prefer-to-have-been-called-times 两条方向一致的组合,关本条",
104
+ },
105
+ "vitest/prefer-describe-function-title": {
106
+ kind: "对侧互斥",
107
+ measured: 48,
108
+ note: "要求 `describe(fn)`,而 valid-title 要求字符串字面量(oxlint 1.85 的 valid-title 是 DummyRule、无任何选项);上游 recommended 收的是 valid-title,取它,26 处 `describe(fn)` 按代码改成同名字符串",
109
+ },
110
+ "vitest/prefer-to-be-truthy": {
111
+ kind: "实测否证",
112
+ measured: 117,
113
+ note: "把 toStrictEqual(true) 放宽成 toTruthy(),接受任意真值是削弱断言",
114
+ },
115
+ "vitest/prefer-to-be-falsy": {
116
+ kind: "实测否证",
117
+ measured: 89,
118
+ note: "同族,放宽成 toFalsy() 同样是削弱断言",
119
+ },
120
+
121
+ // ===== 契约冲突:官方插件/入口形态本身 =====
122
+ "import/no-default-export": {
123
+ kind: "契约冲突",
124
+ measured: 38,
125
+ note: "官方 cordis 入口就是 default export(vendor/loader/src/index.ts:201 先 unwrap default),12 条正是 12 个 bundle 入口",
126
+ },
127
+ "import/no-named-export": {
128
+ kind: "契约冲突",
129
+ measured: 895,
130
+ note: "13 个包与上游 deepseek-harness 一样以具名导出 + 子路径说明符对外暴露",
131
+ },
132
+ "import/prefer-default-export": {
133
+ kind: "契约冲突",
134
+ measured: 58,
135
+ note: "与 no-default-export 同族、方向与 cordis 入口相反",
136
+ },
137
+ "import/no-nodejs-modules": {
138
+ kind: "契约冲突",
139
+ measured: 448,
140
+ note: "host 半就跑在 node 上,`node:` 内置模块是运行环境本身而不是越界",
141
+ },
142
+ "import/no-named-as-default-member": {
143
+ kind: "契约冲突",
144
+ measured: 47,
145
+ note: "对具名导出的默认对象做成员访问是本仓消费官方声明的常态",
146
+ },
147
+ "import/no-anonymous-default-export": {
148
+ kind: "契约冲突",
149
+ measured: 12,
150
+ note: "官方 cordis 入口是匿名对象 default export(registry.ts:92 读 name/Config/apply),具名化等于改官方形态",
151
+ },
152
+ "eslint/no-duplicate-imports": {
153
+ kind: "实测否证",
154
+ measured: 166,
155
+ note: "ESLint 官方已废弃(不区分 import type),由 import/no-duplicates 取代",
156
+ },
157
+
158
+ // ===== 上游不推荐:纯口味,开启只带来语义无关的重写 =====
159
+ "import/group-exports": {
160
+ kind: "上游不推荐",
161
+ measured: 766,
162
+ note: "要求单点导出,与具名导出面冲突",
163
+ },
164
+ "import/exports-last": { kind: "上游不推荐", measured: 528, note: "要求导出语句在文件末尾" },
165
+ "import/no-relative-parent-imports": {
166
+ kind: "上游不推荐",
167
+ measured: 277,
168
+ note: "包内 lib/ 与 test/ 的相对父级引用是既有结构",
169
+ },
170
+ "import/max-dependencies": {
171
+ kind: "上游不推荐",
172
+ measured: 25,
173
+ note: "给 import 条数设上限;host 半消费官方服务面天然依赖多",
174
+ },
175
+ "node/no-process-env": {
176
+ kind: "上游不推荐",
177
+ measured: 51,
178
+ note: "环境变量是本仓的输入面(DSH_HOME、profile 目录、构建期开关);eslint-plugin-n 的 recommended 也不含本条",
179
+ },
180
+ "eslint/no-magic-numbers": {
181
+ kind: "上游不推荐",
182
+ measured: 5774,
183
+ note: "把 HTTP 状态码/毫秒阈值抽成常量的计数规则",
184
+ },
185
+ "eslint/sort-keys": {
186
+ kind: "上游不推荐",
187
+ measured: 4101,
188
+ note: "对象键排序;成对字面量与 locale 字典的可读性会一起牺牲",
189
+ },
190
+ "eslint/sort-imports": {
191
+ kind: "上游不推荐",
192
+ measured: 675,
193
+ note: "字母序 import,与 oxfmt 的分组形态冲突",
194
+ },
195
+ "eslint/capitalized-comments": {
196
+ kind: "上游不推荐",
197
+ measured: 2388,
198
+ note: "本仓注释以中文为主,「首字母大写」对中文注释无从判定",
199
+ },
200
+ "eslint/no-ternary": {
201
+ kind: "上游不推荐",
202
+ measured: 1024,
203
+ note: "禁三元;改写成 if/else 并不会更正确",
204
+ },
205
+ "eslint/no-undefined": {
206
+ kind: "上游不推荐",
207
+ measured: 2009,
208
+ note: "禁 `undefined` 字面量,与本仓大量显式可选返回形态冲突",
209
+ },
210
+ "eslint/no-void": {
211
+ kind: "上游不推荐",
212
+ measured: 172,
213
+ note: "禁 `void` 前缀,而 `void promise` 正是消解 no-floating-promises 的官方写法",
214
+ },
215
+ "unicorn/no-null": {
216
+ kind: "上游不推荐",
217
+ measured: 788,
218
+ note: "禁 null;官方声明与 JSON 边界大量用 null,改成 undefined 会改变序列化形状",
219
+ },
220
+ "typescript/prefer-readonly-parameter-types": {
221
+ kind: "上游不推荐",
222
+ measured: 2433,
223
+ note: "要求每个入参类型全 readonly;官方声明面(SessionEvent/ConfigForm 等)是可变的,改不动也不该改",
224
+ },
225
+
226
+ // ===== 对侧互斥:与本仓已启用的现代语法侧规则直接相反 =====
227
+ "oxc/no-async-await": {
228
+ kind: "对侧互斥",
229
+ measured: 2160,
230
+ note: "禁 async/await,而同表里 promise-function-async、require-await、no-floating-promises 都在推 async",
231
+ },
232
+ "oxc/no-optional-chaining": {
233
+ kind: "对侧互斥",
234
+ measured: 1354,
235
+ note: "禁 `?.`,而 typescript/prefer-optional-chain 正在要求把 && 链改成 `?.`",
236
+ },
237
+ "oxc/no-rest-spread-properties": {
238
+ kind: "对侧互斥",
239
+ measured: 550,
240
+ note: "禁对象 spread,而 ES2024 目标与官方声明的投影写法都靠它",
241
+ },
242
+
243
+ // ===== 文体作用域:测试替身与构建脚本 =====
244
+ "typescript/no-explicit-any": {
245
+ kind: "文体作用域",
246
+ measured: 1,
247
+ note: "用例里的桩件面(mini-react 之类)需要 any 才塞得进官方签名",
248
+ },
249
+ "typescript/no-non-null-assertion": {
250
+ kind: "文体作用域",
251
+ measured: 536,
252
+ note: "用例里 `!` 是「前驱条件已成立」的最短写法",
253
+ },
254
+ "typescript/no-unsafe-type-assertion": {
255
+ kind: "文体作用域",
256
+ measured: 844,
257
+ note: "桩件需要 `as never` / `as unknown as` 跨官方名义类型",
258
+ },
259
+ "typescript/require-await": {
260
+ kind: "文体作用域",
261
+ measured: 467,
262
+ note: "官方 async 契约的同步实现与测试替身撑不住这条;prod 侧的条数按代码修,不进豁免",
263
+ },
264
+ "eslint/require-await": {
265
+ kind: "对侧互斥",
266
+ measured: 481,
267
+ note:
268
+ "它与 typescript/require-await 是同一条判据的两份实现,不是两条判据:探针实测(一段无 await 的 " +
269
+ "async 函数、--type-aware)单开各自都报、同开报两次 ⇒ 2 errors / 1 defect。" +
270
+ "按 typescript-eslint 官方的 extension-rule 口径裁决:留带类型信息的扩展那条、关基线那条。" +
271
+ "覆盖面没有因此变小:全仓逐行去重实测 514 行两点同报、typescript 独有 0 行、eslint 独有只落在" +
272
+ "用例面与配置自身的中间态;而 --type-aware 实测对 .mjs 也会由扩展那条报出," +
273
+ "所以 scripts/*.mjs 面不需要基线那条补位。用例文件由 TEST_OVERRIDES 整族豁免",
274
+ },
275
+ "eslint/max-statements": {
276
+ kind: "文体作用域",
277
+ measured: 549,
278
+ note: "用例体天然长(given/when/then 展开)",
279
+ },
280
+ "eslint/max-lines-per-function": { kind: "文体作用域", measured: 365, note: "同上" },
281
+ "eslint/max-lines": {
282
+ kind: "文体作用域",
283
+ measured: 54,
284
+ note: "单个用例文件的长度不作为缺陷信号",
285
+ },
286
+ "node/no-sync": {
287
+ kind: "文体作用域",
288
+ measured: 299,
289
+ note: "构建脚本与门禁脚本按设计同步读;生产代码的写侧由每包 syncWrites 逐项登记,不在豁免里",
290
+ },
291
+ "node/no-top-level-await": {
292
+ kind: "文体作用域",
293
+ measured: 53,
294
+ note: "脚本以 node 直接执行,顶层 await 是 ESM 常态;prod 侧 0 条",
295
+ },
296
+ "typescript/strict-boolean-expressions": {
297
+ kind: "文体作用域",
298
+ measured: 0,
299
+ note: "scripts/*.mjs 无类型面,隐式 any 上无从判定",
300
+ },
301
+ "typescript/no-implied-eval": {
302
+ kind: "文体作用域",
303
+ measured: 0,
304
+ note: "同上:这条要类型信息才能判",
305
+ },
306
+ "typescript/no-unsafe-member-access": {
307
+ kind: "文体作用域",
308
+ measured: 0,
309
+ note: "无类型标注的脚本里每个值都落进隐式 any",
310
+ },
311
+ "typescript/no-unsafe-call": {
312
+ kind: "文体作用域",
313
+ measured: 0,
314
+ note: NOTE_UNSAFE_SAME_AS_MEMBER_ACCESS,
315
+ },
316
+ "typescript/no-unsafe-assignment": {
317
+ kind: "文体作用域",
318
+ measured: 0,
319
+ note: NOTE_UNSAFE_SAME_AS_MEMBER_ACCESS,
320
+ },
321
+ "typescript/no-unsafe-return": {
322
+ kind: "文体作用域",
323
+ measured: 0,
324
+ note: NOTE_UNSAFE_SAME_AS_MEMBER_ACCESS,
325
+ },
326
+ "typescript/no-unsafe-argument": {
327
+ kind: "文体作用域",
328
+ measured: 0,
329
+ note: NOTE_UNSAFE_SAME_AS_MEMBER_ACCESS,
330
+ },
331
+ "typescript/require-array-sort-compare": {
332
+ kind: "文体作用域",
333
+ measured: 0,
334
+ note: "无类型脚本里「比较器缺失」判不出来",
335
+ },
336
+ "unicorn/no-process-exit": {
337
+ kind: "文体作用域",
338
+ measured: 2,
339
+ note: "CLI 脚本的 process.exit 就是它的返回方式",
340
+ },
341
+ "eslint/no-continue": {
342
+ kind: "文体作用域",
343
+ measured: 53,
344
+ note: "只对 scripts/ 豁免:TS 侧实测 0 条(包代码本来就不用 continue),908 行的门禁脚本改成嵌套 if 只会更难读",
345
+ },
346
+ "unicorn/max-nested-calls": {
347
+ kind: "文体作用域",
348
+ measured: 59,
349
+ note: "只对 scripts/ 豁免:TS 侧实测 0 条;CLI 脚本里链式 map/filter/join 的深度由数据形状决定,不指向缺陷",
350
+ },
351
+ "unicorn/no-useless-undefined": {
352
+ kind: "文体作用域",
353
+ measured: 0,
354
+ note: "脚本侧同上调;prod 仍按配置项 checkArguments:false 生效",
355
+ },
356
+
357
+ // ===== react 插件(owner 拍板开面)的存量处置 =====
358
+ // 前五条是 React Compiler 实验面(上游默认关闭),与 client 卡片的「宿主挂载适配器」形态
359
+ // 整体冲突:卡片在 effect 里同步宿主态到 store、渲染期读 ref、内联闭包传 hook,是适配器
360
+ // 形态而非交互组件渲染形态。单项收敛(useSyncExternalStore 化、闭包提升)留给源码收敛批次;
361
+ // measured 为开面实测条数(58 条里除 react-hooks 7 条外的全部)。
362
+ "react/set-state-in-effect": {
363
+ kind: "契约冲突",
364
+ measured: 16,
365
+ note: "卡片用 effect 同步宿主态到 store 是挂载适配器形态;Compiler 建议的 render 期推导要改写 12 份适配器的状态流",
366
+ },
367
+ "react/no-deriving-state-in-effects": {
368
+ kind: "契约冲突",
369
+ measured: 7,
370
+ note: "适配器形态里「props/state 派生值在 effect 对齐宿主」是挂载动作,不是可下沉到 render 的纯派生",
371
+ },
372
+ "react/exhaustive-effect-dependencies": {
373
+ kind: "契约冲突",
374
+ measured: 8,
375
+ note: "Compiler 版依赖审计对适配器的内联闭包全量追问;与 react-hooks/exhaustive-deps(保留并已修)形成双门,留后者",
376
+ },
377
+ "react/hooks": {
378
+ kind: "契约冲突",
379
+ measured: 13,
380
+ note: "「同一 hook 函数跨渲染同引用」在适配器的内联闭包形态下整体不成立;收敛批次把闭包提升后再回来开",
381
+ },
382
+ "react/refs": {
383
+ kind: "契约冲突",
384
+ measured: 1,
385
+ note: "挂载适配器在 render 期读 ref 对齐宿主布局;收敛批次改成挂载后读",
386
+ },
387
+ "react/todo": {
388
+ kind: "上游不推荐",
389
+ measured: 6,
390
+ note: "报的是 React 编译器 BuildHIR 自己不支持 try/catch(-finally) 的上游 TODO,不是代码缺陷",
391
+ },
392
+ "react/rules-of-hooks": {
393
+ kind: "文体作用域",
394
+ measured: 3,
395
+ note: "只对用例文件豁免(TEST_FILE_OVERRIDES):测试架在组件树外直接驱动卡片适配 hook(useCardOf / useSyncExternalStore 桩),调用次数顺序由桩件固定。键用表载前缀 react/(诊断文案显示 react-hooks(...) 是显示名,配置键以 --rules 的 Source 列为准)",
396
+ },
397
+ };
398
+
399
+ /**
400
+ * 旧 `.oxlintrc.json` 里继承来、此次**取消豁免**且确实零代价的规则。
401
+ *
402
+ * 这里原来记着三条。"零代价"那句话是用 `oxlint --deny=<rule>` 量的,而 `--deny` 盖不住
403
+ * override 层的 off(本文件头注释里有这条实测口径)——重测之后:`eslint/no-continue` 在
404
+ * `scripts/` 有 53 条、`eslint/no-await-in-loop` 有 2 条,于是这两条按文体收窄回
405
+ * `SCRIPTS_OVERRIDES`(TS 侧仍 0 条、仍生效),从这份"已作废"名单里撤掉。留在这儿的
406
+ * 才是真正确认过零代价的。
407
+ */
408
+ export const RETIRED_OFFS: string[] = ["typescript/no-empty-object-type"];
409
+
410
+ export const READONLY_SYNC_ALLOWLIST: string[] = [
411
+ "existsSync",
412
+ "readFileSync",
413
+ "readdirSync",
414
+ "readSync",
415
+ "realpathSync",
416
+ "statSync",
417
+ ];
418
+
419
+ const BASE_PLUGINS: NonNullable<OxlintConfig["plugins"]> = [
420
+ "eslint",
421
+ "typescript",
422
+ "unicorn",
423
+ "oxc",
424
+ "node",
425
+ "promise",
426
+ "vitest",
427
+ "import",
428
+ // react(捆 react / react-hooks / react-refresh / React Compiler 四个规则面)+ jsx-a11y:
429
+ // 11 个带 client 半的包的卡片面是 React(createElement 形态),这两个规则面是 owner 拍板要吃的。
430
+ // 存量按本仓口径处置:能修的修,确属形态冲突的进 OFF_JUSTIFICATIONS(kind+measured+note)。
431
+ "react",
432
+ "jsx-a11y",
433
+ ];
434
+
435
+ /**
436
+ * oxlint 的 **JS plugin** 面(`jsPlugins`,官方标注 alpha、不受 semver 保护)挂
437
+ * `eslint-plugin-sonarjs`,补上"同形/重复"这一类 oxlint 内建没有的判据。
438
+ *
439
+ * 入口必须是**能解析到文件的路径**:实测裸包名 `eslint-plugin-sonarjs` 在 pnpm 布局里
440
+ * 从包目录报 `Cannot find module`(它装在 workspace 根的 node_modules 下),配置就整份加载失败。
441
+ * 所以这里在装载期用 `createRequire` 从本模块解析,绝对路径不落进任何配置文件。
442
+ */
443
+ const SONARJS_ENTRY = createRequire(import.meta.url).resolve("eslint-plugin-sonarjs/cjs/plugin.js");
444
+
445
+ /**
446
+ * 逐条点名而不是通配:JS plugin 的 alpha 面里,一条规则改名或新增就可能整份配置解析失败
447
+ * (oxlint 对未知规则名的行为是**整份配置拒绝加载**,比静默保绿更糟)。
448
+ * 挂载集合与实测条数一并写在这里(owner 的口径:挂上就当场全修,不留待办、
449
+ * 不靠关规则过关):`no-duplicate-string` 全仓 291 条、`cognitive-complexity` 22 条按代码修完,
450
+ * 两条都用**默认阈值**,不调 option(调阈值就是把判据改成能过的那副样子)。
451
+ */
452
+ const SONARJS_RULES: RuleTable = {
453
+ "sonarjs/no-identical-functions": "error",
454
+ "sonarjs/no-duplicated-branches": "error",
455
+ "sonarjs/no-all-duplicated-branches": "error",
456
+ "sonarjs/no-collapsible-if": "error",
457
+ "sonarjs/no-identical-conditions": "error",
458
+ "sonarjs/no-identical-expressions": "error",
459
+ "sonarjs/no-duplicate-string": "error",
460
+ "sonarjs/cognitive-complexity": "error",
461
+ "sonarjs/max-switch-cases": "error",
462
+ "sonarjs/no-inverted-boolean-check": "error",
463
+ "sonarjs/no-duplicate-in-composite": "error",
464
+ "sonarjs/duplicates-in-character-class": "error",
465
+ "sonarjs/no-useless-catch": "error",
466
+ "sonarjs/no-extra-arguments": "error",
467
+ "sonarjs/no-redundant-assignments": "error",
468
+ "sonarjs/prefer-single-boolean-return": "error",
469
+ "sonarjs/label-position": "error",
470
+ };
471
+
472
+ const BASE_CATEGORIES: NonNullable<OxlintConfig["categories"]> = {
473
+ correctness: "error",
474
+ suspicious: "error",
475
+ style: "error",
476
+ perf: "error",
477
+ restriction: "error",
478
+ pedantic: "error",
479
+ nursery: "error",
480
+ };
481
+
482
+ /**
483
+ * `options` 是 **root-only** 字段:从上层目录一次 lint 多个包时,oxlint 会硬报
484
+ * `The options.typeAware option is only supported in the root config`。
485
+ * 因此「每包一份配置」的拓扑只能以包目录为 cwd 跑(`pnpm -r run lint` 天然满足),
486
+ * workspace 根那份只管 `scripts/`。
487
+ */
488
+ const BASE_OPTIONS: NonNullable<OxlintConfig["options"]> = {
489
+ typeAware: true,
490
+ typeCheck: true,
491
+ denyWarnings: true,
492
+ maxWarnings: 0,
493
+ reportUnusedDisableDirectives: "error",
494
+ respectEslintDisableDirectives: false,
495
+ };
496
+
497
+ /**
498
+ * 排除面只留"生成物与非源码树":`client.js`/`host.js` 是 build 产物,`coverage/`、`dist/`、
499
+ * `.toolchain/`、`node_modules/` 同理。
500
+ *
501
+ * 这里原本还排着 `*.config.mjs`、`*.config.ts`、`build-client.mjs`、`build-host.mjs`,此次删掉:
502
+ * 那四类是**人写的源码**,排掉就等于给它们开了盲区——实测因此攒过 5 条指向根本不会被 lint 的
503
+ * 规则的僵尸 `oxlint-disable`(quality-gate/build-client.mjs,删除后无任何判据报出),
504
+ * 而且和 `scripts/*.mjs` 的处理自相矛盾(同为 node 直跑的 .mjs,那边全量在 lint)。
505
+ * 配置文件与 bundler 脚本现在和各包源码走同一套规则,`*.mjs` 仍由 SCRIPTS_OVERRIDES
506
+ * 按文体收窄(类型信息类规则对无类型 .mjs 无从计算)。
507
+ */
508
+ const BASE_IGNORE_PATTERNS: string[] = [
509
+ "**/node_modules/**",
510
+ ".toolchain/**",
511
+ "coverage/**",
512
+ "dist/**",
513
+ "client.js",
514
+ "host.js",
515
+ ];
516
+
517
+ /**
518
+ * oxlint 认识的 vitest 规则全集(取自 `oxlint --rules` 的表格,73 条)。整族在基线 off,再由
519
+ * TEST_FILE_OVERRIDES 在用例文件上 on —— 理由见
520
+ * OFF_JUSTIFICATIONS 里的 `vitest/*`。列成数组而不是散着写,是为了让「上游新增一条 vitest 规则」
521
+ * 默认落在生产文件之外,而不是默认作用到每一个 host.ts。
522
+ */
523
+ const VITEST_ALL_RULES: string[] = [
524
+ "vitest/consistent-each-for",
525
+ "vitest/consistent-test-filename",
526
+ "vitest/consistent-test-it",
527
+ "vitest/consistent-vitest-vi",
528
+ "vitest/expect-expect",
529
+ "vitest/hoisted-apis-on-top",
530
+ "vitest/max-expects",
531
+ "vitest/max-nested-describe",
532
+ "vitest/no-alias-methods",
533
+ "vitest/no-commented-out-tests",
534
+ "vitest/no-conditional-expect",
535
+ "vitest/no-conditional-in-test",
536
+ "vitest/no-conditional-tests",
537
+ "vitest/no-disabled-tests",
538
+ "vitest/no-duplicate-hooks",
539
+ "vitest/no-focused-tests",
540
+ "vitest/no-hooks",
541
+ "vitest/no-identical-title",
542
+ "vitest/no-import-node-test",
543
+ "vitest/no-importing-vitest-globals",
544
+ "vitest/no-interpolation-in-snapshots",
545
+ "vitest/no-large-snapshots",
546
+ "vitest/no-mocks-import",
547
+ "vitest/no-restricted-matchers",
548
+ "vitest/no-restricted-vi-methods",
549
+ "vitest/no-standalone-expect",
550
+ "vitest/no-test-prefixes",
551
+ "vitest/no-test-return-statement",
552
+ "vitest/no-unneeded-async-expect-function",
553
+ "vitest/padding-around-after-all-blocks",
554
+ "vitest/padding-around-test-blocks",
555
+ "vitest/prefer-called-exactly-once-with",
556
+ "vitest/prefer-called-once",
557
+ "vitest/prefer-called-times",
558
+ "vitest/prefer-called-with",
559
+ "vitest/prefer-comparison-matcher",
560
+ "vitest/prefer-describe-function-title",
561
+ "vitest/prefer-each",
562
+ "vitest/prefer-equality-matcher",
563
+ "vitest/prefer-expect-assertions",
564
+ "vitest/prefer-expect-resolves",
565
+ "vitest/prefer-expect-type-of",
566
+ "vitest/prefer-hooks-in-order",
567
+ "vitest/prefer-hooks-on-top",
568
+ "vitest/prefer-import-in-mock",
569
+ "vitest/prefer-importing-vitest-globals",
570
+ "vitest/prefer-lowercase-title",
571
+ "vitest/prefer-mock-promise-shorthand",
572
+ "vitest/prefer-mock-return-shorthand",
573
+ "vitest/prefer-snapshot-hint",
574
+ "vitest/prefer-spy-on",
575
+ "vitest/prefer-strict-boolean-matchers",
576
+ "vitest/prefer-strict-equal",
577
+ "vitest/prefer-to-be",
578
+ "vitest/prefer-to-be-falsy",
579
+ "vitest/prefer-to-be-object",
580
+ "vitest/prefer-to-be-truthy",
581
+ "vitest/prefer-to-contain",
582
+ "vitest/prefer-to-have-been-called-times",
583
+ "vitest/prefer-to-have-length",
584
+ "vitest/prefer-todo",
585
+ "vitest/require-awaited-expect-poll",
586
+ "vitest/require-hook",
587
+ "vitest/require-local-test-context-for-concurrent-snapshots",
588
+ "vitest/require-mock-type-parameters",
589
+ "vitest/require-test-timeout",
590
+ "vitest/require-to-throw-message",
591
+ "vitest/require-top-level-describe",
592
+ "vitest/valid-describe-callback",
593
+ "vitest/valid-expect",
594
+ "vitest/valid-expect-in-promise",
595
+ "vitest/valid-title",
596
+ "vitest/warn-todo",
597
+ ];
598
+
599
+ /** 整族在用例文件上开回来时仍然豁免的那几条(理由逐条在 OFF_JUSTIFICATIONS)。 */
600
+ const VITEST_STILL_OFF_IN_TEST_FILES = new Set<string>([
601
+ "vitest/max-expects",
602
+ "vitest/no-conditional-in-test",
603
+ "vitest/no-hooks",
604
+ "vitest/no-importing-vitest-globals",
605
+ "vitest/prefer-called-times",
606
+ "vitest/prefer-describe-function-title",
607
+ "vitest/prefer-expect-assertions",
608
+ "vitest/prefer-import-in-mock",
609
+ "vitest/prefer-importing-vitest-globals",
610
+ "vitest/prefer-to-be-falsy",
611
+ "vitest/prefer-to-be-truthy",
612
+ "vitest/require-test-timeout",
613
+ ]);
614
+
615
+ export const BASE_RULES: RuleTable = {
616
+ "eslint/one-var": ["error", "never"],
617
+ "eslint/func-style": [
618
+ "error",
619
+ "declaration",
620
+ { allowArrowFunctions: true, overrides: { namedExports: "ignore" } },
621
+ ],
622
+ "eslint/id-length": ["error", { exceptions: ["a", "i", "t", "x", "y", "z"] }],
623
+ "eslint/init-declarations": ["error", "never", { ignoreForLoopInit: true }],
624
+ "eslint/max-lines": ["error", { max: 2000, skipBlankLines: true, skipComments: true }],
625
+ "eslint/max-lines-per-function": [
626
+ "error",
627
+ { max: 120, skipBlankLines: true, skipComments: true },
628
+ ],
629
+ "eslint/complexity": "error",
630
+ "eslint/max-params": ["error", { max: 5 }],
631
+ "eslint/max-statements": ["error", 50],
632
+ "eslint/no-console": ["error", { allow: ["warn", "error", "info"] }],
633
+ "eslint/no-param-reassign": "error",
634
+ "eslint/no-underscore-dangle": [
635
+ "error",
636
+ {
637
+ allow: [
638
+ "__memoryInsightCardApplied",
639
+ "__memoryTdaiCardApplied",
640
+ "__sessionRescueApplied",
641
+ "__wukilDevToolsCardApplied",
642
+ ],
643
+ },
644
+ ],
645
+ // 口味类整族关掉(no-magic-numbers / sort-keys / capitalized-comments / no-ternary …),
646
+ // 每条的类别与实测条数在 OFF_JUSTIFICATIONS 里;配置加载会要求它们在册。
647
+ "eslint/no-magic-numbers": "off",
648
+ "eslint/sort-keys": "off",
649
+ "eslint/sort-imports": "off",
650
+ "eslint/capitalized-comments": "off",
651
+ "eslint/no-ternary": "off",
652
+ "eslint/no-undefined": "off",
653
+ "eslint/no-void": "off",
654
+ "eslint/no-duplicate-imports": "off",
655
+ // require-await 一族:`eslint/require-await` 与 `typescript/require-await` 是同一条判据的两份实现,
656
+ // 同开会把一个缺陷报两次(实测同一段无 await 的 async 函数在 --type-aware 下报 2 errors / 1 defect)。
657
+ // 按 typescript-eslint 对 extension rule 的官方口径留扩展那条、关基线那条;全仓实测两面的判据
658
+ // 面在非用例代码上重合(逐行去重:514 行同点、typescript 独有 0 行、eslint 独有只有本配置
659
+ // 自己的一份中间态),而 `options.typeAware: true` 是常开的 ⇒ .mjs/.cjs 也归扩展那条管
660
+ //(实测 --type-aware 下它对 .mjs 同样报出)。用例文件仍整族豁免(见 TEST_OVERRIDES)。
661
+ "eslint/require-await": "off",
662
+ // import 插件按契约类开(no-cycle / named / first 走 categories),下面这几条与官方形态
663
+ // 冲突或属口味,理由与条数在册。
664
+ "import/no-named-export": "off",
665
+ "import/group-exports": "off",
666
+ "import/exports-last": "off",
667
+ "import/prefer-default-export": "off",
668
+ "import/no-nodejs-modules": "off",
669
+ "import/no-relative-parent-imports": "off",
670
+ "import/max-dependencies": "off",
671
+ // 官方 cordis 入口形态就是 default export:`vendor/cordis/src/registry.ts:92` 读
672
+ // name/Config/apply,`vendor/loader/src/index.ts:201` 先 unwrap `default`。
673
+ // 禁止默认导出等于禁止 12 个 bundle 的入口写法,属契约冲突而非噪声。
674
+ "import/no-default-export": "off",
675
+ // 实测 12 条 = 12 个 bundle 入口(独立 -c 探针逐包复测,含测试文件同为 12):
676
+ // 官方入口形态就是 `export default { name, Config, apply }`
677
+ // (vendor/cordis/src/registry.ts:92 直接读这三个字段),要求先具名再导出等于改官方形态。
678
+ "import/no-anonymous-default-export": "off",
679
+ // 具名导出的默认对象上做成员访问是本仓对 cordis 官方声明的常态用法。
680
+ "import/no-named-as-default-member": "off",
681
+ // 顶层 `import type { X }` 267 处 vs 行内 156 处:混用本身就是「配置格式不统一」的一个实测轴,
682
+ // 方向取多数形态。
683
+ "import/consistent-type-specifier-style": ["error", "prefer-top-level"],
684
+ "import/no-duplicates": "error",
685
+ "import/no-cycle": "error",
686
+ "import/named": "error",
687
+ "import/first": "error",
688
+ "import/newline-after-import": "error",
689
+ "node/no-process-env": "off",
690
+ "node/no-sync": ["error", { ignores: READONLY_SYNC_ALLOWLIST }],
691
+ "node/no-top-level-await": "error",
692
+ "oxc/no-async-await": "off",
693
+ "oxc/no-optional-chaining": "off",
694
+ "oxc/no-rest-spread-properties": "off",
695
+ "promise/always-return": "error",
696
+ "promise/no-promise-in-callback": ["error", { exemptDeclarations: true }],
697
+ "promise/no-return-wrap": "error",
698
+ "promise/param-names": "error",
699
+ // `.then` 链与仓库选定的 async/await 侧冲突:开 error,让 13 条存量按代码改成 await。
700
+ "promise/prefer-await-to-then": "error",
701
+ "typescript/consistent-type-imports": ["error", { disallowTypeAnnotations: false }],
702
+ "typescript/explicit-function-return-type": [
703
+ "error",
704
+ {
705
+ allowDirectConstAssertionInArrowFunctions: true,
706
+ allowExpressions: true,
707
+ allowHigherOrderFunctions: true,
708
+ allowIIFEs: true,
709
+ allowTypedFunctionExpressions: true,
710
+ allowedNames: ["handler", "callback"],
711
+ },
712
+ ],
713
+ "typescript/no-explicit-any": "error",
714
+ "typescript/no-floating-promises": "error",
715
+ "typescript/no-unsafe-assignment": "error",
716
+ "typescript/no-unsafe-type-assertion": "error",
717
+ "typescript/no-non-null-assertion": "error",
718
+ "typescript/require-await": "error",
719
+ "typescript/prefer-readonly-parameter-types": "off",
720
+ "typescript/promise-function-async": ["error", { checkArrowFunctions: false }],
721
+ "typescript/consistent-return": ["error", { treatUndefinedAsUnspecified: true }],
722
+ "typescript/only-throw-error": "error",
723
+ "unicorn/no-array-reduce": ["error", { allowSimpleOperations: true }],
724
+ "unicorn/no-null": "off",
725
+ "unicorn/no-useless-undefined": ["error", { checkArguments: false }],
726
+ // ===== react 插件的 React Compiler 实验面(上游默认关闭):与 client 卡片的「宿主挂载
727
+ // 适配器」形态整体冲突(effect 同步宿主态到 store、渲染期读 ref、内联闭包传 hook 是
728
+ // 适配器形态而非交互组件渲染形态)。单项收敛(useSyncExternalStore 化、闭包提升)留给
729
+ // 源码收敛批次;measured 为开面实测条数,依据逐条在 OFF_JUSTIFICATIONS。
730
+ // react-hooks/exhaustive-deps 与 rules-of-hooks 不在豁免面内:前者 src 侧 4 条已逐站点
731
+ // 结构性修复(稳定化回调 + 补依赖),后者只对用例文件在 TEST_FILE_OVERRIDES 豁免。
732
+ "react/set-state-in-effect": "off",
733
+ "react/no-deriving-state-in-effects": "off",
734
+ "react/exhaustive-effect-dependencies": "off",
735
+ "react/hooks": "off",
736
+ "react/refs": "off",
737
+ "react/todo": "off",
738
+ // nursery 的 11 条类型感知规则整体吃下。`no-unnecessary-condition` 逐站点判:
739
+ // 类型确实非空就删冗余的 `?.` / `??`;运行时真会为空则是类型不准,改宽类型让守卫保留且规则不再报。
740
+ // 用关规则代替这个判断是本仓明令禁止的路径。
741
+ ...Object.fromEntries(VITEST_ALL_RULES.map((rule) => [rule, "off"])),
742
+ };
743
+
744
+ const TEST_FILES: string[] = ["**/*.test.ts", "**/*.test.tsx", "**/test/**", "**/__tests__/**"];
745
+
746
+ /**
747
+ * 用例文件(含 `test/*.ts` 夹具与 helper)共享的豁免:规模类,以及「桩件要跨官方名义类型」
748
+ * 的类型类判据。vitest 的用例结构类判据不在这里 —— 它们在基线整族 off、只在
749
+ * TEST_FILE_OVERRIDES 里开回来,所以 helper 模块天然不在射程内,旧配置那份按目录豁免的
750
+ * TEST_HELPER_OVERRIDES 因此作废。
751
+ */
752
+ export const TEST_OVERRIDES: OxlintOverride = {
753
+ files: TEST_FILES,
754
+ rules: {
755
+ "typescript/no-non-null-assertion": "off",
756
+ "typescript/no-unsafe-type-assertion": "off",
757
+ "typescript/no-explicit-any": "off",
758
+ "typescript/require-await": "off",
759
+ "eslint/max-statements": "off",
760
+ "eslint/max-lines": "off",
761
+ "eslint/max-lines-per-function": "off",
762
+ "node/no-top-level-await": "off",
763
+ },
764
+ };
765
+
766
+ /**
767
+ * vitest 的用例结构类判据**只**在这里生效。
768
+ * `expect-expect` 的 `assertFunctionNames` 取 vitest flavor 的官方默认
769
+ * (`["expect", "expectTypeOf", "assert", "assertType"]`):本仓用例里有相当一部分用
770
+ * `node:assert/strict` 断言,那不是「缺断言」,而是这条规则在 jest flavor 默认值下漏判
771
+ * ——实测 17 条里 5 条属于这一族,补上默认值后就不报了。
772
+ *
773
+ * 剩下两族用规则自带的选项解决,不关规则、也不改判据方向:
774
+ * - 断言写在包内 helper 里(`assertSameLedger()` 一类):`assertFunctionNames` 就是
775
+ * "哪些函数算断言"的名单,走 {@link makeTestFileOverrides} 的 `assertionHelpers` 报名字。
776
+ * 名单只收**裸标识符**:带 `*` / `.` 的通配会把"任何调用都算断言"写进配置,等于掏空规则,直接抛。
777
+ * - 标题以大写术语开头(`PTC`、`W5`、`B1` 这类档位/模式名):`allowedPrefixes` 是
778
+ * prefer-lowercase-title 给的同一条口径(官方例子就是 HTTP 方法前缀)。走 `titlePrefixes` 报名字。
779
+ * 反过来的做法——把标题改成 `pTC` / `w5`——会把领域名字改成坏名字,本仓不采取。
780
+ */
781
+ export const VITEST_ASSERTION_DEFAULTS: string[] = [
782
+ "expect",
783
+ "expectTypeOf",
784
+ "assert",
785
+ "assertType",
786
+ ];
787
+
788
+ const BARE_IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/u;
789
+
790
+ export const TEST_FILE_OVERRIDES: OxlintOverride = {
791
+ files: ["**/*.test.ts", "**/*.test.tsx", "**/__tests__/**/*.spec.ts"],
792
+ rules: {
793
+ ...Object.fromEntries(
794
+ VITEST_ALL_RULES.filter((rule) => !VITEST_STILL_OFF_IN_TEST_FILES.has(rule)).map((rule) => [
795
+ rule,
796
+ "error",
797
+ ]),
798
+ ),
799
+ "vitest/expect-expect": ["error", { assertFunctionNames: VITEST_ASSERTION_DEFAULTS }],
800
+ // 测试架在组件树外直接驱动卡片适配 hook(dir-prep 的 useCardOf、memory-insight 的
801
+ // useSyncExternalStore 桩)是刻意的测试形态:hook 调用次数/顺序由桩件保证固定
802
+ // (见 memory-insight test/client-lifecycle.test.ts 的同构注释)。rules-of-hooks 的
803
+ // 「必须在组件或自定义 hook 里」对这批直接驱动不成立,只在用例文件上豁免。
804
+ "react/rules-of-hooks": "off",
805
+ },
806
+ };
807
+
808
+ /**
809
+ * 生成用例文件的 override(vitest 结构类判据的开与关都在这张表里)。
810
+ * @param assertionHelpers - 本包"内部会断言"的 helper 函数名,并入 `expect-expect` 的名单。
811
+ * @returns 可直接放进 `overrides` 的配置项。
812
+ */
813
+ export interface TestFileOverridesOptions {
814
+ /** 本包"内部会断言"的 helper 函数名,并入 `vitest/expect-expect` 的名单。 */
815
+ assertionHelpers?: string[];
816
+ /** 允许作为用例/套件标题开头的大写术语,并入 `vitest/prefer-lowercase-title` 的 allowedPrefixes。 */
817
+ titlePrefixes?: string[];
818
+ }
819
+
820
+ /**
821
+ * 生成用例文件的 override(vitest 结构类判据的开与关都在这张表里)。
822
+ * @param opts - 本包需要用规则自带选项登记的名单(见文件头的两族说明)。
823
+ * @returns 可直接放进 `overrides` 的配置项。
824
+ */
825
+ export function makeTestFileOverrides(opts: TestFileOverridesOptions = {}): OxlintOverride {
826
+ const assertionHelpers = opts.assertionHelpers ?? [];
827
+ const titlePrefixes = opts.titlePrefixes ?? [];
828
+ const illegal = [...assertionHelpers, ...titlePrefixes].filter(
829
+ (name) => !BARE_IDENTIFIER_RE.test(name),
830
+ );
831
+ if (illegal.length > 0) {
832
+ throw new Error(
833
+ `assertionHelpers / titlePrefixes 只收裸标识符,不接受通配或带点形式(前者等于把 vitest/expect-expect 掏空):${illegal.join(", ")}`,
834
+ );
835
+ }
836
+ const names = [...new Set([...VITEST_ASSERTION_DEFAULTS, ...assertionHelpers])];
837
+ return {
838
+ ...TEST_FILE_OVERRIDES,
839
+ rules: {
840
+ ...TEST_FILE_OVERRIDES.rules,
841
+ "vitest/expect-expect": ["error", { assertFunctionNames: names }],
842
+ ...(titlePrefixes.length === 0
843
+ ? {}
844
+ : {
845
+ "vitest/prefer-lowercase-title": [
846
+ "error",
847
+ { allowedPrefixes: [...new Set(titlePrefixes)] },
848
+ ],
849
+ }),
850
+ },
851
+ };
852
+ }
853
+
854
+ export const SCRIPTS_OVERRIDES: OxlintOverride = {
855
+ files: ["scripts/**", "*.mjs", "*.mts"],
856
+ rules: {
857
+ // 这三条在 TS 侧全部是 0 条 ⇒ 包代码本身已经按它们写,只对 node 直跑的门禁/构建脚本豁免:
858
+ // 那批脚本的控制流形态就是 guard-continue(908 行的门脚本改成嵌套 if 只会更难读),
859
+ // 而 unicorn/max-nested-calls 与 eslint/id-length 在 CLI 脚本里指向上限口味,不指向缺陷。
860
+ // 数的是实测条数:no-continue 53、max-nested-calls 59、id-length 32(scripts 面)。
861
+ "eslint/no-continue": "off",
862
+ "unicorn/max-nested-calls": "off",
863
+ // 与基线同一条规则,只是把 node 脚本里的惯例短名加进白名单(不放宽 min/max,避免变成"另一套标准")。
864
+ "eslint/id-length": [
865
+ "error",
866
+ {
867
+ exceptions: ["a", "i", "t", "x", "y", "z", "n", "p", "s", "v", "e", "f", "ok", "fs", "id"],
868
+ },
869
+ ],
870
+ "unicorn/no-process-exit": "off",
871
+ "unicorn/no-useless-undefined": "off",
872
+ "typescript/no-non-null-assertion": "off",
873
+ "node/no-sync": "off",
874
+ "node/no-top-level-await": "off",
875
+ // 这一族是**类型信息**规则:它们判的是「值在类型面上是 any/unknown」。
876
+ // `scripts/*.mjs` 是 node 直接跑的门禁脚本,仓内没开 `checkJs`,也没有 TS 类型标注
877
+ // ⇒ 每个值都落进隐式 any,实测 1658 条里 1428 条是这一族(不是 1428 个缺陷,
878
+ // 而是这些规则在这里无从计算)。真正能判的仍全量保留:no-undef、no-unused-vars、
879
+ // import/first、unicorn 的 correctness 类、eslint 的规模与可读性类。
880
+ // 若日后把这些脚本迁成带类型的 .ts,这一族要跟着收回来。
881
+ "typescript/no-unsafe-member-access": "off",
882
+ "typescript/no-unsafe-call": "off",
883
+ "typescript/no-unsafe-assignment": "off",
884
+ "typescript/no-unsafe-return": "off",
885
+ "typescript/no-unsafe-argument": "off",
886
+ "typescript/strict-boolean-expressions": "off",
887
+ "typescript/no-implied-eval": "off",
888
+ "typescript/require-array-sort-compare": "off",
889
+ },
890
+ };
891
+
892
+ /**
893
+ * 造一个"只读依据视图":基线的 OFF_JUSTIFICATIONS 加上本次调用带进来的包级例外。
894
+ * **刻意不往导出的那张表里写**(早先的实现是 `OFF_JUSTIFICATIONS[rule] = justification`):
895
+ * 配置模块在同一个 Node 进程里只求值一次,写过的那张表会让 A 包的例外替 B 包的无依据 off 背书
896
+ * ——正是这道门要拦的情形,而且单测里已经复现过一次串味(上一条用例的例外让下一条负例变成正例)。
897
+ */
898
+ function makeJustificationView(
899
+ offReasons: Record<string, OffJustification>,
900
+ ): (rule: string) => OffJustification | undefined {
901
+ const merged: Record<string, OffJustification | undefined> = {
902
+ ...OFF_JUSTIFICATIONS,
903
+ ...offReasons,
904
+ };
905
+ return (rule: string): OffJustification | undefined => {
906
+ const direct = merged[rule];
907
+ if (direct !== undefined) {
908
+ return direct;
909
+ }
910
+ const slash = rule.indexOf("/");
911
+ return slash === -1 ? undefined : merged[`${rule.slice(0, slash)}/*`];
912
+ };
913
+ }
914
+
915
+ /**
916
+ * 每一条 `"off"` 都必须在 OFF_JUSTIFICATIONS 里有依据。抛错而不是「记一行日志」:
917
+ * 配置加载失败会让 oxlint 当场红,把规则关掉这件事不可能静默穿过这道门
918
+ * (实测过的同类失效形态:一条不存在的规则名让整份配置解析失败,13 个包全部静默 lint 0 条却 exit 0)。
919
+ */
920
+ function assertOffsDocumented(
921
+ where: string,
922
+ rules: RuleTable | undefined,
923
+ lookup: (rule: string) => OffJustification | undefined,
924
+ ): void {
925
+ if (rules === undefined) {
926
+ return;
927
+ }
928
+ const undocumented: string[] = [];
929
+ for (const [rule, setting] of Object.entries(rules)) {
930
+ const severity = Array.isArray(setting) ? setting[0] : setting;
931
+ if (severity === "off") {
932
+ const found = lookup(rule);
933
+ if (found === undefined || found.note.trim().length === 0) {
934
+ undocumented.push(rule);
935
+ }
936
+ }
937
+ }
938
+ if (undocumented.length > 0) {
939
+ throw new Error(
940
+ `${where} 里这些规则被关成 off 但没有依据(OFF_JUSTIFICATIONS 查不到,或 note 为空):${undocumented.join(", ")}。要么在代码里修,要么给它补一条 kind + measured + note。`,
941
+ );
942
+ }
943
+ }
944
+
945
+ export interface PluginLintOptions {
946
+ syncWrites?: string[];
947
+ extraRules?: RuleTable;
948
+ extraOverrides?: OxlintOverride[];
949
+ extraIgnorePatterns?: string[];
950
+ /** 追加 oxlint JS plugin(alpha、不受 semver 保护);包内自定义规则走这里。 */
951
+ jsPlugins?: NonNullable<OxlintConfig["jsPlugins"]>;
952
+ /** 包级豁免:每个 off 都要在这里写明 kind / measured / note,否则配置加载即抛错。 */
953
+ offReasons?: Record<string, OffJustification>;
954
+ /**
955
+ * 本包"内部会断言"的用例 helper 名(如 `assertSameLedger`)。报进来的名字并入
956
+ * `vitest/expect-expect` 的 `assertFunctionNames`,规则本身保持 error —— 这是规则给的口径,
957
+ * 不是豁免。只收裸标识符,通配会掏空这条规则,直接抛。
958
+ */
959
+ assertionHelpers?: string[];
960
+ /** 允许开头的用例标题大写术语(`PTC`/`W5`/`B1` 这类档位或模式名),进 allowedPrefixes。 */
961
+ titlePrefixes?: string[];
962
+ }
963
+
964
+ export function definePluginConfig(options: PluginLintOptions = {}): OxlintConfig {
965
+ const {
966
+ syncWrites = [],
967
+ extraRules = {},
968
+ extraOverrides = [],
969
+ extraIgnorePatterns = [],
970
+ jsPlugins,
971
+ offReasons = {},
972
+ assertionHelpers = [],
973
+ titlePrefixes = [],
974
+ } = options;
975
+
976
+ const lookup = makeJustificationView(offReasons);
977
+
978
+ const rules: RuleTable = {
979
+ ...BASE_RULES,
980
+ ...SONARJS_RULES,
981
+ ...(syncWrites.length > 0
982
+ ? {
983
+ "node/no-sync": [
984
+ "error",
985
+ {
986
+ ignores: [...new Set([...READONLY_SYNC_ALLOWLIST, ...syncWrites])],
987
+ },
988
+ ],
989
+ }
990
+ : {}),
991
+ ...extraRules,
992
+ };
993
+
994
+ const overrides = [
995
+ TEST_OVERRIDES,
996
+ makeTestFileOverrides({ assertionHelpers, titlePrefixes }),
997
+ SCRIPTS_OVERRIDES,
998
+ ...extraOverrides,
999
+ ];
1000
+
1001
+ assertOffsDocumented("rules", rules, lookup);
1002
+ for (const [index, override] of overrides.entries()) {
1003
+ assertOffsDocumented(`overrides[${index}]`, override.rules, lookup);
1004
+ }
1005
+
1006
+ return defineConfig({
1007
+ plugins: BASE_PLUGINS,
1008
+ // 刻意不开 `env.vitest`:13 份 vitest 配置都没启用 `globals: true`,测试里裸用
1009
+ // describe/it/expect 属于未声明标识符,应由 no-undef 与 TS2593 报出来(实测确实报)。
1010
+ env: { node: true, es2024: true, browser: true },
1011
+ options: BASE_OPTIONS,
1012
+ categories: BASE_CATEGORIES,
1013
+ ignorePatterns: [...BASE_IGNORE_PATTERNS, ...extraIgnorePatterns],
1014
+ // sonarjs 这条 JS plugin 是常挂的(`SONARJS_RULES` 在 rules 里,缺入口就是整份配置加载失败,
1015
+ // 实测报 `Plugin 'sonarjs' not found`);包内自带的入口追加在后面。
1016
+ jsPlugins: [SONARJS_ENTRY, ...(jsPlugins ?? [])],
1017
+ rules,
1018
+ overrides,
1019
+ });
1020
+ }