Skip to main content

cargo_quality/analyzers/
inline_comments.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4//! Inline comments analyzer for detecting non-doc comments in function bodies.
5//!
6//! This analyzer identifies inline comments (`//`) within function and method
7//! bodies, which violate the documentation standards. All explanations should
8//! be in doc comments (`///`), specifically in the `# Notes` section.
9
10use 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
17/// Analyzer for detecting inline comments inside functions and methods.
18///
19/// Finds non-doc comments within function bodies and suggests moving them
20/// to the function's doc block `# Notes` section with code context.
21///
22/// # Examples
23///
24/// Detects this pattern:
25/// ```ignore
26/// fn calculate() {
27///     let x = read_data();
28///     // Process the data
29///     let y = transform(x);
30/// }
31/// ```
32///
33/// Suggests adding to doc block:
34/// ```ignore
35/// /// Calculate something
36/// ///
37/// /// # Notes
38/// ///
39/// /// - Line 3: `let y = transform(x);` - Process the data
40/// fn calculate() {
41///     let x = read_data();
42///     let y = transform(x);
43/// }
44/// ```
45pub struct InlineCommentsAnalyzer;
46
47impl InlineCommentsAnalyzer {
48    /// Create new inline comments analyzer instance.
49    #[inline]
50    pub fn new() -> Self {
51        Self
52    }
53
54    /// Check function body for inline comments.
55    ///
56    /// Analyzes source code to find inline comments within function boundaries
57    /// and creates issues with suggestions to move them to doc blocks.
58    ///
59    /// # Arguments
60    ///
61    /// * `start_line` - First line of function body
62    /// * `end_line` - Last line of function body
63    /// * `lines` - Source code split into lines
64    ///
65    /// # Returns
66    ///
67    /// Vector of issues found
68    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    /// Find the code line that this comment describes.
121    ///
122    /// Looks for the next non-empty, non-comment line after the comment.
123    ///
124    /// # Arguments
125    ///
126    /// * `lines` - All source code lines
127    /// * `comment_idx` - Index of the comment line (0-based)
128    ///
129    /// # Returns
130    ///
131    /// Option with (line_index, line_content) of related code
132    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    /// Check standalone function for inline comments.
152    ///
153    /// # Arguments
154    ///
155    /// * `func` - Function item to analyze
156    /// * `lines` - Source code split into lines
157    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    /// Check impl block methods for inline comments.
166    ///
167    /// # Arguments
168    ///
169    /// * `impl_block` - Impl block to analyze
170    /// * `lines` - Source code split into lines
171    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}