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