sqruff_lib/rules/layout/
lt03.rs1use 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 if line_position == "leading" {
130 if self.seek_newline(parent.segments(), idx, Direction::Backward) {
131 return true;
132 }
133 if !self.seek_newline(parent.segments(), idx, Direction::Forward) {
136 return true;
137 }
138 }
139 else if line_position == "trailing" {
141 if self.seek_newline(parent.segments(), idx, Direction::Forward) {
142 return true;
143 }
144 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}