Skip to main content

sqrust_rules/layout/
arithmetic_operator_at_line_end.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct ArithmeticOperatorAtLineEnd;
4
5impl Rule for ArithmeticOperatorAtLineEnd {
6    fn name(&self) -> &'static str {
7        "Layout/ArithmeticOperatorAtLineEnd"
8    }
9
10    fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
11        let mut diags = Vec::new();
12
13        for (line_num, line) in ctx.lines() {
14            let trimmed = line.trim_end();
15            if trimmed.is_empty() {
16                continue;
17            }
18
19            // Skip whole-line comments (-- ...)
20            let stripped = trimmed.trim_start();
21            if stripped.starts_with("--") {
22                continue;
23            }
24
25            // Get the last character of the trimmed line
26            let last_char = match trimmed.chars().last() {
27                Some(c) => c,
28                None => continue,
29            };
30
31            // Determine whether the trailing character is a flaggable operator.
32            // Rules:
33            //   '+' => flag
34            //   '/' => flag
35            //   '-' => flag only if preceded by a non-'-' character (guards against trailing --)
36            //   '*' => exempt (SELECT *, COUNT(*))
37            let op_char: Option<char> = match last_char {
38                '+' | '/' => Some(last_char),
39                '-' => {
40                    // Look at the second-to-last char to rule out "--"
41                    let second_last = trimmed.chars().rev().nth(1);
42                    if second_last == Some('-') {
43                        None
44                    } else {
45                        Some('-')
46                    }
47                }
48                _ => None,
49            };
50
51            let op_char = match op_char {
52                Some(c) => c,
53                None => continue,
54            };
55
56            // Check whether the operator character is inside a single-quoted string.
57            // We do a simple single-pass scan of the trimmed line tracking string state.
58            // This handles single-line strings correctly.  Cross-line strings are uncommon
59            // in SQL and are treated as outside-string for the purpose of this check.
60            if operator_is_in_string(trimmed) {
61                continue;
62            }
63
64            // col is 1-indexed position of the trailing operator character.
65            // trimmed has the same leading content as line, just trailing whitespace stripped,
66            // so trimmed.len() characters are already the exact byte-length up to (and
67            // including) the operator.
68            let col = trimmed.len();
69
70            diags.push(Diagnostic {
71                rule: self.name(),
72                message: format!(
73                    "Arithmetic operator '{}' at line end; move to start of next line for clarity",
74                    op_char
75                ),
76                line: line_num,
77                col,
78            });
79        }
80
81        diags
82    }
83}
84
85/// Returns true when the last character of `line` is inside a single-quoted
86/// string literal.  Handles escaped-quote via doubled-quote (`''`) convention.
87fn operator_is_in_string(line: &str) -> bool {
88    let mut in_string = false;
89    let chars: Vec<char> = line.chars().collect();
90    let len = chars.len();
91    let mut i = 0;
92
93    while i < len {
94        let c = chars[i];
95        if in_string {
96            if c == '\'' {
97                // Peek ahead: '' is an escaped quote, stay in string
98                if i + 1 < len && chars[i + 1] == '\'' {
99                    i += 2;
100                    continue;
101                }
102                in_string = false;
103            }
104        } else if c == '\'' {
105            in_string = true;
106        }
107        i += 1;
108    }
109
110    // If we finished the scan still inside a string, the last character is in
111    // a string (the string didn't close on this line).
112    in_string
113}