sqruff_lib/rules/layout/
lt03.rs

1use ahash::AHashMap;
2use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
3use sqruff_lib_core::parser::segments::base::ErasedSegment;
4
5use crate::core::config::Value;
6use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
7use crate::core::rules::context::RuleContext;
8use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
9use crate::utils::reflow::sequence::{ReflowSequence, TargetSide};
10
11#[derive(Debug, Default, Clone)]
12pub struct RuleLT03;
13
14impl Rule for RuleLT03 {
15    fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
16        Ok(RuleLT03.erased())
17    }
18    fn name(&self) -> &'static str {
19        "layout.operators"
20    }
21
22    fn description(&self) -> &'static str {
23        "Operators should follow a standard for being before/after newlines."
24    }
25
26    fn long_description(&self) -> &'static str {
27        r#"
28**Anti-pattern**
29
30In this example, if line_position = leading (or unspecified, as is the default), then the operator + should not be at the end of the second line.
31
32```sql
33SELECT
34    a +
35    b
36FROM foo
37```
38
39**Best practice**
40
41If line_position = leading (or unspecified, as this is the default), place the operator after the newline.
42
43```sql
44SELECT
45    a
46    + b
47FROM foo
48```
49
50If line_position = trailing, place the operator before the newline.
51
52```sql
53SELECT
54    a +
55    b
56FROM foo
57```
58"#
59    }
60    fn groups(&self) -> &'static [RuleGroups] {
61        &[RuleGroups::All, RuleGroups::Layout]
62    }
63
64    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
65        if context.segment.is_type(SyntaxKind::ComparisonOperator) {
66            let comparison_positioning =
67                context.config.raw["layout"]["type"]["comparison_operator"]["line_position"]
68                    .as_string()
69                    .unwrap();
70
71            if self.check_trail_lead_shortcut(
72                &context.segment,
73                context.parent_stack.last().unwrap(),
74                comparison_positioning,
75            ) {
76                return vec![LintResult::new(None, Vec::new(), None, None)];
77            }
78        } else if context.segment.is_type(SyntaxKind::BinaryOperator) {
79            let binary_positioning =
80                context.config.raw["layout"]["type"]["binary_operator"]["line_position"]
81                    .as_string()
82                    .unwrap();
83
84            if self.check_trail_lead_shortcut(
85                &context.segment,
86                context.parent_stack.last().unwrap(),
87                binary_positioning,
88            ) {
89                return vec![LintResult::new(None, Vec::new(), None, None)];
90            }
91        }
92
93        ReflowSequence::from_around_target(
94            &context.segment,
95            context.parent_stack.first().unwrap().clone(),
96            TargetSide::Both,
97            context.config,
98        )
99        .rebreak(context.tables)
100        .results()
101    }
102
103    fn is_fix_compatible(&self) -> bool {
104        true
105    }
106
107    fn crawl_behaviour(&self) -> Crawler {
108        SegmentSeekerCrawler::new(
109            const { SyntaxSet::new(&[SyntaxKind::BinaryOperator, SyntaxKind::ComparisonOperator]) },
110        )
111        .into()
112    }
113}
114
115impl RuleLT03 {
116    pub(crate) fn check_trail_lead_shortcut(
117        &self,
118        segment: &ErasedSegment,
119        parent: &ErasedSegment,
120        line_position: &str,
121    ) -> bool {
122        let idx = parent
123            .segments()
124            .iter()
125            .position(|it| it == segment)
126            .unwrap();
127
128        // Shortcut #1: Leading.
129        if line_position == "leading" {
130            if self.seek_newline(parent.segments(), idx, Direction::Backward) {
131                return true;
132            }
133            // If we didn't find a newline before, if there's _also_ not a newline
134            // after, then we can also shortcut. i.e., it's a comma "mid line".
135            if !self.seek_newline(parent.segments(), idx, Direction::Forward) {
136                return true;
137            }
138        }
139        // Shortcut #2: Trailing.
140        else if line_position == "trailing" {
141            if self.seek_newline(parent.segments(), idx, Direction::Forward) {
142                return true;
143            }
144            // If we didn't find a newline after, if there's _also_ not a newline
145            // before, then we can also shortcut. i.e., it's a comma "mid line".
146            if !self.seek_newline(parent.segments(), idx, Direction::Backward) {
147                return true;
148            }
149        }
150
151        false
152    }
153
154    fn seek_newline(&self, segments: &[ErasedSegment], idx: usize, direction: Direction) -> bool {
155        let segments: &mut dyn Iterator<Item = _> = match direction {
156            Direction::Forward => &mut segments[idx + 1..].iter(),
157            Direction::Backward => &mut segments.iter().take(idx).rev(),
158        };
159
160        for segment in segments {
161            if segment.is_type(SyntaxKind::Newline) {
162                return true;
163            } else if !segment.is_type(SyntaxKind::Whitespace)
164                && !segment.is_type(SyntaxKind::Indent)
165                && !segment.is_type(SyntaxKind::Implicit)
166                && !segment.is_type(SyntaxKind::Comment)
167                && !segment.is_type(SyntaxKind::InlineComment)
168                && !segment.is_type(SyntaxKind::BlockComment)
169            {
170                break;
171            }
172        }
173
174        false
175    }
176}
177
178#[derive(Debug)]
179enum Direction {
180    Forward,
181    Backward,
182}