Pular para o conteúdo

GritQL Plugin Recipes

Este conteúdo não está disponível em sua língua ainda.

This page provides a collection of practical GritQL plugin examples that you can use directly in your projects. Each example is designed to demonstrate a specific GritQL feature while solving a real-world linting problem.

For an introduction to GritQL syntax and the plugin system, see the Linter Plugins and GritQL reference pages first.

To use any of the examples below, save the GritQL snippet to a .grit file in your project and register it in your configuration:

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

Alternatively, you can navigate the playground link attached to each example.


Below, there’s a collection of examples for JavaScript/TypeScript language.

Enforce strict equality except against null

Section titled “Enforce strict equality except against null”

GritQL patterns can have conditions attached via the where clause. Inside a where block, the match operator <: tests whether a variable matches a given pattern, and the not keyword negates that test. Multiple conditions separated by commas must all be true for the pattern to match.

Here we match any == comparison, then use two conditions with not to skip cases where either operand is the literal null — since == null is the one idiomatic use of loose equality:

`$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"
)
}

Matched — neither side is null:

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

Not matched — one side is null, so loose equality is acceptable:

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

Try this example in the Playground

The spread metavariable $... matches zero or more arguments (or list elements) without binding them. The as keyword binds the entire matched node to a variable, so you can reference it later — typically to set the diagnostic span.

We use $... to match .forEach() regardless of how many arguments are passed, and as $call to capture the full expression for the diagnostic 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);
}

Both .forEach() calls are matched (lines 2 and 3). The for...of loop on line 6 is not affected.

Try this example in the Playground

The or operator matches if any of its child patterns match. Here we use it to list multiple banned package names. The anonymous metavariable $_ matches any node without creating a named binding — useful when you don’t care about the value.

We match any import statement, ignore the imported bindings with $_, and check whether the source string matches any of the banned packages:

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

Lines 1, 3, and 4 are matched. Line 2 (dayjs) is not in the banned list.

Try this example in the Playground

You can also catch require() calls in the same file by using a top-level or to match both import styles:

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."
)
}
import _ from "lodash";
import dayjs from "dayjs";
const moment = require("moment");
const utils = require("lodash");

Both import and require() forms are matched for banned packages. dayjs on line 2 is not in the list.

Try this example in the Playground

When a code snippet contains $... as the only argument, it matches zero or more arguments. When you add a named metavariable before it like $first, $..., the pattern requires at least one argument$first must bind to something.

Here $first requires at least one argument, so new Date() (getting “now”) is allowed while new Date("2024-01-15") and similar parsing calls are flagged:

`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);

Line 1 (new Date() with no args) is not matched. Lines 2-4 all have at least one argument, so they trigger the diagnostic.

Try this example in the Playground

A top-level or lets you combine unrelated syntax patterns into a single plugin rule. Each arm can use as $match to unify the variable name so that the shared where clause can reference it consistently — even though the arms match completely different syntax shapes.

Here we combine eval() calls and new Function() constructors into one rule:

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);

Lines 1 and 2 are matched. Line 3 is not — JSON.parse is a different pattern entirely.

Try this example in the Playground

Instead of matching source code snippets, you can match against Biome’s concrete syntax tree (CST) nodes directly. Each node type has a unique PascalCase name like JsConditionalExpression. The contains modifier searches the entire subtree of a matched node, catching nested structures at any depth.

Here we find any ternary that contains another ternary nested inside it:

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;

Line 1 has a single (non-nested) ternary and is not matched. Lines 2 and 3 each contain a ternary inside another ternary.

Try this example in the Playground

We match JsParameters() and use a regex to check whether the parameter list contains 3 or more commas — meaning 4 or more parameters:

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 has 3 parameters and is fine. tooMany and arrow both have 4+ parameters and are matched.

Try this example in the Playground

CST nodes can be nested in the pattern to express structural constraints. Here we match a JsCatchClause whose body field is a JsBlockStatement with an empty statements list ([]). This reads almost like a type assertion: “a catch clause containing a block with no statements.”

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);
}

The first catch block (line 3) is empty and matched. The second one has a statement inside and is not.

Try this example in the Playground

Some CST nodes are specific to TypeScript. The TsAnyType node represents the any keyword wherever it appears as a type annotation. By matching this node directly, you catch every occurrence — in variable declarations, function parameters, return types, and generic arguments.

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;

Every any annotation on lines 1-3 is matched. The unknown on line 4 is a different type and is not affected.

Try this example in the Playground

Since let is a keyword rather than a syntax node, we match JsVariableStatement() and filter with a regex to select only statements whose text starts with 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;

