Muhammadumar1671/django-architect

GitHub: Muhammadumar1671/django-architect

针对大型 Django 代码库的静态架构分析工具,能解析 Django 特有的字符串引用并提供可信的架构评分与 CI 集成。

Stars: 1 | Forks: 0

# Django Architect 针对大型 Django 代码库的静态架构分析。 假设你刚刚加入一家拥有 50 万行 Django 项目代码的公司。只需运行一条 命令即可了解它: ``` pip install django-architect django-architect analyze . ``` ``` ╭──────────────────────────────────────────────────────────────────────────╮ │ Architecture Score 74/100 (C) │ │ │ │ cycle freedom ████████████········ 62 ×0.25 3 app-level │ │ layer conformance ██████████████······ 71 ×0.25 18 errors │ │ coupling health ████████████████···· 82 ×0.20 p95 fan-out 19 │ │ complexity health █████████████████··· 86 ×0.15 7% of SLOC MI<50 │ │ size health ██████████████████·· 90 ×0.10 4 god classes │ │ dead code ███████████████████· 95 ×0.05 6 of 421 modules │ ╰──────────────────────────────────────────────────────────────────────────╯ apps 23 modules 421 symbols 3,908 SLOC 187,442 edges 9,104 max depth 11 references resolved 96.4% parse errors 0 ``` 然后探索它: ``` django-architect serve ``` ## 它的独特之处 **它能读取 Django 隐藏的连接关系。** Django 项目的大部分架构 并不是通过 import 表达的。`ForeignKey("orders.Order")` 是一个字符串。 `path("", views.index)` 是一个字符串。`@receiver(post_save, sender=Order)` 是一个 装饰器参数。`task.delay()` 从不 import worker。import-graph 工具几乎无法展示这些内容。Django Architect 能够解析所有这些,而这 正是一个新人无法通过阅读代码发现的。 **它会告诉你数据的可信度。** 每一条边都带有置信度, 并且标题数字会与*解析率*配对显示:即 解析为具体符号的引用比例。92 分但解析率只有 55%,远 不如 82 分但解析率有 96%,报告会如实说明,而不是掩盖这一点。 **它绝不运行你的代码。** 仅使用 `ast.parse()` —— 没有 `exec`,没有 `import`,没有 Django bootstrap。配置通过静态解释 Python 的安全子集来读取; 任何无法解析的内容都会变成明确的未知状态,而不是进行猜测。 **它适用于已经存在 4,000 个问题的代码库。** Baselines 记录 当前的现实情况,以便 CI 仅针对*新增*问题进行拦截。Suppressions 需要 说明理由并支持过期日期 —— 过期的 suppression 会导致构建失败。 ## 命令 | | | |---|---| | `analyze .` | 完整 pipeline;打印摘要 | | `serve` | 构建并打开交互式报告 | | `report --top 20` | 耦合、循环、最大的服务/模型、热点、死代码 | | `violations` | 检查架构规则(当规则校验失败时退出码为 1) | | `metrics --node OrderService` | 单个节点:用途、带有百分位的指标、依赖项、被依赖项 | | `graph --view model --format mermaid` | 导出图表 | | `export --format sqlite` | 生成可查询的产出物 | | `init` | 根据你现有的层级起草 `architecture.yml` | | `diff base.json head.json` | 比较两次提交 | 退出码:`0` 正常,`1` 出现达到或超过 `--fail-on` 设定值的违规,`2` 分析器 本身运行失败。CI 需要区分后两者。 ## 架构规则 `django-architect init` 会根据你项目现有的内容生成一份初始配置。 在此基础上对其进行收紧: ``` version: 1 layers: api: { roles: [view_cbv, view_func, viewset] } serializers: { roles: [serializer, form] } services: { match: ["*/services.py", "*/services/*.py"], roles: [service] } repositories: { match: ["*/repositories.py", "*/selectors.py"] } models: { roles: [model, manager, queryset] } rules: - id: layered-architecture type: layer-order order: [api, serializers, services, repositories, models] allow_skip: false # api -> models is a violation too severity: error - id: no-orm-in-views type: forbid-edge from: { layers: [api] } to: { roles: [model, manager, queryset] } edge_attr: orm severity: error message: "views must not query the ORM directly -- go through a service" - id: no-app-cycles type: acyclic scope: app severity: error - id: service-fan-out type: threshold selector: { roles: [service] } metric: fan_out max: 15 severity: warning ignore: - paths: ["legacy/**"] rules: ["layered-architecture"] reason: "Tracked in ARCH-142" expires: "2026-12-31" # an expired suppression fails the build ``` 每条违规都会指出导致问题的确切 `file:line`,并在可能的情况下告诉你如何 修复它: ``` apps/orders/views.py:7 [no-orm-in-views] order_list -> Order: views must not query the ORM directly apps/billing/services.py:1 [no-app-cycles] billing -> orders: app dependency cycle fix: break by removing: apps.billing.services -> apps.orders.tasks ``` 循环依赖总是伴随着一个最小反馈边集。“删除这两个 import” 是可操作的;而“这十四个模块相互纠缠”则毫无指导意义。 ### 在现有项目中采用 ``` django-architect init django-architect violations --update-baseline # records today's reality git add architecture.yml .django-architect/baseline.json ``` CI 现在只会因为在此之后新增的违规而失败。 ## 持续集成 ``` - uses: Muhammadumar1671/django-architect@v1 with: baseline: .django-architect/baseline.json fail-on: error comment: true # sticky PR comment, edited in place upload-report: true ``` 该 action 会分析基础 ref,执行 diff,并进行评论: ``` ## Django Architect **Architecture regression** | | Before | After | Change | |---|---:|---:|---:| | Architecture score | 82 | 79 | -3.0 | | Resolution rate | 96.2% | 96.1% | | ### 新增违规 (1) - **error** `no-orm-in-views` — `billing/views.py:44` views must not query the ORM directly ### 新增周期 (1) - `orders -> billing` — break by removing: `orders/services.py:12` ``` ## 检测内容 循环依赖(附带需要切断的边) · 未使用的模块 · 死代码 · 孤立的 service · God class · 过大的模块 · 过多的 import · 紧 耦合 · 高扇入 / 扇出 · 架构热点。 Django 的入口点不受死代码检测的限制。`urls.py`、 migrations、management commands、signal receivers、Celery tasks、admin classes、 middleware,以及 app 注册表自动 import 的每个模块(`models.py`、 `admin.py`、`signals.py` 等)都没有静态的入边,但绝对不是 死代码。弄错这个列表是让未使用代码报告变得 毫无价值的最快方法。 ## 指标 每个符号:圈复杂度和认知复杂度、SLOC、嵌套深度、 参数。 每个模块:Halstead volume、可维护性指数、注释率、imports。 每个模块和 app:扇入和扇出耦合、扇入、扇出、 不稳定性 `I = Ce/(Ca+Ce)`、抽象程度 `A`、距离主序列的距离 `D = |A + I − 1|`、依赖深度。 每项指标都会报告其在你的代码库中的**百分位数**。圈 复杂度为 14 本身毫无意义,直到你发现它在这里处于 94 百分位。 `if TYPE_CHECKING:` 中的 imports 会从运行时耦合中排除 —— 否则 正确进行类型提示的代码得分反而会比无类型代码更差。 ### 架构评分 六个加权组件,全部公开展示,公式公开: | 组件 | 权重 | |---|---:| | 无循环依赖 | 0.25 | | 分层一致性 | 0.25 | | 耦合健康度 | 0.20 | | 复杂度健康度 | 0.15 | | 规模健康度 | 0.10 | | 死代码 | 0.05 | 一个不透明的单一数字要么会被钻空子,要么会被无视。 ## 插件 Django 的支持本身就是一个插件。DRF、Celery 以及 services/repositories 约定也是如此 —— 所有这些都建立在公共 hook API 之上。如果 第一方插件需要访问权限,而 API 却无法提供,那么这个 API 就 有问题。 ``` from django_architect.plugins import BaseClassifier from django_architect.ir import Role class NinjaRouterClassifier(BaseClassifier): name = "ninja.routers" role = Role.VIEWSET base_classes = ("ninja.Router",) ``` ``` [project.entry-points."django_architect"] ninja = "my_package.plugin" ``` Hooks:`da_visit_module`、`da_classify_symbol`、`da_classify_module`、 `da_resolve_reference`、`da_contribute_edges`、`da_register_metrics`、 `da_register_rules`、`da_extend_report`。 崩溃的插件会降低其自身的贡献度并会被报告出来;它永远不会 导致整个运行中断。已加载的插件集会被哈希处理并计入解析缓存键中, 因此安装一个插件绝不会让你看到没有它时生成的旧结果。 详见 [docs/plugins.md](docs/plugins.md)。 ## 工作原理 ``` Discovery → Parse → Resolve → Semantics → Graph → Analysis ``` 在一个规范的中间表示之上进行的六个单向阶段。解析是 最耗时的阶段,也是唯一既进行了并行化又进行了 内容哈希缓存的阶段 —— 完成预热的增量运行只会重新解析 发生更改的部分。 困难的部分不在于解析;而在于 **resolution**。`self.repo.get_order(...)` 毫无意义,除非你知道 `self.repo` 是什么。一个浅层的类型绑定器会 从注解、直接实例化、`__init__` 的 self 赋值以及 类级别的赋值中推断出来 —— 这些结合起来可以恢复大部分 service→repository 和 viewset→serializer 的边。所有它无法解析的内容都会被统计并报告出来, 而不是靠猜。 这里只有一个图。App、package、module、model、service 和 URL 视图 都是它的投影,并且折叠的边保留了指向其背后具体 引用的指针 —— 点击一个 app 到 app 的箭头,你会得到它所代表的 47 个 `file:line` imports。 输出是确定性的:相同的提交,产出字节完全相同的产出物。这 正是让 CI diffing 得以实现的基础,并且这一点由测试来强制保证。 ## 在真实代码库上的实测数据 在 8 核笔记本电脑上进行冷运行,无缓存。在 6,428 个文件中没有出现解析错误。 | 项目 | Modules | SLOC | Edges | 解析率 | 冷运行耗时 | |---|---:|---:|---:|---:|---:| | [django-oscar](https://github.com/django-oscar/django-oscar) | 627 | 51,858 | 25,713 | 83.5% | 5.0s | | [wagtail](https://github.com/wagtail/wagtail) | 977 | 181,684 | 75,073 | 83.7% | 19.4s | | [saleor](https://github.com/saleor/saleor) | 2,855 | 637,636 | 152,505 | 54.7% | 66.5s | | saleor,仅 app 代码 | 1,142 | 169,891 | — | **81.6%** | — | saleor 的峰值内存:1.3 GB。 **为什么 saleor 的标题数字很低,以及为什么这不是缺陷。** 它的测试 套件将 pytest fixtures 作为未注解的参数注入 —— 单单 `staff_api_client` 就出现在了 4,535 个未解析的引用中。任何工具都无法对单纯的 fixture 参数进行类型判定。排除测试(`--exclude-tests`)后,saleor 的应用 代码解析率达到了 81.6%,与其他项目持平。这恰恰正是 解析率这一指标旨在让问题暴露出来而不是将其掩盖的场景。 **已知限制:热运行仅快了约 2 倍,而不是 10 倍。** 设计 假设解析过程占据了主要的执行时间,并且是唯一值得缓存的 阶段。在规模扩大时,这个假设是错误的 —— 在 wagtail 上,一次热运行花费了 4.8s 来重组缓存的 IR,3.0s 构建图,以及 1.0s 计算指标。 Resolution 和图构建每次都会重新运行且没有被缓存。saleor 的热运行耗时约为 55 秒,而冷运行约为 66 秒。解决此问题意味着需要缓存 resolved edges,而不仅是 解析输出。 ## 文档 - [架构设计](docs/specs/2026-07-25-django-architect-design.md) - [编写插件](docs/plugins.md) - [配置参考](docs/configuration.md) ## 状态 Alpha 阶段。引擎、规则、报告、Web UI 和 CI 集成均可正常工作。设计 文档中描述的 AI 层尚未实现。 已在 django-oscar、wagtail 和 saleor 上验证(如上所述):无解析错误, 应用代码的解析率达 81–84%,在 1.3 GB 内存下耗时 66 秒分析了 63.7 万行 SLOC。热运行性能未达到预期目标 —— 请参阅上文提到的限制。 ## 许可证 MIT
标签:Django, Python, 云安全监控, 无后门, 架构分析, 自动化payload嵌入, 调试插件, 逆向工具, 静态分析