1
1
use ahash:: AHashMap ;
2
2
use sqruff_lib_core:: lint_fix:: LintFix ;
3
- use sqruff_lib_core:: parser:: segments:: base:: ErasedSegment ;
3
+ use sqruff_lib_core:: parser:: segments:: base:: { ErasedSegment , SegmentBuilder } ;
4
4
use sqruff_lib_core:: parser:: segments:: fix:: SourceFix ;
5
5
6
6
use crate :: core:: config:: Value ;
@@ -53,31 +53,28 @@ SELECT {{ a }} from {{
53
53
"#
54
54
}
55
55
56
-
57
56
fn groups ( & self ) -> & ' static [ RuleGroups ] {
58
57
& [ RuleGroups :: All , RuleGroups :: Jinja ]
59
58
}
60
59
61
60
fn eval ( & self , context : & RuleContext ) -> Vec < LintResult > {
62
61
debug_assert ! ( context. segment. get_position_marker( ) . is_some( ) ) ;
63
-
62
+
64
63
// If the position marker for the root segment is literal then there's
65
64
// no templated code, so return early
66
65
if context. segment . get_position_marker ( ) . unwrap ( ) . is_literal ( ) {
67
66
return vec ! [ ] ;
68
67
}
69
68
70
- // Need templated file to proceed
71
- let Some ( templated_file) = & context. templated_file else {
72
- return vec ! [ ] ;
73
- } ;
74
-
75
69
let mut results: Vec < LintResult > = vec ! [ ] ;
76
70
77
71
// Work through the templated slices
78
- for raw_slice in templated_file. raw_sliced_iter ( ) {
72
+ for raw_slice in context . templated_file . raw_sliced_iter ( ) {
79
73
// Only want templated slices
80
- if !matches ! ( raw_slice. slice_type. as_str( ) , "templated" | "block_start" | "block_end" ) {
74
+ if !matches ! (
75
+ raw_slice. slice_type. as_str( ) ,
76
+ "templated" | "block_start" | "block_end"
77
+ ) {
81
78
continue ;
82
79
}
83
80
@@ -88,8 +85,13 @@ SELECT {{ a }} from {{
88
85
89
86
// Partition and position
90
87
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 ) ;
88
+ let ( tag_pre, ws_pre, inner, ws_post, tag_post) =
89
+ Self :: get_white_space_ends ( stripped. to_string ( ) ) ;
90
+
91
+ let position = raw_slice
92
+ . raw
93
+ . find ( stripped. chars ( ) . next ( ) . unwrap ( ) )
94
+ . unwrap_or ( 0 ) ;
93
95
94
96
// Whitespace should be single space OR contain newline
95
97
let mut pre_fix = None ;
@@ -117,7 +119,8 @@ SELECT {{ a }} from {{
117
119
) ;
118
120
119
121
// Find raw segment to attach fix to
120
- let Some ( raw_seg) = Self :: find_raw_at_src_index ( context. segment . clone ( ) , src_idx) else {
122
+ let Some ( raw_seg) = Self :: find_raw_at_src_index ( & context. segment , src_idx)
123
+ else {
121
124
continue ;
122
125
} ;
123
126
@@ -126,39 +129,35 @@ SELECT {{ a }} from {{
126
129
continue ;
127
130
}
128
131
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( ) ] ) ,
132
+ let ps_marker = raw_seg
133
+ . get_position_marker ( )
134
+ . map ( |pm| pm. templated_slice . clone ( ) ) ;
135
+ let Some ( ps_marker) = ps_marker else {
136
+ continue ;
137
+ } ;
138
+
139
+ let source_fixes = vec ! [ SourceFix :: new(
140
+ fixed. clone( ) . into( ) ,
141
+ src_idx + position..src_idx + position + stripped. len( ) ,
142
+ ps_marker,
138
143
) ] ;
139
144
140
145
results. push ( LintResult :: new (
141
146
Some ( raw_seg. clone ( ) ) ,
142
- source_fixes,
143
- Some ( format ! ( "Jinja tags should have a single whitespace on either side: {}" , stripped) ) ,
147
+ vec ! [ LintFix :: replace(
148
+ raw_seg. clone( ) ,
149
+ vec![ raw_seg. edit( raw_seg. id( ) , fixed. into( ) , Some ( source_fixes) ) ] ,
150
+ None ,
151
+ ) ] ,
152
+ Some ( format ! (
153
+ "Jinja tags should have a single whitespace on either side: {}" ,
154
+ stripped
155
+ ) ) ,
144
156
None ,
145
157
) ) ;
146
158
}
147
159
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 ! ( ) ;
160
+ results
162
161
}
163
162
164
163
fn is_fix_compatible ( & self ) -> bool {
@@ -167,27 +166,34 @@ SELECT {{ a }} from {{
167
166
168
167
fn crawl_behaviour ( & self ) -> Crawler {
169
168
RootOnlyCrawler . into ( )
170
- }
169
+ }
171
170
}
172
171
173
172
impl RuleJJ01 {
174
173
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 }}" ) ;
174
+ assert ! (
175
+ s. starts_with( '{' ) && s. ends_with( '}' ) ,
176
+ "String must start with {{ and end with }}"
177
+ ) ;
176
178
177
179
// Get the main content between the tag markers
178
- let mut main = s[ 2 ..s. len ( ) - 2 ] . to_string ( ) ;
180
+ let mut main = s[ 2 ..s. len ( ) - 2 ] . to_string ( ) ;
179
181
let mut pre = s[ ..2 ] . to_string ( ) ;
180
- let mut post = s[ s. len ( ) - 2 ..] . to_string ( ) ;
182
+ let mut post = s[ s. len ( ) - 2 ..] . to_string ( ) ;
181
183
182
184
// Handle plus/minus modifiers
183
185
let modifier_chars = [ '+' , '-' ] ;
184
186
if !main. is_empty ( ) && modifier_chars. contains ( & main. chars ( ) . next ( ) . unwrap ( ) ) {
187
+ let first_char = main. chars ( ) . next ( ) . unwrap ( ) ;
185
188
main = main[ 1 ..] . to_string ( ) ;
186
- pre = s[ ..3 ] . to_string ( ) ;
189
+ // Keep the modifier directly after {% or {{
190
+ pre = format ! ( "{}{}" , pre, first_char) ;
187
191
}
188
192
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 ( ) ;
193
+ let last_char = main. chars ( ) . last ( ) . unwrap ( ) ;
194
+ main = main[ ..main. len ( ) - 1 ] . to_string ( ) ;
195
+ // Keep the modifier directly before %} or }}
196
+ post = format ! ( "{}{}" , last_char, post) ;
191
197
}
192
198
193
199
// Split out inner content and surrounding whitespace
@@ -199,7 +205,7 @@ impl RuleJJ01 {
199
205
( pre, pre_ws, inner, post_ws, post)
200
206
}
201
207
202
- fn find_raw_at_src_index ( segment : ErasedSegment , src_idx : usize ) -> Option < ErasedSegment > {
208
+ fn find_raw_at_src_index ( segment : & ErasedSegment , src_idx : usize ) -> Option < & ErasedSegment > {
203
209
// Recursively search to find a raw segment for a position in the source.
204
210
// NOTE: This assumes it's not being called on a `raw`.
205
211
// In the case that there are multiple potential targets, we will find the first.
@@ -208,23 +214,81 @@ impl RuleJJ01 {
208
214
assert ! ( segments. len( ) > 0 , "Segment must have segments" ) ;
209
215
210
216
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) ;
217
+ let Some ( pos_marker) = seg. get_position_marker ( ) else {
218
+ continue ;
219
+ } ;
220
+ // If it's before, skip onward
221
+ if pos_marker. source_slice . end <= src_idx {
222
+ continue ;
223
+ }
224
+ // Is the current segment raw?
225
+ if seg. is_raw ( ) {
226
+ return Some ( seg) ;
226
227
}
228
+ // Otherwise recurse
229
+ return Self :: find_raw_at_src_index ( seg, src_idx) ;
227
230
}
228
231
None
229
232
}
230
- }
233
+ }
234
+
235
+ #[ cfg( test) ]
236
+ mod tests {
237
+ use super :: * ;
238
+ use crate :: {
239
+ core:: { config:: FluffConfig , linter:: core:: Linter } ,
240
+ templaters:: jinja:: JinjaTemplater ,
241
+ } ;
242
+
243
+ #[ test]
244
+ fn test_get_white_space_ends ( ) {
245
+ let cases = vec ! [
246
+ (
247
+ "{{+ my_content }}" ,
248
+ (
249
+ "{{+" . to_string( ) ,
250
+ " " . to_string( ) ,
251
+ "my_content" . to_string( ) ,
252
+ " " . to_string( ) ,
253
+ "}}" . to_string( ) ,
254
+ ) ,
255
+ ) ,
256
+ (
257
+ "{%+if true-%}" ,
258
+ (
259
+ "{%+" . to_string( ) ,
260
+ "" . to_string( ) ,
261
+ "if true" . to_string( ) ,
262
+ "" . to_string( ) ,
263
+ "-%}" . to_string( ) ,
264
+ ) ,
265
+ ) ,
266
+ ] ;
267
+
268
+ for ( input, expected) in cases {
269
+ let result = RuleJJ01 :: get_white_space_ends ( input. to_string ( ) ) ;
270
+ assert_eq ! ( result, expected) ;
271
+ }
272
+ }
273
+
274
+ #[ test]
275
+ fn test_simple_example ( ) {
276
+ let start = "SELECT 1 from {%+if true-%} {{ref('foo')}} {%-endif%}" . to_string ( ) ;
277
+ let want = "SELECT 1 from {%+ if true -%} {{ ref('foo') }} {%- endif %}" . to_string ( ) ;
278
+
279
+ let config = FluffConfig :: from_source (
280
+ r#"
281
+ [sqruff]
282
+ rules = JJ01
283
+ templater = jinja
284
+ "# ,
285
+ None ,
286
+ ) ;
287
+
288
+ let mut linter = Linter :: new ( config, None , Some ( & JinjaTemplater ) , false ) ;
289
+ let result = linter. lint_string_wrapped ( & start, None , true ) ;
290
+
291
+ let fixed = result. paths [ 0 ] . files [ 0 ] . clone ( ) . fix_string ( ) ;
292
+ assert_eq ! ( fixed, want, "\n Expected: {}\n Got: {}" , want, fixed) ;
293
+ }
294
+ }
0 commit comments