Biome

GritQL 插件配方

Biome 常见 Lint 场景下可直接使用的 GritQL 插件示例

本页提供一系列实用的 GritQL 插件示例,你可以直接在项目中使用。每个示例都旨在演示一项特定的 GritQL 特性,同时解决一个实际的 Lint 问题。

若要入门 GritQL 语法与插件系统,请先阅读 Linter 插件GritQL 参考 页面。

要使用下面的任意示例,请将 GritQL 片段保存为项目中的 .grit 文件,并在配置中注册:

{
  "plugins": ["./plugins/your-rule.grit"]
}

或者,你也可以直接打开每个示例附带的 Playground 链接。


JavaScript / TypeScript

以下是针对 JavaScript/TypeScript 语言的一组示例。

强制使用严格相等,与 null 比较除外

GritQL 模式可以通过 where 子句附加条件。在 where 块内,匹配操作符 <: 用于测试变量是否匹配给定模式,not 关键字则对该测试取反。以逗号分隔的多个条件必须全部成立,模式才会匹配。

这里我们匹配任意 == 比较,再使用两个带 not 的条件跳过任一操作数为字面量 null 的情况,因为 == null 是宽松相等唯一的惯用写法:

`$left == $right` where {
    $right <: not `null`,
    $left <: not `null`,
    register_diagnostic(
        span = $left,
        message = "Use `===` instead of `==`. Loose equality is only acceptable when comparing against `null`.",
        severity = "warn"
    )
}

匹配:两侧都不是 null

if (x == 1) {
}
if (x == "hello") {
}

不匹配:一侧是 null,此时宽松相等可以接受:

if (x == null) {
}
if (null == x) {
}

在 Playground 中试用此示例

禁用 forEach:改用 for...of

展开元变量 $... 匹配零个或多个参数(或列表元素),但不绑定它们。as 关键字将整个匹配到的节点绑定到一个变量,方便你稍后引用,通常用于设置诊断信息的 span

我们用 $... 匹配 .forEach(),无论传入多少参数;再用 as $call 捕获完整表达式,作为诊断信息的 span:

`$collection.forEach($...)` as $call where {
    register_diagnostic(
        span = $call,
        message = "Prefer `for...of` over `.forEach()`. It supports `break`, `continue`, and `await`."
    )
}
const items = [1, 2, 3];
items.forEach((item) => console.log(item));
items.forEach((item, index) => {
  console.log(index, item);
});
for (const item of items) {
  console.log(item);
}

两个 .forEach() 调用都被匹配(第 2 行和第 3 行)。第 6 行的 for...of 循环不受影响。

在 Playground 中试用此示例

禁用受限导入

or 操作符在其任意子模式匹配时即匹配成功。这里我们用它列出多个被禁的包名。匿名元变量 $_ 匹配任意节点而不创建具名绑定,适合不关心取值的场景。

我们匹配任意 import 语句,用 $_ 忽略导入绑定,并检查来源字符串是否匹配任一被禁的包:

`import $_ from $source` where {
    $source <: or { `'lodash'`, `'underscore'`, `'moment'` },
    register_diagnostic(
        span = $source,
        message = "This package is not allowed. Use the approved alternative instead."
    )
}

第 1、3、4 行被匹配。第 2 行(dayjs)不在禁用列表中。

在 Playground 中试用此示例

你也可以在同一个文件中捕获 require() 调用,方法是用顶层 or 同时匹配两种导入写法:

or {
    `import $_ from $source`,
    `require($source)`
} where {
    $source <: or { `'lodash'`, `'underscore'`, `'moment'` },
    register_diagnostic(
        span = $source,
        message = "This package is not allowed. Use the approved alternative instead."
    )
}
const moment = require("moment");
const utils = require("lodash");

对于被禁的包,importrequire() 两种形式都会被匹配。第 2 行的 dayjs 不在列表中。

在 Playground 中试用此示例

禁用 new Date():改用日期库

当代码片段中 $... 是唯一参数时,它匹配零个或多个参数。当你在它前面加上具名元变量,如 $first, $...,模式就要求至少一个参数$first 必须绑定到某个内容。

这里 $first 要求至少一个参数,因此 new Date()(获取当前时间)被允许,而 new Date("2024-01-15") 及类似的解析调用会被标记:

`new Date($first, $...)` as $expr where {
    register_diagnostic(
        span = $expr,
        message = "Avoid the `Date` constructor for parsing. Use the project's date utility instead."
    )
}
const now = new Date();
const parsed = new Date("2024-01-15");
const custom = new Date(2024, 0, 15);
const fromTs = new Date(1705276800000);

