架构
Biome 底层的工作方式。
本文介绍 Biome 的部分内部实现,以及它们在项目中的用途。
扫描器
Biome 内置一个扫描器,负责遍历文件系统,提取项目的重要元数据。具体来说,扫描器有以下三种用途:
- 在 monorepo 中发现嵌套的
biome.json/biome.jsonc文件。 - 当启用了
vcs.useIgnoreFile配置时,发现嵌套的.gitignore文件。 - 当启用了项目规则域(domain)中的任意规则时,为项目的
package.json清单文件和源文件建立索引。
扫描器定向
如果没有启用项目规则,扫描器会自动只针对与当前会话相关的目录。
这意味着,如果你有一个大型 monorepo,并在 packages/foo/ 目录内运行 biome check,该目录就会被「定向」。也就是说,以下目录会被扫描,用于查找嵌套配置文件和/或嵌套忽略文件:
- 仓库的根目录。
packages/目录。packages/foo/目录。packages/foo/下的所有子目录,但node_modules/或你的配置排除的目录除外(见 下文)。
与 packages/ 或 packages/foo/ 平级相邻的其他目录会被自动跳过。
同理,如果你在仓库根目录运行 biome format packages/bar/src/index.ts,扫描器会定向 packages/bar/src/ 目录。
如果启用了项目规则,上述优化不再适用。
配置扫描器
可以通过
files.includes 配置扫描器。
解析器与具体语法树(CST)
解析器的架构基于 rowan 的内部复刻,这是一个实现了 Green and Red tree 模式的库。
具体语法树(CST)是一种与抽象语法树(AST)非常相似的数据结构,它记录程序的全部信息,包括 trivia。
Trivia 指所有对程序运行并非必需、但仍需保留的信息:
- 空格
- 制表符
- 注释
每条 trivia 都依附于一个节点。一个节点可以有前导 trivia(leading trivia)和尾部 trivia(trailing trivia)。如果从左往右阅读代码,前导 trivia 出现在关键字之前,尾部 trivia 出现在关键字之后。
前导 trivia 与尾部 trivia 的划分规则如下:
- 直到该 token/关键字之前的所有 trivia(包含换行符)构成前导 trivia;
- 到下一个换行符之前(不含该换行符)的所有内容构成尾部 trivia;
在下面的 JavaScript 代码片段中,// comment 1 是 token ; 的尾部 trivia,// comment 2 是关键字 const 的前导 trivia。下面是 Biome 所表示的 CST 的简化版本:
const a = "foo"; // comment 1
// comment 2
const b = "bar";
0: JS_MODULE@0..55
...
1: SEMICOLON@15..27 ";" [] [Whitespace(" "), Comments("// comment 1")]
1: JS_VARIABLE_STATEMENT@27..55
...
1: CONST_KW@27..45 "const" [Newline("\n"), Comments("// comment 2"), Newline("\n")] [Whitespace(" ")]
3: EOF@55..55 "" [] []
出于设计考虑,CST 永远不被直接访问;开发者可以通过 Red tree 读取其中的信息,使用的 API 由语言语法自动生成。
容错且可恢复的解析器
要构建 CST,解析器必须对错误有韧性并且可恢复:
- 有韧性:解析器在遇到属于该语言的语法错误后能够继续解析;
- 可恢复:解析器能够理解错误发生的位置,并通过创建正确的信息来继续解析;
解析器的恢复能力并不是一门精确的科学,也没有一成不变的规则。也就是说,解析器能否按预期自我恢复,取决于它当时正在解析什么,以及错误发生在哪里。
解析器还会使用 Bogus(伪造)节点,防止下游消费者读到不正确的语法。这些节点用于包装因语法错误而损坏的代码。
在下面的例子中,while 缺少了圆括号,但解析器仍能较好地恢复,用一个还算合理的 CST 表示这段代码。圆括号和循环条件被标记为缺失,代码块则被正确解析:
while {}
JsModule {
interpreter_token: missing (optional),
directives: JsDirectiveList [],
items: JsModuleItemList [
JsWhileStatement {
while_token: WHILE_KW@0..6 "while" [] [Whitespace(" ")],
l_paren_token: missing (required),
test: missing (required),
r_paren_token: missing (required),
body: JsBlockStatement {
l_curly_token: L_CURLY@6..7 "{" [] [],
statements: JsStatementList [],
r_curly_token: R_CURLY@7..8 "}" [] [],
},
},
],
eof_token: EOF@8..8 "" [] [],
}
这是解析阶段产生的错误:
main.tsx:1:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ expected `(` but instead found `{`
> 1 │ while {}
│ ^
ℹ Remove {
下面这段代码就没这么幸运了。在恢复阶段,解析器无法正确理解这段语法,只能依赖 Bogus 节点把某些语法标记为错误。注意 JsBogusStatement:
function}
JsModule {
interpreter_token: missing (optional),
directives: JsDirectiveList [],
items: JsModuleItemList [
TsDeclareFunctionDeclaration {
async_token: missing (optional),
function_token: FUNCTION_KW@0..8 "function" [] [],
id: missing (required),
type_parameters: missing (optional),
parameters: missing (required),
return_type_annotation: missing (optional),
semicolon_token: missing (optional),
},
JsBogusStatement {
items: [
R_CURLY@8..9 "}" [] [],
],
},
],
eof_token: EOF@9..9 "" [] [],
}
这是解析阶段得到的错误:
main.tsx:1:9 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ expected a name for the function in a function declaration, but found none
> 1 │ function}
│ ^
格式化器
Linter
守护进程
Biome 使用服务器-客户端架构来运行任务。
daemon(守护进程)是一种长期运行的服务器, 由 Biome 在后台启动,用于处理来自编辑器和命令行接口(CLI)的请求。