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