Skip to main content

sqrust_rules/layout/
comment_style.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct CommentStyle;
4
5impl Rule for CommentStyle {
6    fn name(&self) -> &'static str {
7        "Layout/CommentStyle"
8    }
9
10    fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
11        find_violations(&ctx.source, self.name())
12    }
13}
14
15fn find_violations(source: &str, rule_name: &'static str) -> Vec<Diagnostic> {
16    let bytes = source.as_bytes();
17    let len = bytes.len();
18    let mut diags = Vec::new();
19
20    let mut i = 0usize;
21    let mut in_string = false;
22
23    while i < len {
24        let byte = bytes[i];
25
26        // ── String tracking ────────────────────────────────────────────────
27        if in_string {
28            if byte == b'\'' {
29                // SQL '' escape: two consecutive single-quotes inside a string
30                if i + 1 < len && bytes[i + 1] == b'\'' {
31                    i += 2;
32                    continue;
33                }
34                in_string = false;
35            }
36            i += 1;
37            continue;
38        }
39
40        // Enter single-quoted string
41        if byte == b'\'' {
42            in_string = true;
43            i += 1;
44            continue;
45        }
46
47        // ── Skip -- line comment (to end of line) ─────────────────────────
48        if i + 1 < len && byte == b'-' && bytes[i + 1] == b'-' {
49            // Advance past the entire line so we don't misidentify anything inside
50            while i < len && bytes[i] != b'\n' {
51                i += 1;
52            }
53            continue;
54        }
55
56        // ── Block comment /* ... */ ────────────────────────────────────────
57        if i + 1 < len && byte == b'/' && bytes[i + 1] == b'*' {
58            let start = i;
59            i += 2; // move past /*
60
61            let mut has_newline = false;
62            let mut closed = false;
63
64            while i < len {
65                if bytes[i] == b'\n' {
66                    has_newline = true;
67                }
68                if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'/' {
69                    i += 2; // move past */
70                    closed = true;
71                    break;
72                }
73                i += 1;
74            }
75
76            // Unclosed block comment: treat as single-line (no newline found)
77            if !has_newline || !closed {
78                // Only flag if there was no newline (i.e. single-line usage)
79                if !has_newline {
80                    let (line, col) = byte_offset_to_line_col(source, start);
81                    diags.push(Diagnostic {
82                        rule: rule_name,
83                        message:
84                            "Single-line /* */ comment; use -- for single-line comments"
85                                .to_string(),
86                        line,
87                        col,
88                    });
89                }
90            }
91
92            continue;
93        }
94
95        i += 1;
96    }
97
98    diags
99}
100
101/// Converts a byte offset into a 1-indexed (line, col) pair.
102fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
103    let mut line = 1usize;
104    let mut line_start = 0usize;
105    for (i, ch) in source.char_indices() {
106        if i == offset {
107            break;
108        }
109        if ch == '\n' {
110            line += 1;
111            line_start = i + 1;
112        }
113    }
114    let col = offset - line_start + 1;
115    (line, col)
116}