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