44so ``@route`` under ``from flask import route`` hits the same rule as
55``@app.route``. An unresolved decorator (``qualified_name is None``) never
66matches: under-approximate rather than guess.
7+
8+ Pattern grammar: ``{a,b}`` alternation (not nested) and a trailing/embedded
9+ ``*`` that matches module MEMBERS only -- it does not cross a ``.``, so
10+ ``rest_framework.viewsets.*`` matches ``ModelViewSet`` but not
11+ ``viewsets.mixins.ListModelMixin``. Everything else is literal.
12+ ``validate_pattern`` rejects anything outside this grammar (unbalanced or
13+ nested ``{``) so a typo in a rules file is a load-time ``RulesError``
14+ (enforced by ``rules.py``), never a crash mid-analysis.
715"""
816from __future__ import annotations
917
1018import ast
1119import re
12- from typing import Any , Dict , Iterable , List , Optional
20+ from typing import TYPE_CHECKING , Any , Dict , Iterable , List , Optional
1321
14- from codeanalyzer .entrypoints .rules import DecoratorRule
1522from codeanalyzer .schema .py_schema import PyEntrypoint
1623
24+ if TYPE_CHECKING :
25+ from codeanalyzer .entrypoints .rules import DecoratorRule
26+
1727# Dispatch names that are HTTP verbs. DRF's ViewSet dispatch names
1828# (list, retrieve, create, ...) are NOT verbs and must not be emitted as such.
1929_HTTP_VERBS = {"get" , "post" , "put" , "patch" , "delete" , "head" , "options" }
2030
2131
32+ class PatternError (ValueError ):
33+ """A ``match`` pattern outside the ``{a,b}`` / ``*`` grammar `_compile` handles."""
34+
35+
36+ def validate_pattern (pattern : str ) -> None :
37+ """Raise ``PatternError`` for unbalanced or nested ``{``."""
38+ depth = 0
39+ for ch in pattern :
40+ if ch == "{" :
41+ depth += 1
42+ if depth > 1 :
43+ raise PatternError (f"nested '{{' is not supported: { pattern !r} " )
44+ elif ch == "}" :
45+ depth -= 1
46+ if depth < 0 :
47+ raise PatternError (f"unmatched '}}': { pattern !r} " )
48+ if depth != 0 :
49+ raise PatternError (f"unbalanced '{{': { pattern !r} " )
50+
51+
2252def match_pattern (pattern : str , qualified_name : Optional [str ]) -> bool :
2353 """``{a,b}`` alternation and trailing ``*``; everything else is literal."""
2454 if not qualified_name :
@@ -27,6 +57,7 @@ def match_pattern(pattern: str, qualified_name: Optional[str]) -> bool:
2757
2858
2959def _compile (pattern : str ) -> str :
60+ validate_pattern (pattern )
3061 out , i = [], 0
3162 while i < len (pattern ):
3263 ch = pattern [i ]
@@ -36,7 +67,7 @@ def _compile(pattern: str) -> str:
3667 out .append ("(?:" + "|" .join (re .escape (a .strip ()) for a in alts ) + ")" )
3768 i = j + 1
3869 elif ch == "*" :
39- out .append (r"[^\s]*" )
70+ out .append (r"[^. \s]*" )
4071 i += 1
4172 else :
4273 out .append (re .escape (ch ))
@@ -65,7 +96,7 @@ def _route_of(dec, spec: Optional[Dict[str, Any]]) -> Optional[str]:
6596 return value if isinstance (value , str ) else None
6697
6798
68- def _methods_of (dec , rule : DecoratorRule , spec : Optional [Dict [str , Any ]]) -> List [str ]:
99+ def _methods_of (dec , spec : Optional [Dict [str , Any ]]) -> List [str ]:
69100 if not spec :
70101 return []
71102 source = spec .get ("from" )
@@ -82,7 +113,7 @@ def _methods_of(dec, rule: DecoratorRule, spec: Optional[Dict[str, Any]]) -> Lis
82113
83114
84115def entrypoints_from_decorators (
85- node , framework : str , rules : Iterable [DecoratorRule ], ruleset : str
116+ node , framework : str , rules : Iterable [" DecoratorRule" ], ruleset : str
86117) -> List [PyEntrypoint ]:
87118 out : List [PyEntrypoint ] = []
88119 for dec in getattr (node , "decorators" , []) or []:
@@ -97,7 +128,7 @@ def entrypoints_from_decorators(
97128 ruleset = ruleset ,
98129 evidence = dec .qualified_name ,
99130 route = _route_of (dec , rule .route ),
100- http_methods = _methods_of (dec , rule , rule .methods ),
131+ http_methods = _methods_of (dec , rule .methods ),
101132 )
102133 )
103134 return out
0 commit comments