Skip to content

Commit c8b3bbd

Browse files
Anuar Navarro Hawachamyreese
Anuar Navarro Hawach
authored andcommitted
Add UsePrimitiveTypes rule
1 parent 8430224 commit c8b3bbd

9 files changed

+175
-14
lines changed

src/fixit/api.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def _fixit_file_wrapper(
225225
autofix: bool = False,
226226
options: Optional[Options] = None,
227227
metrics_hook: Optional[MetricsHook] = None,
228-
) -> List[Result]:
228+
) -> list[Result]:
229229
"""
230230
Wrapper because generators can't be pickled or used directly via multiprocessing
231231
TODO: replace this with some sort of queue or whatever

src/fixit/cli.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323

2424
def splash(
25-
visited: Set[Path], dirty: Set[Path], autofixes: int = 0, fixed: int = 0
25+
visited: set[Path], dirty: set[Path], autofixes: int = 0, fixed: int = 0
2626
) -> None:
2727
def f(v: int) -> str:
2828
return "file" if v == 1 else "files"

src/fixit/config.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def __init__(self, msg: str, rule: QualifiedRule):
7272
super().__init__(msg)
7373
self.rule = rule
7474

75-
def __reduce__(self) -> Tuple[Type[RuntimeError], Any]:
75+
def __reduce__(self) -> tuple[Type[RuntimeError], Any]:
7676
return type(self), (*self.args, self.rule)
7777

7878

@@ -174,7 +174,7 @@ def find_rules(rule: QualifiedRule) -> Iterable[Type[LintRule]]:
174174
raise CollectionError(f"could not import rule(s) {rule}", rule) from e
175175

176176

177-
def walk_module(module: ModuleType) -> Dict[str, Type[LintRule]]:
177+
def walk_module(module: ModuleType) -> dict[str, Type[LintRule]]:
178178
"""
179179
Given a module object, return a mapping of all rule names to classes.
180180
@@ -272,7 +272,7 @@ def collect_rules(
272272
return materialized_rules
273273

274274

275-
def locate_configs(path: Path, root: Optional[Path] = None) -> List[Path]:
275+
def locate_configs(path: Path, root: Optional[Path] = None) -> list[Path]:
276276
"""
277277
Given a file path, locate all relevant config files in priority order.
278278
@@ -307,7 +307,7 @@ def locate_configs(path: Path, root: Optional[Path] = None) -> List[Path]:
307307
return results
308308

309309

310-
def read_configs(paths: List[Path]) -> List[RawConfig]:
310+
def read_configs(paths: List[Path]) -> list[RawConfig]:
311311
"""
312312
Read config data for each path given, and return their raw toml config values.
313313
@@ -400,7 +400,7 @@ def parse_rule(
400400

401401

402402
def merge_configs(
403-
path: Path, raw_configs: List[RawConfig], root: Optional[Path] = None
403+
path: Path, raw_configs: list[RawConfig], root: Optional[Path] = None
404404
) -> Config:
405405
"""
406406
Given multiple raw configs, merge them in priority order.
@@ -594,7 +594,7 @@ def generate_config(
594594
return config
595595

596596

597-
def validate_config(path: Path) -> List[str]:
597+
def validate_config(path: Path) -> list[str]:
598598
"""
599599
Validate the config provided. The provided path is expected to be a valid toml
600600
config file. Any exception found while parsing or importing will be added to a list

src/fixit/rules/chained_instance_check.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def unwrap(self, node: cst.BaseExpression) -> Iterator[cst.BaseExpression]:
139139

140140
def collect_targets(
141141
self, stack: Tuple[cst.BaseExpression, ...]
142-
) -> Tuple[
142+
) -> tuple[
143143
List[cst.BaseExpression], Dict[cst.BaseExpression, List[cst.BaseExpression]]
144144
]:
145145
targets: Dict[cst.BaseExpression, List[cst.BaseExpression]] = {}

src/fixit/rules/cls_in_classmethod.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
class _RenameTransformer(cst.CSTTransformer):
2424
def __init__(
25-
self, names: List[Union[cst.Name, cst.BaseString, cst.Attribute]], new_name: str
25+
self, names: list[Union[cst.Name, cst.BaseString, cst.Attribute]], new_name: str
2626
) -> None:
2727
self.names = names
2828
self.new_name = new_name

src/fixit/rules/no_namedtuple.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def leave_ClassDef(self, original_node: cst.ClassDef) -> None:
184184

185185
def partition_bases(
186186
self, original_bases: Sequence[cst.Arg]
187-
) -> Tuple[Optional[cst.Arg], List[cst.Arg]]:
187+
) -> tuple[Optional[cst.Arg], List[cst.Arg]]:
188188
# Returns a tuple of NamedTuple base object if it exists, and a list of non-NamedTuple bases
189189
namedtuple_base: Optional[cst.Arg] = None
190190
new_bases: List[cst.Arg] = []
+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
from typing import Set
7+
8+
import libcst
9+
10+
from fixit import Invalid, LintRule, Valid
11+
12+
13+
REPLACE_TYPING_TYPE_ANNOTATION: str = (
14+
"Use lowercase primitive type {primitive_type}"
15+
+ "instead of {typing_type} (See [PEP 585 – Type Hinting Generics In Standard Collections](https://peps.python.org/pep-0585/#forward-compatibility))"
16+
)
17+
18+
CUSTOM_TYPES_TO_REPLACE: Set[str] = {"Dict", "List", "Set", "Tuple"}
19+
20+
21+
class UsePrimitiveTypes(LintRule):
22+
"""
23+
Enforces the use of primitive types instead of those in the ``typing`` module ()
24+
since they are available on and ahead of Python ``3.10``.
25+
"""
26+
27+
PYTHON_VERSION = ">= 3.10"
28+
29+
VALID = [
30+
Valid(
31+
"""
32+
def foo() -> list:
33+
pass
34+
""",
35+
),
36+
Valid(
37+
"""
38+
def bar(x: set) -> None:
39+
pass
40+
""",
41+
),
42+
Valid(
43+
"""
44+
def baz(y: tuple) -> None:
45+
pass
46+
""",
47+
),
48+
Valid(
49+
"""
50+
def qux(z: dict) -> None:
51+
pass
52+
""",
53+
),
54+
]
55+
56+
INVALID = [
57+
Invalid(
58+
"""
59+
def foo() -> List[int]:
60+
pass
61+
""",
62+
expected_replacement="""
63+
def foo() -> list[int]:
64+
pass
65+
""",
66+
),
67+
Invalid(
68+
"""
69+
def bar(x: Set[str]) -> None:
70+
pass
71+
""",
72+
expected_replacement="""
73+
def bar(x: set[str]) -> None:
74+
pass
75+
""",
76+
),
77+
Invalid(
78+
"""
79+
def baz(y: Tuple[int, str]) -> None:
80+
pass
81+
""",
82+
expected_replacement="""
83+
def baz(y: tuple[int, str]) -> None:
84+
pass
85+
""",
86+
),
87+
Invalid(
88+
"""
89+
def qux(z: Dict[str, int]) -> None:
90+
pass
91+
""",
92+
expected_replacement="""
93+
def qux(z: dict[str, int]) -> None:
94+
pass
95+
""",
96+
),
97+
]
98+
99+
def __init__(self) -> None:
100+
super().__init__()
101+
self.annotation_counter: int = 0
102+
103+
def visit_Annotation(self, node: libcst.Annotation) -> None:
104+
self.annotation_counter += 1
105+
106+
def leave_Annotation(self, original_node: libcst.Annotation) -> None:
107+
self.annotation_counter -= 1
108+
109+
def visit_FunctionDef(self, node: libcst.FunctionDef) -> None:
110+
# Check return type
111+
if isinstance(node.returns, libcst.Annotation):
112+
if isinstance(node.returns.annotation, libcst.Subscript):
113+
base_type = node.returns.annotation.value
114+
if (
115+
isinstance(base_type, libcst.Name)
116+
and base_type.value in CUSTOM_TYPES_TO_REPLACE
117+
):
118+
new_base_type = base_type.with_changes(
119+
value=base_type.value.lower()
120+
)
121+
new_annotation = node.returns.annotation.with_changes(
122+
value=new_base_type
123+
)
124+
new_returns = node.returns.with_changes(annotation=new_annotation)
125+
new_node = node.with_changes(returns=new_returns)
126+
self.report(
127+
node,
128+
REPLACE_TYPING_TYPE_ANNOTATION.format(
129+
primitive_type=base_type.value.lower(),
130+
typing_type=base_type.value,
131+
),
132+
replacement=new_node,
133+
)
134+
135+
# Check parameter types
136+
for param in node.params.params:
137+
if isinstance(param.annotation, libcst.Annotation):
138+
if isinstance(param.annotation.annotation, libcst.Subscript):
139+
base_type = param.annotation.annotation.value
140+
if (
141+
isinstance(base_type, libcst.Name)
142+
and base_type.value in CUSTOM_TYPES_TO_REPLACE
143+
):
144+
new_base_type = base_type.with_changes(
145+
value=base_type.value.lower()
146+
)
147+
new_annotation = param.annotation.annotation.with_changes(
148+
value=new_base_type
149+
)
150+
new_param_annotation = param.annotation.with_changes(
151+
annotation=new_annotation
152+
)
153+
new_param = param.with_changes(annotation=new_param_annotation)
154+
self.report(
155+
param,
156+
REPLACE_TYPING_TYPE_ANNOTATION.format(
157+
primitive_type=base_type.value.lower(),
158+
typing_type=base_type.value,
159+
),
160+
replacement=new_param,
161+
)

src/fixit/testing.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def gen_all_test_methods(rules: Collection[LintRule]) -> Sequence[TestCasePrecur
164164

165165
def generate_lint_rule_test_cases(
166166
rules: Collection[LintRule],
167-
) -> List[Type[unittest.TestCase]]:
167+
) -> list[Type[unittest.TestCase]]:
168168
test_case_classes: List[Type[unittest.TestCase]] = []
169169
for test_case in gen_all_test_methods(rules):
170170
rule_name = type(test_case.rule).__name__
@@ -191,7 +191,7 @@ def test_method(
191191

192192

193193
def add_lint_rule_tests_to_module(
194-
module_attrs: Dict[str, Any], rules: Collection[LintRule]
194+
module_attrs: dict[str, Any], rules: Collection[LintRule]
195195
) -> None:
196196
"""
197197
Generates classes inheriting from `unittest.TestCase` from the data available in `rules` and adds these to module_attrs.

src/fixit/tests/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ def test_collect_rules(self) -> None:
487487
UseTypesFromTyping.TAGS = {"typing"}
488488
NoNamedTuple.TAGS = {"typing", "tuples"}
489489

490-
def collect_types(cfg: Config) -> List[Type[LintRule]]:
490+
def collect_types(cfg: Config) -> list[Type[LintRule]]:
491491
return sorted([type(rule) for rule in config.collect_rules(cfg)], key=str)
492492

493493
with self.subTest("everything"):

0 commit comments

Comments
 (0)