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