Skip to main content

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_usize(value.len());
191        hasher.write(value.as_bytes())
192      }
193      ConfigKeyValue::Number(value) => {
194        hasher.write_u8(1);
195        hasher.write_i32(*value)
196      }
197      ConfigKeyValue::Bool(value) => {
198        hasher.write_u8(2);
199        hasher.write_u8(if *value { 1 } else { 0 })
200      }
201      ConfigKeyValue::Array(values) => {
202        hasher.write_u8(3);
203        hasher.write_usize(values.len());
204        for value in values {
205          value.hash(hasher);
206        }
207      }
208      ConfigKeyValue::Object(key_values) => {
209        hasher.write_u8(4);
210        hasher.write_usize(key_values.len());
211        for (key, value) in key_values {
212          hasher.write_usize(key.len());
213          hasher.write(key.as_bytes());
214          value.hash(hasher);
215        }
216      }
217      ConfigKeyValue::Null => {
218        hasher.write_u8(5);
219      }
220    }
221  }
222
223  pub fn from_i32(value: i32) -> ConfigKeyValue {
224    ConfigKeyValue::Number(value)
225  }
226
227  #[allow(clippy::should_implement_trait)]
228  pub fn from_str(value: &str) -> ConfigKeyValue {
229    ConfigKeyValue::String(value.to_string())
230  }
231
232  pub fn from_bool(value: bool) -> ConfigKeyValue {
233    ConfigKeyValue::Bool(value)
234  }
235}
236
237impl From<i32> for ConfigKeyValue {
238  fn from(item: i32) -> Self {
239    ConfigKeyValue::from_i32(item)
240  }
241}
242
243impl From<bool> for ConfigKeyValue {
244  fn from(item: bool) -> Self {
245    ConfigKeyValue::from_bool(item)
246  }
247}
248
249impl From<String> for ConfigKeyValue {
250  fn from(item: String) -> Self {
251    ConfigKeyValue::from_str(&item)
252  }
253}
254
255impl From<&str> for ConfigKeyValue {
256  fn from(item: &str) -> Self {
257    ConfigKeyValue::from_str(item)
258  }
259}
260
261#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug, Default, Hash)]
262#[serde(rename_all = "camelCase")]
263pub struct GlobalConfiguration {
264  pub line_width: Option<u32>,
265  pub use_tabs: Option<bool>,
266  pub indent_width: Option<u8>,
267  pub new_line_kind: Option<NewLineKind>,
268}
269
270pub const RECOMMENDED_GLOBAL_CONFIGURATION: RecommendedGlobalConfiguration = RecommendedGlobalConfiguration {
271  line_width: 120,
272  indent_width: 2,
273  use_tabs: false,
274  new_line_kind: NewLineKind::LineFeed,
275};
276
277pub struct RecommendedGlobalConfiguration {
278  pub line_width: u32,
279  pub use_tabs: bool,
280  pub indent_width: u8,
281  pub new_line_kind: NewLineKind,
282}
283
284impl From<RecommendedGlobalConfiguration> for GlobalConfiguration {
285  fn from(config: RecommendedGlobalConfiguration) -> Self {
286    Self {
287      line_width: Some(config.line_width),
288      use_tabs: Some(config.use_tabs),
289      indent_width: Some(config.indent_width),
290      new_line_kind: Some(config.new_line_kind),
291    }
292  }
293}
294
295#[derive(Clone, Serialize)]
296#[serde(rename_all = "camelCase")]
297pub struct ResolveConfigurationResult<T>
298where
299  T: Clone + Serialize,
300{
301  /// The configuration diagnostics.
302  pub diagnostics: Vec<ConfigurationDiagnostic>,
303
304  /// The configuration derived from the unresolved configuration
305  /// that can be used to format a file.
306  pub config: T,
307}
308
309/// Resolves a collection of key value pairs to a GlobalConfiguration.
310pub fn resolve_global_config(config: &mut ConfigKeyMap) -> ResolveConfigurationResult<GlobalConfiguration> {
311  let mut diagnostics = Vec::new();
312
313  let raw_new_line_kind = get_nullable_value::<RawNewLineKind>(config, "newLineKind", &mut diagnostics);
314
315  let resolved_config = GlobalConfiguration {
316    line_width: get_nullable_value(config, "lineWidth", &mut diagnostics),
317    use_tabs: get_nullable_value(config, "useTabs", &mut diagnostics),
318    indent_width: get_nullable_value(config, "indentWidth", &mut diagnostics),
319    new_line_kind: raw_new_line_kind.map(|kind| match kind {
320      RawNewLineKind::Auto => NewLineKind::Auto,
321      RawNewLineKind::LineFeed => NewLineKind::LineFeed,
322      RawNewLineKind::CarriageReturnLineFeed => NewLineKind::CarriageReturnLineFeed,
323      RawNewLineKind::System => {
324        if cfg!(windows) {
325          NewLineKind::CarriageReturnLineFeed
326        } else {
327          NewLineKind::LineFeed
328        }
329      }
330    }),
331  };
332
333  ResolveConfigurationResult {
334    config: resolved_config,
335    diagnostics,
336  }
337}
338
339/// If the provided key exists, takes its value from the provided config and returns it.
340/// If the provided key does not exist, it returns the default value.
341/// Adds a diagnostic if there is any problem deserializing the value.
342pub fn get_value<T>(config: &mut ConfigKeyMap, key: &str, default_value: T, diagnostics: &mut Vec<ConfigurationDiagnostic>) -> T
343where
344  T: std::str::FromStr,
345  <T as std::str::FromStr>::Err: std::fmt::Display,
346{
347  get_nullable_value(config, key, diagnostics).unwrap_or(default_value)
348}
349
350/// If the provided key exists, takes its value from the provided config and returns it.
351/// If the provided key does not exist, it returns None.
352/// Adds a diagnostic if there is any problem deserializing the value.
353pub fn get_nullable_value<T>(config: &mut ConfigKeyMap, key: &str, diagnostics: &mut Vec<ConfigurationDiagnostic>) -> Option<T>
354where
355  T: std::str::FromStr,
356  <T as std::str::FromStr>::Err: std::fmt::Display,
357{
358  if let Some(raw_value) = config.shift_remove(key) {
359    // not exactly the best, but can't think of anything better at the moment
360    let parsed_value = match raw_value {
361      ConfigKeyValue::Bool(value) => value.to_string().parse::<T>().map_err(|e| e.to_string()),
362      ConfigKeyValue::Number(value) => value.to_string().parse::<T>().map_err(|e| e.to_string()),
363      ConfigKeyValue::String(value) => value.parse::<T>().map_err(|e| e.to_string()),
364      ConfigKeyValue::Object(_) | ConfigKeyValue::Array(_) => Err("Arrays and objects are not supported for this value".to_string()),
365      ConfigKeyValue::Null => return None,
366    };
367    match parsed_value {
368      Ok(parsed_value) => Some(parsed_value),
369      Err(message) => {
370        diagnostics.push(ConfigurationDiagnostic {
371          property_name: key.to_string(),
372          message,
373        });
374        None
375      }
376    }
377  } else {
378    None
379  }
380}
381
382pub fn get_nullable_vec<T: std::str::FromStr>(
383  config: &mut ConfigKeyMap,
384  key: &str,
385  get_nullable_value: impl Fn(ConfigKeyValue, usize, &mut Vec<ConfigurationDiagnostic>) -> Option<T>,
386  diagnostics: &mut Vec<ConfigurationDiagnostic>,
387) -> Option<Vec<T>> {
388  match config.shift_remove(key) {
389    Some(value) => match value {
390      ConfigKeyValue::Array(values) => {
391        let mut result = Vec::with_capacity(values.len());
392        for (i, value) in values.into_iter().enumerate() {
393          if let Some(value) = get_nullable_value(value, i, diagnostics) {
394            result.push(value);
395          }
396        }
397        Some(result)
398      }
399      _ => {
400        diagnostics.push(ConfigurationDiagnostic {
401          property_name: key.to_string(),
402          message: "Expected an array.".to_string(),
403        });
404        None
405      }
406    },
407    None => None,
408  }
409}
410
411/// If it exists, moves over the configuration value over from the old key
412/// to the new key and adds a diagnostic.
413pub fn handle_renamed_config_property(config: &mut ConfigKeyMap, old_key: &str, new_key: &str, diagnostics: &mut Vec<ConfigurationDiagnostic>) {
414  if let Some(raw_value) = config.shift_remove(old_key) {
415    if !config.contains_key(new_key) {
416      config.insert(new_key.to_string(), raw_value);
417    }
418    diagnostics.push(ConfigurationDiagnostic {
419      property_name: old_key.to_string(),
420      message: format!("The configuration key was renamed to '{}'", new_key),
421    });
422  }
423}
424
425/// Resolves the `NewLineKind` text from the provided file text and `NewLineKind`.
426pub fn resolve_new_line_kind(file_text: &str, new_line_kind: NewLineKind) -> &'static str {
427  match new_line_kind {
428    NewLineKind::LineFeed => "\n",
429    NewLineKind::CarriageReturnLineFeed => "\r\n",
430    NewLineKind::Auto => {
431      let mut found_slash_n = false;
432      for c in file_text.as_bytes().iter().rev() {
433        if found_slash_n {
434          if c == &(b'\r') {
435            return "\r\n";
436          } else {
437            return "\n";
438          }
439        }
440
441        if c == &(b'\n') {
442          found_slash_n = true;
443        }
444      }
445
446      "\n"
447    }
448  }
449}
450
451/// Gets a diagnostic for each remaining key value pair in the hash map.
452///
453/// This should be done last, so it swallows the hashmap.
454pub fn get_unknown_property_diagnostics(config: ConfigKeyMap) -> Vec<ConfigurationDiagnostic> {
455  let mut diagnostics = Vec::new();
456  for (key, _) in config {
457    diagnostics.push(ConfigurationDiagnostic {
458      property_name: key.to_string(),
459      message: "Unknown property in configuration".to_string(),
460    });
461  }
462  diagnostics
463}
464
465#[cfg(test)]
466mod test {
467  use super::*;
468
469  #[test]
470  fn config_key_value_hash_includes_lengths() {
471    fn get_hash(value: ConfigKeyValue) -> u64 {
472      use std::hash::Hasher;
473      let mut hasher = std::collections::hash_map::DefaultHasher::new();
474      value.hash(&mut hasher);
475      hasher.finish()
476    }
477
478    fn string(value: &str) -> ConfigKeyValue {
479      ConfigKeyValue::String(value.to_string())
480    }
481
482    fn object(key_values: Vec<(&str, ConfigKeyValue)>) -> ConfigKeyValue {
483      ConfigKeyValue::Object(key_values.into_iter().map(|(key, value)| (key.to_string(), value)).collect())
484    }
485
486    let number = ConfigKeyValue::Number;
487    let array = ConfigKeyValue::Array;
488
489    // arrays
490    assert_ne!(
491      get_hash(array(vec![array(vec![number(1)]), number(2)])),
492      get_hash(array(vec![array(vec![number(1), number(2)])]))
493    );
494    // strings
495    assert_ne!(get_hash(array(vec![string("a"), string("b")])), get_hash(array(vec![string("a\0b")])));
496    // objects
497    assert_ne!(
498      get_hash(array(vec![object(vec![("a", number(1))]), number(2)])),
499      get_hash(array(vec![object(vec![("a", number(1)), ("\u{1}\u{2}\0\0", number(0))])]))
500    );
501    // object keys
502    assert_ne!(
503      get_hash(object(vec![("a", string("b"))])),
504      get_hash(object(vec![("a\0\u{1}\0\0\0\0\0\0\0b", ConfigKeyValue::Null)]))
505    );
506
507    assert_eq!(
508      get_hash(array(vec![string("a"), object(vec![("b", number(1))])])),
509      get_hash(array(vec![string("a"), object(vec![("b", number(1))])]))
510    );
511  }
512
513  #[test]
514  fn get_default_config_when_empty() {
515    let config_result = resolve_global_config(&mut ConfigKeyMap::new());
516    let config = config_result.config;
517    assert_eq!(config_result.diagnostics.len(), 0);
518    assert_eq!(config.line_width, None);
519    assert_eq!(config.indent_width, None);
520    assert!(config.new_line_kind.is_none());
521    assert_eq!(config.use_tabs, None);
522  }
523
524  #[test]
525  fn get_values_when_filled() {
526    let mut global_config = ConfigKeyMap::from([
527      (String::from("lineWidth"), ConfigKeyValue::from_i32(80)),
528      (String::from("indentWidth"), ConfigKeyValue::from_i32(8)),
529      (String::from("newLineKind"), ConfigKeyValue::from_str("crlf")),
530      (String::from("useTabs"), ConfigKeyValue::from_bool(true)),
531    ]);
532    let config_result = resolve_global_config(&mut global_config);
533    let config = config_result.config;
534    assert_eq!(config_result.diagnostics.len(), 0);
535    assert_eq!(config.line_width, Some(80));
536    assert_eq!(config.indent_width, Some(8));
537    assert_eq!(config.new_line_kind, Some(NewLineKind::CarriageReturnLineFeed));
538    assert_eq!(config.use_tabs, Some(true));
539  }
540
541  #[test]
542  fn get_diagnostic_for_invalid_enum_config() {
543    let mut global_config = ConfigKeyMap::from([(String::from("newLineKind"), ConfigKeyValue::from_str("something"))]);
544    let diagnostics = resolve_global_config(&mut global_config).diagnostics;
545    assert_eq!(diagnostics.len(), 1);
546    assert_eq!(diagnostics[0].message, "Found invalid value 'something'.");
547    assert_eq!(diagnostics[0].property_name, "newLineKind");
548  }
549
550  #[test]
551  fn get_diagnostic_for_invalid_primitive() {
552    let mut global_config = ConfigKeyMap::from([(String::from("useTabs"), ConfigKeyValue::from_str("something"))]);
553    let diagnostics = resolve_global_config(&mut global_config).diagnostics;
554    assert_eq!(diagnostics.len(), 1);
555    assert_eq!(diagnostics[0].message, "provided string was not `true` or `false`");
556    assert_eq!(diagnostics[0].property_name, "useTabs");
557  }
558
559  #[test]
560  fn get_diagnostic_for_excess_property() {
561    let global_config = ConfigKeyMap::from([(String::from("something"), ConfigKeyValue::from_str("value"))]);
562    let diagnostics = get_unknown_property_diagnostics(global_config);
563    assert_eq!(diagnostics.len(), 1);
564    assert_eq!(diagnostics[0].message, "Unknown property in configuration");
565    assert_eq!(diagnostics[0].property_name, "something");
566  }
567
568  #[test]
569  fn add_diagnostic_for_renamed_property() {
570    let mut config = ConfigKeyMap::new();
571    let mut diagnostics = Vec::new();
572    config.insert("oldProp".to_string(), ConfigKeyValue::from_str("value"));
573    handle_renamed_config_property(&mut config, "oldProp", "newProp", &mut diagnostics);
574    assert_eq!(config.len(), 1);
575    assert_eq!(config.shift_remove("newProp").unwrap(), ConfigKeyValue::from_str("value"));
576    assert_eq!(diagnostics.len(), 1);
577    assert_eq!(diagnostics[0].message, "The configuration key was renamed to 'newProp'");
578    assert_eq!(diagnostics[0].property_name, "oldProp");
579  }
580
581  #[test]
582  fn add_diagnostic_for_renamed_property_when_already_exists() {
583    let mut config = ConfigKeyMap::new();
584    let mut diagnostics = Vec::new();
585    config.insert("oldProp".to_string(), ConfigKeyValue::from_str("new_value"));
586    config.insert("newProp".to_string(), ConfigKeyValue::from_str("value"));
587    handle_renamed_config_property(&mut config, "oldProp", "newProp", &mut diagnostics);
588    assert_eq!(config.len(), 1);
589    assert_eq!(config.shift_remove("newProp").unwrap(), ConfigKeyValue::from_str("value"));
590    assert_eq!(diagnostics.len(), 1);
591    assert_eq!(diagnostics[0].message, "The configuration key was renamed to 'newProp'");
592    assert_eq!(diagnostics[0].property_name, "oldProp");
593  }
594}