1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4
5use crate::{FunctionInfo, SourceFile, SourceModel, TypeRef};
6
7#[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#[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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
34pub struct Location {
35 pub path: PathBuf,
36 pub start_line: usize,
37 #[serde(default, skip_serializing_if = "crate::is_zero_usize")]
39 pub start_col: usize,
40 pub end_line: usize,
41 #[serde(default, skip_serializing_if = "crate::is_zero_usize")]
43 pub end_col: usize,
44 pub name: Option<String>,
45}
46
47#[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 #[serde(skip_serializing_if = "Option::is_none")]
58 pub actual_value: Option<f64>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub threshold: Option<f64>,
62 #[serde(skip_serializing_if = "Option::is_none")]
66 pub risk_score: Option<f64>,
67}
68
69pub trait ProjectQuery: Send + Sync {
76 fn is_called_externally(&self, name: &str, exclude_path: &Path) -> bool;
80
81 fn callers_of(&self, name: &str) -> Vec<PathBuf>;
83
84 fn cross_file_call_counts(&self) -> Vec<((PathBuf, PathBuf), u32)>;
86
87 fn function_home(&self, name: &str) -> Option<PathBuf>;
91
92 fn function_by_name(&self, name: &str) -> Option<(PathBuf, FunctionInfo)>;
94
95 fn class_home(&self, name: &str) -> Option<PathBuf>;
97
98 fn model_by_path(&self, path: &Path) -> Option<SourceModel>;
100
101 fn is_project_type(&self, name: &str) -> bool;
105
106 fn is_third_party(&self, type_ref: &TypeRef) -> bool;
109
110 fn workspace_crate_names(&self) -> Vec<String>;
112
113 fn is_test_path(&self, path: &Path) -> bool;
117
118 fn file_count(&self) -> usize;
122
123 fn function_at(&self, path: &Path, line: u32, col: u32) -> Option<FunctionInfo>;
127}
128
129pub trait ProjectQueryBulk: ProjectQuery {
132 fn iter_models(&self) -> Box<dyn Iterator<Item = (&Path, &SourceModel)> + '_>;
133}
134
135pub 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 pub project: Option<&'a std::sync::Arc<dyn ProjectQuery>>,
144}
145
146pub trait Plugin: Send + Sync {
148 fn name(&self) -> &str;
150
151 fn version(&self) -> &str {
153 env!("CARGO_PKG_VERSION")
154 }
155
156 fn description(&self) -> &str {
158 ""
159 }
160
161 fn authors(&self) -> Vec<String> {
163 vec![env!("CARGO_PKG_AUTHORS").to_string()]
164 }
165
166 fn smells(&self) -> Vec<String> {
170 Vec::new()
171 }
172
173 fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding>;
175}
176
177pub 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
189pub 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}