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