feat(rome_js_analyze): `noUnusedLabels`
Closes
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -70,6 +70,7 @@ define_dategories! {
"lint/nursery/noStringCaseMismatch": "https://docs.rome.tools/lint/rules/noStringCaseMismatch",
"lint/nursery/noUnreachableSuper": "https://rome.tools/docs/lint/rules/noUnreachableSuper",
"lint/nursery/noUnsafeFinally": "https://docs.rome.tools/lint/rules/noUnsafeFinally",
+ "lint/nursery/noUnusedLabels": "https://docs.rome.tools/lint/rules/noUnusedLabels",
"lint/nursery/noUselessSwitchCase": "https://docs.rome.tools/lint/rules/noUselessSwitchCase",
"lint/nursery/noVar": "https://docs.rome.tools/lint/rules/noVar",
"lint/nursery/noVoidTypeReturn": "https://docs.rome.tools/lint/rules/noVoidTypeReturn",

MODIFY
crates/rome_js_analyze/src/analyzers/nursery.rs
crates/rome_js_analyze/src/analyzers/nursery.rs
@@ -26,6 +26,7 @@ mod no_setter_return;
mod no_string_case_mismatch;
mod no_unreachable_super;
mod no_unsafe_finally;
+mod no_unused_labels;
mod no_useless_switch_case;
mod no_void_type_return;
mod no_with;
@@ -36,4 +37,4 @@ 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_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 ,] } }
+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 ,] } }

