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