Skip to content
This repository was archived by the owner on Aug 31, 2023. It is now read-only.

Commit d3bc27a

Browse files
committed
add documentation
1 parent 12fc36b commit d3bc27a

File tree

4 files changed

+106
-3
lines changed

4 files changed

+106
-3
lines changed

crates/rome_analyze/CONTRIBUTING.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,98 @@ If this the rule can retrieve its option with
387387
let options = ctx.options();
388388
```
389389

390+
#### Custom Visitors
391+
392+
Some lint rules may need to deeply inspect the child nodes of a query match
393+
before deciding on whether they should emit a signal or not. These rules can be
394+
inefficient to implement using the query system, as they will lead to redundant
395+
traversal passes being executed over the same syntax tree. To make this more
396+
efficient, you can implement a custom `Queryable` type and and associated
397+
`Visitor` to emit it as part of the analyzer's main traversal pass. As an
398+
example, here's how this could be done to implement the `useYield` rule:
399+
400+
```rust,ignore
401+
// First, create a visitor struct that holds a stack of function syntax nodes and booleans
402+
#[derive(Default)]
403+
struct MissingYieldVisitor {
404+
stack: Vec<(AnyFunctionLike, bool)>,
405+
}
406+
407+
// Implement the `Visitor` trait for this struct
408+
impl Visitor for MissingYieldVisitor {
409+
fn visit(
410+
&mut self,
411+
event: &WalkEvent<SyntaxNode<Self::Language>>,
412+
ctx: VisitorContext<Self::Language>,
413+
) {
414+
match event {
415+
WalkEvent::Enter(node) => {
416+
// When the visitor enters a function node, push a new entry on the stack
417+
if let Some(node) = AnyFunctionLike::cast_ref(node) {
418+
self.stack.push((node, false));
419+
}
420+
421+
if let Some((_, has_yield)) = self.stack.last_mut() {
422+
// When the visitor enters a `yield` expression, set the
423+
// `has_yield` flag for the top entry on the stack to `true`
424+
if JsYieldExpression::can_cast(node.kind()) {
425+
*has_yield = true;
426+
}
427+
}
428+
}
429+
WalkEvent::Leave(node) => {
430+
// When the visitor exits a function, if it matches the node of the top-most
431+
// entry of the stack and the `has_yield` flag is `false`, emit a query match
432+
if let Some(exit_node) = AnyFunctionLike::cast_ref(node) {
433+
if let Some((enter_node, has_yield)) = self.stack.pop() {
434+
assert_eq!(enter_node, exit_node);
435+
if !has_yield {
436+
ctx.match_query(MissingYield(enter_node));
437+
}
438+
}
439+
}
440+
}
441+
}
442+
}
443+
}
444+
445+
// Declare a query match struct type containing a JavaScript function node
446+
struct MissingYield(AnyFunctionLike);
447+
448+
// Implement the `Queryable` trait for this type
449+
impl Queryable for MissingYield {
450+
// `Input` is the type that `ctx.match_query()` is called with in the visitor
451+
type Input = Self;
452+
// `Output` if the type that `ctx.query()` will return in the rule
453+
type Output = AnyFunctionLike;
454+
455+
fn build_visitor(
456+
analyzer: &mut impl AddVisitor<Self::Language>,
457+
_: &<Self::Language as Language>::Root,
458+
) {
459+
// Register our custom visitor to run in the `Syntax` phase
460+
analyzer.add_visitor(Phases::Syntax, MissingYieldVisitor::default());
461+
}
462+
463+
// Extract the output object from the input type
464+
fn unwrap_match(services: &ServiceBag, query: &Self::Input) -> Self::Output {
465+
query.0.clone()
466+
}
467+
}
468+
469+
impl Rule for UseYield {
470+
// Declare the custom `MissingYield` queryable as the rule's query
471+
type Query = MissingYield;
472+
473+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
474+
// Read the function's root node from the queryable output
475+
let query: &AnyFunctionLike = ctx.query();
476+
477+
// ...
478+
}
479+
}
480+
```
481+
390482
# Using Just
391483

392484
It is also possible to do all the steps above using our `Just` automation. For example, we can create

crates/rome_analyze/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ where
107107
}
108108
}
109109

110+
/// Registers a [Visitor] to be executed as part of a given `phase` of the analyzer run
110111
pub fn add_visitor(
111112
&mut self,
112113
phase: Phases,

crates/rome_analyze/src/query.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,26 @@ use rome_rowan::{Language, SyntaxKindSet, TextRange};
44

55
use crate::{registry::Phase, services::FromServices, Phases, ServiceBag, Visitor};
66

7-
/// Trait implemented for all types, for example lint rules can query them to emit diagnostics or code actions.
7+
/// Trait implemented for types that lint rules can query in order to emit diagnostics or code actions.
88
pub trait Queryable: Sized {
99
type Input: QueryMatch;
1010
type Output;
1111

1212
type Language: Language;
1313
type Services: FromServices + Phase;
1414

15+
/// Registers one or more [Visitor] that will emit `Self::Input` query
16+
/// matches during the analyzer run
1517
fn build_visitor(
1618
analyzer: &mut impl AddVisitor<Self::Language>,
1719
root: &<Self::Language as Language>::Root,
1820
);
1921

22+
/// Returns the type of query matches this [Queryable] expects as inputs
23+
///
24+
/// Unless your custom queryable needs to match on a specific
25+
/// [SyntaxKindSet], you should not override the default implementation of
26+
/// this method
2027
fn key() -> QueryKey<Self::Language> {
2128
QueryKey::TypeId(TypeId::of::<Self::Input>())
2229
}
@@ -29,7 +36,9 @@ pub trait Queryable: Sized {
2936
fn unwrap_match(services: &ServiceBag, query: &Self::Input) -> Self::Output;
3037
}
3138

39+
/// This trait is implemented on all types that supports the registration of [Visitor]
3240
pub trait AddVisitor<L: Language> {
41+
/// Registers a [Visitor] for a given `phase`
3342
fn add_visitor<F, V>(&mut self, phase: Phases, visitor: F)
3443
where
3544
F: FnOnce() -> V,

crates/rome_analyze/src/registry.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ impl<L: Language + Default> RuleRegistry<L> {
130130
/// Holds a collection of rules for each phase.
131131
#[derive(Default)]
132132
struct PhaseRules<L: Language> {
133+
/// Maps the [TypeId] of known query matches types to the corresponding list of rules
133134
type_rules: FxHashMap<TypeId, TypeRules<L>>,
134135
/// Holds a list of states for all the rules in this phase
135136
rule_states: Vec<RuleState<L>>,
@@ -188,7 +189,7 @@ impl<L: Language + Default + 'static> RegistryVisitor<L> for RuleRegistryBuilder
188189
.type_rules
189190
.entry(TypeId::of::<SyntaxNode<L>>())
190191
.or_insert_with(|| TypeRules::SyntaxRules { rules: Vec::new() })
191-
else { unreachable!() };
192+
else { unreachable!("the SyntaxNode type has already been registered as a TypeRules instead of a SyntaxRules, this is generally caused by an implementation of `Queryable::key` returning a `QueryKey::TypeId` with the type ID of `SyntaxNode`") };
192193

193194
// Iterate on all the SyntaxKind variants this node can match
194195
for kind in key.iter() {
@@ -214,7 +215,7 @@ impl<L: Language + Default + 'static> RegistryVisitor<L> for RuleRegistryBuilder
214215
.type_rules
215216
.entry(key)
216217
.or_insert_with(|| TypeRules::TypeRules { rules: Vec::new() })
217-
else { unreachable!() };
218+
else { unreachable!("the query type has already been registered as a SyntaxRules instead of a TypeRules, this is generally ca used by an implementation of `Queryable::key` returning a `QueryKey::TypeId` with the type ID of `SyntaxNode`") };
218219

219220
rules.push(rule);
220221
}

0 commit comments

Comments
 (0)