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嵌入, 调试插件, 逆向工具, 静态分析