@aureienbn/克林特-插座-核心
18克林特规则适用于任何TypeScript代码库:吞入错误,片状或空心测试,生产中的测试代码,匿名公共合同,工作空间成员互相泄露,口语收缩.
pnpm add -D @aurelienbbn/oxlint-plugin-core oxlint # oxlint >=1.82.0 <2.0.0
{
"jsPlugins": ["@aurelienbbn/oxlint-plugin-core"],
"rules": {
"core/no-discarded-caught-error": "error",
"core/no-weak-test-assertions": ["error", { "assertionHelpers": ["assertValid"] }],
"core/no-test-sleeps": "error"
}
}
无预设,一种自定义. 启用每个规则的名称; 每一个修补, 但补丁规则的空白行需要一个工具无法做出的决定 .
按工作、选项和决定分列的规则
| 任务 | 规则 |
|---|---|
| 错误 | no-discarded-caught-error, (中文(简体) ), no-error-message-matching |
| 测试 | no-weak-test-assertions, (中文(简体) ), no-test-sleeps, (中文(简体) ), no-stubbed-subject, (中文(简体) ), no-ambient-nondeterminism-in-tests, (中文(简体) ), no-vitest-mocking, (中文(简体) ), no-vitest-in-source, (中文(简体) ), no-test-logic-in-production |
| 合同 | no-exported-anonymous-object-return, (中文(简体) ), no-multi-positional-parameters, (中文(简体) ), no-mutable-exported-state, (中文(简体) ), no-reexport-only-modules, (中文(简体) ), no-let |
| 工作空间 | workspace-boundaries |
| qq 评论 | no-dead-comments |
| qq 版式 | padding-line-between-statements (自动修复), padding-before-exit (自动固定,已贬值) |
| 规则 | 选项 | 默认 |
|---|---|---|
no-discarded-caught-error | logOnlyCallees | ["console", "logger", "log"] |
reasonMarker | "REASON" | |
includeTestFiles | false (测试文件豁免) | |
no-ambient-nondeterminism-in-tests | allowClock | false· ; true 只放弃时钟读取 |
no-weak-test-assertions | assertionHelpers | [] |
no-vitest-mocking | forbidSpies | false (单位:千美元)vi.fn, (中文(简体) ), vi.spyOn 允许) |
no-reexport-only-modules | allow | ["index.ts"] |
no-multi-positional-parameters | exemptFunctionNames, (中文(简体) ), exemptFileBasenames | [], (中文(简体) ), [] |
padding-line-between-statements | 爱斯林特的 { blankLine, prev, next } 列表 | 下方显示的配置 |
workspace-boundaries | apps (可部署的成员目录) | ["apps/**"] |
forbidden (单位:千美元){ from, to, message? } 成员目录对数) | [] |
| 规则 | 决定它的固定需要 |
|---|---|
no-ambient-nondeterminism-in-tests | 场景的地盘,冰冻瞬间,或种子 |
no-dead-comments | 赤裸的 TODO: 跟踪或删除 |
no-discarded-caught-error | 重投、 错误上的分行或记录一个原因 |
no-error-message-matching | 稳定的区分符( 类、 标记、 代码) 取决于错误源 |
no-exported-anonymous-object-return | 计划/ 类型名称及其导出地点 |
no-let | 调整数值构造, 不交换关键字 |
no-multi-positional-parameters | 更新呼叫者, 选择属性名称 |
no-mutable-exported-state | 工厂、不可改变的合同或附属边界 |
no-reexport-only-modules | 将每个消费者都改道到自有模块 |
no-stubbed-subject | 由合作者替换 |
no-test-logic-in-production | 注入依赖性或公共合同 |
no-test-sleeps | 投票条件或假计时表 |
no-vitest-in-source | 合适的测试工具或依赖性接合 |
no-vitest-mocking | 依赖边界和假边界 |
no-weak-test-assertions | 每项试验所称的可观测结果 |
workspace-boundaries | 哪个成员拥有共享代码,哪个子路径输出 |
QQ 处理或解释发现的错误
try {
await sync();
} catch (error) {
console.error(error);
return [];
} // ❌ only logged
promise.catch(() => null); // ❌ discarded
promise.catch(handleFailure); // ✅ handed to a function
try {
access(path);
return true;
} catch {
// REASON: a missing file is the negative answer here
return false; // ✅ reasoned
}
return err.message === "Not found"; // ❌ no-error-message-matching
return error instanceof TimeoutError; // ✅ class, tag, or code
no-discarded-caught-error 财务报告和审定财务报表 no-error-message-matching 细节
no-discarded-caught-error 覆盖 catch 条款,内在 .catch(fn)的第二个参数 .then,当处理器从未丢出(或返回) Promise.reject(...)装订不存在或只达到 console.* 编号 : logger.* 编号 : log.*。 。 。 .
try {
return await load(id);
} catch (error) {
throw new LoadError({ id, cause: error });
} // ✅ rethrown
try {
return await load();
} catch (error) {
if (isNotFound(error)) return undefined;
throw error;
} // ✅ inspected
| 大小写 | 行为 |
|---|---|
| 计为处理 | 将错误传递到任何非日志函数, 返回, 测试 |
| 逃生舱门 | REASON: <≥ 3 words> 处理器内部; 承诺回调也直接位于语句上方. 占位符( E)todo, (中文(简体) ), ignored, (中文(简体) ), this is fine不要数 |
catch {} (字面为空) | 左转 eslint/no-empty |
Effect.catch(...) | qq 资本化接收器是命名空间, 跳过 |
有实例的建构者 .catch(fn) | qq 需要一个理由或禁用注释 |
no-error-message-matching数字 : .message 与字符串字词相对应,以 includes 编号 : startsWith 编号 : endsWith 编号 : match 编号 : search 编号 : indexOf 编号 : RegExp#test (亦于 toLowerCase()- 式调用),或用作 switch 歧视者. String(error), (中文(简体) ), error.toString(), (中文(简体) ), `${error}` 也算在装订量上 测试文件豁免 。 错误形状 = 抓取绑定,拒绝处理器参数,或名称类似 e, (中文(简体) ), err, (中文(简体) ), error, (中文(简体) ), cause, (中文(简体) ), reason, (中文(简体) ), ex, (中文(简体) ), exception, (中文(简体) ), *Error, (中文(简体) ), *Err, (中文(简体) ), *Exception。 。 。 .
测试证明行为,决定性
no-weak-test-assertions 法官每个测试:一个强有力的断言可以澄清这一点.
| 弱 强 | 强 |
|---|---|
expect(load()).toBeDefined() | expect(load()).toEqual({ id: 1 }) |
expect(() => run()).not.toThrow() | expect(() => run()).toThrow("boom") |
toHaveBeenCalledWith(expect.any(Object)) | toHaveBeenCalledWith(expect.any(String), 30) |
await new Promise((r) => setTimeout(r, 500)); // ❌ no-test-sleeps
await vi.waitFor(() => expect(state()).toBe("done")); // ✅ poll the condition
// cart.test.ts
vi.spyOn(cart, "total").mockReturnValue(30); // ❌ no-stubbed-subject
vi.spyOn(pricing, "rate").mockReturnValue(2); // ✅ another module
export const send = (mail) => (process.env.NODE_ENV === "test" ? undefined : deliver(mail)); // ❌ no-test-logic-in-production
使用 Vitest 内源测试回合的项目 no-test-logic-in-production 离开那些文件.
每个弱的形状,睡眠的形状,非决定性的来源, 和只测试标记
二. 弱项
| 弱 强 | 强 |
|---|---|
.toBeTruthy() | 准确的数值 |
expect(true).toBe(true) (文对文 | expect(3).toBe(add(1, 2)) (文学与计算) |
expect(result).toEqual(result) (值对自身) | expect(render()).toMatchSnapshot() |
expect(fn()).toBe(5) 之后 fn.mockReturnValue(5) | fc.assert(fc.property(…)) |
toEqual(expect.anything()) | toEqual({ id: expect.any(String), total: 30 }) |
赤线 toHaveBeenCalled() 编号 : toBeCalled() | .not.toHaveBeenCalled() ("从来没有叫"是准确的) |
toBeInstanceOf(Object) | toEqual(expect.any(Order)) |
expect(typeof x).toBe("string") |
通关卡= expect.anything(), (中文(简体) ), expect.any(Object | Function | Array | String | Number | Boolean), (中文(简体) ), expect.objectContaining({}), (中文(简体) ), expect.arrayContaining([])。 。 。 . 裸体 toThrow() 不计算 : vitest/require-to-throw-message 拥有它, 被绑在测试文件上 @aurelienbbn/oxlint-config。 。 。 .
睡觉 (单位:千美元)*.test.* 编号 : *.spec.*(中文(简体) )
await new Promise((r) => setTimeout(r, 500)); // ❌ timer-only executor
import { setTimeout as sleep } from "node:timers/promises"; // ❌ also `scheduler`
await sleep(250); // ❌ sleep / delay / wait / pause + numeric literal
调用文件 vi.useFakeTimers() 跳过第一个和最后一个形状: 它们的计时器不是壁点.
环境非决定因素 (概念:"在"试验"中消除非决定性","马丁·福勒"
| * 开火 | 除非我们.. |
|---|---|
toLocaleString 编号 : toLocaleDateString 编号 : toLocaleTimeString 没有地方 | —— 说 |
localeCompare 没有第二个参数 | —— 说 |
Intl.*Format 构造时没有区位 | —— 说 |
Math.random() | —— 说 |
new Date() (没有参数), Date.now(), (中文(简体) ), performance.now() | 文件调用 vi.useFakeTimers 编号 : vi.setSystemTime,或 allowClock: true |
阴影 Date 编号 : Math 编号 : Intl 保持安静.
僵尸主题 (单位:千美元)<stem>.test.* 编号 : <stem>.spec.*(中文(简体) )
import * as cart from "./cart"; // also ./cart.js, ../cart/index
vi.spyOn(cart, "total"); // ✅ pass-through spy
命名空间或默认导入同位素相对模块;罐装行为通过 mockReturnValue, (中文(简体) ), mockResolvedValue, (中文(简体) ), mockRejectedValue, (中文(简体) ), mockImplementation (和 …Once),被锁链或被绑在间谍身上 const. 包的进口保持沉默.
生产中的测试逻辑 (概念:生产中的试验逻辑),G. Meszaros, x 单位测试模式) (中文(简体) ). 在测试文件中沉默, Vitest 配置和设置文件, /test-utils/, (中文(简体) ), /testing/ (路径) no-vitest-in-source 允许).
export const send = (mail) => (process.env.NODE_ENV === "test" ? undefined : deliver(mail)); // ❌ also import.meta.env.MODE === "test"
export const retries = process.env.VITEST ? 0 : 3; // ❌ also VITEST_WORKER_ID, JEST_WORKER_ID, import.meta.vitest
export const __testing__ = { parse }; // ❌ name marks it test-only
/** Visible only for unit tests. */
export const parse = (input) => input; // ❌ comment marks it test-only
只测试名称 : __test__ 编号 : __tests__ 编号 : __testing__, (中文(简体) ), _internal(s), (中文(简体) ), internals, (中文(简体) ), testOnly*, (中文(简体) ), *ForTest 编号 : *ForTests 编号 : *ForTesting. 直接在出口上方的“出口用于测试”或“仅用于测试”等评论.
虚拟 API : no-vitest-in-source 导入报表 vitest 非试验来源; no-vitest-mocking 通过 Vitest 模拟API 替换报告模块 vi.fn 编号 : vi.spyOn 与 forbidSpies: true。 。 。 .
公共合同的名称
export const getUser = () => ({ id: user.id }); // ❌ no-exported-anonymous-object-return
export const load: Load = () => ({ id: user.id }); // ✅ named contract
const loadUser = (userId: UserId, includePosts: boolean) => userId; // ❌ no-multi-positional-parameters
export function loadUser(input: { userId: UserId; includePosts: boolean }) {} // ✅ object input
export let currentUserId = undefined; // ❌ no-mutable-exported-state
export * from "./user.js"; // (whole file) // ❌ no-reexport-only-modules, unless listed in `allow`
let total = 0; // ❌ no-let (also `var`)
工作空间成员仅通过出口开会
workspace-boundaries 读取最近的成员 pnpm-workspace.yaml (或 package.json workspaces评议 import, (中文(简体) ), export … from,以及 import() 两名委员之间:
// packages/kernel/src/tick.ts
import { boot } from "@game/client"; // ❌ packages never import apps (members under `apps`)
import { arrow } from "../../ranger/src/arrow.ts"; // ❌ relative path into another member
import { body } from "@game/physics/src/body.ts"; // ❌ not opened by @game/physics `exports`
import { body } from "@game/physics"; // ✅ an exported subpath
{
"core/workspace-boundaries": [
"error",
{
"apps": ["apps/**", "infra/**"],
"forbidden": [
{ "from": "packages/features/*", "to": "packages/features/*", "message": "features meet through the kernel" },
{ "from": "packages/kernel", "to": "packages/adapters/**" }
]
}
]
}
一个没有子路的表象 exports 只打开其根;一个子路径映射到 null 关闭; * 匹配至少一个字符,如节点。 仅按类型进口和再出口被判断为价值进口:一种类型的依赖仍然是依赖. require() 财务报告和审定财务报表 import x = require() 未读。 每个成员以外的文件(root configs,脚本)从不被判断. 目录模式 : * 在一个部分之内, ** 横跨各段。 成员每个 lint 进程读取一次: 添加一个后重新启动编辑器的语言服务器 。 循环与 import/no-cycleKnip的死档案.
QQ 评论描述代码,而非其历史
// Updated to use the new pricing API ❌ change narration (line comments)
// TODO: rename this contract ❌ untracked
// TODO(#123): rename this contract ✅ tracked (also CART-42 or a link)
/** Updated when the cache expires */ ✅ docblock describing behavior
每个 no-dead-comments 形状
| 火灾 | 保持安静 |
|---|---|
// NEW: 编号 : // Updated: 编号 : // Fixed: 标签 | // New customers receive a welcome discount |
// Previously this… 编号 : // Formerly: 编号 : // Was: | // Previously seen cursors are skipped |
// Added … as requested 编号 : … for the review | // TODO CART-42: tighten the types 或链接 |
仅行注释 : // Updated to… 编号 : // Changed from… 编号 : // Now returns… 编号 : // No longer supports… |
还有关闭- brace 标签和占位符脚手架; 叙述只在注释开始时才算数 .
QQ 块、 声明运行和退出站相隔
padding-line-between-statements 选择 ESLint 的选项( 列表 { blankLine, prev, next })及其语句类型. 一个声明上方的评论块属于它: 空白行位于注释上方,对行的评论则以该说明结尾. 自动插入或去除空白行.
function start(runtime) {
const config = load();
// Planning runs need no config.
if (runtime.planning) {
return;
}
runtime.apply(config); // ❌ twice: above the comment, and below the block
}
function start(runtime) {
const config = load();
// Planning runs need no config.
if (runtime.planning) {
return;
}
runtime.apply(config); // ✅
}
在没有选项的情况下启用,它应用了此配置; 任何选项全部替换:
{
"core/padding-line-between-statements": [
"error",
{ "blankLine": "always", "prev": "*", "next": ["block-like", "multiline-expression"] },
{ "blankLine": "always", "prev": ["block-like", "multiline-expression"], "next": "*" },
{ "blankLine": "always", "prev": ["const", "let"], "next": "*" },
{ "blankLine": "any", "prev": ["const", "let"], "next": ["const", "let"] },
{ "blankLine": "always", "prev": "*", "next": ["return", "throw"] }
]
}
| 重叠 | 状态 |
|---|---|
padding-before-exit | qq已贬值:默认配置覆盖它,以及 { "blankLine": "always", "prev": "*", "next": ["return", "throw"] } 独自复制它。 打开它 padding-line-between-statements。 。 。 . |
effect/padding-after-dependencies | 补充:它也分开 yield* a类服务依赖关系 const 在它们下面的逻辑, 声明是免费的。 两条规则都插入了同一条单行空白. |
* 移徙
| 现在报告 | 示例 | 修补( 无自定义) |
|---|---|---|
| 别名导出 | const read = () => ({ id: 1 }); export { read as load }; | 名称返回类型,与直接导出相同 |
| 化名维特 | spec.each(rows)("exists", row => check(row).toBeDefined()) 何时 spec 编号 : check 别名 test 编号 : expect | 确定可观测值,或登记真正的断言助手 |
合同:每个规则都做什么,没有解决
| 专题 | 合同 |
|---|---|
| 建筑政策 | 已命名的导出对象合同和对象样式参数是策略,而不是错误的证明 |
| 公众回报分析 | qq 直接导出, 同文件的导出列表/别名, const 转发别名, 默认识别符, 明显超载签名 qq 嵌入式帮助器返回保持私密 qq 外部再出口和包入点 转录未解决 |
| 调用次数 | 外部强制的回调异常 |
no-let | 可选:故意局部变异可以更清晰更快 |
| 可变出口 | 处理的远期出口没有合并当地申报 |
| 微弱的断言 | 每个测试; 识别真实 expect 链条和有意义的快照/财产断言;任意的匹配者命名方法不是证据 |
| 测试识别 | 已命名的别名、 命名空间导入、 未阴影的全局, each,有条件的修改符。 本地的影子在维特规则之外 快检查所导入的别名的断言支持 |
assertionHelpers | 信任这个本地名称的电话: 帮助者的行为合同是针对你的. 跨模块包接推论不支持 |
登记的合同库存
从软件包导出 pnpm catalog. 规则特定选项和限制在上文和源测试中都有描述.
| 规则/检查 | 触发或审查范围 |
|---|---|
no-ambient-nondeterminism-in-tests | 根据主机区、 真实时钟或 Math.random(): 本地化敏感格式化,没有本地化, new Date() 编号 : Date.now() 编号 : performance.now() 没有假计时器, 和无种子随机. |
no-dead-comments | 否定已死评论:关闭-brace标签,占位符脚手架,没有被跟踪的TODO,并评论会描述一个更改而不是代码. |
no-discarded-caught-error | 需要抓取条款或内置承诺拒绝处理器, 绝不重写和忽略或只记录错误, 以便携带一个合理的标记注释 . |
no-error-message-matching | 通过字符串比较,子字符串或 regex 匹配,或切换,在错误消息的文本上取消分支. |
no-exported-anonymous-object-return | 对直接输出的函数,通过本地化名,或作为默认标识符,要求命名对象返回合同. |
no-let | 拒绝让和 var 声明支持 康斯特和面向表达的代码. |
no-multi-positional-parameters | 需要对象输入而不是函数的多位置参数. |
no-mutable-exported-state | 拒绝导出可变模块状态 . |
no-reexport-only-modules | 除允许的入点桶外,仅再导出其他模块的断开模块. |
no-stubbed-subject | 丢弃 a <stem>.test 文件来自替换其自身的行为 <stem> 模块通过 vi.spyOn(...) 然后是罐头回放值或执行. |
no-test-logic-in-production | 使来自测试跑道或测试模式上的分支和来自被命名或评论为仅用于测试的现有出口成员的生产文件失效. |
no-test-sleeps | 拒绝在测试文件中使用墙钟睡眠:设置Timeout支持的承诺,节点:timers/promise sleep,并等待时间固定的睡眠助手. |
no-vitest-in-source | 拒绝从非测试源文件导入 vitest . |
no-vitest-mocking | Disallow Vitest 嘲笑API 支持确定性测试双人. |
no-weak-test-assertions | 否定个人测试 其唯一的断言 检查存在, 赤裸裸的呼叫, a typeof 或者说 Object 实例中,仅使用通配符的参数,对自身或字面的值,或本地模拟本身的返回值. |
padding-before-exit | 需要返回前的空白行或扔出跟随同一块中另一个语句的语句,切换大小写或程序正文. |
padding-line-between-statements | 需要或不允许语句之间的空行,其中ESLint的选项和一个注释块作为下方语句的一部分被计算;默认为被下方语句块,多行表达式,声明运行和退出. |
workspace-boundaries | 将工作空间成员分开:软件包从不导入应用软件,成员只能通过软件包. json导出和从不通过相对路径互相输入,配置的成员对从不导入. |
信用概念
- 马丁·福勒所著"在测试中消除非决定性"(概念)
- “生产中的试验逻辑”(G. Meszaros,xUnit测试模式)(概念)
- KeepCart/应用工具/工作空间界限(相同的作者;一般带有可配置应用程序和禁止进口矩阵)
- 代码由 Asyrafhussin (MIT,概念被重新执行)
- 由 Asyrafhussin (麻省理工学院,概念被重新执行) 所输入的代码- 关闭- 括号标签和占位符
- https://eslint.org/docs/latest/rules/padding-line-inter-statements 页面存档备份,存于互联网档案馆 (麻省理工学院;概念,独立实施)
- https://eslint.org/docs/latest/rules/padding-line-inter-statements 页面存档备份,存于互联网档案馆 (MIT;选项计划和语句类型,独立执行)