Skip to content

Commit 196838e

Browse files
committed
WIP: move analysis to another module
1 parent ec656e3 commit 196838e

File tree

2 files changed

+260
-190
lines changed

2 files changed

+260
-190
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
//! Analyzer module for clippy-annotation-reporter
2+
//!
3+
//! This module handles all the logic related to analyzing clippy annotations
4+
//! including getting changed files, comparing branches, and producing analysis results.
5+
6+
use anyhow::{Context as _, Result};
7+
use octocrab::Octocrab;
8+
use regex::Regex;
9+
use std::collections::{HashMap, HashSet};
10+
use std::process::Command;
11+
12+
/// Represents a clippy annotation in code
13+
#[derive(Debug, Clone)]
14+
pub struct ClippyAnnotation {
15+
pub file: String,
16+
pub line_number: usize,
17+
pub rule: String,
18+
pub line_content: String,
19+
}
20+
21+
/// Result of annotation analysis
22+
pub struct AnalysisResult {
23+
pub base_annotations: Vec<ClippyAnnotation>,
24+
pub head_annotations: Vec<ClippyAnnotation>,
25+
pub base_counts: HashMap<String, usize>,
26+
pub head_counts: HashMap<String, usize>,
27+
pub changed_files: HashSet<String>,
28+
}
29+
30+
/// Analyzer struct for managing the analysis process
31+
pub struct Analyzer<'a> {
32+
octocrab: &'a Octocrab,
33+
owner: String,
34+
repo: String,
35+
pr_number: u64,
36+
base_branch: String,
37+
head_branch: String,
38+
rules: Vec<String>,
39+
}
40+
41+
impl<'a> Analyzer<'a> {
42+
/// Create a new analyzer instance
43+
pub fn new(
44+
octocrab: &'a Octocrab,
45+
owner: &str,
46+
repo: &str,
47+
pr_number: u64,
48+
base_branch: &str,
49+
head_branch: &str,
50+
rules: &[String],
51+
) -> Self {
52+
Self {
53+
octocrab,
54+
owner: owner.to_string(),
55+
repo: repo.to_string(),
56+
pr_number,
57+
base_branch: base_branch.to_string(),
58+
head_branch: head_branch.to_string(),
59+
rules: rules.to_vec(),
60+
}
61+
}
62+
63+
/// Run the full analysis and return the results
64+
pub async fn analyze(&self) -> Result<AnalysisResult> {
65+
// Get changed files
66+
let changed_files = self.get_changed_files().await?;
67+
68+
if changed_files.is_empty() {
69+
return Err(anyhow::anyhow!("No Rust files changed in this PR"));
70+
}
71+
72+
// Analyze annotations
73+
let result = self.analyze_annotations(&changed_files)?;
74+
75+
Ok(result)
76+
}
77+
78+
/// Get changed Rust files from the PR
79+
async fn get_changed_files(&self) -> Result<Vec<String>> {
80+
println!("Getting changed files from PR #{}...", self.pr_number);
81+
82+
let files = self
83+
.octocrab
84+
.pulls(&self.owner, &self.repo)
85+
.list_files(self.pr_number)
86+
.await
87+
.context("Failed to list PR files")?;
88+
89+
// Filter for Rust files only
90+
let rust_files: Vec<String> = files
91+
.items
92+
.into_iter()
93+
.filter(|file| file.filename.ends_with(".rs"))
94+
.map(|file| file.filename)
95+
.collect();
96+
97+
println!("Found {} changed Rust files", rust_files.len());
98+
99+
Ok(rust_files)
100+
}
101+
102+
/// Analyze clippy annotations in base and head branches
103+
fn analyze_annotations(&self, files: &[String]) -> Result<AnalysisResult> {
104+
println!("Analyzing clippy annotations in {} files...", files.len());
105+
106+
// Create a regex for matching clippy allow annotations
107+
let rule_pattern = self.rules.join("|");
108+
let annotation_regex = Regex::new(&format!(
109+
r"#\s*\[\s*allow\s*\(\s*clippy\s*::\s*({})\s*\)\s*\]",
110+
rule_pattern
111+
))
112+
.context("Failed to compile annotation regex")?;
113+
114+
let mut base_annotations = Vec::new();
115+
let mut head_annotations = Vec::new();
116+
let mut changed_files = HashSet::new();
117+
118+
// Process each file
119+
for file in files {
120+
changed_files.insert(file.clone());
121+
122+
// Get file content from base branch
123+
let base_content = match self.get_file_content(file, &self.base_branch) {
124+
Ok(content) => content,
125+
Err(e) => {
126+
println!(
127+
"Warning: Failed to get {} content from {}: {}",
128+
file, self.base_branch, e
129+
);
130+
String::new()
131+
}
132+
};
133+
134+
// Get file content from head branch
135+
let head_content = match self.get_file_content(file, &self.head_branch) {
136+
Ok(content) => content,
137+
Err(e) => {
138+
println!(
139+
"Warning: Failed to get {} content from {}: {}",
140+
file, self.head_branch, e
141+
);
142+
String::new()
143+
}
144+
};
145+
146+
// Find annotations in base branch
147+
self.find_annotations(
148+
&mut base_annotations,
149+
file,
150+
&base_content,
151+
&annotation_regex,
152+
);
153+
154+
// Find annotations in head branch
155+
self.find_annotations(
156+
&mut head_annotations,
157+
file,
158+
&head_content,
159+
&annotation_regex,
160+
);
161+
}
162+
163+
// Count annotations by rule
164+
let base_counts = self.count_annotations_by_rule(&base_annotations);
165+
let head_counts = self.count_annotations_by_rule(&head_annotations);
166+
167+
println!(
168+
"Analysis complete. Found {} annotations in base branch and {} in head branch",
169+
base_annotations.len(),
170+
head_annotations.len()
171+
);
172+
173+
Ok(AnalysisResult {
174+
base_annotations,
175+
head_annotations,
176+
base_counts,
177+
head_counts,
178+
changed_files,
179+
})
180+
}
181+
182+
/// Get file content from a specific branch
183+
fn get_file_content(&self, file: &str, branch: &str) -> Result<String> {
184+
println!("Getting content for {} from {}", file, branch);
185+
186+
let output = Command::new("git")
187+
.args(["show", &format!("{}:{}", branch, file)])
188+
.output()
189+
.context(format!("Failed to execute git show command for {}", file))?;
190+
191+
if !output.status.success() {
192+
let stderr = String::from_utf8_lossy(&output.stderr);
193+
anyhow::bail!("Git show command failed: {}", stderr);
194+
}
195+
196+
let content =
197+
String::from_utf8(output.stdout).context("Failed to parse file content as UTF-8")?;
198+
199+
Ok(content)
200+
}
201+
202+
/// Find clippy annotations in file content
203+
fn find_annotations(
204+
&self,
205+
annotations: &mut Vec<ClippyAnnotation>,
206+
file: &str,
207+
content: &str,
208+
regex: &Regex,
209+
) {
210+
for (line_number, line) in content.lines().enumerate() {
211+
if let Some(captures) = regex.captures(line) {
212+
if let Some(rule_match) = captures.get(1) {
213+
let rule = rule_match.as_str().to_string();
214+
annotations.push(ClippyAnnotation {
215+
file: file.to_string(),
216+
line_number: line_number + 1,
217+
rule,
218+
line_content: line.trim().to_string(),
219+
});
220+
}
221+
}
222+
}
223+
}
224+
225+
/// Count annotations by rule
226+
fn count_annotations_by_rule(
227+
&self,
228+
annotations: &[ClippyAnnotation],
229+
) -> HashMap<String, usize> {
230+
let mut counts = HashMap::new();
231+
232+
for annotation in annotations {
233+
*counts.entry(annotation.rule.clone()).or_insert(0) += 1;
234+
}
235+
236+
counts
237+
}
238+
}

0 commit comments

Comments
 (0)