Skip to main content

dprint_plugin_json/
format_text.rs

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