Skip to content

Commit

Permalink
docs: add more tips around the analyzer (#2913)
Browse files Browse the repository at this point in the history
Co-authored-by: Ze-Zheng Wu <[email protected]>
  • Loading branch information
ematipico and Sec-ant authored May 19, 2024
1 parent 9a05b77 commit 2d61a95
Showing 1 changed file with 131 additions and 47 deletions.
178 changes: 131 additions & 47 deletions crates/biome_analyze/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,15 @@ Let's say we want to create a new rule called `myRuleName`, which uses the seman
just new-css-lintrule myRuleName
```

1. The `Ast` query type allows you to query the AST of a program.
1. The `CST` query type allows you to query the CST of a program.
1. The `State` type doesn't have to be used, so it can be considered optional. However, it has to be defined as `type State = ()`.
1. Implement the `run` function:

This function is called every time the analyzer finds a match for the query specified by the rule, and may return zero or more "signals".

1. Implement the `diagnostic` function. Follow the [pillars](#explain-a-rule-to-the-user):

```rust,ignore
```rust
impl Rule for UseAwesomeTricks {
// .. code
fn diagnostic(_ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {}
Expand All @@ -93,7 +93,7 @@ Let's say we want to create a new rule called `myRuleName`, which uses the seman

1. Implement the optional `action` function, if we are able to provide a code action:

```rust,ignore
```rust
impl Rule for UseAwesomeTricks {
// .. code
fn action(_ctx: &RuleContext<Self>, _state: &Self::State) -> Option<JsRuleAction> {}
Expand All @@ -102,7 +102,7 @@ Let's say we want to create a new rule called `myRuleName`, which uses the seman

It may return zero or one code action.
Rules can return a code action that can be **safe** or **unsafe**. If a rule returns a code action, you must add `fix_kind` to the macro `declare_rule`.
```rust,ignore
```rust
use biome_analyze::FixKind;
declare_rule!{
fix_kind: FixKind::Safe,
Expand Down Expand Up @@ -153,7 +153,7 @@ We would like to set the options in the `biome.json` configuration file:

The first step is to create the Rust data representation of the rule's options.

```rust,ignore
```rust
use biome_deserialize_macros::Deserializable;

#[derive(Clone, Debug, Default, Deserializable)]
Expand All @@ -180,7 +180,7 @@ for you.

With these types in place, you can set the associated type `Options` of the rule:

```rust,ignore
```rust
impl Rule for MyRule {
type Query = Semantic<JsCallExpression>;
type State = Fix;
Expand All @@ -193,15 +193,15 @@ impl Rule for MyRule {

A rule can retrieve its options with:

```rust,ignore
```rust
let options = ctx.options();
```

The compiler should warn you that `MyRuleOptions` does not implement some required types.
We currently require implementing _serde_'s traits `Deserialize`/`Serialize`.
You can simply use a derive macros:

```rust,ignore
```rust
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
Expand All @@ -227,53 +227,52 @@ pub enum Behavior {

Below, there are many tips and guidelines on how to create a lint rule using Biome infrastructure.

#### Navigating the CST

Then navigating the nodes and tokens of certain nodes, you will notice straight away that the majority of those methods will return a `Result` (`SyntaxResult`).

Generally, you will end up navigating the CST inside the `run` function, and this function will usually return an `Option` or a `Vec`.

- If the `run` function returns an `Option`, you're encouraged to transform `Result` into `Option` and use the try operator `?`. This will make your coding way easier:
```rust
fn run() -> Self::Signals {
let prev_val = js_object_member.value().ok()?;
}
```
- If the `run` function returns a `Vec`, you're encouraged to use the `let else` trick to reduce code branching:

```rust
fn run() -> Self::Signals {
let Ok(prev_val) = js_object_member.value() else {
return vec![]
};
}
```
#### `declare_rule`

#### Query multiple nodes
This macro is used to declare an analyzer rule type, and implement the [RuleMeta] trait for it.

There are times when you might need to query multiple nodes at once. Instead of querying the root of the CST, you can use the macro `declare_node_union!` to "join" multiple nodes into an `enum`:
The macro itself expects the following syntax:

```rust
use biome_rowan::{declare_node_union, AstNode};
use biome_js_syntax::{AnyJsFunction, JsMethodObjectMember, JsMethodClassMember};
use biome_analyze::declare_rule;

declare_node_union! {
pub AnyFunctionLike = AnyJsFunction | JsMethodObjectMember | JsMethodClassMember
declare_rule! {
/// Documentation
pub(crate) ExampleRule {
version: "next",
name: "myRuleName",
language: "js",
recommended: false,
}
}
```

When creating a new node like this, we internally prefix them with `Any*` and postfix them with `*Like`. This is our internal naming convention.
##### Biome lint rules inspired by other lint rules

The type `AnyFunctionLike` implements the trait `AstNode`, which means that it implements all methods such as `syntax`, `children`, etc.
If a **lint** rule is inspired by an existing rule from other ecosystems (ESLint, ESLint plugins, clippy, etc.), you can add a new metadata to the macro called `source`. Its value is `Source`, which is an `enum` that contains various variants.

#### `declare_rule`
If you're implementing a lint rule that matches the behaviour of the ESLint rule `no-debugger`, you'll use the variant `::ESLint` and pass the name of the rule:

This macro is used to declare an analyzer rule type, and implement the [RuleMeta] trait for it.
```rust
use biome_analyze::{declare_rule, Source};

The macro itself expect the following syntax:
declare_rule! {
/// Documentation
pub(crate) ExampleRule {
version: "next",
name: "myRuleName",
language: "js",
recommended: false,
source: Source::Eslint("no-debugger"),
}
}
```

```rust,ignore
use biome_analyze::declare_rule;
If the rule you're implementing has a different behaviour or option, you can add the `source_kind` metadata and use the `SourceKind::Inspired` type.

```rust
use biome_analyze::{declare_rule, Source, SourceKind};

declare_rule! {
/// Documentation
Expand All @@ -282,9 +281,12 @@ declare_rule! {
name: "myRuleName",
language: "js",
recommended: false,
source: Source::Eslint("no-debugger"),
source_kind: SourceKind::Inspired,
}
}
```
By default, `source_kind` is always `SourceKind::Same`.

#### Category Macro

Expand All @@ -296,7 +298,7 @@ by dynamically parsing its string name has the advantage of statically
injecting the category at compile time and checking that it is correctly
registered to the `biome_diagnostics` library.

```rust,ignore
```rust
declare_rule! {
/// Documentation
pub(crate) ExampleRule {
Expand All @@ -318,6 +320,44 @@ impl Rule for ExampleRule {
}
```

#### Navigating the CST

When navigating the nodes and tokens of certain nodes, you will notice straight away that the majority of those methods will return a `Result` (`SyntaxResult`).

Generally, you will end up navigating the CST inside the `run` function, and this function will usually return an `Option` or a `Vec`.

- If the `run` function returns an `Option`, you're encouraged to transform the `Result` into an `Option` and use the try operator `?`. This will make your coding way easier:
```rust
fn run() -> Self::Signals {
let prev_val = js_object_member.value().ok()?;
}
```
- If the `run` function returns a `Vec`, you're encouraged to use the `let else` trick to reduce code branching:

```rust
fn run() -> Self::Signals {
let Ok(prev_val) = js_object_member.value() else {
return vec![]
};
}
```

#### Query multiple nodes

There are times when you might need to query multiple nodes at once. Instead of querying the root of the CST, you can use the macro `declare_node_union!` to "join" multiple nodes into an `enum`:

```rust
use biome_rowan::{declare_node_union, AstNode};
use biome_js_syntax::{AnyJsFunction, JsMethodObjectMember, JsMethodClassMember};

declare_node_union! {
pub AnyFunctionLike = AnyJsFunction | JsMethodObjectMember | JsMethodClassMember
}
```

When creating a new node like this, we internally prefix them with `Any*` and postfix them with `*Like`. This is our internal naming convention.

The type `AnyFunctionLike` implements the trait `AstNode`, which means that it implements all methods such as `syntax`, `children`, etc.

#### Semantic Model

Expand All @@ -337,7 +377,7 @@ for (let i = 0; i < array.length; i++) {
To get started we need to create a new rule using the semantic type `type Query = Semantic<JsForStatement>;`
We can now use the `ctx.model()` to get information about bindings in the for loop.

```rust,ignore
```rust
impl Rule for ForLoopCountReferences {
type Query = Semantic<JsForStatement>;
type State = ();
Expand Down Expand Up @@ -373,6 +413,50 @@ impl Rule for ForLoopCountReferences {
}
```

#### Code action

A rule can implement a code action. A code action provides to the final user the option to fix or change their code.

In a lint rule, for example, it signals an opportunity for the user to fix the diagnostic emitted by the rule.

First, you have to add a new metadata called `fix_kind`, its value is the `FixKind`.

```rust
use biome_analyze::{declare_rule, FixKind};

declare_rule! {
/// Documentation
pub(crate) ExampleRule {
version: "next",
name: "myRuleName",
language: "js",
recommended: false,
fix_kind: FixKind::Safe,
}
}
```

Then, you'll have to implement the `action` function of the trait `Rule` and return a `JsRuleAction`.

`JsRuleAction` needs, among other things, a `mutation` type, which you will use to store all additions, deletions and replacements that you will execute inside the action:

```rust
impl Rule for ExampleRule {
fn action(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<JsRuleAction> {
let mut mutation = ctx.root().begin();

Some(JsRuleAction::new(
ActionCategory::QuickFix,
ctx.metadata().applicability(),
markup! { "Remove the '"{name.text_trimmed()}"' element." }.to_owned(),
mutation,
))
}
}
```

The function `ctx.metadata().applicability()` will compute the `Applicability` type from the `fix_kind` value you provided at the beginning, inside the macro.

#### Custom Visitors

Some lint rules may need to deeply inspect the child nodes of a query match
Expand All @@ -383,7 +467,7 @@ efficient, you can implement a custom `Queryable` type and associated
`Visitor` to emit it as part of the analyzer's main traversal pass. As an
example, here's how this could be done to implement the `useYield` rule:

```rust,ignore
```rust
// First, create a visitor struct that holds a stack of function syntax nodes and booleans
#[derive(Default)]
struct MissingYieldVisitor {
Expand Down Expand Up @@ -484,7 +568,7 @@ A swift way to test your rule is to go inside the `biome_js_analyze/src/lib.rs`

Usually this test is ignored, so remove _comment_ the macro `#[ignore]` macro, change the `let SOURCE` variable to whatever source code you need to test. Then update the rule filter, and add your rule:

```rust,ignore
```rust
let rule_filter = RuleFilter::Rule("nursery", "useAwesomeTrick");
```

Expand Down Expand Up @@ -553,7 +637,7 @@ The documentation needs to adhere to the following rules:

Here's an example of how the documentation could look like:

```rust,ignore
```rust
use biome_analyze::declare_rule;
declare_rule! {
/// Disallow the use of `var`.
Expand Down Expand Up @@ -622,7 +706,7 @@ of deprecation can be multiple.

In order to do, the macro allows adding additional field to add the reason for deprecation

```rust,ignore
```rust
use biome_analyze::declare_rule;

declare_rule! {
Expand Down

0 comments on commit 2d61a95

Please sign in to comment.