Skip to main content

cha_core/
plugin.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4
5use crate::{FunctionInfo, SourceFile, SourceModel, TypeRef};
6
7/// Severity level for a finding.
8#[derive(
9    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, Default,
10)]
11#[serde(rename_all = "lowercase")]
12pub enum Severity {
13    #[default]
14    Hint,
15    Warning,
16    Error,
17}
18
19/// Smell category from refactoring literature.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
21#[serde(rename_all = "snake_case")]
22pub enum SmellCategory {
23    #[default]
24    Bloaters,
25    OoAbusers,
26    ChangePreventers,
27    Dispensables,
28    Couplers,
29    Security,
30}
31
32/// Source location of a finding.
33#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
34pub struct Location {
35    pub path: PathBuf,
36    pub start_line: usize,
37    /// 0-based column of the start position.
38    #[serde(default, skip_serializing_if = "crate::is_zero_usize")]
39    pub start_col: usize,
40    pub end_line: usize,
41    /// 0-based column of the end position.
42    #[serde(default, skip_serializing_if = "crate::is_zero_usize")]
43    pub end_col: usize,
44    pub name: Option<String>,
45}
46
47/// A single analysis finding.
48#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
49pub struct Finding {
50    pub smell_name: String,
51    pub category: SmellCategory,
52    pub severity: Severity,
53    pub location: Location,
54    pub message: String,
55    pub suggested_refactorings: Vec<String>,
56    /// The actual measured value (e.g. line count, complexity score).
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub actual_value: Option<f64>,
59    /// The threshold that was exceeded to produce this finding.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub threshold: Option<f64>,
62    /// Composite priority score: severity × overshoot × hotspot factor.
63    /// Populated by `prioritize_findings` after analysis completes; absent
64    /// for findings produced but not yet ranked (pre-sort).
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub risk_score: Option<f64>,
67}
68
69/// Project-level queries available to all plugins (built-in + WASM).
70///
71/// Methods return owned/cheap-to-copy types (`PathBuf`/`Vec`) so WASM host
72/// imports can wrap them directly without borrow gymnastics. For built-in
73/// plugins needing bulk access (iterate every model), use the
74/// `ProjectQueryBulk` extension trait via downcast.
75pub trait ProjectQuery: Send + Sync {
76    // === Reference relations ===
77
78    /// True if `name` is called from any file other than `exclude_path`.
79    fn is_called_externally(&self, name: &str, exclude_path: &Path) -> bool;
80
81    /// All files that reference `name` (excluding self-references).
82    fn callers_of(&self, name: &str) -> Vec<PathBuf>;
83
84    /// Pre-computed cross-file call counts: `((caller, callee), count)`.
85    fn cross_file_call_counts(&self) -> Vec<((PathBuf, PathBuf), u32)>;
86
87    // === Symbol location ===
88
89    /// First file that declared this function.
90    fn function_home(&self, name: &str) -> Option<PathBuf>;
91
92    /// First `(file, FunctionInfo)` tuple — fuller than `function_home`.
93    fn function_by_name(&self, name: &str) -> Option<(PathBuf, FunctionInfo)>;
94
95    /// First file that declared this class/struct.
96    fn class_home(&self, name: &str) -> Option<PathBuf>;
97
98    /// O(1) model lookup by path.
99    fn model_by_path(&self, path: &Path) -> Option<SourceModel>;
100
101    // === Type system ===
102
103    /// True if `name` is declared somewhere in the project.
104    fn is_project_type(&self, name: &str) -> bool;
105
106    /// True if the type is a genuine third-party dependency
107    /// (External origin AND not stdlib AND not workspace sibling).
108    fn is_third_party(&self, type_ref: &TypeRef) -> bool;
109
110    /// Workspace sibling crate names (Rust workspace) — empty otherwise.
111    fn workspace_crate_names(&self) -> Vec<String>;
112
113    // === Path shape ===
114
115    /// True if path looks like a test file or sits inside a test directory.
116    fn is_test_path(&self, path: &Path) -> bool;
117
118    // === Project metadata ===
119
120    /// Total count of analyzed files.
121    fn file_count(&self) -> usize;
122}
123
124/// Bulk access for in-process plugins. WASM plugins cannot reach this trait —
125/// they're stuck with point queries from `ProjectQuery`.
126pub trait ProjectQueryBulk: ProjectQuery {
127    fn iter_models(&self) -> Box<dyn Iterator<Item = (&Path, &SourceModel)> + '_>;
128}
129
130/// Analysis context passed to plugins.
131pub struct AnalysisContext<'a> {
132    pub file: &'a SourceFile,
133    pub model: &'a SourceModel,
134    pub tree: Option<&'a tree_sitter::Tree>,
135    pub ts_language: Option<&'a tree_sitter::Language>,
136    /// Project-level query interface. Wrapped in Arc so WASM hosts can take
137    /// owned ownership for store lifetime; built-in plugins deref through `&`.
138    pub project: Option<&'a std::sync::Arc<dyn ProjectQuery>>,
139}
140
141/// Core trait that all analyzers implement.
142pub trait Plugin: Send + Sync {
143    /// Unique identifier for this plugin.
144    fn name(&self) -> &str;
145
146    /// Plugin version (e.g. "1.0.0").
147    fn version(&self) -> &str {
148        env!("CARGO_PKG_VERSION")
149    }
150
151    /// Short description of what the plugin detects.
152    fn description(&self) -> &str {
153        ""
154    }
155
156    /// List of authors.
157    fn authors(&self) -> Vec<String> {
158        vec![env!("CARGO_PKG_AUTHORS").to_string()]
159    }
160
161    /// Smell names this plugin can produce.
162    /// Used by the host for smell-level filtering, docs, and `cha plugin list`.
163    /// Default is empty — plugins should override to declare their smells.
164    fn smells(&self) -> Vec<String> {
165        Vec::new()
166    }
167
168    /// Run analysis on a single file and return findings.
169    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding>;
170}
171
172/// Build a Location pointing at a function's name identifier.
173pub fn func_location(path: &std::path::Path, f: &crate::FunctionInfo) -> Location {
174    Location {
175        path: path.into(),
176        start_line: f.start_line,
177        start_col: f.name_col,
178        end_line: f.start_line,
179        end_col: f.name_end_col,
180        name: Some(f.name.clone()),
181    }
182}
183
184/// Build a Location pointing at a class/struct's name identifier.
185pub fn class_location(path: &std::path::Path, c: &crate::ClassInfo) -> Location {
186    Location {
187        path: path.into(),
188        start_line: c.start_line,
189        start_col: c.name_col,
190        end_line: c.start_line,
191        end_col: c.name_end_col,
192        name: Some(c.name.clone()),
193    }
194}