feat(rome_js_analyze): `useYield` rule
Closes
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -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

MODIFY
crates/rome_js_analyze/src/analyzers/nursery.rs
crates/rome_js_analyze/src/analyzers/nursery.rs
@@ -37,4 +37,5 @@ mod use_exponentiation_operator;
mod use_is_nan;
mod use_media_caption;
mod use_numeric_literals;
-declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_access_key :: NoAccessKey , self :: no_assign_in_expressions :: NoAssignInExpressions , self :: no_banned_types :: NoBannedTypes , self :: no_comma_operator :: NoCommaOperator , self :: no_const_enum :: NoConstEnum , self :: no_constructor_return :: NoConstructorReturn , self :: no_distracting_elements :: NoDistractingElements , self :: no_duplicate_case :: NoDuplicateCase , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_duplicate_object_keys :: NoDuplicateObjectKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_extra_semicolons :: NoExtraSemicolons , self :: no_header_scope :: NoHeaderScope , self :: no_inner_declarations :: NoInnerDeclarations , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_non_null_assertion :: NoNonNullAssertion , self :: no_precision_loss :: NoPrecisionLoss , self :: no_redundant_alt :: NoRedundantAlt , self :: no_redundant_use_strict :: NoRedundantUseStrict , self :: no_self_compare :: NoSelfCompare , self :: no_setter_return :: NoSetterReturn , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_unreachable_super :: NoUnreachableSuper , self :: no_unsafe_finally :: NoUnsafeFinally , self :: no_unused_labels :: NoUnusedLabels , self :: no_useless_switch_case :: NoUselessSwitchCase , self :: no_void_type_return :: NoVoidTypeReturn , self :: no_with :: NoWith , self :: use_default_parameter_last :: UseDefaultParameterLast , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_enum_initializers :: UseEnumInitializers , self :: use_exponentiation_operator :: UseExponentiationOperator , self :: use_is_nan :: UseIsNan , self :: use_media_caption :: UseMediaCaption , self :: use_numeric_literals :: UseNumericLiterals ,] } }
+mod use_yield;
+declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_access_key :: NoAccessKey , self :: no_assign_in_expressions :: NoAssignInExpressions , self :: no_banned_types :: NoBannedTypes , self :: no_comma_operator :: NoCommaOperator , self :: no_const_enum :: NoConstEnum , self :: no_constructor_return :: NoConstructorReturn , self :: no_distracting_elements :: NoDistractingElements , self :: no_duplicate_case :: NoDuplicateCase , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_duplicate_object_keys :: NoDuplicateObjectKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_extra_semicolons :: NoExtraSemicolons , self :: no_header_scope :: NoHeaderScope , self :: no_inner_declarations :: NoInnerDeclarations , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_non_null_assertion :: NoNonNullAssertion , self :: no_precision_loss :: NoPrecisionLoss , self :: no_redundant_alt :: NoRedundantAlt , self :: no_redundant_use_strict :: NoRedundantUseStrict , self :: no_self_compare :: NoSelfCompare , self :: no_setter_return :: NoSetterReturn , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_unreachable_super :: NoUnreachableSuper , self :: no_unsafe_finally :: NoUnsafeFinally , self :: no_unused_labels :: NoUnusedLabels , self :: no_useless_switch_case :: NoUselessSwitchCase , self :: no_void_type_return :: NoVoidTypeReturn , self :: no_with :: NoWith , self :: use_default_parameter_last :: UseDefaultParameterLast , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_enum_initializers :: UseEnumInitializers , self :: use_exponentiation_operator :: UseExponentiationOperator , self :: use_is_nan :: UseIsNan , self :: use_media_caption :: UseMediaCaption , self :: use_numeric_literals :: UseNumericLiterals , self :: use_yield :: UseYield ,] } }

ADD
crates/rome_js_analyze/src/analyzers/nursery/use_yield.rs
+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>"."},
+ ))
+ }
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/useYield.js
+// 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;
+ }
+}

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -829,10 +829,12 @@ struct NurserySchema {
use_valid_aria_props: Option<RuleConfiguration>,
#[doc = "Ensure that the attribute passed to the lang attribute is a correct ISO language and/or country."]
use_valid_lang: Option<RuleConfiguration>,
+ #[doc = "Require generator functions to contain yield."]
+ use_yield: Option<RuleConfiguration>,
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 49] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 50] = [
"noAccessKey",
"noAssignInExpressions",
"noBannedTypes",
@@ -882,8 +884,9 @@ impl Nursery {
"useNumericLiterals",
"useValidAriaProps",
"useValidLang",
+ "useYield",
];
- const RECOMMENDED_RULES: [&'static str; 40] = [
+ const RECOMMENDED_RULES: [&'static str; 41] = [
"noAssignInExpressions",
"noBannedTypes",
"noClassAssign",
@@ -924,8 +927,9 @@ impl Nursery {
"useNumericLiterals",
"useValidAriaProps",
"useValidLang",
+ "useYield",
];
- const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 40] = [
+ const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 41] = [
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[1]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[2]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[3]),
@@ -966,6 +970,7 @@ impl Nursery {
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[46]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[47]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[48]),
+ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[49]),
];
pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) }
pub(crate) fn get_enabled_rules(&self) -> IndexSet<RuleFilter> {
@@ -992,7 +997,7 @@ impl Nursery {
pub(crate) fn is_recommended_rule(rule_name: &str) -> bool {
Self::RECOMMENDED_RULES.contains(&rule_name)
}
- pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 40] {
+ pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 41] {
Self::RECOMMENDED_RULES_AS_FILTERS
}
}

MODIFY
editors/vscode/configuration_schema.json
editors/vscode/configuration_schema.json
"type": "null"
}
]
+ },
+ "useYield": {
+ "description": "Require generator functions to contain yield.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
}
}
},

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -489,6 +489,10 @@ export interface Nursery {
* Ensure that the attribute passed to the lang attribute is a correct ISO language and/or country.
*/
useValidLang?: RuleConfiguration;
+ /**
+ * Require generator functions to contain yield.
+ */
+ useYield?: RuleConfiguration;
}
/**
* A list of rules that belong to this group
@@ -809,6 +813,7 @@ export type Category =
| "lint/nursery/useValidForDirection"
| "lint/nursery/useHookAtTopLevel"
| "lint/nursery/noDuplicateJsxProps"
+ | "lint/nursery/useYield"
| "lint/performance/noDelete"
| "lint/security/noDangerouslySetInnerHtml"
| "lint/security/noDangerouslySetInnerHtmlWithChildren"

MODIFY
npm/rome/configuration_schema.json
npm/rome/configuration_schema.json
"type": "null"
}
]
+ },
+ "useYield": {
+ "description": "Require generator functions to contain yield.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
}
}
},

MODIFY
website/src/pages/lint/rules/index.mdx
website/src/pages/lint/rules/index.mdx
@@ -782,5 +782,11 @@ Ensures that ARIA properties <code>aria-*</code> are all valid.
</h3>
Ensure that the attribute passed to the <code>lang</code> attribute is a correct ISO language and/or country.
</section>
+<section class="rule">
+<h3 data-toc-exclude id="useYield">
+ <a href="/lint/rules/useYield">useYield</a>
+</h3>
+Require generator functions to contain <code>yield</code>.
+</section>
</div>

feat(rome_js_analyze): `useYield` rule (#4037)

Closes https://github.com/rome/tools/issues/3994