Skip to main content

rigsql_rules/convention/
cv10.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// CV10: Consistent use of NULL in UNION.
7///
8/// Bare NULL literals in UNION SELECT items should have an explicit type cast
9/// for clarity and consistency.
10#[derive(Debug, Default)]
11pub struct RuleCV10;
12
13impl Rule for RuleCV10 {
14    fn code(&self) -> &'static str {
15        "CV10"
16    }
17    fn name(&self) -> &'static str {
18        "convention.union_null"
19    }
20    fn description(&self) -> &'static str {
21        "Consistent use of NULL in UNION."
22    }
23    fn explanation(&self) -> &'static str {
24        "When using NULL in a UNION query, the type of the NULL column is ambiguous. \
25         Use an explicit CAST (e.g. CAST(NULL AS INTEGER)) to make the intent clear \
26         and avoid potential type-mismatch issues across UNION branches."
27    }
28    fn groups(&self) -> &[RuleGroup] {
29        &[RuleGroup::Convention]
30    }
31    fn is_fixable(&self) -> bool {
32        false
33    }
34
35    fn crawl_type(&self) -> CrawlType {
36        CrawlType::RootOnly
37    }
38
39    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40        let mut violations = Vec::new();
41        find_union_nulls(ctx.root, false, &mut violations);
42        violations
43    }
44}
45
46/// Recursively walk the tree looking for SelectStatements that are part of a
47/// UNION. When we find one, scan its SelectClause items for bare NullLiterals.
48fn find_union_nulls(segment: &Segment, in_union: bool, violations: &mut Vec<LintViolation>) {
49    let children = segment.children();
50
51    // Detect if this node contains a UNION keyword among its children,
52    // which would make sibling SelectStatements part of a UNION.
53    let has_union = children.iter().any(|c| {
54        if let Segment::Token(t) = c {
55            t.token.text.eq_ignore_ascii_case("UNION")
56        } else {
57            false
58        }
59    });
60
61    let union_context = in_union || has_union;
62
63    for child in children {
64        if union_context && child.segment_type() == SegmentType::SelectStatement {
65            check_select_for_bare_null(child, violations);
66        }
67
68        // Also check nested SelectStatements at the same level if has_union
69        if child.segment_type() == SegmentType::SelectClause && union_context {
70            check_clause_for_bare_null(child, violations);
71        }
72
73        find_union_nulls(child, union_context, violations);
74    }
75}
76
77fn check_select_for_bare_null(select: &Segment, violations: &mut Vec<LintViolation>) {
78    select.walk(&mut |seg| {
79        if seg.segment_type() == SegmentType::SelectClause {
80            check_clause_for_bare_null(seg, violations);
81        }
82    });
83}
84
85fn check_clause_for_bare_null(clause: &Segment, violations: &mut Vec<LintViolation>) {
86    for child in clause.children() {
87        find_bare_nulls(child, violations);
88    }
89}
90
91/// Walk looking for NullLiterals that are NOT inside a CastExpression.
92fn find_bare_nulls(segment: &Segment, violations: &mut Vec<LintViolation>) {
93    if segment.segment_type() == SegmentType::CastExpression {
94        return;
95    }
96
97    if segment.segment_type() == SegmentType::NullLiteral {
98        violations.push(LintViolation::new(
99            "CV10",
100            "Bare NULL in UNION. Consider using an explicit CAST.",
101            segment.span(),
102        ));
103        return;
104    }
105
106    for child in segment.children() {
107        find_bare_nulls(child, violations);
108    }
109}