{"record":{"id":"041bed56001e9606","repo":"astral-sh/ruff","slug":"expected-stmt-importfrom","errorCode":null,"errorMessage":"Expected Stmt::ImportFrom","messagePattern":"Expected Stmt::ImportFrom","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/ruff_linter/src/rules/flake8_tidy_imports/rules/relative_imports.rs","lineNumber":104,"sourceCode":"\nfn fix_banned_relative_import(\n    stmt: &Stmt,\n    level: u32,\n    module: Option<&str>,\n    module_path: Option<&[String]>,\n    generator: Generator,\n) -> Option<Fix> {\n    // Only fix is the module path is known.\n    let module_path = resolve_imported_module_path(level, module, module_path)?;\n\n    // Require import to be a valid module:\n    // https://python.org/dev/peps/pep-0008/#package-and-module-names\n    if !module_path.split('.').all(is_identifier) {\n        return None;\n    }\n\n    let Stmt::ImportFrom(ast::StmtImportFrom { names, is_lazy, .. }) = stmt else {\n        panic!(\"Expected Stmt::ImportFrom\");\n    };\n    let node = ast::StmtImportFrom {\n        module: Some(Identifier::new(\n            module_path.to_string(),\n            TextRange::default(),\n        )),\n        names: names.clone(),\n        level: 0,\n        is_lazy: *is_lazy,\n        range: TextRange::default(),\n        node_index: ruff_python_ast::AtomicNodeIndex::NONE,\n    };\n    let content = generator.stmt(&node.into());\n    Some(Fix::unsafe_edit(Edit::range_replacement(\n        content,\n        stmt.range(),\n    )))\n}","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/astral-sh/ruff/blob/26f38c119cac42e4d320ba08f09224fdec74af2c/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/relative_imports.rs#L86-L122","documentation":"This is an internal panic in Ruff's TID252 (banned-relative-import) fix generator. `fix_banned_relative_import` is only supposed to be called with a `Stmt::ImportFrom` node (the caller `banned_relative_import` matches on that variant before calling it), so the `let-else` treats anything else as an unreachable invariant. Hitting it means Ruff's internal assumption was violated (a bug), not user input.","triggerScenarios":"Running `ruff check --fix` (or a fix-only pass) on a file with a relative import (TID252) where the statement passed to the fix builder is not a `from ... import ...` statement. In practice only reachable via a Ruff regression, a buggy plugin/custom rule dispatching fix generation on the wrong statement, or mismatched parser output after AST changes.","commonSituations":"Users on a recent Ruff version after a parser/AST refactor; tooling that reuses Ruff's fix helpers out of context; corrupted or non-standard syntax that desynchronizes statement classification between the diagnostic and fix passes.","solutions":["Upgrade or downgrade Ruff to a version where TID252 fix generation is not broken; check the changelog for the panic","Reproduce with `ruff check --isolated --select TID252 --fix <file>` and file an issue at github.com/astral-sh/ruff with the minimized snippet","Disable the unsafe fix for the rule (`lint.fixable = [\"TID252\"]` removal or `--no-fix --select TID252`) and rewrite the relative import manually to an absolute one","If invoking fixes through the API/rust bindings, ensure you only call `fix_banned_relative_import` with `Stmt::ImportFrom` nodes"],"exampleFix":"# before (pyproject.toml)\n[tool.ruff.lint]\nselect = [\"TID252\"]\n\n# after (workaround while the bug is unfixed: keep the diagnostic, drop the fix)\n[tool.ruff.lint]\nselect = [\"TID252\"]\nfixable = [\"I\", \"F\"]  # exclude TID252 from auto-fixing","handlingStrategy":"try-catch","validationCode":"// shell: reproduce/verify before relying on fixes\nruff check --isolated --select TID252 --no-fix your_file.py  # diagnose without the fix path\n// confirm the statement is a from-import before invoking fix generation (Rust):\nassert matches!(stmt, ast::Stmt::ImportFrom(_));","typeGuard":"fn is_import_from(stmt: &Stmt) -> bool {\n    matches!(stmt, ast::Stmt::ImportFrom(_))\n}","tryCatchPattern":"// Ruff panics abort the process; guard process-level runs\nlet output = std::panic::catch_unwind(|| ruff_check_fix(path))\n    .unwrap_or_else(|_| eprintln!(\"ruff panicked; rerun with --no-fix\"));\n// CLI equivalent: run `ruff check` first, apply `--fix` only if diagnostics succeed","preventionTips":["Run `ruff check` without `--fix` first in CI, then apply fixes in a separate step so a panic never blocks diagnosis","Keep Ruff pinned and upgraded deliberately; read changelogs for TID252/fix regressions","Report panics upstream with minimized snippets instead of working around silently","When extending Ruff, only call fix builders with the statement variant the diagnostic matched"],"tags":["rust","panic","ruff","lint-fix","internal-invariant"],"backgroundTag":"lint-fix-internal-panic","analyzedSha":"26f38c119cac42e4d320ba08f09224fdec74af2c","analyzedAt":"2026-09-05T10:32:37.492Z","contentChangedAt":"2026-09-05T10:32:37.492Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}