Skip to main content

sqruff_lib/utils/reflow/
config.rs

1use std::str::FromStr;
2
3use hashbrown::HashMap;
4use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
5
6use crate::core::config::{FluffConfig, Value};
7use crate::utils::reflow::depth_map::{DepthInfo, StackPositionType};
8use crate::utils::reflow::rebreak::LinePosition;
9use crate::utils::reflow::reindent::{IndentUnit, TrailingComments};
10
11type ConfigDictType = HashMap<SyntaxKind, LayoutTypeConfig>;
12
13#[derive(Debug, Default, PartialEq, Eq, Clone)]
14struct LayoutTypeConfig {
15    spacing_before: Option<Spacing>,
16    spacing_after: Option<Spacing>,
17    spacing_within: Option<Spacing>,
18    line_position: Option<LinePositionConfig>,
19    keyword_line_position: Option<String>,
20    keyword_line_position_exclusions: SyntaxSet,
21}
22
23#[derive(Debug, PartialEq, Eq, Clone, Copy)]
24pub struct LinePositionConfig {
25    position: LinePosition,
26    strict: bool,
27}
28
29impl LinePositionConfig {
30    pub const fn new(position: LinePosition, strict: bool) -> Self {
31        Self { position, strict }
32    }
33
34    pub const fn position(self) -> LinePosition {
35        self.position
36    }
37
38    pub const fn is_strict(self) -> bool {
39        self.strict
40    }
41}
42
43impl FromStr for LinePositionConfig {
44    type Err = String;
45
46    fn from_str(s: &str) -> Result<Self, Self::Err> {
47        let mut parts = s.split(':');
48        let position = parts
49            .next()
50            .ok_or_else(|| "line_position cannot be empty".to_string())?
51            .parse::<LinePosition>()
52            .map_err(|_| format!("Unexpected line_position value: {s}"))?;
53        let strict = match parts.next() {
54            Some("strict") => true,
55            Some(other) => {
56                return Err(format!(
57                    "Unexpected line_position modifier '{other}' in '{s}'"
58                ));
59            }
60            None => false,
61        };
62
63        if parts.next().is_some() {
64            return Err(format!("Unexpected line_position value: {s}"));
65        }
66
67        Ok(Self::new(position, strict))
68    }
69}
70
71/// Holds spacing config for a block and allows easy manipulation
72#[derive(Debug, PartialEq, Eq, Clone)]
73pub struct BlockConfig {
74    pub spacing_before: Spacing,
75    pub spacing_after: Spacing,
76    pub spacing_within: Option<Spacing>,
77    pub line_position: Option<LinePositionConfig>,
78    pub keyword_line_position: Option<String>,
79    pub keyword_line_position_exclusions: SyntaxSet,
80}
81
82impl Default for BlockConfig {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88impl BlockConfig {
89    pub fn new() -> Self {
90        BlockConfig {
91            spacing_before: Spacing::Single,
92            spacing_after: Spacing::Single,
93            spacing_within: None,
94            line_position: None,
95            keyword_line_position: None,
96            keyword_line_position_exclusions: SyntaxSet::EMPTY,
97        }
98    }
99
100    /// Mutate the config based on additional information.
101    fn incorporate(
102        &mut self,
103        before: Option<Spacing>,
104        after: Option<Spacing>,
105        within: Option<Spacing>,
106        line_position: Option<LinePositionConfig>,
107        config: Option<&LayoutTypeConfig>,
108    ) {
109        self.spacing_before = before
110            .or_else(|| config.and_then(|c| c.spacing_before))
111            .unwrap_or(self.spacing_before);
112
113        self.spacing_after = after
114            .or_else(|| config.and_then(|c| c.spacing_after))
115            .unwrap_or(self.spacing_after);
116
117        self.spacing_within = within.or_else(|| config.and_then(|c| c.spacing_within));
118        self.line_position = line_position.or_else(|| config.and_then(|c| c.line_position));
119
120        if let Some(keyword_line_position) = config.and_then(|c| c.keyword_line_position.clone()) {
121            self.keyword_line_position = Some(keyword_line_position);
122        }
123
124        if let Some(keyword_line_position_exclusions) =
125            config.map(|c| c.keyword_line_position_exclusions.clone())
126        {
127            self.keyword_line_position_exclusions = keyword_line_position_exclusions;
128        }
129    }
130}
131
132fn parse_configured_syntax_set(raw: &str) -> SyntaxSet {
133    raw.split(',')
134        .filter_map(|seg_type| {
135            let seg_type = seg_type.trim();
136            if seg_type.is_empty() || seg_type.eq_ignore_ascii_case("none") {
137                None
138            } else {
139                parse_syntax_kind_alias(seg_type)
140            }
141        })
142        .collect()
143}
144
145fn parse_syntax_kind_alias(seg_type: &str) -> Option<SyntaxKind> {
146    match seg_type {
147        "aggregate_order_by" => Some(SyntaxKind::AggregateOrderByClause),
148        _ => seg_type.parse().ok(),
149    }
150}
151
152/// An interface onto the configuration of how segments should reflow.
153///
154/// This acts as the primary translation engine between configuration
155/// held either in dicts for testing, or in the FluffConfig in live
156/// usage, and the configuration used during reflow operations.
157#[derive(Debug, Default, PartialEq, Eq, Clone)]
158pub struct ReflowConfig {
159    configs: ConfigDictType,
160    config_types: SyntaxSet,
161    /// In production, these values are almost _always_ set because we
162    /// use `.from_fluff_config`, but the defaults are here to aid in
163    /// testing.
164    pub(crate) indent_unit: IndentUnit,
165    pub(crate) max_line_length: usize,
166    pub(crate) hanging_indents: bool,
167    pub(crate) allow_implicit_indents: bool,
168    pub(crate) trailing_comments: TrailingComments,
169}
170
171#[derive(Debug, PartialEq, Eq, Clone, Copy)]
172pub enum Spacing {
173    Single,
174    Touch,
175    TouchInline,
176    SingleInline,
177    Any,
178    Align {
179        seg_type: SyntaxKind,
180        within: Option<SyntaxKind>,
181        scope: Option<SyntaxKind>,
182    },
183}
184
185impl FromStr for Spacing {
186    type Err = ();
187
188    fn from_str(s: &str) -> Result<Self, Self::Err> {
189        Ok(match s {
190            "single" => Self::Single,
191            "touch" => Self::Touch,
192            "touch:inline" => Self::TouchInline,
193            "single:inline" => Self::SingleInline,
194            "any" => Self::Any,
195            s => {
196                if let Some(rest) = s.strip_prefix("align") {
197                    let mut args = rest.split(':');
198                    args.next();
199
200                    let seg_type = args.next().map(|it| it.parse().unwrap()).unwrap();
201                    let within = args.next().map(|it| it.parse().unwrap());
202                    let scope = args.next().map(|it| it.parse().unwrap());
203
204                    Spacing::Align {
205                        seg_type,
206                        within,
207                        scope,
208                    }
209                } else {
210                    unimplemented!("{s}")
211                }
212            }
213        })
214    }
215}
216
217impl ReflowConfig {
218    pub fn line_position_for(&self, seg_type: SyntaxKind) -> Option<LinePositionConfig> {
219        self.configs
220            .get(&seg_type)
221            .and_then(|cfg| cfg.line_position)
222    }
223
224    pub fn get_block_config(
225        &self,
226        block_class_types: &SyntaxSet,
227        depth_info: Option<&DepthInfo>,
228    ) -> BlockConfig {
229        let configured_types = block_class_types.clone().intersection(&self.config_types);
230
231        let mut block_config = BlockConfig::new();
232
233        if let Some(depth_info) = depth_info {
234            let (mut parent_start, mut parent_end) = (true, true);
235
236            for (idx, key) in depth_info.stack_hashes.iter().rev().enumerate() {
237                let stack_position = &depth_info.stack_positions[key];
238
239                if !matches!(
240                    stack_position.type_,
241                    Some(StackPositionType::Solo) | Some(StackPositionType::Start)
242                ) {
243                    parent_start = false;
244                }
245
246                if !matches!(
247                    stack_position.type_,
248                    Some(StackPositionType::Solo) | Some(StackPositionType::End)
249                ) {
250                    parent_end = false;
251                }
252
253                if !parent_start && !parent_end {
254                    break;
255                }
256
257                let parent_classes =
258                    &depth_info.stack_class_types[depth_info.stack_class_types.len() - 1 - idx];
259
260                let configured_parent_types =
261                    self.config_types.clone().intersection(parent_classes);
262
263                if parent_start {
264                    for seg_type in configured_parent_types.clone() {
265                        let before = self
266                            .configs
267                            .get(&seg_type)
268                            .and_then(|conf| conf.spacing_before);
269                        block_config.incorporate(before, None, None, None, None);
270                    }
271                }
272
273                if parent_end {
274                    for seg_type in configured_parent_types {
275                        let after = self
276                            .configs
277                            .get(&seg_type)
278                            .and_then(|conf| conf.spacing_after);
279                        block_config.incorporate(None, after, None, None, None);
280                    }
281                }
282            }
283        }
284
285        for seg_type in configured_types {
286            block_config.incorporate(None, None, None, None, self.configs.get(&seg_type));
287        }
288
289        block_config
290    }
291
292    pub fn from_fluff_config(config: &FluffConfig) -> ReflowConfig {
293        let configs = config.raw["layout"]["type"].as_map().unwrap().clone();
294        let config_types = configs
295            .keys()
296            .map(|x| x.parse().unwrap_or_else(|_| unimplemented!("{x}")))
297            .collect::<SyntaxSet>();
298
299        let trailing_comments = config.raw["indentation"]["trailing_comments"]
300            .as_string()
301            .unwrap();
302        let trailing_comments = TrailingComments::from_str(trailing_comments).unwrap();
303
304        let tab_space_size = config.raw["indentation"]["tab_space_size"]
305            .as_int()
306            .unwrap() as usize;
307        let indent_unit = config.raw["indentation"]["indent_unit"]
308            .as_string()
309            .unwrap();
310        let indent_unit = IndentUnit::from_type_and_size(indent_unit, tab_space_size);
311
312        ReflowConfig {
313            configs: convert_to_config_dict(configs),
314            config_types,
315            indent_unit,
316            max_line_length: config.raw["core"]["max_line_length"].as_int().unwrap() as usize,
317            hanging_indents: config.raw["indentation"]["hanging_indents"]
318                .as_bool()
319                .unwrap_or_default(),
320            allow_implicit_indents: config.raw["indentation"]["allow_implicit_indents"]
321                .as_bool()
322                .unwrap(),
323            trailing_comments,
324        }
325    }
326}
327
328fn convert_to_config_dict(input: HashMap<String, Value>) -> ConfigDictType {
329    let mut config_dict = ConfigDictType::new();
330
331    for (key, value) in input {
332        match value {
333            Value::Map(map_value) => {
334                let seg_type = key.parse().unwrap_or_else(|_| unimplemented!("{key}"));
335                config_dict.insert(
336                    seg_type,
337                    LayoutTypeConfig::from_value_map(seg_type, map_value),
338                );
339            }
340            _ => panic!("Expected a Value::Map, found another variant."),
341        }
342    }
343
344    config_dict
345}
346
347impl LayoutTypeConfig {
348    fn from_value_map(seg_type: SyntaxKind, map_value: HashMap<String, Value>) -> Self {
349        Self {
350            spacing_before: spacing_from_map(seg_type, &map_value, "spacing_before"),
351            spacing_after: spacing_from_map(seg_type, &map_value, "spacing_after"),
352            spacing_within: spacing_from_map(seg_type, &map_value, "spacing_within"),
353            line_position: map_value
354                .get("line_position")
355                .map(string_value)
356                .transpose()
357                .unwrap()
358                .map(|it| it.parse().unwrap()),
359            keyword_line_position: map_value
360                .get("keyword_line_position")
361                .map(string_value)
362                .transpose()
363                .unwrap()
364                .map(ToOwned::to_owned),
365            keyword_line_position_exclusions: map_value
366                .get("keyword_line_position_exclusions")
367                .map(string_value)
368                .transpose()
369                .unwrap()
370                .map(parse_configured_syntax_set)
371                .unwrap_or(SyntaxSet::EMPTY),
372        }
373    }
374}
375
376fn spacing_from_map(
377    seg_type: SyntaxKind,
378    map_value: &HashMap<String, Value>,
379    key: &str,
380) -> Option<Spacing> {
381    let spacing = map_value.get(key).map(string_value).transpose().unwrap()?;
382    if spacing == "align" {
383        Some(Spacing::Align {
384            seg_type,
385            within: map_value
386                .get("align_within")
387                .map(string_value)
388                .transpose()
389                .unwrap()
390                .map(|it| it.parse().unwrap()),
391            scope: map_value
392                .get("align_scope")
393                .map(string_value)
394                .transpose()
395                .unwrap()
396                .map(|it| it.parse().unwrap()),
397        })
398    } else {
399        Some(spacing.parse().unwrap())
400    }
401}
402
403fn string_value(value: &Value) -> Result<&str, String> {
404    value
405        .as_string()
406        .ok_or_else(|| "Expected a Value::String, found another variant.".to_string())
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    #[test]
414    fn parses_line_position_config() {
415        let config: LinePositionConfig = "alone:strict".parse().unwrap();
416
417        assert_eq!(config.position(), LinePosition::Alone);
418        assert!(config.is_strict());
419    }
420
421    #[test]
422    fn parses_align_spacing_from_layout_config() {
423        let mut layout = HashMap::new();
424        layout.insert("spacing_before".into(), Value::String("align".into()));
425        layout.insert("align_within".into(), Value::String("select_clause".into()));
426        layout.insert("align_scope".into(), Value::String("statement".into()));
427
428        let config = LayoutTypeConfig::from_value_map(SyntaxKind::AliasExpression, layout);
429
430        assert_eq!(
431            config.spacing_before,
432            Some(Spacing::Align {
433                seg_type: SyntaxKind::AliasExpression,
434                within: Some(SyntaxKind::SelectClause),
435                scope: Some(SyntaxKind::Statement),
436            })
437        );
438    }
439}