From 2d61a95641ae4248c68b542a5bf56e83239f2235 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Sun, 19 May 2024 09:40:56 +0100 Subject: [PATCH] docs: add more tips around the analyzer (#2913) Co-authored-by: Ze-Zheng Wu --- crates/biome_analyze/CONTRIBUTING.md | 178 ++++++++++++++++++++------- 1 file changed, 131 insertions(+), 47 deletions(-) diff --git a/crates/biome_analyze/CONTRIBUTING.md b/crates/biome_analyze/CONTRIBUTING.md index 4347198acb75..a5d0d52fa211 100644 --- a/crates/biome_analyze/CONTRIBUTING.md +++ b/crates/biome_analyze/CONTRIBUTING.md @@ -74,7 +74,7 @@ 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: @@ -82,7 +82,7 @@ Let's say we want to create a new rule called `myRuleName`, which uses the seman 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, _state: &Self::State) -> Option {} @@ -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, _state: &Self::State) -> Option {} @@ -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, @@ -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)] @@ -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; type State = Fix; @@ -193,7 +193,7 @@ impl Rule for MyRule { A rule can retrieve its options with: -```rust,ignore +```rust let options = ctx.options(); ``` @@ -201,7 +201,7 @@ The compiler should warn you that `MyRuleOptions` does not implement some requir 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)] @@ -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 @@ -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 @@ -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 { @@ -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 @@ -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;` 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; type State = (); @@ -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, _state: &Self::State) -> Option { + 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 @@ -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 { @@ -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"); ``` @@ -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`. @@ -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! {