flowscope_core/linter/rules/
cv_007.rs1use crate::linter::rule::{LintContext, LintRule};
7use crate::types::{issue_codes, Issue, IssueAutofixApplicability, IssuePatchEdit};
8use sqlparser::ast::{SetExpr, Statement};
9
10pub struct ConventionStatementBrackets;
11
12impl LintRule for ConventionStatementBrackets {
13 fn code(&self) -> &'static str {
14 issue_codes::LINT_CV_007
15 }
16
17 fn name(&self) -> &'static str {
18 "Statement brackets"
19 }
20
21 fn description(&self) -> &'static str {
22 "Top-level statements should not be wrapped in brackets."
23 }
24
25 fn check(&self, statement: &Statement, ctx: &LintContext) -> Vec<Issue> {
26 let bracket_depth = wrapper_bracket_depth(statement);
27 if bracket_depth > 0 {
28 let mut issue = Issue::info(
29 issue_codes::LINT_CV_007,
30 "Avoid wrapping the full statement in unnecessary brackets.",
31 )
32 .with_statement(ctx.statement_index);
33 if let Some(pairs) = wrapper_bracket_offsets(ctx.statement_sql(), bracket_depth) {
34 let outer_left = pairs[0].0;
35 let mut edits = Vec::with_capacity(pairs.len() * 2);
36 for (left_idx, right_idx) in pairs {
37 edits.push(IssuePatchEdit::new(
38 ctx.span_from_statement_offset(left_idx, left_idx + 1),
39 "",
40 ));
41 edits.push(IssuePatchEdit::new(
42 ctx.span_from_statement_offset(right_idx, right_idx + 1),
43 "",
44 ));
45 }
46 issue = issue
47 .with_span(ctx.span_from_statement_offset(outer_left, outer_left + 1))
48 .with_autofix_edits(IssueAutofixApplicability::Safe, edits);
49 }
50 vec![issue]
51 } else {
52 Vec::new()
53 }
54 }
55}
56
57fn wrapper_bracket_depth(statement: &Statement) -> usize {
58 let Statement::Query(query) = statement else {
59 return 0;
60 };
61
62 let mut depth = 0;
63 let mut body = query.body.as_ref();
64 while let SetExpr::Query(inner_query) = body {
65 depth += 1;
66 body = inner_query.body.as_ref();
67 }
68 depth
69}
70
71fn wrapper_bracket_offsets(sql: &str, depth: usize) -> Option<Vec<(usize, usize)>> {
72 if depth == 0 {
73 return Some(Vec::new());
74 }
75
76 let mut left_bound = 0usize;
77 let mut right_bound = sql.len();
78 let mut pairs = Vec::with_capacity(depth);
79
80 for _ in 0..depth {
81 let left_idx = sql[left_bound..right_bound]
82 .char_indices()
83 .find_map(|(offset, ch)| (!ch.is_whitespace()).then_some((left_bound + offset, ch)))
84 .and_then(|(idx, ch)| (ch == '(').then_some(idx))?;
85
86 let right_idx = sql[left_bound..right_bound]
87 .char_indices()
88 .rev()
89 .find_map(|(offset, ch)| (!ch.is_whitespace()).then_some((left_bound + offset, ch)))
90 .and_then(|(idx, ch)| (ch == ')').then_some(idx))?;
91
92 if right_idx <= left_idx {
93 return None;
94 }
95
96 pairs.push((left_idx, right_idx));
97 left_bound = left_idx + 1;
98 right_bound = right_idx;
99 }
100
101 Some(pairs)
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::parser::parse_sql;
108 use crate::types::IssueAutofixApplicability;
109
110 fn run(sql: &str) -> Vec<Issue> {
111 let statements = parse_sql(sql).expect("parse");
112 let rule = ConventionStatementBrackets;
113 statements
114 .iter()
115 .enumerate()
116 .flat_map(|(index, statement)| {
117 rule.check(
118 statement,
119 &LintContext {
120 sql,
121 statement_range: 0..sql.len(),
122 statement_index: index,
123 },
124 )
125 })
126 .collect()
127 }
128
129 fn apply_issue_autofix(sql: &str, issue: &Issue) -> Option<String> {
130 let autofix = issue.autofix.as_ref()?;
131 let mut edits = autofix.edits.clone();
132 edits.sort_by(|left, right| right.span.start.cmp(&left.span.start));
133
134 let mut out = sql.to_string();
135 for edit in edits {
136 out.replace_range(edit.span.start..edit.span.end, &edit.replacement);
137 }
138 Some(out)
139 }
140
141 #[test]
142 fn flags_wrapped_statement() {
143 let issues = run("(SELECT 1)");
144 assert_eq!(issues.len(), 1);
145 assert_eq!(issues[0].code, issue_codes::LINT_CV_007);
146 }
147
148 #[test]
149 fn does_not_flag_normal_statement() {
150 assert!(run("SELECT 1").is_empty());
151 }
152
153 #[test]
154 fn does_not_flag_parenthesized_subquery_in_from_clause() {
155 assert!(run("SELECT * FROM (SELECT 1) AS t").is_empty());
156 }
157
158 #[test]
159 fn wrapped_statement_emits_safe_autofix_patch() {
160 let sql = "(SELECT 1)";
161 let issues = run(sql);
162 let issue = &issues[0];
163 let autofix = issue.autofix.as_ref().expect("autofix metadata");
164
165 assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
166 assert_eq!(autofix.edits.len(), 2);
167 assert_eq!(autofix.edits[0].span.start, 0);
168 assert_eq!(autofix.edits[0].span.end, 1);
169 assert_eq!(autofix.edits[1].span.start, sql.len() - 1);
170 assert_eq!(autofix.edits[1].span.end, sql.len());
171
172 let fixed = apply_issue_autofix(sql, issue).expect("apply autofix");
173 assert_eq!(fixed, "SELECT 1");
174 }
175
176 #[test]
177 fn nested_wrapped_statement_autofix_removes_all_outer_pairs() {
178 let sql = "((SELECT 1))";
179 let issues = run(sql);
180 let issue = &issues[0];
181 let autofix = issue.autofix.as_ref().expect("autofix metadata");
182 assert_eq!(autofix.edits.len(), 4);
183
184 let fixed = apply_issue_autofix(sql, issue).expect("apply autofix");
185 assert_eq!(fixed, "SELECT 1");
186 }
187}