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#[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 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#[derive(Debug, Default, PartialEq, Eq, Clone)]
158pub struct ReflowConfig {
159 configs: ConfigDictType,
160 config_types: SyntaxSet,
161 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}