Both let statements (lines 1 and 2) are matched. The const on line 3 is not — its text starts with const, so the regex let.* doesn’t match.

Try this example in the Playground

GritQL snippet patterns work inside JSX. Here we match the dangerouslySetInnerHTML prop regardless of the element it’s on or the value passed to it:

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

Matched — any element using the prop:

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

Not matched — no dangerouslySetInnerHTML:

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

Try this example in the Playground

The same approach works for banning inline style props. This encourages the use of CSS classes or CSS-in-JS solutions instead of inline styles:

`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"
)
}

Matched — inline style prop:

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

Not matched — using className instead:

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

Try this example in the Playground


By default, GritQL patterns target JavaScript. The engine biome(1.0) and language css directives at the top of a .grit file switch to Biome’s CSS syntax tree. The !important modifier is represented as a CssDeclarationImportant() node, so we use contains to find any declaration that includes it:

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;
}

Lines 2 and 6 contain !important declarations and are matched.

Try this example in the Playground

Ban hardcoded colors — use CSS custom properties

Section titled “Ban hardcoded colors — use CSS custom properties”

Regex patterns use the r"..." syntax. They match against the text content of a node rather than its syntactic structure. This is useful for matching values like hex color codes that don’t have a dedicated syntax node.

Here we use a regex to match any hex color value in color declarations:

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;
}

Lines 2 and 6 use hardcoded hex colors and are matched. The var() reference on line 3 is not a hex value and passes.

Try this example in the Playground

To also catch rgb() and hsl() functions, combine multiple regex patterns with 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"
)
}

Matched — hex, rgb(), and hsl() values:

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

The var() references on lines 3 and 12 don’t match any of the regex patterns and pass.

Try this example in the Playground

A top-level or lists alternative snippet patterns. Each arm matches independently, so you can ban multiple CSS properties by listing them explicitly:

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;
}

float on line 2 and clear on line 6 are matched. The .modern rule uses display: grid which is not in the banned list.

Try this example in the Playground


The language json directive (used with engine biome(1.0)) targets JSON files. Since JSON snippets with metavariables aren’t supported, use the CST node JsonMemberName() to match any key. Combined with regex and or, this lets you enforce naming conventions.

Here we flag any key that contains an underscore or starts with an uppercase letter — both violate 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) and UserAge (PascalCase) are matched by the regex alternatives. userName and email are valid camelCase and not matched.

Try this example in the Playground


Section titled “Combine multiple related rules in one file”

You can group multiple independent rules into a single .grit file using a top-level or. Each arm has its own pattern, conditions, and diagnostic. The where clause can be placed inside each arm independently, giving each rule its own severity and message.

Here we combine three debug-related checks into one plugin:

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");

Lines 1, 2, 3, and 5 are matched by different arms of the or. Line 4 (console.error) is not in the log, debug, trace list and passes.

Try this example in the Playground


Several examples above use Biome’s CST node names like JsConditionalExpression or TsAnyType. Here’s how to find the right node name for the code you want to match.

  1. Open the Biome Playground.
  2. Paste or type the code snippet you want to match.
  3. Switch to the Syntax tab in the output panel on the right.
  4. The syntax tree is displayed with every node labeled by its type name. Expand nodes to see their children and fields.
  5. Use the node name you find in your GritQL pattern: NodeName() for any instance, or NodeName(field = ...) to match specific children.

Here are some frequently useful Biome CST node names for JavaScript/TypeScript:

Node NameMatches
JsIfStatementif (...) { ... }
JsConditionalExpressiona ? b : c
JsForStatementfor (...; ...; ...) { ... }
JsForOfStatementfor (... of ...) { ... }
JsCallExpressionfn(), obj.method()
JsNewExpressionnew Foo()
JsArrowFunctionExpression() => { ... }
JsFunctionDeclarationfunction foo() { ... }
JsCatchClausecatch (e) { ... }
JsBlockStatement{ ... } (block of statements)
JsFormalParameterA single function parameter
JsParametersThe parameter list (a, b, c)
JsVariableDeclarationconst x = 1, let y = 2
TsAnyType: any type annotation
TsTypeAliastype Foo = ...
TsInterfaceDeclarationinterface Foo { ... }
JsxElement<div>...</div>
JsxSelfClosingElement<img />
JsxAttributeclassName="test", disabled

For CSS:

Node NameMatches
CssDeclarationWithSemicolonproperty: value;
CssComplexSelectordiv > .class

When using CST node names, your .grit file should include the engine and language directives:

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

The engine biome(1.0) directive tells GritQL to use Biome’s syntax tree (as opposed to Tree-sitter’s). The language directive specifies which language grammar to match against — without it, JavaScript is assumed.