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 masterror::AppResult;
11use syn::{File, ImplItem, Item, ItemFn, ItemImpl, spanned::Spanned, visit::Visit};
12
13use crate::analyzer::{AnalysisResult, Analyzer, Fix, Issue};
14
15/// Analyzer for detecting inline comments inside functions and methods.
16///
17/// Finds non-doc comments within function bodies and suggests moving them
18/// to the function's doc block `# Notes` section with code context.
19///
20/// # Examples
21///
22/// Detects this pattern:
23/// ```ignore
24/// fn calculate() {
25///     let x = read_data();
26///     // Process the data
27///     let y = transform(x);
28/// }
29/// ```
30///
31/// Suggests adding to doc block:
32/// ```ignore
33/// /// Calculate something
34/// ///
35/// /// # Notes
36/// ///
37/// /// - Line 3: `let y = transform(x);` - Process the data
38/// fn calculate() {
39///     let x = read_data();
40///     let y = transform(x);
41/// }
42/// ```
43pub struct InlineCommentsAnalyzer;
44
45impl InlineCommentsAnalyzer {
46    /// Create new inline comments analyzer instance.
47    #[inline]
48    pub fn new() -> Self {
49        Self
50    }
51
52    /// Check function body for inline comments.
53    ///
54    /// Analyzes source code to find inline comments within function boundaries
55    /// and creates issues with suggestions to move them to doc blocks.
56    ///
57    /// # Arguments
58    ///
59    /// * `start_line` - First line of function body
60    /// * `end_line` - Last line of function body
61    /// * `lines` - Source code split into lines
62    ///
63    /// # Returns
64    ///
65    /// Vector of issues found
66    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    /// Find the code line that this comment describes.
110    ///
111    /// Looks for the next non-empty, non-comment line after the comment.
112    ///
113    /// # Arguments
114    ///
115    /// * `lines` - All source code lines
116    /// * `comment_idx` - Index of the comment line (0-based)
117    ///
118    /// # Returns
119    ///
120    /// Option with (line_index, line_content) of related code
121    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    /// Check standalone function for inline comments.
141    ///
142    /// # Arguments
143    ///
144    /// * `func` - Function item to analyze
145    /// * `lines` - Source code split into lines
146    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    /// Check impl block methods for inline comments.
155    ///
156    /// # Arguments
157    ///
158    /// * `impl_block` - Impl block to analyze
159    /// * `lines` - Source code split into lines
160    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}