简介

cha logo

察 — 代码健康度分析器

CI License Stars Release

Cha(察,「审视、查看」)是一个可插拔的代码坏味道检测工具集。它通过 tree-sitter 在 AST 层解析源码,运行 34 个内置检测器以及用户提供的 WASM 插件,并以终端输出、JSON、LLM 上下文、SARIF 或 HTML 形式呈现结果。

支持语言:Python(.py)、TypeScript / TSX(.ts.tsx.mts.cts)、Rust(.rs)、Go(.go)、C(.c.h)、C++(.cpp.cc.cxx.hpp.hxx)。

文档结构

  • 安装 —— 把 cha 装到你机器上
  • 快速开始 —— 五分钟跑通几种典型用法
  • 配置 —— .cha.toml、严格度、行内指令
  • Smell 列表 —— 每一条内置检测器、触发条件、调参方法
  • 插件开发 —— 自己写 WASM 插件
  • 命令行 —— 每个子命令的细节
  • LSP 集成 —— 接到你常用的编辑器
  • Cookbook —— 按场景的菜谱

状态

Cha 处于 1.0 之前——核心引擎稳定且自检测,但配置格式和命令行接口仍在演进。每次破坏性变更都会写进 CHANGELOG。

安装

Shell(macOS / Linux)

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/W-Mai/Cha/releases/latest/download/cha-cli-installer.sh | sh

PowerShell(Windows)

powershell -c "irm https://github.com/W-Mai/Cha/releases/latest/download/cha-cli-installer.ps1 | iex"

Homebrew

brew install W-Mai/cellar/cha-cli

从源码

git clone https://github.com/W-Mai/Cha.git
cd Cha
cargo build --release

需要 Rust(edition 2024)。

完整平台清单

每个平台的二进制(含 macOS aarch64 / x86_64、Linux musl + gnu、Windows)和 SHA256 校验和由 cargo-dist 自动同步生成,落在 Install 页 —— 该页是英文,但下载链接和命令都是通用的。

验证

cha --version
cha analyze --help

命令行

5 分钟从 0 到第一个 finding。

1. 装

# macOS / Linux
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/W-Mai/Cha/releases/latest/download/cha-cli-installer.sh | sh

# Windows
powershell -c "irm https://github.com/W-Mai/Cha/releases/latest/download/cha-cli-installer.ps1 | iex"

# Homebrew
brew install W-Mai/cellar/cha-cli

详见 安装

2. 第一次跑

进项目目录:

cd path/to/your-repo
cha analyze

输出大致长这样:

ℹ [data_class] src/types.rs:8-15 Class `User` has 4 fields but no behavior methods, consider Move Method
ℹ [long_method] src/handlers.rs:42 Function `process` is 78 lines (threshold: 50)
⚠ [high_complexity] src/parser.rs:120 Function `parse` has cyclomatic complexity 14 (threshold: 10)
…

47 issue(s) found (0 error, 3 warning, 44 hint).
Tech debt: ~3h 25min | A:12 B:5 C:1 D:0 F:0

每行:严重度图标 + smell 名 + 位置 + 一句话原因。底部有按严重度统计 + 估算技术债。

3. 看详细的

某条 smell 不熟?点 Smell 列表 去翻——34 个内置检测器都有"在抓什么、阈值含义、触发例子"。

或者拿 JSON 给工具消费:

cha analyze --format json | jq '.findings | group_by(.smell_name) | map({smell: .[0].smell_name, count: length})'

4. 调严或调宽

不爽默认阈值?写 .cha.toml

cha init

生成的 .cha.toml 已经带常用插件的默认阈值注释,改数字就行:

[plugins.length]
max_function_lines = 80   # 我们的代码风格函数普遍偏长,把上限抬到 80

[plugins.complexity]
warn_threshold = 15
error_threshold = 25

或者用全局 strictness 缩放:

strictness = "relaxed"  # 所有数值阈值翻倍

详见 配置概览严格度与预设

5. 接老项目

老仓库一上来几百条 finding 没法治?拿一份 baseline,新增的才报:

# 拍快照
cha baseline                                  # 写到 .cha/baseline.json

# 后续只看 baseline 之外的新问题
cha analyze --baseline .cha/baseline.json --fail-on warning

详见 cha baselineBaseline 工作流

接下来

pre-commit hook

提交前自动跑 cha——只检查这次改动的文件,warning 级别以上就拦下提交。

1. 装 pre-commit

pipx install pre-commit
# 或者
brew install pre-commit

2. 在仓库根加 .pre-commit-config.yaml

repos:
  - repo: https://github.com/W-Mai/Cha
    rev: v1.19.0
    hooks:
      - id: cha-analyze

3. 装到 git hooks

pre-commit install

下次 git commit 时会先跑 cha analyze --diff --fail-on warning——只扫这次 staged 的 + 工作区改动的文件,碰到 warning 或 error 就阻止提交。

跑得太吵?

新仓库一接入大量 finding 拦提交,体验很差。三种缓解:

调高 fail 门槛:只拦 error,不管 warning。fork 一份 hook 配置:

hooks:
  - id: cha-analyze
    entry: cha analyze --diff --fail-on error

用 baseline:先 cha baseline 拍快照,hook 只看 baseline 之外:

hooks:
  - id: cha-analyze
    entry: cha analyze --diff --fail-on warning --baseline .cha/baseline.json

完全跳过单次提交

git commit --no-verify

--no-verify 写成习惯——hook 拦不住的事 CI 会拦。

接下来

GitHub Actions

PR 一开就跑 cha,把 finding 自动喷到 Code Scanning 里——你在 PR 文件 diff 旁边能直接看到 cha 标的问题。

最简单的:跑 + 上传 SARIF

.github/workflows/cha.yml

name: Cha
on:
  push:
    branches: [main]
  pull_request:
permissions:
  contents: read
  security-events: write
jobs:
  cha:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: W-Mai/Cha@v1.19.0
        with:
          fail-on: warning
          upload-sarif: true

upload-sarif: true 把结果直接传给 GitHub Code Scanning(要 security-events: write 权限)。之后 PR diff 旁边、Security 标签下都能看到 finding。

Action 的输入

输入默认说明
versionlatest装哪个 cha 版本
formatsarif输出格式:terminal / json / sarif / html
fail-onerror报到这个严重度就让 job 失败:hint / warning / error
plugin只跑指定插件(逗号分隔)
paths.扫哪些路径
upload-sariffalse是否自动上传 SARIF 到 Code Scanning(要 security-events: write

进阶:JSON + 自己处理

不想用 Code Scanning,想自己后处理:

- uses: W-Mai/Cha@v1.19.0
  with:
    format: json
    fail-on: warning
- run: |
    cha analyze --format json > findings.json
    # 用 jq 过滤、丢给内部 dashboard、贴到 PR 评论 …

进阶:只跑改动的文件(PR)

PR 上只检查这次改的,不扫全仓:

- uses: actions/checkout@v4
  with:
    fetch-depth: 0  # 需要历史才能算 diff
- uses: W-Mai/Cha@v1.19.0
  with:
    paths: ''       # 留空让 action 用 --diff
    fail-on: warning

或者直接手动调:

- run: cha analyze --diff --fail-on warning --format sarif --output cha.sarif
- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: cha.sarif

if: always() 让 SARIF 上传步骤即使前一步 --fail-on 失败了也跑——不然 finding 反而上传不了。

接下来

编辑器(LSP)

把 cha 内嵌进编辑器——保存时自动跑、下划线标 finding、悬停看说明。

两条路

VS Code 用户:直接装 Cha 扩展。扩展会自动下载匹配的 cha 二进制,不用手动装 cha。

其他编辑器:手动装 cha安装),然后让编辑器跑 cha lsp 作为 LSP 服务器。

编辑器里你会看到

  • 诊断——保存时跑 cha,finding 在文件里画下划线
  • 悬停——鼠标移到函数上出 markdown 报告卡:行数 / 圈复杂度 / 认知复杂度 / 参数数 / 这个函数的 finding 列表
  • CodeLens——每个函数 / 类上方一行小字:有问题 ⚠ N issue(s),没问题 ✓ <行数>
  • Inlay hint——函数签名末尾一个小标记
  • Code action——快速修复菜单(推荐 refactoring)+ Extract Method
  • 大纲(Document Symbol)—— 有问题的项前面加

下一步

配置

Cha 从项目根目录的 .cha.toml 读配置。生成默认模板:

cha init

配置文件位置

Config::load_for_file 从被分析文件所在目录往上走,一直到项目根,沿途碰到的每个 .cha.toml 都会合并——离文件越近优先级越高,根目录是基础。子包能覆盖只关心的几个键。

大部分项目根目录放一份 .cha.toml 就够。

顶层键

plugins

逐插件配置。所有插件默认开,enabled = false 关掉。[plugins.<名>] 下的其他键作为选项传给插件。

[plugins.length]
enabled = true
max_function_lines = 50
max_class_lines = 200

[plugins.coupling]
max_imports = 15

数值阈值会被 strictness 系数缩放(见下);字符串和 bool 选项原样透传。完整插件键参考见 配置项参考

exclude

要跳过的路径 glob,叠在 .gitignore 之上。

exclude = ["*/tests/fixtures/*", "vendor/*", "**/generated/**"]

debt_weights

按 finding 严重度估算技术债的分钟数。analyze summary 用这个算总技术债。默认 hint = 5warning = 15error = 30

[debt_weights]
hint = 5
warning = 15
error = 30

strictness

数值阈值的整体缩放系数:

  • "relaxed" —— 2.0×(阈值翻倍,更宽松)
  • "default" —— 1.0×
  • "strict" —— 0.5×(阈值减半)
  • 任意浮点,比如 0.7
strictness = "strict"
# 或者
strictness = 0.7

get_usize 把缩放后的结果至少夹到 1,所以 strict 也不会让阈值变成 0。

languages

按语言覆盖——叠在全局插件配置和内置语言 profile 之上。两个子键:plugins(结构跟顶层 plugins 一样)和 disabled_smells(smell 名列表)。

[languages.c.plugins.naming]
enabled = false

[languages.c.plugins.length]
max_function_lines = 80

[languages.python]
disabled_smells = ["naming_too_short"]

内置 profile(目前 c / cpp)先应用,你的覆盖优先。详见 严格度与预设

disabled_smells

全局禁用 smell 列表。一个插件产出多个 smell 但你只想关其中几条时用这个。

disabled_smells = ["naming_too_short", "todo_comment"]

要更精细到单个函数 / 类的禁用,用 行内指令

layers

cha layers 用的模块和 tier 定义。不写就让 cha 自动推断。

[layers.modules]
domain = ["src/domain/**"]
service = ["src/service/**"]
controller = ["src/controller/**"]

[[layers.tiers]]
name = "core"
modules = ["domain"]

[[layers.tiers]]
name = "app"
modules = ["service", "controller"]

相关页

行内指令

在源码里写一行注释——放在函数 / 类前面(或同一行),就能屏蔽这一项的 findings、或者临时给它放宽阈值。

cha:ignore —— 屏蔽 findings

写法效果
// cha:ignore下一项的所有规则都关
// cha:ignore <名>只关一条 smell
// cha:ignore <a>,<b>关多条(逗号分隔)

<名>smell 名(CLI 输出里看到的,比如 long_method / high_complexity / switch_statement),不是插件名length 一个插件就出三种 smell,要写具体的。

cha:set —— 改阈值

写法效果
// cha:set <smell>=<n>把这条 smell 在这一项的阈值临时调到 <n>
// cha:set threshold=<n>把这一项所有"基于阈值"的 smell 阈值都调到 <n>

<n> 是浮点数。如果实际测出来的值还是超过新阈值,finding 还会报。

cha:set 只对actual_value / threshold 数值字段的 smell 有效。布尔型 smell(比如 inappropriate_intimacy)忽略 cha:set——这种用 cha:ignore 关。

注释样式

四种都支持://(Rust / TypeScript / Go / C / C++)、#(Python)、--(Lua / SQL)、/* … */ 块注释:

#![allow(unused)]
fn main() {
// cha:ignore long_method
}
# cha:ignore long_method
/* cha:ignore long_method */

指令必须是整行的开头(去掉空白和注释标记后)。代码尾部的 trailing 注释里写 cha:ignore 不解析。

覆盖范围

指令对一条 finding 生效,要满足下面任一条件:

  1. 指令跟 finding 在同一行,或者
  2. 指令在 finding 起始行的前 2 行内

可以叠多条:

#![allow(unused)]
fn main() {
// cha:ignore long_method
// cha:set high_complexity=25
fn complicated_but_acknowledged() {
    // …
}
}

间隔超过 2 行就不生效。

例子

关一条规则

#![allow(unused)]
fn main() {
// cha:ignore long_method
fn render_template(/* … */) -> String {
    // 200 行模板生成器,知道,故意的
}
}

关多条

// cha:ignore long_method,high_complexity
function migrateLegacyShape(input: unknown) {
  // …
}

给单条 smell 抬阈值

#![allow(unused)]
fn main() {
// cha:set long_method=120
fn parse_protocol_frame(buf: &[u8]) -> Frame {
    // 95 行 —— 超过默认 50 行,但在我们 120 行预算内
}
}

整体抬阈值

# cha:set threshold=200
def state_machine_step(event):
    # 又长又分支多,故意的,长度 / 复杂度都别警告
    ...

抬完之后如果 actual_value < threshold,finding 就被丢掉;超不过的依然会报。

严格度与预设

调默认值有两条路:全局严格度系数(一刀切乘所有阈值)和按语言预设(内置 profile + 你的覆盖)。

严格度

.cha.toml 里写 strictness(或者 CLI 上 --strictness),整体乘所有数值阈值:

系数效果
"relaxed"2.0×阈值翻倍——更宽松
"default"1.0×用插件出厂默认
"strict"0.5×阈值减半——更严
任意浮点 0.7字面量自定义
strictness = "strict"
# 或者
strictness = 0.7

relaxed / default / strict 是命名档;其他值按浮点解析。结果至少夹到 1,strict 也不会让阈值变 0。

只有插件的整数选项(函数长度上限、复杂度阈值、参数数等)会被缩放。比例类的(external_ratioprimitive_ratio 这种 0-1 之间的)按字面量读,不缩放。

内置语言 profile

Cha 给这几种语言带了内置 profile:c / cpp / python / typescript / rust / go。看一下:

cha preset list           # 哪些语言有 profile,各禁用了多少 smell
cha preset show c         # C 的完整解析配置:插件 / smell / 严格度
cha preset show rust

目前只有 c / cpp profile 真的改默认值——其他语言的 profile 列表里有,但没有覆盖。

C / C++ profile

C 是过程式语言,OO 类的检测器默认关掉:

  • 插件级禁用(完全不出 finding):naming / lazy_class / data_class
  • smell 级禁用(插件还跑,但这几条 smell 过滤掉):builder_pattern / null_object_pattern / strategy_pattern / data_clumps

profile 也把大小和耦合阈值调高了——C 项目函数本来就长,include 也多:

插件选项C / C++ 值
lengthmax_function_lines100
lengthmax_file_lines2000
lengthmax_class_lines400
complexitywarn_threshold15
complexityerror_threshold30
cognitive_complexitythreshold25
couplingmax_imports25
long_parameter_listmax_params7

自己覆盖

[languages.<lang>] 下面写啥都覆盖该语言的内置 profile。结构跟全局一样:

# C 上重新打开 naming,但放宽最短名长度
[languages.c.plugins.naming]
enabled = true
min_name_length = 3

# Python 不禁用任何插件,但单独丢掉一条 smell
[languages.python]
disabled_smells = ["naming_too_short"]

# Rust 单独把函数行数上限调严,不动全局 strictness
[languages.rust.plugins.length]
max_function_lines = 40

你写的键优先,内置 profile 在你之下——想重新打开 profile 关掉的插件,写 enabled = true 就行,不用别的仪式。

要看你覆盖之后某语言到底什么配置:

cha preset show <语言>

输出包含解析后的严格度系数、最终启用的所有插件、profile 关掉的插件、以及你额外加的 disabled_smells

相关

配置项参考

.cha.toml 能识别的全部 key。结构由 cha_core::Config 定义;多余的 key 会被静默忽略。

顶层

Key类型默认含义
excludeVec<String>[]跳过的路径 glob 模式。模式是相对项目根的路径。** 匹配任意层。文件遍历器本身就尊重 .gitignore,所以 node_modules/ 这种通常不用写。
strictness"relaxed" / "default" / "strict" / f64"default"阈值整体倍数。relaxed = 2.0×default = 1.0×strict = 0.5×,或者写一个浮点数(0.7)。只作用于整数阈值;比例类阈值不被这个倍数缩放。
disabled_smellsVec<String>[]全局屏蔽的 smell 名。当一个插件出多条 smell(length 一个插件出 long_method / large_class / large_file)但你只想屏蔽其中几条时用。
debt_weightstable见下文每个严重度对应的 tech-debt 分钟数。cha analyze 摘要行用。
pluginstable of tables(默认值)插件级覆盖。见 插件级 section
languagestable of tables{}语言级覆盖。见 语言级 section
layerstable(空)cha layers 的模块 / tier 定义。见 Layers section

debt_weights

[debt_weights]
hint = 5        # 默认 5
warning = 15    # 默认 15
error = 30      # 默认 30

单位是分钟。摘要行的总 debt 显示为 <n>h <n>m

插件级 section

每个插件从 [plugins.<name>] 读自己的配置。结构:

[plugins.<name>]
enabled = true             # 默认 true;写 false 关掉
# ... 插件自己的 key

插件自己的 key 各不相同,默认值跟代码里 Default impl 对齐。完整对照:

[plugins.<name>]可配 key备注
lengthmax_function_lines (50)、max_class_methods (10)、max_class_lines (200)、max_file_lines (500)、complexity_factor_threshold (10.0)越界越多严重度越高。
complexitywarn_threshold (10)、error_threshold (20)Cyclomatic complexity。
cognitive_complexitythreshold (15)超过 2 × threshold 升级为 Error
long_parameter_listmax_params (5)
primitive_obsessionmin_params (3)、primitive_ratio (0.8)
data_clumpsmin_clump_size (3)、min_occurrences (3)
namingmin_name_length (2)、max_name_length (50)
api_surfacemax_exported_ratio (0.8)、max_exported_count (20)、c_max_exported_ratio (1.01)、c_max_exported_count (30)、skip_c_headers (true)C 单独放宽:header 文件本来就是用来对外暴露 API 的。
god_classmax_external_refs (5)、min_wmc (47)、min_tcc (0.33)三项分别是 ATFD(access to foreign data,对外部数据的访问数)、WMC(weighted method count,加权方法数)、TCC(tight class cohesion,类内紧密内聚度),来自 Lanza & Marinescu 的 Object-Oriented Metrics in Practice
brain_methodmin_lines (65)、min_complexity (4)、min_external_refs (7)
couplingmax_imports (15)超过 2 × max_imports 升级为 Error
hub_like_dependencymax_imports (20)
feature_envymin_refs (3)、external_ratio (0.7)
middle_manmin_methods (3)、delegation_ratio (0.5)
message_chainmax_depth (3)
inappropriate_intimacy(无阈值)检测两文件互相 import。
layer_violationlayers = "domain:0,service:1,..."字符串格式:name:rank,name:rank,...。低 rank 不能 import 高 rank。
async_callback_leak(无阈值)检测公开签名里裸出现的 JoinHandle / Future / Channel
switch_statementmax_arms (8)
temporary_fieldmin_methods (3)、max_usage_ratio (0.3)
refused_bequestmin_override_ratio (0.5)、min_methods (3)
design_patternstrategy_min_arms (4)、state_min_arms (3)、builder_min_params (7)、builder_alt_min_params (5)、builder_alt_min_optional (3)、null_object_min_count (3)、template_min_self_calls (3)、template_min_methods (4),以及若干关键词列表一个插件出 6 个 pattern smell,阈值各管各的。
shotgun_surgerymin_co_changes (5)、max_commits (100)git log
divergent_changemin_distinct_reasons (4)、max_commits (50)git log
dead_codeentry_points (各语言默认)列在这里的函数永远不会被判定为死代码。
duplicate_codemin_lines (10)AST hash 比对。
commentsmax_comment_ratio (0.3)、min_lines (10)
lazy_classmax_methods (1)、max_lines (10)
data_classmin_fields (2)
speculative_generality(无阈值)interface / trait 实现 ≤ 1 个。
todo_tracker(无阈值)TODO / FIXME / HACK / XXX。HACK 和 XXX 升级为 Warning。
hardcoded_secret(内置正则集)API key、token、密码、JWT。
unsafe_api(内置调用集)evalexecsystemstrcpygetsunsafeinnerHTMLdangerouslySetInnerHTML 等。
error_handlingmax_unwraps_per_function (3)catch 永远报。

权威默认值在每个插件的 Default for <Analyzer> impl 里,cha-core/src/plugins/ 下找。

语言级 section

[languages.<lang>]
disabled_smells = []
[languages.<lang>.plugins.<name>]
# ... 跟 [plugins.<name>] 同样的 key

<lang> 是 cha 给文件打的语言 ID:pythontypescripttsxrustgoccpp。语言级配置会覆盖全局值,不会跟全局值合并。C 的内置预设默认关闭 naminglazy_classdata_classdesign_pattern,并把 length.max_function_lines 提到 80。

例:

[languages.c.plugins.naming]
enabled = false

[languages.c.plugins.length]
max_function_lines = 80

[languages.python.plugins.long_parameter_list]
max_params = 8                          # Python 的 **kwargs 容忍多一点

Layers section

cha layerslayer_violation smell 用的:

[layers]
modules = { domain = ["src/domain/**"], service = ["src/service/**"], controller = ["src/handlers/**"] }

[[layers.tiers]]
name = "data"
modules = ["domain"]

[[layers.tiers]]
name = "logic"
modules = ["service"]

[[layers.tiers]]
name = "api"
modules = ["controller"]

文件里 tier 的顺序 = 从底到顶。低 tier 不能 import 高 tier。layer_violation 单插件本身有更简单的 inline 写法([plugins.layer_violation] 下的 layers = "..." 字符串),见上表。

行内指令

直接在源码里覆盖配置。完整语法见 行内指令

#![allow(unused)]
fn main() {
// cha:ignore                        — 屏蔽下一个 item 上的所有规则
// cha:ignore long_method            — 屏蔽一条
// cha:ignore long_method,complexity — 屏蔽多条
// cha:set long_method=100           — 单独把 long_method 阈值提到 100
// cha:set threshold=200             — 把所有阈值类规则的阈值提到 200
}

支持 //#/* */ 三种注释。

See also

内置插件参考

README 那张表格的详细版。每个检测器到底在查什么、阈值的含义、什么样的代码会触发它,都在这里。

几点约定:

  • 每个插件的源码都在 cha-core/src/plugins/<name>.rs,下面写的默认值就是各文件里 Default for <Analyzer> 写死的那些数。
  • 阈值都会再乘以全局 strictnessrelaxed 2.0×、default 1.0×、strict 0.5×,也可以写任意小数)。
  • 想改某个插件,在 .cha.toml[plugins.<name>] 节里覆盖;只想放过单个函数 / 类,在源码里写 // cha:set <字段>=<值>// cha:ignore <名字>

Bloaters

length

抓过长的函数、过大的类、过大的文件。

源码:length.rs

smell触发条件严重度
long_method函数行数超过 max_function_lines(默认 50)。如果这个函数本身又复杂——cyclomatic × cognitive ≥ complexity_factor_threshold(默认 10.0)——直接升 Error;否则停在 Warning。Hint / Warning / Error
large_class类里方法数超过 max_class_methods(默认 10),或者类的总行数超过 max_class_lines(默认 200)。Warning
large_file文件超过 max_file_lines(默认 500)行。Warning

complexity_factor 这道闸门是为了不冤枉那些"长但顺"的函数:一段 60 行的 lookup 表构造,复杂度低,那就停在 Warning;同样 60 行但绕来绕去的(complexity 12 × cognitive 14 = 168),就升级到 Error。

[plugins.length]
max_function_lines = 80
max_class_lines    = 300

complexity

圈复杂度,也就是函数里能走出多少条互相独立的路径。每碰到一个分支关键字(ifelse ifwhileforcase&&||?catch)就 +1。

源码:complexity.rs

超过 warn_threshold(默认 10)报 high_complexity Warning;超过 error_threshold(默认 20)直接 Error。

[plugins.complexity]
warn_threshold  = 8
error_threshold = 15

cognitive_complexity

输出形式跟 complexity 一样,但算分方式不同——它会对嵌套加重惩罚。三个并排的 if 比一个 ifif 再套 for 便宜得多。

源码:cognitive_complexity.rs

得分超过 threshold(默认 15)报 Warning;超过两倍阈值升 Error。

它跟 complexity 是互补的,不是二选一:complexity 关心的是"要写多少测试才能覆盖",cognitive_complexity 关心的是"读着累不累"。同一个函数两边都飘红,基本就是该重构了。

[plugins.cognitive_complexity]
threshold = 12

long_parameter_list

参数超过 max_params 个(默认 5)报 Warning。

源码:long_parameter_list.rs

推荐的修法是把相关参数打包成一个结构体(Introduce Parameter Object / Preserve Whole Object),调用方就不用记参数顺序了——参数一多最容易出的 bug 就是把两个 String 传反。

[plugins.long_parameter_list]
max_params = 7

