Custom plugin in 50 lines
A worked example: detect functions whose names contain tmp, temp, or xxx. Project-specific naming hygiene that no built-in detector covers.
The full cha plugin new scaffold is bigger than what we need. This recipe strips it to 50 lines of meaningful Rust + the necessary Cargo.toml. For the structured tour see Plugin development.
Scaffold
cha plugin new no-tmp-names
cd no-tmp-names
Cargo.toml (the scaffold writes a working version; what matters):
[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"
The plugin
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(); 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!( "Function `{}` is named like temporary scaffolding — give it the name it deserves before merge.", f.name ), suggestion: None, }); } } findings } } }
That's the whole plugin: trait PluginImpl with three methods, one loop. No state, no async, no Result ceremony.
Build and install
cha plugin build # compiles to target/wasm32-wasip2/release/no_tmp_names.wasm
cha plugin install no_tmp_names.wasm
install copies the artifact to .cha/plugins/ (project-local). Use --global for ~/.cha/plugins/.
Run it
cha analyze --plugin no-tmp-names src/
Filter to one plugin while iterating; the next cha analyze (no --plugin) runs everything plus your new plugin together.
Iterate
Edit src/lib.rs, then:
cha plugin build
cha plugin install no_tmp_names.wasm # overwrites the previous .wasm
cha analyze --plugin no-tmp-names src/
Cache invalidation is automatic — installing a new .wasm invalidates the cached findings for files that plugin touched.
What's available inside analyze
AnalysisInput exposes:
input.path— path of the file being analysed.input.model— the SourceModel with parsed functions, classes, imports, comments.input.options— values from[plugins.no-tmp-names]in.cha.toml.
Project-wide queries (callers, type origins, file count) live in cha_plugin_sdk::project_query. Tree-sitter S-expression queries live in cha_plugin_sdk::tree_query. The full surface is Plugin development.
See also
- Plugin development — full reference.
examples/— four end-to-end plugins, including a TODO tracker and a React hooks linter.cha plugin