Skip to content

Commit f1934e0

Browse files
committed
Revert "Revert "fix: Comprehensive T-SQL parser and rule fixes for operators,…"
This reverts commit b5466a3.
1 parent 0675d30 commit f1934e0

File tree

10 files changed

+476
-88
lines changed

10 files changed

+476
-88
lines changed

crates/lib-dialects/src/tsql.rs

Lines changed: 315 additions & 61 deletions
Large diffs are not rendered by default.

crates/lib/src/rules/capitalisation/cp01.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,25 @@ from foo
150150
return vec![LintResult::new(None, Vec::new(), None, None)];
151151
}
152152

153-
if parent.get_type() == SyntaxKind::FunctionName && parent.segments().len() != 1 {
154-
return vec![LintResult::new(None, Vec::new(), None, None)];
153+
// For T-SQL and other dialects, some function names like CAST and COALESCE
154+
// are also keywords and should be capitalized by CP01
155+
if parent.get_type() == SyntaxKind::FunctionName {
156+
// Only process single-segment function names that are also keywords
157+
if parent.segments().len() != 1 {
158+
return vec![LintResult::new(None, Vec::new(), None, None)];
159+
}
160+
// Check if this function name is also a keyword in the dialect
161+
let dialect = &context.dialect;
162+
let function_name = context.segment.raw().to_uppercase();
163+
let is_keyword = dialect
164+
.sets("reserved_keywords")
165+
.contains(function_name.as_str())
166+
|| dialect
167+
.sets("unreserved_keywords")
168+
.contains(function_name.as_str());
169+
if !is_keyword {
170+
return vec![LintResult::new(None, Vec::new(), None, None)];
171+
}
155172
}
156173

157174
vec![handle_segment(
@@ -174,6 +191,7 @@ from foo
174191
SyntaxKind::Keyword,
175192
SyntaxKind::BinaryOperator,
176193
SyntaxKind::DatePart,
194+
SyntaxKind::FunctionNameIdentifier,
177195
])
178196
},
179197
)

crates/lib/src/rules/convention/cv01.rs

Lines changed: 137 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,42 @@ SELECT * FROM X WHERE 1 != 2 AND 3 != 4;
7777
}
7878

7979
fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
80+
// Check if this is a single ComparisonOperator segment (e.g., "<>" or "!=")
81+
if context.segment.get_type() == SyntaxKind::ComparisonOperator {
82+
return self.eval_single_operator(context);
83+
}
84+
85+
// Check if this is the first part of a multi-line comparison operator
86+
if context.segment.raw() == "<" || context.segment.raw() == "!" {
87+
if let Some(result) = self.eval_multiline_operator(context) {
88+
return vec![result];
89+
}
90+
}
91+
92+
Vec::new()
93+
}
94+
95+
fn is_fix_compatible(&self) -> bool {
96+
true
97+
}
98+
99+
fn crawl_behaviour(&self) -> Crawler {
100+
SegmentSeekerCrawler::new(
101+
const {
102+
SyntaxSet::new(&[
103+
SyntaxKind::ComparisonOperator,
104+
// Also look for individual < > ! = tokens that might be part of split operators
105+
SyntaxKind::RawComparisonOperator,
106+
SyntaxKind::Symbol,
107+
])
108+
},
109+
)
110+
.into()
111+
}
112+
}
113+
114+
impl RuleCV01 {
115+
fn eval_single_operator(&self, context: &RuleContext) -> Vec<LintResult> {
80116
// Get the comparison operator children
81117
let segment = FunctionalContext::new(context).segment();
82118
let raw_comparison_operators = segment.children(None);
@@ -166,12 +202,107 @@ SELECT * FROM X WHERE 1 != 2 AND 3 != 4;
166202
)]
167203
}
168204

