sqruff_lib/rules/layout/
lt03.rs1use 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 if line_position == LinePosition::Leading {
135 if self.seek_newline(parent.segments(), idx, Direction::Backward) {
136 return true;
137 }
138 if !self.seek_newline(parent.segments(), idx, Direction::Forward) {
141 return true;
142 }
143 }
144 else if line_position == LinePosition::Trailing {
146 if self.seek_newline(parent.segments(), idx, Direction::Forward) {
147 return true;
148 }
149 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}