Skip to main content

cha_core/
plugin.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5use crate::{SourceFile, SourceModel};
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/// Analysis context passed to plugins.
70pub struct AnalysisContext<'a> {
71    pub file: &'a SourceFile,
72    pub model: &'a SourceModel,
73    pub tree: Option<&'a tree_sitter::Tree>,
74    pub ts_language: Option<&'a tree_sitter::Language>,
75}
76
77/// Core trait that all analyzers implement.
78pub trait Plugin: Send + Sync {
79    /// Unique identifier for this plugin.
80    fn name(&self) -> &str;
81
82    /// Plugin version (e.g. "1.0.0").
83    fn version(&self) -> &str {
84        env!("CARGO_PKG_VERSION")
85    }
86
87    /// Short description of what the plugin detects.
88    fn description(&self) -> &str {
89        ""
90    }
91
92    /// List of authors.
93    fn authors(&self) -> Vec<String> {
94        vec![env!("CARGO_PKG_AUTHORS").to_string()]
95    }
96
97    /// Smell names this plugin can produce.
98    /// Used by the host for smell-level filtering, docs, and `cha plugin list`.
99    /// Default is empty — plugins should override to declare their smells.
100    fn smells(&self) -> Vec<String> {
101        Vec::new()
102    }
103
104    /// Run analysis on a single file and return findings.
105    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding>;
106}
107
108/// Build a Location pointing at a function's name identifier.
109pub fn func_location(path: &std::path::Path, f: &crate::FunctionInfo) -> Location {
110    Location {
111        path: path.into(),
112        start_line: f.start_line,
113        start_col: f.name_col,
114        end_line: f.start_line,
115        end_col: f.name_end_col,
116        name: Some(f.name.clone()),
117    }
118}
119
120/// Build a Location pointing at a class/struct's name identifier.
121pub fn class_location(path: &std::path::Path, c: &crate::ClassInfo) -> Location {
122    Location {
123        path: path.into(),
124        start_line: c.start_line,
125        start_col: c.name_col,
126        end_line: c.start_line,
127        end_col: c.name_end_col,
128        name: Some(c.name.clone()),
129    }
130}