|
|
@@ -0,0 +1,843 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+"""
|
|
|
+tools/check_circular_deps.py — CFC 后端 Spring 循环依赖静态检查
|
|
|
+
|
|
|
+用途
|
|
|
+----
|
|
|
+在提交前静态解析 cfc-backend/src/main/java 的全部 Java 源码,构建「Bean 依赖图」,
|
|
|
+区分 **eager 边**(启动期立即实例化)与 **@Lazy 边**(延迟代理,启动期不实例化),
|
|
|
+输出:
|
|
|
+
|
|
|
+ 1. eager 循环依赖清单 —— 启动期致命,必须为空;非空时退出码 1(阻断提交)
|
|
|
+ 2. @Lazy 掩盖的设计环台账 —— 允许存在(既有 30+ 条),仅登记不阻断
|
|
|
+ 3. @Lazy 注入点清单 —— 供人工复核「@Lazy 仅在字段名与 Bean 名严格一致时使用」
|
|
|
+
|
|
|
+解析能力
|
|
|
+--------
|
|
|
+ * 字段注入:@Resource / @Autowired(含 @Lazy、@Qualifier、@Resource(name="..."))
|
|
|
+ * setter 注入:方法上的 @Autowired / @Resource
|
|
|
+ * 构造器注入:构造器上的 @Autowired / @Resource
|
|
|
+ * @Resource 按 by-name 解析(by-name 失败回退 by-type)
|
|
|
+ * @Autowired 按 by-type 解析;接口类型回退到「唯一实现类」
|
|
|
+ * Bean 识别:@Service / @Component / @Repository / @Controller / @RestController /
|
|
|
+ @Configuration / com.etotem.cfc.mapper 下的 Mapper 接口
|
|
|
+
|
|
|
+退出码
|
|
|
+------
|
|
|
+ 0 —— 未发现 eager 循环依赖
|
|
|
+ 1 —— 发现 eager 循环依赖(必须从设计上打破,禁止用 @Lazy 掩盖新环)
|
|
|
+ 2 —— 脚本自身错误(源码目录不存在等)
|
|
|
+
|
|
|
+用法
|
|
|
+----
|
|
|
+ python3 tools/check_circular_deps.py
|
|
|
+"""
|
|
|
+
|
|
|
+import os
|
|
|
+import re
|
|
|
+import sys
|
|
|
+from collections import defaultdict
|
|
|
+
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+# 路径与常量
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+
|
|
|
+ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
|
|
|
+SRC_DIR = os.path.join(ROOT, "cfc-backend", "src", "main", "java")
|
|
|
+
|
|
|
+STEREOTYPE_SIMPLE = {"Service", "Component", "Repository", "Controller",
|
|
|
+ "RestController", "Configuration"}
|
|
|
+MAPPER_PACKAGE_PREFIX = "com.etotem.cfc.mapper"
|
|
|
+
|
|
|
+INJECT_ANNOTATIONS = ("@Resource", "@Autowired")
|
|
|
+LAZY_ANNOTATION = "@Lazy"
|
|
|
+
|
|
|
+# 集合/容器注入:Spring 注入的是「多个候选」,无法归到单一 Bean 节点,解析时跳过
|
|
|
+COLLECTION_TYPES = {
|
|
|
+ "List", "Set", "Map", "Collection", "Iterable", "ArrayList", "LinkedList",
|
|
|
+ "HashSet", "TreeSet", "HashMap", "LinkedHashMap", "TreeMap", "Optional",
|
|
|
+ "ObjectProvider", "Multimap",
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+# 源码预处理
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+
|
|
|
+def strip_comments(src):
|
|
|
+ """把注释替换为等长空格(保留换行与偏移),避免注释中的注解造成误判。"""
|
|
|
+ out = list(src)
|
|
|
+ i, n = 0, len(src)
|
|
|
+ state = None # None / 'line' / 'block' / 'str' / 'char'
|
|
|
+ while i < n:
|
|
|
+ c = src[i]
|
|
|
+ nxt = src[i + 1] if i + 1 < n else ""
|
|
|
+ if state is None:
|
|
|
+ if c == "/" and nxt == "/":
|
|
|
+ state = "line"
|
|
|
+ out[i] = out[i + 1] = " "
|
|
|
+ i += 2
|
|
|
+ continue
|
|
|
+ if c == "/" and nxt == "*":
|
|
|
+ state = "block"
|
|
|
+ out[i] = out[i + 1] = " "
|
|
|
+ i += 2
|
|
|
+ continue
|
|
|
+ if c == '"':
|
|
|
+ state = "str"
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ if c == "'":
|
|
|
+ state = "char"
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ if state == "line":
|
|
|
+ if c == "\n":
|
|
|
+ state = None
|
|
|
+ else:
|
|
|
+ out[i] = " "
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ if state == "block":
|
|
|
+ if c == "*" and nxt == "/":
|
|
|
+ out[i] = out[i + 1] = " "
|
|
|
+ state = None
|
|
|
+ i += 2
|
|
|
+ continue
|
|
|
+ if c != "\n":
|
|
|
+ out[i] = " "
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ if state in ("str", "char"):
|
|
|
+ quote = '"' if state == "str" else "'"
|
|
|
+ if c == "\\":
|
|
|
+ i += 2
|
|
|
+ continue
|
|
|
+ if c == quote:
|
|
|
+ state = None
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ return "".join(out)
|
|
|
+
|
|
|
+
|
|
|
+def decapitalize(name):
|
|
|
+ """Spring 默认 Bean 名:首字母小写(前两字母均大写则保持原样)。"""
|
|
|
+ if not name:
|
|
|
+ return name
|
|
|
+ if len(name) > 1 and name[0].isupper() and name[1].isupper():
|
|
|
+ return name
|
|
|
+ return name[0].lower() + name[1:]
|
|
|
+
|
|
|
+
|
|
|
+def strip_generics(type_text):
|
|
|
+ """List<Foo> -> List;Map<String, Foo> -> Map"""
|
|
|
+ depth = 0
|
|
|
+ out = []
|
|
|
+ for ch in type_text:
|
|
|
+ if ch == "<":
|
|
|
+ depth += 1
|
|
|
+ continue
|
|
|
+ if ch == ">":
|
|
|
+ depth = max(0, depth - 1)
|
|
|
+ continue
|
|
|
+ if depth == 0:
|
|
|
+ out.append(ch)
|
|
|
+ return "".join(out).strip()
|
|
|
+
|
|
|
+
|
|
|
+def simple_type(type_text):
|
|
|
+ """取类型简单名(去包名、泛型、数组)"""
|
|
|
+ t = strip_generics(type_text).strip()
|
|
|
+ t = t.split(".")[-1].strip()
|
|
|
+ while t.endswith("[]"):
|
|
|
+ t = t[:-2].strip()
|
|
|
+ t = t.replace("...", "").strip()
|
|
|
+ return t
|
|
|
+
|
|
|
+
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+# 类型声明解析
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+
|
|
|
+TYPE_DECL_RE = re.compile(
|
|
|
+ r"(?P<prefix>[^{};]*?)\b(?P<kind>class|interface|enum)\s+(?P<name>\w+)"
|
|
|
+ r"(?P<rest>[^{};]*?)\{"
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+class TypeDecl(object):
|
|
|
+ __slots__ = ("name", "kind", "package", "file", "prefix", "rest", "body_start", "body_end")
|
|
|
+
|
|
|
+ def __init__(self, name, kind, package, file, prefix, rest, body_start, body_end):
|
|
|
+ self.name = name
|
|
|
+ self.kind = kind
|
|
|
+ self.package = package
|
|
|
+ self.file = file
|
|
|
+ self.prefix = prefix
|
|
|
+ self.rest = rest
|
|
|
+ self.body_start = body_start
|
|
|
+ self.body_end = body_end
|
|
|
+
|
|
|
+
|
|
|
+def parse_package(src):
|
|
|
+ m = re.search(r"^\s*package\s+([\w.]+)\s*;", src, re.M)
|
|
|
+ return m.group(1) if m else ""
|
|
|
+
|
|
|
+
|
|
|
+def parse_type_decls(src, file_path):
|
|
|
+ decls = []
|
|
|
+ package = parse_package(src)
|
|
|
+ for m in TYPE_DECL_RE.finditer(src):
|
|
|
+ prefix = m.group("prefix")
|
|
|
+ # 仅认列 0 开始的顶层声明(缩进声明视为方法体内的局部类/匿名类,跳过)
|
|
|
+ last_line = prefix.split("\n")[-1] if prefix else ""
|
|
|
+ if last_line[:1] in (" ", "\t"):
|
|
|
+ continue
|
|
|
+ body_start = m.end()
|
|
|
+ depth = 1
|
|
|
+ i = body_start
|
|
|
+ while i < len(src) and depth > 0:
|
|
|
+ if src[i] == "{":
|
|
|
+ depth += 1
|
|
|
+ elif src[i] == "}":
|
|
|
+ depth -= 1
|
|
|
+ i += 1
|
|
|
+ decls.append(TypeDecl(
|
|
|
+ name=m.group("name"),
|
|
|
+ kind=m.group("kind"),
|
|
|
+ package=package,
|
|
|
+ file=file_path,
|
|
|
+ prefix=prefix,
|
|
|
+ rest=m.group("rest"),
|
|
|
+ body_start=body_start,
|
|
|
+ body_end=i - 1,
|
|
|
+ ))
|
|
|
+ return decls
|
|
|
+
|
|
|
+
|
|
|
+def prefix_annotation_names(prefix):
|
|
|
+ """提取声明前注解块中的注解简单名(兼容全限定写法)"""
|
|
|
+ return [m.split(".")[-1] for m in re.findall(r"@\s*([\w.]+)", prefix)]
|
|
|
+
|
|
|
+
|
|
|
+def is_stereotype(prefix):
|
|
|
+ """类声明前的注解块里是否含 Spring stereotype 注解"""
|
|
|
+ return bool(STEREOTYPE_SIMPLE & set(prefix_annotation_names(prefix)))
|
|
|
+
|
|
|
+
|
|
|
+def stereotype_name(prefix):
|
|
|
+ """解析 @Service("xxx") / @Component("xxx") 显式 Bean 名"""
|
|
|
+ for m in re.finditer(r"@\s*([\w.]+)(?:\s*\(\s*\"([^\"]+)\"\s*\))", prefix):
|
|
|
+ if m.group(1).split(".")[-1] in STEREOTYPE_SIMPLE:
|
|
|
+ return m.group(2)
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+# 注入点解析
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+
|
|
|
+class InjectionPoint(object):
|
|
|
+ __slots__ = ("owner", "kind", "field", "type", "annotation", "lazy", "file", "line", "explicit_name")
|
|
|
+
|
|
|
+ def __init__(self, owner, kind, field, type_, annotation, lazy, file, line, explicit_name=None):
|
|
|
+ self.owner = owner
|
|
|
+ self.kind = kind # field / setter / constructor
|
|
|
+ self.field = field
|
|
|
+ self.type = type_
|
|
|
+ self.annotation = annotation # @Resource / @Autowired
|
|
|
+ self.lazy = lazy
|
|
|
+ self.file = file
|
|
|
+ self.line = line
|
|
|
+ self.explicit_name = explicit_name
|
|
|
+
|
|
|
+ def describe(self):
|
|
|
+ loc = "%s:%d" % (os.path.relpath(self.file, ROOT), self.line)
|
|
|
+ return "%s.%s (%s %s%s) -> %s [%s]" % (
|
|
|
+ self.owner, self.field, self.annotation,
|
|
|
+ "@Lazy " if self.lazy else "",
|
|
|
+ self.type, loc, self.kind)
|
|
|
+
|
|
|
+
|
|
|
+ANNOTATION_RE = re.compile(
|
|
|
+ r"\s*@\s*(?P<name>[\w.]+)(?:\s*\((?P<args>(?:[^()]|\([^()]*\))*)\))?")
|
|
|
+
|
|
|
+
|
|
|
+def skip_annotations(src, pos):
|
|
|
+ """跳过紧邻的注解块,返回 (新位置, 注解列表)"""
|
|
|
+ anns = []
|
|
|
+ while True:
|
|
|
+ m = ANNOTATION_RE.match(src, pos)
|
|
|
+ if not m:
|
|
|
+ break
|
|
|
+ anns.append((m.group("name"), m.group("args") or ""))
|
|
|
+ pos = m.end()
|
|
|
+ return pos, anns
|
|
|
+
|
|
|
+
|
|
|
+def find_next_token(src, pos, tokens=(";", "{", "=")):
|
|
|
+ """找到下一个顶层分隔符(跳过字符串/字符字面量与括号)"""
|
|
|
+ depth = 0
|
|
|
+ i = pos
|
|
|
+ n = len(src)
|
|
|
+ while i < n:
|
|
|
+ c = src[i]
|
|
|
+ if c in "\"'":
|
|
|
+ quote = c
|
|
|
+ i += 1
|
|
|
+ while i < n:
|
|
|
+ if src[i] == "\\":
|
|
|
+ i += 2
|
|
|
+ continue
|
|
|
+ if src[i] == quote:
|
|
|
+ break
|
|
|
+ i += 1
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ if c in "([{":
|
|
|
+ depth += 1
|
|
|
+ elif c in ")]}":
|
|
|
+ depth -= 1
|
|
|
+ elif depth == 0 and c in tokens:
|
|
|
+ return c, i
|
|
|
+ i += 1
|
|
|
+ return None, n
|
|
|
+
|
|
|
+
|
|
|
+def parse_field_decl(text):
|
|
|
+ """'private Foo bar' -> ('Foo', 'bar');'private List<Foo> bars' -> ('List', 'bars')"""
|
|
|
+ text = text.strip()
|
|
|
+ if not text:
|
|
|
+ return None, None
|
|
|
+ text = re.sub(r"^(?:public|protected|private|static|final|transient|volatile)\s+", "", text)
|
|
|
+ text = text.strip()
|
|
|
+ if not text:
|
|
|
+ return None, None
|
|
|
+ # 泛型整体作为一个 token
|
|
|
+ m = re.match(r"^(?P<type>[\w.]+(?:\s*<.*?>)?(?:\s*\[\s*\])*)\s+(?P<name>\w+)\s*$", text, re.S)
|
|
|
+ if not m:
|
|
|
+ return None, None
|
|
|
+ return m.group("type"), m.group("name")
|
|
|
+
|
|
|
+
|
|
|
+def split_params(param_text):
|
|
|
+ """按顶层逗号切分参数列表"""
|
|
|
+ parts, buf, depth = [], [], 0
|
|
|
+ for ch in param_text:
|
|
|
+ if ch in "<([":
|
|
|
+ depth += 1
|
|
|
+ elif ch in ">)]":
|
|
|
+ depth -= 1
|
|
|
+ if ch == "," and depth == 0:
|
|
|
+ parts.append("".join(buf))
|
|
|
+ buf = []
|
|
|
+ else:
|
|
|
+ buf.append(ch)
|
|
|
+ if "".join(buf).strip():
|
|
|
+ parts.append("".join(buf))
|
|
|
+ return parts
|
|
|
+
|
|
|
+
|
|
|
+def parse_method_params(text):
|
|
|
+ """解析方法/构造器参数 -> [(type, name), ...]"""
|
|
|
+ idx = text.find("(")
|
|
|
+ if idx < 0:
|
|
|
+ return []
|
|
|
+ start = idx + 1
|
|
|
+ depth = 1
|
|
|
+ i = start
|
|
|
+ while i < len(text) and depth > 0:
|
|
|
+ if text[i] in "([{":
|
|
|
+ depth += 1
|
|
|
+ elif text[i] in ")]}":
|
|
|
+ depth -= 1
|
|
|
+ i += 1
|
|
|
+ inner = text[start:i - 1]
|
|
|
+ result = []
|
|
|
+ for raw in split_params(inner):
|
|
|
+ p = raw.strip()
|
|
|
+ if not p:
|
|
|
+ continue
|
|
|
+ p = re.sub(r"^(?:final|@\w+(\([^)]*\))?)\s+", "", p)
|
|
|
+ p = re.sub(r"^(?:public|protected|private)\s+", "", p).strip()
|
|
|
+ tokens = re.findall(r"[\w.]+(?:\s*<.*?>)?(?:\s*\[\s*\])?|\w+", p)
|
|
|
+ if not tokens:
|
|
|
+ continue
|
|
|
+ if len(tokens) == 1:
|
|
|
+ result.append((tokens[0], tokens[0]))
|
|
|
+ else:
|
|
|
+ result.append((" ".join(tokens[:-1]), tokens[-1]))
|
|
|
+ return result
|
|
|
+
|
|
|
+
|
|
|
+def scan_class_body(src, decl):
|
|
|
+ """扫描类体(顶层成员)中的注入点"""
|
|
|
+ points = []
|
|
|
+ body = src[decl.body_start:decl.body_end]
|
|
|
+ offset = decl.body_start
|
|
|
+ i = 0
|
|
|
+ depth = 0
|
|
|
+ n = len(body)
|
|
|
+ while i < n:
|
|
|
+ c = body[i]
|
|
|
+ if c in "\"'":
|
|
|
+ quote = c
|
|
|
+ i += 1
|
|
|
+ while i < n:
|
|
|
+ if body[i] == "\\":
|
|
|
+ i += 2
|
|
|
+ continue
|
|
|
+ if body[i] == quote:
|
|
|
+ break
|
|
|
+ i += 1
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ if c == "{":
|
|
|
+ depth += 1
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ if c == "}":
|
|
|
+ depth -= 1
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ if depth == 0 and c == "@":
|
|
|
+ # 收集连续的注解块(顺序不限:@Lazy @Autowired / @Autowired @Lazy / @Resource @Lazy)
|
|
|
+ block_start = i
|
|
|
+ pos = i
|
|
|
+ block = []
|
|
|
+ while True:
|
|
|
+ m = ANNOTATION_RE.match(body, pos)
|
|
|
+ if not m:
|
|
|
+ break
|
|
|
+ block.append((m.group("name"), m.group("args") or ""))
|
|
|
+ pos = m.end()
|
|
|
+ if not block:
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ # 注解名归一为简单名(兼容 @org.springframework.context.annotation.Lazy 等全限定写法)
|
|
|
+ simple_names = [a[0].split(".")[-1] for a in block]
|
|
|
+ inject_ann = None
|
|
|
+ if "Resource" in simple_names:
|
|
|
+ inject_ann = "Resource"
|
|
|
+ elif "Autowired" in simple_names:
|
|
|
+ inject_ann = "Autowired"
|
|
|
+ if inject_ann is None:
|
|
|
+ i = max(pos, block_start + 1)
|
|
|
+ continue
|
|
|
+ lazy = "Lazy" in simple_names
|
|
|
+ explicit_name = None
|
|
|
+ for aname, aargs in block:
|
|
|
+ if aname.split(".")[-1] in ("Resource", "Autowired"):
|
|
|
+ mn = re.search(r"name\s*=\s*\"([^\"]+)\"", aargs or "")
|
|
|
+ if mn:
|
|
|
+ explicit_name = mn.group(1)
|
|
|
+ after = pos
|
|
|
+ # 找到声明
|
|
|
+ tok, tok_pos = find_next_token(body, after)
|
|
|
+ if tok is None:
|
|
|
+ i = after
|
|
|
+ continue
|
|
|
+ seg = body[after:tok_pos]
|
|
|
+ if tok in (";", "="):
|
|
|
+ type_text, field = parse_field_decl(seg)
|
|
|
+ if type_text and field:
|
|
|
+ points.append(InjectionPoint(
|
|
|
+ owner=decl.name, kind="field", field=field,
|
|
|
+ type_=simple_type(type_text), annotation="@" + inject_ann,
|
|
|
+ lazy=lazy, file=decl.file,
|
|
|
+ line=src.count("\n", 0, offset + after) + 1,
|
|
|
+ explicit_name=explicit_name))
|
|
|
+ elif tok == "{":
|
|
|
+ params = parse_method_params(seg)
|
|
|
+ is_ctor = re.search(r"\b%s\s*\(" % re.escape(decl.name), seg) is not None
|
|
|
+ for ptype, pname in params:
|
|
|
+ if ptype.startswith("this"):
|
|
|
+ continue
|
|
|
+ points.append(InjectionPoint(
|
|
|
+ owner=decl.name, kind="constructor" if is_ctor else "setter",
|
|
|
+ field=pname, type_=simple_type(ptype), annotation="@" + inject_ann,
|
|
|
+ lazy=lazy, file=decl.file,
|
|
|
+ line=src.count("\n", 0, offset + after) + 1,
|
|
|
+ explicit_name=explicit_name))
|
|
|
+ i = max(after, block_start + 1)
|
|
|
+ continue
|
|
|
+ i += 1
|
|
|
+ return points
|
|
|
+
|
|
|
+
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+# Bean 注册表与依赖解析
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+
|
|
|
+class Registry(object):
|
|
|
+ def __init__(self):
|
|
|
+ self.bean_by_name = {} # bean name -> TypeDecl
|
|
|
+ self.classes_by_simple = defaultdict(list) # simple name -> [TypeDecl]
|
|
|
+ self.interfaces = {} # simple name -> TypeDecl(kind=interface)
|
|
|
+ self.implementations = defaultdict(list) # interface simple name -> [TypeDecl]
|
|
|
+
|
|
|
+ def add(self, decl, bean_name):
|
|
|
+ self.bean_by_name[bean_name] = decl
|
|
|
+ self.classes_by_simple[decl.name].append(decl)
|
|
|
+ if decl.kind == "interface":
|
|
|
+ self.interfaces[decl.name] = decl
|
|
|
+
|
|
|
+ def link(self):
|
|
|
+ for decl in list(self.classes_by_simple.values()):
|
|
|
+ pass
|
|
|
+ for names in self.classes_by_simple.values():
|
|
|
+ for decl in names:
|
|
|
+ rest = decl.rest or ""
|
|
|
+ m = re.search(r"\bimplements\s+([^{]+)", rest)
|
|
|
+ if m:
|
|
|
+ for iface in split_params(m.group(1)):
|
|
|
+ iface = re.sub(r"<.*?>", "", iface).strip().split(".")[-1]
|
|
|
+ if iface:
|
|
|
+ self.implementations[iface].append(decl)
|
|
|
+ m2 = re.search(r"\bextends\s+([^{]+)", rest)
|
|
|
+ if m2 and decl.kind == "interface":
|
|
|
+ for parent in split_params(m2.group(1)):
|
|
|
+ parent = re.sub(r"<.*?>", "", parent).strip().split(".")[-1]
|
|
|
+ if parent:
|
|
|
+ self.implementations[parent].append(decl)
|
|
|
+
|
|
|
+ def resolve(self, point):
|
|
|
+ """返回 (目标 TypeDecl 列表, 解析方式);多实现时保守全连边(宁可多报不漏报)"""
|
|
|
+ if point.type in COLLECTION_TYPES:
|
|
|
+ return [], "collection"
|
|
|
+ # 1) 显式 name:@Resource(name="x") / @Autowired(name="x")
|
|
|
+ if point.explicit_name and point.explicit_name in self.bean_by_name:
|
|
|
+ return [self.bean_by_name[point.explicit_name]], "by-name(explicit)"
|
|
|
+ # 2) @Resource by-name:字段名 = Bean 名(本项目强约束)
|
|
|
+ if point.annotation == "@Resource" and point.field in self.bean_by_name:
|
|
|
+ return [self.bean_by_name[point.field]], "by-name"
|
|
|
+ # 3) by-type:类型简单名直接命中 Bean 类
|
|
|
+ cands = self.classes_by_simple.get(point.type, [])
|
|
|
+ if len(cands) == 1:
|
|
|
+ return cands, "by-type"
|
|
|
+ if len(cands) > 1:
|
|
|
+ return [], "ambiguous-type"
|
|
|
+ # 4) 接口 → 实现类
|
|
|
+ impls = self.implementations.get(point.type, [])
|
|
|
+ if len(impls) == 1:
|
|
|
+ return impls, "by-type(iface->impl)"
|
|
|
+ if len(impls) > 1:
|
|
|
+ primary = [d for d in impls if "@Primary" in (d.prefix or "")]
|
|
|
+ if len(primary) == 1:
|
|
|
+ return primary, "by-primary"
|
|
|
+ return impls, "ambiguous-iface"
|
|
|
+ return [], "unresolved"
|
|
|
+
|
|
|
+
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+# 图算法
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+
|
|
|
+def tarjan_scc(nodes, adj):
|
|
|
+ """返回 SCC 列表(迭代版 Tarjan)"""
|
|
|
+ index = {}
|
|
|
+ low = {}
|
|
|
+ on_stack = {}
|
|
|
+ stack = []
|
|
|
+ result = []
|
|
|
+ counter = [0]
|
|
|
+
|
|
|
+ for root in nodes:
|
|
|
+ if root in index:
|
|
|
+ continue
|
|
|
+ work = [(root, iter(adj.get(root, ())))]
|
|
|
+ index[root] = low[root] = counter[0]
|
|
|
+ counter[0] += 1
|
|
|
+ stack.append(root)
|
|
|
+ on_stack[root] = True
|
|
|
+ while work:
|
|
|
+ v, it = work[-1]
|
|
|
+ advanced = False
|
|
|
+ for w in it:
|
|
|
+ if w not in index:
|
|
|
+ index[w] = low[w] = counter[0]
|
|
|
+ counter[0] += 1
|
|
|
+ stack.append(w)
|
|
|
+ on_stack[w] = True
|
|
|
+ work.append((w, iter(adj.get(w, ()))))
|
|
|
+ advanced = True
|
|
|
+ break
|
|
|
+ if on_stack.get(w):
|
|
|
+ low[v] = min(low[v], index[w])
|
|
|
+ if advanced:
|
|
|
+ continue
|
|
|
+ work.pop()
|
|
|
+ if work:
|
|
|
+ u = work[-1][0]
|
|
|
+ low[u] = min(low[u], low[v])
|
|
|
+ if low[v] == index[v]:
|
|
|
+ comp = []
|
|
|
+ while True:
|
|
|
+ w = stack.pop()
|
|
|
+ on_stack[w] = False
|
|
|
+ comp.append(w)
|
|
|
+ if w == v:
|
|
|
+ break
|
|
|
+ result.append(comp)
|
|
|
+ return result
|
|
|
+
|
|
|
+
|
|
|
+def find_cycle_within(nodes, adj):
|
|
|
+ """在给定节点集合内寻找一条简单环(DFS),返回路径列表"""
|
|
|
+ node_set = set(nodes)
|
|
|
+ for start in sorted(nodes):
|
|
|
+ stack = [(start, [start])]
|
|
|
+ visited = set()
|
|
|
+ while stack:
|
|
|
+ node, path = stack.pop()
|
|
|
+ for nxt in adj.get(node, ()):
|
|
|
+ if nxt == start:
|
|
|
+ return path + [start]
|
|
|
+ if nxt in node_set and nxt not in visited:
|
|
|
+ visited.add(nxt)
|
|
|
+ stack.append((nxt, path + [nxt]))
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def enumerate_cycles(nodes, adj, limit=100):
|
|
|
+ """枚举节点集合内的全部简单环(从环上最小节点起步,保证每环只枚举一次)"""
|
|
|
+ node_set = set(nodes)
|
|
|
+ cycles = []
|
|
|
+ for start in sorted(nodes):
|
|
|
+ if len(cycles) >= limit:
|
|
|
+ break
|
|
|
+ stack = [(start, [start])]
|
|
|
+ while stack:
|
|
|
+ if len(cycles) >= limit:
|
|
|
+ break
|
|
|
+ node, path = stack.pop()
|
|
|
+ for nxt in sorted(adj.get(node, ())):
|
|
|
+ if nxt == start:
|
|
|
+ cycles.append(path + [start])
|
|
|
+ if len(cycles) >= limit:
|
|
|
+ break
|
|
|
+ elif nxt in node_set and nxt not in path and nxt >= start:
|
|
|
+ stack.append((nxt, path + [nxt]))
|
|
|
+ return cycles
|
|
|
+
|
|
|
+
|
|
|
+def find_path(src, dst, adj):
|
|
|
+ """BFS 最短路径 src → dst(含端点),不可达返回 None"""
|
|
|
+ if src == dst:
|
|
|
+ return [src]
|
|
|
+ prev = {src: None}
|
|
|
+ queue = [src]
|
|
|
+ head = 0
|
|
|
+ while head < len(queue):
|
|
|
+ node = queue[head]
|
|
|
+ head += 1
|
|
|
+ for nxt in sorted(adj.get(node, ())):
|
|
|
+ if nxt in prev:
|
|
|
+ continue
|
|
|
+ prev[nxt] = node
|
|
|
+ if nxt == dst:
|
|
|
+ path = [dst]
|
|
|
+ cur = node
|
|
|
+ while cur is not None:
|
|
|
+ path.append(cur)
|
|
|
+ cur = prev[cur]
|
|
|
+ path.reverse()
|
|
|
+ return path
|
|
|
+ queue.append(nxt)
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+# 主流程
|
|
|
+# --------------------------------------------------------------------------- #
|
|
|
+
|
|
|
+def collect_types():
|
|
|
+ registry = Registry()
|
|
|
+ decls = []
|
|
|
+ for dirpath, _dirnames, filenames in os.walk(SRC_DIR):
|
|
|
+ for fn in filenames:
|
|
|
+ if not fn.endswith(".java"):
|
|
|
+ continue
|
|
|
+ path = os.path.join(dirpath, fn)
|
|
|
+ with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
|
|
+ raw = fh.read()
|
|
|
+ src = strip_comments(raw)
|
|
|
+ file_decls = parse_type_decls(src, path)
|
|
|
+ for d in file_decls:
|
|
|
+ decls.append((src, d))
|
|
|
+ # Mapper 接口(MyBatis-Plus 扫描 com.etotem.cfc.mapper)
|
|
|
+ if d.kind == "interface" and d.package.startswith(MAPPER_PACKAGE_PREFIX):
|
|
|
+ registry.add(d, decapitalize(d.name))
|
|
|
+ elif is_stereotype(d.prefix):
|
|
|
+ explicit = stereotype_name(d.prefix)
|
|
|
+ registry.add(d, explicit or decapitalize(d.name))
|
|
|
+ registry.link()
|
|
|
+ return registry, decls
|
|
|
+
|
|
|
+
|
|
|
+def build_graph(registry, decls):
|
|
|
+ edges = [] # (owner, target, point)
|
|
|
+ skipped = 0 # 实体/POJO/无法解析(非 Bean,忽略)
|
|
|
+ ambiguous = [] # 接口多实现,按保守全连边(可能多报)
|
|
|
+ for src, decl in decls:
|
|
|
+ if decl.name not in registry.classes_by_simple:
|
|
|
+ continue
|
|
|
+ if len(registry.classes_by_simple[decl.name]) != 1:
|
|
|
+ continue
|
|
|
+ if not (is_stereotype(decl.prefix) or
|
|
|
+ (decl.kind == "interface" and decl.package.startswith(MAPPER_PACKAGE_PREFIX))):
|
|
|
+ continue
|
|
|
+ points = scan_class_body(src, decl)
|
|
|
+ for p in points:
|
|
|
+ targets, how = registry.resolve(p)
|
|
|
+ if not targets:
|
|
|
+ if how in ("ambiguous-type",):
|
|
|
+ ambiguous.append((p, how))
|
|
|
+ else:
|
|
|
+ skipped += 1
|
|
|
+ continue
|
|
|
+ if how == "ambiguous-iface":
|
|
|
+ ambiguous.append((p, how))
|
|
|
+ for t in targets:
|
|
|
+ edges.append((decl, t, p))
|
|
|
+ return edges, skipped, ambiguous
|
|
|
+
|
|
|
+
|
|
|
+def main():
|
|
|
+ if not os.path.isdir(SRC_DIR):
|
|
|
+ sys.stderr.write("[check_circular_deps] 源码目录不存在: %s\n" % SRC_DIR)
|
|
|
+ return 2
|
|
|
+
|
|
|
+ registry, decls = collect_types()
|
|
|
+ edges, skipped, ambiguous = build_graph(registry, decls)
|
|
|
+
|
|
|
+ # 邻接表
|
|
|
+ eager_adj = defaultdict(set)
|
|
|
+ full_adj = defaultdict(set)
|
|
|
+ edge_info = {}
|
|
|
+ for owner, target, p in edges:
|
|
|
+ if owner.name == target.name:
|
|
|
+ continue
|
|
|
+ kind = "lazy" if p.lazy else "eager"
|
|
|
+ eager_adj[owner.name].add(target.name) if kind == "eager" else None
|
|
|
+ full_adj[owner.name].add(target.name)
|
|
|
+ edge_info[(owner.name, target.name)] = (kind, p)
|
|
|
+
|
|
|
+ class_nodes = set()
|
|
|
+ for decls_of_same_simple_name in registry.classes_by_simple.values():
|
|
|
+ for decl in decls_of_same_simple_name:
|
|
|
+ class_nodes.add(decl.name)
|
|
|
+
|
|
|
+ # ---- 1. eager 环(全部简单环) ----
|
|
|
+ eager_sccs = tarjan_scc(class_nodes, eager_adj)
|
|
|
+ eager_cycles = []
|
|
|
+ for comp in eager_sccs:
|
|
|
+ if len(comp) > 1:
|
|
|
+ sub = {n: [t for t in eager_adj.get(n, ()) if t in comp] for n in comp}
|
|
|
+ eager_cycles.extend(enumerate_cycles(comp, sub))
|
|
|
+ elif comp:
|
|
|
+ node = comp[0]
|
|
|
+ if node in eager_adj.get(node, ()):
|
|
|
+ eager_cycles.append([node, node])
|
|
|
+
|
|
|
+ # ---- 2. @Lazy 掩盖的设计环:逐条 @Lazy 边判断是否位于某个环上 ----
|
|
|
+ design_cycles = []
|
|
|
+ seen_design = set()
|
|
|
+ for (u, v), (kind, _p) in sorted(edge_info.items()):
|
|
|
+ if kind != "lazy":
|
|
|
+ continue
|
|
|
+ path = find_path(v, u, full_adj)
|
|
|
+ if not path:
|
|
|
+ continue # 该 @Lazy 边不在任何环上
|
|
|
+ cycle = [u] + path
|
|
|
+ key = frozenset(cycle[:-1])
|
|
|
+ if key in seen_design:
|
|
|
+ continue
|
|
|
+ seen_design.add(key)
|
|
|
+ design_cycles.append(cycle)
|
|
|
+
|
|
|
+ # ---- 3. @Lazy 注入点清单 ----
|
|
|
+ lazy_points = [p for (_o, _t, p) in edges if p.lazy]
|
|
|
+ lazy_points.sort(key=lambda p: (p.owner, p.field))
|
|
|
+
|
|
|
+ # ---- 输出 ----
|
|
|
+ out = []
|
|
|
+ out.append("=" * 72)
|
|
|
+ out.append("CFC 后端循环依赖静态检查 (tools/check_circular_deps.py)")
|
|
|
+ out.append("=" * 72)
|
|
|
+ out.append("源码目录 : %s" % os.path.relpath(SRC_DIR, ROOT))
|
|
|
+ out.append("Java 文件数 : %d" % sum(1 for _d, _f, files in os.walk(SRC_DIR) for f in files if f.endswith(".java")))
|
|
|
+ out.append("识别 Bean 数 : %d" % len(registry.bean_by_name))
|
|
|
+ out.append("注入点总数 : %d (eager %d / lazy %d)"
|
|
|
+ % (len(edges), len(edges) - len(lazy_points), len(lazy_points)))
|
|
|
+
|
|
|
+ out.append("[1] Eager 循环依赖(启动期致命,必须为空)")
|
|
|
+ if eager_cycles:
|
|
|
+ for i, cyc in enumerate(eager_cycles, 1):
|
|
|
+ out.append(" ✗ 环 %d: %s" % (i, " → ".join(cyc)))
|
|
|
+ for a, b in zip(cyc, cyc[1:]):
|
|
|
+ info = edge_info.get((a, b))
|
|
|
+ if info:
|
|
|
+ out.append(" %s --%s--> %s" % (a, info[0], b))
|
|
|
+ else:
|
|
|
+ out.append(" ✅ 未发现 eager 循环依赖")
|
|
|
+ out.append("")
|
|
|
+
|
|
|
+ out.append("[2] @Lazy 掩盖的设计环台账(既有设计债,仅登记不阻断)")
|
|
|
+ if design_cycles:
|
|
|
+ for i, cyc in enumerate(design_cycles, 1):
|
|
|
+ lazy_edges = []
|
|
|
+ for a, b in zip(cyc, cyc[1:]):
|
|
|
+ info = edge_info.get((a, b))
|
|
|
+ if info and info[0] == "lazy":
|
|
|
+ lazy_edges.append("%s → %s" % (a, b))
|
|
|
+ out.append(" • 环 %d: %s" % (i, " → ".join(cyc)))
|
|
|
+ if lazy_edges:
|
|
|
+ out.append(" @Lazy 断点: %s" % ", ".join(lazy_edges))
|
|
|
+ else:
|
|
|
+ out.append(" (无)")
|
|
|
+ out.append("")
|
|
|
+
|
|
|
+ out.append("[3] @Lazy 注入点清单(字段名须与 Bean 名严格一致)")
|
|
|
+ if lazy_points:
|
|
|
+ for p in lazy_points:
|
|
|
+ out.append(" - %s.%s (%s %s) -> %s [%s:%d]"
|
|
|
+ % (p.owner, p.field, p.annotation,
|
|
|
+ "@Lazy" if p.lazy else "", p.type,
|
|
|
+ os.path.relpath(p.file, ROOT), p.line))
|
|
|
+ else:
|
|
|
+ out.append(" (无)")
|
|
|
+ out.append("")
|
|
|
+
|
|
|
+ if ambiguous:
|
|
|
+ out.append("[4] 接口多实现(按保守全连边,可能产生额外环提示):%d 条" % len(ambiguous))
|
|
|
+ seen = {}
|
|
|
+ for p, how in ambiguous:
|
|
|
+ key = (p.owner, p.field, p.type)
|
|
|
+ seen.setdefault(key, p)
|
|
|
+ for p in list(seen.values())[:15]:
|
|
|
+ out.append(" - %s.%s -> %s (ambiguous-iface)" % (p.owner, p.field, p.type))
|
|
|
+ else:
|
|
|
+ out.append("[4] 所有注入点均唯一解析到 Bean")
|
|
|
+ if skipped:
|
|
|
+ out.append(" 另有 %d 个注入点未解析(实体/POJO/容器类型,正常忽略)" % skipped)
|
|
|
+ out.append("")
|
|
|
+
|
|
|
+ if eager_cycles:
|
|
|
+ out.append("结论: ❌ 发现 %d 条 eager 循环依赖 —— 必须从设计上打破"
|
|
|
+ "(抽窄接口 / 下沉 / 事件解耦),禁止用 @Lazy 掩盖新环。" % len(eager_cycles))
|
|
|
+ out.append("=" * 72)
|
|
|
+ print("\n".join(out))
|
|
|
+ return 1
|
|
|
+
|
|
|
+ out.append("结论: ✅ eager 环 = 0(通过)")
|
|
|
+ out.append("=" * 72)
|
|
|
+ print("\n".join(out))
|
|
|
+ return 0
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ sys.exit(main())
|