Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add(rule): prefer-col-init #56

Merged
merged 1 commit into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,29 @@ def sync_users() -> None:
...
```

### PIE799: prefer-col-init

Check that values are passed in when collections are created rather than
creating an empty collection and then inserting.

```python
# error
bars = []
bar = bar()
bars.append(bar)

# ok
bar = bar()
bars = [bar]

# error
s = deque()
s.append(foo)

# ok
s = deque([foo])
```

## dev

```shell
Expand Down
2 changes: 2 additions & 0 deletions flake8_pie/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from flake8_pie.pie796_prefer_unique_enums import pie786_prefer_unique_enum
from flake8_pie.pie797_no_unnecessary_if_expr import pie797_no_unnecessary_if_expr
from flake8_pie.pie798_no_unnecessary_class import pie798_no_unnecessary_class
from flake8_pie.pie799_prefer_col_init import pie799_prefer_col_init


@dataclass(frozen=True)
Expand Down Expand Up @@ -132,6 +133,7 @@ def visit_IfExp(self, node: ast.IfExp) -> None:
def _visit_body(self, node: Body) -> None:
pie781_assign_and_return(node, self.errors)
pie790_no_unnecessary_pass(node, self.errors)
pie799_prefer_col_init(node, self.errors)

def visit_Module(self, node: ast.Module) -> None:
self._visit_body(node)
Expand Down
78 changes: 78 additions & 0 deletions flake8_pie/pie799_prefer_col_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from __future__ import annotations

import ast
from dataclasses import dataclass
from functools import partial

from typing_extensions import Literal

from flake8_pie.base import Body, Error


@dataclass(frozen=True)
class ColDecl:
lineno: int
name: str
kind: Literal["list", "deque"]


def _get_assign_target(node: ast.stmt) -> ast.Name | None:
if (
isinstance(node, ast.Assign)
and len(node.targets) == 1
and isinstance(node.targets[0], ast.Name)
):
return node.targets[0]
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
return node.target
return None


def pie799_prefer_col_init(node: Body, errors: list[Error]) -> None:
cur_var_name: ColDecl | None = None
for stmt in node.body:
# bail out early in case we have control flow, like a for loop that
# might mutate the variable deceleration.
if not isinstance(stmt, (ast.Assign, ast.AnnAssign, ast.AugAssign, ast.Expr)):
cur_var_name = None
continue

assign_target = _get_assign_target(stmt)
if assign_target is not None:
if isinstance(stmt.value, ast.List):
cur_var_name = ColDecl(
name=assign_target.id, lineno=assign_target.lineno, kind="list"
)
if (
isinstance(stmt.value, ast.Call)
and isinstance(stmt.value.func, ast.Name)
and stmt.value.func.id == "deque"
and len(stmt.value.args) == 0
):
cur_var_name = ColDecl(
name=assign_target.id, lineno=assign_target.lineno, kind="deque"
)

if (
isinstance(stmt, ast.Expr)
and isinstance(stmt.value, ast.Call)
and isinstance(stmt.value.func, ast.Attribute)
and isinstance(stmt.value.func.value, ast.Name)
and cur_var_name is not None
and stmt.value.func.value.id == cur_var_name.name
and (
(cur_var_name.kind == "list" and stmt.value.func.attr == "append")
or (
cur_var_name.kind == "deque"
and stmt.value.func.attr in {"append", "appendleft"}
)
)
):
errors.append(PIE799(lineno=stmt.lineno, col_offset=stmt.col_offset))
cur_var_name = None


PIE799 = partial(
Error,
message="PIE799: prefer-col-init: Consider passing values in when creating the collection.",
)
178 changes: 178 additions & 0 deletions flake8_pie/tests/test_pie799_prefer_col_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
from __future__ import annotations

import ast

import pytest

from flake8_pie import Flake8PieCheck
from flake8_pie.pie799_prefer_col_init import PIE799
from flake8_pie.tests.utils import Error, ex, to_errors

EXAMPLES = [
ex(
code="""
bar = []
foo = Foo()
bar.append(foo)
""",
errors=[PIE799(lineno=4, col_offset=0)],
),
# ex(
# code="""
# if some_cond:
# bar = {}
# foo = Foo()
# bar["foo"] = foo
# """,
# errors=[PIE799(lineno=2, col_offset=0)],
# ),
# ex(
# code="""
# if some_cond:
# bar = dict()
# foo = Foo()
# bar["foo"] = foo
# """,
# errors=[PIE799(lineno=2, col_offset=0)],
# ),
# ex(
# code="""
# bar = set()
# foo = Foo()
# bar.add(foo)
# """,
# errors=[PIE799(lineno=2, col_offset=0)],
# ),
ex(
code="""
bar = []
bar.append(Foo())
""",
errors=[PIE799(lineno=3, col_offset=0)],
),
ex(
code="""
def fn() -> list[Foo]:
foos = []
foo = Foo()
foos.append(foo)
return foos
""",
errors=[PIE799(lineno=5, col_offset=4)],
),
ex(
code="""
bar: list[int] = []
bar.append(Foo())
""",
errors=[PIE799(lineno=3, col_offset=0)],
),
ex(
code="""
bar = []
bar.append(Foo())
bar.append(Foo())
""",
errors=[PIE799(lineno=3, col_offset=0)],
),
ex(
code="""
bar = []
x: int = 10
x += buzz
bar.append(x)
""",
errors=[PIE799(lineno=5, col_offset=0)],
),
ex(
# for loop allows the append() later on
code="""
bar = []
for el in range(buzz):
if el.fuzz > 10:
bar.append("foo")
else:
bar.append("foo")
foo = "foo bar buzz"
bar.append(foo)
""",
errors=[],
),
ex(
code="""
bar = [Foo()]
bar.append(Foo())
""",
errors=[PIE799(lineno=3, col_offset=0)],
),
ex(
code="""
s: Deque[str] = deque()
s.append(Foo())
""",
errors=[PIE799(lineno=3, col_offset=0)],
),
ex(
code="""
s = deque()
s.appendleft(Foo())
""",
errors=[PIE799(lineno=3, col_offset=0)],
),
ex(
code="""
s = deque(bar)
s.append("foo")
""",
errors=[],
),
ex(
code="""
foo = Foo()
bar = [foo]
""",
errors=[],
),
ex(
code="""
s: Deque[str] = deque(["start"])
s.append("foo")
""",
errors=[],
),
ex(
code="""
s = deque(["start"])
s.append("foo")
""",
errors=[],
),
ex(
code="""
s = deque(("start",))
s.append("foo")
""",
errors=[],
),
ex(
code="""
s = deque(bar)
s.append(Foo())
""",
errors=[],
),
ex(
code="""
foos = []
if buzz:
foos.append(Foo())
""",
errors=[],
),
]


@pytest.mark.parametrize("code,errors", EXAMPLES)
def test_prefer_literal(code: str, errors: list[Error]) -> None:
expr = ast.parse(code)
assert to_errors(Flake8PieCheck(expr, filename="foo.py").run()) == errors