GritQL Plugin Recipes
Ce contenu n’est pas encore disponible dans votre langue.
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.
JavaScript / TypeScript
Section titled “JavaScript / TypeScript”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
Ban forEach — prefer for...of
Section titled “Ban forEach — prefer for...of”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
No restricted imports
Section titled “No restricted imports”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
Ban new Date() — use a date library
Section titled “Ban new Date() — use a date library”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
Ban eval() and Function() constructor
Section titled “Ban eval() and Function() constructor”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
No nested ternaries
Section titled “No nested ternaries”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
Limit function parameters
Section titled “Limit function parameters”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
No empty catch blocks
Section titled “No empty catch blocks”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
Disallow any type annotation
Section titled “Disallow any type annotation”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
Enforce const over let
Section titled “Enforce const over let”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
Ban dangerouslySetInnerHTML
Section titled “Ban dangerouslySetInnerHTML”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
No inline style props
Section titled “No inline style props”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
Disallow !important
Section titled “Disallow !important”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
Disallow specific CSS properties
Section titled “Disallow specific CSS properties”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
Enforce JSON key naming conventions
Section titled “Enforce JSON key naming conventions”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
Advanced Patterns
Section titled “Advanced Patterns”Combine multiple related rules in one file
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
Discovering CST Node Names
Section titled “Discovering CST Node Names”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.
Using the Biome Playground
Section titled “Using the Biome Playground”- Open the Biome Playground.
- Paste or type the code snippet you want to match.
- Switch to the Syntax tab in the output panel on the right.
- The syntax tree is displayed with every node labeled by its type name. Expand nodes to see their children and fields.
- Use the node name you find in your GritQL pattern:
NodeName()for any instance, orNodeName(field = ...)to match specific children.
Common node names
Section titled “Common node names”Here are some frequently useful Biome CST node names for JavaScript/TypeScript:
| Node Name | Matches |
|---|---|
JsIfStatement | if (...) { ... } |
JsConditionalExpression | a ? b : c |
JsForStatement | for (...; ...; ...) { ... } |
JsForOfStatement | for (... of ...) { ... } |
JsCallExpression | fn(), obj.method() |
JsNewExpression | new Foo() |
JsArrowFunctionExpression | () => { ... } |
JsFunctionDeclaration | function foo() { ... } |
JsCatchClause | catch (e) { ... } |
JsBlockStatement | { ... } (block of statements) |
JsFormalParameter | A single function parameter |
JsParameters | The parameter list (a, b, c) |
JsVariableDeclaration | const x = 1, let y = 2 |
TsAnyType | : any type annotation |
TsTypeAlias | type Foo = ... |
TsInterfaceDeclaration | interface Foo { ... } |
JsxElement | <div>...</div> |
JsxSelfClosingElement | <img /> |
JsxAttribute | className="test", disabled |
For CSS:
| Node Name | Matches |
|---|---|
CssDeclarationWithSemicolon | property: value; |
CssComplexSelector | div > .class |
Header directives for CST patterns
Section titled “Header directives for CST patterns”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.
Copyright (c) 2023-present Biome Developers and Contributors.