Skip to content
This repository was archived by the owner on Jan 15, 2025. It is now read-only.

Commit b646678

Browse files
committed
check early returns + match statement
1 parent fe01dcb commit b646678

File tree

7 files changed

+95
-28
lines changed

7 files changed

+95
-28
lines changed

.github/workflows/test.yaml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,24 @@ name: Test
33
on: [push]
44

55
jobs:
6-
build:
6+
coverage:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v2
10+
- name: Use Latest Python
11+
uses: actions/setup-python@v2
12+
with:
13+
python-version: "3.10"
14+
- name: Install Python Dependencies
15+
run: pip install -r requirements/nox-deps.txt
16+
- name: Run Tests
17+
run: nox -s test
18+
19+
environments:
720
runs-on: ubuntu-latest
821
strategy:
922
matrix:
1023
python-version: ["3.7", "3.8", "3.9", "3.10"]
11-
1224
steps:
1325
- uses: actions/checkout@v2
1426
- name: Use Python ${{ matrix.python-version }}
@@ -18,4 +30,4 @@ jobs:
1830
- name: Install Python Dependencies
1931
run: pip install -r requirements/nox-deps.txt
2032
- name: Run Tests
21-
run: nox -s test
33+
run: nox -s test -- --no-cov

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ tox
4242
<td>ROH102</td>
4343
<td>Hook was used inside a conditional or loop statement</td>
4444
</tr>
45+
<tr>
46+
<td>ROH103</td>
47+
<td>Hook was used after an early return</td>
48+
</tr>
4549
<tr>
4650
<td>ROH200</td>
4751
<td>

flake8_idom_hooks/rules_of_hooks.py

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1+
from __future__ import annotations
2+
13
import ast
2-
from typing import Optional, Union
4+
import sys
35

46
from .common import CheckContext, set_current
57

68

79
class RulesOfHooksVisitor(ast.NodeVisitor):
810
def __init__(self, context: CheckContext) -> None:
911
self._context = context
10-
self._current_hook: Optional[ast.FunctionDef] = None
11-
self._current_component: Optional[ast.FunctionDef] = None
12-
self._current_function: Optional[ast.FunctionDef] = None
13-
self._current_call: Optional[ast.Call] = None
14-
self._current_conditional: Union[None, ast.If, ast.IfExp, ast.Try] = None
15-
self._current_loop: Union[None, ast.For, ast.While] = None
12+
self._current_call: ast.Call | None = None
13+
self._current_component: ast.FunctionDef | None = None
14+
self._current_conditional: ast.If | ast.IfExp | ast.Try | None = None
15+
self._current_early_return: ast.Return | None = None
16+
self._current_function: ast.FunctionDef | None = None
17+
self._current_hook: ast.FunctionDef | None = None
18+
self._current_loop: ast.For | ast.While | None = None
1619

1720
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
1821
if self._context.is_hook_def(node):
@@ -24,6 +27,7 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
2427
# we need to reset these before enter new hook
2528
conditional=None,
2629
loop=None,
30+
early_return=None,
2731
):
2832
self.generic_visit(node)
2933
elif self._context.is_component_def(node):
@@ -34,13 +38,14 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
3438
# we need to reset these before visiting a new component
3539
conditional=None,
3640
loop=None,
41+
early_return=None,
3742
):
3843
self.generic_visit(node)
3944
else:
4045
with set_current(self, function=node):
4146
self.generic_visit(node)
4247

43-
def _visit_hook_usage(self, node: Union[ast.Name, ast.Attribute]) -> None:
48+
def _visit_hook_usage(self, node: ast.Name | ast.Attribute) -> None:
4449
self._check_if_propper_hook_usage(node)
4550

4651
visit_Attribute = _visit_hook_usage
@@ -53,6 +58,7 @@ def _visit_conditional(self, node: ast.AST) -> None:
5358
visit_If = _visit_conditional
5459
visit_IfExp = _visit_conditional
5560
visit_Try = _visit_conditional
61+
visit_Match = _visit_conditional
5662

5763
def _visit_loop(self, node: ast.AST) -> None:
5864
with set_current(self, loop=node):
@@ -61,14 +67,15 @@ def _visit_loop(self, node: ast.AST) -> None:
6167
visit_For = _visit_loop
6268
visit_While = _visit_loop
6369

