eslint-plugin-unicornprefer-temporal-conversion规则深度解析:从 AVA 快照看 Temporal 转换优化的检测与修复行为
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
本文以 test/snapshots/prefer-temporal-conversion.js.md 这份 AVA 快照报告为主体,结合 docs/rules/prefer-temporal-conversion.md 官方文档与 rules/prefer-temporal-conversion.js 源码实现,全面剖析 eslint-plugin-unicorn 中prefer-temporal-conversion规则的检测模式、自动修复策略、TypeScript 类型感知能力与边界行为。读完本文,你将能准确理解该规则在什么场景下报告、什么场景下直接修复、什么场景下仅给出编辑器建议,以及如何在项目中启用并验证它。
规则定位:为什么需要"直接转换方法"
prefer-temporal-conversion是 eslint-plugin-unicorn 提供的一条代码风格与正确性规则,规则描述为"Prefer direct Temporal conversion methods"(优先使用 Temporal 直接转换方法)。它已被列入recommended与unopinionated两套预设配置(见 readme.md 第 417 行的规则清单),并同时支持--fix自动修复(🔧)与编辑器建议(💡)。
Temporal 转换方法(如ZonedDateTime.prototype.toPlainDate())可以在 Temporal 各类型之间直接转换。与之相对,通过Temporal.PlainDate.from(source)、source.toString()序列化后再次解析、或者手工用source.year、source.month、source.day重建一个对象,都属于间接转换:
- 间接转换引入了大量样板代码;
- 序列化与反序列化路径可能丢失信息,例如亚毫秒精度(submillisecond precision)和日历(calendar)信息。
该规则的目的,就是把这类"可以一步到位"的间接转换统一替换为直接转换方法,同时保留语义等价性。
快照文档是什么:AVA 测试快照的结构解读
test/snapshots/prefer-temporal-conversion.js.md是 AVA 测试框架为test/prefer-temporal-conversion.js生成的快照报告(实际二进制快照保存于同目录下的prefer-temporal-conversion.js.snap)。它逐条记录了每个 invalid 测试用例的:
- 输入代码(Input);
- 报错位置与消息(Error / Message),消息格式为
Prefer \.toPlainDate()` when converting to `Temporal.PlainDate`.`; - 自动修复输出(Output)或编辑器建议(Suggestion)。
这份快照文档共 1525 行,覆盖了三组test.snapshot()测试块(JS 基础用例、类型感知用例、TypeScript 断言用例),是观察规则"检测什么、修复成什么"最直接、最权威的一手证据。
规则覆盖的转换矩阵
根据官方文档与源码中的conversions映射表(rules/prefer-temporal-conversion.js#L44-L65),规则支持的转换关系如下:
| 源类型 | 目标类型 | 推荐方法 |
|---|---|---|
Temporal.ZonedDateTime | Temporal.Instant | .toInstant() |
Temporal.ZonedDateTime | Temporal.PlainDateTime | .toPlainDateTime() |
Temporal.ZonedDateTime或Temporal.PlainDateTime | Temporal.PlainDate | .toPlainDate() |
Temporal.ZonedDateTime或Temporal.PlainDateTime | Temporal.PlainTime | .toPlainTime() |
对应到源码,就是 4 条转换定义:Instant仅接受ZonedDateTime源且不涉及字段重建;PlainDateTime仅接受ZonedDateTime源、字段为日期字段与时间字段的并集;PlainDate与PlainTime均接受两种源,字段分别为['year', 'month', 'day']与['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond']。
快照揭示的六大检测模式
1. 直接单参数转换:Temporal.X.from(source)
快照 invalid(1)、invalid(9)、invalid(14) 等展示了最基础的模式:
const source = Temporal.ZonedDateTime.from("2024-01-02T03:04:05.123456789+00:00[UTC]"); Temporal.Instant.from(source); // ❌ -> source.toInstant() Temporal.PlainDate.from(source); // ❌ -> source.toPlainDate() Temporal.PlainTime.from(source); // ❌ -> source.toPlainTime() Temporal.PlainDateTime.from(source);// ❌ -> source.toPlainDateTime()这类"精确转换"(exact conversions)语义完全等价,因此全部走自动修复(Output 字段给出修复结果)。
2. 序列化往返转换:Temporal.X.from(source.toString() / source.toJSON())
快照 invalid(2)、(3)、(10)、(11) 等覆盖了通过toString()或toJSON()转成字符串再from的往返模式。源码中getConversionMatch对这两种零参方法做了显式识别(rules/prefer-temporal-conversion.js#L131-L135):
Temporal.PlainDate.from(source.toString()); // ❌ -> source.toPlainDate() Temporal.PlainDate.from(source.toJSON()); // ❌ -> source.toPlainDate()注意:当目标是Instant时,序列化往返会降级为编辑器建议而不是自动修复(见下文"修复与建议的边界"),因为ZonedDateTime序列化时会把历史时区偏移取整到分钟,可能丢失秒级精度。
3. Instant 的 epoch 字段重建:fromEpochNanoseconds / fromEpochMilliseconds
快照 invalid(35)-(44)、(47)-(49) 覆盖了从epochNanoseconds或epochMilliseconds重建Instant的写法:
new Temporal.Instant(source.epochNanoseconds); // ❌ -> source.toInstant() Temporal.Instant.fromEpochNanoseconds(source.epochNanoseconds); // ❌ -> source.toInstant() Temporal.Instant.fromEpochMilliseconds(source.epochMilliseconds); // ❌ -> 仅建议 source.toInstant() Temporal.Instant.fromEpochNanoseconds(Temporal.Now.zonedDateTimeISO().epochNanoseconds); // ❌ -> Temporal.Now.zonedDateTimeISO().toInstant()毫秒版本之所以不能自动修复,是因为它丢弃了亚毫秒精度(源码canAutofix: !isMilliseconds,见 rules/prefer-temporal-conversion.js#L145-L150)。快照 invalid(52) 还验证了参数里带/* retain */注释时,规则只报告、不提供任何修复或建议。
4. 构造器参数重建:new Temporal.PlainDate(source.year, source.month, source.day)
快照 invalid(4)、(8)、(12)、(17)、(21) 等展示了从源对象逐字段复制给目标构造器的模式:
new Temporal.PlainDate(source.year, source.month, source.day); // ❌ -> source.toPlainDate() new Temporal.PlainTime(source.hour, source.minute, source.second, source.millisecond, source.microsecond, source.nanosecond); // ❌ -> source.toPlainTime() new Temporal.PlainDateTime(source.year, source.month, source.day, source.hour, ...); // ❌ -> source.toPlainDateTime()时间重建必须包含全部六个时间字段(直到nanosecond),缺少任何一个字段快照(如 valid 用例中的new Temporal.PlainTime(source.hour, source.minute))都不会被报告。源码中getFieldEntries对NewExpression按字段数组下标逐一映射(rules/prefer-temporal-conversion.js#L67-L87)。
5. 属性包(property bag)重建:Temporal.PlainDate.from({year, month, day})
快照 invalid(5)-(7)、(13)、(18)-(20) 等覆盖了对象字面量属性包的场景,这也是模式最丰富的一类:
Temporal.PlainDate.from({year: source.year, month: source.month, day: source.day}); // ❌ -> source.toPlainDate() Temporal.PlainDate.from({year: source.year, monthCode: source.monthCode, day: source.day, calendar: source.calendarId}); // ❌ -> source.toPlainDate() Temporal.PlainTime.from({hour: source.hour, minute: source.minute, ..., nanosecond: source.nanosecond}); // ❌ -> source.toPlainTime()关键规则(源码 rules/prefer-temporal-conversion.js#L89-L125 的getFieldReconstruction):
- 日期属性包中的月份既可以是
month也可以是monthCode(monthCode会按month归一化匹配); - 允许显式携带
calendar: source.calendarId(快照 invalid(6)、(7)); - 属性包不允许出现计算属性名、方法、getter、多余字段或来自不同接收者的字段(快照 valid 用例中
{year: source.year, month: source.month, day: other.day}、{...source}、{["year"]: ...}等均不报告); - 若属性值表达式有副作用(如
(log(), source).year中的序列表达式接收者),也不报告——见快照 invalid(49)、(50) 与源码中hasSideEffect(receiver, ...)检查。
6. 变量别名与复杂表达式接收者
快照 invalid(47)-(50) 验证了规则对"源对象"的识别能力:常量绑定const alias = source; alias.epochNanoseconds、条件表达式(condition ? source : other)、序列表达式(log(), source)都能被追踪并正确替换:
const alias = source; Temporal.Instant.fromEpochNanoseconds(alias.epochNanoseconds); // ❌ -> alias.toInstant() Temporal.Instant.fromEpochNanoseconds((condition ? source : other).epochNanoseconds); // ❌ -> (condition ? source : other).toInstant()自动修复与编辑器建议的边界
官方文档"Fixes and suggestions"一节与快照共同确认了这条核心分界线:语义完全等价的转换自动修复,可能改变结果的转换仅给编辑器建议。
| 模式 | 修复方式 | 原因 |
|---|---|---|
直接单参数from(source) | 自动修复 | 精确转换 |
| 基于纳秒的 Instant 重建 | 自动修复 | 无损 |
| 序列化为 plain 类型(PlainDate/PlainTime/PlainDateTime) | 自动修复 | 无损 |
| 完整时间字段重建 | 自动修复 | 无损 |
| 显式保留源日历的日期属性包 | 自动修复 | 保留日历 |
| 基于毫秒的 Instant 重建 | 仅建议 | 丢弃亚毫秒精度 |
| 序列化为 Instant | 仅建议 | ZonedDateTime序列化将历史时区偏移取整到分钟,可能丢秒 |
| 不含日历的日期属性包 | 仅建议 | 默认回退到 ISO 8601 日历 |
| 日期构造器(带数值参数) | 仅建议 | 数值参数被解释为 ISO 字段,即使提供日历也一样 |
快照中的具体证据:invalid(1) 等自动修复用例输出在Output字段;invalid(4) 等仅建议用例输出在Suggestion 1/1字段,建议文案为"Replace with `.toPlainDate()`."。此外快照 invalid(52)-(55) 证明:只要匹配到的转换代码中包含注释(无论注释在参数、属性还是调用中),规则只报告错误消息,不提供 fix 也不提供 suggestion——这是为了防止修复吞掉开发者注释。
当--fix替换后需要时,源码还会自动补分号或括号(快照 invalid(64) 展示了多行代码场景下在语句开头插入;的修复结果,源码见 rules/prefer-temporal-conversion.js#L198-L210 的fix函数)。
源码实现原理速览
规则的实现入口是 rules/prefer-temporal-conversion.js 中的create函数(#L156-L223),它同时监听CallExpression与NewExpression,核心流程为:
- 识别目标构造器:仅接受
Temporal.X成员表达式(new Temporal.PlainDate(...)、Temporal.PlainDate.from(...)、Temporal.Instant.fromEpochMilliseconds(...)等),且排除可选调用、计算属性名、多余参数与 Spread 参数; - 匹配转换模式:
getConversionMatch依次处理序列化往返(toString/toJSON)、直接对象转换、字段重建(构造器/属性包)、Instant 的 epoch 字段; - 验证源类型:
conversions中通过createTypeCheckers生成类型检查器,静态识别new Temporal.X(...)、Temporal.X.from(...)、Temporal.Now.zonedDateTimeISO()/plainDateTimeISO()以及显式 TypeScript 类型标注(#L44-L65); - 生成问题报告:根据
canAutofix决定挂载fix(自动修复)还是suggest(编辑器建议)。
规则元信息(#L228-L244)声明了type: 'suggestion'、fixable: 'code'、hasSuggestions: true、schema: [](无配置项),并标记recommended: 'unopinionated'。
TypeScript 支持:类型断言、satisfies 与非空断言
快照后半部分(第 3 组用例)专门验证了 TypeScript 下的行为,unwrapTypeScriptExpression会剥离包裹转换输入的各类 TS 表达式(rules/prefer-temporal-conversion.js#L9):
// invalid(60)-(63):接收者上的断言 (source as Temporal.ZonedDateTime).epochNanoseconds // ❌ -> (source as Temporal.ZonedDateTime).toInstant() (source satisfies Temporal.ZonedDateTime).epochNanoseconds // ❌ -> (source satisfies Temporal.ZonedDateTime).toInstant() source!.epochNanoseconds // ❌ -> (source!).toInstant() (<Temporal.ZonedDateTime>source).epochNanoseconds // ❌ -> (<Temporal.ZonedDateTime>source).toInstant() // invalid(1)-(4):属性包整体断言 Temporal.PlainDate.from(({...} as Temporal.PlainDateLike)); // ❌ -> source.toPlainDate() Temporal.PlainDate.from(({...} satisfies Temporal.PlainDateLike)); // ❌ -> source.toPlainDate() Temporal.PlainDate.from((<Temporal.PlainDateLike>{...})); // ❌ -> source.toPlainDate() Temporal.PlainDate.from(({...})!); // ❌ -> source.toPlainDate() // invalid(6)-(8):字段级断言、字符串断言 Temporal.PlainDate.from({year: source.year as number, month: (source.month satisfies number), day: source.day!, calendar: source.calendarId as string}); // ❌ -> source.toPlainDate() Temporal.PlainDate.from(source.toString() as string); // ❌ -> source.toPlainDate() Temporal.PlainDate.from(source.toJSON() satisfies string); // ❌ -> source.toPlainDate()对于注释位于断言内部的情况(invalid(5):as /* keep */ Temporal.PlainDateLike),同样遵守"含注释只报告不修复"的规则。
类型感知模式:利用 TypeScript 类型信息
快照中的typeAware测试块(file.ts+typescriptEslintParser)验证了规则在启用类型信息时的行为。当无法从语法上识别源类型时,规则会借助 TypeScript checker 的getFullyQualifiedName判断符号是否解析为Temporal.ZonedDateTime/Temporal.PlainDateTime(源码 #L54-L62):
declare namespace Temporal { class ZonedDateTime { epochNanoseconds: bigint; year: number; month: number; day: number; calendarId: string; } } declare function getSource(): Temporal.ZonedDateTime; new Temporal.Instant(getSource().epochNanoseconds); // ❌ -> getSource().toInstant() Temporal.Instant.from(getSource()); // ❌ -> getSource().toInstant() // 通过成员访问的属性包也能识别: Temporal.PlainDate.from({year: holder.source.year, month: holder.source.month, day: holder.source.day, calendar: holder.source.calendarId}); // ❌ -> holder.source.toPlainDate()同时快照确认了类型感知的边界:当返回类型是联合类型且包含非 Temporal 成员(如Temporal.ZonedDateTime | undefined、Temporal.ZonedDateTime | string)、类型为unknown/any/Temporal.PlainDate/裸PlainDateTime时,规则不报告;Temporal.ZonedDateTime | Temporal.PlainDateTime联合类型仅在目标是PlainDate/PlainTime时报告(因为两者都有对应的toPlainDate/toPlainTime方法),目标是Instant/PlainDateTime时不报告(快照 invalid(9)、(10) 与 valid 用例对比可见)。
明确不检查的边界
官方文档与快照 valid 用例共同确认了以下场景不会被报告:
- 部分字段或修改过的字段(
{year, month}缺day、source.day + 1); - 多余属性、重复属性、计算属性名、getter 方法;
- 混合接收者(不同字段来自不同对象);
- 展开运算
{...source}、可选链source?.toString()、可选调用source.toString?.(); - 字符串操作结果(
source.toString().slice(0, 10)); - 解析/序列化选项(
Temporal.PlainDate.from(source.toString({calendarName: "never"})))、需要额外参数的转换; - 同类型克隆(
Temporal.PlainDateTime.from(source)源与目标同为 PlainDateTime); Date互操作、任意方法链推断、未知接收者与普通数据对象;Temporal.ZonedDateTime.from(value, options, extra)这类参数数量不合规的源构造。
规则也不追踪重命名导入(仅当本地绑定仍叫Temporal时支持 polyfill 导入,快照 invalid(51) 验证了import {Temporal} from "@js-temporal/polyfill"场景)。
在项目中使用与验证
安装与启用
npm install --save-dev eslint eslint-plugin-unicorn该规则已默认包含在recommended配置中(见 readme.md#L600-L629 的预设配置示例),也可以单独启用:
import unicorn from 'eslint-plugin-unicorn'; import {defineConfig} from 'eslint/config'; export default defineConfig([ { files: ['**/*.js'], plugins: {unicorn}, extends: ['unicorn/recommended'], rules: { 'unicorn/prefer-temporal-conversion': 'error', }, }, ]);由于规则无配置项(schema: []),开箱即用,无需任何选项。
自动修复与编辑器建议
- 运行
npx eslint . --fix可自动修复所有"精确转换"类问题; - 对于会改变结果的转换(毫秒重建、Instant 序列化往返、缺省日历的属性包等),需要借助编辑器建议逐一手动应用,或按官方文档建议先显式添加取整操作(如
source.epochMilliseconds场景下显式round),再交给规则处理。
运行快照测试
在仓库根目录执行测试命令即可重新生成/校验本文解析的快照文档:
npx ava test/prefer-temporal-conversion.js测试用例定义位于 test/prefer-temporal-conversion.js,其注释("Lock down the distinction between exact conversions and restoring discarded information")直接点明了自动修复与建议分界的测试意图;快照输出保存在 test/snapshots/prefer-temporal-conversion.js.md 与prefer-temporal-conversion.js.snap中。
小结
prefer-temporal-conversion规则通过识别六类间接转换模式,将 Temporal 类型间的转换收敛为原生toInstant()/toPlainDate()/toPlainDateTime()/toPlainTime()方法,在消除样板代码的同时避免序列化往返与字段重建带来的精度和日历信息丢失。其"精确转换自动修复、信息有损转换仅给建议、含注释只报告"的三级行为设计,配合对 TypeScript 断言、类型感知、变量别名与复杂表达式的支持,使其既激进又安全。快照文档 test/snapshots/prefer-temporal-conversion.js.md 作为行为契约,完整记录了每个检测分支的输入、报错与修复结果,是理解和使用该规则最可靠的参考手册。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考