primitive_obsession

参数至少 min_params 个(默认 3)、且其中基本类型占比超过 primitive_ratio(默认 0.8)时报 Hint。

源码:primitive_obsession.rs

"基本类型"是各语言自带的数字 / 字符串 / 布尔之类——i32f64boolString&strnumberbooleanany。这条 smell 抓的就是那种"用户 id 是 String、订单 id 也是 String,传参时两个一掉换我没察觉"的失控滑坡。

修法是 Replace Primitive with Object:把语义裹进一个 newtype 或值对象里,类型系统就能替你拦下传错。

[plugins.primitive_obsession]
min_params       = 4
primitive_ratio  = 0.9

data_clumps

同一组参数类型在至少 min_occurrences(默认 3)个不同函数里反复出现,并且这组类型本身长度至少 min_clump_size(默认 3)。每抓到一组报一个 Hint。

源码:data_clumps.rs

primitive_obsession 是同一类问题的两个角度:那条是"一个函数里塞了一堆 primitive",这条是"(String, String, i32) 这三联体在五个函数里都出现了"。修法也是同一个——抽出一个结构体。

[plugins.data_clumps]
min_clump_size  = 4
min_occurrences = 2

naming

源码:naming.rs

smell触发条件严重度
naming_too_short函数 / 类名短于 min_name_length(默认 2 个字符)。Warning
naming_too_long函数 / 类名长于 max_name_length(默认 50 个字符)。Hint
naming_convention类名首字母不是大写(违反 PascalCase)。Hint

naming_convention 是目前 Cha 里唯一带自动修复的 smell —— cha fix 会通过 Plugin::try_fix 把代码里所有引用都改成 PascalCase。改名走的是 AST 路径,字符串字面量和注释里的同名字符串不会被误伤。

C 语言预设里这个插件是关掉的:C 用 snake_case 是惯例,报"违反"只会满屏噪音。

[plugins.naming]
min_name_length = 3
max_name_length = 40

api_surface

源码:api_surface.rs

一个文件里导出的(public)函数和类,要么数量超过绝对值(默认 max_exported_count = 20),要么占比超过总声明数的 max_exported_ratio(默认 0.8),就报一个 large_api_surface Warning。一个文件里如果一共还没 5 个声明,直接跳过——3 个函数的文件谈"暴露过多"没意义。

C / C++ 源文件走一套更宽松的阈值(c_max_exported_count = 30c_max_exported_ratio = 1.01),因为 .c 里 non-static 函数本来就是默认全导出,是 .h 头文件在控制可见性,对 .c 算"导出占比"几乎一定 100%。.h / .hpp / .hxx / .hh / .h++ 这些头文件本身就是公开 API,开了 skip_c_headers(默认 true)会整个跳过。

[plugins.api_surface]
max_exported_count = 15
max_exported_ratio = 0.7

god_class

源码:god_class.rs

一个类要触发 god_class Warning,必须三个信号同时满足:

  • ATFD(Access to Foreign Data):这个类各个方法访问到的"外部类 / 对象"种类数,超过 max_external_refs(默认 5)。说明它伸手伸太远。
  • WMC(Weighted Method Count):所有方法的圈复杂度之和,达到 min_wmc(默认 47)。说明它干得太多。
  • TCC(Tight Class Cohesion):方法两两之间共享至少一个字段的比例,低于 min_tcc(默认 0.33)。说明这些方法对"这个类要干嘛"看法不一致。

默认值是经验阈值(来自一份 45 个 Java 项目的统计)。三个信号取交集是为了压假阳性:一个类只是忙(WMC 高)但还内聚,不报;一个类内聚低但本身很小,也不报。忙、散、还往外伸三件事必须凑齐。

修法是 Extract Class(拆掉一部分职责)或者直接按单一职责原则重构。

[plugins.god_class]
max_external_refs = 7
min_wmc           = 60
min_tcc           = 0.25

brain_method

源码:brain_method.rs

god_class 的函数级对应。一个函数要报 brain_method Warning,三个信号必须同时满足:

  • 行数达到 min_lines(默认 65)。
  • 圈复杂度达到 min_complexity(默认 4)。
  • 外部引用(来自函数自身作用域之外的变量 / 字段 / 函数)的种类数达到 min_external_refs(默认 7)。

只长不绕的函数(低复杂度)不会报;只绕不长的函数(行数不够)也不会报;行数和复杂度都飘红但全是自包含计算,外部引用为零,也不会报。三个信号取交集,刚好夹住那种"做太多事、绕太多弯、还伸太多手"的函数。

修法是 Extract Method(拆函数)和 Move Method(搬到该归属的类去)。

[plugins.brain_method]
min_lines         = 80
min_complexity    = 6
min_external_refs = 10

Couplers

coupling

源码:coupling.rs

文件 import 数超过 max_imports(默认 15)报 high_coupling Warning;超过 2 × max_imports 升级到 Error。

Rust 的 mod 声明不算在内——那是模块组织,不是对外耦合。

[plugins.coupling]
max_imports = 12

hub_like_dependency

源码:hub_like.rs

coupling 是同一类信号,但门槛更高(默认 max_imports = 20),关注角度也不一样:这条不是说"这个文件做太多事"(那是 coupling),而是说这个文件已经成了依赖图里的枢纽节点——一个伸进系统大半的中转站。

两条插件刻意有重合。coupling 抓的是日常意义上"这个文件管太多",hub_like_dependency 抓的是架构意义上"整个项目都从这一个文件转一道"。修法是拆模块,或者插一层 Facade 把扇出收拢。

[plugins.hub_like_dependency]
max_imports = 15

feature_envy

源码:feature_envy.rs

一个函数的外部引用至少 min_refs 个(默认 3),其中单一对象就占了至少 external_ratio(默认 0.7)的份额,报 Hint。

经典例子:Order::shipping_total() 一上来读 customer.addresscustomer.countrycustomer.tax_zonecustomer.discount_tier。这个方法挂在 Order 上,但全程都在扒 Customer。修法是把方法搬到它惦记的那个类去(Move Method)。

[plugins.feature_envy]
min_refs       = 4
external_ratio = 0.8

middle_man

源码:middle_man.rs

一个类至少有 min_methods 个方法(默认 3),其中至少 delegation_ratio(默认 0.5)的方法只是把调用转给别的对象,报 Hint。

一个类如果绝大部分方法都是转发,它本身没在干活——调用方完全可以直接找下游对象。修法是 Remove Middle Man:让调用方绕过去。

注意:少量委托是健康的(封装、生命周期管理)。50% 这个默认值要抓的是那种"已经退化成透传薄壳"的类,不是要把正常的 facade 一起拍掉。

[plugins.middle_man]
min_methods       = 4
delegation_ratio  = 0.6

message_chain

源码:message_chain.rs

函数里出现长于 max_depth(默认 3)的点访问链——比如 a.b.c.d.e——报 Warning。链路通过 tree-sitter 识别(按语言对应 field_expression / member_expression / attribute / selector_expression),不是文本匹配,所以跨行或夹了方法调用的链照样能抓出来。

要抓的不是那串点号,而是它隐含的耦合:a.b().c().d().e() 的调用方知道整条中间链路上每一层的类型。用 Hide Delegatea 直接对外暴露 e,调用方就不用再依赖中间这堆类型形状。

[plugins.message_chain]
max_depth = 4

inappropriate_intimacy

源码:inappropriate_intimacy.rs

文件 A 引入 B,同时 B 也引入 A,两边都在 import 那一行报 Warning。这是"本来该合在一起的两个模块被拆开了"或者"两个不相关的模块互相缠在一起"最直接的征兆。

检测时把相对路径解析到磁盘,依次试常见扩展名(.ts.tsx.rs.py.go.cpp.cc.cxx.c.h.hpp.hxx.js.jsx.mts.cts)。非相对路径(npm 包、第三方 crate)一律忽略——循环必须发生在你自己项目里。

修法是 Move Method(把责任推到一边去)或 Hide Delegate(拉出一个第三方模块同时持有两边)。

layer_violation

源码:layer_violation.rs

默认是关的,要先在 .cha.toml 里配层级:

[plugins.layer_violation]
enabled = true
layers  = "domain:0,service:1,controller:2,ui:3"

每一项是 <路径前缀>:<层级>。文件路径能匹到哪个前缀就属于哪一层。底层文件 import 高层文件,直接 Errordomain 不能 import serviceservice 不能 import controller,以此类推。反向(高层 import 底层)是允许的。

用它在 lint 阶段把整洁架构 / hexagonal / onion 那种分层规则钉死。配好之后,CI 会拦下那种"domain 实体悄悄开始 import 数据库适配器"的慢性漂移。

async_callback_leak

源码:async_callback_leak.rs

一个函数的对外签名里出现裸的异步句柄类型——JoinHandleFutureTaskAbortHandleSender / ReceiverUnboundedSender / UnboundedReceiverPromiseAwaitableCoroutineQueueCancelFuncWaitGrouponeshotmpsc——无论是作为参数类型还是返回类型,都报 Hint。

启动器函数会被豁免:函数名以 spawnlaunchstartrun_asyncfire_dispatch_background_ 开头的,存在的意义本来就是产生句柄,跳过。

要抓的是把并发原语漏过模块边界。一旦你的公开 API 返回 JoinHandle,每一个调用方都得知道你用的是哪套 runtime、怎么管生命周期。把句柄包进领域类型(比如 RenderJob 内部持有 JoinHandle),调用方就用你自己的词汇 cancel / await / 查询,不用学 tokio 那一套。


OO Abusers

switch_statement

源码:switch_statement.rs

函数里的 switch / match 分支数超过 max_arms(默认 8)报 Warning。判定走 tree-sitter(Rust 的 match_expression、TypeScript / C / C++ 的 switch_statement、Python 的 match_statement、Go 的 expression_switch_statement / type_switch_statement),不是字符串匹配,所以注释和字符串里出现的关键字不会误报。

经典修法是 Replace Conditional with Polymorphism:每个分支变成子类 / trait 实现 / 枚举变体的一个方法,调度本身消化进多态调用。值不值得做要看分支组改的频率:如果几乎每周都加一个新分支,多态划算;如果分支集合稳定,留着 switch 反而清楚。

[plugins.switch_statement]
max_arms = 12

temporary_field

源码:temporary_field.rs

一个类至少有 min_methods 个方法(默认 3),其中某个字段只被不超过 max_usage_ratio(默认 0.3,即 30%)的方法用到,每个这样的字段报一个 Hint。零使用的字段不算——那是死代码不是临时字段。

要抓的是那种"以防万一加一个"或者"只在特定场景活一会儿"的字段:一个 _intermediate_buffer 只被 compute() 用、一个 _pending_request_id 只被 cancel() 用。修法是 Extract Class:把这个字段和真正用它的那几个方法一起拆成新对象。

[plugins.temporary_field]
min_methods      = 5
max_usage_ratio  = 0.25

refused_bequest

源码:refused_bequest.rs

子类至少有 min_methods 个方法(默认 3),其中至少 min_override_ratio(默认 0.5)覆盖了父类,报 Hint。

子类把继承下来的东西改写过半,继承关系就名存实亡了——这子类已经不是父类的"是一个",只是把父类当藏起来的成员在用。修法是 Replace Inheritance with Delegation:把父类换成一个字段持有,"override 大半"的子类就老老实实变成一层包装。或者反过来用 Push Down Method:如果父类的某些方法只有一个子类在用,把它们直接搬下去。

[plugins.refused_bequest]
min_override_ratio = 0.6
min_methods        = 4

design_pattern

源码:design_pattern.rs

提示六种结构性模式,各自一个 smell,全部 Hint 级别:

smell触发条件
strategy_pattern函数在某个字段上 dispatch,字段名包含 type_field_keywords 之一(默认 typekindroleactionmode),且分支数至少 strategy_min_arms(默认 4)。
state_pattern同样的形状,但 dispatch 的字段名包含 state_field_keywords 之一(默认 statestatus),分支数至少 state_min_arms(默认 3)。
builder_pattern函数参数数至少 builder_min_params(默认 7);或者参数数至少 builder_alt_min_params(默认 5),且其中可选参数至少 builder_alt_min_optional(默认 3)。
null_object_pattern同一个字段在至少 null_object_min_count(默认 3)个不同函数里都被做了 null check。
template_method_pattern一个类至少有 template_min_methods(默认 4)个方法,其中某个方法在 self 上调用了至少 template_min_self_calls(默认 3)个其他方法。
observer_pattern类有名字带 Listener / Observer / Callback / Handler 的字段,并 / 或有名字带 notify / emit / publish 的方法。

这些都是建议性的——模式不一定永远是对的答案,比如分支固定的小 switch、或者 7 个参数确实是 7 个逻辑独立字段的构造函数。建议只是"这个形状常见地能在模式 X 下变干净",不是"这是错的"。

[plugins.design_pattern]
strategy_min_arms = 5
builder_min_params = 8

# 你项目里用的字段命名不一样的话覆盖这些列表
type_field_keywords  = ["type", "kind", "variant", "tag"]
state_field_keywords = ["state", "phase", "stage"]

Change Preventers

这一组的两条插件不读代码,读的是 git log。每次分析跑一次 git log 然后整轮缓存,回答的是"这个项目实际上是怎么被改的",不是它现在长什么样。

shotgun_surgery

源码:shotgun_surgery.rs

对每个文件,看过去 max_commits 个 commit(默认 100),统计它跟其他每个文件一起被改的次数。某个搭档文件共出现至少 min_co_changes 次(默认 5),就为这一对报一个 Hint。每个文件最多报最常一起出现的前 3 个搭档。

要抓的形态:每次做一个逻辑变更都得同时改一组固定的文件。修法是 Move MethodMove Field——把散在各处的行为聚到一个类里,下一次相同的变更只需要改一处。

容易假阳性的几类:迁移脚本、配置文件、构建清单、锁文件。这些放进 .cha.tomlexclude 里。

[plugins.shotgun_surgery]
min_co_changes = 8
max_commits    = 200

divergent_change

源码:divergent_change.rs

同一份数据,反过来问:不是"哪些文件总一起改",而是"这一个文件因为多少种不同原因被改过"。

"原因" 取的是 conventional commit 的 scope(type(scope): subject 里的 scope),如果没有 scope 就退而取主题第一个词。同一个文件在过去 max_commits 次 commit(默认 50)里跨过至少 min_distinct_reasons(默认 4)种 scope,就报一个 Hint。

要抓的形态:这个文件干的事太杂,各种不相关的需求都会扯到它。修法是 Extract Class——按 scope 边界把文件切开。

这条规则非常依赖 commit message 的卫生。项目没用 conventional commits 的话,fallback(取主题第一个词)只是近似分组,结果会更糙——可以把阈值调高。

[plugins.divergent_change]
min_distinct_reasons = 6
max_commits          = 100

Dispensables

dead_code

源码:dead_code.rs

一个非导出的函数或类,文件内、全项目调用图里都没人引用,而且也不在 entry_points 名单里——报 Hint。

三层信号叠加:

  • 同文件使用 —— 走 AST 标识符扫描。字符串字面量、注释里出现的同名子串不算"引用"。
  • 跨文件调用图 —— 来自 parser 的全项目索引;本文件不用、但别的文件调用过的函数仍然算活的。
  • Token-concat 还原(仅 C / C++)—— 文件里有 #define ... ## 这种宏(X-macro 派发表)时,分析器会扫宏体里的 prefix##X##suffix 槽位,再扫每一处调用点的实参,反推出可能的展开名字(比如 _handleColorAttr)。这些名字会被加进文件的引用集合,避免一个 X-macro 把整个文件的真函数都按死代码报掉。

entry_points 是给框架 / runtime / 构建系统调用、但你代码里看不到的函数留的白名单:默认包含 Rust 的 main / new / default / drop / fmt,Python 的 __init__ / __new__ / __call__ / __enter__ / __exit__ / __del__,Go 的 init,C 的 _start,tokio 的 tokio_main / main_async

如果 ctx.tree 不可用,插件会退回到子串扫描——这只会在 unit test 场景遇到,CLI 实际跑不会触发。

[plugins.dead_code]
entry_points = ["main", "wasm_main", "ffi_entry"]

duplicate_code

源码:duplicate_code.rs

两个或更多函数的 AST 结构哈希一致,每个都超过 10 行,每一份重复都报一个 Warning。哈希计算忽略变量名和具体空白,所以结构一样、变量重命名过的"双胞胎"也能抓出来。

10 行下限是为了不让 trivial 的 getter 和一行函数刷屏(它们经常哈希相同)。修法:Extract Method / Extract Function / Pull Up Method

这条插件没配置项——重复就是重复,10 行下限是写死的实现细节而非旋钮。

comments

源码:comments.rs

函数至少 min_lines 行(默认 10),且其中注释行占比超过 max_comment_ratio(默认 0.3,即 30%),报 Hint。

要抓的不是"注释多本身",而是"用注释来填补结构上的缺失"。一个 20 行函数要写 8 行注释才能解释清楚,通常意味着它应该被拆成三个更小的函数,让函数名替注释发声。

[plugins.comments]
max_comment_ratio = 0.4
min_lines         = 15

lazy_class

源码:lazy_class.rs

类的方法数不超过 max_methods(默认 1)总行数不超过 max_lines(默认 10)报 Hint。Interface / trait 不算——那本来就是刻意保持很小的。

默认值(≤ 1 个方法、≤ 10 行)故意打得很狠,要抓的是教科书式的"为一个 helper 包了个壳,之后再也没长大"。如果你项目里本来就有大量小但有意为之的值类型,把两个上限调高。

[plugins.lazy_class]
max_methods = 2
max_lines   = 20

data_class

源码:data_class.rs

类至少有 min_fields(默认 2)个字段、没有任何行为方法(只有字段访问器 / 修改器 / 构造器 / Default 之类)、并且不是 interface——报 Hint。

要抓的形态叫"贫血领域模型":类只是个状态容器,对自己持有的数据没观点,调用方只好直接读写它的字段。修法是 Move Method——找到代码库其他地方那些专门处理这个类数据的函数,搬进来。

确实该是纯数据的类型(API 边界的 DTO、序列化封装)就老老实实是 data class。这种情况用 // cha:ignore data_class 压掉。

[plugins.data_class]
min_fields = 3

speculative_generality

源码:speculative_generality.rs

interface / trait 在同一个文件里有 0 个或 1 个实现,报 Hint。没配置项——规则就是二选一。

要抓的形态:一个抽象当初为了"以后可能要换实现"加上去,结果到现在只有一个实现。在第二个实现出现之前,这个抽象等于在为你不用的可选性付维护税。修法是把 interface 内联掉;以后真有第二个实现需要时再抽出来不晚。

设计上这条只看本文件。同文件定义、跨模块实现的 trait 不会触发——跨文件检测交给 post-analysis pass cross_layer_import(那不是 Plugin trait 体系下的检测器)。

todo_tracker

源码:todo_tracker.rs

代码里每一条 TODO / FIXME / HACK / XXX 注释都报一个 finding:

标签严重度
HACKWarning
XXXWarning
FIXMEHint
TODOHint

匹配是按词边界的("TODOs" 不会触发,methodo 也不会)。没配置项——四种标签和各自的严重度都写死了。


Security

hardcoded_secret

源码:hardcoded_secret.rs

每个字符串字面量会跟一组固定的"密钥形状"正则匹配:

模式匹配
AWS Access KeyAKIA[0-9A-Z]{16,}
Private Key-----BEGIN (RSA | EC | DSA | OPENSSH )?PRIVATE KEY-----
GitHub Tokengh[ps]_[A-Za-z0-9_]{36,}
Slack Tokenxox[bpors]-[A-Za-z0-9-]{10,}
JWTeyJ...eyJ...(点分隔三段 base64 风格)
Hex Secret整段字面量是 32+ 位 hex
Long Base64-ish Secret整段字面量是 40+ 位 base64 / urlsafe 字符

每命中一条报一个 Warning。匹配只在 string_literal AST 节点上跑,注释、标识符、doc 块里出现的同样字符串不会触发。

"Hex Secret" 和 "Long Base64-ish Secret" 这两条对长确定性常量会假阳性(测试向量、哈希摘要、嵌入资源 ID)。这种逐行用 // cha:ignore hardcoded_secret 压掉。

目前没配置项——模式是写死的。要按你团队的规则做扩展,写个 WASM 插件挂上去。

unsafe_api

源码:unsafe_api.rs

按语言用 tree-sitter query 匹配已知危险调用:

  • Rustunsafe 块、unsafe fn
  • Pythonevalexecos.systemsubprocess.callpickle.load / pickle.loads
  • TypeScriptevalinnerHTML 赋值、React 的 dangerouslySetInnerHTML JSX 属性、document.write
  • C / C++getssprintfstrcpystrcatsystem
  • Goexec.Commandtemplate.HTML

每命中一处报一个 Warning,写明触发的名字和一句原因。AST 路径走的,字符串 "system(rm -rf /)" 写在日志里不会触发。

ctx.tree 不可用时插件直接返回空——比起 grep 一通在字符串、注释里乱报,宁愿沉默。

没配置项——危险 API 名单写死。

error_handling

源码:error_handling.rs

两个独立的 smell 共用一次扫描:

  • unwrap_abuse —— 函数里 .unwrap().expect(...) 的次数超过 max_unwraps_per_function(默认 3),把这个函数里每一处 .unwrap() / .expect() 都报成 Warning。检测走 (call_expression (field_expression (field_identifier) @method)) 然后比对方法名是不是 unwrap / expect
  • empty_catch —— TypeScript / JavaScript 的 catch 或 Python 的 except 块如果是空的,或者只有 pass / 一行注释,报 Warning。Rust 不在这条规则里——match 的空分支大多数时候是有意为之。

阈值要抓的是那种"unwrap 的速度快过错误模型设计"的函数。一处 .unwrap() 跑在已知必成立的不变量上没问题;同一函数十处就该考虑这个函数本身应该返回 Result

[plugins.error_handling]
max_unwraps_per_function = 5

插件开发指南

Cha 是代码坏味道(code smell)检测工具。除了 34 个内置检测器,还可以装第三方插件——一个 WASM 模块,host 把每个文件解析后丢给你的 analyze(),你返回若干 Finding(检测到的坏味道),host 汇合所有 finding 输出给用户。

这一页讲怎么从零写、编译、装、测一个这样的插件。

前置

  • Rust 工具链 + wasm32-wasip1 target:

    rustup target add wasm32-wasip1
    
  • cha CLI 装好且在 $PATH

Quick Start

mkdir my-plugin && cd my-plugin
cha plugin new my-plugin
cha plugin build           # 产出 my_plugin.wasm
cha plugin install my_plugin.wasm
cha analyze src/

包名是 my-plugin,编译产物是 my_plugin.wasm——Cargo 把 - 转成 _。后面 cha analyze 自动加载装好的所有插件。

脚手架

cha plugin new <名字> 生成:

my-plugin/
  Cargo.toml   # cdylib + cha-plugin-sdk + wit-bindgen 依赖
  src/
    lib.rs     # plugin! 宏 + 一个 PluginImpl 最简实现

当前目录空就在原地生成;不空就建一个 <名字>/ 子目录。

插件结构

#![allow(unused)]
fn main() {
cha_plugin_sdk::plugin!(MyPlugin);

struct MyPlugin;

impl PluginImpl for MyPlugin {
    fn name() -> String { "my-plugin".into() }
    fn smells() -> Vec<String> { vec!["my_smell".into()] }
    fn analyze(input: AnalysisInput) -> Vec<Finding> { vec![] }
}
}

plugin! 宏会替你接好 host 跟插件之间的通信、把下面要用到的所有类型 import 进作用域。你只用关心 PluginImpl trait 怎么实现。

类型清单

plugin!(MyPlugin) 之后,下面这些类型自动在作用域里。PluginImpl 是你要实现的 trait。

