sqruff_lib/rules/layout/
lt05.rs

1use ahash::{AHashMap, AHashSet};
2use itertools::enumerate;
3use sqruff_lib_core::dialects::syntax::SyntaxKind;
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, RootOnlyCrawler};
9use crate::utils::reflow::sequence::ReflowSequence;
10
11#[derive(Debug, Default, Clone)]
12pub struct RuleLT05 {
13    ignore_comment_lines: bool,
14    ignore_comment_clauses: bool,
15}
16
17impl Rule for RuleLT05 {
18    fn load_from_config(&self, config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
19        Ok(RuleLT05 {
20            ignore_comment_lines: config["ignore_comment_lines"].as_bool().unwrap(),
21            ignore_comment_clauses: config["ignore_comment_clauses"].as_bool().unwrap(),
22        }
23        .erased())
24    }
25    fn name(&self) -> &'static str {
26        "layout.long_lines"
27    }
28
29    fn description(&self) -> &'static str {
30        "Line is too long."
31    }
32
33    fn long_description(&self) -> &'static str {
34        r#"
35**Anti-pattern**
36
37In this example, the line is too long.
38
39```sql
40SELECT
41    my_function(col1 + col2, arg2, arg3) over (partition by col3, col4 order by col5 rows between unbounded preceding and current row) as my_relatively_long_alias,
42    my_other_function(col6, col7 + col8, arg4) as my_other_relatively_long_alias,
43    my_expression_function(col6, col7 + col8, arg4) = col9 + col10 as another_relatively_long_alias
44FROM my_table
45```
46
47**Best practice**
48
49Wraps the line to be within the maximum line length.
50
51```sql
52SELECT
53    my_function(col1 + col2, arg2, arg3)
54        over (
55            partition by col3, col4
56            order by col5 rows between unbounded preceding and current row
57        )
58        as my_relatively_long_alias,
59    my_other_function(col6, col7 + col8, arg4)
60        as my_other_relatively_long_alias,
61    my_expression_function(col6, col7 + col8, arg4)
62    = col9 + col10 as another_relatively_long_alias
63FROM my_table"#
64    }
65
66    fn groups(&self) -> &'static [RuleGroups] {
67        &[RuleGroups::All, RuleGroups::Core, RuleGroups::Layout]
68    }
69    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
70        let mut results = ReflowSequence::from_root(context.segment.clone(), context.config)
71            .break_long_lines(context.tables)
72            .results();
73
74        let mut to_remove = AHashSet::new();
75
76        if self.ignore_comment_lines {
77            let raw_segments = context.segment.get_raw_segments();
78            for (res_idx, res) in enumerate(&results) {
79                if res.anchor.as_ref().unwrap().is_type(SyntaxKind::Comment)
80                    || res
81                        .anchor
82                        .as_ref()
83                        .unwrap()
84                        .is_type(SyntaxKind::InlineComment)
85                {
86                    to_remove.insert(res_idx);
87                    continue;
88                }
89
90                let pos_marker = res.anchor.as_ref().unwrap().get_position_marker().unwrap();
91                let raw_idx = raw_segments
92                    .iter()
93                    .position(|it| it == res.anchor.as_ref().unwrap())
94                    .unwrap();
95
96                for seg in &raw_segments[raw_idx..] {
97                    if seg.get_position_marker().unwrap().working_line_no
98                        != pos_marker.working_line_no
99                    {
100                        break;
101                    }
102
103                    if seg.is_type(SyntaxKind::Comment) || seg.is_type(SyntaxKind::InlineComment) {
104                        to_remove.insert(res_idx);
105                        break;
106                    } else if seg.is_type(SyntaxKind::Placeholder) {
107                        unimplemented!()
108                    }
109                }
110            }
111        }
112
113        if self.ignore_comment_clauses {
114            let raw_segments = context.segment.get_raw_segments();
115            for (res_idx, res) in enumerate(&results) {
116                let raw_idx = raw_segments
117                    .iter()
118                    .position(|it| it == res.anchor.as_ref().unwrap())
119                    .unwrap();
120
121                for seg in &raw_segments[raw_idx..] {
122                    if seg.get_position_marker().unwrap().working_line_no
123                        != res
124                            .anchor
125                            .as_ref()
126                            .unwrap()
127                            .get_position_marker()
128                            .unwrap()
129                            .working_line_no
130                    {
131                        break;
132                    }
133
134                    let mut is_break = false;
135
136                    for ps in context.segment.path_to(seg) {
137                        if ps.segment.is_type(SyntaxKind::CommentClause)
138                            || ps.segment.is_type(SyntaxKind::CommentEqualsClause)
139                        {
140                            let line_pos =
141                                ps.segment.get_position_marker().unwrap().working_line_pos;
142                            if (line_pos as i32)
143                                < context
144                                    .config
145                                    .get("max_line_length", "core")
146                                    .as_int()
147                                    .unwrap()
148                            {
149                                to_remove.insert(res_idx);
150                                is_break = true;
151                                break;
152                            }
153                        }
154                    }
155
156                    if is_break {
157                        break;
158                    } else {
159                        continue;
160                    }
161                }
162            }
163        }
164
165        for idx in to_remove {
166            results.remove(idx);
167        }
168
169        results
170    }
171
172    fn is_fix_compatible(&self) -> bool {
173        true
174    }
175
176    fn crawl_behaviour(&self) -> Crawler {
177        RootOnlyCrawler.into()
178    }
179}