dprint_plugin_json/
format_text.rs1use 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}