77from __future__ import annotations
88
99from pathlib import Path
10- from typing import Iterable , Iterator
10+ from typing import Dict , Iterable , Iterator
1111
1212from codeanalyzer .entrypoints .detect import detected_frameworks
1313from codeanalyzer .entrypoints .matching import entrypoints_from_bases , entrypoints_from_decorators
1414from codeanalyzer .entrypoints .rules import RuleSet , load_rules
15- from codeanalyzer .schema .py_schema import PyApplication , PyCallable , PyClass
15+ from codeanalyzer .schema .py_schema import PyApplication , PyCallable , PyClass , PyModule
1616from codeanalyzer .utils import logger
1717
1818
@@ -38,35 +38,62 @@ def detect_entrypoints(
3838
3939def _run_stages (app : PyApplication , project_dir : Path , rules : RuleSet ) -> None :
4040 """Stages 0-4. Stage 0 (framework detection) and Stage 3 (decorator and
41- base-class matching) exist so far."""
41+ base-class matching) exist so far.
42+
43+ Base-class resolution needs the OWNING MODULE's import table (a written
44+ base like ``APIView`` only resolves via that module's own
45+ ``from rest_framework.views import APIView``), so this walks module by
46+ module rather than the whole app flat, building one resolver per module.
47+ """
4248 app .entrypoint_report .rulesets = list (rules .rulesets )
4349 frameworks = detected_frameworks (app , project_dir , rules )
4450 app .entrypoint_report .frameworks_detected = sorted (frameworks )
4551
46- ruleset_name = rules .rulesets [- 1 ] if len (rules .rulesets ) > 1 else "shipped"
47- for name in sorted (frameworks ):
48- fw = rules .frameworks [name ]
49- for node in _walk (app ):
50- node .entrypoints .extend (
51- entrypoints_from_decorators (node , name , fw .decorators , ruleset_name )
52- )
53- if isinstance (node , PyClass ) and fw .bases :
54- class_eps , method_eps = entrypoints_from_bases (
55- node , name , fw .bases , ruleset_name , lambda b : b
56- )
57- node .entrypoints .extend (class_eps )
58- for method_name , eps in method_eps .items ():
59- target = (node .callables or {}).get (method_name )
60- if target is not None :
61- target .entrypoints .extend (eps )
52+ names = sorted (frameworks )
53+ for mod in app .symbol_table .values ():
54+ resolve = _base_resolver (mod )
55+ for node in _walk_module (mod ):
56+ for name in names :
57+ fw = rules .frameworks [name ]
58+ node .entrypoints .extend (entrypoints_from_decorators (node , name , fw .decorators ))
59+ if isinstance (node , PyClass ) and fw .bases :
60+ class_eps , method_eps = entrypoints_from_bases (
61+ node , name , fw .bases , resolve
62+ )
63+ node .entrypoints .extend (class_eps )
64+ for method_name , eps in method_eps .items ():
65+ target = (node .callables or {}).get (method_name )
66+ if target is not None :
67+ target .entrypoints .extend (eps )
68+
69+
70+ def _base_resolver (mod : PyModule ):
71+ """A per-module ``resolve`` callable for ``entrypoints_from_bases``, built
72+ from the module's own import table -- exact data already on the node,
73+ never a Jedi guess. Covers ``from x.y import Z[ as W]`` and
74+ ``import x.y[ as z]``, plus a dotted base (``views.APIView``) whose head
75+ is the imported name. A base the import table has no mapping for is
76+ returned unchanged -- under-approximate rather than guess.
77+ """
78+ aliases : Dict [str , str ] = {}
79+ for imp in mod .imports or []:
80+ original = imp .alias or imp .name
81+ aliases [imp .name ] = imp .module if imp .module == original else f"{ imp .module } .{ original } "
82+
83+ def resolve (written : str ) -> str :
84+ head , _ , rest = written .partition ("." )
85+ target = aliases .get (head )
86+ return f"{ target } .{ rest } " if target and rest else (target or written )
87+
88+ return resolve
6289
6390
6491def _derive_flags (app : PyApplication ) -> None :
6592 for node in _walk (app ):
6693 node .is_entrypoint = bool (node .entrypoints )
6794
6895
69- def _walk ( app : PyApplication ) -> Iterator [object ]:
96+ def _walk_module ( mod : PyModule ) -> Iterator [object ]:
7097 def walk_callable (c : PyCallable ) -> Iterator [object ]:
7198 yield c
7299 for inner in (c .callables or {}).values ():
@@ -81,8 +108,12 @@ def walk_class(k: PyClass) -> Iterator[object]:
81108 for inner in (k .types or {}).values ():
82109 yield from walk_class (inner )
83110
111+ for fn in (mod .functions or {}).values ():
112+ yield from walk_callable (fn )
113+ for cls in (mod .types or {}).values ():
114+ yield from walk_class (cls )
115+
116+
117+ def _walk (app : PyApplication ) -> Iterator [object ]:
84118 for mod in app .symbol_table .values ():
85- for fn in (mod .functions or {}).values ():
86- yield from walk_callable (fn )
87- for cls in (mod .types or {}).values ():
88- yield from walk_class (cls )
119+ yield from _walk_module (mod )
0 commit comments