类型说明
AnalysisInputanalyze() 拿到的完整文件上下文
Finding一条 finding
FunctionInfo单个函数的信息(名字、行号、复杂度、参数等)
ClassInfo单个类的信息(方法数、字段、是否导出等)
ImportInfoimport 来源 + 行号 + 是否是模块声明
CommentInfo注释文本 + 行号
ArmValueswitch / match 分支的字面值(StrLit / IntLit / CharLit / Other
FileRoleSource / Test / Doc / Config / Generated
Location文件路径 + 行列范围
SeverityHint / Warning / Error
SmellCategoryBloaters / Couplers / Dispensables / ...
OptionValue配置值类型:Str / Int / Float / Boolean / ListStr
tree_queryAST query 模块(见下)
project_query跨文件查询模块(见下)

AnalysisInput 字段

#![allow(unused)]
fn main() {
pub struct AnalysisInput {
    pub path: String,             // 文件路径
    pub content: String,          // 源码原文
    pub language: String,         // "typescript" | "rust" | "python" | "go" | "c" | "cpp"
    pub total_lines: u32,
    pub role: FileRole,           // Source / Test / Doc / Config / Generated
    pub functions: Vec<FunctionInfo>,
    pub classes: Vec<ClassInfo>,
    pub imports: Vec<ImportInfo>,
    pub comments: Vec<CommentInfo>,
    pub type_aliases: Vec<(String, String)>,
    pub options: Vec<(String, OptionValue)>,  // 来自 .cha.toml
}
}

Warning:WASM 插件跑在沙箱里,没有文件系统权限。读源码用 input.content不要std::fs::read_to_string(&input.path)——会静默返回空字符串。

文件角色

role 字段告诉你正在分析的是什么类型的文件。利用它给不同文件套不同规则:

#![allow(unused)]
fn main() {
fn analyze(input: AnalysisInput) -> Vec<Finding> {
    if input.role == FileRole::Test {
        return vec![];  // 测试文件跳过
    }
    // ...
}
}

Declaring smells

每个 Finding 都带一个 smell_name。在 smells() 里把全部 smell 名声明出来,host 就能:

  • cha plugin list 里展示你这个插件能出哪些 smell
  • 让用户在 .cha.toml 里写 disabled_smells = ["你的_smell"] 来禁用某条
  • 把禁用名单回传给你的插件,你早点跳过这部分计算

input.options 里有个特殊 key __disabled_smells__ 装着用户禁用的 smell 名单。提前跳过:

#![allow(unused)]
fn main() {
use cha_plugin_sdk::is_smell_disabled;

fn analyze(input: AnalysisInput) -> Vec<Finding> {
    let mut out = Vec::new();
    if !is_smell_disabled!(&input.options, "my_smell") {
        // 没被禁的时候才算
    }
    out
}
}

is_smell_disabled! 是个宏(注意感叹号)。它返回 bool

host 也会事后再过滤一遍 finding,所以忘调 is_smell_disabled! 不会让被禁的 smell 漏到用户输出——只是白算一遍。

AST Query API(tree_query

插件可以通过 host 回调跑 tree-sitter query,查当前文件的 AST:

#![allow(unused)]
fn main() {
fn analyze(input: AnalysisInput) -> Vec<Finding> {
    // 找文件里所有 unsafe 块
    // 返回 Vec<Vec<QueryMatch>> —— 外层每个 match 一项,内层每个 capture 一项
    let matches: Vec<Vec<QueryMatch>> = tree_query::run_query("(unsafe_block) @blk");
    for m in &matches {
        for capture in m {
            // capture.node_kind / capture.text / capture.start_line ...
        }
    }

    // 一次跑多个 query(减少 host 边界穿越开销)
    // 返回 Vec<Vec<Vec<QueryMatch>>>,每个 pattern 一项,顺序跟入参一致
    let results: Vec<Vec<Vec<QueryMatch>>> = tree_query::run_queries(&[
        "(if_statement) @if".into(),
        "(for_statement) @for".into(),
    ]);

    // 拿指定位置的 AST 节点。返回 Option<QueryMatch>。
    // 行 1-based,列 0-based。
    if let Some(node) = tree_query::node_at(10, 4) {
        // node.node_kind, node.text, ...
    }

    // 拿一段行范围内的所有命名顶层节点。返回 Vec<QueryMatch>。
    let nodes: Vec<QueryMatch> = tree_query::nodes_in_range(1, 50);

    vec![]
}
}

Query pattern 用 tree-sitter 的 S 表达式 query 语言。重复跑同一个 pattern 没额外开销。

每个 QueryMatch 包含:

  • capture_name —— pattern 里的 @名字
  • node_kind —— tree-sitter 节点类型(比如 "function_definition"
  • text —— 匹配到的源码原文
  • start_line / end_line —— 1-based 行号(跟 FunctionInfo.start_line / ClassInfo.start_line 一致)
  • start_col / end_col —— 0-based 字节列

Note:SDK 里所有行号都是 1-based,列号都是 0-based 字节偏移。

Project Query API(project_query

跨文件分析(调用方、类型来源、其他文件的函数体)通过 project_query host 函数:

调用图

#![allow(unused)]
fn main() {
// 这个函数有没有被本文件之外的人调过?
let unused = !project_query::is_called_externally(&fn_name, &input.path);

// 哪些文件引用了 `name`
let callers: Vec<String> = project_query::callers_of(&fn_name);

// 全项目跨文件调用计数:(caller_path, callee_path, count) 元组
let counts: Vec<(String, String, u32)> = project_query::cross_file_call_counts();
}

符号定义所在

#![allow(unused)]
fn main() {
// 这个函数 / 类首次声明在哪个文件
let f_home: Option<String> = project_query::function_home(&fn_name);
let c_home: Option<String> = project_query::class_home(&class_name);

// 这个函数名对应的 (文件, FunctionInfo) 元组
let f: Option<(String, FunctionInfo)> = project_query::function_by_name(&fn_name);

// 哪个函数声明覆盖了这个 (line, col)?
// 行 1-based,列 0-based。返回最内层匹配(行范围最小的那个)。
if let Some(host_fn) = project_query::function_at(&input.path, line, col) {
    // host_fn.start_line / host_fn.end_line 都是 1-based
}
}

类型来源 / 项目形态

#![allow(unused)]
fn main() {
// 项目里有没有声明这个名字
let is_local = project_query::is_project_type(&type_ref.name);

// 是不是真正的第三方依赖
// (External origin,且不是 stdlib,也不是 workspace 同级 crate)
let is_3p = project_query::is_third_party(&type_ref);

// Rust workspace 同级 crate 名(非 Rust workspace 时是空)
let siblings: Vec<String> = project_query::workspace_crate_names();

// 路径是否符合测试文件特征:__tests__/ / __mocks__/ / .test.ts / .spec.ts 等
if project_query::is_test_path(&input.path) { /* ... */ }

// 全项目分析过的文件总数
let n: u32 = project_query::file_count();
}

function_at 用来回答"这个位置(行列)归属于哪个函数声明"——配合 tree-query 用得多,query 命中一个位置之后想拿到包含它的函数。

FunctionInfo 字段

字段类型含义
nameString函数名
start_line / end_lineu32函数起止行(1-based)
name_col / name_end_colu32函数名标识符的起止列(0-based 字节列)
line_countu32函数体行数
complexityu32圈复杂度(cyclomatic complexity):1 + 分支点数
cognitive_complexityu32认知复杂度(SonarSource 2017):把嵌套深度也算进去的可读性指标
is_exportedbool是否对外暴露(pub / export
parameter_countu32参数个数
parameter_typesVec<TypeRef>参数类型,按声明顺序排列;每项是已经解析好的 TypeRef
parameter_namesVec<String>参数名,跟 parameter_types 一一对应;匿名参数(C void foo(int);)填空串
optional_param_countu32可选参数个数(驱动 Builder pattern 检测)
return_typeOption<TypeRef>声明的返回类型;没标注 / 推断不出时为 None
external_refsVec<String>函数体里引用的"外部对象的字段或方法"名集合(驱动 Feature Envy)
referenced_fieldsVec<String>函数体里访问到的本类字段名(驱动 Temporary Field)
null_check_fieldsVec<String>函数体里做 null/None 判空的字段名(驱动 Null Object pattern)
called_functionsVec<String>函数体里调用到的函数 / 方法名(喂给项目级调用图)
chain_depthu32函数体内方法链的最大长度(驱动 Message Chains,比如 a.b.c.d = 4)
switch_armsu32函数体里 switch / match 分支的总条数
switch_arm_valuesVec<ArmValue>每个 switch / match 分支的字面量值,按源码顺序排;驱动 stringly_typed_dispatch(≥3 条全是字符串字面量)这类基于值的检测
switch_dispatch_targetOption<String>switch / match 是基于哪个字段 / 变量在分发的(驱动 Strategy / State pattern)
is_delegatingbool这个函数是不是单纯转调另一个对象的方法(驱动 Middle Man)
comment_linesu32函数体内注释行数
body_hashOption<String>函数体 AST 结构哈希;驱动重复代码检测,结构等价但变量名不同也能匹中

ClassInfo 字段

字段类型含义
nameString类 / 结构体名
start_line / end_lineu32类起止行(1-based)
name_col / name_end_colu32类名标识符的起止列(0-based 字节列)
line_countu32类体行数
is_exportedbool是否对外暴露
is_interfacebool是不是接口 / 抽象类
has_behaviorbool类里有没有非访问器方法(也就是真业务逻辑);用来区分 Data Class
method_countu32方法总数
field_countu32字段总数
field_namesVec<String>类里声明的字段名
parent_nameOption<String>父类 / 父 trait 名(驱动 Refused Bequest)
override_countu32覆盖了父类多少个方法(驱动 Refused Bequest)
self_call_countu32类里最长的那个方法对自身其它方法的调用次数(驱动 Template Method)
has_listener_fieldbool类里有没有监听器 / 回调集合字段(驱动 Observer pattern 识别)
has_notify_methodbool类里有没有 notify / emit 类型的方法(同上)

读配置项

.cha.toml 里写的选项:

[plugins.my-plugin]
threshold = 10
label = "custom"
tags = ["a", "b"]

用 SDK 提供的取值宏:

#![allow(unused)]
fn main() {
use cha_plugin_sdk::{option_int, option_str, option_list_str};

let threshold = option_int!(&input.options, "threshold").unwrap_or(5);
let label     = option_str!(&input.options, "label").unwrap_or("default");
let tags      = option_list_str!(&input.options, "tags").unwrap_or(&[]);
}

可用的宏:option_str! / option_int! / option_float! / option_bool! / option_list_str! / str_options!

编译

cha plugin build

它跑 cargo build --target wasm32-wasip1 --release,再用内嵌的 WASI adapter 把输出转成 WASM Component。结果是当前目录下的 <名字>.wasm

Warning:发布时不要直接用 cargo build。Cargo 出来的原始 .wasm 是 core module,不是 component——Cha host 加载不了。cha plugin build 包了一层 component 编码(用 wasm-tools component new + WASI adapter)。

开发期间为了调试可以用 cargo build,但重新装之前要再跑一遍 cha plugin build,否则 host 拿到的还是上一版。

WASM 兼容性速查

插件跑在 wasm32-wasip1 + WASI Reactor adapter 里。一些 Rust crate 在这个环境就算能编也不能用:

Crate / API状态备注
regex❌ runtime panicRegex::new() 在当前 host 配置下会失败。改手写字符扫描——常见模式大概 50 LOC,更安全
std::time::SystemTime::now()❌ 不可靠 / panicWASI clock 各 host 不一致。要"今天的日期"就在 .cha.toml 加一个 today 选项
serde_json✅ 能用体积大,但没坑
tree-sitter(Rust crate 本身)❌ 别用插件已经在 WASM 里跑了;要 query 调 host 的 tree_query
文件系统❌ 沙箱关std::fs::read_to_string(&input.path) 返回空。读源码用 input.content
git / 网络❌ 沙箱关没子进程、没 socket

不确定的时候:依赖尽量精简、小模式手写不引 crate、外部状态(时间 / 配置)通过插件选项传进来。

安装

cha plugin install my_plugin.wasm        # 项目级:.cha/plugins/
cp my_plugin.wasm ~/.cha/plugins/        # 全局

每次 cha analyze 都会从这两个位置加载所有 .wasm 插件。

列出 / 卸载

cha plugin list                  # 显示已装插件 + 各自的 smell 名单
cha plugin remove my_plugin      # 用 .wasm 文件名(不带 .wasm 也行)

配置

插件装好就默认启用。在 .cha.toml 里关闭或调参:

[plugins.my-plugin]
enabled = false       # 或者保留默认 true,下面这一项是给插件传配置
threshold = 20

section 名要跟 name() 返回的字符串一致。

测试

Cargo.toml 里加:

[dev-dependencies]
cha-plugin-sdk = { git = "https://github.com/W-Mai/Cha", features = ["test-utils"] }

test-utils feature 没默认开,所以 dev-dependencies 单独写一行带 features 的引用。SDK 还没在 crates.io 上发布,所以走 git

写测试——source(language, code) 给测试一个虚拟源文件:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use cha_plugin_sdk::test_utils::WasmPluginTest;

    #[test]
    fn detects_issue() {
        WasmPluginTest::new()
            .source("typescript", "function todo_fix() {}")
            .assert_finding("my_smell_name");
    }

    #[test]
    fn no_false_positive() {
        WasmPluginTest::new()
            .source("typescript", "function processData() {}")
            .assert_no_finding();
    }

    #[test]
    fn respects_options() {
        WasmPluginTest::new()
            .source("typescript", r#"fetch("https://example.com");"#)
            .option("DOMAIN", "example.com")
            .assert_finding("hardcoded_string");
    }

    #[test]
    fn list_options_work() {
        WasmPluginTest::new()
            .source("typescript", "// REVIEW: needs second look")
            .option_list("extra_tags", &["REVIEW"])
            .assert_finding("extended_todo_tag");
    }
}
}

可用的选项设置:

  • .option(key, value) —— 字符串
  • .option_list(key, &[values]) —— 字符串列表
  • .option_bool(key, true_or_false)
  • .option_int(key, integer)
  • .option_float(key, float)

跑:

cha plugin build
cargo test

cargo test 时如果 .wasm 不存在,WasmPluginTest 会自动跑一次 cha plugin build

断言 API

方法作用
.assert_any_finding()至少一条 finding
.assert_no_finding()没有任何 finding
.assert_finding("name")至少一条命中指定 smell name 的 finding
.assert_no_finding_named("name")没有命中指定 smell name 的 finding
.findings()返回 Vec<Finding>,给自定义断言用

示例插件

仓库 examples/ 下有 4 个端到端示例:

WIT 接口

想看 host 跟插件之间的契约长啥样,完整 WIT 在 wit/plugin.wit

world analyzer {
    use types.{analysis-input, finding};

    import tree-query;
    import project-query;

    export name: func() -> string;
    export version: func() -> string;       // 自动从 Cargo.toml 读
    export description: func() -> string;   // 自动从 Cargo.toml 读
    export authors: func() -> list<string>; // 自动从 Cargo.toml 读
    export smells: func() -> list<string>;  // 来自 PluginImpl::smells(默认空)
    export analyze: func(input: analysis-input) -> list<finding>;
}

命令行

Cha 一共 15 个顶层子命令,加上 pluginpreset 两个嵌套组。下面按用途分。

完整 --help 树用 cha help-markdown 可以一次拉全。

分析

命令用途
analyze跑插件,报代码坏味道——最常用
parsedump 解析结果(函数 / 类 / import / 注释)

报告与历史

命令用途
baseline把当前 finding 拍快照,老问题屏蔽
trend看 finding 数随 commit 怎么变
hotspot改动频度 × 复杂度的热点
deps依赖图:import / 类 / 调用
layers从 import 反推架构层级

配置与调优

命令用途
init生成默认 .cha.toml
schema打印 finding JSON Schema(细节同 init 页)
calibrate按项目统计推荐阈值(P90 / P95)
preset看内置语言 profile 和严格度等级

自动修复

命令用途
fix自动改简单问题(目前只支持 naming_convention

插件

命令用途
plugin new脚手架一个 WASM 插件
plugin build编译 + 打包成 WASM Component
plugin install装到 .cha/plugins/
plugin list列已装插件
plugin remove删插件

插件开发指南 有完整流程。

编辑器集成

命令用途
lsp启 LSP 服务器(标准 stdio 协议)
completions生成 shell 补全脚本

analyze

跑一遍代码坏味道检测,是 Cha 用得最多的命令。

用法

cha analyze [参数] [路径...]

不给路径就扫当前目录(递归 + 遵循 .gitignore)。

示例

# 扫当前目录
cha analyze

# 指定路径 + JSON 输出,碰到 error 级 finding 就让 CI 失败
cha analyze src/ --format json --fail-on error

# 只扫工作区改动过的文件
cha analyze --diff

# 从管道读 diff(PR review 用)
gh pr diff | cha analyze --stdin-diff --fail-on warning

# 只跑指定插件
cha analyze --plugin complexity,naming

# 跳过缓存重跑
cha analyze --no-cache

# 拿 baseline 之外的新增 finding
cha analyze --baseline .cha/baseline.json

# 生成 HTML 报告
cha analyze --format html --output report.html

参数

参数默认说明
--formatterminal输出格式:terminal / json / llm / sarif / html
--fail-onfinding 达到此严重度时退出码 1:hint / warning / error
--difffalse只扫工作区未提交的改动文件
--stdin-difffalse从 stdin 读 unified diff,按 diff 里的范围扫
--plugin <名>全开只跑指定插件(逗号分隔)
--no-cachefalse不用缓存(删掉再跑全量)
--baseline <path>只汇报不在 baseline 文件里的 finding
--output <path>, -o输出写到文件(HTML 等大体积格式用)
--strictness <值>default阈值缩放:relaxed(2×)/ default / strict(0.5×)/ 自定义浮点
--allfalse终端格式:所有 finding 全列,不聚合
--top <N>终端格式:只显示前 N 条最严重的
--focus <类目>只看指定类目(逗号分隔):bloaters / oo_abusers / change_preventers / dispensables / couplers / security

JSON 格式的 schema 用 cha schema 拿。

参考

parse

把文件扔进 tree-sitter 解析,把它能看到的结构 dump 出来——函数、类、import、注释、跨文件引用。debug 插件、确认 Cha 是否正确识别某段代码时用。

用法

cha parse [路径...]

示例

# dump 当前目录
cha parse

# 指定文件
cha parse src/main.rs

# 指定多个路径
cha parse src/ tests/

输出包含每个函数 / 类的位置、行数、复杂度、参数列表、外部引用等——基本是 Cha 内部 model 的可读版。

参数

参数默认说明
路径.要解析的文件或目录(默认当前目录)

参考

  • cha analyze —— 解析之上跑插件检测
  • 插件开发 —— 写自定义插件时会接触到这些 model 字段

baseline

把当前所有 finding 拍个快照。后续 cha analyze --baseline 只报快照之后新增的 finding,老问题屏蔽。

接手老仓库时最常用——legacy 代码不可能一次清完,但又不想新代码退化。

用法

cha baseline [-o <文件>] [路径...]

示例

# 在当前目录生成 baseline,默认写 .cha/baseline.json
cha baseline

# 指定输出位置
cha baseline -o legacy-baseline.json

# 之后 CI 里只看 baseline 之外的新问题
cha analyze --baseline .cha/baseline.json --fail-on warning

典型工作流:项目第一次接 Cha 时跑 cha baseline 提交 baseline 文件,CI 用 --baseline 跑 analyze。新代码一旦引入新 finding,CI 就拦下来;老代码慢慢治理。

参数

参数默认说明
-o, --output <path>.cha/baseline.jsonbaseline 文件路径
路径.扫描范围

参考

fix

自动改简单问题。

当前只支持一种naming_convention —— 把不符合 PascalCase 的类名改对。其他 smell 还得手动修。能修的范围由 Plugin::try_fix 接口决定,未来插件可以提供更多自动修复。

用法

cha fix [参数] [路径...]

示例

# 看会改什么,但不真改
cha fix src/ --dry-run

# 真改
cha fix src/

# 只针对工作区改动过的文件
cha fix --diff

改动是 AST 级的——只改标识符 token,字符串字面量和注释里的同名字符串不会被误伤。

参数

参数默认说明
--dry-runfalse只显示会改什么,不写文件
--difffalse只处理工作区未提交的改动文件
路径.处理范围

参考

deps

画依赖图——文件之间的 import、类继承、函数调用三选一。出图格式有 DOT / Mermaid / PlantUML / DSM / 终端 ASCII / HTML / JSON。

用法

cha deps [参数] [路径...]

示例

# 默认:导入依赖图,DOT 格式
cha deps --format dot

# Mermaid 流程图,按目录粒度聚合
cha deps --format mermaid --depth dir

# 类继承图
cha deps --type classes --format dot

# 只看名字含 Plugin 的类,输出 PlantUML
cha deps --type classes --filter Plugin --detail --format plantuml

# 调用图:谁调用了 analyze?
cha deps --type calls --filter analyze --direction in

# analyze 调用了谁?
cha deps --type calls --filter analyze --direction out

参数

参数默认说明
--typeimports图的类型:imports / classes / calls
--formatdot输出:dot / json / mermaid / plantuml / dsm / terminal / html
--depth自动聚合粒度:file / dir / 数字(自定义层级深度)
--filter <名>只看名字含 <名> 的节点
--exactfalse--filter 改为完全匹配
--detailfalse类图:连同字段、方法签名一起出
--directionboth--type calls 专用:in(被调)/ out(调用别人)/ both
路径.扫描范围

dsm 是依赖结构矩阵——大项目看模块间循环很有用。

参考

layers

从 import 依赖反推架构层级——把目录归到 tier,看谁在违反"低层不能依赖高层"的规矩。

用法

cha layers [参数] [路径...]

示例

# 跑一遍,终端默认输出(带不稳定度色带的表格)
cha layers --format terminal

# 推断结果存进 .cha.toml 里 [layers] 节
cha layers --save

# DSM 矩阵
cha layers --format dsm

# Mermaid 流程图
cha layers --format mermaid

# 覆盖自动推断的目录深度
cha layers --depth 2

跑出来会标出被认定为"违反层级"的边——比如 domain/ 反过来 import 了 controller/。这种边一般就是架构腐化的早期信号。

--save 之后,可以跟 layer_violation 插件配合(在 .cha.toml 启用),让 cha analyze 把跨层 import 当 error 拦下来。

参数

参数默认说明
--formatdot输出:dot / json / mermaid / plantuml / dsm / terminal / html
--savefalse把推断的层级写进 .cha.toml
--depth <N>自动模块聚合的目录深度
路径.扫描范围

参考

hotspot

找重构热点——读 git log 拿出"改动频度 × 复杂度"乘积最高的文件。这种文件改得勤又复杂,是技术债集中的地方。

用法

cha hotspot [参数]

示例

# 默认:最近 100 个 commit,前 20 名
cha hotspot

# 看最近 200 个 commit 的前 10 名,输出 JSON
cha hotspot -c 200 -t 10 --format json

输出每行带:路径、change frequency、complexity 分、composite score。score 高的优先重构投入产出比最高。

参数

参数默认说明
-c, --count <N>100分析最近 N 个 commit
-t, --top <N>20显示前 N 个文件
--formatterminal输出格式:terminal / json / llm / sarif / html

参考

  • cha trend —— 看 finding 总数随 commit 演变
  • cha analyze —— 拿到 complexity 数据的源头

trend

看代码质量随时间的演变——按最近 N 个 commit 各 checkout 一遍跑 analyze,画出 finding 总数 / 严重度分布的曲线。

慢,但一年一两次跑出"我们的技术债到底是涨还是跌"很有用。

用法

cha trend [参数]

示例

# 默认:最近 10 个 commit
cha trend

# 看最近 20 个 commit
cha trend -c 20

# JSON 输出(接 dashboards 用)
cha trend -c 50 --format json

每个 commit 都得 checkout + analyze 一遍,跑得不快——大项目 50 commit 可能要几分钟。

参数

参数默认说明
-c, --count <N>10分析最近 N 个 commit
--formatterminal输出:terminal / json / llm / sarif / html

参考

calibrate

按当前项目的实际统计推荐阈值——P90 当 warning 阈值,P95 当 error 阈值。每个项目的"长函数"、"高复杂度"标准其实不一样,calibrate 帮你按自己代码的分布定。

用法

cha calibrate [--apply] [路径...]

示例

# 看建议(不写文件)
cha calibrate

# 把建议写进 .cha/calibration.toml,analyze 会自动读取
cha calibrate --apply

输出大概长这样:

Metric                Warning(P90)  Error(P95)
long_method                     45          78
high_complexity                  8          14
cognitive_complexity            12          22

--apply 之后产生的 .cha/calibration.toml 会被后续 cha analyze 自动叠加在配置之上。想撤回就删掉这个文件。

参数

参数默认说明
--applyfalse把建议写进 .cha/calibration.toml
路径.统计来源

参考

preset

看 Cha 内置的语言 profile 和严格度等级——每种语言开了哪些插件、关了哪些 smell、阈值调成了什么。看完再写自己的 .cha.toml 覆盖。

两个子命令:list(看哪些语言有 profile)/ show <语言>(看某语言的完整解析配置)。

用法

cha preset list
cha preset show <语言>

<语言> 可以是:rust / typescript / python / go / c / cpp

示例

# 哪些语言有 profile
cha preset list

# C 的完整 profile
cha preset show c

# Rust 的(基本就是默认)
cha preset show rust

cha preset show c 会列出:

  • 当前严格度系数
  • 启用的插件清单
  • 被 profile 禁用的插件 / smell
  • profile 调高 / 调低的阈值

目前实际上只有 C / C++ 的 profile 真的修改默认值(procedural 语言不适用 OO 类规则)。其他语言的 profile 存在但暂时没改默认。

参数

子命令参数说明
list列所有有 profile 的语言
show语言名显示该语言的完整解析配置

参考

plugin

WASM 插件生命周期:脚手架 / 编译 / 安装 / 列出 / 删除。

写插件的细节在 插件开发 —— 那边有完整流程(WIT 接口、tree_query、project_query、option 读取、单测)。本页只是命令清单。

用法

cha plugin <子命令> [...]

子命令

子命令用途
new <名字>脚手架一个新插件项目(cdylib + cha-plugin-sdk)
buildcargo build --target wasm32-wasip1 --release,再用 wasm-tools 转成 Component
install <文件.wasm>装到当前项目 .cha/plugins/(项目级)
list列已装插件 + 它们能产出哪些 smell
remove <名字>卸载(含 .wasm 后缀都行)

示例

# 在当前空目录脚手架一个,或在父目录创建新子目录
mkdir my-rule && cd my-rule
cha plugin new my-rule

# 编译并转 Component
cha plugin build

# 装到本项目
cha plugin install my_rule.wasm

# 装全局(手动 cp)
cp my_rule.wasm ~/.cha/plugins/

# 看装了什么
cha plugin list

# 删掉
cha plugin remove my_rule

装哪儿

  • 项目级:.cha/plugins/(跟着仓库走)
  • 全局:~/.cha/plugins/(个人电脑)

cha analyze 每次都从这两个目录加载所有 .wasm

参考

completions

生成 shell 补全脚本。装上之后 cha <Tab> 自动补命令、参数,还能补已装插件的名字--plugin <Tab> 会列出当前 .cha/plugins/~/.cha/plugins/ 下的插件)。

用法

cha completions <shell>

支持:bash / zsh / fish / powershell / elvish

示例

# fish
cha completions fish > ~/.config/fish/completions/cha.fish

# zsh(用户级)
cha completions zsh > ~/.local/share/zsh/site-functions/_cha
# 或者 oh-my-zsh:放进 ~/.oh-my-zsh/custom/

# bash
cha completions bash > ~/.local/share/bash-completion/completions/cha

# 不带参数:打印简短指引,告诉你应该把脚本放哪
cha completions

下一次起 shell 就生效。

参数

参数默认说明
shell不带参数时只打印帮助;带 bash/zsh/fish/powershell/elvish 输出对应脚本

参考

init / schema

两个相关命令——一个生成默认配置,一个打印 finding 的 JSON Schema。

cha init

在当前目录写一份默认 .cha.toml

cha init

生成的文件已经有所有常用插件的默认阈值注释,改起来直接动数字就行。已存在 .cha.toml 不会覆盖。

模板内容跟 /Users/w-mai/Projects/Cha/static/default.cha.toml 完全一致。

cha schema

打印 Cha 输出 JSON 的 schema(Draft 2020-12 格式):

# 看 schema
cha schema

# 拉成本地文件给工具用
cha schema > findings.schema.json

cha analyze --format json 出来的所有字段都遵循这个 schema。配合 IDE 的 JSON Schema 支持,能在写 .cha.toml 或处理 finding 数据时拿到补全和校验。

JSON Schema 的完整字段说明见 JSON Schema 参考

参考

Command-Line Help for cha

This document contains the help content for the cha command-line program.

Command Overview:

cha

察 — Code quality & architecture analysis engine

Usage: cha [OPTIONS] <COMMAND>

Subcommands:
  • analyze — Analyze source files for code smells
  • baseline — Generate a baseline file from current findings (suppresses known issues)
  • parse — Parse source files and show structure
  • init — Generate a default .cha.toml configuration file
  • schema — Print JSON Schema for the analysis output format
  • fix — Auto-fix simple issues (naming conventions)
  • plugin — Manage WASM plugins
  • deps — Show dependency graph (imports, classes, or calls)
  • trend — Analyze recent git commits to show issue trend
  • hotspot — Show hotspots: files with high change frequency × complexity
  • preset — Show builtin language presets and strictness levels
  • layers — Infer architectural layers from import dependencies
  • calibrate — Auto-suggest thresholds from project statistics (P90=warning, P95=error)
  • completions — Generate shell completion scripts (supports dynamic plugin name completion)
  • lsp — Start the Language Server Protocol server
Options:
  • --config <CONFIG> — Path to config file (default: .cha.toml in project root)

cha analyze

Analyze source files for code smells

Usage: cha analyze [OPTIONS] [PATHS]...

Arguments:
  • <PATHS> — Files or directories to analyze (defaults to current directory)
Options:
  • --format <FORMAT> — Output format

    Default value: terminal

    Possible values: terminal, json, llm, sarif, html

  • --fail-on <FAIL_ON> — Exit with code 1 if findings at this severity or above exist

    Possible values: hint, warning, error

  • --diff — Only analyze files changed in git diff (unstaged)

  • --stdin-diff — Read unified diff from stdin, analyze only changed files/lines

  • --plugin <PLUGIN> — Only run specific plugins (comma-separated names)

  • --no-cache — Disable analysis cache (force full re-analysis)

  • --baseline <BASELINE> — Only report findings not in the baseline file

  • -o, --output <OUTPUT> — Write output to file (used with --format html)

  • --strictness <STRICTNESS> — Strictness level: relaxed (2x), default (1x), strict (0.5x), or a custom float

  • --all — Show all findings without aggregation (terminal format)

  • --top <TOP> — Show only the top N most severe findings (terminal format)

  • --focus <FOCUS> — Only show findings in these categories (comma-separated): bloaters, oo_abusers, change_preventers, dispensables, couplers, security

cha baseline

Generate a baseline file from current findings (suppresses known issues)

Usage: cha baseline [OPTIONS] [PATHS]...

Arguments:
  • <PATHS> — Files or directories to analyze (defaults to current directory)
Options:
  • -o, --output <OUTPUT> — Output path for baseline file (default: .cha/baseline.json)

cha parse

Parse source files and show structure

Usage: cha parse [PATHS]...

Arguments:
  • <PATHS> — Files or directories to parse (defaults to current directory)

cha init

Generate a default .cha.toml configuration file

Usage: cha init

cha schema

Print JSON Schema for the analysis output format

Usage: cha schema

cha fix

Auto-fix simple issues (naming conventions)

Usage: cha fix [OPTIONS] [PATHS]...

Arguments:
  • <PATHS> — Files or directories to fix (defaults to current directory)
Options:
  • --diff — Only fix files changed in git diff (unstaged)
  • --dry-run — Dry run — show what would be changed without modifying files

cha plugin

Manage WASM plugins

Usage: cha plugin <COMMAND>

Subcommands:
  • new — Scaffold a new plugin project
  • build — Build the plugin in the current directory
  • list — List installed plugins
  • install — Install a .wasm file into .cha/plugins/
  • remove — Remove an installed plugin

cha plugin new

Scaffold a new plugin project

Usage: cha plugin new <NAME>

Arguments:
  • <NAME> — Plugin name

cha plugin build

Build the plugin in the current directory

Usage: cha plugin build

cha plugin list

List installed plugins

Usage: cha plugin list

cha plugin install

Install a .wasm file into .cha/plugins/

Usage: cha plugin install <PATH>

Arguments:
  • <PATH> — Path to the .wasm file

cha plugin remove

Remove an installed plugin

Usage: cha plugin remove <NAME>

Arguments:
  • <NAME> — Plugin name (with or without .wasm extension)

cha deps

Show dependency graph (imports, classes, or calls)

Usage: cha deps [OPTIONS] [PATHS]...

Arguments:
  • <PATHS> — Files or directories (defaults to current directory)
Options:
  • --format <FORMAT> — Output format

    Default value: dot

    Possible values: dot, json, mermaid, plantuml, dsm, terminal, html

  • --depth <DEPTH> — Aggregation depth: "file" (default) or "dir" (imports only)

    Default value: file

    Possible values: file, dir

  • --type <TYPE> — Graph type: imports (default), classes, or calls

    Default value: imports

    Possible values: imports, classes, calls

  • --filter <FILTER> — Filter by regex pattern (shows connected subgraph)

  • --exact — Exact match: only show edges directly matching the filter

  • --detail — Show detailed class diagram (fields and methods)

  • --direction <DIRECTION> — Edge direction when filtering: in (who depends on target), out (target depends on), both

    Default value: both

    Possible values: in, out, both

cha trend

Analyze recent git commits to show issue trend

Usage: cha trend [OPTIONS]

Options:
  • -c, --count <COUNT> — Number of commits to analyze (default: 10)

    Default value: 10

  • --format <FORMAT> — Output format (terminal or json)

    Default value: terminal

    Possible values: terminal, json, llm, sarif, html

cha hotspot

Show hotspots: files with high change frequency × complexity

Usage: cha hotspot [OPTIONS]

Options:
  • -c, --count <COUNT> — Number of recent commits to analyze (default: 100)

    Default value: 100

  • -t, --top <TOP> — Show top N files (default: 20)

    Default value: 20

  • --format <FORMAT> — Output format (terminal or json)

    Default value: terminal

    Possible values: terminal, json, llm, sarif, html

cha preset

Show builtin language presets and strictness levels

Usage: cha preset <COMMAND>

Subcommands:
  • list — List all supported languages and their builtin profiles
  • show — Show plugin rules and thresholds for a specific language

cha preset list

List all supported languages and their builtin profiles

Usage: cha preset list

cha preset show

Show plugin rules and thresholds for a specific language

Usage: cha preset show <LANGUAGE>

Arguments:
  • <LANGUAGE> — Language name (rust, typescript, python, go, c, cpp)

cha layers

Infer architectural layers from import dependencies

Usage: cha layers [OPTIONS] [PATHS]...

Arguments:
  • <PATHS> — Files or directories (defaults to current directory)
Options:
  • --save — Save inferred layers to .cha.toml

  • --format <FORMAT> — Output format (dot=terminal table, mermaid, json, plantuml)

    Default value: dot

    Possible values: dot, json, mermaid, plantuml, dsm, terminal, html

  • --depth <DEPTH> — Override auto-detected directory depth for module grouping

cha calibrate

Auto-suggest thresholds from project statistics (P90=warning, P95=error)

Usage: cha calibrate [OPTIONS] [PATHS]...

Arguments:
  • <PATHS> — Files or directories (defaults to current directory)
Options:
  • --apply — Write suggested thresholds to .cha.toml

cha completions

Generate shell completion scripts (supports dynamic plugin name completion)

Usage: cha completions [SHELL]

Arguments:
  • <SHELL> — Shell to generate completions for (bash, zsh, fish, powershell, elvish)

    Possible values: bash, elvish, fish, powershell, zsh

cha lsp

Start the Language Server Protocol server

Usage: cha lsp


This document was generated automatically by clap-markdown.

输出格式

cha analyze --format <格式> 选输出。5 种格式各自针对不同消费者:

格式适用CLI 参数
terminal人眼读,本地开发默认--format terminal(默认)
json工具消费、CI 后处理、jq--format json
sarifGitHub Code Scanning、安全平台--format sarif
html静态报告分发、邮件附件--format html --output report.html
llm喂给 LLM 当上下文--format llm

JSON 的字段定义跟 JSON Schema 参考 对齐——schema 用 cha schema 拿。

通用参数

下面这几个参数对所有格式(或大多数格式)有效:

  • --output <path>, -o:写到文件而不是 stdout(HTML 这种大体积的强烈推荐)
  • --fail-on <级别>:finding 达到 hint / warning / error 时退出码 1
  • --top <N>:只看前 N 条最严重的(终端格式)
  • --all:终端格式不聚合,全部展开
  • --focus <类目>:只看指定类目(bloaters / oo_abusers / change_preventers / dispensables / couplers / security)

终端

默认格式,给人眼看的。颜色 + 表情符号区分严重度,同类 findings 自动聚合。

样例输出

ℹ [data_class] cha-core/src/cache.rs:8-15 Class `FileEntry` has 4 fields but no behavior methods, consider Move Method
  → suggested: Move Method, Encapsulate Field
ℹ [lazy_class] cha-core/src/cache.rs:8:7-8:16 Class `FileEntry` has only 0 method(s) and 8 lines, consider Inline Class
  → suggested: Inline Class
ℹ [primitive_representation] cha-core/src/cache.rs:144:11-144:15 Function `open` carries domain-named `env_hash: u64` (#2) as raw primitive type(s)
  → suggested: Replace Data Value with Object …
…

15 issue(s) found (0 error, 0 warning, 15 hint).

每行:严重度图标 / smell 名 / 路径:行 / 一句话原因。下一行 → suggested: 是推荐的 refactoring。结尾会有总数和按严重度的统计。

适用场景

  • 本地开发实时跑——cha analyze 不带 --format 就这个
  • 在 PR diff 里看新增问题(搭 --diff
  • 命令行检查 cha analyze --top 10 抓最严重那批

备注

  • 终端默认会聚合:相同 smell 在同一文件多次出现会折叠。--all 关掉聚合,--top N 只看最严重 N 条。
  • 颜色看 stdin 是否是 TTY 自动启停,pipe 给 less 时不会乱码。

JSON

机器可读格式。CI 脚本、自定义 dashboard、jq 数据加工都用这个。

样例输出

{
  "findings": [
    {
      "smell_name": "lazy_class",
      "category": "dispensables",
      "severity": "hint",
      "actual_value": 0.0,
      "threshold": 1.0,
      "risk_score": 1.5,
      "location": {
        "path": "cha-core/src/cache.rs",
        "start_line": 8,
        "start_col": 7,
        "end_line": 8,
        "end_col": 16,
        "name": "FileEntry"
      },
      "message": "Class `FileEntry` has only 0 method(s) and 8 lines, consider Inline Class",
      "suggested_refactorings": ["Inline Class"]
    }
  ],
  "summary": {
    "files_analyzed": 1,
    "total_lines": 501,
    "tech_debt_minutes": 60,
    "by_severity": { "hint": 15, "warning": 0, "error": 0 }
  }
}

字段定义遵循 JSON Schema 参考cha schema 拿原始 schema 文件)。

适用场景

  • CI 后处理:用 jq 过滤特定 smell、或者按 severity 聚合
  • 自建 dashboard:定时跑 cha 把结果存数据库
  • 跟其他工具集成:把 finding 喂进 GitHub Issues / 内部 review 平台

jq 食谱

# 只看 warning + error
cha analyze --format json | jq '.findings | map(select(.severity != "hint"))'

# 按 smell 类型计数
cha analyze --format json | jq '.findings | group_by(.smell_name) | map({smell: .[0].smell_name, count: length})'

# 拿出含 baseline 之外新问题的 file 列表
cha analyze --format json --baseline .cha/baseline.json | jq -r '.findings[].location.path' | sort -u

备注

  • 数值字段(actual_value / threshold / risk_score)只在适用时出现——纯 boolean 类型的 smell 不会有这几项
  • summary.tech_debt_minutes.cha.toml[debt_weights] 定的分钟数 × 各 severity 数量算出来

SARIF

SARIF 2.1.0 —— 静态分析结果的标准化交换格式。最大用途:上传 GitHub Code Scanning,让 finding 直接出现在 PR 的 "Files changed" 注释和 Security 标签里。

样例输出

{
  "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
  "runs": [
    {
      "tool": { "driver": { "name": "cha", "version": "1.19.0" } },
      "properties": {
        "health_scores": [
          { "path": "cha-core/src/cache.rs", "grade": "C", "lines": 501, "debt_minutes": 60 }
        ]
      },
      "results": [
        {
          "level": "note",
          "ruleId": "lazy_class",
          "message": { "text": "Class `FileEntry` has only 0 method(s) and 8 lines, consider Inline Class" },
          "locations": [
            { "physicalLocation": {
              "artifactLocation": { "uri": "cha-core/src/cache.rs" },
              "region": { "startLine": 8, "startColumn": 8, "endLine": 8, "endColumn": 17 }
            }}
          ]
        }
      ]
    }
  ]
}

level 映射:hint → note / warning → warning / error → error

适用场景

  • GitHub Code Scanning:CI 跑 cha analyze --format sarif --output cha.sarif,再用 github/codeql-action/upload-sarif 上传。finding 自动渲染成 PR 评论、Security 标签里的 alert。
  • GitLab、Azure DevOps 等也都吃 SARIF
  • 企业代码审查平台(SonarQube、Codacy 等)大多支持 SARIF 导入

CI 集成例子

- run: cha analyze --format sarif --output cha.sarif --fail-on warning
- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: cha.sarif

if: always() 让上传步骤即使前一步 --fail-on 触发了也跑——不然 finding 报告反而上传不了。

备注

  • tool.driver.name = "cha",所以 GitHub UI 里 finding 的来源会显示成 "cha"
  • properties.health_scores 是 Cha 的扩展字段(每文件一个 grade A-F + 估算技术债分钟)。SARIF 标准的消费者会忽略这块,但 cha 自己的 dashboard 用得上
  • SARIF 是 JSON 的超集,比 --format json 多了一层 schema 包装。要纯 finding 数据用 --format json

HTML

自包含 HTML 报告——单个 .html 文件,CSS/JS 内嵌,不依赖外部资源。可以直接发邮件、放静态站、当 PR 评论附件。

用法

cha analyze --format html --output report.html

--output 是必给的——HTML 报告太大,不会往 stdout 喷。

报告内容

  • 顶部 summary:总 finding 数、按严重度 / 类目分布、估算的技术债分钟、每个文件的 grade(A-F)
  • 按文件展开的 finding 列表,带源代码片段(finding 触发行高亮)
  • 按 smell 名分组的索引(点 smell 名跳到所有触发位置)
  • 按类目过滤的标签

适用场景

  • 每周 / 每月报告:CI 定时任务生成,邮件 / Slack 链接发出去
  • PR 大改动评估:本地跑一遍 HTML 给 reviewer,比让他们 checkout 看 terminal 直接
  • 当快照对比:今天的报告 vs 上月的,看 grade 变化
  • 客户 / 上级:技术债不会给非工程师看 JSON,HTML 一目了然

备注

  • 报告默认是英文界面(标签、按钮文字)。后续可能加多语言
  • 文件大小取决于 finding 数量——千百条 finding 的项目报告可能几 MB
  • 不要把含有公司源码片段的 HTML 公开放——里面会内嵌触发行附近的代码

LLM 上下文

专门给 AI / LLM 当上下文用的格式。紧凑的 markdown,没 JSON 包装,不带元数据噪音——直接复制粘进 Claude / ChatGPT / Cursor 等就能让 AI 帮分析、解释、提修复方案。

样例输出

# Code Smell Analysis

## Issue 1

- **Smell**: lazy_class
- **Category**: Dispensables
- **Severity**: Hint
- **Location**: cha-core/src/cache.rs:8:7-8:16 (`FileEntry`)
- **Problem**: Class `FileEntry` has only 0 method(s) and 8 lines, consider Inline Class
- **Suggested refactorings**:
  - Inline Class

## Issue 2

- **Smell**: lazy_class
- **Category**: Dispensables
…

适用场景

  • Code review 时让 AI 给修复建议cha analyze foo.rs --format llm | pbcopy,粘进 Claude 让它一条条改
  • 批量重构计划:把整个文件 / 模块的 finding 喂给 AI,让它估算重构工作量
  • 学习模式:AI 接手解释每条 smell 为什么是问题、Refactoring Guru 上的对应章节
  • 塞进 prompt context:MCP 工具或自动化 agent 把 finding 当输入

跟 JSON 的区别

维度--format json--format llm
体积较大(JSON 字段名 + 嵌套)紧凑(每个 issue ~6 行)
机器友好❌(结构化提取困难)
LLM 友好凑合(要让 AI 解析 JSON)✅(markdown 是 LLM 母语)
适合 jq

要给 AI 看 → llm。要给脚本看 → json

备注

  • 输出是英文的——LLM 现阶段对英文表述的 smell 名字识别更准。如果要中文 prompt,自己包一层告诉 AI "这是英文报告,请用中文回复"
  • 没有 schema——这是设计如此,给 LLM 看不需要 schema

JSON Schema

cha schema 输出一份 JSON Schema 2020-12 文档,描述的是 cha analyze --format json 的输出结构。可以用它来校验 cha 的输出、给其他语言生成类型、给读取 cha findings 的下游工具配 IDE 自动补全。

不是 .cha.toml 的 schema —— cha 的配置没有发布 schema。配置 key 看 配置项参考

生成

cha schema > cha-findings.schema.json

输出是 Vec<Finding> 的 schema,从 cha-core/src/model.rs 里的 Finding 结构体通过 schemars 自动派生而来。每次发版都会重新派生一次,schema 跟当前 Finding 结构始终一致。

怎么用

校验 JSON 输出

cha analyze --format json > findings.json
cha schema > cha-findings.schema.json

# 选一个 JSON Schema 校验器;这里举 check-jsonschema 为例:
check-jsonschema --schemafile cha-findings.schema.json findings.json

退出码 0 表示输出符合 schema。如果有下游工具依赖 cha 的 JSON 输出格式,把这一步加进 CI 能在 cha 升级导致 schema 变化时提前发现,避免下游静默坏掉。

给 IDE 配自动补全

如果你写工具或脚本直接读 findings.json,把 schema 喂给编辑器的 JSON 支持就行。VS Code:

// .vscode/settings.json
{
  "json.schemas": [
    {
      "fileMatch": ["**/findings.json"],
      "url": "./cha-findings.schema.json"
    }
  ]
}

schemastore.org 的编辑器(Helix、用 efm-langserver 的 Neovim 等)可以自己加映射。我们暂时没把 schema 提交到 schemastore,所以路径只能是本地的。

给其他语言生成类型

quicktype 吃 JSON Schema、产出 TypeScript / Python / Java / C# / Go / Rust 等:

quicktype --src-lang schema cha-findings.schema.json -o ChaFindings.ts

输出是带类型的 dataclass / interface,跟 Finding 结构对应。写 dashboard、exporter 或 LSP 旁边的工具时这种生成挺有用。

一条 Finding 长什么样

schema 描述的就是这种东西,每条分析结果一个:

{
  "smell_name": "long_method",
  "category": "Bloaters",
  "severity": "Warning",
  "location": {
    "path": "src/handlers.rs",
    "start_line": 142,
    "start_col": 8,
    "end_line": 198,
    "end_col": 1,
    "name": "process_request"
  },
  "message": "Function `process_request` is 87 lines (threshold: 50)",
  "suggested_refactorings": ["Extract Method"],
  "actual_value": 87.0,
  "threshold": 50.0,
  "risk_score": 1.74
}
字段含义
smell_namesmell ID,如 long_method。多个插件能不能共用同一个 smell 名取决于它们配合,目前没有这种配置。
category取值 Bloaters / Couplers / OOAbusers / ChangePreventers / Dispensables / Security 之一。驱动 --focus 和输出分组。
severityHint / Warning / Error。驱动 --fail-on
location文件路径 + 1-based 行范围 + 0-based 列范围。name 是出问题的符号名(函数名 / 类名);不适用时为 null。
message给人看的,带阈值和实际值。
suggested_refactorings自由形式的标签,对应 Fowler 重构目录里的条目("Extract Method""Replace Conditional with Polymorphism" 等)。
actual_value / threshold实际值和被越过的阈值。非阈值类 smell(比如 unsafe_api)下都是 null。
risk_score严重度乘以越界程度再乘以结构复杂度因子(取该 finding 所在函数 / 类的相对复杂度)。cha trend 子命令在跨 commit 跟踪时用它给 finding 排序。不适用时为 null。

schema 文件本身把这些字段标成 required 或 optional,并给 categoryseverity 列出可取的枚举值。

不走这套 schema 的输出格式

cha analyze --format json 是唯一对应这份 schema 的输出。其他格式各有自己的形状:

  • --format sarifSARIF 2.1.0。要校验请用 SARIF 工具,不是 cha schema
  • --format html 是渲染好的 HTML,不存在 schema。
  • --format llm 是 markdown,给 LLM 喂上下文用,不存在 schema。
  • --format terminal 给人看。

See also

LSP 概览

启动语言服务器:

cha lsp

服务器跑在 stdio 上,VS Code 扩展自动启动。其他编辑器见 其他编辑器

实现的能力

实际代码在 cha-lsp/src/lib.rs

生命周期

  • initialize —— 申明服务器能力。读 initializationOptions.disabledPlugins(JSON 数组),编辑器可以按工作区禁用某些插件
  • initialized —— 后台跑一遍全工作区分析:从根目录走 .gitignore 过滤后的所有源文件,解析 + 缓存
  • shutdown —— 干净退出

文档同步

  • textDocument/didOpen —— 缓存文档文本,给后续 code action 和 inlay hint 用
  • textDocument/didChange —— 全文档同步(TextDocumentSyncKind::FULL)。只更新内存里的文本,不重新分析
  • textDocument/didSave —— 触发全工作区重新分析,刷新所有 finding 和缓存

诊断

  • textDocument/diagnostic —— 单文件 pull 模式诊断。每个 finding 变成一条 Diagnosticsource = "cha"code 是 smell 名,severity 映射 hint/warning/error
  • workspace/diagnostic —— 整个项目的 pull diagnostics。让编辑器 Problems 面板不用打开每个文件就能填满

代码智能

  • textDocument/codeAction —— 两类:
    1. Quick fix ——任何带建议的 cha 诊断都会出 Refactor: <建议> 选项
    2. Extract Method —— long_method 诊断、或者用户手选 ≥3 行时出现,生成 WorkspaceEdit 把选中段抽成 extracted() 函数
  • textDocument/codeLens —— 每个函数 / 类上方一条 lens:有问题时 ⚠ N issue(s) | <行数>,没问题时 ✓ <行数>
  • textDocument/hover —— 函数 hover 出 markdown 报告卡:名字、行数、圈复杂度、认知复杂度、参数数、链深;下面是这函数的所有 finding
  • textDocument/inlayHint —— 函数签名末尾一个小标记:有问题 ⚠N,没问题
  • textDocument/documentSymbol —— 大纲视图(嵌套)。函数显示 cx:<复杂度> <行数>L,类显示 <方法数>m <字段数>f <行数>L。有 warning / error 的项前面加
  • textDocument/semanticTokens/full —— 暴露 function / class 两种 token 类型 + 一个 warning modifier。支持 semantic token 主题的编辑器能给有问题的项加高亮

没实现的

下面这些标准 LSP 请求 cha lsp 不提供:

  • textDocument/completion
  • textDocument/definition
  • textDocument/references
  • textDocument/rename
  • textDocument/signatureHelp
  • textDocument/formatting
  • workspace/didChangeConfiguration

这些通常由语言自己的 LSP 服务器(rust-analyzer / pyright / gopls 等)提供。cha lsp 跟语言 LSP 一起跑就行——多数编辑器会合并多个 server 的结果。

重新分析的触发点

全工作区重跑只发生在 didSave,不是每次按键。所以编辑过程中的诊断保存前是滞后的——这是有意设计,避免按一下键就跑一遍千文件分析。on-disk 缓存(.cha/cache/)让 warm 跑通常亚秒级。

VS Code

直接 Marketplace 装:

Cha — Code Smell Analyzer

或者命令行:

code --install-extension BenignX.vscode-cha

第一次启动

扩展激活时检查 PATH 上有没有 cha 二进制。没有就自动从 GitHub Releases 下载对应平台的 cha,存到扩展全局存储目录。不用手动装 cha

打开 Rust / TypeScript / TSX / Python / Go / C / C++ 文件时自动激活。

配置

主要靠工作区根目录的 .cha.toml(见 配置概览)。VS Code 端基本不用配。

如果需要可以改三个:

设置默认用途
cha.path"cha"改二进制路径——指向开发版或非 PATH 安装
cha.lsp.enabledtrue不卸载扩展,只关 LSP 客户端
cha.disabledPlugins["large_file"]编辑器里要屏蔽的 smell 名。large_file 默认就关着——人都进文件编辑了,再提示"这个文件太长"是噪音。觉得 todo_commenthigh_coupling 等编辑时碍眼也可以加进来;CLI 跑 cha analyze 时这些 smell 仍然会报。

故障排除

  • 看不到诊断 —— 打开 Output 面板下拉选 Cha,下载失败 / 二进制找不到 / LSP 启动错误都在那
  • cha 升级了但扩展用旧版 —— Reload window(命令面板 Developer: Reload Window)。扩展激活时锁定二进制路径
  • 想用别的二进制 —— cha.path 设绝对路径覆盖
  • 公司代理下不下来 —— 手动 装 cha,确保在 PATH 上,再 reload window

你能用的能力

LSP 概览 列的全套,落到 VS Code 里的具体形态:

能力在 VS Code 里看到什么
诊断代码下面的波浪线 + Problems 面板里的条目。严重度跟着 smell 走(Error / Warning / Information)。
Code action出问题的那行旁边出小灯泡,Cmd+.(Mac)或 Ctrl+. 触发。除了 Fowler 风格的重构建议,超长函数还能用内置的 Extract Method
Code lens函数 / 类上方一行小字标注——复杂度、行数、参数数量。
Inlay hint函数名后面跟一截灰色 cx:N cog:N NL(圈复杂度 / 认知复杂度 / 嵌套层数)。觉得碍眼可以在 Editor › Inlay Hints 设置里关掉。
Hover鼠标悬停在函数或类上,弹一份 markdown "评分卡"——指标值、越过哪些阈值。
Document symbolOutline 视图(⌘⇧O),有 finding 的 item 前面带 ⚠ 标记。
Semantic tokens出问题的函数 / 类带 warning modifier,配支持这个 modifier 的主题(比如黄色下划线)效果更明显。
Workspace diagnostics扩展激活时 cha analyze 整个项目,不用挨个打开文件 Problems 也能填满。进度在状态栏。

上面这些都不需要在 VS Code 端额外配什么。

Marketplace

Listing:https://marketplace.visualstudio.com/items?itemName=BenignX.vscode-cha

cha 一起发布——装最新扩展就能用上最新的分析器。

其他编辑器

cha lsp 是标准 stdio LSP server。任何支持自定义 LSP 服务器的编辑器都能接。下面给最小可用配置——细节查各自编辑器文档。

要让服务器接管的文件类型:rust / typescript / typescriptreact / python / go / c / cpp

Neovim(nvim-lspconfig)

local lspconfig = require('lspconfig')
local configs = require('lspconfig.configs')

if not configs.cha then
  configs.cha = {
    default_config = {
      cmd = { 'cha', 'lsp' },
      filetypes = { 'rust', 'typescript', 'typescriptreact', 'python', 'go', 'c', 'cpp' },
      root_dir = lspconfig.util.root_pattern('.cha.toml', '.git'),
      single_file_support = false,
    },
  }
end

lspconfig.cha.setup {}

跟你的语言 LSP 同时挂着用——Neovim 会合并多个 server 的诊断。

Helix(languages.toml

[language-server.cha]
command = "cha"
args = ["lsp"]

[[language]]
name = "rust"
language-servers = ["rust-analyzer", "cha"]

[[language]]
name = "typescript"
language-servers = ["typescript-language-server", "cha"]

# 同样模式扩到 python / go / c / cpp;tsx 文件 Helix 用 "tsx" 这个 name

~/.config/helix/languages.toml

Zed

Zed 用扩展系统管 LSP,最简单的是项目级配置 .zed/settings.json

{
  "lsp": {
    "cha": {
      "binary": { "path": "cha", "arguments": ["lsp"] }
    }
  },
  "languages": {
    "Rust":       { "language_servers": ["rust-analyzer", "cha"] },
    "TypeScript": { "language_servers": ["typescript-language-server", "cha"] },
    "Python":     { "language_servers": ["pyright", "cha"] },
    "Go":         { "language_servers": ["gopls", "cha"] },
    "C":          { "language_servers": ["clangd", "cha"] },
    "C++":        { "language_servers": ["clangd", "cha"] }
  }
}

完整 schema 见 Zed 文档

Sublime Text(LSP 插件)

先装 LSP 插件,然后改 LSP.sublime-settings

{
  "clients": {
    "cha": {
      "enabled": true,
      "command": ["cha", "lsp"],
      "selector": "source.rust | source.ts | source.tsx | source.python | source.go | source.c | source.c++"
    }
  }
}

selector 语法 + per-window 覆盖见 LSP 插件文档

验证连上了

打开支持的文件,故意改超阈值(比如 60+ 行函数触发 long_method 默认 50 阈值),保存——应该出诊断。

如果编辑器里没出但 cha analyze CLI 能报,问题在 LSP 客户端配置——server 跟 cha 都好的,是编辑器没把 server 的诊断接到 UI 上。

烹饪书

每一篇都从一个具体处境讲起,落到一组能直接抄走的命令或配置。

Recipe什么时候读它
从 clippy 迁移Rust 项目原本跑 clippy,现在想加上 Cha 一起跑或者替掉。
Monorepo CI一个仓多个 package,PR 通常只动其中一两个。
遗留代码豁免半路接入 Cha,CI 一上来就被一堆历史 finding 淹了。
50 行写一个插件想要一个项目专属的检测器,今天就要。
给你的项目校准阈值默认阈值要么太严要么太松。
Baseline 工作流baseline 文件的日常节奏:生成、对比、刷新。

如果你刚开始用 Cha,先去 命令行快速开始cha analyze 能跑起来,再回来看这里。

从 clippy 迁移

clippy 和 cha 回答的不是同一个问题。clippy 是 Rust 语言级 lint:它管 borrow check、idiom、生命周期陷阱。cha 看的是设计层面的问题——函数太长、一个类管太多事、一个模块太爱碰另一个模块的内部、依赖关系成了枢纽、跨层调用违规。这些在 cha 里有专门的 smell 名(long_methodgod_classfeature_envyhub_like_dependencylayer_violation),完整列表见 Smell 列表

不要用 cha 替掉 clippy,两个一起跑。下面是接入时常见的两个摩擦点。

1. 并排跑就行

cha 不读 cargo clippy 的输出,两边连配置和 lockfile 都不共享。在你已有的流程里加一行:

# 已有
cargo clippy --all-targets -- -D warnings

# 新增
cha analyze --fail-on warning

CI 里两个独立 step。clippy 在 borrow check 上挂了,cha 就不必跑;反过来,cha 抓到设计问题时,clippy 也已经过了。

2. 调一下 Rust 项目的阈值

cha 的默认阈值跟语言无关。Rust 代码里通常要松一两个:

# .cha.toml
[plugins.length]
max_function_lines = 60   # Rust 的签名 + match 分支吃行数挺快

[plugins.complexity]
warn_threshold = 12
error_threshold = 24      # match 分支多的 Rust 代码,error 调到 24 比较合理

更靠谱的做法是先跑 cha calibrate(用项目自己的统计分布给你算阈值),看看实际的 P90 / P95 是多少,再决定是用那批数还是贴着默认。详见 给你的项目校准阈值

3. clippy lint 跟 cha smell 怎么对应

绝大多数 clippy lint 在 cha 里没对应物,反过来也一样。少数有重叠:

clippy lintcha smell备注
too_many_argumentslong_parameter_listclippy 默认 7,cha 默认 5。
cognitive_complexitycognitive_complexity两边算的是同一个指标(SonarSource 提出的认知复杂度),阈值各管各的。
large_stack_arrays栈大小分析不在 cha 范围内。
mod_module_files风格问题,cha 不管。

两边都有的那几条,一般做法是 clippy 那条留着(clippy 看的是单个函数内部),让 cha 看跨函数 / 跨文件的关系。

4. 屏蔽生成代码的噪音

cha 不读 #[allow(...)]。即使生成代码在 clippy 那边贴了 #[allow],cha 这边照样会报。两种处理方式,把路径写进 exclude

exclude = ["src/generated/**", "build/**"]

或者在出问题的那个 item 顶上贴行内指令:

#![allow(unused)]
fn main() {
// cha:ignore
fn handler_generated_by_macro() { /* ... */ }
}

详见 行内指令

See also

Monorepo CI

一个仓里好几个 package,一个 PR 通常只改其中一两个。但裸跑 cha analyze 会把整棵树都走一遍,剩下那些没改的也算你头上。

下面两种做法,常常配在一起用。

做法 1:只看动过的文件

cha analyze --diff 跑工作区里改动过的文件。在 PR 流水线里,把 PR diff 喂给它:

# 本地:相对工作区改动过的文件
cha analyze --diff

# CI:把 PR diff 用管道送进来
gh pr diff "$PR_NUMBER" | cha analyze --stdin-diff --fail-on warning

--stdin-diff 接受标准 unified-diff 格式。同一份 .cha.toml 照样生效,只是文件列表收窄了。

GitHub Actions 里这么写:

- name: cha (PR diff)
  run: |
    gh pr diff ${{ github.event.pull_request.number }} \
      | cha analyze --stdin-diff --fail-on warning
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

push 到主干的 event 走全量:

- name: cha (push)
  run: cha analyze --fail-on warning

做法 2:每个 package 一份配置

monorepo 里如果是 packages/apipackages/webpackages/shared 这种结构,给每个 package 自己的 .cha.toml,按 package 跑:

for pkg in packages/*/; do
  ( cd "$pkg" && cha analyze --fail-on warning ) || exit 1
done

每份 .cha.toml 都是独立的——这是有意为之。共享库可以严一点(max_function_lines 调小),实验性的 package 可以放宽 complexity。cha 不会让子目录的 .cha.toml 自动继承父目录的设置——因为 monorepo 里不同 package 本来就应该有不同形状的约束。

组合:每包一份 baseline + PR 走 diff

能撑住规模的常见配方:

  1. 每个 package 都有自己的 .cha/baseline.json,接入 cha 那天生成一次。

  2. PR 同时用 --baseline--stdin-diff

    cha analyze --stdin-diff \
      --baseline .cha/baseline.json \
      --fail-on warning < diff.patch
    
  3. push 到主干的全量分析仍然带 --baseline——这样 PR 没动到的文件如果跑出新 finding(比如有人手动改了什么)也能被发现。

旧 finding 被 baseline 屏蔽。新 finding 在改动行上挂掉 CI。diff 之外的旧文件不再处理。

缓存

cha 把解析好的 AST 和 finding 结果缓在 .cha/cache/。在 CI 里把这个目录缓起来:

- uses: actions/cache@v4
  with:
    path: .cha/cache
    key: cha-${{ hashFiles('**/Cargo.lock', '**/package-lock.json', '**/go.sum') }}

cache key 跟着依赖锁文件走就够细了,再细也得不偿失。

See also

遗留代码豁免

半路接入 cha。第一次 cha analyze 报了 200 条 finding,一半是早就在那儿的、跟现在团队无关的代码。CI 要绿,但又不能假装代码没问题。

按优先级三件武器:baseline 表示"以后再说",行内指令 表示"这一处是合理的",配置 exclude 表示"这个路径根本不要看"。

1. 先打 baseline

把当前所有 finding 拍个快照,之后只对新 finding 失败。

cha baseline
git add .cha/baseline.json && git commit -m "Cha baseline at adoption"

CI 用:

cha analyze --baseline .cha/baseline.json --fail-on warning

引入新 finding 的 PR 会挂掉,旧的 finding 静默放过。baseline 文件本身是一组指纹(每条 finding 的稳定标识),体积小、commit 干净、技术债(debt)还掉时 diff 也读得清。

完整流程:Baseline 工作流

2. 行内指令处理特定 item

某个 item 真的有理由违规(200 行的状态机、构造函数实在需要 9 个参数):

#![allow(unused)]
fn main() {
// cha:ignore long_method
fn dispatch_state_machine(&mut self, event: Event) -> State {
    match self.current {
        // ... 200 行有正当理由的代码
    }
}
}
# cha:ignore long_parameter_list
def __init__(self, host, port, user, password, db, ssl_cert, retry, timeout):
    ...

对下一个 item 屏蔽一条、多条或全部:

#![allow(unused)]
fn main() {
// cha:ignore                        — 屏蔽所有
// cha:ignore long_method            — 屏蔽一条
// cha:ignore long_method,complexity — 屏蔽多条
// cha:set long_method=200           — 单独把这个 item 的阈值放宽
}

行内指令不会被写进 baseline 文件——它就是写在源码里的明确决定,做不做 baseline 都在那儿。当你希望豁免在 code review 里看得见时,用它。

完整语法见 行内指令

3. 配置 exclude 整个路径

某些文件 cha 根本不该看——生成代码、第三方 vendoring、测试 fixture:

# .cha.toml
exclude = [
    "vendor/**",
    "src/generated/**",
    "tests/fixtures/**",
    "node_modules/**",   # 文件遍历器本来就尊重 .gitignore,这条通常多余
]

模式是 glob。** 匹配任意层。被排除的路径根本不会被解析——比"跑了再屏蔽"便宜。

决策表

处境用什么
满地都是历史 finding,今天就要绿 CIbaseline
一个文件里一条特别顽固的 finding行内 cha:ignore
一个文件需要单独的阈值行内 cha:set
整个目录就不该被分析配置 exclude

可以叠加。baseline、行内、exclude 是三层独立机制,cha 的处理顺序是 exclude → 分析 → cha:ignore / cha:set → baseline 过滤。一条 finding 要四层全放过才会冒出来。

还债

baseline 不是"永久无视"。隔段时间:

cha baseline                       # 重新生成,捕获当前状态
git diff .cha/baseline.json        # 看少了哪些

如果 git diff 显示有条目消失,说明 debt 还掉了。如果反而出现新条目,那 CI 里的 --baseline 没生效,去查配置。

See also

50 行写一个插件

具体例子:检测函数名里带 tmptempxxx 的函数。这种项目专属的命名洁癖,没有内置 smell 能覆盖。

cha plugin new 生成的脚手架比这里真正需要的多一些。这一篇砍到 50 行 Rust 加一份 Cargo.toml。完整的开发流程参考去 插件开发

起骨架

cha plugin new no-tmp-names
cd no-tmp-names

Cargo.toml(scaffold 自动写的版本能跑,关键内容是这些):

[package]
name = "no-tmp-names"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
cha-plugin-sdk = { git = "https://github.com/W-Mai/Cha" }
wit-bindgen = "0.55"

插件主体

插件实现 PluginImpl trait:拿到 AnalysisInput(一个文件的解析结果 + 配置),返回 Vec<Finding>

src/lib.rs

#![allow(unused)]
fn main() {
use cha_plugin_sdk::{plugin, AnalysisInput, Finding, PluginImpl, Severity};

plugin!(NoTmpNames);

struct NoTmpNames;

const FORBIDDEN: &[&str] = &["tmp", "temp", "xxx"];

impl PluginImpl for NoTmpNames {
    fn name() -> String {
        "no-tmp-names".into()
    }

    fn smells() -> Vec<String> {
        vec!["tmp_named_function".into()]
    }

    fn analyze(input: AnalysisInput) -> Vec<Finding> {
        let mut findings = Vec::new();
        // input.model.functions 是这个文件里所有函数的解析结果;每个 f 是
        // FunctionInfo,带函数名、行号、参数、复杂度等。完整字段表见
        // [插件开发](../plugins/development.md#functioninfo-字段)。
        for f in &input.model.functions {
            let lower = f.name.to_lowercase();
            if FORBIDDEN.iter().any(|bad| lower.contains(bad)) {
                findings.push(Finding {
                    smell: "tmp_named_function".into(),
                    severity: Severity::Hint,
                    line: f.start_line,
                    column: f.name_col + 1,           // 1-based
                    end_line: Some(f.start_line),
                    end_column: Some(f.name_end_col + 1),
                    message: format!(
                        "函数 `{}` 的名字像临时占位——合并前给它一个像样的名字。",
                        f.name
                    ),
                    suggestion: None,
                });
            }
        }
        findings
    }
}
}

整个插件就是 PluginImpl trait 三个方法 + 一个循环。没状态、没 async、没 Result 仪式。

编译并安装

cha plugin build              # 产物在 target/wasm32-wasip2/release/no_tmp_names.wasm
cha plugin install no_tmp_names.wasm

install.wasm 拷到 .cha/plugins/(项目级)。要全局装就加 --global,落到 ~/.cha/plugins/

跑起来

cha analyze --plugin no-tmp-names src/

迭代时只跑这一个插件;下一次 cha analyze(不带 --plugin)会把所有内置插件 + 你新写的这个一起跑。

改一改再跑

改完 src/lib.rs 后:

cha plugin build
cha plugin install no_tmp_names.wasm    # 覆盖上一份 .wasm
cha analyze --plugin no-tmp-names src/

cha 会缓存解析结果加速重复分析。装了新 .wasm 之后,凡是这个插件碰过的文件,缓存都会自动作废,不用手动清。

analyze 里能拿到什么

AnalysisInput 暴露:

  • input.path —— 当前正在分析的文件路径。
  • input.model —— SourceModel,里面是解析好的函数、类、imports、注释。
  • input.options —— .cha.toml[plugins.no-tmp-names] 的值。

想跨文件查询(谁调用了这个函数、这个类型从哪来、本项目一共几个文件)或者写 tree-sitter S 表达式 query,看 插件开发

See also

给你的项目校准阈值

cha 的默认值(max_function_lines=50complexity warn=10)是从 Fowler 的《重构》和 SonarSource 这类静态分析工具里取的经验值。对绿地项目大致合适,对其他几乎所有项目都不太对。

cha calibrate 取你这个项目的实际分布作为参考,把第 90 百分位(P90)作为 warning 阈值,第 95 百分位(P95)作为 error 阈值。意思是:比项目里 95% 的代码还复杂的,会让 CI 失败;比 90% 还复杂的,会出 warning;其余安静通过。

什么时候跑

  • 在已有项目上首次接入 cha。
  • 项目长了一两个季度之后(分布会漂)。
  • 团队对某条 finding 是否"真的算个事"分歧时——让数据说话。

流程

cha calibrate

输出大概长这样:

Analyzed 1284 functions across 73 files.

Metric                    Warning(P90) Error(P95)
────────────────────────────────────────────────
long_method                       42         71
high_complexity                    8         13
cognitive_complexity              11         19

读法:"90% 的函数 ≤ 42 行,95% 的函数 ≤ 71 行",建议 warn 在 42、error 在 71。

如果数感觉对,存下来:

cha calibrate --apply

这会写出 .cha/calibration.toml,里面包含选定的阈值,以及每个指标的 P50 / P75 / P90 / P95 完整分布。下次 cha analyze 会自动读取。

优先级

cha 真正用的阈值,从强到弱:

  1. 行内指令// cha:set max_function_lines=200)。
  2. .cha.toml 里的 [plugins.<name>]
  3. .cha/calibration.tomlcha calibrate --apply 写的)。
  4. 内置默认值。

如果某个阈值在 .cha.toml 里写死了,calibrate 那个数就被忽略——这是有意的,团队明确达成的约定要赢过自动采样。

怎么读那份分布文件

.cha/calibration.toml 长这样:

[long_method]
warning = 42
error = 71
p50 = 18
p75 = 31
p90 = 42
p95 = 71

[high_complexity]
warning = 8
error = 13
p50 = 3
p75 = 5
p90 = 8
p95 = 13

[cognitive_complexity]
warning = 11
error = 19
p50 = 4
p75 = 7
p90 = 11
p95 = 19

百分位放在文件里就是给你手调用的。如果 P90 = 42、P95 = 71,中间的差值意味着有一个长尾——少数几个特别长的函数。把 error 调到 60 能逮住这个尾巴;把 warning 调到 50 让普通函数透气。

strictness 整体倍数

.cha.tomlstrictness 给所有阈值(不管是不是 calibrate 出来的)乘一个倍数:

strictness = "strict"   # 0.5×
strictness = "default"  # 1.0×
strictness = "relaxed"  # 2.0×
strictness = 0.7        # 自定义

先 calibrate 一遍,再用 strictness 整体收紧或放宽。

局限

calibrate 只采样函数级指标。类级(max_class_linesmax_class_methods)和文件级(max_file_lines)阈值不在采样范围内——手填,或者继续用默认。

See also

Baseline 工作流

baseline 是某一时刻所有 finding 的快照。cha analyze --baseline <path> 会把这份快照里的 finding 过滤掉,CI 只对快照之后才出现的 finding 失败。

这一篇是日常节奏:怎么生成、怎么对比、什么时候刷。

生成

在主干上跑一次,时机选在团队明确"这些都先放着"的那一刻:

cha baseline

默认落到 .cha/baseline.json。要换位置用 -o

cha baseline -o .cha/legacy-2026-Q1.json

文件本身是一组指纹——(路径, smell 名, 归一化后的位置)。位置之所以"归一化"是因为行号小幅移动时仍要能匹配(多加几行注释不应该让 finding "复活")。文件体积小、diff 友好,commit 进仓。

git add .cha/baseline.json
git commit -m "Cha baseline at adoption"

CI 里使用

cha analyze --baseline .cha/baseline.json --fail-on warning

baseline 里的指纹静默放过,其他 finding 正常报。新 finding 出现在改动行 → CI 失败;老 finding 继承下来 → CI 安静通过。

GitHub Actions step:

- name: cha
  run: cha analyze --baseline .cha/baseline.json --fail-on warning

对比

过几周看一下变了什么:

cha baseline -o /tmp/now.json
diff -u .cha/baseline.json /tmp/now.json | less

- 行(baseline 里有、现在没有)表示这条 finding 不见了——技术债还掉了。+ 行(现在有、baseline 里没有)正常情况下不会出现;如果出现,说明 CI 里的 --baseline 没生效,去查配置。

刷新

团队还了足够多的 debt、原 baseline 大半已经过时之后,重新生成:

cha baseline                 # 覆盖 .cha/baseline.json
git diff .cha/baseline.json  # 看少了哪些
git commit -am "Refresh Cha baseline (-32 entries)"

刷新可以按节奏(每季度是常见选择),或者在大重构落地后做一次。commit message 里写清楚少了多少条——这是个能在团队回顾会上拿出来讲的真实数字。

多 package

monorepo 里每个 package 一份 baseline:

for pkg in packages/*/; do
  ( cd "$pkg" && cha baseline )
done

每份 .cha/baseline.json 跟对应 package 放一起,CI 按 package 跑。详见 Monorepo CI

baseline 解决的事

  • 规则错了 —— 如果某条 smell 一直在你不关心的代码上炸出来,别把它埋进 baseline。要么在 .cha.toml 里调阈值,要么在那一处用 行内指令,要么把整条插件 enabled = false
  • 指纹漂移(drift) —— baseline 屏蔽的是已有 finding 的指纹。新写的代码即使出了形态完全相同的 finding,只要指纹不一字不差地命中现有条目,照样会报。换句话说 baseline 不是"silence smell × file"那种粗粒度规则,它认的是具体那一处。

See also

常见问题

Cha 跟 clippy 啥区别?

clippy 只针对 Rust,主要抓 lint 风格的问题(潜在 bug、写法建议)。Cha 多语言(Rust / TypeScript / Python / Go / C / C++),关心设计层的味道——长函数、God class、紧耦合、跨层依赖等等。两个工具互补,不冲突。

为啥是 smell 不是 lint?

lint 关心"这段代码可能有 bug",smell 关心"这段代码设计上有味道"。函数 200 行不会让程序崩,但维护起来费劲——那是味道。Cha 的目标是给"明天接手代码的人"省事,不是给 compiler 找茬。

测试 / 自动生成的文件被报了,咋办?

两条路:

  • 配置文件全局过滤.cha.tomlexclude = ["*/tests/fixtures/*", "**/generated/**"]
  • 逐项屏蔽:在文件 / 函数前面写 // cha:ignore(详见 行内指令

测试目录 cha 内置规则有些已经识别了(比如 __tests__/.test.ts)但不全。

应该 baseline 还是 fix?

  • 老代码:用 cha baseline 拍快照,老问题屏蔽,新代码不许新增 finding
  • 新写的代码:直接修,别让债累计

混合策略最实用:baseline 用来"冻结历史债",CI 拦"新增",每个迭代主动还几条老的。

strictness 怎么用?

整体乘所有数值阈值。relaxed = 2.0×、default = 1.0×、strict = 0.5×,也可以写任意浮点。详见 严格度与预设

不知道项目阈值定多少合适,跑 cha calibrate——按你项目实际分布算 P90 / P95 推荐值。

怎么禁掉某条 smell?

三种粒度:

  • 全局禁.cha.tomldisabled_smells = ["naming_too_short"]
  • 按语言禁[languages.python] disabled_smells = ["..."]
  • 逐函数 / 类禁// cha:ignore <smell-name>

Cha 会改我的代码吗?

只有 cha fix 命令会改,而且当前只支持 naming_convention 一种 smell——把不符合 PascalCase 的类名改对。其他 smell 还得手动修。

cha analyze 是只读的。

怎么写自己的插件?

写 WASM 插件,详见 插件开发指南

LSP 为啥没补全 / rename?

Cha 不是语言服务器替代品。补全 / rename / 跳转定义这些应该用语言自己的 LSP(rust-analyzer / pyright / gopls)。Cha 跟它们一起跑,专做 finding 诊断、code lens、hover 报告这些 cha 才有的事。

详见 LSP 概览 "没实现的" 一节。

大仓性能咋样?

两级缓存:L1 内存、L2 bincode 磁盘 + mtime 快路径。第二次跑同一个项目(warm)通常亚秒级。新文件 / 改过的文件才重新解析。

怎么升级?

按你装 cha 的方式来:

# Homebrew
brew upgrade W-Mai/cellar/cha-cli

# Shell 安装脚本——直接重跑覆盖
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/W-Mai/Cha/releases/latest/download/cha-cli-installer.sh | sh

VS Code 扩展自动跟最新 release。

哪种语言覆盖最好?

  • Rust / TypeScript:最成熟,所有 34 个内置插件都跑
  • Go / Python:基本完整,少数 OO 类规则不适用
  • C / C++:tree-sitter 解析支持,但内置 profile 关掉了 OO 类规则(naming / lazy_class / data_class),因为 C 是过程式语言

详见 严格度与预设 的"C / C++ profile"一节。

贡献

发 PR 之前可能用得上的三页内容。都不是必读——项目本身够小,靠读代码也能摸清楚——但每一页都能帮你少跑一轮 review。

页面什么时候读
架构改动跨 crate 边界(cha-corecha-parser、插件 runtime、LSP)时。
写一条 smell加一条新内置检测器(落到 cha-core/src/plugins/)。
发版切一个新版本。流程基本自动化,但有几条不变量要守。

本地开发:

cargo xtask ci          # 把 CI 跑的全跑一遍
cargo xtask test        # 只跑测试
cargo xtask lint        # clippy + fmt
cargo xtask analyze     # cha 自分析(每种输出格式都过一遍)

代码评审风格:commit 切小、关注点切开。bug 修复一个 commit、refactor 下一个、文档再下一个。release 流程cargo xtask release)不做 squash,干净的历史靠你自己。

架构

cha 是一个 Rust workspace,一共七个 crate。依赖方向写死了:cha-core 不依赖 cha-parser(它只通过 cha-core/src/plugin.rs 里的 trait 间接接触 cha-parser 的产物),cha-cli 依赖 cha-corecha-plugin-sdk 谁都不依赖。这个方向不能反。

Crate 关系

flowchart TB
    xtask["xtask<br/><i>CI / 发版自动化</i>"]
    cli["cha-cli<br/><i>二进制</i>"]
    core["cha-core<br/><i>分析核心</i>"]
    lsp["cha-lsp<br/><i>LSP server</i>"]
    parser["cha-parser<br/><i>tree-sitter 封装</i>"]
    sdk["cha-plugin-sdk<br/><i>guest 侧,不依赖 host</i>"]

    xtask -.-> cli
    xtask -.-> core
    cli --> core
    lsp --> core
    parser --> core
    sdk -. WASM .-> core

    classDef host fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20;
    classDef tool fill:#fff8e1,stroke:#f57f17,color:#5d4037;
    classDef guest fill:#e3f2fd,stroke:#1565c0,color:#0d47a1,stroke-dasharray:5 3;
    class core,cli,lsp,parser host;
    class xtask tool;
    class sdk guest;
Crate位置职责
cha-corecha-core/Plugin trait、Finding / SourceModel / SymbolIndex 数据模型、registry、reporter(terminal / JSON / SARIF / HTML / LLM)、WASM runtime、两层缓存。
cha-parsercha-parser/Python、TypeScript / TSX、Rust、Go、C、C++ 的 tree-sitter parser。产出 SourceModelSymbolIndex
cha-clicha-cli/CLI 二进制。子命令在 命令行参考 全列出来了。
cha-lspcha-lsp/LSP server 库 + 入口。诊断、code action、code lens、hover、inlay hint、semantic token、workspace diagnostics。
cha-plugin-sdkcha-plugin-sdk/Guest 侧库 + plugin! 宏。编译目标 wasm32-wasip2。不依赖 cha-core
xtaskxtask/cargo xtask 自动化:citestlintanalyzebumpreleasepublishdocgen-clidocs-checki18n-check
vscode-chavscode-cha/VS Code 扩展。第一次启动时自动下载匹配版本的 cha 二进制。

数据流

flowchart LR
    src["源文件"]
    parser["cha-parser"]
    model[("SourceModel")]
    cfg["config TOML"]
    analyze["Plugin::analyze"]
    findings["Vec&lt;Finding&gt;"]
    cache[("L1 内存 + L2 bincode")]

    src --> parser --> model
    model --> analyze
    cfg --> analyze
    analyze --> findings
    model --> cache
    cache -.命中?.-> analyze

    classDef store fill:#fff3e0,stroke:#e65100,color:#bf360c;
    classDef proc fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20;
    class model,cache store;
    class parser,analyze proc;

SourceModel 是统一的中间格式。每个插件拿到的都是同一份 &AnalysisContext { file, model, config }。每个文件只解析一次,结果按缓存 key 哈希后在所有插件间共享。

WASM 插件多一跳:cha-core::wasm 里的 host adapter 把 AnalysisInputAnalysisContext 的一个子集——只保留能跨 WASM 边界传递的字段,定义在 wit/cha-plugin.wit 里)序列化送过去;guest 侧的 cha-plugin-sdk 把它反序列化成 Rust 类型给插件用。

Plugin trait

内置检测器实现 cha_core::Plugin

#![allow(unused)]
fn main() {
pub trait Plugin: Send + Sync {
    fn name(&self) -> &str;
    fn smells(&self) -> Vec<String>;
    fn description(&self) -> &str;
    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding>;
}
}

WASM 插件实现 cha_plugin_sdk::PluginImpl —— 跟 Plugin 形状对应的另一个 trait,只是返回 String 不返回 &str(WIT 不支持借用类型)。cha-core::wasm 的 host bridge 让 PluginImpl 实现可以跟原生 Plugin 一起进同一个 registry。

缓存

两层,都在 cha-core::cache 里:

  • L1:进程内的 DashMap<PathBuf, CachedResult>。生命周期 = 一次 cha analyze
  • L2.cha/cache/ 下的 bincode 文件。缓存 key 是 (文件 mtime, 文件大小, 插件集合 hash, config hash)。mtime 没变就直接跳过解析。

插件集合 hash 包含已安装的 .wasm 文件 —— 装新插件 / 重装插件会自动作废这个插件碰过的缓存。

想扩什么动哪里

想做的事改哪里
加一条内置 smellcha-core/src/plugins/ 加文件 + 在 cha-core/src/registry.rs 注册
支持新语言cha-parser/src/<lang>.rs + 在 cha-parser/src/lib.rs 里映射
加一条 CLI 子命令cha-cli/src/<subcommand>.rs + 在 cha-cli/src/main.rs 里接进来
给 WASM 插件暴露新能力wit/cha-plugin.wit、重生成 binding、在 cha-core/src/wasm.rs 里实现 host adapter、再在 cha-plugin-sdk/src/lib.rs 里暴露
加 LSP 能力cha-lsp/src/lib.rs

See also

写一条 smell

下面拿 MiddleManAnalyzer源码,64 行)作为例子走一遍。一共四步:写 analyzer、选 category、注册、补测试和文档。

这一篇是写内置 smell(跟 cha-core 一起编译进 cha 的那种)。如果你只想写一条项目专属的、不进主仓的检测器,写 WASM 插件 —— 见 50 行写一个插件

第 1 步:写 analyzer

新建 cha-core/src/plugins/<your_smell>.rs

#![allow(unused)]
fn main() {
use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};

pub struct MiddleManAnalyzer {
    pub min_methods: usize,
    pub delegation_ratio: f64,
}

impl Default for MiddleManAnalyzer {
    fn default() -> Self {
        Self {
            min_methods: 3,
            delegation_ratio: 0.5,
        }
    }
}

impl Plugin for MiddleManAnalyzer {
    fn name(&self) -> &str { "middle_man" }
    fn smells(&self) -> Vec<String> { vec!["middle_man".into()] }
    fn description(&self) -> &str { "Class that only delegates to others" }

    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
        ctx.model.classes.iter()
            .filter(|c| {
                c.method_count >= self.min_methods
                    && c.delegating_method_count > 0
                    && (c.delegating_method_count as f64 / c.method_count as f64)
                        >= self.delegation_ratio
            })
            .map(|c| Finding {
                smell_name: "middle_man".into(),
                category: SmellCategory::Couplers,
                severity: Severity::Hint,
                location: Location {
                    path: ctx.file.path.clone(),
                    start_line: c.start_line,
                    start_col: c.name_col,
                    end_line: c.start_line,
                    end_col: c.name_end_col,
                    name: Some(c.name.clone()),
                },
                message: format!(
                    "Class `{}` delegates {}/{} methods, acting as a middle man",
                    c.name, c.delegating_method_count, c.method_count
                ),
                suggested_refactorings: vec!["Remove Middle Man".into()],
                actual_value: Some(c.delegating_method_count as f64 / c.method_count as f64),
                threshold: Some(self.delegation_ratio),
                risk_score: None,
            })
            .collect()
    }
}
}

约定:

  • struct 字段就是阈值。 analyze() 里不要出现魔法数字。默认值放 Default
  • name() 是插件标识。--plugin <name>[plugins.<name>] config、// cha:ignore <name> 里都是它。
  • smells() 列出这个插件会产出的所有 smell 名。 大多数插件只有一条 smell 跟 name() 同名;少数会产出多条(比如 length 一个插件出 long_method / large_class / large_file)。
  • 严重度Severity::Hint 给纯风格层面的发现,Warning 给确实会伤害可读性 / 正确性的,Error 给 CI 应该拒绝的。
  • actual_valuethreshold 是数值字段,message 文案和 --explain 都会用上。只要 smell 有数值指标就填。

第 2 步:选 SmellCategory

类别决定 CLI 输出、JSON report 和 --focus 的分组。按 smell 真实形态选:

Category装什么
Bloaters长出来的代码(long_methodgod_classcomplexity)。
Couplers模块之间耦合过紧(couplingfeature_envymiddle_man)。
OOAbusers面向对象构造用错地方(switch_statementrefused_bequestdesign_pattern)。
ChangePreventers一处修改逼迫多处修改(shotgun_surgerydivergent_change)。
Dispensables删了不影响功能的(dead_codeduplicate_codelazy_class)。
Security危险调用 / 泄露的密钥(hardcoded_secretunsafe_api)。

如果一条 smell 看着横跨两个 category,选更具体的那个——SmellCategory 是个枚举,一条 finding 只能挂一个。

第 3 步:注册

cha-core/src/plugins/mod.rs

#![allow(unused)]
fn main() {
mod middle_man;
pub use middle_man::MiddleManAnalyzer;
}

cha-core/src/registry.rs,找到对应 category 的 register_*_plugins 函数,加上:

#![allow(unused)]
fn main() {
register_if_enabled(plugins, config, "middle_man", || {
    let mut p = MiddleManAnalyzer::default();
    apply_usize(config, "middle_man", "min_methods", &mut p.min_methods);
    apply_f64(config, "middle_man", "delegation_ratio", &mut p.delegation_ratio);
    Box::new(p)
});
}

apply_*.cha.toml[plugins.middle_man] 的配置覆盖到默认阈值上。analyzer 没有可配字段就不写 apply_*

register_if_enabled 自己会处理 [plugins.middle_man]enabled = false 的情况,你不用管。

第 4 步:测试 + 文档

新建 cha-core/src/plugins/<your_smell>_tests.rs(或者塞进现有测试文件)。模板:

#![allow(unused)]
fn main() {
#[test]
fn fires_on_middle_man() {
    let src = r#"
        class Wrapper {
            fn foo(&self) { self.inner.foo() }
            fn bar(&self) { self.inner.bar() }
            fn baz(&self) { self.inner.baz() }
        }
    "#;
    let findings = analyze_with(MiddleManAnalyzer::default(), "rust", src);
    assert_eq!(findings.len(), 1);
    assert_eq!(findings[0].smell_name, "middle_man");
}

#[test]
fn does_not_fire_below_threshold() {
    // ... 2 个委托方法,默认 min_methods=3
}
}

测试粒度要细:一条测"该报的有报",一条测"低于阈值不报",每个值得测的边界一条。cha-core/tests/fixtures/ 下的 fixture 测试是用来测跨插件交互的,单插件单元测试用内联源码字符串更清楚。

然后改三处文档:

  1. README.md 插件表 —— 在对应 SmellCategory 段加一行,写 smell 名、默认阈值、严重度。README.zh-CN.md 同步加。
  2. docs/plugins.md —— 写完整描述,附一个能触发它的代码示例。docs/plugins.zh-CN.md 同步加。
  3. CHANGELOG.md[Unreleased] —— 在 "Added" 下加一行。

book 里的插件参考页是通过 {{#include}}docs/plugins.md 的,不用单独改。

自验

cargo xtask ci   # 跑 build + test + lint + analyze

接着 dogfood —— 用新插件分析 cha 自己的代码:

cargo run -- analyze --plugin middle_man cha-core/

如果它在 cha 自己的代码上报 finding,得做判断:是真问题(去修 cha),还是误报(收紧 analyzer)。

See also

发版

发版流程是一条命令 —— cargo xtask release。它会推、等 CI、打 tag、等 release workflow,最后发到 crates.io。这一篇写两件事:按这条命令前要确认的几条不变量;中途出错时要怎么处理。

发版前 checklist

cargo xtask release 之前:

  • 工作树干净(git status --short 没有输出)。release 脚本不干净就不跑。
  • 你在 main,且与 origin/main 同步。
  • CHANGELOG.md 里的 [Unreleased] 已经写好这次的变更内容。release 脚本不会自动生成 release notes —— 文件里写什么、release 出什么。
  • cargo xtask ci 本地跑过。
  • cargo xtask analyze 通过(cha 自分析)。

然后 bump:

cargo xtask bump <major|minor|patch>

这会改 workspace 里所有 Cargo.tomlversion,刷新所有 Cargo.lock,同步 vscode-cha/package.json 的版本号。单独一个 commit

git add -p             # -p 是 patch 模式,让你逐 hunk 选择要 stage 的内容;只挑 bump 相关的几行
git commit -m "🔖: bump version to x.y.z"

bump 不会把 [Unreleased] 内容自动搬到一个版本号 section。你要在同一个 commit 里手动搬:

 ## [Unreleased]

+## [1.20.0] - 2026-06-04
+
 ### Added
 - ...

新 section 要带日期和版本号,空的 [Unreleased] 留在最上面。

发版

cargo xtask release

它做的事情,按顺序:

  1. 检查工作树是否干净。
  2. 读 workspace 版本,算出 tag(v<version>)。
  3. git push origin main
  4. ci.yml 在这次 push 的 sha 上通过,最多 20 分钟。
  5. 创建并推 tag(v<version>)。
  6. release.yml 跑完,最多 30 分钟(它跑 cargo-dist 出各平台二进制和 installer,再挂到 GitHub release 上)。
  7. 按依赖顺序对每个 crate 跑 cargo publish

脚本是分步幂等的:前面任何一步挂掉,修完都可以从头再跑。第 4 步 CI 挂了的话 tag 还没打,修完再跑一次 cargo xtask release 就行——版本不变,等下一次 CI 通过即可。

中途出错怎么办

push 之后 CI 挂了(第 4 步): push 已经发生,但 tag 还没打。修完再 cargo xtask release

release workflow 挂了(第 6 步): tag 已经存在。release.yml 可以从 GitHub UI 重新跑(gh run rerun <id>)。如果挂的是你的代码问题,需要切一个新版本 —— 重新 bump、从第 1 步重来。绝对不要在同一个版本号上重打 tag:cargo-dist 的 installer 把版本号写死在脚本里,重打 tag 会让已经下载过 installer 的用户和现在的产物对不上号、静默坏掉。

cargo publish 挂了(第 7 步): 有些 crate 可能已经发出去了,有些没。再跑 cargo xtask publish(不带 release)就行。crates.io 对已发布的版本会直接拒绝、不会重复发布,所以重跑是安全的。

release.yml 产出什么

  • 各平台二进制:macOS aarch64 / x86_64、Linux aarch64 / x86_64(musl + gnu)、Windows x86_64。
  • Installer:shell(cha-cli-installer.sh)、PowerShell(cha-cli-installer.ps1)、Homebrew tap entry。
  • Release notes:从 CHANGELOG.md 抽出本版本对应内容。
  • 所有产物挂在 v<version> 这个 GitHub release 上。

发版后

  • cargo xtask release 在本地除了打那个 tag 之外不写任何东西,不需要追加 commit。
  • 看一眼 Marketplace,10 分钟左右会更新。如果没动,去 vscode-publish.yml workflow 看日志 —— 它是在 tag 触发时自动发的。
  • README、pre-commit / GitHub Action 片段里写死的版本号要不要更新自己定。当前那些写的是 v1.19.0;如果你出了 v1.20.0,要么改文档,要么接受用户拷过去的版本号会暂时落后一档。

Yank

如果一个版本被发现有严重问题:

cargo yank --version 1.20.0 cha-cli
cargo yank --version 1.20.0 cha-core
# ... 每个 crate 都来一遍

yank 不是删除,crate 还在;只是 cargo 在解析依赖时不会再选中这个版本。GitHub release 的 tag 和二进制都还在。修完之后切个 1.20.1 出去。

See also

学术参考

Cha 的检测器不是凭空拍脑袋。下面是源码中真正引用了的文献——按 detector 分组,每条说明哪条 smell 用到。

God Class / Brain Method

M. Lanza, R. Marinescu. Object-Oriented Metrics in Practice: Using Software Metrics to Characterize, Evaluate, and Improve the Design of Object-Oriented Systems. Springer, 2006. doi:10.1007/3-540-39538-5.

  • 第 6.1 章——god_class 的检测策略 (ATFD > Few) AND (WMC ≥ VeryHigh) AND (TCC < 1/3),阈值取自 45 个 Java 项目的统计
  • 第 6.2 章——brain_method 的多指标组合:长 + 复杂 + 外部引用多

Cognitive Complexity

G. A. Campbell. Cognitive Complexity: A new way of measuring understandability. SonarSource White Paper, 2017. https://www.sonarsource.com/resources/white-papers/cognitive-complexity/.

  • cognitive_complexity 算法基础——衡量"读着累不累",对嵌套加权惩罚

Error Handling

G. Padua, W. Shang. Revisiting Exception Handling Practices with Exception Flow Analysis. Empirical Software Engineering, vol. 23, no. 6, pp. 3337–3383, 2018. doi:10.1007/s10664-018-9601-8.

A. Rahman, C. Parnin, L. Williams. The Seven Sins: Security Smells in Infrastructure as Code Scripts. ICSE 2019, pp. 164–175. doi:10.1109/ICSE.2019.00033.

  • error_handling —— 空 catch、unwrap 滥用的检测启发自这两篇

Hub-Like Dependency

F. Arcelli Fontana, I. Pigazzini, R. Roveda, M. Zanoni. Architectural Smells Detected by Tools: a Catalogue Proposal. ECSA 2019. doi:10.1145/3344948.3344982.

R. C. Martin. Agile Software Development: Principles, Patterns, and Practices. Prentice Hall, 2003. ISBN: 978-0135974445. 第 20 章 Stable Dependencies Principle

  • hub_like_dependency —— 高扇出导致的"枢纽节点"识别 + Stable Dependencies 原则

Unsafe API

CWE-676: Use of Potentially Dangerous Function. https://cwe.mitre.org/data/definitions/676.html.

  • unsafe_api 危险调用清单(eval / exec / system / sprintf / strcpy / strcat / gets / unsafe / innerHTML 等)的依据

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

[1.20.0] - 2026-06-05

🎉 First minor since v1.0 — and the first release with a proper home on the web. https://cha.to01.icu is now a real product page, not just a README dump. Star the repo, share the link, and tell your linter Cha said hi.

The analyzer itself didn't move — every --version-visible behavior is identical to v1.19.0. What landed is the documentation, presentation, and integration story: somewhere we can actually point new users.

Added

  • Bilingual documentation site at https://cha.to01.icu — landing page (oranda) plus a full mdbook tree at /book/ (English) and /book/zh-CN/ (中文). 60+ pages covering install, quick-start, every CLI subcommand, every output format, LSP integration for VS Code / Helix / Neovim / Zed, configuration reference, JSON Schema, plugin development, six worked recipes (migrate from clippy, monorepo CI, suppress in legacy code, custom plugin in 50 lines, calibrate to your codebase, baseline workflow), three contributor guides (architecture, writing a smell, releasing), and an academic-references page tracing every smell back to its source.
  • Cookbook recipes — six task-oriented walkthroughs: migrate-from-clippy, monorepo-ci, suppress-legacy, custom-plugin-50loc, calibrate, baseline. Each starts with a problem statement and ends with copy-pasteable commands. Authored in English and Chinese (the Chinese versions are written natively, not translated, so the prose style matches the rest of zh-CN).
  • Contributor guidescontributing/architecture.md documents the seven-crate workspace and data flow with mermaid diagrams; writing-a-smell.md walks through MiddleManAnalyzer as the worked example; releasing.md is the runbook for cargo xtask bumprelease.
  • Configuration reference — every .cha.toml key, grouped by plugin, with thresholds and defaults sourced from cha-core/src/plugins/. JSON Schema reference page documenting cha schema output and how to wire it into IDEs.
  • Bilingual landing page (LANDING.md) — hero with logo + tagline + three CTAs, six feature cards (detectors, WASM SDK, LSP, git-aware analysis, output formats, two-level cache), 30-second get-started block, smell-category table, and editor integration links. Replaces the previous "README dumped into the homepage" experience.
  • CJK-aware search via Pagefind 1.4 — replaces mdbook's bundled elasticlunr (which silently dropped non-ASCII tokens, leaving the zh-CN tree un-searchable). Press s or / on any docs page to open a modal that indexes both language trees.
  • Per-page social meta + branded OG card — every page emits og:title / og:image / twitter:card so Slack / iMessage / 微信 link previews show a real card instead of a 32×32 favicon. The 1200×628 card embeds the Cha logo and is regenerable via python3 static/gen_og_card.py.
  • Custom 404 page — site-wide warm-themed 404 with the Cha logo and shortcuts back into the live parts of the site, replacing GitHub Pages' generic gray 404.
  • Language switcher in mdbook header — the toolbar globe icon switches between EN and zh-CN trees and remembers per-page equivalents.
  • xtask docs-check — verifies every page referenced in book/src(-zh-CN)/SUMMARY.md actually exists; runs as part of cargo xtask ci so a broken SUMMARY can't ship.
  • xtask i18n-check — flags zh-CN pages whose git ctime trails their English counterpart, surfacing translation drift.
  • xtask docgen-cli — generates book/src/reference/cli-manual.md from cha help-markdown so the CLI manual page in docs always matches the binary's actual --help. Hidden cha help-markdown subcommand added for this.
  • VS Code extension page — full inventory of what each LSP capability looks like inside VS Code (wavy underlines, lightbulbs, code-lens overlays, inlay hints, hover cards, semantic-token modifier, status-bar workspace scan progress) plus the cha.disabledPlugins setting documentation.

Fixed

  • tree-sitter S-expression query link — the link in docs/plugin-development.md and the zh-CN translation pointed at /syntax-highlighting/queries, which 404s on the current tree-sitter docs site. Updated to /using-parsers/queries/.
  • docs/plugin-development.md FunctionInfo / ClassInfo field tables — were bare struct dumps; now per-field semantic tables (type + what each field actually drives) for both languages.
  • README plugin table — every entry now has an anchor link into docs/plugins.md's detailed description, in both README.md and README.zh-CN.md. The previous UnstableDependency row (which never matched a real detector) was removed and async_callback_leak added.

Changed

  • Project homepagehttps://cha.to01.icu is the canonical entry point. README still works on github.com but now points at the docs site for anything beyond the quick-start.
  • CI — the Web workflow builds the EN tree via oranda, re-builds the zh-CN tree via mdbook with MDBOOK_BOOK__SRC=src-zh-CN, indexes both with Pagefind, and restores public/CNAME + drops public/404.html after oranda's clean. Deploys via JamesIves/github-pages-deploy-action@v4.6.4.

Notes for upgraders

Nothing to do. cha analyze produces the same output, .cha.toml accepts the same keys, every CLI flag still works. This is a documentation release: the binary moves from "had a single README" to "had a 60-page bilingual docs site" without changing anything you'd notice from the terminal.

If you want to celebrate by reading something other than --help: https://cha.to01.icu.

[1.19.0] - 2026-05-22

Hardcoded thresholds and keyword lists become plugin config; cha fix stops hardcoding smell names; the last two text-scanning detectors switch to AST queries.

Added

  • Plugin::try_fix(finding, ctx) -> Option<Patch> — every plugin can now contribute auto-fixes. cha fix walks all enabled plugins and asks each one. Adding fix support for a new smell is one trait override, not a host-side if smell_name == ... patch.
  • cha_core::Patch / cha_core::TextEdit — public byte-range edit types for plugin authors. Edits within a single finding are applied in reverse byte-offset order.
  • DesignPatternAdvisor config — 8 magic-number thresholds (strategy_min_arms, state_min_arms, builder_min_params, builder_alt_min_params, builder_alt_min_optional, null_object_min_count, template_min_self_calls, template_min_methods) and 2 keyword lists (type_field_keywords, state_field_keywords) are now overridable via [plugins.design_pattern].
  • GodClassAnalyzer::min_tcc — Tight Class Cohesion threshold (Lanza-Marinescu's 1/3) is configurable.
  • cache.rs walker skip-dirs — extended from {target, node_modules, dist} to also skip build, out, __pycache__, venv, .venv, vendor. Reduces unnecessary .cha.toml discovery work in polyglot repos.

Fixed

  • switch_statement / message_chain: replaced bespoke text scanners (find_switch_keyword, walk_chain and ~140 lines of hand-rolled tokenization) with tree-sitter queries against switch_statement / match_expression / field_expression / member_expression / selector_expression. Smell counts unchanged, but keyword positions are now sourced from the AST. No more false positives on keywords inside strings.
  • inappropriate_intimacy import resolution: extension probe expanded from {.ts, .tsx, .rs} to also include .py, .go, .cpp, .cc, .cxx, .c, .h, .hpp, .hxx, .js, .jsx, .mts, .cts. Sibling-file lookups in non-JS/Rust projects no longer silently fail.
  • calibrate.rs table rendering: 3 metric labels were hardcoded across 3 separate println blocks. Adding a calibration metric now means one entry in a (label, samples) array.

Changed

  • cha fix delegates to Plugin::try_fix for every finding — no more filter(|f| f.smell_name == "naming_convention") on the host. Existing behavior preserved (NamingAnalyzer fixes naming_convention PascalCase violations); other plugins return None until they opt in.

[1.18.0] - 2026-05-22

Built-in detectors now use AST queries instead of text scanning. Several core plugins previously did substring matches that misfired on strings, comments, and unrelated identifiers.

Added

  • cha_core::query — host-side tree-sitter query helper (run_query / run_queries / node_to_match). Both built-in plugins and the WASM tree_query host import now go through this single API.
  • DeadCodeAnalyzer::entry_points — entry-point names are now configurable via [plugins.dead_code] entry_points = [...]. Default list expanded from Rust-only (5 names) to multi-language (Rust + Python __init__ etc + Go init + C _start + tokio).
  • LengthAnalyzer::complexity_factor_threshold — was hardcoded 10.0, now configurable via [plugins.length].

Fixed

  • unsafe_api: rewritten from line-based line.contains + odd-quote-count heuristic to per-language tree-sitter queries. Picks up real sprintf/strcpy/strcat/system call sites that the line-based heuristic missed. Comments and string literals containing keywords like unsafe no longer false-positive.
  • dead_code: substring is_in_file_referenced replaced with AST identifier scan. Token-concat macro detection rewritten — instead of nuking the entire file when any #define ... ## exists, parse define bodies for prefix##X##suffix slots, scan call sites for invocation arguments, synthesize plausible expansion names, and add them to the reference set. X-macro dispatch tables (e.g. STYLE_DEF) no longer hide every dispatch function. IdentifierPositions lookup is now O(1) per symbol via HashMap<name, Vec<line>>.
  • error_handling: unwrap_abuse uses tree-sitter ((call_expression field_expression unwrap|expect)); empty-catch detection is per-language (Rust skipped, TS catch_clause, Python except_clause). String literals and comments containing the substring unwrap or catch no longer trigger.
  • hardcoded_secret: regex matches now run against string_literal node text only, not full source lines. Comments and identifier names with secret-like substrings no longer false-positive.
  • cha fix: String::replace whole-content substitution replaced with tree-sitter identifier-node range collection + byte-offset reverse substitution. The previous implementation could rewrite identifier names inside string literals and comments, corrupting source files.
  • git_metrics::check_test_ratio: f.contains("test") || f.contains("spec") replaced with cha_core::is_test_path. The substring check wrongly counted request.rs / spectrum.rs etc. as test files, polluting the test-to-production ratio that drives low_test_ratio.
  • wasm.rs::infer_file_role: replaced duplicate test-path heuristics with cha_core::is_test_path. WASM plugins' FileRole::Test classification now matches the canonical convention used elsewhere (__tests__/, __mocks__/, .test.ts, .spec.ts).
  • find_macro_invocation_args: word-boundary check added — STYLE_DEF no longer matches STYLE_DEFINE invocations.

Removed

  • unsafe_api is_in_string heuristic — superseded by tree-sitter queries that distinguish string literals at the AST level.
  • error_handling line-based detect_empty_catch — replaced with grammar-aware queries.
  • HostState::query_cache — query compilation now lives in cha_core::query (compile-on-demand; LRU caching to be added if measurement warrants).

[1.17.0] - 2026-05-21

Added

  • project_query::function_at(path, line, col) — new host import returning the FunctionInfo whose body contains the given position. Useful for tree-query–driven detectors that need to disambiguate which declared function a queried position belongs to.
  • WasmPluginTest::option_list / option_bool / option_int / option_float — list and typed option setters in the test harness, replacing the previous string-only option().

Changed (breaking for WASM plugins)

  • tree_query::QueryMatch.start_line / end_line are now 1-based (was 0-based). Aligns with FunctionInfo / ClassInfo / CommentInfo line numbering — no more per-plugin off-by-one conversion. Inputs to node_at(line, col) and nodes_in_range(start, end) are likewise 1-based now.
  • Existing plugins compiled against the pre-1.17 WIT will need to be rebuilt against the new SDK; instantiation will fail loudly otherwise.

Fixed

  • react-hooks example plugin — false positives on hook_after_early_return (in sibling components and inside return expressions like return useState()) eliminated by switching to project_query::function_at for host-function disambiguation. Now reports 5 true positives / 0 false positives on the 6-component .tsx fixture (was 5 / 2).

Documentation

  • docs/plugin-development.md: added Line/Column convention note, Project Query API section, WASM Compatibility Cheatsheet (regex panics, no clock, no FS), cha plugin build vs cargo build distinction, and new option helpers in Testing.

[1.16.0] - 2026-05-21

Added

  • TsxParser in cha-parser.tsx files now route to a parser using tree_sitter_typescript::LANGUAGE_TSX, so JSX nodes (jsx_element, jsx_attribute, jsx_self_closing_element) are first-class AST citizens. WASM plugins can now match them via tree_query::run_query.
  • examples/wasm-plugin-react-hooks — example WASM plugin demonstrating tree_query integration. Detects 5 React Rules of Hooks violations: hooks called from non-component functions, hooks in conditionals, hooks in loops, hooks after early return, and hooks in nested callbacks.
  • examples/wasm-plugin-todo-tracker — example WASM plugin demonstrating extended TODO comment tracking beyond the builtin todo_tracker. Adds 5 new smells: extended tag set (BUG/WIP/OPTIMIZE/PERF/DEPRECATED + user-configurable extras), (by:YYYY-MM-DD) expiration, priority escalation (!/!!/!!!), per-file TODO hotspot detection, and required-attribution policy.

Notes

  • WIT unchanged at cha:plugin@0.3.0 (no breaking change).
  • Routing for .ts / .mts / .cts continues to use LANGUAGE_TYPESCRIPT. Only .tsx switched.

[1.15.0] - 2026-05-14

Added

  • ProjectQuery trait in cha-core — plugins now access cross-file data through a typed interface on AnalysisContext.project instead of host-side post-hoc string-matched filtering. 12 methods cover the project-level queries existing post-analysis passes need: is_called_externally, callers_of, function_home/function_by_name/class_home, is_third_party, workspace_crate_names, is_test_path, etc. WASM plugins also gain access via the project-query host import.
  • ProjectQueryBulk trait extends ProjectQuery for in-process iteration (iter_models); not exposed to WASM.
  • cha_core::is_test_path — public utility consolidating two duplicated implementations.
  • example-wasm unused_helper smell — demonstrates project_query::callers_of callback.

Changed

  • WIT bumped to cha:plugin@0.3.0 (breaking) — adds project-query host import. External plugins compiled against 0.2.0 must rebuild.
  • large_api_surface C/C++ heuristics.h/.hpp headers are now skipped (their 100% public surface is by design); .c/.cpp implementation files use a higher count threshold (30, configurable as c_max_exported_count) and the ratio gate is effectively off (configurable as c_max_exported_ratio). lvgl baseline: 393 → 34 findings (-91%).
  • dead_code is now project-aware — uses ProjectQuery::is_called_externally to confirm cross-file usage; the per-file text search is just an early shortcut. The token-concat macro heuristic (#define ... ##) remains because parsers don't macro-expand. lvgl baseline: 67 → 6 findings (-91%).

Removed

  • Plugin::cross_file_aware_smells trait method — replaced by typed query through AnalysisContext.project.
  • cha-cli::cross_file_filter module — the post-hoc string-matched filter is gone; plugins produce final findings using the typed trait.
  • 3 duplicated workspace_crate_names impls + 3 duplicated is_third_party/is_external_leak impls + 2 duplicated is_test_path impls — all consolidated.

Added

  • New .cha.toml config keys for api_surface: max_exported_ratio, c_max_exported_count, c_max_exported_ratio, skip_c_headers. All language-aware defaults preserved.

[1.14.0] - 2026-05-14

Added

  • Plugin AST Query API — WASM plugins can now execute tree-sitter queries against the current file's AST via the tree-query host import interface (run-query, run-queries, node-at, nodes-in-range). Enables plugins to do custom structural pattern matching without reimplementing parsing.
  • file-role enum in analysis-input — host infers whether a file is source, test, doc, config, or generated from its path, allowing plugins to apply differential detection strategies.
  • SourceModel enrichmentanalysis-input now includes comments, type-aliases, parameter-names, switch-arm-values, and is-module-decl fields previously only available to internal plugins.
  • parse_file_full() in cha-parser — returns ParseResult carrying model + tree-sitter Tree + Language for downstream use by WASM host callbacks.

Changed

  • WIT bumped to cha:plugin@0.2.0 — breaking change: plugins compiled against 0.1.0 must be recompiled. No behavioral change for existing internal plugins.

[1.13.1] - 2026-04-30

Added

  • abstraction_leak_surgery detector — files that co-change in git history and share a third-party type in their function signatures. Upgrade of the classic shotgun_surgery: instead of "these files always change together" (agnostic of why), this pinpoints "these files always change together because they all depend on the same external type" — the shared external type is the concrete abstraction leak driving the co-change cascade. Severity Hint.
    • Inputs: git co-change counts (git log --name-only -N, threshold ≥ 5 commits in last 100) × per-file TypeOrigin::External sets derived from parameter / return types. Workspace-sibling crates auto-whitelisted (same mechanism cross_boundary_chain / leaky_public_signature use), so cha_core-internal dependencies between cha-parser / cha-cli don't fire.
    • Cha self-baseline: 10 genuine findings, all pointing at the 5 language parsers sharing tree_sitter::Node — exactly the abstraction leak the detector is designed to find (tree-sitter upgrades ripple across every parser file). lvgl src/: 0 (C project, no External origins).

[1.13.0] - 2026-04-30

Added

  • primitive_representation detector (roadmap S8.2). Flags function parameters whose name carries a domain concept (user_id, email, status_code, api_url, password, language, …) but whose type is a raw scalar primitive (String, i32, bool, char, …). Signals an opportunity to introduce a newtype / value object to preserve the invariant. Per-parameter detection groups all offending params of one function into a single hint. Complements the existing primitive_obsession (which looks at per-function ratio): this fires on even a single param when it's clearly a business concept.
    • Business-token and noise-token vocabularies are deliberately narrow to keep signal-to-noise high. Substring matches are ruled out (tokens must be standalone words — widget_identifier does not trigger on id).
    • Parameters already typed with project-local newtypes (e.g. id: UserId where UserId is TypeOrigin::Local) are skipped — the author already did the right thing.
    • Container types (Path, PathBuf, Vec, Arc, Box, HashMap, …) are treated as domain-carrying and excluded; wrapping path: &Path in a newtype would destroy the abstraction.
    • Only runs on is_exported functions — private helpers are noise for a design signal aimed at public API hygiene.
    • Cha self-analyze: 14 findings (all genuine — rel_path/env_hash/language/key/hash as raw types). lvgl src/ baseline: 53 findings (TTF platformID/encodingID/languageID/nameID: int, file-explorer path/dir: char pointers, …).
  • stringly_typed_dispatch detector (roadmap S8.8). Flags functions whose switch/match body dispatches on ≥ 3 string or ≥ 3 integer literal arms — classic "the arm values should have been an enum" smell. Char-literal arms (C tokenisers) skipped. Enum-variant / structural-pattern arms classify as Other and never contribute to the threshold, so match event { Event::Click => …, Event::Scroll => …, _ => … } stays quiet while match s { "click" => …, "scroll" => …, "submit" => … } fires. Severity Hint. Complements S8.2 primitive_representation (signature side) with the body-side dispatch signal.
    • New cha_core::ArmValue enum (Str / Int / Char / Other) + FunctionInfo.switch_arm_values + FunctionSymbol.switch_arm_values. Populated by every parser via a new shared cha-parser/src/switch_arms.rs helper — language-specific arm-node kinds funnel through one classifier.
    • Cha self-baseline: 20 findings (all node-kind dispatchers in the 6 language parsers — valid detections, users can add // cha:ignore stringly_typed_dispatch if the dispatch shape is forced by tree-sitter). lvgl src/ baseline: 23 findings (PNG/JPEG/QR error-code dispatchers, color-format size tables, TTF bytecode interpreter).
  • cross_boundary_chain detector (roadmap S8.U4). Flags functions where chain_depth ≥ 3 and the chain's root parameter is externally-typed (TypeOrigin::External(crate)) — the function is reaching into a third-party library's internal field layout, not just over-chaining local data. Companion to the existing message_chain (which fires on depth regardless of source): cross_boundary_chain is narrower but a stronger abstraction-leak signal. Severity Hint.
    • Workspace crates are auto-whitelisted (same mechanism leaky_public_signature uses), so sibling cha_core::Finding traversals inside this repo don't fire. Cha self-baseline: 4 findings, all genuine tree_sitter::Node traversals in cha-parser. lvgl src/ baseline: 0 (C project, few External origins by design).
    • Zero parser changes — reuses chain_depth, parameter_types (with origin), parameter_names, external_refs. Pure post-pass on ProjectIndex.
  • FunctionInfo.parameter_names + FunctionSymbol.parameter_names (cha-core). Parallel to parameter_types: identifier names in declaration order. All six parsers (Rust / TS / Python / Go / C / C++) extract these; self / C++ this positions skipped to stay length-aligned with parameter_types. Enables name-semantic analyses like primitive_representation, future LSP hover with full signatures, future cha summary.
  • New helpers cha_parser::rust_imports::rust_param_names and cha_parser::cpp::c_param_name extract identifier names from their language's declarator chains; reused across all C/C++ function-definition sites.

[1.12.0] - 2026-04-28

Added

  • SymbolIndex — structural view of a file, cached separately from SourceModel. New type in cha-core::model carrying the fields consumers like cha deps, LSP workspace-symbols, and future cha summary all share — class/function names + signatures + positions + type_aliases — without per-function-body data (complexity, body hash, TypeRef origin, cognitive, chain depth etc. stay in SourceModel).
    • ProjectCache::{get,put}_symbols store to symbols/{chash}.bin, mirrored independently of parse/{chash}.bin. Same env_hash mechanism invalidates both on parser code changes.
    • cached_symbols(path) is a new warm fast path that skips SourceModel deserialisation entirely — symbols/{chash}.bin is roughly 10% the size of parse/{chash}.bin.
    • cached_parse now populates both caches on every fresh parse, so the two views are always in lockstep.
    • lvgl src/ warm benchmarks (379 files): deps --type imports 1.28s → 38ms (34×), --type classes 1.30s → 56ms (23×), --type calls 1.30s → 48ms (27×). Edge counts unchanged vs. pre-migration (1351/142/8109).
    • cha-cli/src/c_oop_enrich grows a enrich_c_oop_symbols / attribute_methods_by_name_from_symbols pair alongside the existing SourceModel functions. Shared attribute_one_raw keeps attribution rules single-sourced; build-index / write-back are deliberate parallel code paths because the two storage types have to stay independent.
    • cha-cli/src/parse_cache.rs (new module) hosts both cached_parse and cached_symbols.
  • C++ parser now handles ClassName::method() out-of-class definitions, namespaces, and templates. Three gaps in the previous CppParser have been closed:
    • void Foo::bar() {...} (and ::global(), A::B::c(), destructors Foo::~Foo(), operators Foo::operator+()) was silently dropped — find_func_name_node only accepted bare identifier declarators. It now also unwraps qualified_identifier, destructor_name, and operator_name.
    • Out-of-class method definitions now attribute to their owning same-file class: void Foo::bar() bumps ClassInfo::method_count on Foo and flips has_behavior. Cross-file attribution still runs through cha-cli::c_oop_enrich.
    • namespace_definition, linkage_specification (extern "C" { ... }), and template_declaration are now explicitly matched in the top-level dispatch (previously fell through to the generic recursion arm) — same observable behaviour, but the nesting constructs are now a stable hook rather than an accidental default-case artefact.
    • C++-specific declarator helpers moved to a new cha-parser/src/cpp.rs so c_lang.rs stays below the large_file gate.
  • SourceModel.type_aliases now populated for Rust, TypeScript, Python, and Go (previously all four returned empty vec![] with parser-side TODOs). Each parser recognises its language's alias form and records (alias, rhs) pairs: Rust type X = Y; / pub type X<T> = Y;, TypeScript type X = Y; / export type X<T> = Y;, Python 3.12+ type X = Y and pre-3.12 X: TypeAlias = Y, Go type X = Y (only the true alias form — type X Y defined types are excluded). Plain Python X = Y assignments remain unclassified (too ambiguous). Shared extraction lives in a new cha-parser/src/type_aliases.rs module so per-language files stay below the large_file gate.

Changed

  • boundary_leak detector migrated to ProjectIndex. The three smells it emits (abstraction_boundary_leak, return_type_leak, test_only_type_in_production) previously parsed the whole project a second time — the codebase noted a "cached model occasionally drops typedef aliases" concern with root cause TBD. v1.11.0's binary-mtime cache keying removed the suspected root cause, and a new cache::tests::cache_roundtrip_preserves_type_aliases unit test makes the invariant a testable one. boundary_leak::detect now takes &ProjectIndex and shares the same parse pass as anemic_domain_model, typed_intimacy, module_envy, and friends. Verified against lvgl's src/ tree: 155 findings before = 155 findings after (abstraction_boundary_leak: 154, return_type_leak: 1). Completes roadmap S8.infra.4.

Fixed

  • C++: template specialisation methods attribute to the right class. template<> void Foo<int>::bar() used to drop on the floor because the qualifier Foo<int> (a template_type node) didn't match the stored class name Foo. attach_to_class now strips trailing <...> template arguments before matching, so out-of-class specialisations attribute correctly. Same stripping applies to any declaration whose declarator surfaces Foo<...> as the owning scope.
  • C++: real inheritance (class Derived : public Base) now recognised. extract_class consults the base_class_clause child and pulls the first type_identifier (or template_type's underlying name) as parent_name. Falls back to the first-field heuristic only when no base clause is present, so legacy C struct-embedding cases still work. Also fixes the class-name extraction for templated classes so template<typename T> class Foo {...} stores "Foo" instead of "Foo<T>".
  • C++: reference-return methods no longer vanish. const int& Foo::get() and similar reference_declarator-wrapped definitions used to be silently dropped because tree-sitter-cpp's reference_declarator has no declarator field — the declarator walker returned None. Both find_func_name_node (c_lang) and descend_to_qualified_identifier (cpp) now fall back to the first named child when the field is absent. Same fix path covers reference-return + qualified (const T& Foo::bar()) so class attribution still works. 6 additional regression tests (reference/pointer return types, multi-method attribution, constructor, extern "C", const member) added in cha-parser/tests/cpp_enhancements.rs (14 total).

[1.11.1] - 2026-04-27

Changed

  • Internal: split git-backed post-analysis passes (unstable_dependency, bus_factor, low_test_ratio) out of cha-cli/src/analyze.rs into a new cha-cli/src/git_metrics module. No behaviour change; analyze.rs drops below the 850-line large_file threshold that cargo xtask analyze gates on. collect_top_level in the C parser also picks up // cha:ignore high_complexity alongside the existing cognitive-complexity ignore after the declaration arm added one branch.

Note: 1.11.0 was tagged in the repo but the CI self-analyze gate failed on the above source-dir warnings so crates.io was never updated. 1.11.1 is the first shipped release of the 1.11 line.

[1.11.0] - 2026-04-27

Fixed

  • Cache invalidation now tracks the cha binary, not CARGO_PKG_VERSION. env_hash folds in std::env::current_exe()'s mtime, so any new binary — developer rebuild after editing parser code, or end-user upgrade to a new release — invalidates stale cached SourceModel entries. The previous version-based key allowed parser behaviour changes shipped without a cargo xtask bump to silently serve wrong cached data (which is what hid the header-declaration parser fix from users with existing .cha/cache). Falls back to CARGO_PKG_VERSION when current_exe() fails (unusual — sandboxed runners).
  • C/C++ parser now extracts function declarations from header files (void foo(int); — no body). Previously the parser only recognised function_definition nodes at the top level, silently dropping every prototype in a .h file. This broke cha deps --type classes --detail on C projects (every widget method displayed as private), leaky_public_signature (blind to the real public API), and the c_oop_enrich::tighten_exports pass (demoted public .c implementations whose .h declaration didn't parse). Variadic + attribute-macro signatures like foo(..., ...) LV_FORMAT_ATTRIBUTE(4, 5) remain an edge case because tree-sitter-c errors on the macro. Existing .cha/cache/ entries are stale after this fix and need to be deleted manually — the cache key hashes CARGO_PKG_VERSION, not parser behaviour.

Changed

  • C OOP attribution is now longest-prefix + inheritance-aware: given struct derived_t { base_t obj; ... } (first-field embedded base), derived_do(base_t *obj) attributes to derived_t rather than the base, because the function name's longest matching prefix points at the specific subclass and derived_t's ancestor chain includes base_t. Eliminates the previous over-attribution where short prefixes caused base classes to absorb methods that morally belong to subclasses. Large C codebases see base classes drop hundreds of borrowed methods; subclasses now correctly show their own methods in cha deps --type classes --detail UML.
  • cha deps --type classes --detail now uses the project-wide C OOP attribution from c_oop_enrich to fill methods on C/C++ UML output. Previously relied on a same-directory heuristic that missed cross-module methods; now picks up methods on shared metaclasses regardless of which file they live in. Also runs enrich on parse_all_models so C models read by deps see the corrected method_count / has_behavior / is_exported.
  • C OOP cross-file method attribution: new cha-cli/src/c_oop_enrich module runs inside ProjectIndex::parse to rewrite ClassInfo.method_count / has_behavior and tighten FunctionInfo.is_exported for C / C++ projects. Uses tokenisation (snake_case, PascalCase, camelCase, acronyms) + typedef alias following to attribute free functions to structs via the universal foo_t + foo_xxx(foo_t *self) convention. Forward declarations and full definitions of the same struct share attribution. Third-party types declared only in .c files (no .h declaration) get demoted from exported to internal. Only affects post-analysis index-backed detectors (anemic_domain_model, leaky_public_signature, etc.); per-file Plugin detectors (lazy_class, data_class) still see the unenriched model and remain disabled in the C profile.
  • Replaces the previous same-file associate_methods in cha-parser::c_lang (deleted) and the same-directory c_oop_filter post-hoc filter in cha-cli (deleted) with a single project-wide enrichment pass.

Added

  • cha analyze --focus <category> — comma-separated filter keeping only findings whose SmellCategory matches one of the listed values (bloaters, oo_abusers, change_preventers, dispensables, couplers, security). Unknown categories warn on stderr instead of crashing. Lets users narrow a noisy analyze run to a single architectural concern.
  • Finding.risk_score: Option<f64> — composite priority (severity × overshoot × hotspot factor) populated by prioritize_findings after analysis. Surfaces why a finding ranks where it does in reporter output and JSON/SARIF. Schema regenerated.
  • leaky_public_signature — flags an exported function whose parameters or return type mention a third-party crate's type. Workspace-internal crates (derived from project file paths) and Rust's built-in modules (std, core, alloc, proc_macro, …) are filtered out so intra-workspace and prelude types don't fire. Hint severity.
  • cross_layer_import — post-analysis pass that automatically infers project layers from the import graph (Martin's instability) and flags imports crossing boundaries upward. No configuration required; defers to the existing layer_violation plugin when the user has written an explicit [plugins.layer_violation] config. Warning severity (architectural violation).

[1.10.0] - 2026-04-25

Added

  • god_config — flags a Config/Settings/Options/Context/Env/AppState/Store-shaped type (exact name or *Config/*Settings/*Options suffix) passed as a parameter to ≥ 10 distinct functions spanning ≥ 3 files. Signals ambient configuration leaking everywhere instead of each caller taking only the fields it actually needs. Hint severity.
  • circular_abstraction — flags two files whose functions call each other's functions in both directions (≥ 2 calls each way). Catches behaviour-level mutual dependency that import-graph cycle detection misses when the callees are re-exported or wrapped. Complements typed_intimacy (type flow) with call flow. Hint severity.
  • parameter_position_inconsistency — flags functions where a domain type appears at a different parameter position than the project-wide majority. Requires ≥ 3 usages of the same type across functions and disagreement on position; primitives, unresolved-origin types, mutable-ref out-params (&mut Vec<_>), and self receivers are skipped. Hint severity.

Changed

  • Internal: cha-cli/src/project_index.rs — shared ProjectIndex owns parsed models plus derived maps (function_home, class_home, project_type_names, function_by_name). anemic_domain_model, typed_intimacy, module_envy, and parameter_position_inconsistency build the index once per analyze call instead of each rebuilding their own copies. No behaviour change; behaviourally identical on self-analyze. Boundary_leak still parses fresh because of a stale-typedef cache bug not yet rooted out.

[1.9.0] - 2026-04-25

Added

  • module_envy — flags a function that makes ≥ 3 calls into another file in the project while making ≤ half as many calls within its own file. The function is a "resident" of the wrong module — its body does work that belongs in the envied module. Suppresses test → common.rs pairs and calls to conventional helper filenames (utils, helpers, shared, prelude, …) where cross-file dependency is idiomatic, not misplaced. Hint severity.
  • typed_intimacy — flags file pairs whose function signatures exchange each other's declared types in both directions. Stronger signal than import-level inappropriate_intimacy: the pair literally accepts/returns types defined in each other, indicating they're functionally fused at the type boundary. Emits one finding per side of the pair, listing the shared type names. Hint severity.
  • async_callback_leak — flags a function signature that exposes a raw concurrency primitive (JoinHandle, Future, Task, Sender, Receiver, Promise, Awaitable, Coroutine, CancelFunc, …) in its return type or parameters. Skips launcher-shaped names (spawn_*, launch_*, start_*) where exposing the handle is the function's whole purpose. Hint severity.
  • anemic_domain_model — flags a class that is pure data (≥ 2 fields, no behavior) paired with one or more external service-shaped functions (filename ends in service/manager/handler/helper/util, or function name starts with a service verb prefix like process_/validate_/calculate_) that take the class as a first parameter. Promotes a data_class hint into an architectural finding when there's evidence the paired service owns behavior that should live on the class itself. Hint severity.
  • test_only_type_in_production — warns when production code references a class/struct declared only in test files (mocks, stubs, fixtures). Surfaces test scaffolding bleeding into shipping code. Warning severity.
  • return_type_leak post-analysis finding — dual of abstraction_boundary_leak. Detects when a dispatcher fans out to ≥ 3 sibling handlers whose return types are all the same non-local type, surfacing missing Anti-Corruption Layer on the way out. lvgl scan identifies thorvg's TVG_API leaking through dispatcher boundaries.
  • FunctionInfo.return_type: Option<TypeRef> — parsers extract the declared return type and resolve its origin through the same imports/type-registry pipeline as parameters. WIT schema grows an optional return-type field.
  • Container-expression primitive fallback: PEP 585 dict[K, V] / list[T] / tuple[...] resolve to Primitive instead of Unknown, eliminating false positives on Python handlers that return built-in container types.

Changed

  • WIT function-info record gains return-type: option<type-ref>breaking for WASM plugins, rebuild against the new SDK.
  • cha-cli/src/analyze.rs — extracted C OOP false-positive filter to c_oop_filter.rs and split run_post_analysis into git-based and signature-based helpers to keep the orchestrator lean as more post-analysis passes land.

[1.8.0] - 2026-04-25

Added

  • abstraction_boundary_leak post-analysis finding — detects dispatcher functions that fan out to ≥ 3 sibling callbacks which all share the same non-local type in corresponding parameter positions. Flagged as a missing Anti-Corruption Layer. lvgl scan shows 11/13 true-positive rate identifying GLAD/SDL/STB/Win32 leaks.
  • FunctionInfo.parameter_types now carries TypeRef { name, raw, origin } where origin is Local | External(module) | Primitive | Unknown. Each parser resolves origins from file imports: Rust use_declaration, TS import_statement, Python import / from, Go import_spec with go.mod module root lookup, C/C++ primitive seeding.
  • Parser normalisation helpers in cha-parser/src/type_ref.rs unwrap &'a mut Vec<Option<T>>, []T, List[T], pkg.Type etc. down to the innermost identifier for import lookup.
  • Universal-primitive fallback in resolve (String, PathBuf, HashMap, int, boolean, etc.) so common prelude types without explicit imports don't trip the detector.
  • unwrap_abuse now emits one finding per .unwrap() / .expect( call site (was: single finding at function name). IDE underlines each call directly.
  • switch_statement now points at the switch / match keyword inside the function body (was: function name).
  • message_chain now points at the a.b.c.d chain expression itself (was: function name). Heuristic text scan, falls back to function name when the chain can't be textually located.

Changed

  • FunctionInfo.parameter_types type changed from Vec<String> to Vec<TypeRef>breaking change for WASM plugins and cached SourceModels. WIT schema adds type-ref record and type-origin variant. Rebuilding against the new SDK picks up generated types automatically.
  • Parsers no longer sort parameter_types — declaration order is preserved, fixing latent .first()-based C OOP heuristics that silently depended on alphabetical ordering. data_clumps plugin now sorts its own key locally.

[1.7.1] - 2026-04-24

Fixed

  • cargo xtask releasewait_for_workflow now filters runs by the commit SHA (for ci.yml) and the tag branch (for release.yml), instead of taking the latest run unconditionally. Previously a stale success on an unrelated commit would cause the release flow to skip waiting and publish to crates.io while the new CI was still queued; a stale failure would abort a release that would otherwise pass.

[1.7.0] - 2026-04-23

Added

  • cha analyze --top N flag — show only the N most severe findings (terminal format), complements --all
  • Smell-level disable: disabled_smells = ["smell_name"] in .cha.toml (global) or under [languages.<lang>] (language-scoped). Finer-grained than disabling a whole plugin when it produces multiple smells
  • Plugin::smells() — plugins declare which smell_name values they can produce. Exposed as a WIT export for WASM plugins
  • cha plugin list now shows each plugin's declared smells
  • cha preset show <lang> now shows effective disabled smells
  • SDK helper cha_plugin_sdk::is_smell_disabled!(&input.options, "smell_name") — WASM plugins can skip disabled work proactively

Changed

  • C/C++ builtin profile: builder_pattern, null_object_pattern, strategy_pattern, data_clumps are now properly disabled via smell-level config (previously tried — and failed — to disable them by plugin name)
  • WIT analyzer world gains smells: func() -> list<string> export — breaking change for WASM plugins (recompile to pick up default impl)

Fixed

  • lvgl-scale improvement: analyze now emits ~1200 fewer false positives because smell-level disables actually take effect

[1.6.0] - 2026-04-23

Added

  • Location now has start_col/end_col fields — all findings precise to column level
  • FunctionInfo/ClassInfo have name_col/name_end_col — parser records identifier position
  • ImportInfo has col — import statement column position
  • Terminal output shows file:line:col when column info available
  • SARIF output fills startColumn/endColumn (1-based per spec)
  • LSP diagnostics use precise column range

Changed

  • All 37 builtin plugins now point findings at the function/class name, not the entire body
  • Line-scanning plugins (unsafe_api, hardcoded_secret, todo_tracker, error_handling) report exact column
  • WIT records gain column fields — location.start-col/end-col, function-info.name-col/name-end-col, class-info.name-col/name-end-col, import-info.colbreaking change for WASM plugins

[1.5.0] - 2026-04-22

Added

  • VS Code cha.disabledPlugins setting — suppress specific findings via initializationOptions
  • Hover report card shows actual plugin findings with severity icons
  • Coupling/hub_like findings mark import line range precisely

Changed

  • LSP architecture: all handlers read from ProjectCache — no per-handler plugin execution
  • LSP uses pull-only diagnostics (textDocument/diagnostic), removed push duplicates
  • CodeLens shows findings count + severity instead of raw parse metrics
  • Inlay Hints show findings summary (⚠N or ✓)
  • File-level findings (large_file, shotgun_surgery, etc.) mark only line 1

Fixed

  • Duplicate diagnostics (push + pull) in VS Code
  • disabledPlugins now filters by finding name, not plugin name
  • LSP shares .cha/cache/ with CLI via ProjectCache

[1.4.2] - 2026-04-22

Added

  • VS Code: auto-detect outdated cha binary — prompt update when version mismatches extension
  • VS Code: debug logs in ensureBinary for diagnostics
  • VS Code e2e: real VS Code test on 3 platforms (ubuntu/macos/windows) with sinon stub for user Download click

Fixed

  • SDK macros: include build.rs in package
  • VS Code: Windows download (.zip + PowerShell + .exe)
  • VS Code: exclude test files from .vsix via .vscodeignore
  • CI: vscode e2e set continue-on-error for network flakiness

[1.4.1] - 2026-04-21

Added

  • VS Code extension CI: vsce package validation + download e2e test on GitHub Actions
  • Download e2e test imports actual extension code (shared download.ts module)

Fixed

  • Windows binary download: use .zip + PowerShell extraction + .exe binary name

[1.4.0] - 2026-04-21

Added

  • LSP Semantic Tokens: highlight functions/classes with warning modifier based on findings
  • LSP Workspace Diagnostics: full project analysis without opening files
  • LSP textDocument/diagnostic: pull-based diagnostics per file
  • LSP Progress: progress notification during workspace diagnostics scan

[1.3.0] - 2026-04-21

Added

  • LSP Document Symbols: outline view with ⚠ markers based on actual findings severity
  • LSP: Document Symbols ⚠ markers now respect .cha.toml thresholds (no hardcoded values)

Changed

  • Upgraded wasmtime 43 → 44
  • Include tests in cha-core crate package (eliminates publish warnings)

[1.2.0] - 2026-04-21

Added

  • LSP CodeLens: show complexity, cognitive, lines, params above every function/class
  • LSP Hover: detailed quality report card on hover (markdown table)
  • LSP Inlay Hints: inline cx/cog/lines annotations at end of function definitions

[1.1.0] - 2026-04-21

Added

  • Cache v2: bincode serialization + per-file parse cache + mtime fast-path
  • L1 in-memory parse cache — zero disk I/O for repeated access within same process
  • Cached imports in meta for instant unstable_dependency analysis
  • ProjectCache with L1/L2 architecture shared across analyze/layers/deps/calibrate

Changed

  • Performance: cha analyze 26x faster on warm cache (87s → 3.3s on 3201 files)
  • Performance: cha layers 16x faster (13s → 0.8s)
  • Performance: cha deps 14x faster (13s → 0.9s)
  • Performance: cha calibrate 22x faster (13s → 0.6s)

Fixed

  • O(n²) algorithm in unstable_dependency / compute_afferent replaced with HashMap O(1) lookup
  • Findings cache wiped by duplicate ProjectCache::open in post-analysis
  • Cache invalidation now includes cha binary version (upgrade = auto-invalidate)
  • Skip filter_c_oop_false_positives when no lazy_class/data_class findings exist

[1.0.10] - 2026-04-21

Added

  • Global --config <path> flag for all subcommands — load config from custom file
  • ImportInfo.is_module_decl field to distinguish module declarations from imports

Fixed

  • Rust mod declarations no longer inflate high_coupling count

[1.0.9] - 2026-04-20

Added

  • cha layers --format html — interactive architecture diagram with CSS Grid
  • Layer violations show file-level evidence (which file includes which)
  • Layer violations sorted by instability gap (most severe first)
  • Rust mod declarations treated as file imports for layer analysis
  • Manual layer/module config in .cha.toml ([layers.modules] + [[layers.tiers]])

[1.0.8] - 2026-04-20

Added

  • cha calibrate command: auto-suggest thresholds from project statistics (P90/P95)
  • cha calibrate --apply saves to .cha/calibration.toml, auto-applied by cha analyze
  • Finding priority sorting: most severe issues shown first (severity × overshoot × compound)
  • Short module names in all output formats (terminal/DSM/dot/mermaid)

Changed

  • DSM output limited to top 25 modules by file count

Fixed

  • Skip parent→child layer violations (reduces lvgl false positives 87→37)

[1.0.7] - 2026-04-20

Added

  • Module inference rewrite: directory elbow + LCOM4 adaptive split + ICR + TCC quality metrics
  • cha layers --depth N to override auto-detected directory depth
  • cha layers --format dsm|terminal output formats
  • Composite risk scoring for long_method: risk = lines_ratio × complexity_factor

Changed

  • Module inference algorithm: replaced Union-Find with directory elbow + LCOM4 + ICR
  • long_method severity now based on composite risk (Hint/Warning/Error at risk 1/2/4)

Fixed

  • cha:ignore directive now covers up to 2 lines before a function
  • Fixed corrupted dot output and switched to LR layout for better layer readability

[1.0.6] - 2026-04-20

Added

  • Language-adaptive thresholds: C/C++ profile with higher defaults (long_method=100, complexity=15, large_file=2000)
  • Smart terminal aggregation: findings >5 grouped into summary + top 3 worst, --all flag for full listing
  • cha layers command: infer architectural layers from import dependencies
  • cha layers --format dot|mermaid|json|plantuml with layered architecture diagram

[1.0.5] - 2026-04-17

Fixed

  • VS Code extension: download URL corrected (cha-cli- prefix), extract path for cargo-dist tarball
  • VS Code extension: download with progress bar and cancellation support
  • VS Code extension: removed system PATH fallback for reliable self-testing
  • cargo publish no longer needs --allow-dirty (WIT copies tracked in git, include in Cargo.toml)

[1.0.4] - 2026-04-17

Added

  • cha:set inline directive: override thresholds per-function/class via comments (// cha:set rule_name=value)
  • Finding.actual_value and Finding.threshold fields for post-filter re-evaluation
  • cha lsp subcommand: start LSP server from unified binary (+3MB)
  • deps --direction in|out|both: filter edges by direction (who depends on target vs target depends on)
  • deps --format plantuml: PlantUML output for component and class diagrams
  • C OOP false positive filter: removes lazy_class/data_class for structs with cross-file methods
  • .pre-commit-hooks.yaml: pre-commit framework integration
  • action.yml: GitHub Action for CI analysis with SARIF upload
  • VS Code extension (vscode-cha/): cha LSP integration, auto-download binary, esbuild bundle

Fixed

  • .h files with C++ constructs now parsed as C++ (content sniffing)
  • class MACRO Name {} no longer misidentified as function definition
  • WIT Finding record now includes actual_value/threshold fields
  • build.rs auto-copies wit/plugin.wit for crates.io packaging
  • VS Code extension: esbuild bundle, LICENSE, .vscodeignore, publisher ID, homepage

[0.7.0] - 2026-04-17

Added

  • Dynamic shell completion for --plugin via CompleteEnv (unstable-dynamic): eval "$(COMPLETE=zsh cha)"
  • PluginRegistry::plugin_info() for runtime plugin discovery with descriptions
  • Plugin trait unified: version(), description(), authors() with defaults from Cargo.toml
  • All 33 builtin plugins now have description text for shell completion
  • completions subcommand now outputs dynamic completion scripts; shows usage when called without args
  • --strictness flag: relaxed (2x), default (1x), strict (0.5x), or custom float — scales all numeric thresholds
  • Per-language plugin config: [languages.c.plugins.naming] overrides in .cha.toml
  • Builtin C language profile: disables naming, lazy_class, data_class, builder/null_object/strategy pattern by default
  • cha preset list/show subcommand — display language profiles and plugin rules
  • SourceModel.type_aliases — unified typedef/type alias tracking across all languages
  • C OOP heuristic: associate functions with structs via inheritance chain + same-module matching
  • --exact --detail now shows only directly matched classes, not parents/children
  • C parser extract_params now includes pointer info (Type *) from AST
  • UML class diagrams: static functions shown as private (-), non-static as public (+)

Changed

  • Config struct now has strictness and languages fields (fully backward compatible)
  • get_usize() applies strictness scaling factor automatically
  • cmd_analyze refactored into AnalyzeOpts + run_post_analysis() + apply_filters()
  • parse_all_models returns (PathBuf, SourceModel) pairs for correct file-model association

Fixed

  • C/C++ parser: static functions now correctly marked is_exported = false; header files always exported
  • Reduces large_api_surface false positives by ~51% and enables accurate dead_code detection for C
  • shotgun_surgery, divergent_change, bus_factor now use single batch git log call instead of per-file — fixes freeze on large repos (lvgl: >2min → 23s)
  • C OOP method association resolves typedef aliases for cross-file matching
  • class_dir prefers struct definitions with fields over forward declarations

[0.6.2] - 2026-04-15

Added

  • All parser fields implemented for C/C++, Go, Python (zero TODO(parser) remaining)
  • C-style struct inheritance detection via first-field type + typedef alias resolution
  • --filter now shows connected subgraph (children + parent chain, no siblings)
  • --exact flag for direct-match-only filtering
  • --filter supports regex patterns
  • --detail flag for UML class diagrams with fields, types, and methods
  • ClassInfo.field_types field across all parsers and WIT interface

Fixed

  • C parser: recurse into #ifdef/#if preprocessor blocks for struct/include detection
  • C parser: typedef struct { ... } Name now correctly parsed
  • Filter traversal: parent chain walk without sibling expansion; fixed infinite loop

[0.6.1] - 2026-04-14

Added

  • SourceModel.comments — parsers now extract comments via tree-sitter for language-aware analysis
  • todo_tracker now uses parsed comment nodes instead of raw text scanning

Fixed

  • cha trend — suppressed git worktree stdout leak; fixed progress bar overlap
  • Progress bar spinner now uses braille animation with steady tick
  • Extracted new_progress_bar helper; added progress bars to cha deps
  • Unimplemented parser fields marked with TODO(parser) comments for self-tracking

[0.6.0] - 2026-04-14

Added

  • god_class plugin — God Class detection (ATFD>5, WMC>=47, TCC<0.33) [Lanza & Marinescu 2006]
  • brain_method plugin — Brain Method detection (LOC>65, CYCLO>=4, NOAV>7) [Lanza & Marinescu 2006]
  • hub_like_dependency plugin — detect modules with excessive import fan-out [Arcelli Fontana et al. 2019]
  • error_handling plugin — detect empty catch blocks and unwrap/expect abuse [Padua & Shang 2018]
  • unstable_dependency — post-analysis pass using Martin's instability metric I=Ce/(Ca+Ce)
  • cognitive_complexity plugin — nesting-aware complexity metric, threshold 15 [SonarSource 2017]
  • todo_tracker plugin — detect leftover TODO/FIXME/HACK/XXX comments
  • unsafe_api plugin — detect dangerous function calls per language [CWE-676]
  • low_test_ratio — warn when test code < 50% of production code
  • tangled_change — detect commits touching unrelated modules [Tornhill 2015]
  • bus_factor — knowledge distribution risk detection [Nagappan et al. 2008]
  • cha hotspot subcommand — git change frequency × complexity [Tornhill 2015]

Fixed

  • Duplicate plugin registration bug in register_advanced_plugins

[0.5.2] - 2026-04-13

Added

  • cha trend subcommand — analyze recent git commits via worktree, show issue count trend (terminal ASCII + JSON)
  • // cha:ignore comment directive — suppress findings per function/line, supports //, #, --, /* */ styles
  • cha deps --type classes — class hierarchy graph (extends/implements)
  • cha deps --type calls — function call graph with recursion detection (blue dashed)
  • cha deps --filter <name> — filter graph to specific class/function

Fixed

  • Cache invalidation now scans all .cha.toml files in subdirectories, not just root

[0.5.1] - 2026-04-12

Added

  • cha deps --type classes — class/struct/trait hierarchy graph (extends/implements)
  • cha deps --type calls — function call graph with recursion detection (blue dashed lines)
  • cha deps --filter <name> — filter graph to specific class/function
  • FunctionInfo.called_functions field in parser output and WIT interface

[0.5.0] - 2026-04-12

Added

  • cha deps subcommand — import dependency graph with --format dot|json|mermaid, --depth file|dir, cycle detection with red highlighting
  • Go language support (.go) — functions, structs, interfaces, imports, complexity, chain depth
  • C language support (.c/.h) — functions, structs, includes, complexity
  • C++ language support (.cpp/.cc/.cxx/.hpp/.hxx) — functions, classes, includes, complexity
  • Health scores in JSON output (health_scores field) and SARIF output (properties.health_scores)
  • [debt_weights] config section in .cha.toml — customize remediation time per severity (hint/warning/error)
  • Plugin-level parallel analysis via rayon par_iter

Fixed

  • HTML report: show only ±5 context lines around findings instead of full file source, collapse file sections by default

0.4.0 - 2026-04-11

Added

  • Tech debt summary in terminal output: total estimated remediation time + grade distribution
  • --format html — self-contained HTML report with dark theme, source code highlighting, health scores, and collapsible file sections
  • --output <path> flag to write report to file
  • hardcoded_secret plugin — detects API keys, tokens, passwords, private keys, JWTs in source code
  • SmellCategory::Security variant for security-related findings

0.3.0 - 2026-04-10

Added

  • Incremental analysis cache (.cha/cache/) — skips unchanged files, ~70x speedup on warm runs
  • --no-cache flag to force full re-analysis
  • Cache auto-invalidates when .cha.toml or plugins change
  • cha baseline — generate a baseline file of current findings, suppress known issues
  • --baseline <path> flag on cha analyze to only report new findings
  • Code health scores (A–F) per file in terminal output, based on issue density and severity

0.2.0 - 2026-04-10

Added

  • Python language support (.py) — functions, classes, imports, complexity, chain depth, delegating detection

Fixed

  • xtask bump now dynamically scans all Cargo.toml files instead of hardcoded paths, and refreshes all Cargo.lock files
  • Duplicate PythonParser import in cha-parser
  • cha-lsp/Cargo.toml version not updated by xtask bump

0.1.1 - 2026-04-10

Added

  • cha completions <shell> — generate shell completion scripts (bash/zsh/fish/powershell); auto-installed via Homebrew

Fixed

  • cha plugin new hint now shows cha plugin build instead of cargo build, and uses correct underscore filename
  • WASM plugin e2e test: plugin dir detection when cha plugin new uses cwd directly
  • Unused Path import in cha-plugin-sdk test-utils

Changed

  • cha-lsp: marked publish = false, not distributed via crates.io
  • xtask: refactored cmd_publish/cmd_bump to reduce complexity

0.1.0 - 2026-04-10

Added

Core Analysis

  • 25 built-in code smell plugins covering Bloaters, Couplers, OO Abusers, Change Preventers, and Dispensables
  • 9 new plugins: TemporaryField, SpeculativeGenerality, RefusedBequest, ShotgunSurgery, DivergentChange, LazyClass, DataClass, MiddleMan, FeatureEnvy
  • DesignPatternAdvisor: suggests Strategy, State, Builder, Null Object, Template Method, Observer patterns
  • TypeScript and Rust AST parsing via Tree-sitter
  • Structural duplication detection via AST hash

WASM Plugin System

  • WIT interface with full model fields (FunctionInfo, ClassInfo) and typed option-value variant
  • cha-plugin-sdk crate: zero-config plugin development — no WIT file needed, plugin! macro embeds WIT at compile time
  • cha plugin new/build/install/list/remove CLI subcommands
  • Auto-conversion of WASM binary to WASM Component in cha plugin build
  • test-utils feature: WasmPluginTest builder for plugin unit testing
  • Plugin metadata (version, description, authors) auto-filled from plugin's Cargo.toml
  • Config options passed from .cha.toml to plugins as typed OptionValue

CLI

  • cha analyze — recursive analysis with .gitignore awareness, --diff, --stdin-diff, --plugin filter
  • cha parse — inspect AST structure
  • cha init — generate default config
  • cha fix — auto-fix naming violations
  • cha schema — print JSON Schema for output format
  • Output formats: terminal, JSON, SARIF, LLM context
  • --fail-on exit code control

LSP

  • Real-time diagnostics on open/change/save
  • Code action suggestions

Tooling

  • cargo xtask ci/build/test/lint/analyze/lsp-test/plugin-test/plugin-e2e
  • cargo xtask bump <major|minor|patch> — version bump across all crates
  • cargo xtask publish [--dry-run] — publish to crates.io in topological order
  • cargo-dist: multi-platform binaries (macOS/Linux/Windows), shell/powershell/homebrew/msi installers
  • oranda: project website with release artifacts