feat(rome_js_analyze): noUnnecessaryContinue
MODIFY
crates/rome_js_analyze/src/analyzers.rs
crates/rome_js_analyze/src/analyzers.rs
@@ -20,6 +20,8 @@ mod no_negation_else;
pub(crate) use no_negation_else::NoNegationElse;
mod no_sparse_array;
pub(crate) use no_sparse_array::NoSparseArray;
+mod no_unnecessary_continue;
+pub(crate) use no_unnecessary_continue::NoUnnecessaryContinue;
mod no_unsafe_negation;
pub(crate) use no_unsafe_negation::NoUnsafeNegation;
mod no_unused_template_literal;

ADD
crates/rome_js_analyze/src/analyzers/no_unnecessary_continue.rs
+use rome_analyze::{
+ context::RuleContext, declare_rule, ActionCategory, Ast, Rule, RuleCategory, RuleDiagnostic,
+};
+use rome_console::markup;
+use rome_diagnostics::Applicability;
+use rome_js_syntax::{
+ JsAnyRoot, JsContinueStatement, JsLabeledStatement, JsSyntaxKind, JsSyntaxNode,
+};
+use rome_rowan::{AstNode, SyntaxElement};
+
+use crate::JsRuleAction;
+
+declare_rule! {
+ /// Avoid using unnecessary `ContinueStatement`.
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ /// ```js,expect_diagnostic
+ /// loop: for (let i = 0; i < 5; i++) {
+ /// continue loop;
+ /// }
+ /// ```
+ /// ```js,expect_diagnostic
+ /// while (i--) {
+ /// continue;
+ /// }
+ /// ```
+ /// ```js,expect_diagnostic
+ /// while (1) {
+ /// continue;
+ /// }
+ /// ```
+ /// ```js,expect_diagnostic
+ /// for (let i = 0; i < 10; i++) {
+ /// if (i > 5) {
+ /// console.log("foo");
+ /// continue;
+ /// } else if (i >= 5 && i < 8) {
+ /// console.log("test");
+ /// } else {
+ /// console.log("test");
+ /// }
+ /// }
+ /// ```
+ /// ```js,expect_diagnostic
+ /// for (let i = 0; i < 9; i++) {
+ /// continue;
+ /// }
+ /// ```
+ ///
+ /// ```js, expect_diagnostic
+ /// test2: do {
+ /// continue test2;
+ /// } while (true);
+ /// ```
+ ///
+ /// ### Valid
+ /// ```js
+ /// while (i) {
+ /// if (i > 5) {
+ /// continue;
+ /// }
+ /// console.log(i);
+ /// i--;
+ /// }
+ ///
+ /// loop: while (1) {
+ /// forLoop: for (let i = 0; i < 5; i++) {
+ /// if (someCondition) {
+ /// continue loop;
+ /// }
+ /// }
+ /// }
+ /// ```
+ pub(crate) NoUnnecessaryContinue = "noUnnecessaryContinue"
+}
+
+impl Rule for NoUnnecessaryContinue {
+ const CATEGORY: RuleCategory = RuleCategory::Lint;
+
+ type Query = Ast<JsContinueStatement>;
+ type State = ();
+ type Signals = Option<Self::State>;
+
+ fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
+ let Ast(node) = ctx.query();
+ is_continue_un_necessary(node)?.then(|| ())
+ }
+
+ fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
+ let Ast(node) = ctx.query();
+ Some(RuleDiagnostic::warning(
+ node.range(),
+ markup! {
+ "Unnecessary continue statement"
+ },
+ ))
+ }
+
+ fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
+ let Ast(node) = ctx.query();
+ let root_syntax = ctx.root().into_syntax();
+ let syntax = node.clone().into_syntax();
+ // Get parent of `ContinueStatement` SyntaxNode .
+ let parent = syntax.parent()?;
+ // Find index of `ContinueStatement` SyntaxNode in parent.
+ let wrapper_syntax_node = SyntaxElement::Node(syntax);
+ let index = parent
+ .children_with_tokens()
+ .position(|n| n == wrapper_syntax_node)?;
+ // Remove `ContinueStatement` SyntaxNode from parent.
+ let next_parent = parent.clone().splice_slots(index..=index, []);
+ let next_root_syntax = root_syntax.replace_child(
+ SyntaxElement::Node(parent),
+ SyntaxElement::Node(next_parent),
+ )?;
+ let root = JsAnyRoot::cast(next_root_syntax)?;
+ Some(JsRuleAction {
+ category: ActionCategory::QuickFix,
+ applicability: Applicability::MaybeIncorrect,
+ message: markup! { "Delete the unnecessary continue statement" }.to_owned(),
+ root,
+ })
+ }
+}
+
+fn is_continue_un_necessary(node: &JsContinueStatement) -> Option<bool> {
+ use rome_js_syntax::JsSyntaxKind::*;
+ let syntax = node.clone().into_syntax();
+ let mut current = syntax.clone();
+ let mut ancestors = vec![];
+ let mut loop_stmt = None;
+ while let Some(parent) = current.parent() {
+ if !matches!(
+ parent.kind(),
+ JS_FOR_IN_STATEMENT
+ | JS_FOR_OF_STATEMENT
+ | JS_FOR_STATEMENT
+ | JS_WHILE_STATEMENT
+ | JS_DO_WHILE_STATEMENT
+ ) {
+ ancestors.push(parent.clone());
+ } else {
+ loop_stmt = Some(parent);
+ break;
+ }
+ current = parent;
+ }
+ let loop_stmt = loop_stmt?;
+ if ancestors.is_empty() {
+ return Some(true);
+ }
+ Some(
+ is_continue_last_statement(&ancestors, syntax.clone()).unwrap_or(false)
+ && contains_parent_loop_label(syntax.clone(), loop_stmt).unwrap_or(false)
+ && is_continue_inside_last_ancestors(&ancestors, syntax).unwrap_or(false),
+ )
+}
+
+fn is_continue_last_statement(ancestors: &[JsSyntaxNode], syntax: JsSyntaxNode) -> Option<bool> {
+ let first_node = ancestors.first()?;
+ if first_node.kind() == JsSyntaxKind::JS_STATEMENT_LIST {
+ Some(first_node.children().last()? == syntax)
+ } else {
+ None
+ }
+}
+
+/// return true if continue label is undefined or equal to its parent's looplabel
+fn contains_parent_loop_label(node: JsSyntaxNode, loop_stmt: JsSyntaxNode) -> Option<bool> {
+ let continue_stmt = JsContinueStatement::cast(node)?;
+ let continue_stmt_label = continue_stmt.label_token();
+ if let Some(label) = continue_stmt_label {
+ let label_stmt = JsLabeledStatement::cast(loop_stmt.parent()?)?;
+ Some(label_stmt.label_token().ok()?.text_trimmed() == label.text_trimmed())
+ } else {
+ Some(true)
+ }
+}
+
+fn is_continue_inside_last_ancestors(
+ ancestors: &[JsSyntaxNode],
+ syntax: JsSyntaxNode,
+) -> Option<bool> {
+ let len = ancestors.len();
+ for ancestor_window in ancestors.windows(2).rev() {
+ let parent = &ancestor_window[1];
+ let child = &ancestor_window[0];
+ if parent.kind() == JsSyntaxKind::JS_STATEMENT_LIST {
+ let body = parent.children();
+ let last_body_node = body.last()?;
+ if !((len == 1 && last_body_node == syntax) || (len > 1 && &last_body_node == child)) {
+ return Some(false);
+ }
+ }
+ }
+ Some(true)
+}

