From a775f51ec4a8835f022868c49b5c2abfde6eed7d Mon Sep 17 00:00:00 2001 From: Daverball Date: Sun, 29 Dec 2024 09:32:36 +0100 Subject: [PATCH 1/9] [`flake8-type-checking`] Apply `TC008` more eagerly in typing context --- .../fixtures/flake8_type_checking/TC008.pyi | 23 ++ .../TC008_typing_execution_context.py | 28 ++ .../src/rules/flake8_type_checking/mod.rs | 2 + .../rules/type_alias_quotes.rs | 1 + ...g__tests__quoted-type-alias_TC008.pyi.snap | 216 +++++++++++++ ...ias_TC008_typing_execution_context.py.snap | 293 ++++++++++++++++++ 6 files changed, 563 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py create mode 100644 crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap create mode 100644 crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi new file mode 100644 index 00000000000000..9a47a164627953 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi @@ -0,0 +1,23 @@ +from __future__ import annotations + +from typing import TypeAlias, TYPE_CHECKING + +from foo import Foo + +a: TypeAlias = 'int' # TC008 +b: TypeAlias = 'Foo' # TC008 +c: TypeAlias = 'Foo[str]' # TC008 +d: TypeAlias = 'Foo.bar' # TC008 + +type B = 'Foo' # TC008 +type C = 'Foo[str]' # TC008 +type D = 'Foo.bar' # TC008 + + +class Baz: + a: TypeAlias = 'Baz' # TC008 + type A = 'Baz' # TC008 + + class Nested: + a: TypeAlias = 'Baz' # TC008 + type A = 'Baz' # TC008 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py new file mode 100644 index 00000000000000..2883569d6f734e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from typing import TypeAlias, TYPE_CHECKING + +from foo import Foo + +if TYPE_CHECKING: + from typing import Dict + + OptStr: TypeAlias = str | None + Bar: TypeAlias = Foo[int] + + a: TypeAlias = 'int' # TC008 + b: TypeAlias = 'Dict' # TC008 + c: TypeAlias = 'Foo' # TC008 + d: TypeAlias = 'Foo[str]' # TC008 + e: TypeAlias = 'Foo.bar' # TC008 + f: TypeAlias = 'Foo | None' # TC008 + g: TypeAlias = 'OptStr' # TC008 + h: TypeAlias = 'Bar' # TC008 + i: TypeAlias = Foo['str'] # TC008 + j: TypeAlias = 'Baz' # TC008 + k: TypeAlias = 'k | None' # TC008 + l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) + m: TypeAlias = ('int' # TC008 + | None) + n: TypeAlias = ('int' # TC008 (fix removes comment currently) + ' | None') diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs index ab95fe3ac56fd9..84b6771c0651dd 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs @@ -67,6 +67,8 @@ mod tests { // so we want to make sure their fixes are not going around in circles. #[test_case(Rule::UnquotedTypeAlias, Path::new("TC007.py"))] #[test_case(Rule::QuotedTypeAlias, Path::new("TC008.py"))] + #[test_case(Rule::QuotedTypeAlias, Path::new("TC008.pyi"))] + #[test_case(Rule::QuotedTypeAlias, Path::new("TC008_typing_execution_context.py"))] fn type_alias_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index 47411228fdcc9c..8f470f631743b2 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -260,6 +260,7 @@ pub(crate) fn quoted_type_alias( // explicit type aliases require some additional checks to avoid false positives if checker.semantic().in_annotated_type_alias_value() + && checker.semantic().execution_context().is_runtime() && quotes_are_unremovable(checker.semantic(), expr) { return; diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap new file mode 100644 index 00000000000000..db420b6432353e --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap @@ -0,0 +1,216 @@ +--- +source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +--- +TC008.pyi:7:16: TC008 [*] Remove quotes from type alias + | +5 | from foo import Foo +6 | +7 | a: TypeAlias = 'int' # TC008 + | ^^^^^ TC008 +8 | b: TypeAlias = 'Foo' # TC008 +9 | c: TypeAlias = 'Foo[str]' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +4 4 | +5 5 | from foo import Foo +6 6 | +7 |-a: TypeAlias = 'int' # TC008 + 7 |+a: TypeAlias = int # TC008 +8 8 | b: TypeAlias = 'Foo' # TC008 +9 9 | c: TypeAlias = 'Foo[str]' # TC008 +10 10 | d: TypeAlias = 'Foo.bar' # TC008 + +TC008.pyi:8:16: TC008 [*] Remove quotes from type alias + | + 7 | a: TypeAlias = 'int' # TC008 + 8 | b: TypeAlias = 'Foo' # TC008 + | ^^^^^ TC008 + 9 | c: TypeAlias = 'Foo[str]' # TC008 +10 | d: TypeAlias = 'Foo.bar' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +5 5 | from foo import Foo +6 6 | +7 7 | a: TypeAlias = 'int' # TC008 +8 |-b: TypeAlias = 'Foo' # TC008 + 8 |+b: TypeAlias = Foo # TC008 +9 9 | c: TypeAlias = 'Foo[str]' # TC008 +10 10 | d: TypeAlias = 'Foo.bar' # TC008 +11 11 | + +TC008.pyi:9:16: TC008 [*] Remove quotes from type alias + | + 7 | a: TypeAlias = 'int' # TC008 + 8 | b: TypeAlias = 'Foo' # TC008 + 9 | c: TypeAlias = 'Foo[str]' # TC008 + | ^^^^^^^^^^ TC008 +10 | d: TypeAlias = 'Foo.bar' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +6 6 | +7 7 | a: TypeAlias = 'int' # TC008 +8 8 | b: TypeAlias = 'Foo' # TC008 +9 |-c: TypeAlias = 'Foo[str]' # TC008 + 9 |+c: TypeAlias = Foo[str] # TC008 +10 10 | d: TypeAlias = 'Foo.bar' # TC008 +11 11 | +12 12 | type B = 'Foo' # TC008 + +TC008.pyi:10:16: TC008 [*] Remove quotes from type alias + | + 8 | b: TypeAlias = 'Foo' # TC008 + 9 | c: TypeAlias = 'Foo[str]' # TC008 +10 | d: TypeAlias = 'Foo.bar' # TC008 + | ^^^^^^^^^ TC008 +11 | +12 | type B = 'Foo' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +7 7 | a: TypeAlias = 'int' # TC008 +8 8 | b: TypeAlias = 'Foo' # TC008 +9 9 | c: TypeAlias = 'Foo[str]' # TC008 +10 |-d: TypeAlias = 'Foo.bar' # TC008 + 10 |+d: TypeAlias = Foo.bar # TC008 +11 11 | +12 12 | type B = 'Foo' # TC008 +13 13 | type C = 'Foo[str]' # TC008 + +TC008.pyi:12:10: TC008 [*] Remove quotes from type alias + | +10 | d: TypeAlias = 'Foo.bar' # TC008 +11 | +12 | type B = 'Foo' # TC008 + | ^^^^^ TC008 +13 | type C = 'Foo[str]' # TC008 +14 | type D = 'Foo.bar' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +9 9 | c: TypeAlias = 'Foo[str]' # TC008 +10 10 | d: TypeAlias = 'Foo.bar' # TC008 +11 11 | +12 |-type B = 'Foo' # TC008 + 12 |+type B = Foo # TC008 +13 13 | type C = 'Foo[str]' # TC008 +14 14 | type D = 'Foo.bar' # TC008 +15 15 | + +TC008.pyi:13:10: TC008 [*] Remove quotes from type alias + | +12 | type B = 'Foo' # TC008 +13 | type C = 'Foo[str]' # TC008 + | ^^^^^^^^^^ TC008 +14 | type D = 'Foo.bar' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +10 10 | d: TypeAlias = 'Foo.bar' # TC008 +11 11 | +12 12 | type B = 'Foo' # TC008 +13 |-type C = 'Foo[str]' # TC008 + 13 |+type C = Foo[str] # TC008 +14 14 | type D = 'Foo.bar' # TC008 +15 15 | +16 16 | + +TC008.pyi:14:10: TC008 [*] Remove quotes from type alias + | +12 | type B = 'Foo' # TC008 +13 | type C = 'Foo[str]' # TC008 +14 | type D = 'Foo.bar' # TC008 + | ^^^^^^^^^ TC008 + | + = help: Remove quotes + +ℹ Safe fix +11 11 | +12 12 | type B = 'Foo' # TC008 +13 13 | type C = 'Foo[str]' # TC008 +14 |-type D = 'Foo.bar' # TC008 + 14 |+type D = Foo.bar # TC008 +15 15 | +16 16 | +17 17 | class Baz: + +TC008.pyi:18:20: TC008 [*] Remove quotes from type alias + | +17 | class Baz: +18 | a: TypeAlias = 'Baz' # TC008 + | ^^^^^ TC008 +19 | type A = 'Baz' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +15 15 | +16 16 | +17 17 | class Baz: +18 |- a: TypeAlias = 'Baz' # TC008 + 18 |+ a: TypeAlias = Baz # TC008 +19 19 | type A = 'Baz' # TC008 +20 20 | +21 21 | class Nested: + +TC008.pyi:19:14: TC008 [*] Remove quotes from type alias + | +17 | class Baz: +18 | a: TypeAlias = 'Baz' # TC008 +19 | type A = 'Baz' # TC008 + | ^^^^^ TC008 +20 | +21 | class Nested: + | + = help: Remove quotes + +ℹ Safe fix +16 16 | +17 17 | class Baz: +18 18 | a: TypeAlias = 'Baz' # TC008 +19 |- type A = 'Baz' # TC008 + 19 |+ type A = Baz # TC008 +20 20 | +21 21 | class Nested: +22 22 | a: TypeAlias = 'Baz' # TC008 + +TC008.pyi:22:24: TC008 [*] Remove quotes from type alias + | +21 | class Nested: +22 | a: TypeAlias = 'Baz' # TC008 + | ^^^^^ TC008 +23 | type A = 'Baz' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +19 19 | type A = 'Baz' # TC008 +20 20 | +21 21 | class Nested: +22 |- a: TypeAlias = 'Baz' # TC008 + 22 |+ a: TypeAlias = Baz # TC008 +23 23 | type A = 'Baz' # TC008 + +TC008.pyi:23:18: TC008 [*] Remove quotes from type alias + | +21 | class Nested: +22 | a: TypeAlias = 'Baz' # TC008 +23 | type A = 'Baz' # TC008 + | ^^^^^ TC008 + | + = help: Remove quotes + +ℹ Safe fix +20 20 | +21 21 | class Nested: +22 22 | a: TypeAlias = 'Baz' # TC008 +23 |- type A = 'Baz' # TC008 + 23 |+ type A = Baz # TC008 diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap new file mode 100644 index 00000000000000..a5884d0863d597 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap @@ -0,0 +1,293 @@ +--- +source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +--- +TC008_typing_execution_context.py:13:20: TC008 [*] Remove quotes from type alias + | +11 | Bar: TypeAlias = Foo[int] +12 | +13 | a: TypeAlias = 'int' # TC008 + | ^^^^^ TC008 +14 | b: TypeAlias = 'Dict' # TC008 +15 | c: TypeAlias = 'Foo' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +10 10 | OptStr: TypeAlias = str | None +11 11 | Bar: TypeAlias = Foo[int] +12 12 | +13 |- a: TypeAlias = 'int' # TC008 + 13 |+ a: TypeAlias = int # TC008 +14 14 | b: TypeAlias = 'Dict' # TC008 +15 15 | c: TypeAlias = 'Foo' # TC008 +16 16 | d: TypeAlias = 'Foo[str]' # TC008 + +TC008_typing_execution_context.py:14:20: TC008 [*] Remove quotes from type alias + | +13 | a: TypeAlias = 'int' # TC008 +14 | b: TypeAlias = 'Dict' # TC008 + | ^^^^^^ TC008 +15 | c: TypeAlias = 'Foo' # TC008 +16 | d: TypeAlias = 'Foo[str]' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +11 11 | Bar: TypeAlias = Foo[int] +12 12 | +13 13 | a: TypeAlias = 'int' # TC008 +14 |- b: TypeAlias = 'Dict' # TC008 + 14 |+ b: TypeAlias = Dict # TC008 +15 15 | c: TypeAlias = 'Foo' # TC008 +16 16 | d: TypeAlias = 'Foo[str]' # TC008 +17 17 | e: TypeAlias = 'Foo.bar' # TC008 + +TC008_typing_execution_context.py:15:20: TC008 [*] Remove quotes from type alias + | +13 | a: TypeAlias = 'int' # TC008 +14 | b: TypeAlias = 'Dict' # TC008 +15 | c: TypeAlias = 'Foo' # TC008 + | ^^^^^ TC008 +16 | d: TypeAlias = 'Foo[str]' # TC008 +17 | e: TypeAlias = 'Foo.bar' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +12 12 | +13 13 | a: TypeAlias = 'int' # TC008 +14 14 | b: TypeAlias = 'Dict' # TC008 +15 |- c: TypeAlias = 'Foo' # TC008 + 15 |+ c: TypeAlias = Foo # TC008 +16 16 | d: TypeAlias = 'Foo[str]' # TC008 +17 17 | e: TypeAlias = 'Foo.bar' # TC008 +18 18 | f: TypeAlias = 'Foo | None' # TC008 + +TC008_typing_execution_context.py:16:20: TC008 [*] Remove quotes from type alias + | +14 | b: TypeAlias = 'Dict' # TC008 +15 | c: TypeAlias = 'Foo' # TC008 +16 | d: TypeAlias = 'Foo[str]' # TC008 + | ^^^^^^^^^^ TC008 +17 | e: TypeAlias = 'Foo.bar' # TC008 +18 | f: TypeAlias = 'Foo | None' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +13 13 | a: TypeAlias = 'int' # TC008 +14 14 | b: TypeAlias = 'Dict' # TC008 +15 15 | c: TypeAlias = 'Foo' # TC008 +16 |- d: TypeAlias = 'Foo[str]' # TC008 + 16 |+ d: TypeAlias = Foo[str] # TC008 +17 17 | e: TypeAlias = 'Foo.bar' # TC008 +18 18 | f: TypeAlias = 'Foo | None' # TC008 +19 19 | g: TypeAlias = 'OptStr' # TC008 + +TC008_typing_execution_context.py:17:20: TC008 [*] Remove quotes from type alias + | +15 | c: TypeAlias = 'Foo' # TC008 +16 | d: TypeAlias = 'Foo[str]' # TC008 +17 | e: TypeAlias = 'Foo.bar' # TC008 + | ^^^^^^^^^ TC008 +18 | f: TypeAlias = 'Foo | None' # TC008 +19 | g: TypeAlias = 'OptStr' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +14 14 | b: TypeAlias = 'Dict' # TC008 +15 15 | c: TypeAlias = 'Foo' # TC008 +16 16 | d: TypeAlias = 'Foo[str]' # TC008 +17 |- e: TypeAlias = 'Foo.bar' # TC008 + 17 |+ e: TypeAlias = Foo.bar # TC008 +18 18 | f: TypeAlias = 'Foo | None' # TC008 +19 19 | g: TypeAlias = 'OptStr' # TC008 +20 20 | h: TypeAlias = 'Bar' # TC008 + +TC008_typing_execution_context.py:18:20: TC008 [*] Remove quotes from type alias + | +16 | d: TypeAlias = 'Foo[str]' # TC008 +17 | e: TypeAlias = 'Foo.bar' # TC008 +18 | f: TypeAlias = 'Foo | None' # TC008 + | ^^^^^^^^^^^^ TC008 +19 | g: TypeAlias = 'OptStr' # TC008 +20 | h: TypeAlias = 'Bar' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +15 15 | c: TypeAlias = 'Foo' # TC008 +16 16 | d: TypeAlias = 'Foo[str]' # TC008 +17 17 | e: TypeAlias = 'Foo.bar' # TC008 +18 |- f: TypeAlias = 'Foo | None' # TC008 + 18 |+ f: TypeAlias = Foo | None # TC008 +19 19 | g: TypeAlias = 'OptStr' # TC008 +20 20 | h: TypeAlias = 'Bar' # TC008 +21 21 | i: TypeAlias = Foo['str'] # TC008 + +TC008_typing_execution_context.py:19:20: TC008 [*] Remove quotes from type alias + | +17 | e: TypeAlias = 'Foo.bar' # TC008 +18 | f: TypeAlias = 'Foo | None' # TC008 +19 | g: TypeAlias = 'OptStr' # TC008 + | ^^^^^^^^ TC008 +20 | h: TypeAlias = 'Bar' # TC008 +21 | i: TypeAlias = Foo['str'] # TC008 + | + = help: Remove quotes + +ℹ Safe fix +16 16 | d: TypeAlias = 'Foo[str]' # TC008 +17 17 | e: TypeAlias = 'Foo.bar' # TC008 +18 18 | f: TypeAlias = 'Foo | None' # TC008 +19 |- g: TypeAlias = 'OptStr' # TC008 + 19 |+ g: TypeAlias = OptStr # TC008 +20 20 | h: TypeAlias = 'Bar' # TC008 +21 21 | i: TypeAlias = Foo['str'] # TC008 +22 22 | j: TypeAlias = 'Baz' # TC008 + +TC008_typing_execution_context.py:20:20: TC008 [*] Remove quotes from type alias + | +18 | f: TypeAlias = 'Foo | None' # TC008 +19 | g: TypeAlias = 'OptStr' # TC008 +20 | h: TypeAlias = 'Bar' # TC008 + | ^^^^^ TC008 +21 | i: TypeAlias = Foo['str'] # TC008 +22 | j: TypeAlias = 'Baz' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +17 17 | e: TypeAlias = 'Foo.bar' # TC008 +18 18 | f: TypeAlias = 'Foo | None' # TC008 +19 19 | g: TypeAlias = 'OptStr' # TC008 +20 |- h: TypeAlias = 'Bar' # TC008 + 20 |+ h: TypeAlias = Bar # TC008 +21 21 | i: TypeAlias = Foo['str'] # TC008 +22 22 | j: TypeAlias = 'Baz' # TC008 +23 23 | k: TypeAlias = 'k | None' # TC008 + +TC008_typing_execution_context.py:21:24: TC008 [*] Remove quotes from type alias + | +19 | g: TypeAlias = 'OptStr' # TC008 +20 | h: TypeAlias = 'Bar' # TC008 +21 | i: TypeAlias = Foo['str'] # TC008 + | ^^^^^ TC008 +22 | j: TypeAlias = 'Baz' # TC008 +23 | k: TypeAlias = 'k | None' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +18 18 | f: TypeAlias = 'Foo | None' # TC008 +19 19 | g: TypeAlias = 'OptStr' # TC008 +20 20 | h: TypeAlias = 'Bar' # TC008 +21 |- i: TypeAlias = Foo['str'] # TC008 + 21 |+ i: TypeAlias = Foo[str] # TC008 +22 22 | j: TypeAlias = 'Baz' # TC008 +23 23 | k: TypeAlias = 'k | None' # TC008 +24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) + +TC008_typing_execution_context.py:22:20: TC008 [*] Remove quotes from type alias + | +20 | h: TypeAlias = 'Bar' # TC008 +21 | i: TypeAlias = Foo['str'] # TC008 +22 | j: TypeAlias = 'Baz' # TC008 + | ^^^^^ TC008 +23 | k: TypeAlias = 'k | None' # TC008 +24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) + | + = help: Remove quotes + +ℹ Safe fix +19 19 | g: TypeAlias = 'OptStr' # TC008 +20 20 | h: TypeAlias = 'Bar' # TC008 +21 21 | i: TypeAlias = Foo['str'] # TC008 +22 |- j: TypeAlias = 'Baz' # TC008 + 22 |+ j: TypeAlias = Baz # TC008 +23 23 | k: TypeAlias = 'k | None' # TC008 +24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) +25 25 | m: TypeAlias = ('int' # TC008 + +TC008_typing_execution_context.py:23:20: TC008 [*] Remove quotes from type alias + | +21 | i: TypeAlias = Foo['str'] # TC008 +22 | j: TypeAlias = 'Baz' # TC008 +23 | k: TypeAlias = 'k | None' # TC008 + | ^^^^^^^^^^ TC008 +24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) +25 | m: TypeAlias = ('int' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +20 20 | h: TypeAlias = 'Bar' # TC008 +21 21 | i: TypeAlias = Foo['str'] # TC008 +22 22 | j: TypeAlias = 'Baz' # TC008 +23 |- k: TypeAlias = 'k | None' # TC008 + 23 |+ k: TypeAlias = k | None # TC008 +24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) +25 25 | m: TypeAlias = ('int' # TC008 +26 26 | | None) + +TC008_typing_execution_context.py:24:20: TC008 [*] Remove quotes from type alias + | +22 | j: TypeAlias = 'Baz' # TC008 +23 | k: TypeAlias = 'k | None' # TC008 +24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) + | ^^^^^ TC008 +25 | m: TypeAlias = ('int' # TC008 +26 | | None) + | + = help: Remove quotes + +ℹ Safe fix +21 21 | i: TypeAlias = Foo['str'] # TC008 +22 22 | j: TypeAlias = 'Baz' # TC008 +23 23 | k: TypeAlias = 'k | None' # TC008 +24 |- l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) + 24 |+ l: TypeAlias = int | None # TC008 (because TC010 is not enabled) +25 25 | m: TypeAlias = ('int' # TC008 +26 26 | | None) +27 27 | n: TypeAlias = ('int' # TC008 (fix removes comment currently) + +TC008_typing_execution_context.py:25:21: TC008 [*] Remove quotes from type alias + | +23 | k: TypeAlias = 'k | None' # TC008 +24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) +25 | m: TypeAlias = ('int' # TC008 + | ^^^^^ TC008 +26 | | None) +27 | n: TypeAlias = ('int' # TC008 (fix removes comment currently) + | + = help: Remove quotes + +ℹ Safe fix +22 22 | j: TypeAlias = 'Baz' # TC008 +23 23 | k: TypeAlias = 'k | None' # TC008 +24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) +25 |- m: TypeAlias = ('int' # TC008 + 25 |+ m: TypeAlias = (int # TC008 +26 26 | | None) +27 27 | n: TypeAlias = ('int' # TC008 (fix removes comment currently) +28 28 | ' | None') + +TC008_typing_execution_context.py:27:21: TC008 [*] Remove quotes from type alias + | +25 | m: TypeAlias = ('int' # TC008 +26 | | None) +27 | n: TypeAlias = ('int' # TC008 (fix removes comment currently) + | _____________________^ +28 | | ' | None') + | |_________________^ TC008 + | + = help: Remove quotes + +ℹ Unsafe fix +24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) +25 25 | m: TypeAlias = ('int' # TC008 +26 26 | | None) +27 |- n: TypeAlias = ('int' # TC008 (fix removes comment currently) +28 |- ' | None') + 27 |+ n: TypeAlias = (int | None) From 348fd86b520efd6ae6308fb3fba9045de16d0353 Mon Sep 17 00:00:00 2001 From: Daverball Date: Sun, 29 Dec 2024 09:48:35 +0100 Subject: [PATCH 2/9] Execution context doesn't work, since it's already set to typing inside a quoted type expression. --- .../src/rules/flake8_type_checking/rules/type_alias_quotes.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index 8f470f631743b2..cd6f011c01bf85 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -260,7 +260,8 @@ pub(crate) fn quoted_type_alias( // explicit type aliases require some additional checks to avoid false positives if checker.semantic().in_annotated_type_alias_value() - && checker.semantic().execution_context().is_runtime() + && !checker.source_type.is_stub() + && !checker.semantic().in_type_checking_block() && quotes_are_unremovable(checker.semantic(), expr) { return; From a9f2d802ac1a977379bc761dce77e06d6fe74d4b Mon Sep 17 00:00:00 2001 From: Daverball Date: Sun, 29 Dec 2024 23:26:31 +0100 Subject: [PATCH 3/9] Avoids false positives for true forward references --- .../fixtures/flake8_type_checking/TC008.pyi | 4 +- .../TC008_typing_execution_context.py | 2 +- .../rules/type_alias_quotes.rs | 37 +++++++++++---- ...g__tests__quoted-type-alias_TC008.pyi.snap | 46 ++----------------- ...ias_TC008_typing_execution_context.py.snap | 39 ++++------------ .../ruff/rules/missing_fstring_syntax.rs | 1 + crates/ruff_python_semantic/src/model.rs | 20 ++++++-- 7 files changed, 62 insertions(+), 87 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi index 9a47a164627953..c521a4245c7e83 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi @@ -15,9 +15,9 @@ type D = 'Foo.bar' # TC008 class Baz: - a: TypeAlias = 'Baz' # TC008 + a: TypeAlias = 'Baz' # False negative in stubs type A = 'Baz' # TC008 class Nested: - a: TypeAlias = 'Baz' # TC008 + a: TypeAlias = 'Baz' # False negative in stubs type A = 'Baz' # TC008 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py index 2883569d6f734e..da561b23564f42 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py @@ -20,7 +20,7 @@ h: TypeAlias = 'Bar' # TC008 i: TypeAlias = Foo['str'] # TC008 j: TypeAlias = 'Baz' # TC008 - k: TypeAlias = 'k | None' # TC008 + k: TypeAlias = 'k | None' # False negative in type checking block l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) m: TypeAlias = ('int' # TC008 | None) diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index cd6f011c01bf85..cf15b2a653d6b8 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -219,7 +219,11 @@ fn collect_typing_references<'a>( let Some(binding_id) = checker.semantic().resolve_name(name) else { return; }; - if checker.semantic().simulate_runtime_load(name).is_some() { + if checker + .semantic() + .simulate_runtime_load(name, false) + .is_some() + { return; } @@ -260,8 +264,6 @@ pub(crate) fn quoted_type_alias( // explicit type aliases require some additional checks to avoid false positives if checker.semantic().in_annotated_type_alias_value() - && !checker.source_type.is_stub() - && !checker.semantic().in_type_checking_block() && quotes_are_unremovable(checker.semantic(), expr) { return; @@ -293,11 +295,22 @@ fn quotes_are_unremovable(semantic: &SemanticModel, expr: &Expr) -> bool { ctx: ExprContext::Load, .. }) => quotes_are_unremovable(semantic, value), - // for subscripts and attributes we don't know whether it's safe - // to do at runtime, since the operation may only be available at - // type checking time. E.g. stubs only generics. Or stubs only - // type aliases. - Expr::Subscript(_) | Expr::Attribute(_) => true, + Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { + // for subscripts we don't know whether it's safe to do at runtime + // since the operation may only be available at type checking time. + // E.g. stubs only generics. + if !semantic.in_stub_file() && !semantic.in_type_checking_block() { + return true; + } + quotes_are_unremovable(semantic, value) || quotes_are_unremovable(semantic, slice) + } + Expr::Attribute(ast::ExprAttribute { value, .. }) => { + // for attributes we also don't know whether it's safe + if !semantic.in_stub_file() && !semantic.in_type_checking_block() { + return true; + } + quotes_are_unremovable(semantic, value) + } Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => { for elt in elts { if quotes_are_unremovable(semantic, elt) { @@ -307,7 +320,13 @@ fn quotes_are_unremovable(semantic: &SemanticModel, expr: &Expr) -> bool { false } Expr::Name(name) => { - semantic.resolve_name(name).is_some() && semantic.simulate_runtime_load(name).is_none() + semantic.resolve_name(name).is_some() + && semantic + .simulate_runtime_load( + name, + semantic.in_stub_file() || semantic.in_type_checking_block(), + ) + .is_none() } _ => false, } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap index db420b6432353e..560f4113b4ac7d 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap @@ -142,29 +142,10 @@ TC008.pyi:14:10: TC008 [*] Remove quotes from type alias 16 16 | 17 17 | class Baz: -TC008.pyi:18:20: TC008 [*] Remove quotes from type alias - | -17 | class Baz: -18 | a: TypeAlias = 'Baz' # TC008 - | ^^^^^ TC008 -19 | type A = 'Baz' # TC008 - | - = help: Remove quotes - -ℹ Safe fix -15 15 | -16 16 | -17 17 | class Baz: -18 |- a: TypeAlias = 'Baz' # TC008 - 18 |+ a: TypeAlias = Baz # TC008 -19 19 | type A = 'Baz' # TC008 -20 20 | -21 21 | class Nested: - TC008.pyi:19:14: TC008 [*] Remove quotes from type alias | 17 | class Baz: -18 | a: TypeAlias = 'Baz' # TC008 +18 | a: TypeAlias = 'Baz' # False negative in stubs 19 | type A = 'Baz' # TC008 | ^^^^^ TC008 20 | @@ -175,34 +156,17 @@ TC008.pyi:19:14: TC008 [*] Remove quotes from type alias ℹ Safe fix 16 16 | 17 17 | class Baz: -18 18 | a: TypeAlias = 'Baz' # TC008 +18 18 | a: TypeAlias = 'Baz' # False negative in stubs 19 |- type A = 'Baz' # TC008 19 |+ type A = Baz # TC008 20 20 | 21 21 | class Nested: -22 22 | a: TypeAlias = 'Baz' # TC008 - -TC008.pyi:22:24: TC008 [*] Remove quotes from type alias - | -21 | class Nested: -22 | a: TypeAlias = 'Baz' # TC008 - | ^^^^^ TC008 -23 | type A = 'Baz' # TC008 - | - = help: Remove quotes - -ℹ Safe fix -19 19 | type A = 'Baz' # TC008 -20 20 | -21 21 | class Nested: -22 |- a: TypeAlias = 'Baz' # TC008 - 22 |+ a: TypeAlias = Baz # TC008 -23 23 | type A = 'Baz' # TC008 +22 22 | a: TypeAlias = 'Baz' # False negative in stubs TC008.pyi:23:18: TC008 [*] Remove quotes from type alias | 21 | class Nested: -22 | a: TypeAlias = 'Baz' # TC008 +22 | a: TypeAlias = 'Baz' # False negative in stubs 23 | type A = 'Baz' # TC008 | ^^^^^ TC008 | @@ -211,6 +175,6 @@ TC008.pyi:23:18: TC008 [*] Remove quotes from type alias ℹ Safe fix 20 20 | 21 21 | class Nested: -22 22 | a: TypeAlias = 'Baz' # TC008 +22 22 | a: TypeAlias = 'Baz' # False negative in stubs 23 |- type A = 'Baz' # TC008 23 |+ type A = Baz # TC008 diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap index a5884d0863d597..1cf8b18b2beeb8 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap @@ -166,7 +166,7 @@ TC008_typing_execution_context.py:20:20: TC008 [*] Remove quotes from type alias 20 |+ h: TypeAlias = Bar # TC008 21 21 | i: TypeAlias = Foo['str'] # TC008 22 22 | j: TypeAlias = 'Baz' # TC008 -23 23 | k: TypeAlias = 'k | None' # TC008 +23 23 | k: TypeAlias = 'k | None' # False negative in type checking block TC008_typing_execution_context.py:21:24: TC008 [*] Remove quotes from type alias | @@ -175,7 +175,7 @@ TC008_typing_execution_context.py:21:24: TC008 [*] Remove quotes from type alias 21 | i: TypeAlias = Foo['str'] # TC008 | ^^^^^ TC008 22 | j: TypeAlias = 'Baz' # TC008 -23 | k: TypeAlias = 'k | None' # TC008 +23 | k: TypeAlias = 'k | None' # False negative in type checking block | = help: Remove quotes @@ -186,7 +186,7 @@ TC008_typing_execution_context.py:21:24: TC008 [*] Remove quotes from type alias 21 |- i: TypeAlias = Foo['str'] # TC008 21 |+ i: TypeAlias = Foo[str] # TC008 22 22 | j: TypeAlias = 'Baz' # TC008 -23 23 | k: TypeAlias = 'k | None' # TC008 +23 23 | k: TypeAlias = 'k | None' # False negative in type checking block 24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) TC008_typing_execution_context.py:22:20: TC008 [*] Remove quotes from type alias @@ -195,7 +195,7 @@ TC008_typing_execution_context.py:22:20: TC008 [*] Remove quotes from type alias 21 | i: TypeAlias = Foo['str'] # TC008 22 | j: TypeAlias = 'Baz' # TC008 | ^^^^^ TC008 -23 | k: TypeAlias = 'k | None' # TC008 +23 | k: TypeAlias = 'k | None' # False negative in type checking block 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) | = help: Remove quotes @@ -206,35 +206,14 @@ TC008_typing_execution_context.py:22:20: TC008 [*] Remove quotes from type alias 21 21 | i: TypeAlias = Foo['str'] # TC008 22 |- j: TypeAlias = 'Baz' # TC008 22 |+ j: TypeAlias = Baz # TC008 -23 23 | k: TypeAlias = 'k | None' # TC008 +23 23 | k: TypeAlias = 'k | None' # False negative in type checking block 24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) 25 25 | m: TypeAlias = ('int' # TC008 -TC008_typing_execution_context.py:23:20: TC008 [*] Remove quotes from type alias - | -21 | i: TypeAlias = Foo['str'] # TC008 -22 | j: TypeAlias = 'Baz' # TC008 -23 | k: TypeAlias = 'k | None' # TC008 - | ^^^^^^^^^^ TC008 -24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) -25 | m: TypeAlias = ('int' # TC008 - | - = help: Remove quotes - -ℹ Safe fix -20 20 | h: TypeAlias = 'Bar' # TC008 -21 21 | i: TypeAlias = Foo['str'] # TC008 -22 22 | j: TypeAlias = 'Baz' # TC008 -23 |- k: TypeAlias = 'k | None' # TC008 - 23 |+ k: TypeAlias = k | None # TC008 -24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) -25 25 | m: TypeAlias = ('int' # TC008 -26 26 | | None) - TC008_typing_execution_context.py:24:20: TC008 [*] Remove quotes from type alias | 22 | j: TypeAlias = 'Baz' # TC008 -23 | k: TypeAlias = 'k | None' # TC008 +23 | k: TypeAlias = 'k | None' # False negative in type checking block 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) | ^^^^^ TC008 25 | m: TypeAlias = ('int' # TC008 @@ -245,7 +224,7 @@ TC008_typing_execution_context.py:24:20: TC008 [*] Remove quotes from type alias ℹ Safe fix 21 21 | i: TypeAlias = Foo['str'] # TC008 22 22 | j: TypeAlias = 'Baz' # TC008 -23 23 | k: TypeAlias = 'k | None' # TC008 +23 23 | k: TypeAlias = 'k | None' # False negative in type checking block 24 |- l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) 24 |+ l: TypeAlias = int | None # TC008 (because TC010 is not enabled) 25 25 | m: TypeAlias = ('int' # TC008 @@ -254,7 +233,7 @@ TC008_typing_execution_context.py:24:20: TC008 [*] Remove quotes from type alias TC008_typing_execution_context.py:25:21: TC008 [*] Remove quotes from type alias | -23 | k: TypeAlias = 'k | None' # TC008 +23 | k: TypeAlias = 'k | None' # False negative in type checking block 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) 25 | m: TypeAlias = ('int' # TC008 | ^^^^^ TC008 @@ -265,7 +244,7 @@ TC008_typing_execution_context.py:25:21: TC008 [*] Remove quotes from type alias ℹ Safe fix 22 22 | j: TypeAlias = 'Baz' # TC008 -23 23 | k: TypeAlias = 'k | None' # TC008 +23 23 | k: TypeAlias = 'k | None' # False negative in type checking block 24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) 25 |- m: TypeAlias = ('int' # TC008 25 |+ m: TypeAlias = (int # TC008 diff --git a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs index ca19c06346d23a..6678b90d66ac03 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs @@ -216,6 +216,7 @@ fn should_be_fstring( id, literal.range(), semantic.scope_id, + false, ) .map_or(true, |id| semantic.binding(id).kind.is_builtin()) { diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index f43cf24de1ec5e..ae8997158e0a35 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -710,8 +710,17 @@ impl<'a> SemanticModel<'a> { /// /// References from within an [`ast::Comprehension`] can produce incorrect /// results when referring to a [`BindingKind::NamedExprAssignment`]. - pub fn simulate_runtime_load(&self, name: &ast::ExprName) -> Option { - self.simulate_runtime_load_at_location_in_scope(name.id.as_str(), name.range, self.scope_id) + pub fn simulate_runtime_load( + &self, + name: &ast::ExprName, + allow_typing_only_bindings: bool, + ) -> Option { + self.simulate_runtime_load_at_location_in_scope( + name.id.as_str(), + name.range, + self.scope_id, + allow_typing_only_bindings, + ) } /// Simulates a runtime load of the given symbol. @@ -743,6 +752,7 @@ impl<'a> SemanticModel<'a> { symbol: &str, symbol_range: TextRange, scope_id: ScopeId, + allow_typing_only_bindings: bool, ) -> Option { let mut seen_function = false; let mut class_variables_visible = true; @@ -785,7 +795,7 @@ impl<'a> SemanticModel<'a> { // runtime binding with a source-order inaccurate one for shadowed_id in scope.shadowed_bindings(binding_id) { let binding = &self.bindings[shadowed_id]; - if binding.context.is_typing() { + if !allow_typing_only_bindings && binding.context.is_typing() { continue; } if let BindingKind::Annotation @@ -820,7 +830,9 @@ impl<'a> SemanticModel<'a> { _ => binding_id, }; - if self.bindings[candidate_id].context.is_typing() { + if !allow_typing_only_bindings + && self.bindings[candidate_id].context.is_typing() + { continue; } From 39f4e10a42a64c0eeb83b5e42f02f651d7615754 Mon Sep 17 00:00:00 2001 From: Daverball Date: Sun, 29 Dec 2024 23:33:49 +0100 Subject: [PATCH 4/9] Adds some use-before-define test cases --- .../fixtures/flake8_type_checking/TC008.pyi | 2 + .../TC008_typing_execution_context.py | 5 +- ...g__tests__quoted-type-alias_TC008.pyi.snap | 169 ++++++++++-------- ...ias_TC008_typing_execution_context.py.snap | 40 ++--- 4 files changed, 112 insertions(+), 104 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi index c521a4245c7e83..024cfca7fbd9cf 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi @@ -8,10 +8,12 @@ a: TypeAlias = 'int' # TC008 b: TypeAlias = 'Foo' # TC008 c: TypeAlias = 'Foo[str]' # TC008 d: TypeAlias = 'Foo.bar' # TC008 +e: TypeAlias = 'Baz' # OK type B = 'Foo' # TC008 type C = 'Foo[str]' # TC008 type D = 'Foo.bar' # TC008 +type E = 'Baz' # TC008 class Baz: diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py index da561b23564f42..86e7ec4a4a3b8e 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py @@ -19,10 +19,13 @@ g: TypeAlias = 'OptStr' # TC008 h: TypeAlias = 'Bar' # TC008 i: TypeAlias = Foo['str'] # TC008 - j: TypeAlias = 'Baz' # TC008 + j: TypeAlias = 'Baz' # OK (this would be treated as use before define) k: TypeAlias = 'k | None' # False negative in type checking block l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) m: TypeAlias = ('int' # TC008 | None) n: TypeAlias = ('int' # TC008 (fix removes comment currently) ' | None') + + + class Baz: ... diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap index 560f4113b4ac7d..1fd20f6a9ab1a8 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap @@ -40,7 +40,7 @@ TC008.pyi:8:16: TC008 [*] Remove quotes from type alias 8 |+b: TypeAlias = Foo # TC008 9 9 | c: TypeAlias = 'Foo[str]' # TC008 10 10 | d: TypeAlias = 'Foo.bar' # TC008 -11 11 | +11 11 | e: TypeAlias = 'Baz' # OK TC008.pyi:9:16: TC008 [*] Remove quotes from type alias | @@ -49,6 +49,7 @@ TC008.pyi:9:16: TC008 [*] Remove quotes from type alias 9 | c: TypeAlias = 'Foo[str]' # TC008 | ^^^^^^^^^^ TC008 10 | d: TypeAlias = 'Foo.bar' # TC008 +11 | e: TypeAlias = 'Baz' # OK | = help: Remove quotes @@ -59,8 +60,8 @@ TC008.pyi:9:16: TC008 [*] Remove quotes from type alias 9 |-c: TypeAlias = 'Foo[str]' # TC008 9 |+c: TypeAlias = Foo[str] # TC008 10 10 | d: TypeAlias = 'Foo.bar' # TC008 -11 11 | -12 12 | type B = 'Foo' # TC008 +11 11 | e: TypeAlias = 'Baz' # OK +12 12 | TC008.pyi:10:16: TC008 [*] Remove quotes from type alias | @@ -68,8 +69,7 @@ TC008.pyi:10:16: TC008 [*] Remove quotes from type alias 9 | c: TypeAlias = 'Foo[str]' # TC008 10 | d: TypeAlias = 'Foo.bar' # TC008 | ^^^^^^^^^ TC008 -11 | -12 | type B = 'Foo' # TC008 +11 | e: TypeAlias = 'Baz' # OK | = help: Remove quotes @@ -79,102 +79,123 @@ TC008.pyi:10:16: TC008 [*] Remove quotes from type alias 9 9 | c: TypeAlias = 'Foo[str]' # TC008 10 |-d: TypeAlias = 'Foo.bar' # TC008 10 |+d: TypeAlias = Foo.bar # TC008 -11 11 | -12 12 | type B = 'Foo' # TC008 -13 13 | type C = 'Foo[str]' # TC008 +11 11 | e: TypeAlias = 'Baz' # OK +12 12 | +13 13 | type B = 'Foo' # TC008 -TC008.pyi:12:10: TC008 [*] Remove quotes from type alias +TC008.pyi:13:10: TC008 [*] Remove quotes from type alias | -10 | d: TypeAlias = 'Foo.bar' # TC008 -11 | -12 | type B = 'Foo' # TC008 +11 | e: TypeAlias = 'Baz' # OK +12 | +13 | type B = 'Foo' # TC008 | ^^^^^ TC008 -13 | type C = 'Foo[str]' # TC008 -14 | type D = 'Foo.bar' # TC008 +14 | type C = 'Foo[str]' # TC008 +15 | type D = 'Foo.bar' # TC008 | = help: Remove quotes ℹ Safe fix -9 9 | c: TypeAlias = 'Foo[str]' # TC008 10 10 | d: TypeAlias = 'Foo.bar' # TC008 -11 11 | -12 |-type B = 'Foo' # TC008 - 12 |+type B = Foo # TC008 -13 13 | type C = 'Foo[str]' # TC008 -14 14 | type D = 'Foo.bar' # TC008 -15 15 | +11 11 | e: TypeAlias = 'Baz' # OK +12 12 | +13 |-type B = 'Foo' # TC008 + 13 |+type B = Foo # TC008 +14 14 | type C = 'Foo[str]' # TC008 +15 15 | type D = 'Foo.bar' # TC008 +16 16 | type E = 'Baz' # TC008 -TC008.pyi:13:10: TC008 [*] Remove quotes from type alias +TC008.pyi:14:10: TC008 [*] Remove quotes from type alias | -12 | type B = 'Foo' # TC008 -13 | type C = 'Foo[str]' # TC008 +13 | type B = 'Foo' # TC008 +14 | type C = 'Foo[str]' # TC008 | ^^^^^^^^^^ TC008 -14 | type D = 'Foo.bar' # TC008 +15 | type D = 'Foo.bar' # TC008 +16 | type E = 'Baz' # TC008 | = help: Remove quotes ℹ Safe fix -10 10 | d: TypeAlias = 'Foo.bar' # TC008 -11 11 | -12 12 | type B = 'Foo' # TC008 -13 |-type C = 'Foo[str]' # TC008 - 13 |+type C = Foo[str] # TC008 -14 14 | type D = 'Foo.bar' # TC008 -15 15 | -16 16 | - -TC008.pyi:14:10: TC008 [*] Remove quotes from type alias - | -12 | type B = 'Foo' # TC008 -13 | type C = 'Foo[str]' # TC008 -14 | type D = 'Foo.bar' # TC008 +11 11 | e: TypeAlias = 'Baz' # OK +12 12 | +13 13 | type B = 'Foo' # TC008 +14 |-type C = 'Foo[str]' # TC008 + 14 |+type C = Foo[str] # TC008 +15 15 | type D = 'Foo.bar' # TC008 +16 16 | type E = 'Baz' # TC008 +17 17 | + +TC008.pyi:15:10: TC008 [*] Remove quotes from type alias + | +13 | type B = 'Foo' # TC008 +14 | type C = 'Foo[str]' # TC008 +15 | type D = 'Foo.bar' # TC008 | ^^^^^^^^^ TC008 +16 | type E = 'Baz' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +12 12 | +13 13 | type B = 'Foo' # TC008 +14 14 | type C = 'Foo[str]' # TC008 +15 |-type D = 'Foo.bar' # TC008 + 15 |+type D = Foo.bar # TC008 +16 16 | type E = 'Baz' # TC008 +17 17 | +18 18 | + +TC008.pyi:16:10: TC008 [*] Remove quotes from type alias + | +14 | type C = 'Foo[str]' # TC008 +15 | type D = 'Foo.bar' # TC008 +16 | type E = 'Baz' # TC008 + | ^^^^^ TC008 | = help: Remove quotes ℹ Safe fix -11 11 | -12 12 | type B = 'Foo' # TC008 -13 13 | type C = 'Foo[str]' # TC008 -14 |-type D = 'Foo.bar' # TC008 - 14 |+type D = Foo.bar # TC008 -15 15 | -16 16 | -17 17 | class Baz: - -TC008.pyi:19:14: TC008 [*] Remove quotes from type alias - | -17 | class Baz: -18 | a: TypeAlias = 'Baz' # False negative in stubs -19 | type A = 'Baz' # TC008 +13 13 | type B = 'Foo' # TC008 +14 14 | type C = 'Foo[str]' # TC008 +15 15 | type D = 'Foo.bar' # TC008 +16 |-type E = 'Baz' # TC008 + 16 |+type E = Baz # TC008 +17 17 | +18 18 | +19 19 | class Baz: + +TC008.pyi:21:14: TC008 [*] Remove quotes from type alias + | +19 | class Baz: +20 | a: TypeAlias = 'Baz' # False negative in stubs +21 | type A = 'Baz' # TC008 | ^^^^^ TC008 -20 | -21 | class Nested: +22 | +23 | class Nested: | = help: Remove quotes ℹ Safe fix -16 16 | -17 17 | class Baz: -18 18 | a: TypeAlias = 'Baz' # False negative in stubs -19 |- type A = 'Baz' # TC008 - 19 |+ type A = Baz # TC008 -20 20 | -21 21 | class Nested: -22 22 | a: TypeAlias = 'Baz' # False negative in stubs - -TC008.pyi:23:18: TC008 [*] Remove quotes from type alias - | -21 | class Nested: -22 | a: TypeAlias = 'Baz' # False negative in stubs -23 | type A = 'Baz' # TC008 +18 18 | +19 19 | class Baz: +20 20 | a: TypeAlias = 'Baz' # False negative in stubs +21 |- type A = 'Baz' # TC008 + 21 |+ type A = Baz # TC008 +22 22 | +23 23 | class Nested: +24 24 | a: TypeAlias = 'Baz' # False negative in stubs + +TC008.pyi:25:18: TC008 [*] Remove quotes from type alias + | +23 | class Nested: +24 | a: TypeAlias = 'Baz' # False negative in stubs +25 | type A = 'Baz' # TC008 | ^^^^^ TC008 | = help: Remove quotes ℹ Safe fix -20 20 | -21 21 | class Nested: -22 22 | a: TypeAlias = 'Baz' # False negative in stubs -23 |- type A = 'Baz' # TC008 - 23 |+ type A = Baz # TC008 +22 22 | +23 23 | class Nested: +24 24 | a: TypeAlias = 'Baz' # False negative in stubs +25 |- type A = 'Baz' # TC008 + 25 |+ type A = Baz # TC008 diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap index 1cf8b18b2beeb8..0da908ea8ef87f 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap @@ -145,7 +145,7 @@ TC008_typing_execution_context.py:19:20: TC008 [*] Remove quotes from type alias 19 |+ g: TypeAlias = OptStr # TC008 20 20 | h: TypeAlias = 'Bar' # TC008 21 21 | i: TypeAlias = Foo['str'] # TC008 -22 22 | j: TypeAlias = 'Baz' # TC008 +22 22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) TC008_typing_execution_context.py:20:20: TC008 [*] Remove quotes from type alias | @@ -154,7 +154,7 @@ TC008_typing_execution_context.py:20:20: TC008 [*] Remove quotes from type alias 20 | h: TypeAlias = 'Bar' # TC008 | ^^^^^ TC008 21 | i: TypeAlias = Foo['str'] # TC008 -22 | j: TypeAlias = 'Baz' # TC008 +22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) | = help: Remove quotes @@ -165,7 +165,7 @@ TC008_typing_execution_context.py:20:20: TC008 [*] Remove quotes from type alias 20 |- h: TypeAlias = 'Bar' # TC008 20 |+ h: TypeAlias = Bar # TC008 21 21 | i: TypeAlias = Foo['str'] # TC008 -22 22 | j: TypeAlias = 'Baz' # TC008 +22 22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) 23 23 | k: TypeAlias = 'k | None' # False negative in type checking block TC008_typing_execution_context.py:21:24: TC008 [*] Remove quotes from type alias @@ -174,7 +174,7 @@ TC008_typing_execution_context.py:21:24: TC008 [*] Remove quotes from type alias 20 | h: TypeAlias = 'Bar' # TC008 21 | i: TypeAlias = Foo['str'] # TC008 | ^^^^^ TC008 -22 | j: TypeAlias = 'Baz' # TC008 +22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) 23 | k: TypeAlias = 'k | None' # False negative in type checking block | = help: Remove quotes @@ -185,34 +185,13 @@ TC008_typing_execution_context.py:21:24: TC008 [*] Remove quotes from type alias 20 20 | h: TypeAlias = 'Bar' # TC008 21 |- i: TypeAlias = Foo['str'] # TC008 21 |+ i: TypeAlias = Foo[str] # TC008 -22 22 | j: TypeAlias = 'Baz' # TC008 +22 22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) 23 23 | k: TypeAlias = 'k | None' # False negative in type checking block 24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) -TC008_typing_execution_context.py:22:20: TC008 [*] Remove quotes from type alias - | -20 | h: TypeAlias = 'Bar' # TC008 -21 | i: TypeAlias = Foo['str'] # TC008 -22 | j: TypeAlias = 'Baz' # TC008 - | ^^^^^ TC008 -23 | k: TypeAlias = 'k | None' # False negative in type checking block -24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) - | - = help: Remove quotes - -ℹ Safe fix -19 19 | g: TypeAlias = 'OptStr' # TC008 -20 20 | h: TypeAlias = 'Bar' # TC008 -21 21 | i: TypeAlias = Foo['str'] # TC008 -22 |- j: TypeAlias = 'Baz' # TC008 - 22 |+ j: TypeAlias = Baz # TC008 -23 23 | k: TypeAlias = 'k | None' # False negative in type checking block -24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) -25 25 | m: TypeAlias = ('int' # TC008 - TC008_typing_execution_context.py:24:20: TC008 [*] Remove quotes from type alias | -22 | j: TypeAlias = 'Baz' # TC008 +22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) 23 | k: TypeAlias = 'k | None' # False negative in type checking block 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) | ^^^^^ TC008 @@ -223,7 +202,7 @@ TC008_typing_execution_context.py:24:20: TC008 [*] Remove quotes from type alias ℹ Safe fix 21 21 | i: TypeAlias = Foo['str'] # TC008 -22 22 | j: TypeAlias = 'Baz' # TC008 +22 22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) 23 23 | k: TypeAlias = 'k | None' # False negative in type checking block 24 |- l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) 24 |+ l: TypeAlias = int | None # TC008 (because TC010 is not enabled) @@ -243,7 +222,7 @@ TC008_typing_execution_context.py:25:21: TC008 [*] Remove quotes from type alias = help: Remove quotes ℹ Safe fix -22 22 | j: TypeAlias = 'Baz' # TC008 +22 22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) 23 23 | k: TypeAlias = 'k | None' # False negative in type checking block 24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) 25 |- m: TypeAlias = ('int' # TC008 @@ -270,3 +249,6 @@ TC008_typing_execution_context.py:27:21: TC008 [*] Remove quotes from type alias 27 |- n: TypeAlias = ('int' # TC008 (fix removes comment currently) 28 |- ' | None') 27 |+ n: TypeAlias = (int | None) +29 28 | +30 29 | +31 30 | class Baz: ... From 44dccdc02b0b88cbbc4927420fceb09a6043a2bd Mon Sep 17 00:00:00 2001 From: Daverball Date: Fri, 3 Jan 2025 14:00:49 +0100 Subject: [PATCH 5/9] Disable `TC008` in stubs. Clean up stuff that's no longer needed. --- .../fixtures/flake8_type_checking/TC008.pyi | 25 --- crates/ruff_linter/src/checkers/ast/mod.rs | 2 +- .../src/rules/flake8_type_checking/mod.rs | 1 - .../rules/type_alias_quotes.rs | 9 +- ...g__tests__quoted-type-alias_TC008.pyi.snap | 201 ------------------ 5 files changed, 4 insertions(+), 234 deletions(-) delete mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi delete mode 100644 crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi deleted file mode 100644 index 024cfca7fbd9cf..00000000000000 --- a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.pyi +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations - -from typing import TypeAlias, TYPE_CHECKING - -from foo import Foo - -a: TypeAlias = 'int' # TC008 -b: TypeAlias = 'Foo' # TC008 -c: TypeAlias = 'Foo[str]' # TC008 -d: TypeAlias = 'Foo.bar' # TC008 -e: TypeAlias = 'Baz' # OK - -type B = 'Foo' # TC008 -type C = 'Foo[str]' # TC008 -type D = 'Foo.bar' # TC008 -type E = 'Baz' # TC008 - - -class Baz: - a: TypeAlias = 'Baz' # False negative in stubs - type A = 'Baz' # TC008 - - class Nested: - a: TypeAlias = 'Baz' # False negative in stubs - type A = 'Baz' # TC008 diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index e88d87d0e9cdf6..5b6994a7befdab 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -2324,7 +2324,7 @@ impl<'a> Checker<'a> { let parsed_expr = parsed_annotation.expression(); self.visit_expr(parsed_expr); if self.semantic.in_type_alias_value() { - if self.enabled(Rule::QuotedTypeAlias) { + if !self.source_type.is_stub() && self.enabled(Rule::QuotedTypeAlias) { flake8_type_checking::rules::quoted_type_alias( self, parsed_expr, diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs index 84b6771c0651dd..7719a83cf21f91 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs @@ -67,7 +67,6 @@ mod tests { // so we want to make sure their fixes are not going around in circles. #[test_case(Rule::UnquotedTypeAlias, Path::new("TC007.py"))] #[test_case(Rule::QuotedTypeAlias, Path::new("TC008.py"))] - #[test_case(Rule::QuotedTypeAlias, Path::new("TC008.pyi"))] #[test_case(Rule::QuotedTypeAlias, Path::new("TC008_typing_execution_context.py"))] fn type_alias_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index cf15b2a653d6b8..92b1d7d07775a3 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -299,14 +299,14 @@ fn quotes_are_unremovable(semantic: &SemanticModel, expr: &Expr) -> bool { // for subscripts we don't know whether it's safe to do at runtime // since the operation may only be available at type checking time. // E.g. stubs only generics. - if !semantic.in_stub_file() && !semantic.in_type_checking_block() { + if !semantic.in_type_checking_block() { return true; } quotes_are_unremovable(semantic, value) || quotes_are_unremovable(semantic, slice) } Expr::Attribute(ast::ExprAttribute { value, .. }) => { // for attributes we also don't know whether it's safe - if !semantic.in_stub_file() && !semantic.in_type_checking_block() { + if !semantic.in_type_checking_block() { return true; } quotes_are_unremovable(semantic, value) @@ -322,10 +322,7 @@ fn quotes_are_unremovable(semantic: &SemanticModel, expr: &Expr) -> bool { Expr::Name(name) => { semantic.resolve_name(name).is_some() && semantic - .simulate_runtime_load( - name, - semantic.in_stub_file() || semantic.in_type_checking_block(), - ) + .simulate_runtime_load(name, semantic.in_type_checking_block()) .is_none() } _ => false, diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap deleted file mode 100644 index 1fd20f6a9ab1a8..00000000000000 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.pyi.snap +++ /dev/null @@ -1,201 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs ---- -TC008.pyi:7:16: TC008 [*] Remove quotes from type alias - | -5 | from foo import Foo -6 | -7 | a: TypeAlias = 'int' # TC008 - | ^^^^^ TC008 -8 | b: TypeAlias = 'Foo' # TC008 -9 | c: TypeAlias = 'Foo[str]' # TC008 - | - = help: Remove quotes - -ℹ Safe fix -4 4 | -5 5 | from foo import Foo -6 6 | -7 |-a: TypeAlias = 'int' # TC008 - 7 |+a: TypeAlias = int # TC008 -8 8 | b: TypeAlias = 'Foo' # TC008 -9 9 | c: TypeAlias = 'Foo[str]' # TC008 -10 10 | d: TypeAlias = 'Foo.bar' # TC008 - -TC008.pyi:8:16: TC008 [*] Remove quotes from type alias - | - 7 | a: TypeAlias = 'int' # TC008 - 8 | b: TypeAlias = 'Foo' # TC008 - | ^^^^^ TC008 - 9 | c: TypeAlias = 'Foo[str]' # TC008 -10 | d: TypeAlias = 'Foo.bar' # TC008 - | - = help: Remove quotes - -ℹ Safe fix -5 5 | from foo import Foo -6 6 | -7 7 | a: TypeAlias = 'int' # TC008 -8 |-b: TypeAlias = 'Foo' # TC008 - 8 |+b: TypeAlias = Foo # TC008 -9 9 | c: TypeAlias = 'Foo[str]' # TC008 -10 10 | d: TypeAlias = 'Foo.bar' # TC008 -11 11 | e: TypeAlias = 'Baz' # OK - -TC008.pyi:9:16: TC008 [*] Remove quotes from type alias - | - 7 | a: TypeAlias = 'int' # TC008 - 8 | b: TypeAlias = 'Foo' # TC008 - 9 | c: TypeAlias = 'Foo[str]' # TC008 - | ^^^^^^^^^^ TC008 -10 | d: TypeAlias = 'Foo.bar' # TC008 -11 | e: TypeAlias = 'Baz' # OK - | - = help: Remove quotes - -ℹ Safe fix -6 6 | -7 7 | a: TypeAlias = 'int' # TC008 -8 8 | b: TypeAlias = 'Foo' # TC008 -9 |-c: TypeAlias = 'Foo[str]' # TC008 - 9 |+c: TypeAlias = Foo[str] # TC008 -10 10 | d: TypeAlias = 'Foo.bar' # TC008 -11 11 | e: TypeAlias = 'Baz' # OK -12 12 | - -TC008.pyi:10:16: TC008 [*] Remove quotes from type alias - | - 8 | b: TypeAlias = 'Foo' # TC008 - 9 | c: TypeAlias = 'Foo[str]' # TC008 -10 | d: TypeAlias = 'Foo.bar' # TC008 - | ^^^^^^^^^ TC008 -11 | e: TypeAlias = 'Baz' # OK - | - = help: Remove quotes - -ℹ Safe fix -7 7 | a: TypeAlias = 'int' # TC008 -8 8 | b: TypeAlias = 'Foo' # TC008 -9 9 | c: TypeAlias = 'Foo[str]' # TC008 -10 |-d: TypeAlias = 'Foo.bar' # TC008 - 10 |+d: TypeAlias = Foo.bar # TC008 -11 11 | e: TypeAlias = 'Baz' # OK -12 12 | -13 13 | type B = 'Foo' # TC008 - -TC008.pyi:13:10: TC008 [*] Remove quotes from type alias - | -11 | e: TypeAlias = 'Baz' # OK -12 | -13 | type B = 'Foo' # TC008 - | ^^^^^ TC008 -14 | type C = 'Foo[str]' # TC008 -15 | type D = 'Foo.bar' # TC008 - | - = help: Remove quotes - -ℹ Safe fix -10 10 | d: TypeAlias = 'Foo.bar' # TC008 -11 11 | e: TypeAlias = 'Baz' # OK -12 12 | -13 |-type B = 'Foo' # TC008 - 13 |+type B = Foo # TC008 -14 14 | type C = 'Foo[str]' # TC008 -15 15 | type D = 'Foo.bar' # TC008 -16 16 | type E = 'Baz' # TC008 - -TC008.pyi:14:10: TC008 [*] Remove quotes from type alias - | -13 | type B = 'Foo' # TC008 -14 | type C = 'Foo[str]' # TC008 - | ^^^^^^^^^^ TC008 -15 | type D = 'Foo.bar' # TC008 -16 | type E = 'Baz' # TC008 - | - = help: Remove quotes - -ℹ Safe fix -11 11 | e: TypeAlias = 'Baz' # OK -12 12 | -13 13 | type B = 'Foo' # TC008 -14 |-type C = 'Foo[str]' # TC008 - 14 |+type C = Foo[str] # TC008 -15 15 | type D = 'Foo.bar' # TC008 -16 16 | type E = 'Baz' # TC008 -17 17 | - -TC008.pyi:15:10: TC008 [*] Remove quotes from type alias - | -13 | type B = 'Foo' # TC008 -14 | type C = 'Foo[str]' # TC008 -15 | type D = 'Foo.bar' # TC008 - | ^^^^^^^^^ TC008 -16 | type E = 'Baz' # TC008 - | - = help: Remove quotes - -ℹ Safe fix -12 12 | -13 13 | type B = 'Foo' # TC008 -14 14 | type C = 'Foo[str]' # TC008 -15 |-type D = 'Foo.bar' # TC008 - 15 |+type D = Foo.bar # TC008 -16 16 | type E = 'Baz' # TC008 -17 17 | -18 18 | - -TC008.pyi:16:10: TC008 [*] Remove quotes from type alias - | -14 | type C = 'Foo[str]' # TC008 -15 | type D = 'Foo.bar' # TC008 -16 | type E = 'Baz' # TC008 - | ^^^^^ TC008 - | - = help: Remove quotes - -ℹ Safe fix -13 13 | type B = 'Foo' # TC008 -14 14 | type C = 'Foo[str]' # TC008 -15 15 | type D = 'Foo.bar' # TC008 -16 |-type E = 'Baz' # TC008 - 16 |+type E = Baz # TC008 -17 17 | -18 18 | -19 19 | class Baz: - -TC008.pyi:21:14: TC008 [*] Remove quotes from type alias - | -19 | class Baz: -20 | a: TypeAlias = 'Baz' # False negative in stubs -21 | type A = 'Baz' # TC008 - | ^^^^^ TC008 -22 | -23 | class Nested: - | - = help: Remove quotes - -ℹ Safe fix -18 18 | -19 19 | class Baz: -20 20 | a: TypeAlias = 'Baz' # False negative in stubs -21 |- type A = 'Baz' # TC008 - 21 |+ type A = Baz # TC008 -22 22 | -23 23 | class Nested: -24 24 | a: TypeAlias = 'Baz' # False negative in stubs - -TC008.pyi:25:18: TC008 [*] Remove quotes from type alias - | -23 | class Nested: -24 | a: TypeAlias = 'Baz' # False negative in stubs -25 | type A = 'Baz' # TC008 - | ^^^^^ TC008 - | - = help: Remove quotes - -ℹ Safe fix -22 22 | -23 23 | class Nested: -24 24 | a: TypeAlias = 'Baz' # False negative in stubs -25 |- type A = 'Baz' # TC008 - 25 |+ type A = Baz # TC008 From 5947f64f2bf488ca4f40ac3ea3ff1828b0cd27c6 Mon Sep 17 00:00:00 2001 From: David Salvisberg Date: Fri, 3 Jan 2025 14:25:51 +0100 Subject: [PATCH 6/9] Update crates/ruff_linter/src/checkers/ast/mod.rs Co-authored-by: Alex Waygood --- crates/ruff_linter/src/checkers/ast/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 5b6994a7befdab..53c448bfaf2631 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -2324,6 +2324,7 @@ impl<'a> Checker<'a> { let parsed_expr = parsed_annotation.expression(); self.visit_expr(parsed_expr); if self.semantic.in_type_alias_value() { + // stub files are covered by PYI020 if !self.source_type.is_stub() && self.enabled(Rule::QuotedTypeAlias) { flake8_type_checking::rules::quoted_type_alias( self, From 5db1af595a20835df51dfbe9ce9a423914c46c91 Mon Sep 17 00:00:00 2001 From: Daverball Date: Fri, 3 Jan 2025 14:42:20 +0100 Subject: [PATCH 7/9] Applies suggested change --- .../rules/type_alias_quotes.rs | 6 +-- .../ruff/rules/missing_fstring_syntax.rs | 4 +- crates/ruff_python_semantic/src/model.rs | 38 ++++++++++++++++--- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index 92b1d7d07775a3..cc663022d041b1 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -4,7 +4,7 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailab use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast as ast; use ruff_python_ast::{Expr, Stmt}; -use ruff_python_semantic::{Binding, SemanticModel}; +use ruff_python_semantic::{Binding, SemanticModel, TypingOnlyBindingsStatus}; use ruff_python_stdlib::typing::{is_pep_593_generic_type, is_standard_library_literal}; use ruff_text_size::Ranged; @@ -221,7 +221,7 @@ fn collect_typing_references<'a>( }; if checker .semantic() - .simulate_runtime_load(name, false) + .simulate_runtime_load(name, TypingOnlyBindingsStatus::Disallowed) .is_some() { return; @@ -322,7 +322,7 @@ fn quotes_are_unremovable(semantic: &SemanticModel, expr: &Expr) -> bool { Expr::Name(name) => { semantic.resolve_name(name).is_some() && semantic - .simulate_runtime_load(name, semantic.in_type_checking_block()) + .simulate_runtime_load(name, semantic.in_type_checking_block().into()) .is_none() } _ => false, diff --git a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs index 6678b90d66ac03..186b9636e38a0b 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs @@ -7,7 +7,7 @@ use ruff_python_ast as ast; use ruff_python_literal::format::FormatSpec; use ruff_python_parser::parse_expression; use ruff_python_semantic::analyze::logging::is_logger_candidate; -use ruff_python_semantic::{Modules, SemanticModel}; +use ruff_python_semantic::{Modules, SemanticModel, TypingOnlyBindingsStatus}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -216,7 +216,7 @@ fn should_be_fstring( id, literal.range(), semantic.scope_id, - false, + TypingOnlyBindingsStatus::Disallowed, ) .map_or(true, |id| semantic.binding(id).kind.is_builtin()) { diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index ae8997158e0a35..56cd21ddbc89f1 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -713,13 +713,13 @@ impl<'a> SemanticModel<'a> { pub fn simulate_runtime_load( &self, name: &ast::ExprName, - allow_typing_only_bindings: bool, + typing_only_bindings_status: TypingOnlyBindingsStatus, ) -> Option { self.simulate_runtime_load_at_location_in_scope( name.id.as_str(), name.range, self.scope_id, - allow_typing_only_bindings, + typing_only_bindings_status, ) } @@ -752,7 +752,7 @@ impl<'a> SemanticModel<'a> { symbol: &str, symbol_range: TextRange, scope_id: ScopeId, - allow_typing_only_bindings: bool, + typing_only_bindings_status: TypingOnlyBindingsStatus, ) -> Option { let mut seen_function = false; let mut class_variables_visible = true; @@ -795,7 +795,9 @@ impl<'a> SemanticModel<'a> { // runtime binding with a source-order inaccurate one for shadowed_id in scope.shadowed_bindings(binding_id) { let binding = &self.bindings[shadowed_id]; - if !allow_typing_only_bindings && binding.context.is_typing() { + if typing_only_bindings_status.is_disallowed() + && binding.context.is_typing() + { continue; } if let BindingKind::Annotation @@ -830,7 +832,7 @@ impl<'a> SemanticModel<'a> { _ => binding_id, }; - if !allow_typing_only_bindings + if typing_only_bindings_status.is_disallowed() && self.bindings[candidate_id].context.is_typing() { continue; @@ -2065,6 +2067,32 @@ impl ShadowedBinding { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TypingOnlyBindingsStatus { + Allowed, + Disallowed, +} + +impl TypingOnlyBindingsStatus { + pub const fn is_allowed(self) -> bool { + matches!(self, TypingOnlyBindingsStatus::Allowed) + } + + pub const fn is_disallowed(self) -> bool { + matches!(self, TypingOnlyBindingsStatus::Disallowed) + } +} + +impl From for TypingOnlyBindingsStatus { + fn from(value: bool) -> Self { + if value { + TypingOnlyBindingsStatus::Allowed + } else { + TypingOnlyBindingsStatus::Disallowed + } + } +} + bitflags! { /// A select list of Python modules that the semantic model can explicitly track. #[derive(Debug)] From ea541a6e83ba33f646357ab56c71bd05aa28fda4 Mon Sep 17 00:00:00 2001 From: Daverball Date: Mon, 6 Jan 2025 13:13:53 +0100 Subject: [PATCH 8/9] Adds note and link to `PYI020` --- .../src/rules/flake8_type_checking/rules/type_alias_quotes.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index cc663022d041b1..414db7045e2122 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -98,6 +98,10 @@ impl Violation for UnquotedTypeAlias { /// type OptInt = int | None /// ``` /// +/// To achieve the same thing in stub files you should instead take a look at +/// [`quoted-annotation-in-stub`](quoted-annotation-in-stub.md), which gets rid +/// of all the unnecessary quotes in stub files. +/// /// ## Fix safety /// This rule's fix is marked as safe, unless the type annotation contains comments. /// From 9e8642ff7135e1dabb24994090bc027c8abc02eb Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 8 Jan 2025 12:04:05 +0000 Subject: [PATCH 9/9] more docs --- .../rules/type_alias_quotes.rs | 31 +++++++++++++------ .../pyupgrade/rules/quoted_annotation.rs | 6 ++++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index 414db7045e2122..e2f3c7a04e6c7b 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -68,14 +68,20 @@ impl Violation for UnquotedTypeAlias { /// /// ## Why is this bad? /// Unnecessary string forward references can lead to additional overhead -/// in runtime libraries making use of type hints, as well as lead to bad +/// in runtime libraries making use of type hints. They can also have bad /// interactions with other runtime uses like [PEP 604] type unions. /// -/// For explicit type aliases the quotes are only considered redundant -/// if the type expression contains no subscripts or attribute accesses -/// this is because of stubs packages. Some types will only be subscriptable -/// at type checking time, similarly there may be some module-level -/// attributes like type aliases that are only available in the stubs. +/// PEP-613 type aliases are only flagged by the rule if Ruff can have high +/// confidence that the quotes are unnecessary. Specifically, any PEP-613 +/// type alias where the type expression on the right-hand side contains +/// subscripts or attribute accesses will not be flagged. This is because +/// type aliases can reference types that are, for example, generic in stub +/// files but not at runtime. That can mean that a type checker expects the +/// referenced type to be subscripted with type arguments despite the fact +/// that doing so would fail at runtime if the type alias value was not +/// quoted. Similarly, a type alias might need to reference a module-level +/// attribute that exists in a stub file but not at runtime, meaning that +/// the type alias value would need to be quoted to avoid a runtime error. /// /// ## Example /// Given: @@ -98,13 +104,18 @@ impl Violation for UnquotedTypeAlias { /// type OptInt = int | None /// ``` /// -/// To achieve the same thing in stub files you should instead take a look at -/// [`quoted-annotation-in-stub`](quoted-annotation-in-stub.md), which gets rid -/// of all the unnecessary quotes in stub files. -/// /// ## Fix safety /// This rule's fix is marked as safe, unless the type annotation contains comments. /// +/// ## See also +/// This rule only applies to type aliases in non-stub files. For removing quotes in other +/// contexts or in stub files, see: +/// +/// - [`quoted-annotation-in-stub`](quoted-annotation-in-stub.md): A rule that +/// removes all quoted annotations from stub files +/// - [`quoted-annotation`](quoted-annotation.md): A rule that removes unnecessary quotes +/// from *annotations* in runtime files. +/// /// ## References /// - [PEP 613 – Explicit Type Aliases](https://peps.python.org/pep-0613/) /// - [PEP 695: Generic Type Alias](https://peps.python.org/pep-0695/#generic-type-alias) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs index 7ee7cee03a919f..19a35c06b297c5 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs @@ -54,6 +54,12 @@ use crate::checkers::ast::Checker; /// bar: Bar /// ``` /// +/// ## See also +/// - [`quoted-annotation-in-stub`](quoted-annotation-in-stub.md): A rule that +/// removes all quoted annotations from stub files +/// - [`quoted-type-alias`](quoted-type-alias.md): A rule that removes unnecessary quotes +/// from type aliases. +/// /// ## References /// - [PEP 563 – Postponed Evaluation of Annotations](https://peps.python.org/pep-0563/) /// - [Python documentation: `__future__`](https://docs.python.org/3/library/__future__.html#module-__future__)