Skip to main content

sqrust_rules/layout/
max_statement_length.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct MaxStatementLength {
4    pub max_lines: usize,
5}
6
7impl Default for MaxStatementLength {
8    fn default() -> Self {
9        MaxStatementLength { max_lines: 50 }
10    }
11}
12
13impl Rule for MaxStatementLength {
14    fn name(&self) -> &'static str {
15        "Layout/MaxStatementLength"
16    }
17
18    fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
19        let mut diags = Vec::new();
20        let statements = split_statements(&ctx.source);
21
22        for (byte_offset, stmt_text) in statements {
23            let line_count = count_lines(stmt_text);
24            if line_count == 0 {
25                // Empty / whitespace-only statement — skip
26                continue;
27            }
28
29            if line_count > self.max_lines {
30                // Determine the 1-indexed line number where this statement starts
31                // in the full source by counting newlines before the statement.
32                let start_line = ctx.source[..byte_offset]
33                    .chars()
34                    .filter(|&c| c == '\n')
35                    .count()
36                    + 1;
37
38                // Find the first non-whitespace character column (1-indexed) on
39                // the starting line.
40                let start_col = ctx
41                    .source
42                    .lines()
43                    .nth(start_line - 1)
44                    .map(|l| {
45                        l.chars()
46                            .position(|c| !c.is_whitespace())
47                            .map(|p| p + 1)
48                            .unwrap_or(1)
49                    })
50                    .unwrap_or(1);
51
52                diags.push(Diagnostic {
53                    rule: self.name(),
54                    message: format!(
55                        "Statement spans {} lines, exceeding the maximum of {} lines",
56                        line_count, self.max_lines
57                    ),
58                    line: start_line,
59                    col: start_col,
60                });
61            }
62        }
63
64        diags
65    }
66}
67
68/// Count lines spanned by the statement text (including trailing `;` line).
69/// Returns 0 for whitespace-only text so empty statements are skipped.
70fn count_lines(text: &str) -> usize {
71    if text.trim().is_empty() {
72        return 0;
73    }
74    // Trim only *leading* blank lines (a statement may have trailing content
75    // on the `;` line).  We keep the trailing `;` line in the count.
76    // `lines()` splits on `\n` and includes partial lines at the end.
77    text.lines().count()
78}
79
80/// Split `source` into (byte_offset_of_start, statement_text) pairs.
81///
82/// Each returned slice extends *through* the `;` terminator (inclusive) so
83/// that the `;` line is counted as part of the statement span.  Splitting
84/// respects single-quoted string literals so embedded `;` are not treated as
85/// terminators.
86///
87/// The trailing remainder (no `;`) is also returned if non-empty.
88fn split_statements(source: &str) -> Vec<(usize, &str)> {
89    let mut statements = Vec::new();
90    let mut start = 0usize;
91    let mut in_string = false;
92    let bytes = source.as_bytes();
93    let len = bytes.len();
94    let mut i = 0usize;
95
96    while i < len {
97        let b = bytes[i];
98
99        if in_string {
100            if b == b'\'' {
101                // Doubled-quote escape: ''
102                if i + 1 < len && bytes[i + 1] == b'\'' {
103                    i += 2;
104                    continue;
105                }
106                in_string = false;
107            }
108        } else {
109            match b {
110                b'\'' => {
111                    in_string = true;
112                }
113                b';' => {
114                    // Include the `;` in the statement text (end is i+1).
115                    statements.push((start, &source[start..=i]));
116                    start = i + 1;
117                }
118                _ => {}
119            }
120        }
121
122        i += 1;
123    }
124
125    // Remainder after last `;` (or entire source if no `;` present)
126    if start < source.len() {
127        let remainder = &source[start..];
128        if !remainder.trim().is_empty() {
129            statements.push((start, remainder));
130        }
131    }
132
133    statements
134}