- 编程语言
- 编译器
- 语言运行时
- 标准库
- 开发工具
【免费下载链接】sdk
The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
本文档是 Dart Analysis Server 内部关于"重构(Refactor)"功能的设计说明(原文见 pkg/analysis_server/doc/design/features/refactors.md),它界定了 analysis_server 中所有代码修改类操作的分类方式、语义保持的取舍原则以及生成代码的工程规范。阅读本文后,你将理解编辑器里"灯泡"(quick fix / assist / refactor)背后的统一架构:Fix、Assist、Global Refactor 三类操作的差异与 UX 影响,服务端在何种情况下允许改变代码语义,以及生成代码时应遵循的风格与"不完整代码"处理策略,并能结合仓库源码定位到具体的实现位置。
从协议入口看 Refactor 的定位
Refactor 是用户可以从编辑器中选择执行、从而修改代码的动作。在 Analysis Server 中,这类能力通过两套协议暴露:
- LSP 协议:通过
textDocument/codeActions请求提供(对应 LSP 3.17 规范,原文以链接形式引用); - Legacy 协议:通过
edit.getAssists、edit.getAvailableRefactorings、edit.getFixes、edit.getRefactoring四个请求提供。
在 LSP 实现侧,可以找到这些请求对应的处理链,例如 pkg/analysis_server/lib/src/lsp/handlers/code_actions/abstract_code_actions_producer.dart 中即围绕textDocument/codeAction请求组织 code action 的产出与优先级。
虽然用户倾向于把所有重构都看成"同一件事",但服务器内部将其划分为三类。这种划分部分源于实现层面的考量,更重要的是因为三类操作在**用户体验(UX)**上有着不同的含义与影响。
划分依据:两个特征维度
三类重构的分类基于重构的两个特征:
编辑创建时间(edit creation time)
- Eager Refactor(即时重构):在 code action 返回之前就完成代码变更的计算。如果无法计算出代码编辑,则该 code action 根本不会返回。这样能保证:只要用户选中该操作,编辑就能成功应用。
- Lazy Refactor(惰性重构):返回 code action 时并不计算具体的代码编辑。通常只做一组最小化的检查,确保仅在重构"有意义"时才显示该操作,但用户选中后仍可能失败。此类失败场景下,服务器会显示一条消息向用户解释失败原因。
可用性(availability)
- 部分重构仅在存在诊断(diagnostic)指示问题时才可用;
- 另一些重构只要编辑器选区位于恰当的 token 上就始终可用(无论是否有诊断)。
三种重构类型
Fixes(修复)
Fixes 是用于解决代码中由诊断所指示问题的代码变更,始终以 eager 方式计算。
Fixes 可以被触发用于:
- 修复单个文件中单个位置上的单个诊断;
- 修复单个文件中某个诊断报告的所有位置;
- 修复工作区所有文件中全部诊断的所有位置(通过
dart fix以及基于 LSP 的 IDE)。
在仓库中,Fix 的实现集中在 pkg/analysis_server/lib/src/services/correction/fix.dart(及其fix_internal.dart),具体到每个诊断的修复产出于 pkg/analysis_server/lib/src/services/correction/dart/ 目录下——该目录包含数百个以行为命名的修复生产者,例如remove_unused_import.dart(移除未使用导入)、add_null_check.dart(添加空检查)、convert_to_switch_expression.dart(转换为 switch 表达式)等。跨文件的批量修复由 pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart 负责调度,这正是dart fix命令在命令行侧的对应实现。
Assists(辅助操作)
Assists 是即使没有诊断也始终可用的代码变更,始终以 eager 方式计算。
Assists 只能在单个文件的单个位置触发。
Assist 的统一入口位于 pkg/analysis_server/lib/src/services/correction/assist.dart(以及assist_internal.dart)。由于不依赖诊断,Assists 的可用性完全取决于编辑器选区所在的代码上下文,例如将if-else转换为条件表达式、提取局部变量等操作在无错误代码上同样可以被触发。
Global Refactors(全局重构)
Global Refactors 是可能涉及多个库、甚至跨多个 package 变更的代码重构(当这些 package 同时打开在 IDE 工作区中时)。Global Refactors始终以 lazy 方式计算。
Global Refactors 只能在单个文件的单个位置触发。
全局重构的实现框架位于 pkg/analysis_server/lib/src/services/refactoring/framework/,其中:
- refactoring_processor.dart 负责重构的整体执行流程;
- refactoring_producer.dart 定义重构生产者;
- refactoring_context.dart 封装触发重构时的上下文信息。
具体的全局重构(如移动顶层声明到新文件move_top_level_to_file.dart、重命名相关的add_import_prefix.dart/remove_import_prefix.dart等)则放在 pkg/analysis_server/lib/src/services/refactoring/ 目录下。
术语提醒:在很多语境下(例如 issue tracker 中),术语 "refactor" 有时泛指任意类型的重构,有时又特指 global refactor。本文档统一使用更长的名字以保持清晰。
语义保持(Preserving Semantics)
对于"重构是否应该改变代码语义",文档明确表示没有一刀切的禁令——有些重构正是因为改变了语义才有价值。甚至可以论证大多数 Fix 都在改变语义:把代码从"无法编译"变为"可编译"本身就是语义变化。本节讨论的是一组判断准则,用于决定何时应当保持语义、何时允许改变语义。
用户期望(User Expectations)
首先要问的问题:用户在多大程度上会合理地期望语义被保持?
- 合理假设保持语义的例子:把switch 语句转换为 switch 表达式的重构,用户会期望它保持 switch 的语义。
- 合理期望语义发生变化的例子:把方法标记为
async并把返回类型改为Future的重构,用户能预期到这会改变代码语义。
微妙 vs. 明显的变化(Subtle vs. Obvious Changes)
如果某个重构将改变代码语义,那么这种改变应当对用户显而易见。语义变化越微妙,就越不适合让语义发生改变。例如:
- 合适:把方法转换为
async的 assist——虽然改变了语义,但因为返回类型被修改、新增了关键字,改变非常容易看见(对应实现见 pkg/analysis_server/lib/src/services/correction/dart/add_async.dart)。 - 不合适:一种会改变查找作用域、使得某些标识符在没有任何提示的情况下被解析到不同目标的修改——这种变化过于微妙。
此外,作用范围也影响判断:
- 若 Fix 只在单个位置应用,语义变化通常更明显;
- 若 Fix 在大型代码库中批量应用,语义变化可能很容易被忽略,因为受影响的文件可能根本没有被打开(用户看不到变化)。
产出损坏代码(Producing Broken Code)
几乎不存在(如果有的话)合理的理由让重构产出无法编译的代码。但存在两个已知例外:
- 作用于已损坏代码的重构:某些重构会作用在本来就无法编译的代码上,此时结果仍然损坏是合理的——只要没有"坏得更厉害"。但通常不应由重构引入新的诊断到代码中。
- 客户端明确告知且用户确认继续:如果客户端允许服务器向用户通报当前状况,并且用户明确表示希望继续,那么继续执行重构是有意义的。
生成代码(Generated Code)
大多数重构在运行时会生成代码。本节描述生成代码时应遵循的工程实践,而遵循这些实践的最佳方式之一,就是使用DartFileEditBuilder和DartEditBuilder类中定义的工具方法来生成代码。
这两个类位于 pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_dart.dart(实现位于 pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart)。从实现可以看到,DartEditBuilderImpl会依据CodeStyleOptions(由启用的 lint 推导)决定生成代码的细节,例如:
- 是否在
dynamic类型处写明类型注解(对应always_specify_types等 lint,见shouldWriteDynamicNonReturnTypes/shouldWriteDynamicReturnTypes的判定逻辑); - 提供
addLinkedEdit、canWriteType、createLinkedEditBuilder等能力,支持生成带关联编辑(linked edit)的代码片段。
这从源码层面印证了文档"生成的代码应尽量遵循已启用 lint 所强制执行的风格"这一要求。
风格(Style)
- 生成的代码应尽可能接近用户可能亲手写出的代码。例如:如果认为用户更可能写带块体的方法而不是表达式体方法,那么生成方法时应产出块体;同样,如果认为大多数用户在返回
Future的方法上会使用async,生成时应包含该修饰符。 - 生成的代码应尽可能遵循已启用 lint 所强制执行的风格。例如:若启用了强制统一字符串字面量定界符的 lint,则生成的所有字符串字面量都应使用该定界符。
- 生成的代码应在格式上接近 formatter 的输出。服务器会合理尝试生成带恰当缩进和 token 间空格的代码,但不需要包含自动换行、刻意添加或省略的逗号等。一旦未来获得对文件局部区域运行 formatter 的能力,就应该对生成代码使用 formatter。
不完整代码(Incomplete Code)
有时生成不完整代码是必要的。例如:根据一个示例调用生成方法时,服务器可以推断出参数的数量与类型、甚至返回类型,但无法知道方法体该如何实现。
生成不完整代码时,应尽量让"代码不完整"这一事实显而易见,有两种做法:
- 生成带编译错误的代码;
- 在生成代码中附带
TODO注释。
例如:生成一个需要返回值的方法时,可以选择生成返回null的代码。但如果返回类型本身可空,就不会有任何信号提示该方法是不完整的——这种情况下,更好的选择是生成一个不返回值的方法(从而产生编译错误作为信号)。
一个"反例":当服务器生成对某个具体方法的 override 时,会添加对被覆盖方法的super 调用。如果原方法有返回值,则返回被覆盖方法的返回值——这样代码没有任何诊断,因此服务器会补上一个TODO注释。同时代码保持了语义等价。这看似违反了上面的建议,但严格来说这段代码并非不完整,只是通常不是用户真正想写的内容。这类"补齐 override"的逻辑可在 pkg/analysis_server/lib/src/services/correction/dart/create_missing_overrides.dart 等生产者中查看其实现形态。
小结
Analysis Server 的重构体系可以用一张表概括:
| 维度 | Fixes | Assists | Global Refactors |
|---|---|---|---|
| 是否依赖诊断 | 依赖(由诊断触发) | 不依赖(选区在合适 token 上即可) | 不依赖(选区在合适 token 上即可) |
| 编辑计算方式 | Eager(返回前算好) | Eager(返回前算好) | Lazy(选中后才计算,可能失败) |
| 作用范围 | 单位置 / 单文件全部位置 / 全工作区(dart fix) | 单文件单位置 | 多库、跨 package,但触发点仍是单文件单位置 |
| 语义保持 | 允许改变(如修复编译错误),但变化需可感知 | 默认保持,变化需明显 | 由具体重构决定,需在 UX 上可感知 |
无论哪一类,其共同的工程底线是:尽量不产出比原来更坏的代码、不引入新的诊断;生成代码时贴近用户习惯、贴近 lint 与 formatter 的约束;必要的不完整代码必须给出显式信号(编译错误或 TODO 注释)。理解了这套分类与准则,无论是阅读 pkg/analysis_server/lib/src/services/correction/dart/ 下数百个修复/辅助生产者的源码,还是自己为 Analysis Server 或分析器插件编写新的 code action,都能快速定位其定位与应遵守的规则。
- 编程语言
- 编译器
- 语言运行时
- 标准库
- 开发工具
【免费下载链接】sdk
The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
相关推荐
PhoneInfoga 实战指南:scan 命令行扫描与 Web 服务部署
PhoneInfoga 实战指南:scan 命令行扫描与 Web 服务部署 PhoneInfoga 是一个面向电话号码的信息收集(OSINT)框架,其核心使用方
编程语言编译器语言运行时标准库开发工具OBS Studio 代码风格规范深度解读:clang-format 强制规则、各语言守则与架构设计准则
OBS Studio 代码风格规范深度解读:clang format 强制规则、各语言守则与架构设计准则 OBS Studio 的 CODESTYLE.md h
音视频直播屏幕录制桌面应用视频Dart Analysis Server 的 Sort Members 命令:成员排序规则与源码实现深度解析
Dart Analysis Server 的 Sort Members 命令:成员排序规则与源码实现深度解析 Sort Members(排序成员)是 Dart
编程语言编译器语言运行时标准库开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考