第 1 行(无参数的 new Date())不被匹配。第 2 至 4 行都至少有一个参数,因此触发诊断信息。

在 Playground 中试用此示例

禁用 eval()Function() 构造器

顶层 or 让你把互不相关的语法模式组合进单条插件规则。每个分支都可以用 as $match 统一变量名,使共享的 where 子句能一致地引用它,即使各分支匹配的语法形态完全不同。

这里我们把 eval() 调用和 new Function() 构造器合并为一条规则:

or {
    `eval($code)` as $match,
    `new Function($...)` as $match
} where {
    register_diagnostic(
        span = $match,
        message = "Dynamic code evaluation is not allowed. Avoid `eval()` and `new Function()`."
    )
}
eval("alert(1)");
const fn = new Function("a", "b", "return a + b");
const safe = JSON.parse(data);

第 1、2 行被匹配。第 3 行不会:JSON.parse 完全是另一种模式。

在 Playground 中试用此示例

禁止嵌套三元表达式

除了匹配源代码片段,你还可以直接匹配 Biome 的具体语法树(CST)节点。每种节点类型都有一个唯一的 PascalCase 名称,如 JsConditionalExpressioncontains 修饰符会搜索匹配节点的整个子树,捕获任意深度的嵌套结构。

这里我们找出内部嵌套了另一个三元表达式的任意三元表达式:

engine biome(1.0)
language js(typescript, jsx)

JsConditionalExpression() as $outer where {
    $outer <: contains JsConditionalExpression() as $inner,
    register_diagnostic(
        span = $inner,
        message = "Nested ternary expressions are not allowed. Use `if`/`else` instead."
    )
}
const a = x ? 1 : 0;
const b = x ? (y ? 1 : 2) : 0;
const c = x ? 1 : y ? 2 : 3;

第 1 行只有一个(未嵌套的)三元表达式,不被匹配。第 2、3 行都包含嵌套在另一个三元表达式内的三元表达式。

在 Playground 中试用此示例

限制函数参数数量

我们匹配 JsParameters(),并用正则表达式检查参数列表是否包含 3 个及以上逗号,即 4 个及以上参数:

engine biome(1.0)
language js(typescript, jsx)

JsParameters() as $params where {
    $params <: r".*,.*,.*,.*",
    register_diagnostic(
        span = $params,
        message = "Functions should not have more than 3 parameters. Use an options object instead.",
        severity = "warn"
    )
}
function ok(a, b, c) {}
function tooMany(a, b, c, d) {}
const arrow = (a, b, c, d, e) => {};

ok 有 3 个参数,没有问题。tooManyarrow 都有 4 个以上参数,被匹配。

在 Playground 中试用此示例

禁止空的 catch 块

CST 节点可以在模式中嵌套,以表达结构性约束。这里我们匹配一个 JsCatchClause,其 body 字段是一个 statements 列表为空([])的 JsBlockStatement。这读起来几乎像类型断言:"包含不含任何语句的块的 catch 子句"。

engine biome(1.0)
language js(typescript, jsx)

JsCatchClause(body = JsBlockStatement(statements = [])) as $catch where {
    register_diagnostic(
        span = $catch,
        message = "Empty catch blocks are not allowed. Handle the error or add a comment explaining why it is ignored."
    )
}
try {
  riskyOperation();
} catch (e) {}

try {
  anotherOp();
} catch (e) {
  console.error(e);
}

第一个 catch 块(第 3 行)为空,被匹配。第二个内部有语句,不被匹配。

在 Playground 中试用此示例

不允许 any 类型注解

某些 CST 节点专属于 TypeScriptTsAnyType 节点表示作为类型注解出现的 any 关键字,无论它出现在何处。直接匹配该节点,就能捕获所有出现位置:变量声明、函数参数、返回类型和泛型参数。

engine biome(1.0)
language js(typescript)

TsAnyType() as $any where {
    register_diagnostic(
        span = $any,
        message = "Don't use `any`. Use `unknown`, a specific type, or a generic instead.",
        severity = "warn"
    )
}
let x: any = 1;
function foo(x: any): any {
  return x;
}
const arr: Array<any> = [];
let safe: unknown = 1;

第 1 至 3 行的每个 any 注解都会被匹配。第 4 行的 unknown 是不同类型,不受影响。

在 Playground 中试用此示例

优先使用 const 而非 let

由于 let 是关键字而非语法节点,我们匹配 JsVariableStatement(),并用正则表达式筛选,只保留文本以 let 开头的语句:

engine biome(1.0)
language js(typescript, jsx)

