cargo_quality/analyzers/
inline_comments.rs1use masterror::AppResult;
11use syn::{File, ImplItem, Item, ItemFn, ItemImpl, spanned::Spanned, visit::Visit};
12
13use crate::analyzer::{AnalysisResult, Analyzer, Fix, Issue};
14
15pub struct InlineCommentsAnalyzer;
44
45impl InlineCommentsAnalyzer {
46 #[inline]
48 pub fn new() -> Self {
49 Self
50 }
51
52 fn check_block(start_line: usize, end_line: usize, lines: &[&str]) -> Vec<Issue> {
67 let mut issues = Vec::new();
68
69 if start_line >= end_line {
70 return issues;
71 }
72
73 for line_num in start_line..end_line {
74 let idx = line_num.saturating_sub(1);
75
76 let Some(line) = lines.get(idx) else {
77 continue;
78 };
79
80 let trimmed = line.trim();
81
82 if trimmed.starts_with("//") && !trimmed.starts_with("///") {
83 let comment_text = trimmed.trim_start_matches("//").trim();
84
85 let code_line = Self::find_related_code_line(lines, idx);
86
87 let suggestion = if let Some((_code_idx, code)) = code_line {
88 format!(
89 "Move to doc block # Notes section:\n/// - {} - `{}`",
90 comment_text,
91 code.trim()
92 )
93 } else {
94 format!("Move to doc block # Notes section:\n/// - {}", comment_text)
95 };
96
97 issues.push(Issue {
98 line: line_num,
99 column: 1,
100 message: format!("Inline comment found: \"{}\"\n{}", comment_text, suggestion),
101 fix: Fix::None
102 });
103 }
104 }
105
106 issues
107 }
108
109 fn find_related_code_line<'a>(
122 lines: &[&'a str],
123 comment_idx: usize
124 ) -> Option<(usize, &'a str)> {
125 for (offset, line) in lines.iter().enumerate().skip(comment_idx + 1) {
126 let trimmed = line.trim();
127
128 if trimmed.is_empty() || trimmed.starts_with("//") {
129 continue;
130 }
131
132 if !trimmed.starts_with('}') {
133 return Some((offset, line));
134 }
135 }
136
137 None
138 }
139
140 fn check_function(func: &ItemFn, lines: &[&str]) -> Vec<Issue> {
147 let span = func.block.span();
148 let start_line = span.start().line;
149 let end_line = span.end().line;
150
151 Self::check_block(start_line, end_line, lines)
152 }
153
154 fn check_impl_block(impl_block: &ItemImpl, lines: &[&str]) -> Vec<Issue> {
161 let mut issues = Vec::new();
162
163 for item in &impl_block.items {
164 if let ImplItem::Fn(method) = item {
165 let span = method.block.span();
166 let start_line = span.start().line;
167 let end_line = span.end().line;
168
169 issues.extend(Self::check_block(start_line, end_line, lines));
170 }
171 }
172
173 issues
174 }
175}
176
177impl Analyzer for InlineCommentsAnalyzer {
178 fn name(&self) -> &'static str {
179 "inline_comments"
180 }
181
182 fn analyze(&self, ast: &File, content: &str) -> AppResult<AnalysisResult> {
183 let lines: Vec<&str> = content.lines().collect();
184 let mut visitor = FunctionVisitor {
185 issues: Vec::new(),
186 lines: &lines
187 };
188 visitor.visit_file(ast);
189
190 Ok(AnalysisResult {
191 issues: visitor.issues,
192 fixable_count: 0
193 })
194 }
195
196 fn fix(&self, _ast: &mut File) -> AppResult<usize> {
197 Ok(0)
198 }
199}
200
201struct FunctionVisitor<'a> {
202 issues: Vec<Issue>,
203 lines: &'a [&'a str]
204}
205
206impl<'ast, 'a> Visit<'ast> for FunctionVisitor<'a> {
207 fn visit_item(&mut self, node: &'ast Item) {
208 match node {
209 Item::Fn(func) => {
210 let func_issues = InlineCommentsAnalyzer::check_function(func, self.lines);
211 self.issues.extend(func_issues);
212 }
213 Item::Impl(impl_block) => {
214 let impl_issues = InlineCommentsAnalyzer::check_impl_block(impl_block, self.lines);
215 self.issues.extend(impl_issues);
216 }
217 _ => {}
218 }
219 syn::visit::visit_item(self, node);
220 }
221}
222
223impl Default for InlineCommentsAnalyzer {
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_analyzer_name() {
235 let analyzer = InlineCommentsAnalyzer::new();
236 assert_eq!(analyzer.name(), "inline_comments");
237 }
238
239 #[test]
240 fn test_detect_inline_comment_in_function() {
241 let analyzer = InlineCommentsAnalyzer::new();
242 let content = r#"fn main() {
243 let x = 1;
244 // This is a comment
245 let y = 2;
246}"#;
247 let code = syn::parse_str(content).unwrap();
248
249 let result = analyzer.analyze(&code, content).unwrap();
250 assert_eq!(result.issues.len(), 1);
251 assert!(result.issues[0].message.contains("This is a comment"));
252 }
253
254 #[test]
255 fn test_ignore_doc_comments() {
256 let analyzer = InlineCommentsAnalyzer::new();
257 let content = r#"fn main() {
258 let x = 1;
259 /// This is a doc comment
260 let y = 2;
261}"#;
262 let code = syn::parse_str(content).unwrap();
263
264 let result = analyzer.analyze(&code, content).unwrap();
265 assert_eq!(result.issues.len(), 0);
266 }
267
268 #[test]
269 fn test_ignore_function_without_comments() {
270 let analyzer = InlineCommentsAnalyzer::new();
271 let content = r#"fn main() {
272 let x = 1;
273 let y = 2;
274}"#;
275 let code = syn::parse_str(content).unwrap();
276
277 let result = analyzer.analyze(&code, content).unwrap();
278 assert_eq!(result.issues.len(), 0);
279 }
280
281 #[test]
282 fn test_detect_multiple_comments() {
283 let analyzer = InlineCommentsAnalyzer::new();
284 let content = r#"fn process() {
285 // Read data
286 let x = read();
287 // Transform
288 let y = transform(x);
289 // Write result
290 write(y);
291}"#;
292 let code = syn::parse_str(content).unwrap();
293
294 let result = analyzer.analyze(&code, content).unwrap();
295 assert_eq!(result.issues.len(), 3);
296 }
297
298 #[test]
299 fn test_comment_with_code_context() {
300 let analyzer = InlineCommentsAnalyzer::new();
301 let content = r#"fn main() {
302 // Calculate sum
303 let sum = a + b;
304}"#;
305 let code = syn::parse_str(content).unwrap();
306
307 let result = analyzer.analyze(&code, content).unwrap();
308 assert_eq!(result.issues.len(), 1);
309 assert!(result.issues[0].message.contains("Calculate sum"));
310 assert!(result.issues[0].message.contains("`let sum = a + b;`"));
311 }
312
313 #[test]
314 fn test_detect_comment_in_method() {
315 let analyzer = InlineCommentsAnalyzer::new();
316 let content = r#"struct Foo;
317
318impl Foo {
319 fn method(&self) {
320 // Process data
321 let x = 1;
322 }
323}"#;
324 let code = syn::parse_str(content).unwrap();
325
326 let result = analyzer.analyze(&code, content).unwrap();
327 assert_eq!(result.issues.len(), 1);
328 assert!(result.issues[0].message.contains("Process data"));
329 }
330
331 #[test]
332 fn test_multiple_methods_with_comments() {
333 let analyzer = InlineCommentsAnalyzer::new();
334 let content = r#"struct Foo;
335
336impl Foo {
337 fn first(&self) {
338 // Comment 1
339 let a = 1;
340 }
341
342 fn second(&self) {
343 // Comment 2
344 let b = 2;
345 }
346}"#;
347 let code = syn::parse_str(content).unwrap();
348
349 let result = analyzer.analyze(&code, content).unwrap();
350 assert_eq!(result.issues.len(), 2);
351 }
352
353 #[test]
354 fn test_fixable_count_is_zero() {
355 let analyzer = InlineCommentsAnalyzer::new();
356 let content = r#"fn main() {
357 // Comment
358 let x = 1;
359}"#;
360 let code = syn::parse_str(content).unwrap();
361
362 let result = analyzer.analyze(&code, content).unwrap();
363 assert_eq!(result.fixable_count, 0);
364 }
365
366 #[test]
367 fn test_fix_returns_zero() {
368 let analyzer = InlineCommentsAnalyzer::new();
369 let content = r#"fn main() {
370 // Comment
371 let x = 1;
372}"#;
373 let mut code = syn::parse_str(content).unwrap();
374
375 let fixed = analyzer.fix(&mut code).unwrap();
376 assert_eq!(fixed, 0);
377 }
378
379 #[test]
380 fn test_default_implementation() {
381 let analyzer = InlineCommentsAnalyzer;
382 assert_eq!(analyzer.name(), "inline_comments");
383 }
384
385 #[test]
386 fn test_comment_before_closing_brace() {
387 let analyzer = InlineCommentsAnalyzer::new();
388 let content = r#"fn main() {
389 let x = 1;
390 // Final comment
391}"#;
392 let code = syn::parse_str(content).unwrap();
393
394 let result = analyzer.analyze(&code, content).unwrap();
395 assert_eq!(result.issues.len(), 1);
396 }
397
398 #[test]
399 fn test_empty_comment() {
400 let analyzer = InlineCommentsAnalyzer::new();
401 let content = r#"fn main() {
402 //
403 let x = 1;
404}"#;
405 let code = syn::parse_str(content).unwrap();
406
407 let result = analyzer.analyze(&code, content).unwrap();
408 assert_eq!(result.issues.len(), 1);
409 }
410
411 #[test]
412 fn test_comment_with_multiple_slashes() {
413 let analyzer = InlineCommentsAnalyzer::new();
414 let content = r#"fn main() {
415 //// Comment
416 let x = 1;
417}"#;
418 let code = syn::parse_str(content).unwrap();
419
420 let result = analyzer.analyze(&code, content).unwrap();
421 assert_eq!(result.issues.len(), 0);
422 }
423
424 #[test]
425 fn test_nested_blocks_with_comments() {
426 let analyzer = InlineCommentsAnalyzer::new();
427 let content = r#"fn main() {
428 if true {
429 // Nested comment
430 let x = 1;
431 }
432}"#;
433 let code = syn::parse_str(content).unwrap();
434
435 let result = analyzer.analyze(&code, content).unwrap();
436 assert_eq!(result.issues.len(), 1);
437 }
438}