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, 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 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 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 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}