dprint_plugin_json/
format_text.rs

1use std::path::Path;
2
3use anyhow::bail;
4use anyhow::Result;
5use dprint_core::configuration::resolve_new_line_kind;
6use dprint_core::formatting::PrintOptions;
7use jsonc_parser::parse_to_ast;
8use jsonc_parser::CollectOptions;
9use jsonc_parser::CommentCollectionStrategy;
10use jsonc_parser::ParseResult;
11
12use super::configuration::Configuration;
13use super::generation::generate;
14
15pub fn format_text(path: &Path, text: &str, config: &Configuration) -> Result<Option<String>> {
16  let result = format_text_inner(path, text, config)?;
17  if result == text {
18    Ok(None)
19  } else {
20    Ok(Some(result))
21  }
22}
23
24fn format_text_inner(path: &Path, text: &str, config: &Configuration) -> Result<String> {
25  let text = strip_bom(text);
26  let parse_result = parse(text)?;
27  let is_jsonc = is_jsonc_file(path, config);
28  Ok(dprint_core::formatting::format(
29    || generate(parse_result, text, config, is_jsonc),
30    config_to_print_options(text, config),
31  ))
32}
33
34#[cfg(feature = "tracing")]
35pub fn trace_file(text: &str, config: &Configuration) -> dprint_core::formatting::TracingResult {
36  let parse_result = parse(text).unwrap();
37
38  dprint_core::formatting::trace_printing(
39    || generate(parse_result, text, config),
40    config_to_print_options(text, config),
41  )
42}
43
44fn strip_bom(text: &str) -> &str {
45  text.strip_prefix("\u{FEFF}").unwrap_or(text)
46}
47
48fn parse(text: &str) -> Result<ParseResult<'_>> {
49  let parse_result = parse_to_ast(
50    text,
51    &CollectOptions {
52      comments: CommentCollectionStrategy::Separate,
53      tokens: true,
54    },
55    &Default::default(),
56  );
57  match parse_result {
58    Ok(result) => Ok(result),
59    Err(err) => bail!(dprint_core::formatting::utils::string_utils::format_diagnostic(
60      Some((err.range().start, err.range().end)),
61      &err.kind().to_string(),
62      text,
63    )),
64  }
65}
66
67fn config_to_print_options(text: &str, config: &Configuration) -> PrintOptions {
68  PrintOptions {
69    indent_width: config.indent_width,
70    max_width: config.line_width,
71    use_tabs: config.use_tabs,
72    new_line_text: resolve_new_line_kind(text, config.new_line_kind),
73  }
74}
75
76fn is_jsonc_file(path: &Path, config: &Configuration) -> bool {
77  fn has_jsonc_extension(path: &Path) -> bool {
78    if let Some(ext) = path.extension() {
79      return ext.to_string_lossy().to_ascii_lowercase() == "jsonc";
80    }
81
82    false
83  }
84
85  fn is_special_json_file(path: &Path, config: &Configuration) -> bool {
86    let path = path.to_string_lossy();
87    for file_name in &config.json_trailing_comma_files {
88      if path.ends_with(file_name) {
89        return true;
90      }
91    }
92
93    false
94  }
95
96  has_jsonc_extension(path) || is_special_json_file(path, config)
97}
98
99#[cfg(test)]
100mod tests {
101  use std::path::PathBuf;
102
103  use crate::configuration::ConfigurationBuilder;
104
105  use super::super::configuration::resolve_config;
106  use super::*;
107  use dprint_core::configuration::*;
108
109  #[test]
110  fn should_error_on_syntax_diagnostic() {
111    let global_config = GlobalConfiguration::default();
112    let config = resolve_config(ConfigKeyMap::new(), &global_config).config;
113    let message = format_text(Path::new("."), "{ &*&* }", &config)
114      .err()
115      .unwrap()
116      .to_string();
117    assert_eq!(
118      message,
119      concat!("Line 1, column 3: Unexpected token\n", "\n", "  { &*&* }\n", "    ~")
120    );
121  }
122
123  #[test]
124  fn no_panic_diagnostic_at_multibyte_char() {
125    let global_config = GlobalConfiguration::default();
126    let config = resolve_config(ConfigKeyMap::new(), &global_config).config;
127    let message = format_text(Path::new("."), "{ \"a\":\u{200b}5 }", &config)
128      .err()
129      .unwrap()
130      .to_string();
131    assert_eq!(
132      message,
133      "Line 1, column 7: Unexpected token\n\n  { \"a\":\u{200b}5 }\n        ~"
134    );
135  }
136
137  #[test]
138  fn no_panic_diagnostic_multiple_values() {
139    let global_config = GlobalConfiguration::default();
140    let config = resolve_config(ConfigKeyMap::new(), &global_config).config;
141    let message = format_text(Path::new("."), "{},\n", &config).err().unwrap().to_string();
142    assert_eq!(
143      message,
144      "Line 1, column 3: Text cannot contain more than one JSON value\n\n  {},"
145    );
146  }
147
148  #[test]
149  fn test_is_jsonc_file() {
150    let config = ConfigurationBuilder::new()
151      .json_trailing_comma_files(vec!["tsconfig.json".to_string(), ".vscode/settings.json".to_string()])
152      .build();
153    assert!(!is_jsonc_file(&PathBuf::from("/asdf.json"), &config));
154    assert!(is_jsonc_file(&PathBuf::from("/asdf.jsonc"), &config));
155    assert!(is_jsonc_file(&PathBuf::from("/ASDF.JSONC"), &config));
156    assert!(is_jsonc_file(&PathBuf::from("/tsconfig.json"), &config));
157    assert!(is_jsonc_file(&PathBuf::from("/test/.vscode/settings.json"), &config));
158    assert!(!is_jsonc_file(&PathBuf::from("/test/vscode/settings.json"), &config));
159    if cfg!(windows) {
160      assert!(is_jsonc_file(&PathBuf::from("test\\.vscode\\settings.json"), &config));
161    }
162  }
163
164  #[test]
165  fn should_strip_bom() {
166    for input_text in ["\u{FEFF}{}", "\u{FEFF}{ }"] {
167      let global_config = GlobalConfiguration::default();
168      let config = resolve_config(ConfigKeyMap::new(), &global_config).config;
169      let output_text = format_text(Path::new("."), input_text, &config).unwrap().unwrap();
170      assert_eq!(output_text, "{}\n");
171    }
172  }
173}