JsVariableStatement() as $stmt where {
    $stmt <: r"let.*",
    register_diagnostic(
        span = $stmt,
        message = "Prefer `const` unless the variable is reassigned.",
        severity = "hint"
    )
}
let x = 1;
let y = "hello";
const z = true;

两条 let 语句(第 1、2 行)都被匹配。第 3 行的 const 不会:其文本以 const 开头,因此正则 let.* 不匹配。

在 Playground 中试用此示例

禁用 dangerouslySetInnerHTML

GritQL 片段模式在 JSX 内同样有效。这里我们匹配 dangerouslySetInnerHTML prop,无论它位于哪个元素、传入什么值:

`dangerouslySetInnerHTML=$value` as $attr where {
    register_diagnostic(
        span = $attr,
        message = "Do not use `dangerouslySetInnerHTML`. Sanitize content and render it safely instead."
    )
}

匹配:任何使用该 prop 的元素:

<div dangerouslySetInnerHTML={{ __html: content }} />
<p dangerouslySetInnerHTML={{ __html: text }}></p>

不匹配:没有 dangerouslySetInnerHTML

<div className="safe">{content}</div>

在 Playground 中试用此示例

禁止内联 style prop

同样的方法可用于禁用内联 style prop。这能促使开发者改用 CSS 类或 CSS-in-JS 方案,而非内联样式:

`style=$value` as $attr where {
    register_diagnostic(
        span = $attr,
        message = "Avoid inline `style` props. Use a CSS class or a styled component instead.",
        severity = "warn"
    )
}

匹配:内联 style prop:

<button style={{ color: "red" }}>Click</button>
<div style={{ margin: 0, padding: 10 }}>Content</div>

不匹配:改用 className

<span className="highlight">OK</span>

在 Playground 中试用此示例


CSS

不允许 !important

默认情况下,GritQL 模式针对 JavaScript。.grit 文件顶部的 engine biome(1.0)language css 指令可切换到 Biome 的 CSS 语法树。!important 修饰符表现为一个 CssDeclarationImportant() 节点,因此我们用 contains 查找包含它的任意声明:

engine biome(1.0)
language css

CssDeclarationWithSemicolon() as $decl where {
    $decl <: contains CssDeclarationImportant(),
    register_diagnostic(
        span = $decl,
        message = "Avoid `!important`. Increase selector specificity or restructure your styles instead."
    )
}
.button {
  color: red !important;
  display: flex;
}
.override {
  margin: 0 !important;
}

第 2、6 行包含 !important 声明,被匹配。

在 Playground 中试用此示例

禁用硬编码颜色:改用 CSS 自定义属性

正则模式使用 r"..." 语法,匹配节点的文本内容而非语法结构。这对于匹配十六进制颜色码这类没有专用语法节点的值很有用。

这里我们用正则匹配 color 声明中的任意十六进制颜色值:

language css;

`color: $value` as $decl where {
    $value <: r"#[0-9a-fA-F]+",
    register_diagnostic(
        span = $value,
        message = "Don't use hardcoded hex colors. Use a CSS custom property (e.g. `var(--color-primary)`) instead.",
        severity = "warn"
    )
}
.header {
  color: #ff0000;
  background: var(--bg-primary);
}
.text {
  color: #1a2b3c;
}

第 2、6 行使用了硬编码的十六进制颜色,被匹配。第 3 行的 var() 引用不是十六进制值,通过检查。

在 Playground 中试用此示例

若要同时捕获 rgb()hsl() 函数,可用 or 组合多个正则模式:

language css;

`color: $value` as $decl where {
    $value <: or {
        r"#[0-9a-fA-F]+",
        r"rgba?\(.*\)",
        r"hsla?\(.*\)"
    },
    register_diagnostic(
        span = $value,
        message = "Don't use hardcoded colors. Use a CSS custom property instead.",
        severity = "warn"
    )
}

匹配:十六进制、rgb()hsl() 值:

.header {
  color: #ff0000;
  background: var(--bg-primary);
}
.alert {
  color: rgb(255, 0, 0);
}
.text {
  color: hsl(200, 50%, 50%);
}
.safe {
  color: var(--text-primary);
}

第 3 和第 12 行的 var() 引用不匹配任何正则模式,通过检查。

在 Playground 中试用此示例

不允许特定的 CSS 属性

顶层 or 列出多个备选片段模式。每个分支独立匹配,因此你只需显式列出,就能禁用多个 CSS 属性:

language css;

