diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_29.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_29.py new file mode 100644 index 00000000000000..246f12b8180124 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_29.py @@ -0,0 +1,23 @@ +"""Regression test for #10451. + +Annotations in a class are allowed to be forward references +if `from __future__ import annotations` is active, +even if they're in a class included in +`lint.flake8-type-checking.runtime-evaluated-base-classes`. + +They're not allowed to refer to symbols that cannot be *resolved* +at runtime, however. +""" + +from __future__ import annotations + +from sqlalchemy.orm import DeclarativeBase, Mapped + + +class Base(DeclarativeBase): + some_mapping: Mapped[list[Bar]] | None = None # Should not trigger F821 (resolveable forward reference) + simplified: list[Bar] | None = None # Should not trigger F821 (resolveable forward reference) + + +class Bar: + pass diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index f8d8b5fd9eb5e3..503690624cac29 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -180,6 +180,29 @@ mod tests { Ok(()) } + #[test_case(Rule::UndefinedName, Path::new("F821_29.py"))] + fn rules_with_flake8_type_checking_settings_enabled( + rule_code: Rule, + path: &Path, + ) -> Result<()> { + let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("pyflakes").join(path).as_path(), + &LinterSettings { + flake8_type_checking: crate::rules::flake8_type_checking::settings::Settings { + runtime_required_base_classes: vec![ + "pydantic.BaseModel".to_string(), + "sqlalchemy.orm.DeclarativeBase".to_string(), + ], + ..Default::default() + }, + ..LinterSettings::for_rule(rule_code) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_29.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_29.py.snap new file mode 100644 index 00000000000000..d0b409f39ee0ba --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_29.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +--- + diff --git a/repro.py b/repro.py new file mode 100644 index 00000000000000..9a8bbbdc310126 --- /dev/null +++ b/repro.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from sqlalchemy.orm import DeclarativeBase, Mapped + +if TYPE_CHECKING: + from whatever import whatever + + +class Base(DeclarativeBase): + some_mapping: Mapped[list[Bar]] | None = None # F821 Undefined name `Bar` + simplified: list[Bar] | None = None # F821 Undefined name `Bar` + bad: whatever + + +class Bar: + pass