mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-19 01:50:38 +00:00
Implement flake8-future-annotations
FA100 (#3979)
This commit is contained in:
parent
cd2e7fa72a
commit
01b372a75c
34 changed files with 375 additions and 15 deletions
|
@ -263,6 +263,7 @@ quality tools, including:
|
||||||
- [flake8-eradicate](https://pypi.org/project/flake8-eradicate/)
|
- [flake8-eradicate](https://pypi.org/project/flake8-eradicate/)
|
||||||
- [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)
|
- [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)
|
||||||
- [flake8-executable](https://pypi.org/project/flake8-executable/)
|
- [flake8-executable](https://pypi.org/project/flake8-executable/)
|
||||||
|
- [flake8-future-annotations](https://pypi.org/project/flake8-future-annotations/)
|
||||||
- [flake8-gettext](https://pypi.org/project/flake8-gettext/)
|
- [flake8-gettext](https://pypi.org/project/flake8-gettext/)
|
||||||
- [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/)
|
- [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/)
|
||||||
- [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions)
|
- [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions)
|
||||||
|
|
7
crates/ruff/resources/test/fixtures/flake8_future_annotations/edge_case.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/flake8_future_annotations/edge_case.py
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from typing import List
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
|
||||||
|
def main(_: List[int]) -> None:
|
||||||
|
a_list: t.List[str] = []
|
||||||
|
a_list.append("hello")
|
6
crates/ruff/resources/test/fixtures/flake8_future_annotations/from_typing_import.py
vendored
Normal file
6
crates/ruff/resources/test/fixtures/flake8_future_annotations/from_typing_import.py
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
a_list: List[str] = []
|
||||||
|
a_list.append("hello")
|
8
crates/ruff/resources/test/fixtures/flake8_future_annotations/from_typing_import_many.py
vendored
Normal file
8
crates/ruff/resources/test/fixtures/flake8_future_annotations/from_typing_import_many.py
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from typing import Dict, List, Optional, Set, Union, cast
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
a_list: List[Optional[str]] = []
|
||||||
|
a_list.append("hello")
|
||||||
|
a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {})
|
||||||
|
a_dict[1] = {True, False}
|
6
crates/ruff/resources/test/fixtures/flake8_future_annotations/import_typing.py
vendored
Normal file
6
crates/ruff/resources/test/fixtures/flake8_future_annotations/import_typing.py
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import typing
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
a_list: typing.List[str] = []
|
||||||
|
a_list.append("hello")
|
6
crates/ruff/resources/test/fixtures/flake8_future_annotations/import_typing_as.py
vendored
Normal file
6
crates/ruff/resources/test/fixtures/flake8_future_annotations/import_typing_as.py
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
a_list: t.List[str] = []
|
||||||
|
a_list.append("hello")
|
|
@ -0,0 +1,7 @@
|
||||||
|
def main() -> None:
|
||||||
|
a_list: list[str] = []
|
||||||
|
a_list.append("hello")
|
||||||
|
|
||||||
|
|
||||||
|
def hello(y: dict[str, int]) -> None:
|
||||||
|
del y
|
7
crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_union.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_union.py
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
def main() -> None:
|
||||||
|
a_list: list[str] | None = []
|
||||||
|
a_list.append("hello")
|
||||||
|
|
||||||
|
|
||||||
|
def hello(y: dict[str, int] | None) -> None:
|
||||||
|
del y
|
|
@ -0,0 +1,8 @@
|
||||||
|
def main() -> None:
|
||||||
|
a_list: list[str | None] = []
|
||||||
|
a_list.append("hello")
|
||||||
|
|
||||||
|
|
||||||
|
def hello(y: dict[str | None, int]) -> None:
|
||||||
|
z: tuple[str, str | None, str] = tuple(y)
|
||||||
|
del z
|
3
crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_no_types.py
vendored
Normal file
3
crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_no_types.py
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
def main() -> str:
|
||||||
|
a_str = "hello"
|
||||||
|
return a_str
|
10
crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_non_simplifiable_types.py
vendored
Normal file
10
crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_non_simplifiable_types.py
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
|
||||||
|
class Stuff(NamedTuple):
|
||||||
|
x: int
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
a_list = Stuff(5)
|
||||||
|
print(a_list)
|
6
crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_uses_future.py
vendored
Normal file
6
crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_uses_future.py
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
a_list: list[str] = []
|
||||||
|
a_list.append("hello")
|
8
crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_variable_name.py
vendored
Normal file
8
crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_variable_name.py
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import typing
|
||||||
|
|
||||||
|
IRRELEVANT = typing.TypeVar
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
List: list[str] = []
|
||||||
|
List.append("hello")
|
|
@ -44,12 +44,12 @@ use crate::registry::{AsRule, Rule};
|
||||||
use crate::rules::{
|
use crate::rules::{
|
||||||
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
|
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
|
||||||
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
|
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
|
||||||
flake8_django, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat,
|
flake8_django, flake8_errmsg, flake8_future_annotations, flake8_gettext,
|
||||||
flake8_import_conventions, flake8_logging_format, flake8_pie, flake8_print, flake8_pyi,
|
flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format, flake8_pie,
|
||||||
flake8_pytest_style, flake8_raise, flake8_return, flake8_self, flake8_simplify,
|
flake8_print, flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_self,
|
||||||
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, flake8_use_pathlib, flynt,
|
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments,
|
||||||
mccabe, numpy, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks,
|
flake8_use_pathlib, flynt, mccabe, numpy, pandas_vet, pep8_naming, pycodestyle, pydocstyle,
|
||||||
pylint, pyupgrade, ruff, tryceratops,
|
pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
|
||||||
};
|
};
|
||||||
use crate::settings::types::PythonVersion;
|
use crate::settings::types::PythonVersion;
|
||||||
use crate::settings::{flags, Settings};
|
use crate::settings::{flags, Settings};
|
||||||
|
@ -1077,7 +1077,6 @@ where
|
||||||
pyupgrade::rules::unnecessary_builtin_import(self, stmt, module, names);
|
pyupgrade::rules::unnecessary_builtin_import(self, stmt, module, names);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.settings.rules.enabled(Rule::BannedApi) {
|
if self.settings.rules.enabled(Rule::BannedApi) {
|
||||||
if let Some(module) =
|
if let Some(module) =
|
||||||
helpers::resolve_imported_module_path(level, module, self.module_path)
|
helpers::resolve_imported_module_path(level, module, self.module_path)
|
||||||
|
@ -2258,12 +2257,25 @@ where
|
||||||
match &expr.node {
|
match &expr.node {
|
||||||
ExprKind::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
ExprKind::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||||
// Ex) Optional[...], Union[...]
|
// Ex) Optional[...], Union[...]
|
||||||
|
if self
|
||||||
|
.settings
|
||||||
|
.rules
|
||||||
|
.enabled(Rule::MissingFutureAnnotationsImport)
|
||||||
|
&& (self.settings.target_version < PythonVersion::Py310
|
||||||
|
&& (self.settings.target_version >= PythonVersion::Py37
|
||||||
|
&& !self.ctx.future_annotations()
|
||||||
|
&& self.ctx.in_annotation()))
|
||||||
|
&& analyze::typing::is_pep604_builtin(value, &self.ctx)
|
||||||
|
{
|
||||||
|
flake8_future_annotations::rules::missing_future_annotations(self, value);
|
||||||
|
}
|
||||||
if !self.settings.pyupgrade.keep_runtime_typing
|
if !self.settings.pyupgrade.keep_runtime_typing
|
||||||
&& self.settings.rules.enabled(Rule::NonPEP604Annotation)
|
&& self.settings.rules.enabled(Rule::NonPEP604Annotation)
|
||||||
&& (self.settings.target_version >= PythonVersion::Py310
|
&& (self.settings.target_version >= PythonVersion::Py310
|
||||||
|| (self.settings.target_version >= PythonVersion::Py37
|
|| (self.settings.target_version >= PythonVersion::Py37
|
||||||
&& self.ctx.future_annotations()
|
&& self.ctx.future_annotations()
|
||||||
&& self.ctx.in_annotation()))
|
&& self.ctx.in_annotation()))
|
||||||
|
&& analyze::typing::is_pep604_builtin(value, &self.ctx)
|
||||||
{
|
{
|
||||||
pyupgrade::rules::use_pep604_annotation(self, expr, value, slice);
|
pyupgrade::rules::use_pep604_annotation(self, expr, value, slice);
|
||||||
}
|
}
|
||||||
|
@ -2321,6 +2333,20 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ex) List[...]
|
// Ex) List[...]
|
||||||
|
if self
|
||||||
|
.settings
|
||||||
|
.rules
|
||||||
|
.enabled(Rule::MissingFutureAnnotationsImport)
|
||||||
|
&& (self.settings.target_version < PythonVersion::Py39
|
||||||
|
&& (self.settings.target_version >= PythonVersion::Py37
|
||||||
|
&& !self.ctx.future_annotations()
|
||||||
|
&& self.ctx.in_annotation()))
|
||||||
|
&& analyze::typing::is_pep585_builtin(expr, &self.ctx)
|
||||||
|
{
|
||||||
|
flake8_future_annotations::rules::missing_future_annotations(
|
||||||
|
self, expr,
|
||||||
|
);
|
||||||
|
}
|
||||||
if !self.settings.pyupgrade.keep_runtime_typing
|
if !self.settings.pyupgrade.keep_runtime_typing
|
||||||
&& self.settings.rules.enabled(Rule::NonPEP585Annotation)
|
&& self.settings.rules.enabled(Rule::NonPEP585Annotation)
|
||||||
&& (self.settings.target_version >= PythonVersion::Py39
|
&& (self.settings.target_version >= PythonVersion::Py39
|
||||||
|
@ -2372,6 +2398,18 @@ where
|
||||||
}
|
}
|
||||||
ExprKind::Attribute(ast::ExprAttribute { attr, value, .. }) => {
|
ExprKind::Attribute(ast::ExprAttribute { attr, value, .. }) => {
|
||||||
// Ex) typing.List[...]
|
// Ex) typing.List[...]
|
||||||
|
if self
|
||||||
|
.settings
|
||||||
|
.rules
|
||||||
|
.enabled(Rule::MissingFutureAnnotationsImport)
|
||||||
|
&& (self.settings.target_version < PythonVersion::Py39
|
||||||
|
&& (self.settings.target_version >= PythonVersion::Py37
|
||||||
|
&& !self.ctx.future_annotations()
|
||||||
|
&& self.ctx.in_annotation()))
|
||||||
|
&& analyze::typing::is_pep585_builtin(expr, &self.ctx)
|
||||||
|
{
|
||||||
|
flake8_future_annotations::rules::missing_future_annotations(self, expr);
|
||||||
|
}
|
||||||
if !self.settings.pyupgrade.keep_runtime_typing
|
if !self.settings.pyupgrade.keep_runtime_typing
|
||||||
&& self.settings.rules.enabled(Rule::NonPEP585Annotation)
|
&& self.settings.rules.enabled(Rule::NonPEP585Annotation)
|
||||||
&& (self.settings.target_version >= PythonVersion::Py39
|
&& (self.settings.target_version >= PythonVersion::Py39
|
||||||
|
|
|
@ -331,6 +331,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||||
(Flake8Annotations, "206") => Rule::MissingReturnTypeClassMethod,
|
(Flake8Annotations, "206") => Rule::MissingReturnTypeClassMethod,
|
||||||
(Flake8Annotations, "401") => Rule::AnyType,
|
(Flake8Annotations, "401") => Rule::AnyType,
|
||||||
|
|
||||||
|
// flake8-future-annotations
|
||||||
|
(Flake8FutureAnnotations, "100") => Rule::MissingFutureAnnotationsImport,
|
||||||
|
|
||||||
// flake8-2020
|
// flake8-2020
|
||||||
(Flake82020, "101") => Rule::SysVersionSlice3,
|
(Flake82020, "101") => Rule::SysVersionSlice3,
|
||||||
(Flake82020, "102") => Rule::SysVersion2,
|
(Flake82020, "102") => Rule::SysVersion2,
|
||||||
|
|
|
@ -290,6 +290,8 @@ ruff_macros::register_rules!(
|
||||||
rules::flake8_annotations::rules::MissingReturnTypeStaticMethod,
|
rules::flake8_annotations::rules::MissingReturnTypeStaticMethod,
|
||||||
rules::flake8_annotations::rules::MissingReturnTypeClassMethod,
|
rules::flake8_annotations::rules::MissingReturnTypeClassMethod,
|
||||||
rules::flake8_annotations::rules::AnyType,
|
rules::flake8_annotations::rules::AnyType,
|
||||||
|
// flake8-future-annotations
|
||||||
|
rules::flake8_future_annotations::rules::MissingFutureAnnotationsImport,
|
||||||
// flake8-2020
|
// flake8-2020
|
||||||
rules::flake8_2020::rules::SysVersionSlice3,
|
rules::flake8_2020::rules::SysVersionSlice3,
|
||||||
rules::flake8_2020::rules::SysVersion2,
|
rules::flake8_2020::rules::SysVersion2,
|
||||||
|
@ -769,6 +771,9 @@ pub enum Linter {
|
||||||
/// [flake8-executable](https://pypi.org/project/flake8-executable/)
|
/// [flake8-executable](https://pypi.org/project/flake8-executable/)
|
||||||
#[prefix = "EXE"]
|
#[prefix = "EXE"]
|
||||||
Flake8Executable,
|
Flake8Executable,
|
||||||
|
/// [flake8-future-annotations](https://pypi.org/project/flake8-future-annotations/)
|
||||||
|
#[prefix = "FA"]
|
||||||
|
Flake8FutureAnnotations,
|
||||||
/// [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/)
|
/// [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/)
|
||||||
#[prefix = "ISC"]
|
#[prefix = "ISC"]
|
||||||
Flake8ImplicitStrConcat,
|
Flake8ImplicitStrConcat,
|
||||||
|
|
40
crates/ruff/src/rules/flake8_future_annotations/mod.rs
Normal file
40
crates/ruff/src/rules/flake8_future_annotations/mod.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
//! Rules from [flake8-future-annotations](https://pypi.org/project/flake8-future-annotations/).
|
||||||
|
pub(crate) mod rules;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use test_case::test_case;
|
||||||
|
|
||||||
|
use crate::registry::Rule;
|
||||||
|
use crate::settings::types::PythonVersion;
|
||||||
|
use crate::test::test_path;
|
||||||
|
use crate::{assert_messages, settings};
|
||||||
|
|
||||||
|
#[test_case(Path::new("edge_case.py"); "edge_case")]
|
||||||
|
#[test_case(Path::new("from_typing_import.py"); "from_typing_import")]
|
||||||
|
#[test_case(Path::new("from_typing_import_many.py"); "from_typing_import_many")]
|
||||||
|
#[test_case(Path::new("import_typing.py"); "import_typing")]
|
||||||
|
#[test_case(Path::new("import_typing_as.py"); "import_typing_as")]
|
||||||
|
#[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_non_simplifiable_types.py"); "ok_non_simplifiable_types")]
|
||||||
|
#[test_case(Path::new("ok_uses_future.py"); "ok_uses_future")]
|
||||||
|
#[test_case(Path::new("ok_variable_name.py"); "ok_variable_name")]
|
||||||
|
fn rules(path: &Path) -> Result<()> {
|
||||||
|
let snapshot = path.to_string_lossy().into_owned();
|
||||||
|
let diagnostics = test_path(
|
||||||
|
Path::new("flake8_future_annotations").join(path).as_path(),
|
||||||
|
&settings::Settings {
|
||||||
|
target_version: PythonVersion::Py37,
|
||||||
|
..settings::Settings::for_rule(Rule::MissingFutureAnnotationsImport)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
assert_messages!(snapshot, diagnostics);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
78
crates/ruff/src/rules/flake8_future_annotations/rules.rs
Normal file
78
crates/ruff/src/rules/flake8_future_annotations/rules.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
use rustpython_parser::ast::Expr;
|
||||||
|
|
||||||
|
use ruff_diagnostics::{Diagnostic, Violation};
|
||||||
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
use ruff_python_ast::call_path::format_call_path;
|
||||||
|
|
||||||
|
use crate::checkers::ast::Checker;
|
||||||
|
|
||||||
|
/// ## What it does
|
||||||
|
/// Checks for missing `from __future__ import annotations` imports upon
|
||||||
|
/// detecting type annotations that can be written more succinctly under
|
||||||
|
/// PEP 563.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// PEP 563 enabled the use of a number of convenient type annotations, such as
|
||||||
|
/// `list[str]` instead of `List[str]`, or `str | None` instead of
|
||||||
|
/// `Optional[str]`. However, these annotations are only available on Python
|
||||||
|
/// 3.9 and higher, _unless_ the `from __future__ import annotations` import is present.
|
||||||
|
///
|
||||||
|
/// By adding the `__future__` import, the pyupgrade rules can automatically
|
||||||
|
/// migrate existing code to use the new syntax, even for older Python versions.
|
||||||
|
/// This rule thus pairs well with pyupgrade and with Ruff's pyupgrade rules.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```python
|
||||||
|
/// from typing import List, Dict, Optional
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// def function(a_dict: Dict[str, Optional[int]]) -> None:
|
||||||
|
/// a_list: List[str] = []
|
||||||
|
/// a_list.append("hello")
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Use instead:
|
||||||
|
/// ```python
|
||||||
|
/// from __future__ import annotations
|
||||||
|
///
|
||||||
|
/// from typing import List, Dict, Optional
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// def function(a_dict: Dict[str, Optional[int]]) -> None:
|
||||||
|
/// a_list: List[str] = []
|
||||||
|
/// a_list.append("hello")
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// After running the additional pyupgrade rules:
|
||||||
|
/// ```python
|
||||||
|
/// from __future__ import annotations
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// def function(a_dict: dict[str, int | None]) -> None:
|
||||||
|
/// a_list: list[str] = []
|
||||||
|
/// a_list.append("hello")
|
||||||
|
/// ```
|
||||||
|
#[violation]
|
||||||
|
pub struct MissingFutureAnnotationsImport {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Violation for MissingFutureAnnotationsImport {
|
||||||
|
#[derive_message_formats]
|
||||||
|
fn message(&self) -> String {
|
||||||
|
let MissingFutureAnnotationsImport { name } = self;
|
||||||
|
format!("Missing `from __future__ import annotations`, but uses `{name}`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FA100
|
||||||
|
pub(crate) fn missing_future_annotations(checker: &mut Checker, expr: &Expr) {
|
||||||
|
if let Some(binding) = checker.ctx.resolve_call_path(expr) {
|
||||||
|
checker.diagnostics.push(Diagnostic::new(
|
||||||
|
MissingFutureAnnotationsImport {
|
||||||
|
name: format_call_path(&binding),
|
||||||
|
},
|
||||||
|
expr.range(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
|
||||||
|
---
|
||||||
|
edge_case.py:6:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
|
||||||
|
|
|
||||||
|
6 | def main(_: List[int]) -> None:
|
||||||
|
7 | a_list: t.List[str] = []
|
||||||
|
| ^^^^^^ FA100
|
||||||
|
8 | a_list.append("hello")
|
||||||
|
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
|
||||||
|
---
|
||||||
|
from_typing_import.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
|
||||||
|
|
|
||||||
|
5 | def main() -> None:
|
||||||
|
6 | a_list: List[str] = []
|
||||||
|
| ^^^^ FA100
|
||||||
|
7 | a_list.append("hello")
|
||||||
|
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
|
||||||
|
---
|
||||||
|
from_typing_import_many.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
|
||||||
|
|
|
||||||
|
5 | def main() -> None:
|
||||||
|
6 | a_list: List[Optional[str]] = []
|
||||||
|
| ^^^^ FA100
|
||||||
|
7 | a_list.append("hello")
|
||||||
|
8 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {})
|
||||||
|
|
|
||||||
|
|
||||||
|
from_typing_import_many.py:5:18: FA100 Missing `from __future__ import annotations`, but uses `typing.Optional`
|
||||||
|
|
|
||||||
|
5 | def main() -> None:
|
||||||
|
6 | a_list: List[Optional[str]] = []
|
||||||
|
| ^^^^^^^^ FA100
|
||||||
|
7 | a_list.append("hello")
|
||||||
|
8 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {})
|
||||||
|
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
|
||||||
|
---
|
||||||
|
import_typing.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
|
||||||
|
|
|
||||||
|
5 | def main() -> None:
|
||||||
|
6 | a_list: typing.List[str] = []
|
||||||
|
| ^^^^^^^^^^^ FA100
|
||||||
|
7 | a_list.append("hello")
|
||||||
|
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
|
||||||
|
---
|
||||||
|
import_typing_as.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
|
||||||
|
|
|
||||||
|
5 | def main() -> None:
|
||||||
|
6 | a_list: t.List[str] = []
|
||||||
|
| ^^^^^^ FA100
|
||||||
|
7 | a_list.append("hello")
|
||||||
|
|
|
||||||
|
|
||||||
|
|
|
@ -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,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,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,0 +1,4 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
|
||||||
|
---
|
||||||
|
|
|
@ -14,6 +14,7 @@ pub mod flake8_debugger;
|
||||||
pub mod flake8_django;
|
pub mod flake8_django;
|
||||||
pub mod flake8_errmsg;
|
pub mod flake8_errmsg;
|
||||||
pub mod flake8_executable;
|
pub mod flake8_executable;
|
||||||
|
pub mod flake8_future_annotations;
|
||||||
pub mod flake8_gettext;
|
pub mod flake8_gettext;
|
||||||
pub mod flake8_implicit_str_concat;
|
pub mod flake8_implicit_str_concat;
|
||||||
pub mod flake8_import_conventions;
|
pub mod flake8_import_conventions;
|
||||||
|
|
|
@ -70,6 +70,15 @@ pub fn is_pep585_builtin(expr: &Expr, context: &Context) -> bool {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if `Expr` represents a reference to a typing object with a
|
||||||
|
/// PEP 603 built-in.
|
||||||
|
pub fn is_pep604_builtin(expr: &Expr, context: &Context) -> bool {
|
||||||
|
context.resolve_call_path(expr).map_or(false, |call_path| {
|
||||||
|
context.match_typing_call_path(&call_path, "Optional")
|
||||||
|
|| context.match_typing_call_path(&call_path, "Union")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_immutable_annotation(context: &Context, expr: &Expr) -> bool {
|
pub fn is_immutable_annotation(context: &Context, expr: &Expr) -> bool {
|
||||||
match &expr.node {
|
match &expr.node {
|
||||||
ExprKind::Name(_) | ExprKind::Attribute(_) => {
|
ExprKind::Name(_) | ExprKind::Attribute(_) => {
|
||||||
|
|
4
ruff.schema.json
generated
4
ruff.schema.json
generated
|
@ -1816,6 +1816,10 @@
|
||||||
"F9",
|
"F9",
|
||||||
"F90",
|
"F90",
|
||||||
"F901",
|
"F901",
|
||||||
|
"FA",
|
||||||
|
"FA1",
|
||||||
|
"FA10",
|
||||||
|
"FA100",
|
||||||
"FBT",
|
"FBT",
|
||||||
"FBT0",
|
"FBT0",
|
||||||
"FBT00",
|
"FBT00",
|
||||||
|
|
|
@ -5,6 +5,7 @@ Example usage:
|
||||||
|
|
||||||
scripts/check_ecosystem.py <path/to/ruff1> <path/to/ruff2>
|
scripts/check_ecosystem.py <path/to/ruff1> <path/to/ruff2>
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
@ -18,7 +19,7 @@ import time
|
||||||
from asyncio.subprocess import PIPE, create_subprocess_exec
|
from asyncio.subprocess import PIPE, create_subprocess_exec
|
||||||
from contextlib import asynccontextmanager, nullcontext
|
from contextlib import asynccontextmanager, nullcontext
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, NamedTuple, Optional, Self
|
from typing import TYPE_CHECKING, NamedTuple, Self
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||||
|
@ -31,13 +32,13 @@ class Repository(NamedTuple):
|
||||||
|
|
||||||
org: str
|
org: str
|
||||||
repo: str
|
repo: str
|
||||||
ref: Optional[str]
|
ref: str | None
|
||||||
select: str = ""
|
select: str = ""
|
||||||
ignore: str = ""
|
ignore: str = ""
|
||||||
exclude: str = ""
|
exclude: str = ""
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def clone(self: Self, checkout_dir: Path) -> "AsyncIterator[Path]":
|
async def clone(self: Self, checkout_dir: Path) -> AsyncIterator[Path]:
|
||||||
"""Shallow clone this repository to a temporary directory."""
|
"""Shallow clone this repository to a temporary directory."""
|
||||||
if checkout_dir.exists():
|
if checkout_dir.exists():
|
||||||
logger.debug(f"Reusing {self.org}/{self.repo}")
|
logger.debug(f"Reusing {self.org}/{self.repo}")
|
||||||
|
@ -101,7 +102,7 @@ async def check(
|
||||||
select: str = "",
|
select: str = "",
|
||||||
ignore: str = "",
|
ignore: str = "",
|
||||||
exclude: str = "",
|
exclude: str = "",
|
||||||
) -> "Sequence[str]":
|
) -> Sequence[str]:
|
||||||
"""Run the given ruff binary against the specified path."""
|
"""Run the given ruff binary against the specified path."""
|
||||||
logger.debug(f"Checking {name} with {ruff}")
|
logger.debug(f"Checking {name} with {ruff}")
|
||||||
ruff_args = ["check", "--no-cache", "--exit-zero"]
|
ruff_args = ["check", "--no-cache", "--exit-zero"]
|
||||||
|
@ -148,7 +149,7 @@ class Diff(NamedTuple):
|
||||||
"""Return true if this diff is non-empty."""
|
"""Return true if this diff is non-empty."""
|
||||||
return bool(self.removed or self.added)
|
return bool(self.removed or self.added)
|
||||||
|
|
||||||
def __iter__(self: Self) -> "Iterator[str]":
|
def __iter__(self: Self) -> Iterator[str]:
|
||||||
"""Iterate through the changed lines in diff format."""
|
"""Iterate through the changed lines in diff format."""
|
||||||
for line in heapq.merge(sorted(self.removed), sorted(self.added)):
|
for line in heapq.merge(sorted(self.removed), sorted(self.added)):
|
||||||
if line in self.removed:
|
if line in self.removed:
|
||||||
|
@ -161,7 +162,7 @@ async def compare(
|
||||||
ruff1: Path,
|
ruff1: Path,
|
||||||
ruff2: Path,
|
ruff2: Path,
|
||||||
repo: Repository,
|
repo: Repository,
|
||||||
checkouts: Optional[Path] = None,
|
checkouts: Path | None = None,
|
||||||
) -> Diff | None:
|
) -> Diff | None:
|
||||||
"""Check a specific repository against two versions of ruff."""
|
"""Check a specific repository against two versions of ruff."""
|
||||||
removed, added = set(), set()
|
removed, added = set(), set()
|
||||||
|
@ -254,8 +255,8 @@ async def main(
|
||||||
*,
|
*,
|
||||||
ruff1: Path,
|
ruff1: Path,
|
||||||
ruff2: Path,
|
ruff2: Path,
|
||||||
projects_jsonl: Optional[Path],
|
projects_jsonl: Path | None,
|
||||||
checkouts: Optional[Path] = None,
|
checkouts: Path | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Check two versions of ruff against a corpus of open-source code."""
|
"""Check two versions of ruff against a corpus of open-source code."""
|
||||||
if projects_jsonl:
|
if projects_jsonl:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue