Skip to content

Commit 1ef0359

Browse files
committed
add the analyzer module to clippy-annotation-reporter. This module
reviews the files in the repo to parse and count the allow annotations usage. It also compares changed files to their base to determine the diffs of counts.
1 parent 89037c4 commit 1ef0359

File tree

4 files changed

+1448
-0
lines changed

4 files changed

+1448
-0
lines changed
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Functions for finding and parsing clippy annotations
5+
6+
use crate::analyzer::crate_detection::get_crate_for_file;
7+
use crate::analyzer::ClippyAnnotation;
8+
use anyhow::{Context, Result};
9+
use regex::Regex;
10+
use std::collections::HashMap;
11+
use std::rc::Rc;
12+
13+
/// Find clippy annotations in file content
14+
pub(super) fn find_annotations(
15+
annotations: &mut Vec<ClippyAnnotation>,
16+
file: &str,
17+
content: &str,
18+
regex: &Regex,
19+
rule_cache: &mut HashMap<String, Rc<String>>,
20+
) {
21+
// Use Rc for file path
22+
let file_rc = Rc::new(file.to_owned());
23+
24+
for line in content.lines() {
25+
if let Some(captures) = regex.captures(line) {
26+
if let Some(rule_match) = captures.get(1) {
27+
let rule_str = rule_match.as_str().to_owned();
28+
29+
// Get or create Rc for this rule
30+
let rule_rc = match rule_cache.get(&rule_str) {
31+
Some(cached) => Rc::clone(cached),
32+
None => {
33+
let rc = Rc::new(rule_str.clone());
34+
rule_cache.insert(rule_str, Rc::clone(&rc));
35+
rc
36+
}
37+
};
38+
39+
annotations.push(ClippyAnnotation {
40+
file: Rc::clone(&file_rc),
41+
rule: rule_rc,
42+
});
43+
}
44+
}
45+
}
46+
}
47+
48+
/// Count annotations by rule
49+
pub(super) fn count_annotations_by_rule(
50+
annotations: &[ClippyAnnotation],
51+
) -> HashMap<Rc<String>, usize> {
52+
let mut counts = HashMap::with_capacity(annotations.len().min(20));
53+
54+
for annotation in annotations {
55+
*counts.entry(Rc::clone(&annotation.rule)).or_insert(0) += 1;
56+
}
57+
58+
counts
59+
}
60+
61+
/// Count annotations by crate
62+
pub(super) fn count_annotations_by_crate(
63+
annotations: &[ClippyAnnotation],
64+
) -> HashMap<Rc<String>, usize> {
65+
let mut counts = HashMap::new();
66+
let mut crate_cache: HashMap<String, Rc<String>> = HashMap::new();
67+
68+
for annotation in annotations {
69+
let file_path = annotation.file.as_str();
70+
71+
// Use cached crate name if we've seen this file before
72+
let crate_name = match crate_cache.get(file_path) {
73+
Some(name) => name.clone(),
74+
None => {
75+
let name = Rc::new(get_crate_for_file(file_path).to_owned());
76+
crate_cache.insert(file_path.to_owned(), Rc::clone(&name));
77+
78+
name
79+
}
80+
};
81+
82+
*counts.entry(crate_name).or_insert(0) += 1;
83+
}
84+
85+
counts
86+
}
87+
88+
/// Create a regex for matching clippy allow annotations
89+
pub(super) fn create_annotation_regex(rules: &[String]) -> Result<Regex> {
90+
if rules.is_empty() {
91+
return Err(anyhow::anyhow!("Cannot create regex with empty rules list"));
92+
}
93+
94+
let rule_pattern = rules.join("|");
95+
let regex = Regex::new(&format!(
96+
r"#\s*\[\s*allow\s*\(\s*clippy\s*::\s*({})\s*\)\s*\]",
97+
rule_pattern
98+
))
99+
.context("Failed to compile annotation regex")?;
100+
101+
Ok(regex)
102+
}
103+
104+
#[cfg(test)]
105+
mod tests {
106+
use super::*;
107+
use std::rc::Rc;
108+
109+
#[test]
110+
fn test_count_annotations_by_rule() {
111+
// Create test annotations
112+
let rule1 = Rc::new("clippy::unwrap_used".to_owned());
113+
let rule2 = Rc::new("clippy::match_bool".to_owned());
114+
let file = Rc::new("src/main.rs".to_owned());
115+
116+
let annotations = vec![
117+
ClippyAnnotation {
118+
file: file.clone(),
119+
rule: rule1.clone(),
120+
},
121+
ClippyAnnotation {
122+
file: file.clone(),
123+
rule: rule1.clone(),
124+
},
125+
ClippyAnnotation {
126+
file: file.clone(),
127+
rule: rule2.clone(),
128+
},
129+
ClippyAnnotation {
130+
file: file.clone(),
131+
rule: rule1.clone(),
132+
},
133+
];
134+
135+
let counts = count_annotations_by_rule(&annotations);
136+
137+
assert_eq!(counts.len(), 2, "Should have counts for 2 rules");
138+
assert_eq!(counts[&rule1], 3, "Rule1 should have 3 annotations");
139+
assert_eq!(counts[&rule2], 1, "Rule2 should have 1 annotation");
140+
}
141+
142+
#[test]
143+
fn test_count_annotations_by_rule_empty() {
144+
// Test with empty annotations
145+
let annotations: Vec<ClippyAnnotation> = vec![];
146+
let counts = count_annotations_by_rule(&annotations);
147+
148+
assert_eq!(
149+
counts.len(),
150+
0,
151+
"Empty annotations should produce empty counts"
152+
);
153+
}
154+
155+
#[test]
156+
fn test_count_annotations_by_crate() {
157+
use std::fs::{self, File};
158+
use std::io::Write;
159+
use tempfile::TempDir;
160+
161+
// Create a temporary directory structure for testing
162+
let temp_dir = TempDir::new().expect("Failed to create temp directory");
163+
let temp_path = temp_dir.path();
164+
165+
// Create a directory structure with two different crates
166+
// crate1
167+
// ├── Cargo.toml (package.name = "crate1")
168+
// └── src
169+
// ├── lib.rs
170+
// └── module.rs
171+
// crate2
172+
// ├── Cargo.toml (package.name = "crate2")
173+
// └── src
174+
// └── main.rs
175+
176+
// Create directories
177+
let crate1_dir = temp_path.join("crate1");
178+
let crate1_src_dir = crate1_dir.join("src");
179+
let crate2_dir = temp_path.join("crate2");
180+
let crate2_src_dir = crate2_dir.join("src");
181+
182+
fs::create_dir_all(&crate1_src_dir).expect("Failed to create crate1/src directory");
183+
fs::create_dir_all(&crate2_src_dir).expect("Failed to create crate2/src directory");
184+
185+
// Create Cargo.toml files with specific package names
186+
let crate1_cargo = crate1_dir.join("Cargo.toml");
187+
let mut cargo1_file =
188+
File::create(&crate1_cargo).expect("Failed to create crate1 Cargo.toml");
189+
writeln!(
190+
cargo1_file,
191+
r#"[package]
192+
name = "crate1"
193+
version = "0.1.0"
194+
edition = "2021"
195+
"#
196+
)
197+
.expect("Failed to write to crate1 Cargo.toml");
198+
199+
let crate2_cargo = crate2_dir.join("Cargo.toml");
200+
let mut cargo2_file =
201+
File::create(&crate2_cargo).expect("Failed to create crate2 Cargo.toml");
202+
writeln!(
203+
cargo2_file,
204+
r#"[package]
205+
name = "crate2"
206+
version = "0.1.0"
207+
edition = "2021"
208+
"#
209+
)
210+
.expect("Failed to write to crate2 Cargo.toml");
211+
212+
// Create source files
213+
let crate1_lib = crate1_src_dir.join("lib.rs");
214+
let mut lib_file = File::create(&crate1_lib).expect("Failed to create lib.rs");
215+
writeln!(lib_file, "// Empty lib file").expect("Failed to write to lib.rs");
216+
217+
let crate1_module = crate1_src_dir.join("module.rs");
218+
let mut module_file = File::create(&crate1_module).expect("Failed to create module.rs");
219+
writeln!(module_file, "// Empty module file").expect("Failed to write to module.rs");
220+
221+
let crate2_main = crate2_src_dir.join("main.rs");
222+
let mut main_file = File::create(&crate2_main).expect("Failed to create main.rs");
223+
writeln!(main_file, "// Empty main file").expect("Failed to write to main.rs");
224+
225+
// Create test annotations with the real file paths
226+
let rule = Rc::new("clippy::unwrap_used".to_owned());
227+
228+
let crate1_lib_path = Rc::new(crate1_lib.to_string_lossy().to_string());
229+
let crate1_module_path = Rc::new(crate1_module.to_string_lossy().to_string());
230+
let crate2_main_path = Rc::new(crate2_main.to_string_lossy().to_string());
231+
232+
let annotations = vec![
233+
ClippyAnnotation {
234+
file: crate1_lib_path.clone(),
235+
rule: rule.clone(),
236+
},
237+
ClippyAnnotation {
238+
file: crate1_module_path.clone(),
239+
rule: rule.clone(),
240+
},
241+
ClippyAnnotation {
242+
file: crate1_module_path.clone(), // Another annotation in the same file
243+
rule: rule.clone(),
244+
},
245+
ClippyAnnotation {
246+
file: crate2_main_path.clone(),
247+
rule: rule.clone(),
248+
},
249+
];
250+
251+
let counts = count_annotations_by_crate(&annotations);
252+
253+
assert_eq!(counts.len(), 2, "Should have counts for 2 crates");
254+
255+
let crate1_count = counts
256+
.iter()
257+
.find(|(k, _)| k.contains("crate1"))
258+
.map(|(_, v)| *v)
259+
.unwrap_or(0);
260+
261+
let crate2_count = counts
262+
.iter()
263+
.find(|(k, _)| k.contains("crate2"))
264+
.map(|(_, v)| *v)
265+
.unwrap_or(0);
266+
267+
assert_eq!(crate1_count, 3, "crate1 should have 3 annotations");
268+
assert_eq!(crate2_count, 1, "crate2 should have 1 annotation");
269+
}
270+
271+
#[test]
272+
fn test_count_annotations_by_crate_empty() {
273+
// Test with empty annotations
274+
let annotations: Vec<ClippyAnnotation> = vec![];
275+
let counts = count_annotations_by_crate(&annotations);
276+
277+
assert_eq!(
278+
counts.len(),
279+
0,
280+
"Empty annotations should produce empty counts"
281+
);
282+
}
283+
284+
#[test]
285+
fn test_create_annotation_regex_single_rule() {
286+
let rules = vec!["unwrap_used".to_owned()]; // Rule without clippy:: prefix
287+
let regex = create_annotation_regex(&rules).expect("Failed to create regex");
288+
289+
// Test matching
290+
assert!(regex.is_match("#[allow(clippy::unwrap_used)]"));
291+
assert!(regex.is_match("#[allow(clippy:: unwrap_used )]")); // With spaces
292+
assert!(regex.is_match("# [ allow ( clippy :: unwrap_used ) ]")); // With more spaces
293+
294+
// Test non-matching
295+
assert!(!regex.is_match("#[allow(clippy::unused_imports)]"));
296+
assert!(!regex.is_match("#[allow(unwrap_used)]")); // Missing clippy::
297+
assert!(!regex.is_match("clippy::unwrap_used")); // Missing #[allow()]
298+
}
299+
#[test]
300+
fn test_create_annotation_regex_multiple_rules() {
301+
let rules = vec!["unwrap_used".to_owned(), "match_bool".to_owned()];
302+
let regex = create_annotation_regex(&rules).expect("Failed to create regex");
303+
304+
assert!(regex.is_match("#[allow(clippy::unwrap_used)]"));
305+
assert!(regex.is_match("#[allow(clippy::match_bool)]"));
306+
307+
// Test mixed spacing and formatting
308+
assert!(regex.is_match("#[allow(clippy:: unwrap_used )]")); // With spaces
309+
assert!(regex.is_match("# [ allow ( clippy :: match_bool ) ]")); // With more spaces
310+
311+
// Test non-matching
312+
assert!(!regex.is_match("#[allow(clippy::unused_imports)]"));
313+
assert!(!regex.is_match("#[allow(unwrap_used)]")); // Missing clippy::
314+
}
315+
316+
#[test]
317+
fn test_create_annotation_regex_empty_rules() {
318+
let rules: Vec<String> = vec![];
319+
let result = create_annotation_regex(&rules);
320+
321+
assert!(
322+
result.is_err(),
323+
"Creating regex with empty rules should fail"
324+
);
325+
}
326+
}

0 commit comments

Comments
 (0)