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