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

Commit ab18beb

Browse files
authored
feat(rome_js_analyze): useYield rule (#4037)
Closes #3994
1 parent 15cee37 commit ab18beb

File tree

11 files changed

+641
-5
lines changed

11 files changed

+641
-5
lines changed

crates/rome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ define_dategories! {
9393
"lint/nursery/useValidForDirection": "https://docs.rome.tools/lint/rules/useValidForDirection",
9494
"lint/nursery/useHookAtTopLevel": "https://docs.rome.tools/lint/rules/useHookAtTopLevel",
9595
"lint/nursery/noDuplicateJsxProps": "https://docs.rome.tools/lint/rules/noDuplicateJsxProps",
96+
"lint/nursery/useYield": "https://docs.rome.tools/lint/rules/useYield",
9697
// Insert new nursery rule here
9798

9899
// performance

crates/rome_js_analyze/src/analyzers/nursery.rs

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
use rome_analyze::context::RuleContext;
2+
use rome_analyze::{
3+
declare_rule, AddVisitor, Phases, QueryMatch, Queryable, Rule, RuleDiagnostic, ServiceBag,
4+
Visitor, VisitorContext,
5+
};
6+
use rome_console::markup;
7+
use rome_js_syntax::{
8+
AnyJsFunction, JsLanguage, JsMethodClassMember, JsMethodObjectMember, JsStatementList,
9+
JsYieldExpression, TextRange, WalkEvent,
10+
};
11+
use rome_rowan::{declare_node_union, AstNode, AstNodeList, Language, SyntaxNode};
12+
13+
declare_rule! {
14+
/// Require generator functions to contain `yield`.
15+
///
16+
/// This rule generates warnings for generator functions that do not have the `yield` keyword.
17+
///
18+
/// Source: [require-await](https://eslint.org/docs/latest/rules/require-yield).
19+
///
20+
/// ## Examples
21+
///
22+
/// ### Invalid
23+
///
24+
/// ```js,expect_diagnostic
25+
/// function* foo() {
26+
/// return 10;
27+
/// }
28+
/// ```
29+
///
30+
/// ### Valid
31+
/// ```js
32+
/// function* foo() {
33+
/// yield 5;
34+
/// return 10;
35+
/// }
36+
///
37+
/// function foo() {
38+
/// return 10;
39+
/// }
40+
///
41+
/// // This rule does not warn on empty generator functions.
42+
/// function* foo() { }
43+
/// ```
44+
pub(crate) UseYield {
45+
version: "next",
46+
name: "useYield",
47+
recommended: true,
48+
}
49+
}
50+
51+
declare_node_union! {
52+
pub(crate) AnyFunctionLike = AnyJsFunction | JsMethodObjectMember | JsMethodClassMember
53+
}
54+
55+
impl AnyFunctionLike {
56+
fn is_generator(&self) -> bool {
57+
match self {
58+
AnyFunctionLike::AnyJsFunction(any_js_function) => any_js_function.is_generator(),
59+
AnyFunctionLike::JsMethodClassMember(method_class_member) => {
60+
method_class_member.star_token().is_some()
61+
}
62+
AnyFunctionLike::JsMethodObjectMember(method_obj_member) => {
63+
method_obj_member.star_token().is_some()
64+
}
65+
}
66+
}
67+
68+
fn statements(&self) -> Option<JsStatementList> {
69+
Some(match self {
70+
AnyFunctionLike::AnyJsFunction(any_js_function) => any_js_function
71+
.body()
72+
.ok()?
73+
.as_js_function_body()?
74+
.statements(),
75+
AnyFunctionLike::JsMethodClassMember(method_class_member) => {
76+
method_class_member.body().ok()?.statements()
77+
}
78+
AnyFunctionLike::JsMethodObjectMember(method_obj_member) => {
79+
method_obj_member.body().ok()?.statements()
80+
}
81+
})
82+
}
83+
}
84+
85+
#[derive(Default)]
86+
struct MissingYieldVisitor {
87+
/// Vector to hold a function node and a boolean indicating whether the function
88+
/// contains an `yield` expression or not.
89+
stack: Vec<(AnyFunctionLike, bool)>,
90+
}
91+
92+
impl Visitor for MissingYieldVisitor {
93+
type Language = JsLanguage;
94+
95+
fn visit(
96+
&mut self,
97+
event: &WalkEvent<SyntaxNode<Self::Language>>,
98+
mut ctx: VisitorContext<Self::Language>,
99+
) {
100+
match event {
101+
WalkEvent::Enter(node) => {
102+
// When the visitor enters a function node, push a new entry on the stack
103+
if let Some(node) = AnyFunctionLike::cast_ref(node) {
104+
self.stack.push((node, false));
105+
}
106+
107+
if JsYieldExpression::can_cast(node.kind()) {
108+
// When the visitor enters a `yield` expression, set the
109+
// `has_yield` flag for the top entry on the stack to `true`
110+
if let Some((_, has_yield)) = self.stack.last_mut() {
111+
*has_yield = true;
112+
}
113+
}
114+
}
115+
WalkEvent::Leave(node) => {
116+
// When the visitor exits a function, if it matches the node of the top-most
117+
// entry of the stack and the `has_yield` flag is `false`, emit a query match
118+
if let Some(exit_node) = AnyFunctionLike::cast_ref(node) {
119+
if let Some((enter_node, has_yield)) = self.stack.pop() {
120+
if enter_node == exit_node && !has_yield {
121+
ctx.match_query(MissingYield(enter_node));
122+
}
123+
}
124+
}
125+
}
126+
}
127+
}
128+
}
129+
130+
pub(crate) struct MissingYield(AnyFunctionLike);
131+
132+
impl QueryMatch for MissingYield {
133+
fn text_range(&self) -> TextRange {
134+
self.0.range()
135+
}
136+
}
137+
138+
impl Queryable for MissingYield {
139+
type Input = Self;
140+
type Language = JsLanguage;
141+
type Output = AnyFunctionLike;
142+
type Services = ();
143+
144+
fn build_visitor(
145+
analyzer: &mut impl AddVisitor<Self::Language>,
146+
_: &<Self::Language as Language>::Root,
147+
) {
148+
analyzer.add_visitor(Phases::Syntax, MissingYieldVisitor::default);
149+
}
150+
151+
fn unwrap_match(_: &ServiceBag, query: &Self::Input) -> Self::Output {
152+
query.0.clone()
153+
}
154+
}
155+
156+
impl Rule for UseYield {
157+
type Query = MissingYield;
158+
type State = ();
159+
type Signals = Option<Self::State>;
160+
type Options = ();
161+
162+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
163+
let query = ctx.query();
164+
165+
// Don't emit diagnostic for non-generators or generators with an empty body
166+
if !query.is_generator() || query.statements()?.is_empty() {
167+
return None;
168+
}
169+
170+
Some(())
171+
}
172+
173+
fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
174+
Some(RuleDiagnostic::new(
175+
rule_category!(),
176+
ctx.query().range(),
177+
markup! {"This generator function doesn't contain "<Emphasis>"yield"</Emphasis>"."},
178+
))
179+
}
180+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Valid
2+
function foo() {
3+
return 0;
4+
}
5+
6+
function* foo() {
7+
yield 0;
8+
}
9+
10+
function* foo() {}
11+
12+
(function* foo() {
13+
yield 0;
14+
})();
15+
16+
(function* foo() {})();
17+
18+
const obj = {
19+
*foo() {
20+
yield 0;
21+
},
22+
};
23+
24+
const obj = { *foo() {} };
25+
26+
class A {
27+
*foo() {
28+
yield 0;
29+
}
30+
}
31+
32+
class A {
33+
*foo() {}
34+
}
35+
36+
// Invalid
37+
function* foo() {
38+
return 0;
39+
}
40+
41+
(function* foo() {
42+
return 0;
43+
})();
44+
45+
const obj = {
46+
*foo() {
47+
return 0;
48+
},
49+
};
50+
51+
class A {
52+
*foo() {
53+
return 0;
54+
}
55+
}
56+
57+
function* foo() {
58+
function* bar() {
59+
yield 0;
60+
}
61+
}
62+
63+
function* foo() {
64+
function* bar() {
65+
return 0;
66+
}
67+
yield 0;
68+
}
69+
70+
function* foo() {
71+
function* bar() {
72+
yield 0;
73+
}
74+
return 0;
75+
}
76+
77+
function* foo() {
78+
const obj = {
79+
*bar() {
80+
return 0;
81+
},
82+
};
83+
84+
class A {
85+
*baz() {
86+
return 0;
87+
}
88+
}
89+
90+
if (a === 'a') {
91+
yield 0;
92+
}
93+
}

0 commit comments

Comments
 (0)