- 编程语言
- 编译器
- 语言运行时
- 标准库
- 开发工具
【免费下载链接】sdk
The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
导读
本文以 design_notes.md 为核心骨架,深入剖析 Dart SDK 中 Analysis Server 的语句补全(Statement Completion)功能:当你在编辑器中敲下"智能回车"时,它如何自动补全当前语句缺失的分号、括号与花括号,让代码快速达到语法完整。文章既完整继承设计文档中的全部代码构造清单与示例,也结合statement_completion.dart的源码实现、edit.getStatementCompletion协议链路及测试用例,说明该功能的触发条件、处理流程与已知边界。读完本文,你将掌握该功能的完整设计理念、每一类语句/声明的具体补全规则,以及如何在 Analysis Server 插件中接入这一能力。
功能定位:为"当前语句"补齐语法
设计文档开篇给出该功能的使命:
为当前语句添加缺失的必需语法,目标是让语句在语法上变得完整(syntactically complete)。并非所有情况都能做到,无法做到时采用 best-effort 策略。
几个关键概念需要先厘清:
- 术语来源:"语句补全"(Statement Completion)一词源自 IntelliJ,在 IntelliJ 中它被称为更广义的Smart Enter。它并不局限于语法意义上的statement(语句),而是面向更广义的代码构造(code construct)——声明(declaration)、语句(statement)以及部分表达式(expression)都可以被补全。
- 补全内容:绝大多数情况下补全的只是标点符号——分号
;、括号()和花括号{},少数情况(如do语句缺失的while关键字)才会补全单词。 - "当前语句"的判定:以编辑器中主光标的当前位置所命中的代码构造为准。设计文档明确说明忽略 IntelliJ 中多个次要光标(secondary cursors),只考虑主光标。
- 已完整时的行为:如果当前语句在语法上已经完整,那么该功能只做一件事——插入一个换行。例如光标位于
for语句或while语句主体的闭花括号之后时即是如此。设计文档给出的心智模型是:用户正在向前编写代码,敲下 Smart Enter 时期望看到"前进";光标应当落在最可能继续编辑的位置,而无论之前的代码存在多少错误。就像用户说:"我这行写完了,帮我收尾并把我带到下一行。"
代码构造总览
设计文档将可补全的代码构造分为三大类,并标注了实现状态:
| 类别 | 已支持([x]) | 未处理 |
|---|---|---|
| 声明 Declarations | 函数/方法/类补花括号;函数/方法参数列表补右括号;变量补分号 | 泛型(Generics)当前不处理 |
| 表达式 Expressions | 未闭合字符串补终止符;未正确终止的列表补闭括号(可能带尾逗号) | Map 字面量(解析器错误恢复易与代码块的花括号混淆) |
| 语句 Statements | do / for / for-each / if / switch / try / while / 表达式语句 / 控制流块 | — |
此外设计文档还提到:存在若干"可补匹配右括号"的场景,但由于主流编辑器默认成对插入括号,该场景优先级不高,未作为重点。
声明(Declarations)的补全规则
设计文档指出声明部分"要做的工作有限",已实现三项:
- 函数、方法、类:若尚未定义函数体,补一对花括号
{}。对应源码实现为_complete_functionDeclaration(statement_completion.dart)与_complete_classDeclaration(同文件 L391-L409)。后者只在BlockClassBody的左花括号为合成(synthetic)token 且恰好只有一个诊断错误时触发。 - 函数、方法:若参数列表缺失右括号,补上
)。测试用例test_functionDeclNoParen与test_methodDeclNoParen验证了String source(^会被补全为String source() {的效果。 - 变量:补终止分号。对应
_complete_variableDeclaration(L1128-L1136),直接在节点末尾插入;并把退出位置(exit position)置于换行后的下一行。
一个细节:局部函数声明FunctionDeclarationStatement在解析出现异常时会走_complete_functionDeclarationStatement(L781-L817),针对=>箭头函数体补分号与换行,补全消息为"Add a semicolon and newline"。
表达式(Expressions)的补全规则
表达式部分由_checkExpressions(L297-L389)统一处理,它的工作方式不是直接修改 AST,而是扫描当前节点范围内的语法错误诊断,找到对应错误后再补符号并"移除"该错误:
- 未终止字符串:依据
unterminatedStringLiteral诊断补终止符。实现会先判断字符串是否带r前缀(raw 字符串),再判断是单行还是三引号多行字符串,进而选择补'/"还是'''/"""。测试用例覆盖了'text^→'text'、r"text^→r"text"、三引号'''text^→'''text'''等场景。 - 未正确终止的 List:依据
expectedToken中缺失]的诊断,找到其祖先ListLiteral,若右括号为合成 token 则补]。多行列表还会补尾逗号与正确缩进(,$eol$indent])。 - Map 不处理:设计文档明确说明——解析器的错误恢复很容易把代码块的花括号误判为 Map 的花括号。源码中这段处理被完整注释掉(L362-L388),注释写道"以下代码与
]的处理类似但效果不佳",印证了这一设计决策。测试test_mapAssign与test_mapAssignMissingColon均被标记为@failingTest(期望失败)。
语句(Statements)的补全规则:核心章节
设计文档强调:以关键字开头的语句,必须在部分语句中至少包含该关键字,补全才会发生。以下逐类说明。
do 语句
这是少数几个会补全"真实单词"的场景之一:
- 只要
do关键字存在,就补主体花括号; while关键字缺失时自动补while;while存在(或可补上)时,补条件括号;- 最后补终止分号。
源码_complete_doStatement(L462-L543)完整实现了这一流程,甚至处理了do;while这种残缺形态(先删除;再补结构)。测试test_keywordOnly展示了do^被补全为do {\n /**/ \n} while (^);,光标停在条件括号内;test_noWhile展示了do {}被补全为do {} while (^);。
for 语句
解析器无法区分 for 语句与 for-each 语句,除非控制部分中至少出现一个分号;或in关键字。若两者都没有,补全最多只能补主体花括号。
对于真正的 for 语句(_complete_forStatement,L626-L725):
- 控制部分会被调整为恰好两个分号(补条件、补更新部分);
- 主体花括号缺失时补上;
- 处理多种残缺形态:
for (;;^)、for (int i = 0;^)、for (;/* */^)、for (int i = 0^)(缺左分隔符)等,测试类_ForCompletionTest逐一验证。
for-each 语句
规则最简单:主体花括号缺失时补上。_complete_forEachStatement(L545-L567)与_complete_forEachStatementRest(L569-L624)还额外处理了缺循环变量(for (in xs)^→ 光标置于变量位置)与缺迭代对象(for (var x in)^)的场景。
if 语句
if-else 等结构可以无限复杂,因此设计上刻意忽略else关键字,保持简单:
- 从仅有的
if关键字出发,补条件括号 + 主体花括号。
实现上,if 与 while 共用同一套逻辑:_complete_ifOrWhileStatement(L819-L840)调用_complete_keywordCondition(L878-L913)处理"关键字-条件-块"三件套,并通过_KeywordConditionBlockStructure(L1318-L1333)这个辅助类封装公共结构。_complete_ifStatement(L842-L876)额外处理了else分支主体缺失的情况(仅当光标位于else之后时补花括号)。
switch 语句
- 给出
switch关键字后:选择器括号缺失则补(),主体花括号缺失则补{}; - 对光标所在的单个
case或default子句:补终止冒号:(只补光标所在子句的冒号,不涉及其他子句)。
对应_complete_switchStatement(L979-L1027):先处理合成括号,再通过_findInvalidElement定位光标命中的非法成员(SwitchCase/SwitchDefault),在其表达式或关键字末尾补:。注意测试test_caseNoColon目前因 dart-lang/sdk#49759 标记为@FailingTest,而带// @dart=2.19语言版本标记的同一场景(test_caseNoColon_language219)则通过——这是解析器对模式语法(pattern syntax)的错误恢复差异导致的已知边界。
try 语句
- 语句仅剩
try关键字时:补主体花括号,不补任何子句(on / catch / finally 都不会被自动创建); - on 子句:补其主体花括号;
- catch 子句:补参数列表括号 + 主体花括号;
- finally 子句:补主体花括号。
_complete_tryStatement(L1029-L1126)按此逻辑分支处理:先看try主体左花括号是否合成,再看是否有"非法元素"(光标命中的残缺 catch 子句),最后单独处理 finally。测试类_TryCompletionTest覆盖了try^、on^、on Exception^、catch ^、finally^、on catch^等全部形态。
while 语句
与 if 语句结构完全相同,实现共享——_complete_whileStatement(L1138-L1154)只是包了一层_KeywordConditionBlockStructure后转发给_complete_ifOrWhileStatement。测试注释也明确说明:"while 的测试用例由_IfCompletionTest覆盖,若实现变更应在此复制同一套测试。"
表达式语句(方法/函数调用)
- 表达式是调用(invocation)时:补右括号
); - 补终止分号
;。
_complete_methodCall(L915-L946)先通过expectedToken缺失)的诊断定位ArgumentList,在光标与参数表末尾的较小偏移处补),再检查是否有缺失;的诊断并补分号,最后插入换行并把退出位置定位到下一行。测试test_noCloseParen、test_noCloseParenWithSemicolon1/2验证了'sample'.substring(3^补全为'sample'.substring(3);的效果。
控制流块(Control-flow Blocks)——光标跳出的魔法
这是设计文档中唯一给出代码示例的部分,功能也最"聪明":
在作为控制流语句(do、for、for-each、if、while)主体的代码块中写完return或throw之后,光标会被移出该代码块,准备好开始编写控制流语句之后的下一句:
if (isFinished()) { releaseResources(); return; // 在此处调用 'smart enter' } // 继续在这里输入实现位于_complete_controlFlowBlock(L411-L460):要求当前节点是ReturnStatement或含ThrowExpression的表达式语句,且其父级是Block、祖父级是 do/for/if/while 之一。处理时若发现缺失分号诊断,会先在return关键字或throw关键字之后补;,再在块结束位置插入换行与缩进,并把exitPosition(退出位置)设置为块外。测试类_ControlFlowCompletionTest中的test_ifThrow、test_doReturnUnterminated、test_forThrowUnterminated、test_whileReturnExpr等用例均验证了这一"补分号并移出块"的行为。
源码级剖析:核心数据结构与主流程
设计文档偏重行为描述,而 statement_completion.dart(共 1333 行)提供了完整实现。几个关键构件:
DartStatementCompletion(L29-L86):补全种类的枚举,共 14 种,包括No_COMPLETION(无可用的补全)、SIMPLE_ENTER("在行尾插入换行")、SIMPLE_SEMICOLON("加分号与换行")、以及COMPLETE_CLASS_DECLARATION、COMPLETE_CONTROL_FLOW_BLOCK、COMPLETE_DO_STMT、COMPLETE_IF_STMT、COMPLETE_FOR_STMT、COMPLETE_FOR_EACH_STMT、COMPLETE_FUNCTION_DECLARATION、COMPLETE_SWITCH_STMT、COMPLETE_TRY_STMT、COMPLETE_VARIABLE_DECLARATION、COMPLETE_WHILE_STMT。StatementCompletion(L91-L101):一次补全的结果,包含kind(补全种类)与change(要应用的SourceChange)。StatementCompletionContext(L104-L109):计算上下文,承载ResolvedUnitResult(已解析单元结果)与selectionOffset(光标偏移)。StatementCompletionProcessor(L133 起):核心处理器。compute()(L169-L233)是主流程。
compute()的决策链值得展开:
- 通过
unit.nodeCovering(offset: selectionOffset)定位光标命中的 AST 节点(_selectedNode,L1269-L1270); - 向上回溯到最近的
Statement或非语句声明(thisOrAncestorMatching); - 若命中
Block且非空,取块内最后一条语句;空语句/空块则上溯到父节点; - 收集该节点范围内类型为
SYNTACTIC_ERROR(语法错误)的诊断——这是补全触发的关键信号:没有语法错误时走"轻量路径"(if / for / while / 控制流块 / 简单换行),有语法错误时才启用 do / switch / try / 声明补全 / 简单分号 / 方法调用等全部补全器; - 各种
_complete_*方法按优先级依次尝试,任何一个成功即返回对应StatementCompletion; - 全部失败则回退到
_complete_simpleEnter()(插入换行)或No_COMPLETION。
补全结果的落点由_setCompletion(L1272-L1280)完成:把exitPosition写进change.selection(即补全后光标应停留的位置),并写入人类可读的message(与枚举中的描述一致,测试断言即据此匹配)。编辑的生成则通过_addInsertEdit/_addReplaceEdit/_insertBuilder组合完成,其中_addReplaceEdit(L240-L260)会按偏移量有序插入编辑,避免冲突。
协议链路:edit.getStatementCompletion 请求
语句补全通过 Analysis Server 的 Legacy 协议暴露给 IDE:
- 协议方法名定义在 protocol_constants.dart:
editRequestGetStatementCompletion = 'edit.getStatementCompletion'; - 处理器为
EditGetStatementCompletionHandler(edit_get_statement_completion.dart),它解析请求参数(file与offset),调用server.getResolvedUnit(file)得到解析结果,构建StatementCompletionContext与StatementCompletionProcessor,执行compute()后把SourceChange作为EditGetStatementCompletionResult返回; - 注册点在 legacy_analysis_server.dart:
editRequestGetStatementCompletion: EditGetStatementCompletionHandler.new。
因此一条完整的调用链是:IDE 发送edit.getStatementCompletion(携带文件路径与光标 offset)→EditGetStatementCompletionHandler→StatementCompletionProcessor.compute()→ 返回含编辑序列与新光标位置的SourceChange→ IDE 应用编辑。任何基于 Analysis Server 的编辑器插件(如 VS Code 的 Dart 插件)都可以借此实现 Smart Enter 体验。
测试验证体系
该功能拥有完整的单元测试,位于 statement_completion_test.dart(1459 行),并在 test_all.dart 中注册。测试按功能分 11 个反射式测试类:
_ControlFlowCompletionTest、_DeclarationCompletionTest、_DoCompletionTest、_ExpressionCompletionTest、_ForCompletionTest、_ForEachCompletionTest、_IfCompletionTest、_SimpleCompletionTest、_SwitchCompletionTest、_TryCompletionTest、_WhileCompletionTest。
测试采用统一的驱动方式:_prepareCompletion把含^标记的代码片段解析为ResolvedUnitResult,以标记位置为光标偏移调用StatementCompletionProcessor.compute();_assertHasChange断言补全消息、应用编辑后的完整代码以及最终光标位置(^出现处)。这种"期望代码 + 期望光标"的断言风格让每一类补全行为都有明确的回归保障,例如:
int v = 1^→int v = 1;\n^(补分号并把光标移到下一行);String source()^→String source() {\n ^\n}(补函数体花括号,光标停在体内);class Sample^→class Sample {\n ^\n}(补类体花括号);if (true) return 0^→if (true) return 0;\n^(无块时退化为补分号);for (int i = 0^)→for (int i = 0; ^; )(补两个分号,光标停在条件位置)。
已知边界与限制(设计文档明示)
为保证事实准确,这里汇总设计文档与源码共同确认的限制:
- Map 字面量不补全:错误恢复难以区分 Map 花括号与代码块花括号(源码中相关实现被注释保留);
- 泛型不处理;
- 匹配右括号的补全非优先级:因编辑器默认成对插入括号;
- else 被忽略:if 语句补全不做复杂 if-else 链的推导;
- for / for-each 的歧义:无分号且无
in时无法判定类型,仅补主体花括号; - try 语句不自动创建子句:只补已有子句缺失的括号/花括号;
- 部分解析器差异:
case冒号补全在模式语法(language 3.x)下存在已知失败(issue #49759),且不同解析器(Analyzer 与 Fasta/CFE)对同一残缺代码可能产生不同的错误恢复结果(测试注释中多处提及)。
总结
Statement Completion(Smart Enter)是 Dart Analysis Server 提供给 IDE 的一项"语法收尾"能力:它以光标命中的代码构造为对象,基于语法错误诊断判断缺失的标点,通过edit.getStatementCompletion请求返回一组编辑与新的光标位置。设计文档为它划定了清晰的边界——声明、表达式、语句三大类共十余种补全规则,以及在 Map、泛型、if-else 等场景下的刻意取舍;而 statement_completion.dart 与配套测试则给出了可直接阅读、可验证的实现与回归保障。对于希望为 Dart 编辑器实现智能换行体验的开发者,这份设计文档与源码是一份不可多得的完整参考。
- 编程语言
- 编译器
- 语言运行时
- 标准库
- 开发工具
【免费下载链接】sdk
The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
相关推荐
Hasura graphql-engine SQL Server Upsert 突变设计与实现:if_matched 子句与 MERGE 语句深度解析
Hasura graphql engine SQL Server Upsert 突变设计与实现:if_matched 子句与 MERGE 语句深度解析 本文围绕
后端API网关数据库GraphQLSass 语句与语法解析规范(Statement & Grammar)深度解读
Sass 语句与语法解析规范(Statement & Grammar)深度解读 本篇技术指南以 Sass 官方规范中的 spec/statement.md ht
前端Dart Analysis Server 代码编辑功能体系深入解析:Quick Fix、Quick Assist 与 Refactoring 的设计与实现
Dart Analysis Server 代码编辑功能体系深入解析:Quick Fix、Quick Assist 与 Refactoring 的设计与实现 An
编程语言编译器语言运行时标准库开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考