or {
    `float: $value`,
    `clear: $value`
} as $decl where {
    register_diagnostic(
        span = $decl,
        message = "The `float` and `clear` properties are not allowed. Use Flexbox or Grid for layout."
    )
}
.sidebar {
  float: left;
  width: 200px;
}
.clearfix {
  clear: both;
}
.modern {
  display: grid;
}

第 2 行的 float 和第 6 行的 clear 被匹配。.modern 规则使用了 display: grid,不在禁用列表中。

在 Playground 中试用此示例


JSON

强制 JSON 键命名规范

language json 指令(与 engine biome(1.0) 搭配使用)针对 JSON 文件。由于不支持带元变量的 JSON 片段,请使用 CST 节点 JsonMemberName() 匹配任意键。再结合正则or,即可强制执行命名规范。

这里我们标记包含下划线或以大写字母开头的任意键,两者都违反 camelCase:

engine biome(1.0)
language json

JsonMemberName() as $name where {
    $name <: or { r".*_.*", r".[A-Z].*" },
    register_diagnostic(
        span = $name,
        message = "JSON keys must use camelCase.",
        severity = "warn"
    )
}
{
  "userName": "alice",
  "user_name": "bob",
  "UserAge": 30,
  "email": "a@b.com"
}

user_name(snake_case)和 UserAge(PascalCase)被正则的备选分支匹配。userNameemail 是合法的 camelCase,不被匹配。

在 Playground 中试用此示例


进阶模式

你可以用顶层 or多条独立规则归入单个 .grit 文件。每个分支都有自己的模式、条件和诊断信息。where 子句可以独立地放在每个分支内,让每条规则拥有各自的严重性等级和消息。

这里我们把三项与调试相关的检查组合为一个插件:

or {
    `debugger` as $match where {
        register_diagnostic(
            span = $match,
            message = "Remove `debugger` statements before committing."
        )
    },
    `alert($...)` as $match where {
        register_diagnostic(
            span = $match,
            message = "Remove `alert()` calls before committing."
        )
    },
    `console.$method($...)` as $match where {
        $method <: or { `log`, `debug`, `trace` },
        register_diagnostic(
            span = $match,
            message = "Remove debug logging before committing.",
            severity = "warn"
        )
    }
}
debugger;
alert("test");
console.log("debug info");
console.error("real error");
console.debug("trace");

第 1、2、3、5 行分别被 or 的不同分支匹配。第 4 行(console.error)不在 logdebugtrace 列表中,通过检查。

在 Playground 中试用此示例


查找 CST 节点名称

上面的多个示例使用了 Biome 的 CST 节点名称,如 JsConditionalExpressionTsAnyType。以下介绍如何为你要匹配的代码找到正确的节点名称。

使用 Biome Playground

  1. 打开 Biome Playground
  2. 粘贴或输入你想匹配的代码片段。
  3. 在右侧的输出面板中切换到 Syntax 标签页。
  4. 语法树会展示每个节点及其类型名称标签。展开节点可查看其子节点和字段。
  5. 将找到的节点名称用于你的 GritQL 模式:用 NodeName() 匹配任意实例,或用 NodeName(field = ...) 匹配特定子节点。

常用节点名称

以下是 JavaScript/TypeScript 常用的一部分 Biome CST 节点名称:

节点名称匹配内容
JsIfStatementif (...) { ... }
JsConditionalExpressiona ? b : c
JsForStatementfor (...; ...; ...) { ... }
JsForOfStatementfor (... of ...) { ... }
JsCallExpressionfn(), obj.method()
JsNewExpressionnew Foo()
JsArrowFunctionExpression() => { ... }
JsFunctionDeclarationfunction foo() { ... }
JsCatchClausecatch (e) { ... }
JsBlockStatement{ ... }(语句块)
JsFormalParameter单个函数参数
JsParameters参数列表 (a, b, c)
JsVariableDeclarationconst x = 1, let y = 2
TsAnyType: any 类型注解
TsTypeAliastype Foo = ...
TsInterfaceDeclarationinterface Foo { ... }
JsxElement<div>...</div>
JsxSelfClosingElement<img />
JsxAttributeclassName="test", disabled

对于 CSS:

节点名称匹配内容
CssDeclarationWithSemicolonproperty: value;
CssComplexSelectordiv > .class

CST 模式的头部指令

使用 CST 节点名称时,你的 .grit 文件应包含 engine 和 language 指令:

engine biome(1.0)
language js(typescript, jsx)

engine biome(1.0) 指令告诉 GritQL 使用 Biome 的语法树(而非 Tree-sitter 的)。language 指令指定要匹配的语言语法;不加该指令时,默认按 JavaScript 处理。