70+
def visit_Return(self, node: ast.Return) -> None:
71+
self._current_early_return = node
72+
6473
def _check_if_hook_defined_in_function(self, node: ast.FunctionDef) -> None:
6574
if self._current_function is not None:
6675
msg = f"hook {node.name!r} defined as closure in function {self._current_function.name!r}"
6776
self._context.add_error(100, node, msg)
6877

69-
def _check_if_propper_hook_usage(
70-
self, node: Union[ast.Name, ast.Attribute]
71-
) -> None:
78+
def _check_if_propper_hook_usage(self, node: ast.Name | ast.Attribute) -> None:
7279
if isinstance(node, ast.Name):
7380
name = node.id
7481
else:
@@ -83,14 +90,24 @@ def _check_if_propper_hook_usage(
8390

8491
loop_or_conditional = self._current_conditional or self._current_loop
8592
if loop_or_conditional is not None:
86-
node_type = type(loop_or_conditional)
87-
node_type_to_name = {
88-
ast.If: "if statement",
89-
ast.IfExp: "inline if expression",
90-
ast.Try: "try statement",
91-
ast.For: "for loop",
92-
ast.While: "while loop",
93-
}
94-
node_name = node_type_to_name[node_type]
93+
node_name = _NODE_TYPE_TO_NAME[type(loop_or_conditional)]
9594
msg = f"hook {name!r} used inside {node_name}"
9695
self._context.add_error(102, node, msg)
96+
97+
if self._current_early_return:
98+
self._context.add_error(
99+
103,
100+
node,
101+
f"hook {name!r} used after an early return",
102+
)
103+
104+
105+
_NODE_TYPE_TO_NAME = {
106+
ast.If: "if statement",
107+
ast.IfExp: "inline if expression",
108+
ast.Try: "try statement",
109+
ast.For: "for loop",
110+
ast.While: "while loop",
111+
}
112+
if sys.version_info >= (3, 10):
113+
_NODE_TYPE_TO_NAME[ast.Match] = "match statement"

noxfile.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,18 @@ def test_suite(session: Session) -> None:
4646
def test_coverage(session: Session) -> None:
4747
install_requirements(session, "test-env")
4848
session.install("-e", ".")
49-
session.run("pytest", "tests", "--cov=flake8_idom_hooks", "--cov-report=term")
49+
50+
posargs = session.posargs[:]
51+
52+
if "--no-cov" in session.posargs:
53+
posargs.remove("--no-cov")
54+
session.log("Coverage won't be checked")
55+
session.install(".")
56+
else:
57+
posargs += ["--cov=flake8_idom_hooks", "--cov-report=term"]
58+
session.install("-e", ".")
59+
60+
session.run("pytest", "tests", *posargs)
5061

5162

5263
def install_requirements(session: Session, name: str) -> None:

tests/cases/hook_usage.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,11 @@ def Component():
159159
@component
160160
def use_other():
161161
use_state
162+
163+
164+
@component
165+
def example():
166+
if True:
167+
return None
168+
# error: ROH103 hook 'use_state' used after an early return
169+
use_state()

tests/cases/match_statement.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@component
2+
def example():
3+
match something:
4+
case int:
5+
# error: ROH102 hook 'use_state' used inside match statement
6+
use_state()

tests/test_cases.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ast
2+
import sys
23
from pathlib import Path
34

45
import pytest
@@ -32,14 +33,22 @@ def setup_plugin(args):
3233
"",
3334
"hook_usage.py",
3435
),
35-
(
36-
"--exhaustive-hook-deps",
37-
"exhaustive_deps.py",
38-
),
3936
(
4037
"",
4138
"no_exhaustive_deps.py",
4239
),
40+
pytest.param(
41+
"",
42+
"match_statement.py",
43+
marks=pytest.mark.skipif(
44+
sys.version_info < (3, 10),
45+
reason="Match statement only in Python 3.10 and above",
46+
),
47+
),
48+
(
49+
"--exhaustive-hook-deps",
50+
"exhaustive_deps.py",
51+
),
4352
(
4453
r"--component-decorator-pattern ^(component|custom_component)$",
4554
"custom_component_decorator_pattern.py",

0 commit comments

Comments
 (0)