mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-03 05:03:33 +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
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(_) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue