1
1
use ahash:: AHashMap ;
2
- use sqruff_lib_core:: dialects:: syntax:: { SyntaxKind , SyntaxSet } ;
3
- use sqruff_lib_core:: parser:: segments:: base:: SegmentBuilder ;
2
+ use sqruff_lib_core:: lint_fix:: LintFix ;
3
+ use sqruff_lib_core:: parser:: segments:: base:: ErasedSegment ;
4
+ use sqruff_lib_core:: parser:: segments:: fix:: SourceFix ;
4
5
5
6
use crate :: core:: config:: Value ;
6
7
use crate :: core:: rules:: base:: { Erased , ErasedRule , LintResult , Rule , RuleGroups } ;
7
8
use crate :: core:: rules:: context:: RuleContext ;
8
- use crate :: core:: rules:: crawlers:: { Crawler , SegmentSeekerCrawler } ;
9
- use crate :: utils:: reflow:: sequence:: { Filter , ReflowInsertPosition , ReflowSequence , TargetSide } ;
9
+ use crate :: core:: rules:: crawlers:: { Crawler , RootOnlyCrawler } ;
10
10
11
11
#[ derive( Debug , PartialEq , Eq , Clone , Copy ) ]
12
12
pub enum Aliasing {
@@ -15,12 +15,11 @@ pub enum Aliasing {
15
15
}
16
16
17
17
#[ derive( Debug , Clone ) ]
18
- pub struct RuleJJ01 {
19
- }
18
+ pub struct RuleJJ01 ;
20
19
21
20
impl Rule for RuleJJ01 {
22
21
fn load_from_config ( & self , _config : & AHashMap < String , Value > ) -> Result < ErasedRule , String > {
23
- Ok ( )
22
+ Ok ( RuleJJ01 . erased ( ) )
24
23
}
25
24
26
25
fn name ( & self ) -> & ' static str {
@@ -54,19 +53,178 @@ SELECT {{ a }} from {{
54
53
"#
55
54
}
56
55
56
+
57
57
fn groups ( & self ) -> & ' static [ RuleGroups ] {
58
58
& [ RuleGroups :: All , RuleGroups :: Jinja ]
59
59
}
60
60
61
- fn eval ( & self , rule_cx : & RuleContext ) -> Vec < LintResult > {
62
- unimplemented ! ( )
61
+ fn eval ( & self , context : & RuleContext ) -> Vec < LintResult > {
62
+ debug_assert ! ( context. segment. get_position_marker( ) . is_some( ) ) ;
63
+
64
+ // If the position marker for the root segment is literal then there's
65
+ // no templated code, so return early
66
+ if context. segment . get_position_marker ( ) . unwrap ( ) . is_literal ( ) {
67
+ return vec ! [ ] ;
68
+ }
69
+
70
+ // Need templated file to proceed
71
+ let Some ( templated_file) = & context. templated_file else {
72
+ return vec ! [ ] ;
73
+ } ;
74
+
75
+ let mut results: Vec < LintResult > = vec ! [ ] ;
76
+
77
+ // Work through the templated slices
78
+ for raw_slice in templated_file. raw_sliced_iter ( ) {
79
+ // Only want templated slices
80
+ if !matches ! ( raw_slice. slice_type. as_str( ) , "templated" | "block_start" | "block_end" ) {
81
+ continue ;
82
+ }
83
+
84
+ let stripped = raw_slice. raw . trim ( ) ;
85
+ if stripped. is_empty ( ) || !stripped. starts_with ( '{' ) || !stripped. ends_with ( '}' ) {
86
+ continue ;
87
+ }
88
+
89
+ // Partition and position
90
+ let src_idx = raw_slice. source_idx ;
91
+ let ( tag_pre, ws_pre, inner, ws_post, tag_post) = Self :: get_white_space_ends ( stripped. to_string ( ) ) ;
92
+ let position = raw_slice. raw . find ( stripped. chars ( ) . next ( ) . unwrap ( ) ) . unwrap_or ( 0 ) ;
93
+
94
+ // Whitespace should be single space OR contain newline
95
+ let mut pre_fix = None ;
96
+ let mut post_fix = None ;
97
+
98
+ if ws_pre. is_empty ( ) || ( ws_pre != " " && !ws_pre. contains ( '\n' ) ) {
99
+ pre_fix = Some ( " " ) ;
100
+ }
101
+ if ws_post. is_empty ( ) || ( ws_post != " " && !ws_post. contains ( '\n' ) ) {
102
+ post_fix = Some ( " " ) ;
103
+ }
104
+
105
+ // Skip if no fixes needed
106
+ if pre_fix. is_none ( ) && post_fix. is_none ( ) {
107
+ continue ;
108
+ }
109
+
110
+ let fixed = format ! (
111
+ "{}{}{}{}{}" ,
112
+ tag_pre,
113
+ pre_fix. unwrap_or( & ws_pre) ,
114
+ inner,
115
+ post_fix. unwrap_or( & ws_post) ,
116
+ tag_post
117
+ ) ;
118
+
119
+ // Find raw segment to attach fix to
120
+ let Some ( raw_seg) = Self :: find_raw_at_src_index ( context. segment . clone ( ) , src_idx) else {
121
+ continue ;
122
+ } ;
123
+
124
+ // Skip if segment already has fixes
125
+ if !raw_seg. get_source_fixes ( ) . is_empty ( ) {
126
+ continue ;
127
+ }
128
+
129
+ let source_fixes = vec ! [ LintFix :: replace(
130
+ raw_seg. clone( ) ,
131
+ vec![ ] ,
132
+ Some ( vec![ SourceFix :: new(
133
+ fixed. into( ) ,
134
+ src_idx + position..src_idx + position + stripped. len( ) ,
135
+ // This position in the templated file is rough, but close enough for sequencing.
136
+ raw_seg. get_position_marker( ) . unwrap( ) . templated_slice. clone( ) ,
137
+ ) . erased( ) ] ) ,
138
+ ) ] ;
139
+
140
+ results. push ( LintResult :: new (
141
+ Some ( raw_seg. clone ( ) ) ,
142
+ source_fixes,
143
+ Some ( format ! ( "Jinja tags should have a single whitespace on either side: {}" , stripped) ) ,
144
+ None ,
145
+ ) ) ;
146
+ }
147
+
148
+ // results.push(LintResult::new(
149
+ // Some(raw_seg.clone()),
150
+ // vec![LintFix::replace(
151
+ // raw_seg,
152
+ // [raw_seg.edit(source_fixes)],
153
+ // None,
154
+ // )],
155
+ // Some(format!("Jinja tags should have a single whitespace on either side: {}", stripped)),
156
+ // raw_seg.get_position_marker().unwrap().source_slice,
157
+ // ));
158
+ // }
159
+ // results
160
+
161
+ unimplemented ! ( ) ;
63
162
}
64
163
65
164
fn is_fix_compatible ( & self ) -> bool {
66
165
true
67
166
}
68
167
69
168
fn crawl_behaviour ( & self ) -> Crawler {
70
- SegmentSeekerCrawler :: new ( const { SyntaxSet :: new ( & [ SyntaxKind :: AliasExpression ] ) } ) . into ( )
71
- }
169
+ RootOnlyCrawler . into ( )
170
+ }
72
171
}
172
+
173
+ impl RuleJJ01 {
174
+ fn get_white_space_ends ( s : String ) -> ( String , String , String , String , String ) {
175
+ assert ! ( s. starts_with( '{' ) && s. ends_with( '}' ) , "String must start with {{ and end with }}" ) ;
176
+
177
+ // Get the main content between the tag markers
178
+ let mut main = s[ 2 ..s. len ( ) -2 ] . to_string ( ) ;
179
+ let mut pre = s[ ..2 ] . to_string ( ) ;
180
+ let mut post = s[ s. len ( ) -2 ..] . to_string ( ) ;
181
+
182
+ // Handle plus/minus modifiers
183
+ let modifier_chars = [ '+' , '-' ] ;
184
+ if !main. is_empty ( ) && modifier_chars. contains ( & main. chars ( ) . next ( ) . unwrap ( ) ) {
185
+ main = main[ 1 ..] . to_string ( ) ;
186
+ pre = s[ ..3 ] . to_string ( ) ;
187
+ }
188
+ if !main. is_empty ( ) && modifier_chars. contains ( & main. chars ( ) . last ( ) . unwrap ( ) ) {
189
+ main = main[ ..main. len ( ) -1 ] . to_string ( ) ;
190
+ post = s[ s. len ( ) -3 ..] . to_string ( ) ;
191
+ }
192
+
193
+ // Split out inner content and surrounding whitespace
194
+ let inner = main. trim ( ) . to_string ( ) ;
195
+ let pos = main. find ( & inner) . unwrap_or ( 0 ) ;
196
+ let pre_ws = main[ ..pos] . to_string ( ) ;
197
+ let post_ws = main[ pos + inner. len ( ) ..] . to_string ( ) ;
198
+
199
+ ( pre, pre_ws, inner, post_ws, post)
200
+ }
201
+
202
+ fn find_raw_at_src_index ( segment : ErasedSegment , src_idx : usize ) -> Option < ErasedSegment > {
203
+ // Recursively search to find a raw segment for a position in the source.
204
+ // NOTE: This assumes it's not being called on a `raw`.
205
+ // In the case that there are multiple potential targets, we will find the first.
206
+ assert ! ( !segment. is_raw( ) , "Segment must not be raw" ) ;
207
+ let segments = segment. segments ( ) ;
208
+ assert ! ( segments. len( ) > 0 , "Segment must have segments" ) ;
209
+
210
+ for seg in segments {
211
+ if let Some ( pos_marker) = seg. get_position_marker ( ) {
212
+ let src_slice = pos_marker. source_slice . clone ( ) ;
213
+
214
+ // If it's before, skip onward
215
+ if src_slice. end <= src_idx {
216
+ continue ;
217
+ }
218
+
219
+ // Is the current segment raw?
220
+ if seg. is_raw ( ) {
221
+ return Some ( seg. clone ( ) ) ;
222
+ }
223
+
224
+ // Otherwise recurse
225
+ return Self :: find_raw_at_src_index ( seg. clone ( ) , src_idx) ;
226
+ }
227
+ }
228
+ None
229
+ }
230
+ }
0 commit comments