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

Commit

Permalink
feat(rome_js_analyze): useYield rule (#4037)
Browse files Browse the repository at this point in the history
Closes #3994
  • Loading branch information
kaioduarte authored Jan 5, 2023
1 parent 15cee37 commit ab18beb
Show file tree
Hide file tree
Showing 11 changed files with 641 additions and 5 deletions.
1 change: 1 addition & 0 deletions crates/rome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ define_dategories! {
"lint/nursery/useValidForDirection": "https://docs.rome.tools/lint/rules/useValidForDirection",
"lint/nursery/useHookAtTopLevel": "https://docs.rome.tools/lint/rules/useHookAtTopLevel",
"lint/nursery/noDuplicateJsxProps": "https://docs.rome.tools/lint/rules/noDuplicateJsxProps",
"lint/nursery/useYield": "https://docs.rome.tools/lint/rules/useYield",
// Insert new nursery rule here

// performance
Expand Down
3 changes: 2 additions & 1 deletion crates/rome_js_analyze/src/analyzers/nursery.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

180 changes: 180 additions & 0 deletions crates/rome_js_analyze/src/analyzers/nursery/use_yield.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use rome_analyze::context::RuleContext;
use rome_analyze::{
declare_rule, AddVisitor, Phases, QueryMatch, Queryable, Rule, RuleDiagnostic, ServiceBag,
Visitor, VisitorContext,
};
use rome_console::markup;
use rome_js_syntax::{
AnyJsFunction, JsLanguage, JsMethodClassMember, JsMethodObjectMember, JsStatementList,
JsYieldExpression, TextRange, WalkEvent,
};
use rome_rowan::{declare_node_union, AstNode, AstNodeList, Language, SyntaxNode};

declare_rule! {
/// Require generator functions to contain `yield`.
///
/// This rule generates warnings for generator functions that do not have the `yield` keyword.
///
/// Source: [require-await](https://eslint.org/docs/latest/rules/require-yield).
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// function* foo() {
/// return 10;
/// }
/// ```
///
/// ### Valid
/// ```js
/// function* foo() {
/// yield 5;
/// return 10;
/// }
///
/// function foo() {
/// return 10;
/// }
///
/// // This rule does not warn on empty generator functions.
/// function* foo() { }
/// ```
pub(crate) UseYield {
version: "next",
name: "useYield",
recommended: true,
}
}

declare_node_union! {
pub(crate) AnyFunctionLike = AnyJsFunction | JsMethodObjectMember | JsMethodClassMember
}

impl AnyFunctionLike {
fn is_generator(&self) -> bool {
match self {
AnyFunctionLike::AnyJsFunction(any_js_function) => any_js_function.is_generator(),
AnyFunctionLike::JsMethodClassMember(method_class_member) => {
method_class_member.star_token().is_some()
}
AnyFunctionLike::JsMethodObjectMember(method_obj_member) => {
method_obj_member.star_token().is_some()
}
}
}

fn statements(&self) -> Option<JsStatementList> {
Some(match self {
AnyFunctionLike::AnyJsFunction(any_js_function) => any_js_function
.body()
.ok()?
.as_js_function_body()?
.statements(),
AnyFunctionLike::JsMethodClassMember(method_class_member) => {
method_class_member.body().ok()?.statements()
}
AnyFunctionLike::JsMethodObjectMember(method_obj_member) => {
method_obj_member.body().ok()?.statements()
}
})
}
}

#[derive(Default)]
struct MissingYieldVisitor {
/// Vector to hold a function node and a boolean indicating whether the function
/// contains an `yield` expression or not.
stack: Vec<(AnyFunctionLike, bool)>,
}

impl Visitor for MissingYieldVisitor {
type Language = JsLanguage;

fn visit(
&mut self,
event: &WalkEvent<SyntaxNode<Self::Language>>,
mut ctx: VisitorContext<Self::Language>,
) {
match event {
WalkEvent::Enter(node) => {
// When the visitor enters a function node, push a new entry on the stack
if let Some(node) = AnyFunctionLike::cast_ref(node) {
self.stack.push((node, false));
}

if JsYieldExpression::can_cast(node.kind()) {
// When the visitor enters a `yield` expression, set the
// `has_yield` flag for the top entry on the stack to `true`
if let Some((_, has_yield)) = self.stack.last_mut() {
*has_yield = true;
}
}
}
WalkEvent::Leave(node) => {
// When the visitor exits a function, if it matches the node of the top-most
// entry of the stack and the `has_yield` flag is `false`, emit a query match
if let Some(exit_node) = AnyFunctionLike::cast_ref(node) {
if let Some((enter_node, has_yield)) = self.stack.pop() {
if enter_node == exit_node && !has_yield {
ctx.match_query(MissingYield(enter_node));
}
}
}
}
}
}
}

pub(crate) struct MissingYield(AnyFunctionLike);

impl QueryMatch for MissingYield {
fn text_range(&self) -> TextRange {
self.0.range()
}
}

impl Queryable for MissingYield {
type Input = Self;
type Language = JsLanguage;
type Output = AnyFunctionLike;
type Services = ();

fn build_visitor(
analyzer: &mut impl AddVisitor<Self::Language>,
_: &<Self::Language as Language>::Root,
) {
analyzer.add_visitor(Phases::Syntax, MissingYieldVisitor::default);
}

fn unwrap_match(_: &ServiceBag, query: &Self::Input) -> Self::Output {
query.0.clone()
}
}

impl Rule for UseYield {
type Query = MissingYield;
type State = ();
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let query = ctx.query();

// Don't emit diagnostic for non-generators or generators with an empty body
if !query.is_generator() || query.statements()?.is_empty() {
return None;
}

Some(())
}

fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
Some(RuleDiagnostic::new(
rule_category!(),
ctx.query().range(),
markup! {"This generator function doesn't contain "<Emphasis>"yield"</Emphasis>"."},
))
}
}
93 changes: 93 additions & 0 deletions crates/rome_js_analyze/tests/specs/nursery/useYield.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Valid
function foo() {
return 0;
}

function* foo() {
yield 0;
}

function* foo() {}

(function* foo() {
yield 0;
})();

(function* foo() {})();

const obj = {
*foo() {
yield 0;
},
};

const obj = { *foo() {} };

class A {
*foo() {
yield 0;
}
}

class A {
*foo() {}
}

// Invalid
function* foo() {
return 0;
}

(function* foo() {
return 0;
})();

const obj = {
*foo() {
return 0;
},
};

class A {
*foo() {
return 0;
}
}

function* foo() {
function* bar() {
yield 0;
}
}

function* foo() {
function* bar() {
return 0;
}
yield 0;
}

function* foo() {
function* bar() {
yield 0;
}
return 0;
}

function* foo() {
const obj = {
*bar() {
return 0;
},
};

class A {
*baz() {
return 0;
}
}

if (a === 'a') {
yield 0;
}
}
Loading

0 comments on commit ab18beb

Please sign in to comment.