Skip to main content

sqruff_lib/rules/layout/
lt03.rs

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