check_circular_deps.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. tools/check_circular_deps.py — CFC 后端 Spring 循环依赖静态检查
  5. 用途
  6. ----
  7. 在提交前静态解析 cfc-backend/src/main/java 的全部 Java 源码,构建「Bean 依赖图」,
  8. 区分 **eager 边**(启动期立即实例化)与 **@Lazy 边**(延迟代理,启动期不实例化),
  9. 输出:
  10. 1. eager 循环依赖清单 —— 启动期致命,必须为空;非空时退出码 1(阻断提交)
  11. 2. @Lazy 掩盖的设计环台账 —— 允许存在(既有 30+ 条),仅登记不阻断
  12. 3. @Lazy 注入点清单 —— 供人工复核「@Lazy 仅在字段名与 Bean 名严格一致时使用」
  13. 解析能力
  14. --------
  15. * 字段注入:@Resource / @Autowired(含 @Lazy、@Qualifier、@Resource(name="..."))
  16. * setter 注入:方法上的 @Autowired / @Resource
  17. * 构造器注入:构造器上的 @Autowired / @Resource
  18. * @Resource 按 by-name 解析(by-name 失败回退 by-type)
  19. * @Autowired 按 by-type 解析;接口类型回退到「唯一实现类」
  20. * Bean 识别:@Service / @Component / @Repository / @Controller / @RestController /
  21. @Configuration / com.etotem.cfc.mapper 下的 Mapper 接口
  22. 退出码
  23. ------
  24. 0 —— 未发现 eager 循环依赖
  25. 1 —— 发现 eager 循环依赖(必须从设计上打破,禁止用 @Lazy 掩盖新环)
  26. 2 —— 脚本自身错误(源码目录不存在等)
  27. 用法
  28. ----
  29. python3 tools/check_circular_deps.py
  30. """
  31. import os
  32. import re
  33. import sys
  34. from collections import defaultdict
  35. # --------------------------------------------------------------------------- #
  36. # 路径与常量
  37. # --------------------------------------------------------------------------- #
  38. ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
  39. SRC_DIR = os.path.join(ROOT, "cfc-backend", "src", "main", "java")
  40. STEREOTYPE_SIMPLE = {"Service", "Component", "Repository", "Controller",
  41. "RestController", "Configuration"}
  42. MAPPER_PACKAGE_PREFIX = "com.etotem.cfc.mapper"
  43. INJECT_ANNOTATIONS = ("@Resource", "@Autowired")
  44. LAZY_ANNOTATION = "@Lazy"
  45. # 集合/容器注入:Spring 注入的是「多个候选」,无法归到单一 Bean 节点,解析时跳过
  46. COLLECTION_TYPES = {
  47. "List", "Set", "Map", "Collection", "Iterable", "ArrayList", "LinkedList",
  48. "HashSet", "TreeSet", "HashMap", "LinkedHashMap", "TreeMap", "Optional",
  49. "ObjectProvider", "Multimap",
  50. }
  51. # --------------------------------------------------------------------------- #
  52. # 源码预处理
  53. # --------------------------------------------------------------------------- #
  54. def strip_comments(src):
  55. """把注释替换为等长空格(保留换行与偏移),避免注释中的注解造成误判。"""
  56. out = list(src)
  57. i, n = 0, len(src)
  58. state = None # None / 'line' / 'block' / 'str' / 'char'
  59. while i < n:
  60. c = src[i]
  61. nxt = src[i + 1] if i + 1 < n else ""
  62. if state is None:
  63. if c == "/" and nxt == "/":
  64. state = "line"
  65. out[i] = out[i + 1] = " "
  66. i += 2
  67. continue
  68. if c == "/" and nxt == "*":
  69. state = "block"
  70. out[i] = out[i + 1] = " "
  71. i += 2
  72. continue
  73. if c == '"':
  74. state = "str"
  75. i += 1
  76. continue
  77. if c == "'":
  78. state = "char"
  79. i += 1
  80. continue
  81. i += 1
  82. continue
  83. if state == "line":
  84. if c == "\n":
  85. state = None
  86. else:
  87. out[i] = " "
  88. i += 1
  89. continue
  90. if state == "block":
  91. if c == "*" and nxt == "/":
  92. out[i] = out[i + 1] = " "
  93. state = None
  94. i += 2
  95. continue
  96. if c != "\n":
  97. out[i] = " "
  98. i += 1
  99. continue
  100. if state in ("str", "char"):
  101. quote = '"' if state == "str" else "'"
  102. if c == "\\":
  103. i += 2
  104. continue
  105. if c == quote:
  106. state = None
  107. i += 1
  108. continue
  109. return "".join(out)
  110. def decapitalize(name):
  111. """Spring 默认 Bean 名:首字母小写(前两字母均大写则保持原样)。"""
  112. if not name:
  113. return name
  114. if len(name) > 1 and name[0].isupper() and name[1].isupper():
  115. return name
  116. return name[0].lower() + name[1:]
  117. def strip_generics(type_text):
  118. """List<Foo> -> List;Map<String, Foo> -> Map"""
  119. depth = 0
  120. out = []
  121. for ch in type_text:
  122. if ch == "<":
  123. depth += 1
  124. continue
  125. if ch == ">":
  126. depth = max(0, depth - 1)
  127. continue
  128. if depth == 0:
  129. out.append(ch)
  130. return "".join(out).strip()
  131. def simple_type(type_text):
  132. """取类型简单名(去包名、泛型、数组)"""
  133. t = strip_generics(type_text).strip()
  134. t = t.split(".")[-1].strip()
  135. while t.endswith("[]"):
  136. t = t[:-2].strip()
  137. t = t.replace("...", "").strip()
  138. return t
  139. # --------------------------------------------------------------------------- #
  140. # 类型声明解析
  141. # --------------------------------------------------------------------------- #
  142. TYPE_DECL_RE = re.compile(
  143. r"(?P<prefix>[^{};]*?)\b(?P<kind>class|interface|enum)\s+(?P<name>\w+)"
  144. r"(?P<rest>[^{};]*?)\{"
  145. )
  146. class TypeDecl(object):
  147. __slots__ = ("name", "kind", "package", "file", "prefix", "rest", "body_start", "body_end")
  148. def __init__(self, name, kind, package, file, prefix, rest, body_start, body_end):
  149. self.name = name
  150. self.kind = kind
  151. self.package = package
  152. self.file = file
  153. self.prefix = prefix
  154. self.rest = rest
  155. self.body_start = body_start
  156. self.body_end = body_end
  157. def parse_package(src):
  158. m = re.search(r"^\s*package\s+([\w.]+)\s*;", src, re.M)
  159. return m.group(1) if m else ""
  160. def parse_type_decls(src, file_path):
  161. decls = []
  162. package = parse_package(src)
  163. for m in TYPE_DECL_RE.finditer(src):
  164. prefix = m.group("prefix")
  165. # 仅认列 0 开始的顶层声明(缩进声明视为方法体内的局部类/匿名类,跳过)
  166. last_line = prefix.split("\n")[-1] if prefix else ""
  167. if last_line[:1] in (" ", "\t"):
  168. continue
  169. body_start = m.end()
  170. depth = 1
  171. i = body_start
  172. while i < len(src) and depth > 0:
  173. if src[i] == "{":
  174. depth += 1
  175. elif src[i] == "}":
  176. depth -= 1
  177. i += 1
  178. decls.append(TypeDecl(
  179. name=m.group("name"),
  180. kind=m.group("kind"),
  181. package=package,
  182. file=file_path,
  183. prefix=prefix,
  184. rest=m.group("rest"),
  185. body_start=body_start,
  186. body_end=i - 1,
  187. ))
  188. return decls
  189. def prefix_annotation_names(prefix):
  190. """提取声明前注解块中的注解简单名(兼容全限定写法)"""
  191. return [m.split(".")[-1] for m in re.findall(r"@\s*([\w.]+)", prefix)]
  192. def is_stereotype(prefix):
  193. """类声明前的注解块里是否含 Spring stereotype 注解"""
  194. return bool(STEREOTYPE_SIMPLE & set(prefix_annotation_names(prefix)))
  195. def stereotype_name(prefix):
  196. """解析 @Service("xxx") / @Component("xxx") 显式 Bean 名"""
  197. for m in re.finditer(r"@\s*([\w.]+)(?:\s*\(\s*\"([^\"]+)\"\s*\))", prefix):
  198. if m.group(1).split(".")[-1] in STEREOTYPE_SIMPLE:
  199. return m.group(2)
  200. return None
  201. # --------------------------------------------------------------------------- #
  202. # 注入点解析
  203. # --------------------------------------------------------------------------- #
  204. class InjectionPoint(object):
  205. __slots__ = ("owner", "kind", "field", "type", "annotation", "lazy", "file", "line", "explicit_name")
  206. def __init__(self, owner, kind, field, type_, annotation, lazy, file, line, explicit_name=None):
  207. self.owner = owner
  208. self.kind = kind # field / setter / constructor
  209. self.field = field
  210. self.type = type_
  211. self.annotation = annotation # @Resource / @Autowired
  212. self.lazy = lazy
  213. self.file = file
  214. self.line = line
  215. self.explicit_name = explicit_name
  216. def describe(self):
  217. loc = "%s:%d" % (os.path.relpath(self.file, ROOT), self.line)
  218. return "%s.%s (%s %s%s) -> %s [%s]" % (
  219. self.owner, self.field, self.annotation,
  220. "@Lazy " if self.lazy else "",
  221. self.type, loc, self.kind)
  222. ANNOTATION_RE = re.compile(
  223. r"\s*@\s*(?P<name>[\w.]+)(?:\s*\((?P<args>(?:[^()]|\([^()]*\))*)\))?")
  224. def skip_annotations(src, pos):
  225. """跳过紧邻的注解块,返回 (新位置, 注解列表)"""
  226. anns = []
  227. while True:
  228. m = ANNOTATION_RE.match(src, pos)
  229. if not m:
  230. break
  231. anns.append((m.group("name"), m.group("args") or ""))
  232. pos = m.end()
  233. return pos, anns
  234. def find_next_token(src, pos, tokens=(";", "{", "=")):
  235. """找到下一个顶层分隔符(跳过字符串/字符字面量与括号)"""
  236. depth = 0
  237. i = pos
  238. n = len(src)
  239. while i < n:
  240. c = src[i]
  241. if c in "\"'":
  242. quote = c
  243. i += 1
  244. while i < n:
  245. if src[i] == "\\":
  246. i += 2
  247. continue
  248. if src[i] == quote:
  249. break
  250. i += 1
  251. i += 1
  252. continue
  253. if c in "([{":
  254. depth += 1
  255. elif c in ")]}":
  256. depth -= 1
  257. elif depth == 0 and c in tokens:
  258. return c, i
  259. i += 1
  260. return None, n
  261. def parse_field_decl(text):
  262. """'private Foo bar' -> ('Foo', 'bar');'private List<Foo> bars' -> ('List', 'bars')"""
  263. text = text.strip()
  264. if not text:
  265. return None, None
  266. text = re.sub(r"^(?:public|protected|private|static|final|transient|volatile)\s+", "", text)
  267. text = text.strip()
  268. if not text:
  269. return None, None
  270. # 泛型整体作为一个 token
  271. m = re.match(r"^(?P<type>[\w.]+(?:\s*<.*?>)?(?:\s*\[\s*\])*)\s+(?P<name>\w+)\s*$", text, re.S)
  272. if not m:
  273. return None, None
  274. return m.group("type"), m.group("name")
  275. def split_params(param_text):
  276. """按顶层逗号切分参数列表"""
  277. parts, buf, depth = [], [], 0
  278. for ch in param_text:
  279. if ch in "<([":
  280. depth += 1
  281. elif ch in ">)]":
  282. depth -= 1
  283. if ch == "," and depth == 0:
  284. parts.append("".join(buf))
  285. buf = []
  286. else:
  287. buf.append(ch)
  288. if "".join(buf).strip():
  289. parts.append("".join(buf))
  290. return parts
  291. def parse_method_params(text):
  292. """解析方法/构造器参数 -> [(type, name), ...]"""
  293. idx = text.find("(")
  294. if idx < 0:
  295. return []
  296. start = idx + 1
  297. depth = 1
  298. i = start
  299. while i < len(text) and depth > 0:
  300. if text[i] in "([{":
  301. depth += 1
  302. elif text[i] in ")]}":
  303. depth -= 1
  304. i += 1
  305. inner = text[start:i - 1]
  306. result = []
  307. for raw in split_params(inner):
  308. p = raw.strip()
  309. if not p:
  310. continue
  311. p = re.sub(r"^(?:final|@\w+(\([^)]*\))?)\s+", "", p)
  312. p = re.sub(r"^(?:public|protected|private)\s+", "", p).strip()
  313. tokens = re.findall(r"[\w.]+(?:\s*<.*?>)?(?:\s*\[\s*\])?|\w+", p)
  314. if not tokens:
  315. continue
  316. if len(tokens) == 1:
  317. result.append((tokens[0], tokens[0]))
  318. else:
  319. result.append((" ".join(tokens[:-1]), tokens[-1]))
  320. return result
  321. def scan_class_body(src, decl):
  322. """扫描类体(顶层成员)中的注入点"""
  323. points = []
  324. body = src[decl.body_start:decl.body_end]
  325. offset = decl.body_start
  326. i = 0
  327. depth = 0
  328. n = len(body)
  329. while i < n:
  330. c = body[i]
  331. if c in "\"'":
  332. quote = c
  333. i += 1
  334. while i < n:
  335. if body[i] == "\\":
  336. i += 2
  337. continue
  338. if body[i] == quote:
  339. break
  340. i += 1
  341. i += 1
  342. continue
  343. if c == "{":
  344. depth += 1
  345. i += 1
  346. continue
  347. if c == "}":
  348. depth -= 1
  349. i += 1
  350. continue
  351. if depth == 0 and c == "@":
  352. # 收集连续的注解块(顺序不限:@Lazy @Autowired / @Autowired @Lazy / @Resource @Lazy)
  353. block_start = i
  354. pos = i
  355. block = []
  356. while True:
  357. m = ANNOTATION_RE.match(body, pos)
  358. if not m:
  359. break
  360. block.append((m.group("name"), m.group("args") or ""))
  361. pos = m.end()
  362. if not block:
  363. i += 1
  364. continue
  365. # 注解名归一为简单名(兼容 @org.springframework.context.annotation.Lazy 等全限定写法)
  366. simple_names = [a[0].split(".")[-1] for a in block]
  367. inject_ann = None
  368. if "Resource" in simple_names:
  369. inject_ann = "Resource"
  370. elif "Autowired" in simple_names:
  371. inject_ann = "Autowired"
  372. if inject_ann is None:
  373. i = max(pos, block_start + 1)
  374. continue
  375. lazy = "Lazy" in simple_names
  376. explicit_name = None
  377. for aname, aargs in block:
  378. if aname.split(".")[-1] in ("Resource", "Autowired"):
  379. mn = re.search(r"name\s*=\s*\"([^\"]+)\"", aargs or "")
  380. if mn:
  381. explicit_name = mn.group(1)
  382. after = pos
  383. # 找到声明
  384. tok, tok_pos = find_next_token(body, after)
  385. if tok is None:
  386. i = after
  387. continue
  388. seg = body[after:tok_pos]
  389. if tok in (";", "="):
  390. type_text, field = parse_field_decl(seg)
  391. if type_text and field:
  392. points.append(InjectionPoint(
  393. owner=decl.name, kind="field", field=field,
  394. type_=simple_type(type_text), annotation="@" + inject_ann,
  395. lazy=lazy, file=decl.file,
  396. line=src.count("\n", 0, offset + after) + 1,
  397. explicit_name=explicit_name))
  398. elif tok == "{":
  399. params = parse_method_params(seg)
  400. is_ctor = re.search(r"\b%s\s*\(" % re.escape(decl.name), seg) is not None
  401. for ptype, pname in params:
  402. if ptype.startswith("this"):
  403. continue
  404. points.append(InjectionPoint(
  405. owner=decl.name, kind="constructor" if is_ctor else "setter",
  406. field=pname, type_=simple_type(ptype), annotation="@" + inject_ann,
  407. lazy=lazy, file=decl.file,
  408. line=src.count("\n", 0, offset + after) + 1,
  409. explicit_name=explicit_name))
  410. i = max(after, block_start + 1)
  411. continue
  412. i += 1
  413. return points
  414. # --------------------------------------------------------------------------- #
  415. # Bean 注册表与依赖解析
  416. # --------------------------------------------------------------------------- #
  417. class Registry(object):
  418. def __init__(self):
  419. self.bean_by_name = {} # bean name -> TypeDecl
  420. self.classes_by_simple = defaultdict(list) # simple name -> [TypeDecl]
  421. self.interfaces = {} # simple name -> TypeDecl(kind=interface)
  422. self.implementations = defaultdict(list) # interface simple name -> [TypeDecl]
  423. def add(self, decl, bean_name):
  424. self.bean_by_name[bean_name] = decl
  425. self.classes_by_simple[decl.name].append(decl)
  426. if decl.kind == "interface":
  427. self.interfaces[decl.name] = decl
  428. def link(self):
  429. for decl in list(self.classes_by_simple.values()):
  430. pass
  431. for names in self.classes_by_simple.values():
  432. for decl in names:
  433. rest = decl.rest or ""
  434. m = re.search(r"\bimplements\s+([^{]+)", rest)
  435. if m:
  436. for iface in split_params(m.group(1)):
  437. iface = re.sub(r"<.*?>", "", iface).strip().split(".")[-1]
  438. if iface:
  439. self.implementations[iface].append(decl)
  440. m2 = re.search(r"\bextends\s+([^{]+)", rest)
  441. if m2 and decl.kind == "interface":
  442. for parent in split_params(m2.group(1)):
  443. parent = re.sub(r"<.*?>", "", parent).strip().split(".")[-1]
  444. if parent:
  445. self.implementations[parent].append(decl)
  446. def resolve(self, point):
  447. """返回 (目标 TypeDecl 列表, 解析方式);多实现时保守全连边(宁可多报不漏报)"""
  448. if point.type in COLLECTION_TYPES:
  449. return [], "collection"
  450. # 1) 显式 name:@Resource(name="x") / @Autowired(name="x")
  451. if point.explicit_name and point.explicit_name in self.bean_by_name:
  452. return [self.bean_by_name[point.explicit_name]], "by-name(explicit)"
  453. # 2) @Resource by-name:字段名 = Bean 名(本项目强约束)
  454. if point.annotation == "@Resource" and point.field in self.bean_by_name:
  455. return [self.bean_by_name[point.field]], "by-name"
  456. # 3) by-type:类型简单名直接命中 Bean 类
  457. cands = self.classes_by_simple.get(point.type, [])
  458. if len(cands) == 1:
  459. return cands, "by-type"
  460. if len(cands) > 1:
  461. return [], "ambiguous-type"
  462. # 4) 接口 → 实现类
  463. impls = self.implementations.get(point.type, [])
  464. if len(impls) == 1:
  465. return impls, "by-type(iface->impl)"
  466. if len(impls) > 1:
  467. primary = [d for d in impls if "@Primary" in (d.prefix or "")]
  468. if len(primary) == 1:
  469. return primary, "by-primary"
  470. return impls, "ambiguous-iface"
  471. return [], "unresolved"
  472. # --------------------------------------------------------------------------- #
  473. # 图算法
  474. # --------------------------------------------------------------------------- #
  475. def tarjan_scc(nodes, adj):
  476. """返回 SCC 列表(迭代版 Tarjan)"""
  477. index = {}
  478. low = {}
  479. on_stack = {}
  480. stack = []
  481. result = []
  482. counter = [0]
  483. for root in nodes:
  484. if root in index:
  485. continue
  486. work = [(root, iter(adj.get(root, ())))]
  487. index[root] = low[root] = counter[0]
  488. counter[0] += 1
  489. stack.append(root)
  490. on_stack[root] = True
  491. while work:
  492. v, it = work[-1]
  493. advanced = False
  494. for w in it:
  495. if w not in index:
  496. index[w] = low[w] = counter[0]
  497. counter[0] += 1
  498. stack.append(w)
  499. on_stack[w] = True
  500. work.append((w, iter(adj.get(w, ()))))
  501. advanced = True
  502. break
  503. if on_stack.get(w):
  504. low[v] = min(low[v], index[w])
  505. if advanced:
  506. continue
  507. work.pop()
  508. if work:
  509. u = work[-1][0]
  510. low[u] = min(low[u], low[v])
  511. if low[v] == index[v]:
  512. comp = []
  513. while True:
  514. w = stack.pop()
  515. on_stack[w] = False
  516. comp.append(w)
  517. if w == v:
  518. break
  519. result.append(comp)
  520. return result
  521. def find_cycle_within(nodes, adj):
  522. """在给定节点集合内寻找一条简单环(DFS),返回路径列表"""
  523. node_set = set(nodes)
  524. for start in sorted(nodes):
  525. stack = [(start, [start])]
  526. visited = set()
  527. while stack:
  528. node, path = stack.pop()
  529. for nxt in adj.get(node, ()):
  530. if nxt == start:
  531. return path + [start]
  532. if nxt in node_set and nxt not in visited:
  533. visited.add(nxt)
  534. stack.append((nxt, path + [nxt]))
  535. return None
  536. def enumerate_cycles(nodes, adj, limit=100):
  537. """枚举节点集合内的全部简单环(从环上最小节点起步,保证每环只枚举一次)"""
  538. node_set = set(nodes)
  539. cycles = []
  540. for start in sorted(nodes):
  541. if len(cycles) >= limit:
  542. break
  543. stack = [(start, [start])]
  544. while stack:
  545. if len(cycles) >= limit:
  546. break
  547. node, path = stack.pop()
  548. for nxt in sorted(adj.get(node, ())):
  549. if nxt == start:
  550. cycles.append(path + [start])
  551. if len(cycles) >= limit:
  552. break
  553. elif nxt in node_set and nxt not in path and nxt >= start:
  554. stack.append((nxt, path + [nxt]))
  555. return cycles
  556. def find_path(src, dst, adj):
  557. """BFS 最短路径 src → dst(含端点),不可达返回 None"""
  558. if src == dst:
  559. return [src]
  560. prev = {src: None}
  561. queue = [src]
  562. head = 0
  563. while head < len(queue):
  564. node = queue[head]
  565. head += 1
  566. for nxt in sorted(adj.get(node, ())):
  567. if nxt in prev:
  568. continue
  569. prev[nxt] = node
  570. if nxt == dst:
  571. path = [dst]
  572. cur = node
  573. while cur is not None:
  574. path.append(cur)
  575. cur = prev[cur]
  576. path.reverse()
  577. return path
  578. queue.append(nxt)
  579. return None
  580. # --------------------------------------------------------------------------- #
  581. # 主流程
  582. # --------------------------------------------------------------------------- #
  583. def collect_types():
  584. registry = Registry()
  585. decls = []
  586. for dirpath, _dirnames, filenames in os.walk(SRC_DIR):
  587. for fn in filenames:
  588. if not fn.endswith(".java"):
  589. continue
  590. path = os.path.join(dirpath, fn)
  591. with open(path, "r", encoding="utf-8", errors="replace") as fh:
  592. raw = fh.read()
  593. src = strip_comments(raw)
  594. file_decls = parse_type_decls(src, path)
  595. for d in file_decls:
  596. decls.append((src, d))
  597. # Mapper 接口(MyBatis-Plus 扫描 com.etotem.cfc.mapper)
  598. if d.kind == "interface" and d.package.startswith(MAPPER_PACKAGE_PREFIX):
  599. registry.add(d, decapitalize(d.name))
  600. elif is_stereotype(d.prefix):
  601. explicit = stereotype_name(d.prefix)
  602. registry.add(d, explicit or decapitalize(d.name))
  603. registry.link()
  604. return registry, decls
  605. def build_graph(registry, decls):
  606. edges = [] # (owner, target, point)
  607. skipped = 0 # 实体/POJO/无法解析(非 Bean,忽略)
  608. ambiguous = [] # 接口多实现,按保守全连边(可能多报)
  609. for src, decl in decls:
  610. if decl.name not in registry.classes_by_simple:
  611. continue
  612. if len(registry.classes_by_simple[decl.name]) != 1:
  613. continue
  614. if not (is_stereotype(decl.prefix) or
  615. (decl.kind == "interface" and decl.package.startswith(MAPPER_PACKAGE_PREFIX))):
  616. continue
  617. points = scan_class_body(src, decl)
  618. for p in points:
  619. targets, how = registry.resolve(p)
  620. if not targets:
  621. if how in ("ambiguous-type",):
  622. ambiguous.append((p, how))
  623. else:
  624. skipped += 1
  625. continue
  626. if how == "ambiguous-iface":
  627. ambiguous.append((p, how))
  628. for t in targets:
  629. edges.append((decl, t, p))
  630. return edges, skipped, ambiguous
  631. def main():
  632. if not os.path.isdir(SRC_DIR):
  633. sys.stderr.write("[check_circular_deps] 源码目录不存在: %s\n" % SRC_DIR)
  634. return 2
  635. registry, decls = collect_types()
  636. edges, skipped, ambiguous = build_graph(registry, decls)
  637. # 邻接表
  638. eager_adj = defaultdict(set)
  639. full_adj = defaultdict(set)
  640. edge_info = {}
  641. for owner, target, p in edges:
  642. if owner.name == target.name:
  643. continue
  644. kind = "lazy" if p.lazy else "eager"
  645. eager_adj[owner.name].add(target.name) if kind == "eager" else None
  646. full_adj[owner.name].add(target.name)
  647. edge_info[(owner.name, target.name)] = (kind, p)
  648. class_nodes = set()
  649. for decls_of_same_simple_name in registry.classes_by_simple.values():
  650. for decl in decls_of_same_simple_name:
  651. class_nodes.add(decl.name)
  652. # ---- 1. eager 环(全部简单环) ----
  653. eager_sccs = tarjan_scc(class_nodes, eager_adj)
  654. eager_cycles = []
  655. for comp in eager_sccs:
  656. if len(comp) > 1:
  657. sub = {n: [t for t in eager_adj.get(n, ()) if t in comp] for n in comp}
  658. eager_cycles.extend(enumerate_cycles(comp, sub))
  659. elif comp:
  660. node = comp[0]
  661. if node in eager_adj.get(node, ()):
  662. eager_cycles.append([node, node])
  663. # ---- 2. @Lazy 掩盖的设计环:逐条 @Lazy 边判断是否位于某个环上 ----
  664. design_cycles = []
  665. seen_design = set()
  666. for (u, v), (kind, _p) in sorted(edge_info.items()):
  667. if kind != "lazy":
  668. continue
  669. path = find_path(v, u, full_adj)
  670. if not path:
  671. continue # 该 @Lazy 边不在任何环上
  672. cycle = [u] + path
  673. key = frozenset(cycle[:-1])
  674. if key in seen_design:
  675. continue
  676. seen_design.add(key)
  677. design_cycles.append(cycle)
  678. # ---- 3. @Lazy 注入点清单 ----
  679. lazy_points = [p for (_o, _t, p) in edges if p.lazy]
  680. lazy_points.sort(key=lambda p: (p.owner, p.field))
  681. # ---- 输出 ----
  682. out = []
  683. out.append("=" * 72)
  684. out.append("CFC 后端循环依赖静态检查 (tools/check_circular_deps.py)")
  685. out.append("=" * 72)
  686. out.append("源码目录 : %s" % os.path.relpath(SRC_DIR, ROOT))
  687. out.append("Java 文件数 : %d" % sum(1 for _d, _f, files in os.walk(SRC_DIR) for f in files if f.endswith(".java")))
  688. out.append("识别 Bean 数 : %d" % len(registry.bean_by_name))
  689. out.append("注入点总数 : %d (eager %d / lazy %d)"
  690. % (len(edges), len(edges) - len(lazy_points), len(lazy_points)))
  691. out.append("[1] Eager 循环依赖(启动期致命,必须为空)")
  692. if eager_cycles:
  693. for i, cyc in enumerate(eager_cycles, 1):
  694. out.append(" ✗ 环 %d: %s" % (i, " → ".join(cyc)))
  695. for a, b in zip(cyc, cyc[1:]):
  696. info = edge_info.get((a, b))
  697. if info:
  698. out.append(" %s --%s--> %s" % (a, info[0], b))
  699. else:
  700. out.append(" ✅ 未发现 eager 循环依赖")
  701. out.append("")
  702. out.append("[2] @Lazy 掩盖的设计环台账(既有设计债,仅登记不阻断)")
  703. if design_cycles:
  704. for i, cyc in enumerate(design_cycles, 1):
  705. lazy_edges = []
  706. for a, b in zip(cyc, cyc[1:]):
  707. info = edge_info.get((a, b))
  708. if info and info[0] == "lazy":
  709. lazy_edges.append("%s → %s" % (a, b))
  710. out.append(" • 环 %d: %s" % (i, " → ".join(cyc)))
  711. if lazy_edges:
  712. out.append(" @Lazy 断点: %s" % ", ".join(lazy_edges))
  713. else:
  714. out.append(" (无)")
  715. out.append("")
  716. out.append("[3] @Lazy 注入点清单(字段名须与 Bean 名严格一致)")
  717. if lazy_points:
  718. for p in lazy_points:
  719. out.append(" - %s.%s (%s %s) -> %s [%s:%d]"
  720. % (p.owner, p.field, p.annotation,
  721. "@Lazy" if p.lazy else "", p.type,
  722. os.path.relpath(p.file, ROOT), p.line))
  723. else:
  724. out.append(" (无)")
  725. out.append("")
  726. if ambiguous:
  727. out.append("[4] 接口多实现(按保守全连边,可能产生额外环提示):%d 条" % len(ambiguous))
  728. seen = {}
  729. for p, how in ambiguous:
  730. key = (p.owner, p.field, p.type)
  731. seen.setdefault(key, p)
  732. for p in list(seen.values())[:15]:
  733. out.append(" - %s.%s -> %s (ambiguous-iface)" % (p.owner, p.field, p.type))
  734. else:
  735. out.append("[4] 所有注入点均唯一解析到 Bean")
  736. if skipped:
  737. out.append(" 另有 %d 个注入点未解析(实体/POJO/容器类型,正常忽略)" % skipped)
  738. out.append("")
  739. if eager_cycles:
  740. out.append("结论: ❌ 发现 %d 条 eager 循环依赖 —— 必须从设计上打破"
  741. "(抽窄接口 / 下沉 / 事件解耦),禁止用 @Lazy 掩盖新环。" % len(eager_cycles))
  742. out.append("=" * 72)
  743. print("\n".join(out))
  744. return 1
  745. out.append("结论: ✅ eager 环 = 0(通过)")
  746. out.append("=" * 72)
  747. print("\n".join(out))
  748. return 0
  749. if __name__ == "__main__":
  750. sys.exit(main())