mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 04:19:18 +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-errmsg](https://pypi.org/project/flake8-errmsg/)
|
||||
- [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-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/)
|
||||
- [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::{
|
||||
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
|
||||
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
|
||||
flake8_django, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat,
|
||||
flake8_import_conventions, flake8_logging_format, flake8_pie, flake8_print, flake8_pyi,
|
||||
flake8_pytest_style, flake8_raise, flake8_return, flake8_self, flake8_simplify,
|
||||
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, flake8_use_pathlib, flynt,
|
||||
mccabe, numpy, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks,
|
||||
pylint, pyupgrade, ruff, tryceratops,
|
||||
flake8_django, flake8_errmsg, flake8_future_annotations, flake8_gettext,
|
||||
flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format, flake8_pie,
|
||||
flake8_print, flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_self,
|
||||
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments,
|
||||
flake8_use_pathlib, flynt, mccabe, numpy, pandas_vet, pep8_naming, pycodestyle, pydocstyle,
|
||||
pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
|
||||
};
|
||||
use crate::settings::types::PythonVersion;
|
||||
use crate::settings::{flags, Settings};
|
||||
|
@ -1077,7 +1077,6 @@ where
|
|||
pyupgrade::rules::unnecessary_builtin_import(self, stmt, module, names);
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(Rule::BannedApi) {
|
||||
if let Some(module) =
|
||||
helpers::resolve_imported_module_path(level, module, self.module_path)
|
||||
|
@ -2258,12 +2257,25 @@ where
|
|||
match &expr.node {
|
||||
ExprKind::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||
// 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
|
||||
&& self.settings.rules.enabled(Rule::NonPEP604Annotation)
|
||||
&& (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)
|
||||
{
|
||||
pyupgrade::rules::use_pep604_annotation(self, expr, value, slice);
|
||||
}
|
||||
|
@ -2321,6 +2333,20 @@ where
|
|||
}
|
||||
|
||||
// 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
|
||||
&& self.settings.rules.enabled(Rule::NonPEP585Annotation)
|
||||
&& (self.settings.target_version >= PythonVersion::Py39
|
||||
|
@ -2372,6 +2398,18 @@ where
|
|||
}
|
||||
ExprKind::Attribute(ast::ExprAttribute { attr, value, .. }) => {
|
||||
// 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
|
||||
&& self.settings.rules.enabled(Rule::NonPEP585Annotation)
|
||||
&& (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, "401") => Rule::AnyType,
|
||||
|
||||
// flake8-future-annotations
|
||||
(Flake8FutureAnnotations, "100") => Rule::MissingFutureAnnotationsImport,
|
||||
|
||||
// flake8-2020
|
||||
(Flake82020, "101") => Rule::SysVersionSlice3,
|
||||
(Flake82020, "102") => Rule::SysVersion2,
|
||||
|
|
|
@ -290,6 +290,8 @@ ruff_macros::register_rules!(
|
|||
rules::flake8_annotations::rules::MissingReturnTypeStaticMethod,
|
||||
rules::flake8_annotations::rules::MissingReturnTypeClassMethod,
|
||||
rules::flake8_annotations::rules::AnyType,
|
||||
// flake8-future-annotations
|
||||
rules::flake8_future_annotations::rules::MissingFutureAnnotationsImport,
|
||||
// flake8-2020
|
||||
rules::flake8_2020::rules::SysVersionSlice3,
|
||||
rules::flake8_2020::rules::SysVersion2,
|
||||
|
@ -769,6 +771,9 @@ pub enum Linter {
|
|||
/// [flake8-executable](https://pypi.org/project/flake8-executable/)
|
||||
#[prefix = "EXE"]
|
||||
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/)
|
||||
#[prefix = "ISC"]
|
||||
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_errmsg;
|
||||
pub mod flake8_executable;
|
||||
pub mod flake8_future_annotations;
|
||||
pub mod flake8_gettext;
|
||||
pub mod flake8_implicit_str_concat;
|
||||
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 {
|
||||
match &expr.node {
|
||||
ExprKind::Name(_) | ExprKind::Attribute(_) => {
|
||||
|
|
4
ruff.schema.json
generated
4
ruff.schema.json
generated
|
@ -1816,6 +1816,10 @@
|
|||
"F9",
|
||||
"F90",
|
||||
"F901",
|
||||
"FA",
|
||||
"FA1",
|
||||
"FA10",
|
||||
"FA100",
|
||||
"FBT",
|
||||
"FBT0",
|
||||
"FBT00",
|
||||
|
|
|
@ -5,6 +5,7 @@ Example usage:
|
|||
|
||||
scripts/check_ecosystem.py <path/to/ruff1> <path/to/ruff2>
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
|
@ -18,7 +19,7 @@ import time
|
|||
from asyncio.subprocess import PIPE, create_subprocess_exec
|
||||
from contextlib import asynccontextmanager, nullcontext
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, NamedTuple, Optional, Self
|
||||
from typing import TYPE_CHECKING, NamedTuple, Self
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||
|
@ -31,13 +32,13 @@ class Repository(NamedTuple):
|
|||
|
||||
org: str
|
||||
repo: str
|
||||
ref: Optional[str]
|
||||
ref: str | None
|
||||
select: str = ""
|
||||
ignore: str = ""
|
||||
exclude: str = ""
|
||||
|
||||
@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."""
|
||||
if checkout_dir.exists():
|
||||
logger.debug(f"Reusing {self.org}/{self.repo}")
|
||||
|
@ -101,7 +102,7 @@ async def check(
|
|||
select: str = "",
|
||||
ignore: str = "",
|
||||
exclude: str = "",
|
||||
) -> "Sequence[str]":
|
||||
) -> Sequence[str]:
|
||||
"""Run the given ruff binary against the specified path."""
|
||||
logger.debug(f"Checking {name} with {ruff}")
|
||||
ruff_args = ["check", "--no-cache", "--exit-zero"]
|
||||
|
@ -148,7 +149,7 @@ class Diff(NamedTuple):
|
|||
"""Return true if this diff is non-empty."""
|
||||
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."""
|
||||
for line in heapq.merge(sorted(self.removed), sorted(self.added)):
|
||||
if line in self.removed:
|
||||
|
@ -161,7 +162,7 @@ async def compare(
|
|||
ruff1: Path,
|
||||
ruff2: Path,
|
||||
repo: Repository,
|
||||
checkouts: Optional[Path] = None,
|
||||
checkouts: Path | None = None,
|
||||
) -> Diff | None:
|
||||
"""Check a specific repository against two versions of ruff."""
|
||||
removed, added = set(), set()
|
||||
|
@ -254,8 +255,8 @@ async def main(
|
|||
*,
|
||||
ruff1: Path,
|
||||
ruff2: Path,
|
||||
projects_jsonl: Optional[Path],
|
||||
checkouts: Optional[Path] = None,
|
||||
projects_jsonl: Path | None,
|
||||
checkouts: Path | None = None,
|
||||
) -> None:
|
||||
"""Check two versions of ruff against a corpus of open-source code."""
|
||||
if projects_jsonl:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue