sqruff_lib/utils/reflow/
config.rs

1use std::str::FromStr;
2
3use ahash::AHashMap;
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::reindent::{IndentUnit, TrailingComments};
9
10type ConfigElementType = AHashMap<String, String>;
11type ConfigDictType = AHashMap<SyntaxKind, ConfigElementType>;
12
13/// Holds spacing config for a block and allows easy manipulation
14#[derive(Debug, PartialEq, Eq, Clone)]
15pub struct BlockConfig {
16    pub spacing_before: Spacing,
17    pub spacing_after: Spacing,
18    pub spacing_within: Option<Spacing>,
19    pub line_position: Option<&'static str>,
20}
21
22impl Default for BlockConfig {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl BlockConfig {
29    pub fn new() -> Self {
30        BlockConfig {
31            spacing_before: Spacing::Single,
32            spacing_after: Spacing::Single,
33            spacing_within: None,
34            line_position: None,
35        }
36    }
37
38    fn convert_line_position(line_position: &str) -> &'static str {
39        match line_position {
40            "alone" => "alone",
41            "leading" => "leading",
42            "trailing" => "trailing",
43            "alone:strict" => "alone:strict",
44            _ => unreachable!("Expected 'alone', 'leading' found '{}'", line_position),
45        }
46    }
47
48    /// Mutate the config based on additional information
49    pub fn incorporate(
50        &mut self,
51        before: Option<Spacing>,
52        after: Option<Spacing>,
53        within: Option<Spacing>,
54        line_position: Option<&'static str>,
55        config: Option<&ConfigElementType>,
56    ) {
57        let empty = AHashMap::new();
58        let config = config.unwrap_or(&empty);
59
60        self.spacing_before = before
61            .or_else(|| config.get("spacing_before").map(|it| it.parse().unwrap()))
62            .unwrap_or(self.spacing_before);
63
64        self.spacing_after = after
65            .or_else(|| config.get("spacing_after").map(|it| it.parse().unwrap()))
66            .unwrap_or(self.spacing_after);
67
68        self.spacing_within =
69            within.or_else(|| config.get("spacing_within").map(|it| it.parse().unwrap()));
70
71        self.line_position = line_position.or_else(|| {
72            let line_position = config.get("line_position");
73            match line_position {
74                Some(value) => Some(Self::convert_line_position(value)),
75                None => None,
76            }
77        });
78    }
79}
80
81/// An interface onto the configuration of how segments should reflow.
82///
83/// This acts as the primary translation engine between configuration
84/// held either in dicts for testing, or in the FluffConfig in live
85/// usage, and the configuration used during reflow operations.
86#[derive(Debug, Default, PartialEq, Eq, Clone)]
87pub struct ReflowConfig {
88    configs: ConfigDictType,
89    config_types: SyntaxSet,
90    /// In production, these values are almost _always_ set because we
91    /// use `.from_fluff_config`, but the defaults are here to aid in
92    /// testing.
93    pub(crate) indent_unit: IndentUnit,
94    pub(crate) max_line_length: usize,
95    pub(crate) hanging_indents: bool,
96    pub(crate) allow_implicit_indents: bool,
97    pub(crate) trailing_comments: TrailingComments,
98}
99
100#[derive(Debug, PartialEq, Eq, Clone, Copy)]
101pub enum Spacing {
102    Single,
103    Touch,
104    TouchInline,
105    SingleInline,
106    Any,
107    Align {
108        seg_type: SyntaxKind,
109        within: Option<SyntaxKind>,
110        scope: Option<SyntaxKind>,
111    },
112}
113
114impl FromStr for Spacing {
115    type Err = ();
116
117    fn from_str(s: &str) -> Result<Self, Self::Err> {
118        Ok(match s {
119            "single" => Self::Single,
120            "touch" => Self::Touch,
121            "touch:inline" => Self::TouchInline,
122            "single:inline" => Self::SingleInline,
123            "any" => Self::Any,
124            s => {
125                if let Some(rest) = s.strip_prefix("align") {
126                    let mut args = rest.split(':');
127                    args.next();
128
129                    let seg_type = args.next().map(|it| it.parse().unwrap()).unwrap();
130                    let within = args.next().map(|it| it.parse().unwrap());
131                    let scope = args.next().map(|it| it.parse().unwrap());
132
133                    Spacing::Align {
134                        seg_type,
135                        within,
136                        scope,
137                    }
138                } else {
139                    unimplemented!("{s}")
140                }
141            }
142        })
143    }
144}
145
146impl ReflowConfig {
147    pub fn get_block_config(
148        &self,
149        block_class_types: &SyntaxSet,
150        depth_info: Option<&DepthInfo>,
151    ) -> BlockConfig {
152        let configured_types = block_class_types.clone().intersection(&self.config_types);
153
154        let mut block_config = BlockConfig::new();
155
156        if let Some(depth_info) = depth_info {
157            let (mut parent_start, mut parent_end) = (true, true);
158
159            for (idx, key) in depth_info.stack_hashes.iter().rev().enumerate() {
160                let stack_position = &depth_info.stack_positions[key];
161
162                if !matches!(
163                    stack_position.type_,
164                    Some(StackPositionType::Solo) | Some(StackPositionType::Start)
165                ) {
166                    parent_start = false;
167                }
168
169                if !matches!(
170                    stack_position.type_,
171                    Some(StackPositionType::Solo) | Some(StackPositionType::End)
172                ) {
173                    parent_end = false;
174                }
175
176                if !parent_start && !parent_end {
177                    break;
178                }
179
180                let parent_classes =
181                    &depth_info.stack_class_types[depth_info.stack_class_types.len() - 1 - idx];
182
183                let configured_parent_types =
184                    self.config_types.clone().intersection(parent_classes);
185
186                if parent_start {
187                    for seg_type in configured_parent_types.clone() {
188                        let before = self
189                            .configs
190                            .get(&seg_type)
191                            .and_then(|conf| conf.get("spacing_before"))
192                            .map(|it| it.as_str());
193                        let before = before.map(|it| it.parse().unwrap());
194
195                        block_config.incorporate(before, None, None, None, None);
196                    }
197                }
198
199                if parent_end {
200                    for seg_type in configured_parent_types {
201                        let after = self
202                            .configs
203                            .get(&seg_type)
204                            .and_then(|conf| conf.get("spacing_after"))
205                            .map(|it| it.as_str());
206
207                        let after = after.map(|it| it.parse().unwrap());
208                        block_config.incorporate(None, after, None, None, None);
209                    }
210                }
211            }
212        }
213
214        for seg_type in configured_types {
215            block_config.incorporate(None, None, None, None, self.configs.get(&seg_type));
216        }
217
218        block_config
219    }
220
221    pub fn from_fluff_config(config: &FluffConfig) -> ReflowConfig {
222        let configs = config.raw["layout"]["type"].as_map().unwrap().clone();
223        let config_types = configs
224            .keys()
225            .map(|x| x.parse().unwrap_or_else(|_| unimplemented!("{x}")))
226            .collect::<SyntaxSet>();
227
228        let trailing_comments = config.raw["indentation"]["trailing_comments"]
229            .as_string()
230            .unwrap();
231        let trailing_comments = TrailingComments::from_str(trailing_comments).unwrap();
232
233        let tab_space_size = config.raw["indentation"]["tab_space_size"]
234            .as_int()
235            .unwrap() as usize;
236        let indent_unit = config.raw["indentation"]["indent_unit"]
237            .as_string()
238            .unwrap();
239        let indent_unit = IndentUnit::from_type_and_size(indent_unit, tab_space_size);
240
241        let mut configs = convert_to_config_dict(configs);
242        let keys: Vec<_> = configs.keys().copied().collect();
243
244        for seg_type in keys {
245            for key in ["spacing_before", "spacing_after"] {
246                if configs[&seg_type].get(key).map(String::as_str) == Some("align") {
247                    let mut new_key = format!("align:{}", seg_type.as_str());
248                    if let Some(align_within) = configs[&seg_type].get("align_within") {
249                        new_key.push_str(&format!(":{align_within}"));
250
251                        if let Some(align_scope) = configs[&seg_type].get("align_scope") {
252                            new_key.push_str(&format!(":{align_scope}"));
253                        }
254                    }
255
256                    *configs.get_mut(&seg_type).unwrap().get_mut(key).unwrap() = new_key;
257                }
258            }
259        }
260
261        ReflowConfig {
262            configs,
263            config_types,
264            indent_unit,
265            max_line_length: config.raw["core"]["max_line_length"].as_int().unwrap() as usize,
266            hanging_indents: config.raw["indentation"]["hanging_indents"]
267                .as_bool()
268                .unwrap_or_default(),
269            allow_implicit_indents: config.raw["indentation"]["allow_implicit_indents"]
270                .as_bool()
271                .unwrap(),
272            trailing_comments,
273        }
274    }
275}
276
277fn convert_to_config_dict(input: AHashMap<String, Value>) -> ConfigDictType {
278    let mut config_dict = ConfigDictType::new();
279
280    for (key, value) in input {
281        match value {
282            Value::Map(map_value) => {
283                let element = map_value
284                    .into_iter()
285                    .map(|(inner_key, inner_value)| {
286                        if let Value::String(value_str) = inner_value {
287                            (inner_key, value_str.into())
288                        } else {
289                            panic!("Expected a Value::String, found another variant.");
290                        }
291                    })
292                    .collect::<ConfigElementType>();
293                config_dict.insert(
294                    key.parse().unwrap_or_else(|_| unimplemented!("{key}")),
295                    element,
296                );
297            }
298            _ => panic!("Expected a Value::Map, found another variant."),
299        }
300    }
301
302    config_dict
303}