dprint_core/
configuration.rs

1use std::hash::Hash;
2
3use indexmap::IndexMap;
4use serde::Deserialize;
5use serde::Serialize;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct ParseConfigurationError(pub String);
9
10impl std::fmt::Display for ParseConfigurationError {
11  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12    format!("Found invalid value '{}'.", self.0).fmt(f)
13  }
14}
15
16#[macro_export]
17macro_rules! generate_str_to_from {
18    ($enum_name:ident, $([$member_name:ident, $string_value:expr]),* ) => {
19        impl std::str::FromStr for $enum_name {
20            type Err = ParseConfigurationError;
21
22            fn from_str(s: &str) -> Result<Self, Self::Err> {
23                match s {
24                    $($string_value => Ok($enum_name::$member_name)),*,
25                    _ => Err(ParseConfigurationError(String::from(s))),
26                }
27            }
28        }
29
30        impl std::fmt::Display for $enum_name {
31            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32                match self {
33                    $($enum_name::$member_name => write!(f, $string_value)),*,
34                }
35            }
36        }
37    };
38}
39
40#[derive(Clone, PartialEq, Eq, Debug, Copy, Serialize, Deserialize, Hash)]
41pub enum RawNewLineKind {
42  /// Decide which newline kind to use based on the last newline in the file.
43  #[serde(rename = "auto")]
44  Auto,
45  /// Use slash n new lines.
46  #[serde(rename = "lf")]
47  LineFeed,
48  /// Use slash r slash n new lines.
49  #[serde(rename = "crlf")]
50  CarriageReturnLineFeed,
51  /// Use the system standard (ex. crlf on Windows)
52  #[serde(rename = "system")]
53  System,
54}
55
56generate_str_to_from![
57  RawNewLineKind,
58  [Auto, "auto"],
59  [LineFeed, "lf"],
60  [CarriageReturnLineFeed, "crlf"],
61  [System, "system"]
62];
63
64#[derive(Clone, PartialEq, Eq, Debug, Copy, Serialize, Deserialize, Hash)]
65pub enum NewLineKind {
66  /// Decide which newline kind to use based on the last newline in the file.
67  #[serde(rename = "auto")]
68  Auto,
69  /// Use slash n new lines.
70  #[serde(rename = "lf")]
71  LineFeed,
72  /// Use slash r slash n new lines.
73  #[serde(rename = "crlf")]
74  CarriageReturnLineFeed,
75}
76
77generate_str_to_from![NewLineKind, [Auto, "auto"], [LineFeed, "lf"], [CarriageReturnLineFeed, "crlf"]];
78
79/// Represents a problem within the configuration.
80#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
81#[serde(rename_all = "camelCase")]
82pub struct ConfigurationDiagnostic {
83  /// The property name the problem occurred on.
84  pub property_name: String,
85  /// The diagnostic message that should be displayed to the user
86  pub message: String,
87}
88
89impl std::fmt::Display for ConfigurationDiagnostic {
90  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91    write!(f, "{} ({})", self.message, self.property_name)
92  }
93}
94
95pub type ConfigKeyMap = IndexMap<String, ConfigKeyValue>;
96
97#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
98#[serde(untagged)]
99pub enum ConfigKeyValue {
100  String(String),
101  Number(i32),
102  Bool(bool),
103  Array(Vec<ConfigKeyValue>),
104  Object(ConfigKeyMap),
105  Null,
106}
107
108impl ConfigKeyValue {
109  pub fn as_string(&self) -> Option<&String> {
110    match self {
111      ConfigKeyValue::String(value) => Some(value),
112      _ => None,
113    }
114  }
115
116  pub fn as_number(&self) -> Option<i32> {
117    match self {
118      ConfigKeyValue::Number(value) => Some(*value),
119      _ => None,
120    }
121  }
122
123  pub fn as_bool(&self) -> Option<bool> {
124    match self {
125      ConfigKeyValue::Bool(value) => Some(*value),
126      _ => None,
127    }
128  }
129
130  pub fn as_array(&self) -> Option<&Vec<ConfigKeyValue>> {
131    match self {
132      ConfigKeyValue::Array(values) => Some(values),
133      _ => None,
134    }
135  }
136
137  pub fn as_object(&self) -> Option<&ConfigKeyMap> {
138    match self {
139      ConfigKeyValue::Object(values) => Some(values),
140      _ => None,
141    }
142  }
143
144  pub fn into_string(self) -> Option<String> {
145    match self {
146      ConfigKeyValue::String(value) => Some(value),
147      _ => None,
148    }
149  }
150
151  pub fn into_number(self) -> Option<i32> {
152    match self {
153      ConfigKeyValue::Number(value) => Some(value),
154      _ => None,
155    }
156  }
157
158  pub fn into_bool(self) -> Option<bool> {
159    match self {
160      ConfigKeyValue::Bool(value) => Some(value),
161      _ => None,
162    }
163  }
164
165  pub fn into_array(self) -> Option<Vec<ConfigKeyValue>> {
166    match self {
167      ConfigKeyValue::Array(values) => Some(values),
168      _ => None,
169    }
170  }
171
172  pub fn into_object(self) -> Option<ConfigKeyMap> {
173    match self {
174      ConfigKeyValue::Object(values) => Some(values),
175      _ => None,
176    }
177  }
178
179  pub fn is_null(&self) -> bool {
180    matches!(self, ConfigKeyValue::Null)
181  }
182
183  /// Gets a hash of the configuration value. This is used for incremental formatting
184  /// and the Hash trait is not implemented to discourage using this in other places.
185  #[allow(clippy::should_implement_trait)]
186  pub fn hash(&self, hasher: &mut impl std::hash::Hasher) {
187    match self {
188      ConfigKeyValue::String(value) => {
189        hasher.write_u8(0);
190        hasher.write(value.as_bytes())
191      }
192      ConfigKeyValue::Number(value) => {
193        hasher.write_u8(1);
194        hasher.write_i32(*value)
195      }
196      ConfigKeyValue::Bool(value) => {
197        hasher.write_u8(2);
198        hasher.write_u8(if *value { 1 } else { 0 })
199      }
200      ConfigKeyValue::Array(values) => {
201        hasher.write_u8(3);
202        for value in values {
203          value.hash(hasher);
204        }
205      }
206      ConfigKeyValue::Object(key_values) => {
207        hasher.write_u8(4);
208        for (key, value) in key_values {
209          hasher.write(key.as_bytes());
210          value.hash(hasher);
211        }
212      }
213      ConfigKeyValue::Null => {
214        hasher.write_u8(5);
215      }
216    }
217  }
218
219  pub fn from_i32(value: i32) -> ConfigKeyValue {
220    ConfigKeyValue::Number(value)
221  }
222
223  #[allow(clippy::should_implement_trait)]
224  pub fn from_str(value: &str) -> ConfigKeyValue {
225    ConfigKeyValue::String(value.to_string())
226  }
227
228  pub fn from_bool(value: bool) -> ConfigKeyValue {
229    ConfigKeyValue::Bool(value)
230  }
231}
232
233impl From<i32> for ConfigKeyValue {
234  fn from(item: i32) -> Self {
235    ConfigKeyValue::from_i32(item)
236  }
237}
238
239impl From<bool> for ConfigKeyValue {
240  fn from(item: bool) -> Self {
241    ConfigKeyValue::from_bool(item)
242  }
243}
244
245impl From<String> for ConfigKeyValue {
246  fn from(item: String) -> Self {
247    ConfigKeyValue::from_str(&item)
248  }
249}
250
251impl From<&str> for ConfigKeyValue {
252  fn from(item: &str) -> Self {
253    ConfigKeyValue::from_str(item)
254  }
255}
256
257#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug, Default, Hash)]
258#[serde(rename_all = "camelCase")]
259pub struct GlobalConfiguration {
260  pub line_width: Option<u32>,
261  pub use_tabs: Option<bool>,
262  pub indent_width: Option<u8>,
263  pub new_line_kind: Option<NewLineKind>,
264}
265
266pub const RECOMMENDED_GLOBAL_CONFIGURATION: RecommendedGlobalConfiguration = RecommendedGlobalConfiguration {
267  line_width: 120,
268  indent_width: 2,
269  use_tabs: false,
270  new_line_kind: NewLineKind::LineFeed,
271};
272
273pub struct RecommendedGlobalConfiguration {
274  pub line_width: u32,
275  pub use_tabs: bool,
276  pub indent_width: u8,
277  pub new_line_kind: NewLineKind,
278}
279
280impl From<RecommendedGlobalConfiguration> for GlobalConfiguration {
281  fn from(config: RecommendedGlobalConfiguration) -> Self {
282    Self {
283      line_width: Some(config.line_width),
284      use_tabs: Some(config.use_tabs),
285      indent_width: Some(config.indent_width),
286      new_line_kind: Some(config.new_line_kind),
287    }
288  }
289}
290
291#[derive(Clone, Serialize)]
292#[serde(rename_all = "camelCase")]
293pub struct ResolveConfigurationResult<T>
294where
295  T: Clone + Serialize,
296{
297  /// The configuration diagnostics.
298  pub diagnostics: Vec<ConfigurationDiagnostic>,
299
300  /// The configuration derived from the unresolved configuration
301  /// that can be used to format a file.
302  pub config: T,
303}
304
305/// Resolves a collection of key value pairs to a GlobalConfiguration.
306pub fn resolve_global_config(config: &mut ConfigKeyMap) -> ResolveConfigurationResult<GlobalConfiguration> {
307  let mut diagnostics = Vec::new();
308
309  let raw_new_line_kind = get_nullable_value::<RawNewLineKind>(config, "newLineKind", &mut diagnostics);
310
311  let resolved_config = GlobalConfiguration {
312    line_width: get_nullable_value(config, "lineWidth", &mut diagnostics),
313    use_tabs: get_nullable_value(config, "useTabs", &mut diagnostics),
314    indent_width: get_nullable_value(config, "indentWidth", &mut diagnostics),
315    new_line_kind: raw_new_line_kind.map(|kind| match kind {
316      RawNewLineKind::Auto => NewLineKind::Auto,
317      RawNewLineKind::LineFeed => NewLineKind::LineFeed,
318      RawNewLineKind::CarriageReturnLineFeed => NewLineKind::CarriageReturnLineFeed,
319      RawNewLineKind::System => {
320        if cfg!(windows) {
321          NewLineKind::CarriageReturnLineFeed
322        } else {
323          NewLineKind::LineFeed
324        }
325      }
326    }),
327  };
328
329  ResolveConfigurationResult {
330    config: resolved_config,
331    diagnostics,
332  }
333}
334
335/// If the provided key exists, takes its value from the provided config and returns it.
336/// If the provided key does not exist, it returns the default value.
337/// Adds a diagnostic if there is any problem deserializing the value.
338pub fn get_value<T>(config: &mut ConfigKeyMap, key: &str, default_value: T, diagnostics: &mut Vec<ConfigurationDiagnostic>) -> T
339where
340  T: std::str::FromStr,
341  <T as std::str::FromStr>::Err: std::fmt::Display,
342{
343  get_nullable_value(config, key, diagnostics).unwrap_or(default_value)
344}
345
346/// If the provided key exists, takes its value from the provided config and returns it.
347/// If the provided key does not exist, it returns None.
348/// Adds a diagnostic if there is any problem deserializing the value.
349pub fn get_nullable_value<T>(config: &mut ConfigKeyMap, key: &str, diagnostics: &mut Vec<ConfigurationDiagnostic>) -> Option<T>
350where
351  T: std::str::FromStr,
352  <T as std::str::FromStr>::Err: std::fmt::Display,
353{
354  if let Some(raw_value) = config.shift_remove(key) {
355    // not exactly the best, but can't think of anything better at the moment
356    let parsed_value = match raw_value {
357      ConfigKeyValue::Bool(value) => value.to_string().parse::<T>().map_err(|e| e.to_string()),
358      ConfigKeyValue::Number(value) => value.to_string().parse::<T>().map_err(|e| e.to_string()),
359      ConfigKeyValue::String(value) => value.parse::<T>().map_err(|e| e.to_string()),
360      ConfigKeyValue::Object(_) | ConfigKeyValue::Array(_) => Err("Arrays and objects are not supported for this value".to_string()),
361      ConfigKeyValue::Null => return None,
362    };
363    match parsed_value {
364      Ok(parsed_value) => Some(parsed_value),
365      Err(message) => {
366        diagnostics.push(ConfigurationDiagnostic {
367          property_name: key.to_string(),
368          message,
369        });
370        None
371      }
372    }
373  } else {
374    None
375  }
376}
377
378pub fn get_nullable_vec<T: std::str::FromStr>(
379  config: &mut ConfigKeyMap,
380  key: &str,
381  get_nullable_value: impl Fn(ConfigKeyValue, usize, &mut Vec<ConfigurationDiagnostic>) -> Option<T>,
382  diagnostics: &mut Vec<ConfigurationDiagnostic>,
383) -> Option<Vec<T>> {
384  match config.shift_remove(key) {
385    Some(value) => match value {
386      ConfigKeyValue::Array(values) => {
387        let mut result = Vec::with_capacity(values.len());
388        for (i, value) in values.into_iter().enumerate() {
389          if let Some(value) = get_nullable_value(value, i, diagnostics) {
390            result.push(value);
391          }
392        }
393        Some(result)
394      }
395      _ => {
396        diagnostics.push(ConfigurationDiagnostic {
397          property_name: key.to_string(),
398          message: "Expected an array.".to_string(),
399        });
400        None
401      }
402    },
403    None => None,
404  }
405}
406
407/// If it exists, moves over the configuration value over from the old key
408/// to the new key and adds a diagnostic.
409pub fn handle_renamed_config_property(config: &mut ConfigKeyMap, old_key: &str, new_key: &str, diagnostics: &mut Vec<ConfigurationDiagnostic>) {
410  if let Some(raw_value) = config.shift_remove(old_key) {
411    if !config.contains_key(new_key) {
412      config.insert(new_key.to_string(), raw_value);
413    }
414    diagnostics.push(ConfigurationDiagnostic {
415      property_name: old_key.to_string(),
416      message: format!("The configuration key was renamed to '{}'", new_key),
417    });
418  }
419}
420
421/// Resolves the `NewLineKind` text from the provided file text and `NewLineKind`.
422pub fn resolve_new_line_kind(file_text: &str, new_line_kind: NewLineKind) -> &'static str {
423  match new_line_kind {
424    NewLineKind::LineFeed => "\n",
425    NewLineKind::CarriageReturnLineFeed => "\r\n",
426    NewLineKind::Auto => {
427      let mut found_slash_n = false;
428      for c in file_text.as_bytes().iter().rev() {
429        if found_slash_n {
430          if c == &(b'\r') {
431            return "\r\n";
432          } else {
433            return "\n";
434          }
435        }
436
437        if c == &(b'\n') {
438          found_slash_n = true;
439        }
440      }
441
442      "\n"
443    }
444  }
445}
446
447/// Gets a diagnostic for each remaining key value pair in the hash map.
448///
449/// This should be done last, so it swallows the hashmap.
450pub fn get_unknown_property_diagnostics(config: ConfigKeyMap) -> Vec<ConfigurationDiagnostic> {
451  let mut diagnostics = Vec::new();
452  for (key, _) in config {
453    diagnostics.push(ConfigurationDiagnostic {
454      property_name: key.to_string(),
455      message: "Unknown property in configuration".to_string(),
456    });
457  }
458  diagnostics
459}
460
461#[cfg(test)]
462mod test {
463  use super::*;
464
465  #[test]
466  fn get_default_config_when_empty() {
467    let config_result = resolve_global_config(&mut ConfigKeyMap::new());
468    let config = config_result.config;
469    assert_eq!(config_result.diagnostics.len(), 0);
470    assert_eq!(config.line_width, None);
471    assert_eq!(config.indent_width, None);
472    assert!(config.new_line_kind.is_none());
473    assert_eq!(config.use_tabs, None);
474  }
475
476  #[test]
477  fn get_values_when_filled() {
478    let mut global_config = ConfigKeyMap::from([
479      (String::from("lineWidth"), ConfigKeyValue::from_i32(80)),
480      (String::from("indentWidth"), ConfigKeyValue::from_i32(8)),
481      (String::from("newLineKind"), ConfigKeyValue::from_str("crlf")),
482      (String::from("useTabs"), ConfigKeyValue::from_bool(true)),
483    ]);
484    let config_result = resolve_global_config(&mut global_config);
485    let config = config_result.config;
486    assert_eq!(config_result.diagnostics.len(), 0);
487    assert_eq!(config.line_width, Some(80));
488    assert_eq!(config.indent_width, Some(8));
489    assert_eq!(config.new_line_kind, Some(NewLineKind::CarriageReturnLineFeed));
490    assert_eq!(config.use_tabs, Some(true));
491  }
492
493  #[test]
494  fn get_diagnostic_for_invalid_enum_config() {
495    let mut global_config = ConfigKeyMap::from([(String::from("newLineKind"), ConfigKeyValue::from_str("something"))]);
496    let diagnostics = resolve_global_config(&mut global_config).diagnostics;
497    assert_eq!(diagnostics.len(), 1);
498    assert_eq!(diagnostics[0].message, "Found invalid value 'something'.");
499    assert_eq!(diagnostics[0].property_name, "newLineKind");
500  }
501
502  #[test]
503  fn get_diagnostic_for_invalid_primitive() {
504    let mut global_config = ConfigKeyMap::from([(String::from("useTabs"), ConfigKeyValue::from_str("something"))]);
505    let diagnostics = resolve_global_config(&mut global_config).diagnostics;
506    assert_eq!(diagnostics.len(), 1);
507    assert_eq!(diagnostics[0].message, "provided string was not `true` or `false`");
508    assert_eq!(diagnostics[0].property_name, "useTabs");
509  }
510
511  #[test]
512  fn get_diagnostic_for_excess_property() {
513    let global_config = ConfigKeyMap::from([(String::from("something"), ConfigKeyValue::from_str("value"))]);
514    let diagnostics = get_unknown_property_diagnostics(global_config);
515    assert_eq!(diagnostics.len(), 1);
516    assert_eq!(diagnostics[0].message, "Unknown property in configuration");
517    assert_eq!(diagnostics[0].property_name, "something");
518  }
519
520  #[test]
521  fn add_diagnostic_for_renamed_property() {
522    let mut config = ConfigKeyMap::new();
523    let mut diagnostics = Vec::new();
524    config.insert("oldProp".to_string(), ConfigKeyValue::from_str("value"));
525    handle_renamed_config_property(&mut config, "oldProp", "newProp", &mut diagnostics);
526    assert_eq!(config.len(), 1);
527    assert_eq!(config.shift_remove("newProp").unwrap(), ConfigKeyValue::from_str("value"));
528    assert_eq!(diagnostics.len(), 1);
529    assert_eq!(diagnostics[0].message, "The configuration key was renamed to 'newProp'");
530    assert_eq!(diagnostics[0].property_name, "oldProp");
531  }
532
533  #[test]
534  fn add_diagnostic_for_renamed_property_when_already_exists() {
535    let mut config = ConfigKeyMap::new();
536    let mut diagnostics = Vec::new();
537    config.insert("oldProp".to_string(), ConfigKeyValue::from_str("new_value"));
538    config.insert("newProp".to_string(), ConfigKeyValue::from_str("value"));
539    handle_renamed_config_property(&mut config, "oldProp", "newProp", &mut diagnostics);
540    assert_eq!(config.len(), 1);
541    assert_eq!(config.shift_remove("newProp").unwrap(), ConfigKeyValue::from_str("value"));
542    assert_eq!(diagnostics.len(), 1);
543    assert_eq!(diagnostics[0].message, "The configuration key was renamed to 'newProp'");
544    assert_eq!(diagnostics[0].property_name, "oldProp");
545  }
546}