use rowan::TextRange;
use rowan::ast::AstNode as _;
use crate::ast::IfExpr;
use crate::ast::kinds::is_trivia;
use crate::linter::diagnostic::{Diagnostic, Fix, ViolationData};
use crate::linter::rules::matchers;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode};
pub struct UnnecessaryNesting;
const EXAMPLES: &[Example] = &[Example {
caption: "An `if` whose only body is another `if` can be a single `if`:",
source: "if (a) {\n if (b) {\n do_thing()\n }\n}\n",
}];
impl Rule for UnnecessaryNesting {
fn id(&self) -> &'static str {
"unnecessary-nesting"
}
fn description(&self) -> &'static str {
"Flag an `if` whose entire body is a second `if`—the two could be a \
single `if` with the conditions joined by `&&`, dropping a needless \
level of nesting. It fires only when neither `if` has an `else` (an \
`else` on either side changes what runs) and the inner `if` is the sole \
statement of the outer one.\n\nThe fix joins the conditions with `&&`, \
parenthesizing each non-primary condition so the grouping is preserved. \
It is unsafe (collapsing dedents the body, so a reformat may follow) and \
withheld when it would drop a comment."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::IF_EXPR]
}
fn check(&self, el: &SyntaxElement, _ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(node) = el.as_node() else {
return;
};
let Some(outer) = IfExpr::cast(node.clone()) else {
return;
};
if outer.else_keyword().is_some() {
return;
}
let then = outer.then_elements().as_deref().and_then(sole_expr);
let Some(inner) = then.and_then(|t| inner_if(&t)) else {
return;
};
if inner.else_keyword().is_some() {
return;
}
let (Some(outer_cond), Some(inner_cond), Some(inner_body)) = (
outer.condition_elements().as_deref().and_then(sole_expr),
inner.condition_elements().as_deref().and_then(sole_expr),
inner.then_elements().as_deref().and_then(sole_expr),
) else {
return;
};
let full = node.text_range();
let (start, end) = (usize::from(full.start()), usize::from(full.end()));
let keep = [
outer_cond.text_range(),
inner_cond.text_range(),
inner_body.text_range(),
];
let fix = (!drops_comment(node, &keep)).then(|| {
let combined = format!("{} && {}", wrap(&outer_cond), wrap(&inner_cond));
let body = matchers::element_text(&inner_body);
Fix::unsafe_(
start,
end,
format!("if ({combined}) {body}"),
"Combine the nested `if` conditions with `&&`",
)
});
let range = inner
.if_keyword()
.map_or_else(|| inner.syntax().text_range(), |k| k.text_range());
sink.push(Diagnostic {
rule: "unnecessary-nesting",
severity: Default::default(),
path: Default::default(),
range,
message: ViolationData::new(
"unnecessary-nesting",
"this `if` is nested in another `if` with no `else`",
)
.with_suggestion("Combine the two conditions with `&&` to drop a level of nesting."),
fix,
});
}
}
fn inner_if(then: &SyntaxElement) -> Option<IfExpr> {
let node = then.as_node()?;
if let Some(inner) = IfExpr::cast(node.clone()) {
return Some(inner);
}
if node.kind() == SyntaxKind::BLOCK_EXPR {
return block_sole_stmt(node).and_then(IfExpr::cast);
}
None
}
fn block_sole_stmt(block: &SyntaxNode) -> Option<SyntaxNode> {
let mut it = block.children_with_tokens().filter(|e| {
!matches!(
e.kind(),
SyntaxKind::LBRACE
| SyntaxKind::RBRACE
| SyntaxKind::SEMICOLON
| SyntaxKind::WHITESPACE
| SyntaxKind::NEWLINE
| SyntaxKind::COMMENT
)
});
let first = it.next()?;
it.next().is_none().then_some(())?;
first.into_node()
}
fn sole_expr(elements: &[SyntaxElement]) -> Option<SyntaxElement> {
let mut it = elements
.iter()
.filter(|e| !is_trivia(e.kind()) && e.kind() != SyntaxKind::COMMENT);
let first = it.next()?;
it.next().is_none().then(|| first.clone())
}
fn wrap(el: &SyntaxElement) -> String {
let text = matchers::element_text(el);
if needs_paren(el) {
format!("({text})")
} else {
text
}
}
fn needs_paren(el: &SyntaxElement) -> bool {
if matchers::is_atom(el) {
return false;
}
!matches!(
el.as_node().map(|n| n.kind()),
Some(
SyntaxKind::CALL_EXPR
| SyntaxKind::SUBSET_EXPR
| SyntaxKind::SUBSET2_EXPR
| SyntaxKind::PAREN_EXPR
)
)
}
fn drops_comment(outer: &SyntaxNode, keep: &[TextRange]) -> bool {
outer.descendants_with_tokens().any(|el| {
el.kind() == SyntaxKind::COMMENT && !keep.iter().any(|k| k.contains_range(el.text_range()))
})
}