Skip to content

Commit

Permalink
feat(lint/noUselessElse): add rule
Browse files Browse the repository at this point in the history
  • Loading branch information
Conaclos committed Sep 21, 2023
1 parent 6baef2b commit 544da95
Show file tree
Hide file tree
Showing 21 changed files with 1,361 additions and 32 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom

- Add [noMisleadingInstantiator](https://biomejs.dev/linter/rules/no-mileading-instantiator) rule. The rule reports the misleading use of the `new` and `constructor` methods. Contributed by @unvalley

- Add [noUselessElse](https://biomejs.dev/linter/rules/no-useless-else) rule.
The rule reports `else` clauses that can be omitted because their `if` branches break.
Contributed by @Conaclos

#### Bug fixes

- Fix [#294](https://github.com/biomejs/biome/issues/294). [noConfusingVoidType](https://biomejs.dev/linter/rules/no-confusing-void-type/) no longer reports false positives for return types. Contributed by @b4s36t4
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ define_categories! {
"lint/a11y/noSvgWithoutTitle": "https://biomejs.dev/linter/rules/no-svg-without-title",
"lint/a11y/useAltText": "https://biomejs.dev/linter/rules/use-alt-text",
"lint/a11y/useAnchorContent": "https://biomejs.dev/linter/rules/use-anchor-content",
"lint/a11y/useValidAriaValues": "https://biomejs.dev/linter/rules/use-valid-aria-values",
"lint/a11y/useAriaPropsForRole": "https://biomejs.dev/linter/rules/use-aria-props-for-role",
"lint/a11y/useButtonType": "https://biomejs.dev/linter/rules/use-button-type",
"lint/a11y/useHeadingContent": "https://biomejs.dev/linter/rules/use-heading-content",
Expand All @@ -38,6 +37,7 @@ define_categories! {
"lint/a11y/useMediaCaption": "https://biomejs.dev/linter/rules/use-media-caption",
"lint/a11y/useValidAnchor": "https://biomejs.dev/linter/rules/use-valid-anchor",
"lint/a11y/useValidAriaProps": "https://biomejs.dev/linter/rules/use-valid-aria-props",
"lint/a11y/useValidAriaValues": "https://biomejs.dev/linter/rules/use-valid-aria-values",
"lint/a11y/useValidLang": "https://biomejs.dev/linter/rules/use-valid-lang",
"lint/complexity/noBannedTypes": "https://biomejs.dev/linter/rules/no-banned-types",
"lint/complexity/noExtraBooleanCast": "https://biomejs.dev/linter/rules/no-extra-boolean-cast",
Expand Down Expand Up @@ -96,6 +96,7 @@ define_categories! {
"lint/nursery/noGlobalIsFinite": "https://biomejs.dev/linter/rules/no-global-is-finite",
"lint/nursery/noGlobalIsNan": "https://biomejs.dev/linter/rules/no-global-is-nan",
"lint/nursery/noMisleadingInstantiator": "https://biomejs.dev/linter/rules/no-misleading-instantiator",
"lint/nursery/noUselessElse": "https://biomejs.dev/lint/rules/no-useless-else",
"lint/nursery/noVoid": "https://biomejs.dev/linter/rules/no-void",
"lint/nursery/useArrowFunction": "https://biomejs.dev/linter/rules/use-arrow-function",
"lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/lint/rules/use-biome-suppression-comment",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/analyzers/nursery.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

195 changes: 195 additions & 0 deletions crates/biome_js_analyze/src/analyzers/nursery/no_useless_else.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
use biome_analyze::{
context::RuleContext, declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic,
};
use biome_console::markup;
use biome_diagnostics::Applicability;
use biome_js_factory::make;
use biome_js_syntax::{AnyJsStatement, JsElseClause, JsIfStatement, JsStatementList};
use biome_rowan::{AstNode, AstNodeList, BatchMutationExt};

use crate::JsRuleAction;

declare_rule! {
/// Disallow `else` block when the `if` block breaks early.
///
/// If an `if` block breaks early using a breaking statement (`return`, `break`, `continue`, or `throw`),
/// then the `else` block becomes useless.
/// Its contents can be placed outside of the block.
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// while (x > 0) {
/// if (f(x)) {
/// break;
/// } else {
/// x++
/// }
/// }
/// ```
///
/// ```js,expect_diagnostic
/// function f() {
/// if (x < 0) {
/// return 0;
/// } else {
/// return x;
/// }
/// }
/// ```
///
/// ```js,expect_diagnostic
/// function f() {
/// if (x < 0) {
/// throw new RangeError();
/// } else {
/// return x;
/// }
/// }
/// ```
///
/// ## Valid
///
/// ```js
/// function f() {
/// if (x < 0) {
/// return 0;
/// }
/// return x;
/// }
/// ```
pub(crate) NoUselessElse {
version: "next",
name: "noUselessElse",
recommended: true,
}
}

impl Rule for NoUselessElse {
type Query = Ast<JsElseClause>;
type State = ();
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let else_clause = ctx.query();
let if_stmt = else_clause.parent::<JsIfStatement>()?;
let mut stmt_stack = vec![(if_stmt.consequent().ok()?, ScopeMetadata::default())];
while let Some((stmt, metadata)) = stmt_stack.pop() {
match stmt {
AnyJsStatement::JsBlockStatement(block_stmt) => {
let Some(last) = block_stmt.statements().iter().last() else {
// empty statement block
return None;
};
stmt_stack.push((last, metadata));
}
AnyJsStatement::JsBreakStatement(_) => {
if metadata.is_breakable {
// We are inside a breakable structure (switch statement)
// that we saw in a previous iteration.
return None;
}
}
AnyJsStatement::JsContinueStatement(_)
| AnyJsStatement::JsReturnStatement(_)
| AnyJsStatement::JsThrowStatement(_) => {}
AnyJsStatement::JsIfStatement(if_stmt) => {
let Some(else_clause) = if_stmt.else_clause() else {
// No else clause
return None;
};
stmt_stack.push((if_stmt.consequent().ok()?, metadata));
stmt_stack.push((else_clause.alternate().ok()?, metadata));
}
AnyJsStatement::JsSwitchStatement(switch_stmt) => {
// To simplify, We do not take fallthoughs into account.
// Thus, this can miss some useless else.
let cases = switch_stmt.cases();
let Some(last_case) = cases.last() else {
// Empty switch
return None;
};
if last_case.consequent().is_empty() {
return None;
}
for switch_clause in cases.iter() {
if let Some(last) = switch_clause.consequent().last() {
stmt_stack.push((last, ScopeMetadata { is_breakable: true }));
}
}
}
_ => {
// labeled statements, loops, try-catch, with statement, and others
return None;
}
}
}
Some(())
}

fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
let else_clause = ctx.query();
Some(
RuleDiagnostic::new(
rule_category!(),
else_clause.range(),
markup! {
"This "<Emphasis>"else"</Emphasis>" clause can be omitted."
},
)
.detail(
else_clause.syntax().parent()?.text_trimmed_range(),
markup! {
"This "<Emphasis>"if"</Emphasis>" statement uses an early breaking statement."
},
),
)
}

fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
let else_clause = ctx.query();
let if_stmt = else_clause.parent::<JsIfStatement>()?;
if let Some(stmts_list) = JsStatementList::cast(if_stmt.syntax().parent()?) {
let else_alternative = else_clause.alternate().ok()?;
let if_pos = stmts_list
.iter()
.position(|x| x.syntax() == if_stmt.syntax())?;
let new_if_stmt = AnyJsStatement::JsIfStatement(if_stmt.clone().with_else_clause(None))
.with_trailing_trivia_pieces([])?;
let prev_stmts = stmts_list.iter().take(if_pos).chain([new_if_stmt]);
let next_stmts = stmts_list.iter().skip(if_pos + 1);
// We collect the statements because `chain` is not able to produce an `ExactSizeIterator`.
let new_stmts: Vec<_> =
if let AnyJsStatement::JsBlockStatement(else_block_stmts) = else_alternative {
prev_stmts
.chain(else_block_stmts.statements().iter())
.chain(next_stmts)
.collect()
} else {
prev_stmts
.chain([else_alternative])
.chain(next_stmts)
.collect()
};
let new_stmts_list = make::js_statement_list(new_stmts);
let mut mutation = ctx.root().begin();
mutation.replace_node_discard_trivia(stmts_list, new_stmts_list);
return Some(JsRuleAction {
category: ActionCategory::QuickFix,
applicability: Applicability::MaybeIncorrect,
message: markup! { "Omit the "<Emphasis>"else"</Emphasis>" clause." }.to_owned(),
mutation,
});
}
None
}
}

#[derive(Debug, Copy, Clone, Default)]
struct ScopeMetadata {
// We are inside a breakable structure
is_breakable: bool,
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ impl Rule for UseIsArray {
make::token(T![.]),
make::js_name(make::ident("isArray")).into(),
);
let arg = AnyJsCallArgument::AnyJsExpression(node.left().ok()?.trim()?);
let arg = AnyJsCallArgument::AnyJsExpression(node.left().ok()?.trim_trivia()?);
let instanceof_trailing_trivia = node.instanceof_token().ok()?.trailing_trivia().pieces();
let args = make::js_call_arguments(
make::token(T!['(']).with_trailing_trivia_pieces(trim_leading_trivia_pieces(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
function f (x) {
if (x < 0) {
throw new RangeError();
} else {
return x;
}
}

function f (x) {
f();
if (x < 0) {
throw new RangeError();
} else {
g();
}
h();
}

function f (x) {
if (x < 0) {
throw new RangeError();
} else return x;
}

function f (x) {
if (x < 0)
throw new RangeError();
else
return x;
}

function f (x) {
if (x < 0) {
throw new RangeError();
} else if (x === 0) {
return 1;
} else {
return x;
}
}

function f (x) {
while (true) {
if (x < 0) {
break;
} else {
x -= g(x)
}
}
return x;
}

function f (x) {
while (true) {
if (x < 0) {
break;
} else {
x -= g(x)
}
}
return x;
}

function f (x) {
if (x > 0 && x < 5) {
switch (x) {
case 0:
case 1:
return 0;
default:
return x;
}
} else {
return x;
}
}

function f (x) { // 0
// 1
if (x > 0 && x < 5) {
switch (x) {
case 0:
case 1:
return 0;
default:
return x;
}
} /*a*/ else /*b*/ { // c
// d
return x; // e
// f
} // g
// 2
} // 3
Loading

0 comments on commit 544da95

Please sign in to comment.