Skip to main content

sqrust_rules/layout/
consistent_comment_style.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct ConsistentCommentStyle;
4
5impl Rule for ConsistentCommentStyle {
6    fn name(&self) -> &'static str {
7        "Layout/ConsistentCommentStyle"
8    }
9
10    fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
11        find_violations(&ctx.source, self.name())
12    }
13}
14
15/// A comment occurrence: style and byte offset of its start.
16#[derive(Clone, Copy, PartialEq)]
17enum CommentStyle {
18    Line,  // --
19    Block, // /* ... */
20}
21
22fn find_violations(source: &str, rule_name: &'static str) -> Vec<Diagnostic> {
23    let bytes = source.as_bytes();
24    let len = bytes.len();
25
26    let mut in_string = false;
27    let mut i = 0usize;
28
29    // Collect all comment occurrences as (style, byte_offset).
30    let mut occurrences: Vec<(CommentStyle, usize)> = Vec::new();
31
32    while i < len {
33        let byte = bytes[i];
34
35        // ── String tracking ────────────────────────────────────────────────
36        if in_string {
37            if byte == b'\'' {
38                // SQL '' escape: two consecutive single-quotes inside a string
39                if i + 1 < len && bytes[i + 1] == b'\'' {
40                    i += 2;
41                    continue;
42                }
43                in_string = false;
44            }
45            i += 1;
46            continue;
47        }
48
49        // Enter single-quoted string
50        if byte == b'\'' {
51            in_string = true;
52            i += 1;
53            continue;
54        }
55
56        // ── Line comment: -- ───────────────────────────────────────────────
57        if i + 1 < len && byte == b'-' && bytes[i + 1] == b'-' {
58            occurrences.push((CommentStyle::Line, i));
59            // Advance past the entire line
60            while i < len && bytes[i] != b'\n' {
61                i += 1;
62            }
63            continue;
64        }
65
66        // ── Block comment: /* ... */ ───────────────────────────────────────
67        if i + 1 < len && byte == b'/' && bytes[i + 1] == b'*' {
68            occurrences.push((CommentStyle::Block, i));
69            i += 2; // move past /*
70            // Advance past block comment body
71            while i < len {
72                if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'/' {
73                    i += 2; // move past */
74                    break;
75                }
76                i += 1;
77            }
78            continue;
79        }
80
81        i += 1;
82    }
83
84    if occurrences.is_empty() {
85        return Vec::new();
86    }
87
88    // Count each style
89    let line_count = occurrences.iter().filter(|(s, _)| *s == CommentStyle::Line).count();
90    let block_count = occurrences.iter().filter(|(s, _)| *s == CommentStyle::Block).count();
91
92    // If only one style is used, no violation
93    if line_count == 0 || block_count == 0 {
94        return Vec::new();
95    }
96
97    // Both styles present: determine which is the minority
98    // When counts are equal, the "minority" is whichever style was seen SECOND
99    // (i.e., the style that the first occurrence of the second style represents)
100    let minority_style = if line_count < block_count {
101        // line comments are rarer — flag the first line comment
102        CommentStyle::Line
103    } else if block_count < line_count {
104        // block comments are rarer — flag the first block comment
105        CommentStyle::Block
106    } else {
107        // Equal counts: flag the first occurrence of the style that appears second
108        // The second style seen is the one that was NOT the first comment in the file
109        let first_style = occurrences[0].0;
110        if first_style == CommentStyle::Line {
111            CommentStyle::Block
112        } else {
113            CommentStyle::Line
114        }
115    };
116
117    // Find the first occurrence of the minority style
118    let &(_, offset) = occurrences
119        .iter()
120        .find(|(s, _)| *s == minority_style)
121        .expect("minority style has at least one occurrence");
122
123    let (line, col) = byte_offset_to_line_col(source, offset);
124    vec![Diagnostic {
125        rule: rule_name,
126        message: "Inconsistent comment style: file mixes -- and /* */ comments".to_string(),
127        line,
128        col,
129    }]
130}
131
132/// Converts a byte offset into a 1-indexed (line, col) pair.
133fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
134    let mut line = 1usize;
135    let mut line_start = 0usize;
136    for (i, ch) in source.char_indices() {
137        if i == offset {
138            break;
139        }
140        if ch == '\n' {
141            line += 1;
142            line_start = i + 1;
143        }
144    }
145    let col = offset - line_start + 1;
146    (line, col)
147}