astral-sh/ruff · info

Expected outer if to have indented body and no else

Error message

Expected outer if to have indented body and no else

What it means

The SIM102 collapse destructures the outer `if` expecting an indented body and no `orelse`; `elif`/`else` clauses (or a suite libcst parsed as lines rather than an `IndentedBlock`) fail this pattern and abort the fix, because merging conditions is only well-defined for a plain two-level `if` nesting.

Source

Thrown at crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs:368

        let indented_block = match_indented_block(&mut embedding.body)?;
        indented_block.indent = Some(outer_indent);

        let Some(statement) = indented_block.body.first_mut() else {
            bail!("Expected indented block to have at least one statement")
        };
        statement
    };

    let outer_if = match_if(statement)?;

    let libcst_native::If {
        body: libcst_native::Suite::IndentedBlock(outer_body),
        orelse: None,
        ..
    } = outer_if
    else {
        bail!("Expected outer if to have indented body and no else")
    };

    let [
        libcst_native::Statement::Compound(libcst_native::CompoundStatement::If(
            inner_if @ libcst_native::If { orelse: None, .. },
        )),
    ] = &mut *outer_body.body
    else {
        bail!("Expected one inner if statement");
    };

    outer_if.test =
        libcst_native::Expression::BooleanOperation(Box::new(libcst_native::BooleanOperation {
            left: Box::new(parenthesize_and_operand(outer_if.test.clone())),
            operator: libcst_native::BooleanOp::And {
                whitespace_before: space(),
                whitespace_after: space(),
            },

View on GitHub (pinned to 15f3fe6b15)

Solutions

  1. Rewrite so the outer `if` has no else branch, or merge conditions manually into `if a and b:`
  2. Restructure `elif` chains explicitly before relying on the autofix
  3. Suppress SIM102 for constructs where the nesting is intentional

Example fix

// before
if a:
    if b:
        do()
    else:
        other()
// after (manual)
if a and b:
    do()
elif a:
    other()
Defensive patterns

Strategy: validation

Validate before calling

python - <<'EOF'
import ast, sys
tree = ast.parse(open(sys.argv[1]).read())
for n in ast.walk(tree):
    if isinstance(n, ast.If) and (n.orelse or isinstance(getattr(n, 'finalbody', None), list)):
        print('if/else at line', n.lineno, 'is not SIM102-autofixable')
EOF

Prevention

When it happens

Trigger: `ruff check --fix` on `if a: ... elif b:` or `if a: ... else: ...` where the nested-if match put an `elif`/`else` in the outer node, or an outer `if` body libcst didn't represent as an `IndentedBlock`.

Common situations: `elif` chains that look collapsible, trailing `else` blocks after the inner `if`, unusual one-liner `if a: if b: c` formatting.

Related errors


AI-assisted analysis of astral-sh/ruff@15f3fe6b15 (2026-09-05). Data as JSON: /api/errors/5725824cf3c93f63. Report an issue: GitHub.