wowlab_tidy/languages/rust/rules/api/
similar_fns.rs1#[cfg(test)]
2use googletest::prelude::*;
3use wowlab_types::sim::FastMap;
4
5use crate::{
6 Example, Violation,
7 languages::workspace::{FunctionRecord, WorkspaceCtx},
8 matches_ignore, violation,
9};
10
11const MIN_SHARED_SHINGLES: usize = 5;
12const PERCENT_DENOMINATOR: usize = 100;
13const CANDIDATE_PERCENT: usize = 10;
14
15#[rustfmt::skip]
16const EXAMPLES: &[Example] = &[
17 Example {
18 label: "exact function bodies",
19 code: "fn one(a: i32) -> i32 { let b = a + 1; let c = b + 2; let d = c + 3; let e = d + 4; let f = e + 5; let g = f + 6; let h = g + 7; let i = h + 8; let j = i + 9; let k = j + 10; k }\nfn two(a: i32) -> i32 { let b = a + 1; let c = b + 2; let d = c + 3; let e = d + 4; let f = e + 5; let g = f + 6; let h = g + 7; let i = h + 8; let j = i + 9; let k = j + 10; k }",
20 pass: false,
21 },
22 Example {
23 label: "near function bodies",
24 code: "fn one(a: i32) -> i32 { let b = a + 1; let c = b + 2; let d = c + 3; let e = d + 4; let f = e + 5; let g = f + 6; let h = g + 7; let i = h + 8; let j = i + 9; let k = j + 10; k }\nfn two(a: i32) -> i32 { let b = a + 1; let c = b + 2; let d = c + 3; let e = d + 4; let f = e + 5; let g = f + 6; let h = g + 8; let i = h + 8; let j = i + 9; let k = j + 10; k }",
25 pass: false,
26 },
27 Example {
28 label: "short bodies are exempt",
29 code: "fn one() -> i32 { 1 }\nfn two() -> i32 { 1 }",
30 pass: true,
31 },
32];
33
34crate::workspace_rule!(
35 similar_fns,
36 "Find exact and near duplicate function bodies; full-workspace runs are authoritative.",
37 "Clone detection identifies behavior that should usually be shared behind one implementation.",
38 Low,
39 params {
40 min_tokens: i64 = 40,
41 jaccard_percent: i64 = 85
42 },
43);
44
45#[derive(Clone, Copy)]
46struct Located<'a> {
47 rel: &'a str,
48 record: &'a FunctionRecord,
49}
50
51fn check_similar_fns(ctx: &WorkspaceCtx<'_>) -> Vec<Violation> {
52 let min_tokens = ctx.config.get_usize("rust_similar_fns", &PARAMS[0]);
53 let jaccard_percent = ctx.config.get_usize("rust_similar_fns", &PARAMS[1]);
54 let records: Vec<Located<'_>> = ctx
55 .files
56 .iter()
57 .filter(|file| !matches_ignore(&file.rel, ctx.config.ignore_patterns("rust_similar_fns")))
58 .flat_map(|file| {
59 file.functions
60 .iter()
61 .filter(|function| function.body_token_count >= min_tokens)
62 .map(|record| Located {
63 rel: &file.rel,
64 record,
65 })
66 })
67 .collect();
68 let mut index = FastMap::<_, Vec<usize>>::default();
69
70 for (record_index, located) in records.iter().enumerate() {
71 for fingerprint in &located.record.body_shingles {
72 index.entry(*fingerprint).or_default().push(record_index);
73 }
74 }
75
76 let mut shared = FastMap::<(usize, usize), usize>::default();
77
78 for members in index.values() {
79 for (position, left) in members.iter().enumerate() {
80 for right in members.iter().skip(position + 1) {
81 *shared.entry((*left, *right)).or_default() += 1;
82 }
83 }
84 }
85
86 shared
87 .into_iter()
88 .filter_map(|((left_index, right_index), shared_count)| {
89 let left = *records.get(left_index)?;
90 let right = *records.get(right_index)?;
91 let left_shingles = &left.record.body_shingles;
92 let right_shingles = &right.record.body_shingles;
93 let smaller = left_shingles.len().min(right_shingles.len());
94 let threshold = MIN_SHARED_SHINGLES.max(
95 smaller
96 .saturating_mul(CANDIDATE_PERCENT)
97 .div_ceil(PERCENT_DENOMINATOR),
98 );
99
100 if shared_count < threshold {
101 return None;
102 }
103
104 let exact = left.record.body_checksum == right.record.body_checksum;
105 let union = left_shingles
106 .len()
107 .saturating_add(right_shingles.len())
108 .saturating_sub(shared_count);
109 let near = shared_count.saturating_mul(PERCENT_DENOMINATOR)
110 >= union.saturating_mul(jaccard_percent);
111
112 if !exact && !near {
113 return None;
114 }
115
116 let (anchor, counterpart) =
117 if (left.rel, left.record.line) > (right.rel, right.record.line) {
118 (left, right)
119 } else {
120 (right, left)
121 };
122
123 Some(violation(
124 anchor.rel,
125 anchor.record.line,
126 format!(
127 "{} function clone ({} shared shingles); similar to {}:{} ({})",
128 if exact { "exact" } else { "near" },
129 shared_count,
130 counterpart.rel,
131 counterpart.record.line,
132 counterpart.record.name
133 ),
134 ))
135 })
136 .collect()
137}
138
139crate::tidy_workspace_test!(check_similar_fns, {
140 crate::example_tests!(EXAMPLES, check_similar_fns);
141
142 #[gtest]
143 fn configured_test_paths_are_exempt() -> Result<()> {
144 let source = EXAMPLES[0].code;
145 let violations = crate::test_support::check_workspace_sources_with_ignore(
146 &[("crates/example/src/tests.rs", source)],
147 "rust_similar_fns",
148 &["**/tests.rs"],
149 check_similar_fns,
150 );
151
152 verify_true!(violations.is_empty())?;
153
154 Ok(())
155 }
156});