Skip to main content

flowscope_core/linter/
config.rs

1//! Configuration for the SQL linter.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use std::collections::{BTreeMap, HashSet};
6
7/// Configuration for the SQL linter.
8///
9/// Controls which lint rules are enabled/disabled. By default, all rules are enabled.
10#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
11#[serde(rename_all = "camelCase")]
12pub struct LintConfig {
13    /// Master toggle for linting (default: true).
14    #[serde(default = "default_enabled")]
15    pub enabled: bool,
16
17    /// List of rule codes to disable (e.g., ["LINT_AM_008"]).
18    #[serde(default, skip_serializing_if = "Vec::is_empty")]
19    pub disabled_rules: Vec<String>,
20
21    /// Per-rule option objects keyed by rule reference (`LINT_*`, `AL01`,
22    /// `aliasing.table`, etc).
23    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
24    pub rule_configs: BTreeMap<String, serde_json::Value>,
25}
26
27impl Default for LintConfig {
28    fn default() -> Self {
29        Self {
30            enabled: true,
31            disabled_rules: Vec::new(),
32            rule_configs: BTreeMap::new(),
33        }
34    }
35}
36
37fn default_enabled() -> bool {
38    true
39}
40
41impl LintConfig {
42    /// Returns true if a specific rule is enabled.
43    pub fn is_rule_enabled(&self, code: &str) -> bool {
44        let requested =
45            canonicalize_rule_code(code).unwrap_or_else(|| code.trim().to_ascii_uppercase());
46        let disabled: HashSet<String> = self
47            .disabled_rules
48            .iter()
49            .filter_map(|rule| canonicalize_rule_code(rule))
50            .collect();
51
52        self.enabled && !disabled.contains(&requested)
53    }
54
55    /// Returns a rule-level config object, if present.
56    pub fn rule_config_object(
57        &self,
58        code: &str,
59    ) -> Option<&serde_json::Map<String, serde_json::Value>> {
60        self.matching_rule_config_value(code)?.as_object()
61    }
62
63    /// Returns a string option for a rule config.
64    pub fn rule_option_str(&self, code: &str, key: &str) -> Option<&str> {
65        self.rule_config_object(code)?.get(key)?.as_str()
66    }
67
68    /// Returns a boolean option for a rule config.
69    pub fn rule_option_bool(&self, code: &str, key: &str) -> Option<bool> {
70        self.rule_config_object(code)?.get(key)?.as_bool()
71    }
72
73    /// Returns an unsigned integer option for a rule config.
74    pub fn rule_option_usize(&self, code: &str, key: &str) -> Option<usize> {
75        let value = self.rule_config_object(code)?.get(key)?.as_u64()?;
76        usize::try_from(value).ok()
77    }
78
79    /// Returns a list-of-string option for a rule config.
80    pub fn rule_option_string_list(&self, code: &str, key: &str) -> Option<Vec<String>> {
81        let values = self.rule_config_object(code)?.get(key)?.as_array()?;
82        values
83            .iter()
84            .map(|value| value.as_str().map(str::to_string))
85            .collect()
86    }
87
88    /// Returns an object for a named top-level config section.
89    pub fn config_section_object(
90        &self,
91        section: &str,
92    ) -> Option<&serde_json::Map<String, serde_json::Value>> {
93        self.rule_configs
94            .iter()
95            .find_map(|(rule_ref, value)| rule_ref.eq_ignore_ascii_case(section).then_some(value))
96            .and_then(serde_json::Value::as_object)
97    }
98
99    /// Returns a string option from a named top-level config section.
100    pub fn section_option_str(&self, section: &str, key: &str) -> Option<&str> {
101        self.config_section_object(section)?.get(key)?.as_str()
102    }
103
104    /// Returns a boolean option from a named top-level config section.
105    pub fn section_option_bool(&self, section: &str, key: &str) -> Option<bool> {
106        self.config_section_object(section)?.get(key)?.as_bool()
107    }
108
109    /// Returns an unsigned integer option from a named top-level config section.
110    pub fn section_option_usize(&self, section: &str, key: &str) -> Option<usize> {
111        let value = self.config_section_object(section)?.get(key)?.as_u64()?;
112        usize::try_from(value).ok()
113    }
114
115    /// Returns a boolean option from the top-level SQLFluff `core` config map.
116    pub fn core_option_bool(&self, key: &str) -> Option<bool> {
117        self.section_option_bool("core", key)
118    }
119
120    fn matching_rule_config_value(&self, code: &str) -> Option<&serde_json::Value> {
121        let canonical = canonicalize_rule_code(code)?;
122        self.rule_configs.iter().find_map(|(rule_ref, value)| {
123            (canonicalize_rule_code(rule_ref).as_deref() == Some(canonical.as_str()))
124                .then_some(value)
125        })
126    }
127}
128
129/// Canonicalizes a user-facing rule spec to a canonical `LINT_*` code.
130pub fn canonicalize_rule_code(rule: &str) -> Option<String> {
131    let raw = rule.trim();
132    if raw.is_empty() {
133        return None;
134    }
135
136    let normalized = raw.to_ascii_uppercase();
137
138    if let Some(code) = canonical_lint_code(&normalized) {
139        return Some(code.to_string());
140    }
141
142    if let Some(code) = underscore_or_short_code_to_lint(&normalized) {
143        return Some(code);
144    }
145
146    if let Some(code) = dotted_name_to_code(&normalized) {
147        return Some(code.to_string());
148    }
149
150    None
151}
152
153/// SQLFluff dotted rule name for a canonical rule code.
154pub fn sqlfluff_name_for_code(code: &str) -> Option<&'static str> {
155    let canonical = canonicalize_rule_code(code)?;
156    match canonical.as_str() {
157        "LINT_AL_001" => Some("aliasing.table"),
158        "LINT_AL_002" => Some("aliasing.column"),
159        "LINT_AL_003" => Some("aliasing.expression"),
160        "LINT_AL_004" => Some("aliasing.unique.table"),
161        "LINT_AL_005" => Some("aliasing.unused"),
162        "LINT_AL_006" => Some("aliasing.length"),
163        "LINT_AL_007" => Some("aliasing.forbid"),
164        "LINT_AL_008" => Some("aliasing.unique.column"),
165        "LINT_AL_009" => Some("aliasing.self_alias.column"),
166        "LINT_AM_001" => Some("ambiguous.distinct"),
167        "LINT_AM_002" => Some("ambiguous.union"),
168        "LINT_AM_003" => Some("ambiguous.order_by"),
169        "LINT_AM_004" => Some("ambiguous.column_count"),
170        "LINT_AM_005" => Some("ambiguous.join"),
171        "LINT_AM_006" => Some("ambiguous.column_references"),
172        "LINT_AM_007" => Some("ambiguous.set_columns"),
173        "LINT_AM_008" => Some("ambiguous.join_condition"),
174        "LINT_AM_009" => Some("ambiguous.order_by_limit"),
175        "LINT_CP_001" => Some("capitalisation.keywords"),
176        "LINT_CP_002" => Some("capitalisation.identifiers"),
177        "LINT_CP_003" => Some("capitalisation.functions"),
178        "LINT_CP_004" => Some("capitalisation.literals"),
179        "LINT_CP_005" => Some("capitalisation.types"),
180        "LINT_CV_001" => Some("convention.not_equal"),
181        "LINT_CV_002" => Some("convention.coalesce"),
182        "LINT_CV_003" => Some("convention.select_trailing_comma"),
183        "LINT_CV_004" => Some("convention.count_rows"),
184        "LINT_CV_005" => Some("convention.is_null"),
185        "LINT_CV_006" => Some("convention.terminator"),
186        "LINT_CV_007" => Some("convention.statement_brackets"),
187        "LINT_CV_008" => Some("convention.left_join"),
188        "LINT_CV_009" => Some("convention.blocked_words"),
189        "LINT_CV_010" => Some("convention.quoted_literals"),
190        "LINT_CV_011" => Some("convention.casting_style"),
191        "LINT_CV_012" => Some("convention.join_condition"),
192        "LINT_JJ_001" => Some("jinja.padding"),
193        "LINT_LT_001" => Some("layout.spacing"),
194        "LINT_LT_002" => Some("layout.indent"),
195        "LINT_LT_003" => Some("layout.operators"),
196        "LINT_LT_004" => Some("layout.commas"),
197        "LINT_LT_005" => Some("layout.long_lines"),
198        "LINT_LT_006" => Some("layout.functions"),
199        "LINT_LT_007" => Some("layout.cte_bracket"),
200        "LINT_LT_008" => Some("layout.cte_newline"),
201        "LINT_LT_009" => Some("layout.select_targets"),
202        "LINT_LT_010" => Some("layout.select_modifiers"),
203        "LINT_LT_011" => Some("layout.set_operators"),
204        "LINT_LT_012" => Some("layout.end_of_file"),
205        "LINT_LT_013" => Some("layout.start_of_file"),
206        "LINT_LT_014" => Some("layout.keyword_newline"),
207        "LINT_LT_015" => Some("layout.newlines"),
208        "LINT_RF_001" => Some("references.from"),
209        "LINT_RF_002" => Some("references.qualification"),
210        "LINT_RF_003" => Some("references.consistent"),
211        "LINT_RF_004" => Some("references.keywords"),
212        "LINT_RF_005" => Some("references.special_chars"),
213        "LINT_RF_006" => Some("references.quoting"),
214        "LINT_ST_001" => Some("structure.else_null"),
215        "LINT_ST_002" => Some("structure.simple_case"),
216        "LINT_ST_003" => Some("structure.unused_cte"),
217        "LINT_ST_004" => Some("structure.nested_case"),
218        "LINT_ST_005" => Some("structure.subquery"),
219        "LINT_ST_006" => Some("structure.column_order"),
220        "LINT_ST_007" => Some("structure.using"),
221        "LINT_ST_008" => Some("structure.distinct"),
222        "LINT_ST_009" => Some("structure.join_condition_order"),
223        "LINT_ST_010" => Some("structure.constant_expression"),
224        "LINT_ST_011" => Some("structure.unused_join"),
225        "LINT_ST_012" => Some("structure.consecutive_semicolons"),
226        "LINT_TQ_001" => Some("tsql.sp_prefix"),
227        "LINT_TQ_002" => Some("tsql.procedure_begin_end"),
228        "LINT_TQ_003" => Some("tsql.empty_batch"),
229        _ => None,
230    }
231}
232
233fn canonical_lint_code(normalized: &str) -> Option<&'static str> {
234    if !normalized.starts_with("LINT_") {
235        return None;
236    }
237
238    if sqlfluff_name_for_canonical_code(normalized).is_some() {
239        Some(match normalized {
240            "LINT_AL_001" => "LINT_AL_001",
241            "LINT_AL_002" => "LINT_AL_002",
242            "LINT_AL_003" => "LINT_AL_003",
243            "LINT_AL_004" => "LINT_AL_004",
244            "LINT_AL_005" => "LINT_AL_005",
245            "LINT_AL_006" => "LINT_AL_006",
246            "LINT_AL_007" => "LINT_AL_007",
247            "LINT_AL_008" => "LINT_AL_008",
248            "LINT_AL_009" => "LINT_AL_009",
249            "LINT_AM_001" => "LINT_AM_001",
250            "LINT_AM_002" => "LINT_AM_002",
251            "LINT_AM_003" => "LINT_AM_003",
252            "LINT_AM_004" => "LINT_AM_004",
253            "LINT_AM_005" => "LINT_AM_005",
254            "LINT_AM_006" => "LINT_AM_006",
255            "LINT_AM_007" => "LINT_AM_007",
256            "LINT_AM_008" => "LINT_AM_008",
257            "LINT_AM_009" => "LINT_AM_009",
258            "LINT_CP_001" => "LINT_CP_001",
259            "LINT_CP_002" => "LINT_CP_002",
260            "LINT_CP_003" => "LINT_CP_003",
261            "LINT_CP_004" => "LINT_CP_004",
262            "LINT_CP_005" => "LINT_CP_005",
263            "LINT_CV_001" => "LINT_CV_001",
264            "LINT_CV_002" => "LINT_CV_002",
265            "LINT_CV_003" => "LINT_CV_003",
266            "LINT_CV_004" => "LINT_CV_004",
267            "LINT_CV_005" => "LINT_CV_005",
268            "LINT_CV_006" => "LINT_CV_006",
269            "LINT_CV_007" => "LINT_CV_007",
270            "LINT_CV_008" => "LINT_CV_008",
271            "LINT_CV_009" => "LINT_CV_009",
272            "LINT_CV_010" => "LINT_CV_010",
273            "LINT_CV_011" => "LINT_CV_011",
274            "LINT_CV_012" => "LINT_CV_012",
275            "LINT_JJ_001" => "LINT_JJ_001",
276            "LINT_LT_001" => "LINT_LT_001",
277            "LINT_LT_002" => "LINT_LT_002",
278            "LINT_LT_003" => "LINT_LT_003",
279            "LINT_LT_004" => "LINT_LT_004",
280            "LINT_LT_005" => "LINT_LT_005",
281            "LINT_LT_006" => "LINT_LT_006",
282            "LINT_LT_007" => "LINT_LT_007",
283            "LINT_LT_008" => "LINT_LT_008",
284            "LINT_LT_009" => "LINT_LT_009",
285            "LINT_LT_010" => "LINT_LT_010",
286            "LINT_LT_011" => "LINT_LT_011",
287            "LINT_LT_012" => "LINT_LT_012",
288            "LINT_LT_013" => "LINT_LT_013",
289            "LINT_LT_014" => "LINT_LT_014",
290            "LINT_LT_015" => "LINT_LT_015",
291            "LINT_RF_001" => "LINT_RF_001",
292            "LINT_RF_002" => "LINT_RF_002",
293            "LINT_RF_003" => "LINT_RF_003",
294            "LINT_RF_004" => "LINT_RF_004",
295            "LINT_RF_005" => "LINT_RF_005",
296            "LINT_RF_006" => "LINT_RF_006",
297            "LINT_ST_001" => "LINT_ST_001",
298            "LINT_ST_002" => "LINT_ST_002",
299            "LINT_ST_003" => "LINT_ST_003",
300            "LINT_ST_004" => "LINT_ST_004",
301            "LINT_ST_005" => "LINT_ST_005",
302            "LINT_ST_006" => "LINT_ST_006",
303            "LINT_ST_007" => "LINT_ST_007",
304            "LINT_ST_008" => "LINT_ST_008",
305            "LINT_ST_009" => "LINT_ST_009",
306            "LINT_ST_010" => "LINT_ST_010",
307            "LINT_ST_011" => "LINT_ST_011",
308            "LINT_ST_012" => "LINT_ST_012",
309            "LINT_TQ_001" => "LINT_TQ_001",
310            "LINT_TQ_002" => "LINT_TQ_002",
311            "LINT_TQ_003" => "LINT_TQ_003",
312            _ => return None,
313        })
314    } else {
315        None
316    }
317}
318
319fn sqlfluff_name_for_canonical_code(code: &str) -> Option<&'static str> {
320    match code {
321        "LINT_AL_001" => Some("aliasing.table"),
322        "LINT_AL_002" => Some("aliasing.column"),
323        "LINT_AL_003" => Some("aliasing.expression"),
324        "LINT_AL_004" => Some("aliasing.unique.table"),
325        "LINT_AL_005" => Some("aliasing.unused"),
326        "LINT_AL_006" => Some("aliasing.length"),
327        "LINT_AL_007" => Some("aliasing.forbid"),
328        "LINT_AL_008" => Some("aliasing.unique.column"),
329        "LINT_AL_009" => Some("aliasing.self_alias.column"),
330        "LINT_AM_001" => Some("ambiguous.distinct"),
331        "LINT_AM_002" => Some("ambiguous.union"),
332        "LINT_AM_003" => Some("ambiguous.order_by"),
333        "LINT_AM_004" => Some("ambiguous.column_count"),
334        "LINT_AM_005" => Some("ambiguous.join"),
335        "LINT_AM_006" => Some("ambiguous.column_references"),
336        "LINT_AM_007" => Some("ambiguous.set_columns"),
337        "LINT_AM_008" => Some("ambiguous.join_condition"),
338        "LINT_AM_009" => Some("ambiguous.order_by_limit"),
339        "LINT_CP_001" => Some("capitalisation.keywords"),
340        "LINT_CP_002" => Some("capitalisation.identifiers"),
341        "LINT_CP_003" => Some("capitalisation.functions"),
342        "LINT_CP_004" => Some("capitalisation.literals"),
343        "LINT_CP_005" => Some("capitalisation.types"),
344        "LINT_CV_001" => Some("convention.not_equal"),
345        "LINT_CV_002" => Some("convention.coalesce"),
346        "LINT_CV_003" => Some("convention.select_trailing_comma"),
347        "LINT_CV_004" => Some("convention.count_rows"),
348        "LINT_CV_005" => Some("convention.is_null"),
349        "LINT_CV_006" => Some("convention.terminator"),
350        "LINT_CV_007" => Some("convention.statement_brackets"),
351        "LINT_CV_008" => Some("convention.left_join"),
352        "LINT_CV_009" => Some("convention.blocked_words"),
353        "LINT_CV_010" => Some("convention.quoted_literals"),
354        "LINT_CV_011" => Some("convention.casting_style"),
355        "LINT_CV_012" => Some("convention.join_condition"),
356        "LINT_JJ_001" => Some("jinja.padding"),
357        "LINT_LT_001" => Some("layout.spacing"),
358        "LINT_LT_002" => Some("layout.indent"),
359        "LINT_LT_003" => Some("layout.operators"),
360        "LINT_LT_004" => Some("layout.commas"),
361        "LINT_LT_005" => Some("layout.long_lines"),
362        "LINT_LT_006" => Some("layout.functions"),
363        "LINT_LT_007" => Some("layout.cte_bracket"),
364        "LINT_LT_008" => Some("layout.cte_newline"),
365        "LINT_LT_009" => Some("layout.select_targets"),
366        "LINT_LT_010" => Some("layout.select_modifiers"),
367        "LINT_LT_011" => Some("layout.set_operators"),
368        "LINT_LT_012" => Some("layout.end_of_file"),
369        "LINT_LT_013" => Some("layout.start_of_file"),
370        "LINT_LT_014" => Some("layout.keyword_newline"),
371        "LINT_LT_015" => Some("layout.newlines"),
372        "LINT_RF_001" => Some("references.from"),
373        "LINT_RF_002" => Some("references.qualification"),
374        "LINT_RF_003" => Some("references.consistent"),
375        "LINT_RF_004" => Some("references.keywords"),
376        "LINT_RF_005" => Some("references.special_chars"),
377        "LINT_RF_006" => Some("references.quoting"),
378        "LINT_ST_001" => Some("structure.else_null"),
379        "LINT_ST_002" => Some("structure.simple_case"),
380        "LINT_ST_003" => Some("structure.unused_cte"),
381        "LINT_ST_004" => Some("structure.nested_case"),
382        "LINT_ST_005" => Some("structure.subquery"),
383        "LINT_ST_006" => Some("structure.column_order"),
384        "LINT_ST_007" => Some("structure.using"),
385        "LINT_ST_008" => Some("structure.distinct"),
386        "LINT_ST_009" => Some("structure.join_condition_order"),
387        "LINT_ST_010" => Some("structure.constant_expression"),
388        "LINT_ST_011" => Some("structure.unused_join"),
389        "LINT_ST_012" => Some("structure.consecutive_semicolons"),
390        "LINT_TQ_001" => Some("tsql.sp_prefix"),
391        "LINT_TQ_002" => Some("tsql.procedure_begin_end"),
392        "LINT_TQ_003" => Some("tsql.empty_batch"),
393        _ => None,
394    }
395}
396
397fn underscore_or_short_code_to_lint(normalized: &str) -> Option<String> {
398    if normalized.len() == 6 {
399        let mut chars = normalized.chars();
400        let p0 = chars.next()?;
401        let p1 = chars.next()?;
402        let underscore = chars.next()?;
403        let d0 = chars.next()?;
404        let d1 = chars.next()?;
405        let d2 = chars.next()?;
406
407        if p0.is_ascii_alphabetic()
408            && p1.is_ascii_alphabetic()
409            && underscore == '_'
410            && d0.is_ascii_digit()
411            && d1.is_ascii_digit()
412            && d2.is_ascii_digit()
413        {
414            let lint = format!("LINT_{}", normalized);
415            return canonical_lint_code(&lint).map(str::to_string);
416        }
417    }
418
419    if normalized.len() >= 3 {
420        let prefix = &normalized[..2];
421        let digits = &normalized[2..];
422
423        if prefix.chars().all(|c| c.is_ascii_alphabetic())
424            && digits.chars().all(|c| c.is_ascii_digit())
425        {
426            let number: usize = digits.parse().ok()?;
427            if number == 0 || number > 999 {
428                return None;
429            }
430            let lint = format!("LINT_{}_{number:03}", prefix);
431            return canonical_lint_code(&lint).map(str::to_string);
432        }
433    }
434
435    None
436}
437
438fn dotted_name_to_code(alias: &str) -> Option<&'static str> {
439    match alias {
440        "ALIASING.TABLE" => Some("LINT_AL_001"),
441        "ALIASING.COLUMN" => Some("LINT_AL_002"),
442        "ALIASING.EXPRESSION" => Some("LINT_AL_003"),
443        "ALIASING.UNIQUE.TABLE" => Some("LINT_AL_004"),
444        "ALIASING.UNUSED" => Some("LINT_AL_005"),
445        "ALIASING.LENGTH" => Some("LINT_AL_006"),
446        "ALIASING.FORBID" => Some("LINT_AL_007"),
447        "ALIASING.UNIQUE.COLUMN" => Some("LINT_AL_008"),
448        "ALIASING.SELF_ALIAS.COLUMN" => Some("LINT_AL_009"),
449        "AMBIGUOUS.DISTINCT" => Some("LINT_AM_001"),
450        "AMBIGUOUS.UNION" => Some("LINT_AM_002"),
451        "AMBIGUOUS.ORDER_BY" => Some("LINT_AM_003"),
452        "AMBIGUOUS.COLUMN_COUNT" => Some("LINT_AM_004"),
453        "AMBIGUOUS.JOIN" => Some("LINT_AM_005"),
454        "AMBIGUOUS.COLUMN_REFERENCES" => Some("LINT_AM_006"),
455        "AMBIGUOUS.SET_COLUMNS" => Some("LINT_AM_007"),
456        "AMBIGUOUS.JOIN_CONDITION" | "AMBIGUOUS.JOIN.CONDITION" => Some("LINT_AM_008"),
457        "AMBIGUOUS.ORDER_BY_LIMIT" => Some("LINT_AM_009"),
458        "CAPITALISATION.KEYWORDS" => Some("LINT_CP_001"),
459        "CAPITALISATION.IDENTIFIERS" => Some("LINT_CP_002"),
460        "CAPITALISATION.FUNCTIONS" => Some("LINT_CP_003"),
461        "CAPITALISATION.LITERALS" => Some("LINT_CP_004"),
462        "CAPITALISATION.TYPES" => Some("LINT_CP_005"),
463        "CONVENTION.NOT_EQUAL" => Some("LINT_CV_001"),
464        "CONVENTION.COALESCE" => Some("LINT_CV_002"),
465        "CONVENTION.SELECT_TRAILING_COMMA" => Some("LINT_CV_003"),
466        "CONVENTION.COUNT_ROWS" | "CONVENTION.STAR_COUNT" => Some("LINT_CV_004"),
467        "CONVENTION.IS_NULL" => Some("LINT_CV_005"),
468        "CONVENTION.TERMINATOR" => Some("LINT_CV_006"),
469        "CONVENTION.STATEMENT_BRACKETS" => Some("LINT_CV_007"),
470        "CONVENTION.LEFT_JOIN" => Some("LINT_CV_008"),
471        "CONVENTION.BLOCKED_WORDS" => Some("LINT_CV_009"),
472        "CONVENTION.QUOTED_LITERALS" => Some("LINT_CV_010"),
473        "CONVENTION.CASTING_STYLE" => Some("LINT_CV_011"),
474        "CONVENTION.JOIN_CONDITION" | "CONVENTION.JOIN" => Some("LINT_CV_012"),
475        "JJ.PADDING" | "JINJA.PADDING" | "JJ.JJ01" => Some("LINT_JJ_001"),
476        "LAYOUT.SPACING" => Some("LINT_LT_001"),
477        "LAYOUT.INDENT" => Some("LINT_LT_002"),
478        "LAYOUT.OPERATORS" => Some("LINT_LT_003"),
479        "LAYOUT.COMMAS" => Some("LINT_LT_004"),
480        "LAYOUT.LONG_LINES" => Some("LINT_LT_005"),
481        "LAYOUT.FUNCTIONS" => Some("LINT_LT_006"),
482        "LAYOUT.CTE_BRACKET" => Some("LINT_LT_007"),
483        "LAYOUT.CTE_NEWLINE" => Some("LINT_LT_008"),
484        "LAYOUT.SELECT_TARGETS" => Some("LINT_LT_009"),
485        "LAYOUT.SELECT_MODIFIERS" => Some("LINT_LT_010"),
486        "LAYOUT.SET_OPERATORS" => Some("LINT_LT_011"),
487        "LAYOUT.END_OF_FILE" => Some("LINT_LT_012"),
488        "LAYOUT.START_OF_FILE" => Some("LINT_LT_013"),
489        "LAYOUT.KEYWORD_NEWLINE" => Some("LINT_LT_014"),
490        "LAYOUT.NEWLINES" => Some("LINT_LT_015"),
491        "REFERENCES.FROM" => Some("LINT_RF_001"),
492        "REFERENCES.QUALIFICATION" => Some("LINT_RF_002"),
493        "REFERENCES.CONSISTENT" => Some("LINT_RF_003"),
494        "REFERENCES.KEYWORDS" => Some("LINT_RF_004"),
495        "REFERENCES.SPECIAL_CHARS" => Some("LINT_RF_005"),
496        "REFERENCES.QUOTING" => Some("LINT_RF_006"),
497        "STRUCTURE.ELSE_NULL" => Some("LINT_ST_001"),
498        "STRUCTURE.SIMPLE_CASE" => Some("LINT_ST_002"),
499        "STRUCTURE.UNUSED_CTE" => Some("LINT_ST_003"),
500        "STRUCTURE.NESTED_CASE" => Some("LINT_ST_004"),
501        "STRUCTURE.SUBQUERY" => Some("LINT_ST_005"),
502        "STRUCTURE.COLUMN_ORDER" => Some("LINT_ST_006"),
503        "STRUCTURE.USING" => Some("LINT_ST_007"),
504        "STRUCTURE.DISTINCT" => Some("LINT_ST_008"),
505        "STRUCTURE.JOIN_CONDITION_ORDER" => Some("LINT_ST_009"),
506        "STRUCTURE.CONSTANT_EXPRESSION" => Some("LINT_ST_010"),
507        "STRUCTURE.UNUSED_JOIN" => Some("LINT_ST_011"),
508        "STRUCTURE.CONSECUTIVE_SEMICOLONS" => Some("LINT_ST_012"),
509        "TSQL.SP_PREFIX" => Some("LINT_TQ_001"),
510        "TSQL.PROCEDURE_BEGIN_END" => Some("LINT_TQ_002"),
511        "TSQL.EMPTY_BATCH" => Some("LINT_TQ_003"),
512        _ => None,
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519
520    #[test]
521    fn default_config_enables_all() {
522        let config = LintConfig::default();
523        assert!(config.enabled);
524        assert!(config.is_rule_enabled("LINT_AM_008"));
525    }
526
527    #[test]
528    fn disabled_rules_are_case_insensitive_and_trimmed() {
529        let config = LintConfig {
530            enabled: true,
531            disabled_rules: vec![" lint_am_009 ".to_string(), " LINT_ST_006".to_string()],
532            rule_configs: BTreeMap::new(),
533        };
534        assert!(!config.is_rule_enabled("LINT_AM_009"));
535        assert!(!config.is_rule_enabled("lint_st_006"));
536        assert!(config.is_rule_enabled("LINT_CV_007"));
537    }
538
539    #[test]
540    fn disabled_rules_support_dotted_names() {
541        let config = LintConfig {
542            enabled: true,
543            disabled_rules: vec![
544                " ambiguous.join ".to_string(),
545                "AMBIGUOUS.UNION".to_string(),
546            ],
547            rule_configs: BTreeMap::new(),
548        };
549        assert!(!config.is_rule_enabled("LINT_AM_005"));
550        assert!(!config.is_rule_enabled("LINT_AM_002"));
551        assert!(config.is_rule_enabled("LINT_AM_001"));
552    }
553
554    #[test]
555    fn canonicalize_rule_supports_short_and_underscore_forms() {
556        assert_eq!(
557            canonicalize_rule_code("al01"),
558            Some("LINT_AL_001".to_string())
559        );
560        assert_eq!(
561            canonicalize_rule_code("AL_001"),
562            Some("LINT_AL_001".to_string())
563        );
564        assert_eq!(
565            canonicalize_rule_code("ambiguous.order_by"),
566            Some("LINT_AM_003".to_string())
567        );
568        assert_eq!(canonicalize_rule_code("unknown"), None);
569    }
570
571    #[test]
572    fn sqlfluff_name_lookup_works() {
573        assert_eq!(
574            sqlfluff_name_for_code("LINT_CV_008"),
575            Some("convention.left_join")
576        );
577        assert_eq!(sqlfluff_name_for_code("cv08"), Some("convention.left_join"));
578        assert_eq!(sqlfluff_name_for_code("unknown"), None);
579    }
580
581    #[test]
582    fn master_toggle_off_disables_everything() {
583        let config = LintConfig {
584            enabled: false,
585            disabled_rules: vec![],
586            rule_configs: BTreeMap::new(),
587        };
588        assert!(!config.is_rule_enabled("LINT_AM_008"));
589    }
590
591    #[test]
592    fn deserialization_defaults() {
593        let json = "{}";
594        let config: LintConfig = serde_json::from_str(json).expect("valid lint config json");
595        assert!(config.enabled);
596        assert!(config.disabled_rules.is_empty());
597        assert!(config.rule_configs.is_empty());
598    }
599
600    #[test]
601    fn rule_config_options_resolve_by_dotted_or_code() {
602        let config = LintConfig {
603            enabled: true,
604            disabled_rules: vec![],
605            rule_configs: BTreeMap::from([
606                (
607                    "aliasing.table".to_string(),
608                    serde_json::json!({"aliasing": "implicit"}),
609                ),
610                (
611                    "LINT_AL_002".to_string(),
612                    serde_json::json!({"aliasing": "explicit"}),
613                ),
614                (
615                    "AM06".to_string(),
616                    serde_json::json!({"group_by_and_order_by_style": "explicit"}),
617                ),
618                (
619                    "references.consistent".to_string(),
620                    serde_json::json!({"single_table_references": "qualified"}),
621                ),
622            ]),
623        };
624
625        assert_eq!(
626            config.rule_option_str("LINT_AL_001", "aliasing"),
627            Some("implicit")
628        );
629        assert_eq!(
630            config.rule_option_str("aliasing.column", "aliasing"),
631            Some("explicit")
632        );
633        assert_eq!(
634            config.rule_option_str("LINT_AM_006", "group_by_and_order_by_style"),
635            Some("explicit")
636        );
637        assert_eq!(
638            config.rule_option_str("RF03", "single_table_references"),
639            Some("qualified")
640        );
641    }
642
643    #[test]
644    fn core_options_resolve_case_insensitively() {
645        let config = LintConfig {
646            enabled: true,
647            disabled_rules: vec![],
648            rule_configs: BTreeMap::from([(
649                "CORE".to_string(),
650                serde_json::json!({"ignore_templated_areas": true}),
651            )]),
652        };
653
654        assert_eq!(
655            config.core_option_bool("ignore_templated_areas"),
656            Some(true)
657        );
658    }
659
660    #[test]
661    fn section_options_resolve_case_insensitively() {
662        let config = LintConfig {
663            enabled: true,
664            disabled_rules: vec![],
665            rule_configs: BTreeMap::from([(
666                "INDENTATION".to_string(),
667                serde_json::json!({"indent_unit": "tab", "tab_space_size": 2}),
668            )]),
669        };
670
671        assert_eq!(
672            config.section_option_str("indentation", "indent_unit"),
673            Some("tab")
674        );
675        assert_eq!(
676            config.section_option_usize("indentation", "tab_space_size"),
677            Some(2)
678        );
679    }
680}