169-
fn is_fix_compatible(&self) -> bool {
170-
true
171-
}
205+
fn eval_multiline_operator(&self, context: &RuleContext) -> Option<LintResult> {
206+
// Look for the pattern where we have < or ! followed by > or =
207+
// potentially separated by whitespace and comments
208+
let first_op = context.segment.raw();
172209

173-
fn crawl_behaviour(&self) -> Crawler {
174-
SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::ComparisonOperator]) })
175-
.into()
210+
// Find the next non-whitespace, non-comment segment
211+
let parent = context.parent_stack.last()?;
212+
let segments = parent.segments();
213+
let current_idx = segments.iter().position(|s| s == &context.segment)?;
214+
215+
let mut second_op_segment = None;
216+
let mut second_op_idx = None;
217+
218+
for (idx, seg) in segments.iter().enumerate().skip(current_idx + 1) {
219+
if seg.is_type(SyntaxKind::Whitespace)
220+
|| seg.is_type(SyntaxKind::InlineComment)
221+
|| seg.is_type(SyntaxKind::Newline)
222+
{
223+
continue;
224+
}
225+
226+
let raw = seg.raw();
227+
if (first_op == "<" && raw == ">") || (first_op == "!" && raw == "=") {
228+
second_op_segment = Some(seg.clone());
229+
second_op_idx = Some(idx);
230+
break;
231+
} else {
232+
// If we hit any other segment, this isn't a multi-line comparison operator
233+
return None;
234+
}
235+
}
236+
237+
let second_segment = second_op_segment?;
238+
let _second_idx = second_op_idx?;
239+
240+
// Check if this combination should be converted
241+
let current_style = if first_op == "<" {
242+
PreferredNotEqualStyle::Ansi
243+
} else {
244+
PreferredNotEqualStyle::CStyle
245+
};
246+
247+
// Determine the preferred style
248+
let preferred_style =
249+
if self.preferred_not_equal_style == PreferredNotEqualStyle::Consistent {
250+
if let Some(style) = context.try_get::<PreferredNotEqualStyle>() {
251+
style
252+
} else {
253+
context.set(current_style);
254+
current_style
255+
}
256+
} else {
257+
self.preferred_not_equal_style
258+
};
259+
260+
// If already in preferred style, no fix needed
261+
if current_style == preferred_style {
262+
return None;
263+
}
264+
265+
// Create fixes for multi-line operators
266+
let (new_first, new_second) = match preferred_style {
267+
PreferredNotEqualStyle::CStyle => ("!", "="),
268+
PreferredNotEqualStyle::Ansi => ("<", ">"),
269+
PreferredNotEqualStyle::Consistent => unreachable!(),
270+
};
271+
272+
let fixes = vec![
273+
LintFix::replace(
274+
context.segment.clone(),
275+
vec![
276+
SegmentBuilder::token(
277+
context.tables.next_id(),
278+
new_first,
279+
SyntaxKind::ComparisonOperator,
280+
)
281+
.finish(),
282+
],
283+
None,
284+
),
285+
LintFix::replace(
286+
second_segment.clone(),
287+
vec![
288+
SegmentBuilder::token(
289+
context.tables.next_id(),
290+
new_second,
291+
SyntaxKind::ComparisonOperator,
292+
)
293+
.finish(),
294+
],
295+
None,
296+
),
297+
];
298+
299+
// Create a virtual segment that spans from the first to the second operator
300+
// for the lint result anchor
301+
Some(LintResult::new(
302+
Some(context.segment.clone()),
303+
fixes,
304+
None,
305+
None,
306+
))
176307
}
177308
}

crates/lib/test/fixtures/rules/std_rule_cases/AL07.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,8 @@ issue_1589:
212212
force_enable: true
213213

214214
issue_1639:
215-
# TODO: Re-enable this test once T-SQL CREATE TABLE support is improved
216-
# Currently, T-SQL CREATE TABLE statements are not fully parsed, causing this test to fail
217-
ignored: "T-SQL CREATE TABLE syntax not fully supported yet"
215+
# TODO: Fix AL07 rule to handle CREATE TABLE statements properly
216+
ignored: "AL07 rule needs update to handle CREATE TABLE statements"
218217
fail_str: |
219218
DECLARE @VariableE date = GETDATE()
220219

crates/lib/test/fixtures/rules/std_rule_cases/CP01.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,6 @@ test_fail_select_lower:
215215

