Skip to content

Commit

Permalink
Add FA102 (using new-style annotation without annotations import)
Browse files Browse the repository at this point in the history
Refs #3072

Refs #4553
  • Loading branch information
akx committed May 29, 2023
1 parent e8957a5 commit 974680e
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 0 deletions.
25 changes: 25 additions & 0 deletions crates/ruff/src/checkers/ast/mod.rs
Expand Up @@ -2206,6 +2206,19 @@ where
}
}
}
// PEP585 list[...], dict[...] without future when required by Python version
if self.enabled(Rule::MissingFutureAnnotationsImportNewStyle)
&& self.settings.target_version < PythonVersion::Py39
&& !self.semantic_model.future_annotations()
&& self.semantic_model.in_annotation()
&& analyze::typing::is_pep585_generic(value, &self.semantic_model)
{
flake8_future_annotations::rules::missing_future_annotations_new_style(
self,
"PEP585 collection",
expr,
);
}

if self.semantic_model.match_typing_expr(value, "Literal") {
self.semantic_model.flags |= SemanticModelFlags::LITERAL;
Expand Down Expand Up @@ -3190,6 +3203,18 @@ where
op: Operator::BitOr,
..
}) => {
// Target version < 3.10, but using a PEP604 `|` operator.
if self.enabled(Rule::MissingFutureAnnotationsImportNewStyle)
&& self.settings.target_version < PythonVersion::Py310
&& self.semantic_model.in_annotation()
{
flake8_future_annotations::rules::missing_future_annotations_new_style(
self,
"PEP604 union",
expr,
);
}

if self.is_stub {
if self.enabled(Rule::DuplicateUnionMember)
&& self.semantic_model.in_type_definition()
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/codes.rs
Expand Up @@ -319,6 +319,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {

// flake8-future-annotations
(Flake8FutureAnnotations, "100") => (RuleGroup::Unspecified, Rule::MissingFutureAnnotationsImportOldStyle),
(Flake8FutureAnnotations, "102") => (RuleGroup::Unspecified, Rule::MissingFutureAnnotationsImportNewStyle),

// flake8-2020
(Flake82020, "101") => (RuleGroup::Unspecified, Rule::SysVersionSlice3),
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/registry.rs
Expand Up @@ -268,6 +268,7 @@ ruff_macros::register_rules!(
rules::flake8_annotations::rules::AnyType,
// flake8-future-annotations
rules::flake8_future_annotations::rules::MissingFutureAnnotationsImportOldStyle,
rules::flake8_future_annotations::rules::MissingFutureAnnotationsImportNewStyle,
// flake8-2020
rules::flake8_2020::rules::SysVersionSlice3,
rules::flake8_2020::rules::SysVersion2,
Expand Down
18 changes: 18 additions & 0 deletions crates/ruff/src/rules/flake8_future_annotations/mod.rs
Expand Up @@ -37,4 +37,22 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}

#[test_case(Path::new("no_future_import_uses_lowercase.py"); "no_future_import_uses_lowercase")]
#[test_case(Path::new("no_future_import_uses_union.py"); "no_future_import_uses_union")]
#[test_case(Path::new("no_future_import_uses_union_inner.py"); "no_future_import_uses_union_inner")]
#[test_case(Path::new("ok_no_types.py"); "ok_no_types")]
#[test_case(Path::new("ok_uses_future.py"); "ok_uses_future")]
fn fa102(path: &Path) -> Result<()> {
let snapshot = format!("fa102_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_future_annotations").join(path).as_path(),
&settings::Settings {
target_version: PythonVersion::Py37,
..settings::Settings::for_rule(Rule::MissingFutureAnnotationsImportNewStyle)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}
@@ -0,0 +1,44 @@
use rustpython_parser::ast::{Expr, Ranged};

use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};

use crate::checkers::ast::Checker;

/// ## What it does
/// Checks for missing `from __future__ import annotations` imports upon
/// detecting type annotations that are written in PEP 585 or PEP 604 style but
/// the target Python version doesn't support them without the import.
///
/// ## Example
///
/// ```python
/// def function(a_dict: dict[str, int | None]) -> None:
/// a_list: list[str] = []
/// a_list.append("hello")
/// ```
/// would raise an exception at runtime before Python 3.9 and Python 3.10.
#[violation]
pub struct MissingFutureAnnotationsImportNewStyle {
kind: String,
expr: String,
}

impl Violation for MissingFutureAnnotationsImportNewStyle {
#[derive_message_formats]
fn message(&self) -> String {
let MissingFutureAnnotationsImportNewStyle { kind, expr } = self;
format!("Missing `from __future__ import annotations`, but uses {kind} `{expr}`")
}
}

/// FA102
pub(crate) fn missing_future_annotations_new_style(checker: &mut Checker, kind: &str, expr: &Expr) {
checker.diagnostics.push(Diagnostic::new(
MissingFutureAnnotationsImportNewStyle {
kind: kind.to_string(),
expr: checker.locator.slice(expr.range()).to_string(),
},
expr.range(),
));
}
4 changes: 4 additions & 0 deletions crates/ruff/src/rules/flake8_future_annotations/rules/mod.rs
@@ -1,5 +1,9 @@
pub(crate) use missing_future_annotations_new_style::{
missing_future_annotations_new_style, MissingFutureAnnotationsImportNewStyle,
};
pub(crate) use missing_future_annotations_old_style::{
missing_future_annotations_old_style, MissingFutureAnnotationsImportOldStyle,
};

mod missing_future_annotations_new_style;
mod missing_future_annotations_old_style;
@@ -0,0 +1,12 @@
---
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
---
no_future_import_uses_lowercase.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP585 collection `list[str]`
|
2 | def main() -> None:
3 | a_list: list[str] = []
| ^^^^^^^^^ FA102
4 | a_list.append("hello")
|


@@ -0,0 +1,20 @@
---
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
---
no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP604 union `list[str] | None`
|
2 | def main() -> None:
3 | a_list: list[str] | None = []
| ^^^^^^^^^^^^^^^^ FA102
4 | a_list.append("hello")
|

no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP585 collection `list[str]`
|
2 | def main() -> None:
3 | a_list: list[str] | None = []
| ^^^^^^^^^ FA102
4 | a_list.append("hello")
|


@@ -0,0 +1,36 @@
---
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
---
no_future_import_uses_union_inner.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP585 collection `list[str | None]`
|
2 | def main() -> None:
3 | a_list: list[str | None] = []
| ^^^^^^^^^^^^^^^^ FA102
4 | a_list.append("hello")
|

no_future_import_uses_union_inner.py:2:18: FA102 Missing `from __future__ import annotations`, but uses PEP604 union `str | None`
|
2 | def main() -> None:
3 | a_list: list[str | None] = []
| ^^^^^^^^^^ FA102
4 | a_list.append("hello")
|

no_future_import_uses_union_inner.py:7:8: FA102 Missing `from __future__ import annotations`, but uses PEP585 collection `tuple[str, str | None, str]`
|
7 | def hello(y: dict[str | None, int]) -> None:
8 | z: tuple[str, str | None, str] = tuple(y)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA102
9 | del z
|

no_future_import_uses_union_inner.py:7:19: FA102 Missing `from __future__ import annotations`, but uses PEP604 union `str | None`
|
7 | def hello(y: dict[str | None, int]) -> None:
8 | z: tuple[str, str | None, str] = tuple(y)
| ^^^^^^^^^^ FA102
9 | del z
|


@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
---

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
---

0 comments on commit 974680e

Please sign in to comment.