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    /// Find the function whose body contains the given (line, col) position
124    /// in `path`. Lines are 1-based, columns are 0-based.
125    /// Returns the innermost matching function (smallest line range).
126    fn function_at(&self, path: &Path, line: u32, col: u32) -> Option<FunctionInfo>;
127}
128
129/// Bulk access for in-process plugins. WASM plugins cannot reach this trait —
130/// they're stuck with point queries from `ProjectQuery`.
131pub trait ProjectQueryBulk: ProjectQuery {
132    fn iter_models(&self) -> Box<dyn Iterator<Item = (&Path, &SourceModel)> + '_>;
133}
134
135/// Analysis context passed to plugins.
136pub struct AnalysisContext<'a> {
137    pub file: &'a SourceFile,
138    pub model: &'a SourceModel,
139    pub tree: Option<&'a tree_sitter::Tree>,
140    pub ts_language: Option<&'a tree_sitter::Language>,
141    /// Project-level query interface. Wrapped in Arc so WASM hosts can take
142    /// owned ownership for store lifetime; built-in plugins deref through `&`.
143    pub project: Option<&'a std::sync::Arc<dyn ProjectQuery>>,
144}
145
146/// Core trait that all analyzers implement.
147pub trait Plugin: Send + Sync {
148    /// Unique identifier for this plugin.
149    fn name(&self) -> &str;
150
151    /// Plugin version (e.g. "1.0.0").
152    fn version(&self) -> &str {
153        env!("CARGO_PKG_VERSION")
154    }
155
156    /// Short description of what the plugin detects.
157    fn description(&self) -> &str {
158        ""
159    }
160
161    /// List of authors.
162    fn authors(&self) -> Vec<String> {
163        vec![env!("CARGO_PKG_AUTHORS").to_string()]
164    }
165
166    /// Smell names this plugin can produce.
167    /// Used by the host for smell-level filtering, docs, and `cha plugin list`.
168    /// Default is empty — plugins should override to declare their smells.
169    fn smells(&self) -> Vec<String> {
170        Vec::new()
171    }
172
173    /// Run analysis on a single file and return findings.
174    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding>;
175}
176
177/// Build a Location pointing at a function's name identifier.
178pub fn func_location(path: &std::path::Path, f: &crate::FunctionInfo) -> Location {
179    Location {
180        path: path.into(),
181        start_line: f.start_line,
182        start_col: f.name_col,
183        end_line: f.start_line,
184        end_col: f.name_end_col,
185        name: Some(f.name.clone()),
186    }
187}
188
189/// Build a Location pointing at a class/struct's name identifier.
190pub fn class_location(path: &std::path::Path, c: &crate::ClassInfo) -> Location {
191    Location {
192        path: path.into(),
193        start_line: c.start_line,
194        start_col: c.name_col,
195        end_line: c.start_line,
196        end_col: c.name_end_col,
197        name: Some(c.name.clone()),
198    }
199}