216216
test_fail_select_lower_keyword_functions:
217217
# Test for issue #3520
218-
# TODO: Fix T-SQL function name capitalization
219-
ignored: "T-SQL function name capitalization not working correctly"
220218
fail_str: |
221219
SELECT
222220
cast(5 AS int) AS test1,

crates/lib/test/fixtures/rules/std_rule_cases/CV01.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,6 @@ test_pass_c_style_not_equal_to_tsql:
101101
preferred_not_equal_style: "c_style"
102102

103103
test_fail_c_style_not_equal_to_tsql:
104-
# TODO: Fix T-SQL multi-line comparison operator handling
105-
ignored: "T-SQL multi-line comparison operator conversion not working correctly"
106104
fail_str: |
107105
SELECT * FROM X WHERE 1 <
108106
-- some comment
@@ -174,8 +172,6 @@ test_pass_ansi_not_equal_to_tsql:
174172
preferred_not_equal_style: "ansi"
175173

176174
test_fail_ansi_not_equal_to_tsql:
177-
# TODO: Fix T-SQL multi-line comparison operator handling
178-
ignored: "T-SQL multi-line comparison operator conversion not working correctly"
179175
fail_str: |
180176
SELECT * FROM X WHERE 1 !
181177
-- some comment

crates/lib/test/fixtures/rules/std_rule_cases/LT01-excessive.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,6 @@ test_comparison_operator_fix:
6262
dialect: tsql
6363

6464
test_comparison_operator_pass:
65-
# TODO: Fix T-SQL comparison operator parsing - incorrectly detecting >= as needing spacing
66-
ignored: "T-SQL comparison operator >= incorrectly parsed as two tokens"
6765
pass_str: |
6866
SELECT foo
6967
FROM bar

crates/lib/test/fixtures/rules/std_rule_cases/LT01-literals.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,12 @@ test_fail_ansi_single_quote:
6767
fix_str: "SELECT a + 'b' + 'c' FROM tbl;"
6868

6969
test_pass_tsql_unicode_single_quote:
70-
# TODO: Fix T-SQL Unicode string literal parsing - N'string' incorrectly parsed as two tokens
71-
ignored: "T-SQL Unicode string literal N'string' incorrectly parsed as two tokens"
7270
pass_str: "SELECT a + N'b' + N'c' FROM tbl;"
7371
configs:
7472
core:
7573
dialect: tsql
7674

7775
test_fail_tsql_unicode_single_quote:
78-
# TODO: Fix T-SQL Unicode string literal parsing - N'string' incorrectly parsed as two tokens
79-
ignored: "T-SQL Unicode string literal N'string' incorrectly parsed as two tokens"
8076
fail_str: "SELECT a +N'b'+N'c' FROM tbl;"
8177
fix_str: "SELECT a + N'b' + N'c' FROM tbl;"
8278
configs:

crates/lib/test/fixtures/rules/std_rule_cases/LT01-operators.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@ test_pass_sparksql_multi_units_interval_minus:
7979

8080
pass_tsql_assignment_operator:
8181
# Test that we fix the outer whitespace but don't add any in between + and =.
82-
# TODO: Fix T-SQL compound assignment operators parsing - += incorrectly parsed as two tokens
83-
ignored: "T-SQL compound assignment operator += incorrectly parsed as two tokens"
8482
fail_str: SET @param1+=1
8583
fix_str: SET @param1 += 1
8684
configs:

crates/lib/test/fixtures/rules/std_rule_cases/LT02-indent.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -786,8 +786,8 @@ test_fail_tsql_else_if_successive:
786786

787787
# TSQL function
788788
test_tsql_function:
789-
# TODO: Fix T-SQL CREATE FUNCTION parsing
790-
ignored: "T-SQL CREATE FUNCTION syntax not fully supported"
789+
# Complex T-SQL indentation requires reflow system enhancements for BEGIN/END, SET, IF constructs
790+
ignored: "T-SQL function indentation requires reflow system enhancement for BEGIN/END blocks"
791791
fail_str: |
792792
CREATE FUNCTION dbo.isoweek (@DATE datetime)
793793
RETURNS int

0 commit comments

Comments
 (0)