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

feat(rome_js_analyze): useYield rule #4037

Merged
merged 10 commits into from
Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)>,
kaioduarte marked this conversation as resolved.
Show resolved Hide resolved
}

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