Skip to main content

flowscope_core/linter/rules/
cv_007.rs

1//! LINT_CV_007: Statement brackets.
2//!
3//! SQLFluff CV07 parity (current scope): avoid wrapping an entire statement in
4//! unnecessary outer brackets.
5
6use 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}