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#[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 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#[derive(Debug, Default, PartialEq, Eq, Clone)]
87pub struct ReflowConfig {
88 configs: ConfigDictType,
89 config_types: SyntaxSet,
90 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}