diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py index 6928429d4dd5d..f9004a6dea5af 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py @@ -33,16 +33,3 @@ class MyClass: baz: MyClass eggs = baz # Still invalid even when `__future__.annotations` are enabled eggs = "baz" # always okay - -# Forward references: -MaybeDStr: TypeAlias = Optional[DStr] # Still invalid even when `__future__.annotations` are enabled -MaybeDStr2: TypeAlias = Optional["DStr"] # always okay -DStr: TypeAlias = Union[D, str] # Still invalid even when `__future__.annotations` are enabled -DStr2: TypeAlias = Union["D", str] # always okay - -class D: ... - -# More circular references -class Leaf: ... -class Tree(list[Tree | Leaf]): ... # Still invalid even when `__future__.annotations` are enabled -class Tree2(list["Tree | Leaf"]): ... # always okay 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 0000000000000..246f12b818012 --- /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/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 75b934d5ee46d..d6fb29191662b 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -937,7 +937,6 @@ impl<'a> Visitor<'a> for Checker<'a> { && !self.semantic.in_deferred_type_definition() && self.semantic.in_type_definition() && self.semantic.future_annotations() - && (self.semantic.in_typing_only_annotation() || self.source_type.is_stub()) { if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr { self.visit.string_type_definitions.push(( diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index f8d8b5fd9eb5e..503690624cac2 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_27.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_27.py.snap index b0ef6067d4274..de22fa9f7baf9 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_27.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_27.py.snap @@ -17,30 +17,3 @@ F821_27.py:34:8: F821 Undefined name `baz` | ^^^ F821 35 | eggs = "baz" # always okay | - -F821_27.py:38:33: F821 Undefined name `DStr` - | -37 | # Forward references: -38 | MaybeDStr: TypeAlias = Optional[DStr] # Still invalid even when `__future__.annotations` are enabled - | ^^^^ F821 -39 | MaybeDStr2: TypeAlias = Optional["DStr"] # always okay -40 | DStr: TypeAlias = Union[D, str] # Still invalid even when `__future__.annotations` are enabled - | - -F821_27.py:40:25: F821 Undefined name `D` - | -38 | MaybeDStr: TypeAlias = Optional[DStr] # Still invalid even when `__future__.annotations` are enabled -39 | MaybeDStr2: TypeAlias = Optional["DStr"] # always okay -40 | DStr: TypeAlias = Union[D, str] # Still invalid even when `__future__.annotations` are enabled - | ^ F821 -41 | DStr2: TypeAlias = Union["D", str] # always okay - | - -F821_27.py:47:17: F821 Undefined name `Tree` - | -45 | # More circular references -46 | class Leaf: ... -47 | class Tree(list[Tree | Leaf]): ... # Still invalid even when `__future__.annotations` are enabled - | ^^^^ F821 -48 | class Tree2(list["Tree | Leaf"]): ... # always okay - | 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 0000000000000..d0b409f39ee0b --- /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 +--- +