ADD
crates/rome_js_analyze/src/analyzers/nursery/no_unused_labels.rs
+use rome_analyze::context::RuleContext;
+use rome_analyze::{
+ declare_rule, ActionCategory, AddVisitor, Phases, QueryMatch, Queryable, Rule, RuleDiagnostic,
+ ServiceBag, Visitor, VisitorContext,
+};
+use rome_console::markup;
+use rome_diagnostics::Applicability;
+use rome_js_syntax::{
+ JsBreakStatement, JsContinueStatement, JsLabeledStatement, JsLanguage, TextRange, WalkEvent,
+};
+
+use rome_rowan::{AstNode, BatchMutationExt, Language, NodeOrToken, SyntaxNode};
+use rustc_hash::FxHashMap;
+
+use crate::control_flow::AnyJsControlFlowRoot;
+use crate::JsRuleAction;
+
+declare_rule! {
+ /// Disallow unused labels.
+ ///
+ /// Labels that are declared and never used are most likely an error due to incomplete refactoring.
+ ///
+ /// Source: https://eslint.org/docs/latest/rules/no-unused-labels
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```cjs,expect_diagnostic
+ /// LOOP: for (const x of xs) {
+ /// if (x > 0) {
+ /// break;
+ /// }
+ /// f(x);
+ /// }
+ /// ```
+ ///
+ /// ### Valid
+ ///
+ /// ```cjs
+ /// LOOP: for (const x of xs) {
+ /// if (x > 0) {
+ /// break LOOP;
+ /// }
+ /// f(x);
+ /// }
+ /// ```
+ ///
+ pub(crate) NoUnusedLabels {
+ version: "next",
+ name: "noUnusedLabels",
+ recommended: true,
+ }
+}
+
+#[derive(Default)]
+struct UnusedLabelVisitor {
+ root_id: usize,
+ // Key = (root_id, label)
+ labels: FxHashMap<(usize, String), JsLabeledStatement>,
+}
+
+impl UnusedLabelVisitor {
+ fn insert(&mut self, label: String, label_stmt: JsLabeledStatement) {
+ self.labels.insert((self.root_id, label), label_stmt);
+ }
+
+ fn remove(&mut self, label: String) -> Option<JsLabeledStatement> {
+ self.labels.remove(&(self.root_id, label))
+ }
+}
+
+impl Visitor for UnusedLabelVisitor {
+ type Language = JsLanguage;
+
+ fn visit(
+ &mut self,
+ event: &WalkEvent<SyntaxNode<Self::Language>>,
+ mut ctx: VisitorContext<Self::Language>,
+ ) {
+ match event {
+ WalkEvent::Enter(node) => {
+ if AnyJsControlFlowRoot::cast_ref(node).is_some() {
+ self.root_id += 1;
+ } else if let Some(label_stmt) = JsLabeledStatement::cast_ref(node) {
+ if let Ok(label_tok) = label_stmt.label_token() {
+ self.insert(label_tok.text_trimmed().to_owned(), label_stmt);
+ }
+ } else if let Some(break_stmt) = JsBreakStatement::cast_ref(node) {
+ if let Some(label_tok) = break_stmt.label_token() {
+ self.remove(label_tok.text_trimmed().to_owned());
+ }
+ } else if let Some(continue_stmt) = JsContinueStatement::cast_ref(node) {
+ if let Some(label_tok) = continue_stmt.label_token() {
+ self.remove(label_tok.text_trimmed().to_owned());
+ }
+ }
+ }
+ WalkEvent::Leave(node) => {
+ if AnyJsControlFlowRoot::cast_ref(node).is_some() {
+ self.root_id -= 1;
+ } else if let Some(stmt_label) = JsLabeledStatement::cast_ref(node) {
+ if let Ok(label_tok) = stmt_label.label_token() {
+ let result = self.remove(label_tok.text_trimmed().to_owned());
+ if let Some(label_stmt) = result {
+ ctx.match_query(UnusedLabel(label_stmt));
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+pub(crate) struct UnusedLabel(JsLabeledStatement);
+
+impl QueryMatch for UnusedLabel {
+ fn text_range(&self) -> TextRange {
+ self.0.range()
+ }
+}
+
+impl Queryable for UnusedLabel {
+ type Input = Self;
+ type Language = JsLanguage;
+ type Output = JsLabeledStatement;
+ type Services = ();
+
+ fn build_visitor(
+ analyzer: &mut impl AddVisitor<Self::Language>,
+ _: &<Self::Language as Language>::Root,
+ ) {
+ analyzer.add_visitor(Phases::Syntax, UnusedLabelVisitor::default);
+ }
+
+ // Extract the output object from the input type
+ fn unwrap_match(_: &ServiceBag, query: &Self::Input) -> Self::Output {
+ query.0.clone()
+ }
+}
+
+impl Rule for NoUnusedLabels {
+ type Query = UnusedLabel;
+ type State = ();
+ type Signals = Option<Self::State>;
+ type Options = ();
+
+ fn run(_: &RuleContext<Self>) -> Option<Self::State> {
+ Some(())
+ }
+
+ fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
+ let unused_label = ctx.query();
+ Some(RuleDiagnostic::new(
+ rule_category!(),
+ unused_label.label_token().ok()?.text_trimmed_range(),
+ markup! {
+ "Unused "<Emphasis>"label"</Emphasis>"."
+ },
+ ))
+ }
+
+ fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
+ let unused_label = ctx.query();
+ let body = unused_label.body().ok()?;
+ let mut mutation = ctx.root().begin();
+ mutation.replace_element(
+ NodeOrToken::Node(unused_label.syntax().to_owned()),
+ NodeOrToken::Node(body.syntax().to_owned()),
+ );
+ Some(JsRuleAction {
+ category: ActionCategory::QuickFix,
+ applicability: Applicability::MaybeIncorrect,
+ message: markup! {"Remove the unused "<Emphasis>"label"</Emphasis>"."}.to_owned(),
+ mutation,
+ })
+ }
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/noUnusedLabels/invalid.js
+/*before*/ A /*inner*/: /*after*/ var foo = 0;
+
+B: {
+ foo();
+}
+
+C: for (let i = 0; i < 10; ++i) {
+ foo();
+}
+
+D: var foo = 0;
+
+E: {
+ foo();
+ bar();
+}
+
+F: for (var i = 0; i < 10; ++i) {
+ foo();
+ if (a) break;
+ bar();
+}
+
+G: for (var i = 0; i < 10; ++i) {
+ foo();
+ if (a) continue;
+ bar();
+}
+
+A: for (var i = 0; i < 10; ++i) {
+ H: break A;
+}
+
+I: {
+ var I = 0;
+ console.log(I);
+}
+
+J: /* comment */ foo;
+
+K /* comment */: foo;
+
+L: {
+ function f() {
+ L: {
+ break L;
+ }
+ }
+}
+
+M: {
+ class X {
+ static {
+ M: {
+ break M;
+ }
+ }
+
+ method() {
+ M: {
+ break M;
+ }
+ }
+ }
+}
+
+/*
+ * Below is fatal errors.
+ * "A: break B",
+ * "A: function foo() { break A; }",
+ * "A: class Foo { foo() { break A; } }",
+ * "A: { A: { break A; } }"
+ */

ADD
crates/rome_js_analyze/tests/specs/nursery/noUnusedLabels/valid.js
+A: {
+ if (foo()) {
+ break A;
+ }
+ bar();
+}
+
+B: for (let i = 0; i < 10; ++i) {
+ if (foo()) {
+ break B;
+ }
+ bar();
+}
+
+A: break A;
+
+A: {
+ foo();
+ break A;
+ bar();
+}
+
+A: if (a) {
+ foo();
+ if (b) break A;
+ bar();
+}
+
+A: for (var i = 0; i < 10; ++i) {
+ foo();
+ if (a) break A;
+ bar();
+}
+
+A: for (var i = 0; i < 10; ++i) {
+ foo();
+ if (a) continue A;
+ bar();
+}
+
+A: {
+ B: break B;
+ C: for (var i = 0; i < 10; ++i) {
+ foo();
+ if (a) break A;
+ if (c) continue C;
+ bar();
+ }
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/noUnusedLabels/valid.js.snap
+---
+source: crates/rome_js_analyze/tests/spec_tests.rs
+assertion_line: 92
+expression: valid.js
+---
+# Input
+```js
+A: {
+ if (foo()) {
+ break A;
+ }
+ bar();
+}
+
+B: for (let i = 0; i < 10; ++i) {
+ if (foo()) {
+ break B;
+ }
+ bar();
+}
+
+A: break A;
+
+A: {
+ foo();
+ break A;
+ bar();
+}
+
+A: if (a) {
+ foo();
+ if (b) break A;
+ bar();
+}
+
+A: for (var i = 0; i < 10; ++i) {
+ foo();
+ if (a) break A;
+ bar();
+}
+
+A: for (var i = 0; i < 10; ++i) {
+ foo();
+ if (a) continue A;
+ bar();
+}
+
+A: {
+ B: break B;
+ C: for (var i = 0; i < 10; ++i) {
+ foo();
+ if (a) break A;
+ if (c) continue C;
+ bar();
+ }
+}
+
+```
+
+

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -787,6 +787,8 @@ struct NurserySchema {
no_unreachable_super: Option<RuleConfiguration>,
#[doc = "Disallow control flow statements in finally blocks."]
no_unsafe_finally: Option<RuleConfiguration>,
+ #[doc = "Disallow unused labels."]
+ no_unused_labels: Option<RuleConfiguration>,
#[doc = "Disallow useless case in switch statements."]
no_useless_switch_case: Option<RuleConfiguration>,
#[doc = "Disallow the use of var"]
@@ -830,7 +832,7 @@ struct NurserySchema {
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 48] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 49] = [
"noAccessKey",
"noAssignInExpressions",
"noBannedTypes",
@@ -859,6 +861,7 @@ impl Nursery {
"noStringCaseMismatch",
"noUnreachableSuper",
"noUnsafeFinally",
+ "noUnusedLabels",
"noUselessSwitchCase",
"noVar",
"noVoidTypeReturn",
@@ -880,7 +883,7 @@ impl Nursery {
"useValidAriaProps",
"useValidLang",
];
- const RECOMMENDED_RULES: [&'static str; 39] = [
+ const RECOMMENDED_RULES: [&'static str; 40] = [
"noAssignInExpressions",
"noBannedTypes",
"noClassAssign",
@@ -904,6 +907,7 @@ impl Nursery {
"noStringCaseMismatch",
"noUnreachableSuper",
"noUnsafeFinally",
+ "noUnusedLabels",
"noUselessSwitchCase",
"noVar",
"noVoidTypeReturn",
@@ -921,7 +925,7 @@ impl Nursery {
"useValidAriaProps",
"useValidLang",
];
- const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 39] = [
+ const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 40] = [
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[1]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[2]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[3]),
@@ -949,18 +953,19 @@ impl Nursery {
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[29]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[30]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[31]),
- RuleFilter::Rule("nursery", Self::CATEGORY_RULES[33]),
- RuleFilter::Rule("nursery", Self::CATEGORY_RULES[35]),
+ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[32]),
+ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[34]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[36]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[37]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[38]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[39]),
- RuleFilter::Rule("nursery", Self::CATEGORY_RULES[42]),
+ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[40]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[43]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[44]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[45]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[46]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[47]),
+ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[48]),
];
pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) }
pub(crate) fn get_enabled_rules(&self) -> IndexSet<RuleFilter> {
@@ -987,7 +992,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>; 39] {
+ pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 40] {
Self::RECOMMENDED_RULES_AS_FILTERS
}
}

MODIFY
editors/vscode/configuration_schema.json
editors/vscode/configuration_schema.json
}
]
},
+ "noUnusedLabels": {
+ "description": "Disallow unused labels.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"noUselessSwitchCase": {
"description": "Disallow useless case in switch statements.",
"anyOf": [

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -401,6 +401,10 @@ export interface Nursery {
* Disallow control flow statements in finally blocks.
*/
noUnsafeFinally?: RuleConfiguration;
+ /**
+ * Disallow unused labels.
+ */
+ noUnusedLabels?: RuleConfiguration;
/**
* Disallow useless case in switch statements.
*/
@@ -782,6 +786,7 @@ export type Category =
| "lint/nursery/noStringCaseMismatch"
| "lint/nursery/noUnreachableSuper"
| "lint/nursery/noUnsafeFinally"
+ | "lint/nursery/noUnusedLabels"
| "lint/nursery/noUselessSwitchCase"
| "lint/nursery/noVar"
| "lint/nursery/noVoidTypeReturn"

MODIFY
npm/rome/configuration_schema.json
npm/rome/configuration_schema.json
}
]
},
+ "noUnusedLabels": {
+ "description": "Disallow unused labels.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"noUselessSwitchCase": {
"description": "Disallow useless case in switch statements.",
"anyOf": [

MODIFY
website/src/pages/lint/rules/index.mdx
website/src/pages/lint/rules/index.mdx
@@ -656,6 +656,12 @@ a superclass
Disallow control flow statements in finally blocks.
</section>
<section class="rule">
+<h3 data-toc-exclude id="noUnusedLabels">
+ <a href="/lint/rules/noUnusedLabels">noUnusedLabels</a>
+</h3>
+Disallow unused labels.
+</section>
+<section class="rule">
<h3 data-toc-exclude id="noUselessSwitchCase">
<a href="/lint/rules/noUselessSwitchCase">noUselessSwitchCase</a>
</h3>

feat(rome_js_analyze): `noUnusedLabels` (#4096)

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