Skip to main content

sqruff_lib/rules/layout/
lt05.rs

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