progmune-runtime 3.7.13 → 3.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -161,14 +161,68 @@ class UrlCollector(ast.NodeVisitor):
161
161
  def __init__(self, filepath):
162
162
  self.file = filepath
163
163
  self.routes = []
164
+ # DRF ViewSet/DefaultRouter(REALWORLD_STRUCTURAL_V3:router.register
165
+ # + include(router.urls) 的路由由运行时生成,urlpatterns 解析不可见)
166
+ self.router_regs = {} # router 变量 -> [(prefix, viewName)]
167
+ self.router_slash = {} # router 变量 -> trailing_slash
168
+ self.router_uses = [] # (prefixBase, routerVar) urlpatterns 里的 include
164
169
 
165
170
  def visit_Assign(self, node):
166
171
  for t in node.targets:
167
- if isinstance(t, ast.Name) and t.id == "urlpatterns" and isinstance(node.value, (ast.List, ast.Tuple)):
172
+ if not isinstance(t, ast.Name):
173
+ continue
174
+ # DefaultRouter 定义(trailing_slash 关键字,默认 True)
175
+ if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name) \
176
+ and node.value.func.id == "DefaultRouter":
177
+ slash = True
178
+ for kw in node.value.keywords:
179
+ if kw.arg == "trailing_slash" and isinstance(kw.value, ast.Constant):
180
+ slash = bool(kw.value.value)
181
+ self.router_slash[t.id] = slash
182
+ self.router_regs.setdefault(t.id, [])
183
+ # urlpatterns:扫描直连条目,并展开 include(router.urls) 的 ViewSet
184
+ if t.id == "urlpatterns" and isinstance(node.value, (ast.List, ast.Tuple)):
168
185
  for elt in node.value.elts:
169
186
  self._scan_entry(elt)
187
+ for (prefix, var) in self.router_uses:
188
+ self._expand_router_routes(prefix, var)
170
189
  self.generic_visit(node)
171
190
 
191
+ def visit_Expr(self, node):
192
+ # router.register(r'articles', ArticleViewSet, ...) 语句
193
+ call = node.value
194
+ if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute) \
195
+ and call.func.attr == "register":
196
+ var = name_of(call.func.value)
197
+ if not var or var not in self.router_regs:
198
+ return
199
+ prefix = ""
200
+ if call.args and isinstance(call.args[0], ast.Constant):
201
+ prefix = str(call.args[0].value)
202
+ view = None
203
+ if len(call.args) > 1:
204
+ a1 = call.args[1]
205
+ if isinstance(a1, ast.Name):
206
+ view = a1.id
207
+ elif isinstance(a1, ast.Attribute):
208
+ view = a1.attr
209
+ if view:
210
+ self.router_regs.setdefault(var, []).append((prefix, view))
211
+ self.generic_visit(node)
212
+
213
+ def _expand_router_routes(self, prefix_base, var):
214
+ # 本工具不做 include 前缀传播(urlconf 各文件 pattern 独立)——
215
+ # prefix_base 仅作路由存在性触发,不拼进 pattern
216
+ for (prefix, view) in self.router_regs.get(var, []):
217
+ base = "^" + prefix
218
+ # DRF 默认 action 路由:list/create 集合级、retrieve/update/
219
+ # destroy 详情级(权限由 views 表判定,规则在 TS 检测器侧)
220
+ for pat in (base + "/?$", base + "/(?P<pk>[^/.]+)/?$"):
221
+ self.routes.append({
222
+ "pattern": pat, "urlname": "", "view": view, "kind": "cbv",
223
+ "file": self.file, "viewset": True,
224
+ })
225
+
172
226
  def _scan_entry(self, elt):
173
227
  if not isinstance(elt, ast.Call):
174
228
  return
@@ -199,6 +253,13 @@ class UrlCollector(ast.NodeVisitor):
199
253
  elif isinstance(view_ref, ast.Call) and isinstance(view_ref.func, ast.Name):
200
254
  if view_ref.func.id == "include":
201
255
  kind = "include"
256
+ # include(router.urls):记录 router 变量(同一文件的
257
+ # router.register 展开见 _expand_router_routes)
258
+ if view_ref.args and isinstance(view_ref.args[0], ast.Attribute) \
259
+ and view_ref.args[0].attr == "urls":
260
+ rvar = name_of(view_ref.args[0].value)
261
+ if rvar:
262
+ self.router_uses.append((pattern, rvar))
202
263
  else:
203
264
  kind = "fbv"
204
265
  view_name = view_ref.func.id
@@ -26,8 +26,10 @@ SKIP_DIRS = {"tests", "test", "deps", "venv", "env", "node_modules", "vendor",
26
26
  ".git", "migrations", "__pycache__", "scripts", "docs",
27
27
  "staticfiles", "static"}
28
28
 
29
+ # jwt 必在词表:flask_jwt_extended 的 @jwt_required 是生态头号认证装饰器
30
+ # (REALWORLD_STRUCTURAL_V4:缺 "jwt" → 10 个受保护 mutation 全误报 FP)
29
31
  AUTH_WORDS = ("auth", "login", "permission", "token", "credential", "session",
30
- "user")
32
+ "user", "jwt")
31
33
 
32
34
  MUTATION_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
33
35
 
@@ -30,9 +30,13 @@ import sys
30
30
 
31
31
  # 认证词表(依赖函数名命中即视为 auth-like;与 TS 侧 annotation-suggest 词表同源)
32
32
  # 注意:扫描器只做「结构提取」,规则判定(豁免词/方法门控)在 TS 检测器侧。
33
+ # 注意:不含裸 "user"——get_profile_by_username_from_path 等 DB 查询
34
+ # 依赖名含 user 会被误标认证(REALWORLD_STRUCTURAL_V2 假保护 FN)。
35
+ # 真认证依赖用 current_user/authorizer 等强词识别(get_current_user
36
+ # 含 current_user ✓)。
33
37
  AUTH_WORDS = (
34
- "auth", "login", "token", "user", "credential", "session",
35
- "bearer", "permission", "current_user", "api_key", "oauth",
38
+ "auth", "login", "token", "credential", "session",
39
+ "bearer", "permission", "current_user", "api_key", "oauth", "jwt",
36
40
  )
37
41
 
38
42
  SKIP_DIRS = {"tests", "test", "deps", "venv", "env", "node_modules", "vendor",