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
124pub trait ProjectQueryBulk: ProjectQuery {
127 fn iter_models(&self) -> Box<dyn Iterator<Item = (&Path, &SourceModel)> + '_>;
128}
129
130pub struct AnalysisContext<'a> {
132 pub file: &'a SourceFile,
133 pub model: &'a SourceModel,
134 pub tree: Option<&'a tree_sitter::Tree>,
135 pub ts_language: Option<&'a tree_sitter::Language>,
136 pub project: Option<&'a std::sync::Arc<dyn ProjectQuery>>,
139}
140
141pub trait Plugin: Send + Sync {
143 fn name(&self) -> &str;
145
146 fn version(&self) -> &str {
148 env!("CARGO_PKG_VERSION")
149 }
150
151 fn description(&self) -> &str {
153 ""
154 }
155
156 fn authors(&self) -> Vec<String> {
158 vec![env!("CARGO_PKG_AUTHORS").to_string()]
159 }
160
161 fn smells(&self) -> Vec<String> {
165 Vec::new()
166 }
167
168 fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding>;
170}
171
172pub fn func_location(path: &std::path::Path, f: &crate::FunctionInfo) -> Location {
174 Location {
175 path: path.into(),
176 start_line: f.start_line,
177 start_col: f.name_col,
178 end_line: f.start_line,
179 end_col: f.name_end_col,
180 name: Some(f.name.clone()),
181 }
182}
183
184pub fn class_location(path: &std::path::Path, c: &crate::ClassInfo) -> Location {
186 Location {
187 path: path.into(),
188 start_line: c.start_line,
189 start_col: c.name_col,
190 end_line: c.start_line,
191 end_col: c.name_end_col,
192 name: Some(c.name.clone()),
193 }
194}