1+ import ast
2+ from contextlib import contextmanager
13from pkg_resources import (
24 get_distribution as _get_distribution ,
35 DistributionNotFound as _DistributionNotFound ,
46)
7+ from typing import List , Tuple , Iterator , Union , Optional
58
69
710try :
@@ -17,8 +20,106 @@ class Plugin:
1720 version = __version__
1821 options = None
1922
20- def __init__ (self , tree ):
23+ def __init__ (self , tree : ast . Module ):
2124 self ._tree = tree
2225
2326 def run (self ):
24- return [(7 , 39 , "hooks cannot be used in conditionals" , type (self ))]
27+ visitor = HookRulesVisitor ()
28+ visitor .visit (self ._tree )
29+ cls = type (self )
30+ return [(line , col , msg , cls ) for line , col , msg in visitor .errors ]
31+
32+
33+ class HookRulesVisitor (ast .NodeVisitor ):
34+ def __init__ (self ):
35+ self .errors : List [Tuple [int , int , str ]] = []
36+ self ._current_function : Optional [ast .FunctionDef ] = None
37+ self ._current_call : Optional [ast .Call ] = None
38+ self ._current_conditional : Union [None , ast .If , ast .IfExp , ast .Try ] = None
39+ self ._current_loop : Union [None , ast .For , ast .While ] = None
40+
41+ def visit_FunctionDef (self , node : ast .FunctionDef ) -> None :
42+ self ._check_if_hook_defined_in_function (node )
43+ with self ._set_current (function = node ):
44+ self .generic_visit (node )
45+
46+ def _visit_hook_usage (self , node : ast .AST ) -> None :
47+ self ._check_if_propper_hook_usage (node )
48+
49+ visit_Attribute = _visit_hook_usage
50+ visit_Name = _visit_hook_usage
51+
52+ def _visit_conditional (self , node : ast .AST ) -> None :
53+ with self ._set_current (conditional = node ):
54+ self .generic_visit (node )
55+
56+ visit_If = _visit_conditional
57+ visit_IfExp = _visit_conditional
58+ visit_Try = _visit_conditional
59+
60+ def _visit_loop (self , node : ast .AST ) -> None :
61+ with self ._set_current (loop = node ):
62+ self .generic_visit (node )
63+
64+ visit_For = _visit_loop
65+ visit_While = _visit_loop
66+
67+ def _check_if_hook_defined_in_function (self , node : ast .FunctionDef ) -> None :
68+ if self ._current_function is not None and _is_hook_or_element_def (node ):
69+ msg = f"Hook { node .name !r} defined inside another function."
70+ self .errors .append ((node .lineno , node .col_offset , msg ))
71+
72+ def _check_if_propper_hook_usage (self , node : Union [ast .Name , ast .Attribute ]):
73+ if isinstance (node , ast .Name ):
74+ name = node .id
75+ else :
76+ name = node .attr
77+
78+ if not _is_hook_function_name (name ):
79+ return
80+
81+ if not _is_hook_or_element_def (self ._current_function ):
82+ msg = f"Hook { name !r} used outside element or hook definition."
83+ self .errors .append ((node .lineno , node .col_offset , msg ))
84+ return
85+
86+ _loop_or_conditional = self ._current_conditional or self ._current_loop
87+ if _loop_or_conditional is not None :
88+ node_type = type (_loop_or_conditional )
89+ node_type_to_name = {
90+ ast .If : "if statement" ,
91+ ast .IfExp : "inline if expression" ,
92+ ast .Try : "try statement" ,
93+ ast .For : "for loop" ,
94+ ast .While : "while loop" ,
95+ }
96+ node_name = node_type_to_name [node_type ]
97+ msg = f"Hook { name !r} used inside { node_name } ."
98+ self .errors .append ((node .lineno , node .col_offset , msg ))
99+ return
100+
101+ @contextmanager
102+ def _set_current (self , ** attrs ) -> Iterator [None ]:
103+ old_attrs = {k : getattr (self , f"_current_{ k } " ) for k in attrs }
104+ for k , v in attrs .items ():
105+ setattr (self , f"_current_{ k } " , v )
106+ try :
107+ yield
108+ finally :
109+ for k , v in old_attrs .items ():
110+ setattr (self , f"_current_{ k } " , v )
111+
112+
113+ def _is_hook_or_element_def (node : Optional [ast .FunctionDef ]) -> bool :
114+ if node is None :
115+ return False
116+ else :
117+ return _is_element_function_name (node .name ) or _is_hook_function_name (node .name )
118+
119+
120+ def _is_element_function_name (name : str ) -> bool :
121+ return name [0 ].upper () == name [0 ] and "_" not in name
122+
123+
124+ def _is_hook_function_name (name : str ) -> bool :
125+ return name .lstrip ("_" ).startswith ("use_" )
0 commit comments