sqruff_lib/rules/layout/
lt07.rs1use ahash::{AHashMap, AHashSet};
2use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
3use sqruff_lib_core::lint_fix::LintFix;
4use sqruff_lib_core::parser::segments::base::{ErasedSegment, SegmentBuilder};
5
6use crate::core::config::Value;
7use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
8use crate::core::rules::context::RuleContext;
9use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
10use crate::utils::functional::context::FunctionalContext;
11
12#[derive(Debug, Default, Clone)]
13pub struct RuleLT07;
14
15impl Rule for RuleLT07 {
16 fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
17 Ok(RuleLT07.erased())
18 }
19 fn name(&self) -> &'static str {
20 "layout.cte_bracket"
21 }
22
23 fn description(&self) -> &'static str {
24 "'WITH' clause closing bracket should be on a new line."
25 }
26
27 fn long_description(&self) -> &'static str {
28 r#"
29**Anti-pattern**
30
31In this example, the closing bracket is on the same line as CTE.
32
33```sql
34 WITH zoo AS (
35 SELECT a FROM foo)
36
37 SELECT * FROM zoo
38```
39
40**Best practice**
41
42Move the closing bracket on a new line.
43
44```sql
45WITH zoo AS (
46 SELECT a FROM foo
47)
48
49SELECT * FROM zoo
50```
51"#
52 }
53
54 fn groups(&self) -> &'static [RuleGroups] {
55 &[RuleGroups::All, RuleGroups::Core, RuleGroups::Layout]
56 }
57 fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
58 let segments = FunctionalContext::new(context)
59 .segment()
60 .children(Some(|seg| seg.is_type(SyntaxKind::CommonTableExpression)));
61
62 let mut cte_end_brackets = AHashSet::new();
63 for cte in segments.iterate_segments() {
64 let cte_start_bracket = cte
65 .children(None)
66 .find_last(Some(|seg| seg.is_type(SyntaxKind::Bracketed)))
67 .children(None)
68 .find_first(Some(|seg: &ErasedSegment| {
69 seg.is_type(SyntaxKind::StartBracket)
70 }));
71
72 let cte_end_bracket = cte
73 .children(None)
74 .find_last(Some(|seg| seg.is_type(SyntaxKind::Bracketed)))
75 .children(None)
76 .find_first(Some(|seg: &ErasedSegment| {
77 seg.is_type(SyntaxKind::EndBracket)
78 }));
79
80 if !cte_start_bracket.is_empty() && !cte_end_bracket.is_empty() {
81 if cte_start_bracket[0]
82 .get_position_marker()
83 .unwrap()
84 .line_no()
85 == cte_end_bracket[0].get_position_marker().unwrap().line_no()
86 {
87 continue;
88 }
89 cte_end_brackets.insert(cte_end_bracket[0].clone());
90 }
91 }
92
93 for seg in cte_end_brackets {
94 let mut contains_non_whitespace = false;
95 let idx = context
96 .segment
97 .get_raw_segments()
98 .iter()
99 .position(|it| it == &seg)
100 .unwrap();
101 if idx > 0 {
102 for elem in context.segment.get_raw_segments()[..idx].iter().rev() {
103 if elem.is_type(SyntaxKind::Newline) {
104 break;
105 } else if !(matches!(
106 elem.get_type(),
107 SyntaxKind::Indent | SyntaxKind::Implicit
108 ) || elem.is_type(SyntaxKind::Dedent)
109 || elem.is_type(SyntaxKind::Whitespace))
110 {
111 contains_non_whitespace = true;
112 break;
113 }
114 }
115 }
116
117 if contains_non_whitespace {
118 return vec![LintResult::new(
119 seg.clone().into(),
120 vec![LintFix::create_before(
121 seg,
122 vec![SegmentBuilder::newline(context.tables.next_id(), "\n")],
123 )],
124 None,
125 None,
126 )];
127 }
128 }
129
130 Vec::new()
131 }
132
133 fn is_fix_compatible(&self) -> bool {
134 false
135 }
136
137 fn crawl_behaviour(&self) -> Crawler {
138 SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::WithCompoundStatement]) })
139 .into()
140 }
141}