MODIFY
crates/rome_js_analyze/src/registry.rs
crates/rome_js_analyze/src/registry.rs
@@ -41,6 +41,9 @@ where
if filter.match_rule::<NoSparseArray>() {
rules.push::<NoSparseArray>();
}
+ if filter.match_rule::<NoUnnecessaryContinue>() {
+ rules.push::<NoUnnecessaryContinue>();
+ }
if filter.match_rule::<NoUnsafeNegation>() {
rules.push::<NoUnsafeNegation>();
}

ADD
crates/rome_js_analyze/tests/specs/noUnnecessaryContinue.js
+// invalid
+loop: for (let i = 0; i < 5; i++) {
+ continue loop;
+}
+while (i--) {
+ continue;
+}
+while (1) {
+ continue;
+}
+for (let i = 0; i < 10; i++) {
+ if (i > 5) {
+ console.log("foo");
+ continue;
+ } else if (i >= 5 && i < 8) {
+ console.log("test");
+ } else {
+ console.log("test");
+ }
+}
+for (let i = 0; i < 9; i++) {
+ continue;
+}
+
+test: for (let i = 0; i < 9; i++) continue test;
+
+test2: do {
+ continue test2;
+} while (true);
+// valid
+test3: do {
+ if (i < 2) {
+ continue test3;
+ }
+ i++;
+} while (true);
+while (i) {
+ if (i > 5) {
+ continue;
+ }
+ console.log(i);
+ i--;
+}
+while (i) {
+ continue;
+ console.log(i);
+}
+while (condition) {
+ if (conditionZ) {
+ if (conditionX) {
+ console.log("log");
+ continue;
+ }
+ console.log("log");
+ if (conditionY) {
+ console.log("log");
+ }
+ }
+}
+loop: while (1) {
+ forLoop: for (let i = 0; i < 5; i++) {
+ if (someCondition) {
+ continue loop;
+ }
+ }
+}
+loop: for (let i = 0; i < 10; i++) {
+ for (let j = 0; j < byteLength; j++) {
+ if (condition) {
+ continue loop;
+ }
+ }
+}

MODIFY
website/src/docs/lint/rules/index.md
website/src/docs/lint/rules/index.md
@@ -84,6 +84,13 @@ Disallow negation in the condition of an <code>if</code> statement if it has an
Disallow sparse arrays
</div>
<div class="rule">
+<h3 data-toc-exclude id="noUnnecessaryContinue">
+ <a href="/docs/lint/rules/noUnnecessaryContinue">noUnnecessaryContinue</a>
+ <a class="header-anchor" href="#noUnnecessaryContinue"></a>
+</h3>
+Avoid using unnecessary <code>ContinueStatement</code>.
+</div>
+<div class="rule">
<h3 data-toc-exclude id="noUnsafeNegation">
<a href="/docs/lint/rules/noUnsafeNegation">noUnsafeNegation</a>
<a class="header-anchor" href="#noUnsafeNegation"></a>

feat(rome_js_analyze): noUnnecessaryContinue (#2796)