1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::path::PathBuf;
4
5use proc_macro2::{LineColumn, TokenStream, TokenTree};
6use sofie_model::syntax::use_paths;
7use syn::{ImplItem, Item, ItemImpl, Type, Visibility};
8
9use crate::count_as_f64;
10use crate::lines::LineKind;
11use crate::workspace::{CrateSource, SourceFile};
12
13const CODE_LINES_THRESHOLD: f64 = 300.0;
15const METHODS_THRESHOLD: f64 = 15.0;
16const DEPENDENCIES_THRESHOLD: f64 = 12.0;
17const PUBLIC_METHODS_THRESHOLD: f64 = 10.0;
18
19const INTERNAL_PATH_ROOTS: [&str; 2] = ["crate", "super"];
20const WORKSPACE_CRATE_PREFIX: &str = "sofie_";
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub struct TypeMetrics {
24 pub code_lines: usize,
25 pub methods: usize,
26 pub public_methods: usize,
27 pub dependencies: usize,
28}
29
30impl TypeMetrics {
31 #[must_use]
32 pub fn score(self) -> f64 {
33 count_as_f64(self.code_lines) / CODE_LINES_THRESHOLD
34 + count_as_f64(self.methods) / METHODS_THRESHOLD
35 + count_as_f64(self.dependencies) / DEPENDENCIES_THRESHOLD
36 + count_as_f64(self.public_methods) / PUBLIC_METHODS_THRESHOLD
37 }
38}
39
40#[derive(Debug)]
41pub struct GodType {
42 pub type_name: String,
43 pub crate_name: String,
44 pub files: Vec<PathBuf>,
45 pub metrics: TypeMetrics,
46}
47
48impl GodType {
49 #[must_use]
50 pub fn score(&self) -> f64 {
51 self.metrics.score()
52 }
53}
54
55impl fmt::Display for GodType {
56 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57 let files: Vec<String> = self
58 .files
59 .iter()
60 .map(|file| file.display().to_string())
61 .collect();
62 write!(
63 formatter,
64 "{}::{} score {:.2} ({} code lines, {} methods, {} public, {} dependencies) in {}",
65 self.crate_name,
66 self.type_name,
67 self.score(),
68 self.metrics.code_lines,
69 self.metrics.methods,
70 self.metrics.public_methods,
71 self.metrics.dependencies,
72 files.join(", ")
73 )
74 }
75}
76
77#[must_use]
78pub fn rank_god_types(crates: &[CrateSource]) -> Vec<GodType> {
79 let mut ranking: Vec<GodType> = crates.iter().flat_map(crate_god_types).collect();
80 ranking.sort_by(|left, right| right.score().total_cmp(&left.score()));
81 ranking
82}
83
84#[derive(Default)]
85struct TypeAccumulator {
86 metrics: TypeMetrics,
87 dependencies: BTreeSet<String>,
88 files: BTreeSet<PathBuf>,
89}
90
91fn crate_god_types(crate_source: &CrateSource) -> Vec<GodType> {
92 let mut types: BTreeMap<String, TypeAccumulator> = BTreeMap::new();
93 for file in &crate_source.files {
94 add_file_impls(file, &mut types);
95 }
96 types
97 .into_iter()
98 .map(|(type_name, accumulator)| GodType {
99 type_name,
100 crate_name: crate_source.name.clone(),
101 metrics: TypeMetrics {
102 dependencies: accumulator.dependencies.len(),
103 ..accumulator.metrics
104 },
105 files: accumulator.files.into_iter().collect(),
106 })
107 .collect()
108}
109
110fn add_file_impls(file: &SourceFile, types: &mut BTreeMap<String, TypeAccumulator>) {
111 let mut impls = Vec::new();
112 collect_inherent_impls(&file.syntax.items, &mut impls);
113 let mut imports = Vec::new();
114 collect_internal_imports(&file.syntax.items, &mut imports);
115 let identifiers = located_identifiers(file.tokens.clone());
116 for item_impl in impls {
117 let Some(type_name) = self_type_name(&item_impl.self_ty) else {
118 continue;
119 };
120 let accumulator = types.entry(type_name).or_default();
121 let metrics = &mut accumulator.metrics;
122 metrics.code_lines += impl_code_lines(item_impl, &file.lines);
123 metrics.methods += item_impl
124 .items
125 .iter()
126 .filter(|item| matches!(item, ImplItem::Fn(_)))
127 .count();
128 metrics.public_methods += public_method_count(item_impl);
129 accumulator
130 .dependencies
131 .extend(imports_used_by(item_impl, &imports, &identifiers));
132 accumulator.files.insert(file.path.clone());
133 }
134}
135
136fn collect_inherent_impls<'syntax>(items: &'syntax [Item], found: &mut Vec<&'syntax ItemImpl>) {
137 for item in items {
138 match item {
139 Item::Impl(item_impl) if item_impl.trait_.is_none() => found.push(item_impl),
140 Item::Mod(module) => {
141 if let Some((_, module_items)) = &module.content {
142 collect_inherent_impls(module_items, found);
143 }
144 }
145 _ => {}
146 }
147 }
148}
149
150fn self_type_name(self_type: &Type) -> Option<String> {
151 let Type::Path(type_path) = self_type else {
152 return None;
153 };
154 type_path
155 .path
156 .segments
157 .last()
158 .map(|segment| segment.ident.to_string())
159}
160
161fn impl_code_lines(item_impl: &ItemImpl, line_kinds: &[LineKind]) -> usize {
162 let first_line = item_impl.impl_token.span.start().line;
163 let last_line = item_impl.brace_token.span.close().end().line;
164 line_kinds[first_line - 1..last_line]
165 .iter()
166 .filter(|kind| **kind == LineKind::Code)
167 .count()
168}
169
170fn public_method_count(item_impl: &ItemImpl) -> usize {
171 item_impl
172 .items
173 .iter()
174 .filter(|item| matches!(item, ImplItem::Fn(method) if matches!(method.vis, Visibility::Public(_))))
175 .count()
176}
177
178struct Import {
179 path: String,
180 binding: Option<String>,
182}
183
184fn collect_internal_imports(items: &[Item], found: &mut Vec<Import>) {
185 for item in items {
186 match item {
187 Item::Use(item_use) => found.extend(
188 use_paths(&item_use.tree)
189 .into_iter()
190 .filter(|used| is_internal_path(&used.segments))
191 .map(|used| {
192 let glob = used.binding.is_none().then(|| "*".to_owned());
193 Import {
194 path: used
195 .segments
196 .into_iter()
197 .chain(glob)
198 .collect::<Vec<_>>()
199 .join("::"),
200 binding: used.binding,
201 }
202 }),
203 ),
204 Item::Mod(module) => {
205 if let Some((_, module_items)) = &module.content {
206 collect_internal_imports(module_items, found);
207 }
208 }
209 _ => {}
210 }
211 }
212}
213
214fn imports_used_by(
216 item_impl: &ItemImpl,
217 imports: &[Import],
218 identifiers: &[(LineColumn, String)],
219) -> Vec<String> {
220 let start = item_impl.impl_token.span.start();
221 let end = item_impl.brace_token.span.close().end();
222 let mentioned: BTreeSet<&str> = identifiers
223 .iter()
224 .filter(|(position, _)| (start..end).contains(position))
225 .map(|(_, identifier)| identifier.as_str())
226 .collect();
227 imports
228 .iter()
229 .filter(|import| {
230 import
231 .binding
232 .as_deref()
233 .is_none_or(|binding| mentioned.contains(binding))
234 })
235 .map(|import| import.path.clone())
236 .collect()
237}
238
239fn located_identifiers(tokens: TokenStream) -> Vec<(LineColumn, String)> {
240 let mut found = Vec::new();
241 for token in tokens {
242 match token {
243 TokenTree::Ident(identifier) => {
244 found.push((identifier.span().start(), identifier.to_string()));
245 }
246 TokenTree::Group(group) => found.extend(located_identifiers(group.stream())),
247 TokenTree::Punct(_) | TokenTree::Literal(_) => {}
248 }
249 }
250 found
251}
252
253fn is_internal_path(segments: &[String]) -> bool {
254 segments.first().is_some_and(|root| {
255 INTERNAL_PATH_ROOTS.contains(&root.as_str()) || root.starts_with(WORKSPACE_CRATE_PREFIX)
256 })
257}
258
259#[cfg(test)]
260mod tests {
261 use std::path::PathBuf;
262
263 use super::{GodType, TypeMetrics, rank_god_types};
264 use crate::workspace::{CrateSource, SourceFile};
265
266 fn crate_with(files: &[(&str, &str)]) -> CrateSource {
267 CrateSource {
268 name: "sofie-sample".to_owned(),
269 files: files
270 .iter()
271 .map(|(path, text)| {
272 SourceFile::parse(PathBuf::from(path), text).expect("sample sources parse")
273 })
274 .collect(),
275 }
276 }
277
278 fn rank(files: &[(&str, &str)]) -> Vec<GodType> {
279 rank_god_types(&[crate_with(files)])
280 }
281
282 #[test]
283 fn score_follows_the_weighted_formula() {
284 let metrics = TypeMetrics {
285 code_lines: 300,
286 methods: 15,
287 public_methods: 10,
288 dependencies: 12,
289 };
290 assert!((metrics.score() - 4.0).abs() < 1e-9);
291 }
292
293 #[test]
294 fn measures_methods_public_methods_and_code_lines() {
295 let source = "struct Widget;\nimpl Widget {\n // why\n pub fn a() {}\n\n fn b() {}\n pub(crate) fn c() {}\n const D: u8 = 1;\n}\n";
296 let ranking = rank(&[("lib.rs", source)]);
297 assert_eq!(ranking.len(), 1);
298 assert_eq!(ranking[0].type_name, "Widget");
299 let expected = TypeMetrics {
300 code_lines: 6,
301 methods: 3,
302 public_methods: 1,
303 dependencies: 0,
304 };
305 assert_eq!(ranking[0].metrics, expected);
306 }
307
308 #[test]
309 fn merges_impls_of_one_type_across_files_and_ignores_trait_impls() {
310 let first = "use crate::a::B;\nimpl Widget { fn a() {} }\n";
311 let second = "use super::{c::D, e::*};\nimpl crate::Widget { fn b() {} }\nimpl Clone for Widget { fn clone(&self) -> Self { todo!() } }\n";
312 let ranking = rank(&[("a.rs", first), ("b.rs", second)]);
313 assert_eq!(ranking.len(), 1);
314 assert_eq!(ranking[0].metrics.methods, 2);
315 assert_eq!(ranking[0].metrics.code_lines, 2);
316 assert_eq!(
317 ranking[0].files,
318 [PathBuf::from("a.rs"), PathBuf::from("b.rs")]
319 );
320 }
321
322 #[test]
323 fn counts_the_internal_imports_the_impls_mention_and_every_glob() {
324 let source = "use std::fmt;\nuse sofie_core::Thing;\nuse crate::{a::{self, B}, a::B as C, unused::D, e::*};\nmod inner { use super::x::Y; }\nimpl Widget { fn f(_: fmt::Debug, _: Thing, _: a::Z, _: B, _: C, _: Y) {} }\nimpl Other {}\n";
325 let ranking = rank(&[("lib.rs", source)]);
326 let dependencies = |name: &str| {
327 ranking
328 .iter()
329 .find(|god_type| god_type.type_name == name)
330 .map(|god_type| god_type.metrics.dependencies)
331 };
332 assert_eq!(dependencies("Widget"), Some(5));
333 assert_eq!(dependencies("Other"), Some(1));
334 }
335
336 #[test]
337 fn finds_impls_inside_inline_modules_and_ranks_worst_first() {
338 let source =
339 "impl Small {}\nmod inner {\n impl Large { pub fn a() {} pub fn b() {} }\n}\n";
340 let ranking = rank(&[("lib.rs", source)]);
341 let names: Vec<&str> = ranking
342 .iter()
343 .map(|god_type| god_type.type_name.as_str())
344 .collect();
345 assert_eq!(names, ["Large", "Small"]);
346 }
347}