Skip to content

Commit ee4d4f8

Browse files
committed
temp [ci skip]
1 parent d09e813 commit ee4d4f8

File tree

5 files changed

+253
-14
lines changed

5 files changed

+253
-14
lines changed

crates/lib-core/src/parser/segments/base.rs

+5
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ impl ErasedSegment {
139139
}
140140
}
141141

142+
/// Return true if this segment has no children
143+
pub fn is_raw(&self) -> bool {
144+
self.segments().is_empty()
145+
}
146+
142147
pub fn segments(&self) -> &[ErasedSegment] {
143148
match &self.value.kind {
144149
NodeOrTokenKind::Node(node) => &node.segments,

crates/lib-core/src/templaters/base.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ impl TemplatedFileInner {
233233
self.templated_str.is_some()
234234
}
235235

236+
pub fn raw_sliced_iter(&self) -> impl Iterator<Item = &RawFileSlice> {
237+
self.raw_sliced.iter()
238+
}
239+
236240
/// Get the line number and position of a point in the source file.
237241
/// Args:
238242
/// - char_pos: The character position in the relevant file.
@@ -540,8 +544,8 @@ pub enum RawFileSliceType {
540544
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
541545
pub struct RawFileSlice {
542546
/// Source string
543-
raw: String,
544-
pub(crate) slice_type: String,
547+
pub raw: String,
548+
pub slice_type: String,
545549
/// Offset from beginning of source string
546550
pub source_idx: usize,
547551
slice_subtype: Option<RawFileSliceType>,

crates/lib/src/rules/jinja.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
use crate::core::rules::base::ErasedRule;
2+
13
pub mod jj01;
24

35
pub fn rules() -> Vec<ErasedRule> {
46
use crate::core::rules::base::Erased as _;
57

6-
vec![jj01::RuleJJ01::default().erased()]
8+
vec![jj01::RuleJJ01.erased()]
79
}

crates/lib/src/rules/jinja/jj01.rs

+169-11
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
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;
45

56
use crate::core::config::Value;
67
use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
78
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};
1010

1111
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
1212
pub enum Aliasing {
@@ -15,12 +15,11 @@ pub enum Aliasing {
1515
}
1616

1717
#[derive(Debug, Clone)]
18-
pub struct RuleJJ01 {
19-
}
18+
pub struct RuleJJ01;
2019

2120
impl Rule for RuleJJ01 {
2221
fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
23-
Ok()
22+
Ok(RuleJJ01.erased())
2423
}
2524

2625
fn name(&self) -> &'static str {
@@ -54,19 +53,178 @@ SELECT {{ a }} from {{
5453
"#
5554
}
5655

56+
5757
fn groups(&self) -> &'static [RuleGroups] {
5858
&[RuleGroups::All, RuleGroups::Jinja]
5959
}
6060

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!();
63162
}
64163

65164
fn is_fix_compatible(&self) -> bool {
66165
true
67166
}
68167

69168
fn crawl_behaviour(&self) -> Crawler {
70-
SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::AliasExpression]) }).into()
71-
}
169+
RootOnlyCrawler.into()
170+
}
72171
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
rule: JJ01
2+
3+
test_simple:
4+
pass_str: SELECT 1 from {{ ref('foo') }}
5+
6+
test_simple_modified:
7+
# Test that the plus/minus notation works fine.
8+
pass_str: SELECT 1 from {%+ if true -%} foo {%- endif %}
9+
10+
test_simple_modified_fail:
11+
# Test that the plus/minus notation works fine.
12+
fail_str: SELECT 1 from {%+if true-%} {{ref('foo')}} {%-endif%}
13+
fix_str: SELECT 1 from {%+ if true -%} {{ ref('foo') }} {%- endif %}
14+
15+
test_fail_jinja_tags_no_space:
16+
fail_str: SELECT 1 from {{ref('foo')}}
17+
fix_str: SELECT 1 from {{ ref('foo') }}
18+
19+
test_fail_jinja_tags_multiple_spaces:
20+
fail_str: SELECT 1 from {{ ref('foo') }}
21+
fix_str: SELECT 1 from {{ ref('foo') }}
22+
23+
test_fail_jinja_tags_no_space_2:
24+
fail_str: SELECT 1 from {{+ref('foo')-}}
25+
fix_str: SELECT 1 from {{+ ref('foo') -}}
26+
27+
test_pass_newlines:
28+
# It's ok if there are newlines.
29+
pass_str:
30+
SELECT 1 from {{
31+
ref('foo')
32+
}}
33+
34+
test_fail_templated_segment_contains_leading_literal:
35+
fail_str: |
36+
SELECT user_id
37+
FROM
38+
`{{"gcp_project"}}.{{"dataset"}}.campaign_performance`
39+
fix_str: |
40+
SELECT user_id
41+
FROM
42+
`{{ "gcp_project" }}.{{ "dataset" }}.campaign_performance`
43+
configs:
44+
core:
45+
dialect: bigquery
46+
47+
test_fail_segment_contains_multiple_templated_slices_last_one_bad:
48+
fail_str: CREATE TABLE `{{ "project" }}.{{ "dataset" }}.{{"table"}}`
49+
fix_str: CREATE TABLE `{{ "project" }}.{{ "dataset" }}.{{ "table" }}`
50+
configs:
51+
core:
52+
dialect: bigquery
53+
54+
test_fail_jinja_tags_no_space_no_content:
55+
fail_str: SELECT {{""-}}1
56+
fix_str: SELECT {{ "" -}}1
57+
58+
test_fail_jinja_tags_across_segment_boundaries:
59+
fail_str: SELECT a{{-"1 + b"}}2
60+
fix_str: SELECT a{{- "1 + b" }}2
61+
62+
test_pass_python_templater:
63+
pass_str: SELECT * FROM hello.{my_table};
64+
configs:
65+
core:
66+
templater: python
67+
templater:
68+
python:
69+
context:
70+
my_table: foo

0 commit comments

Comments
 (0)