]>
Commit | Line | Data |
---|---|---|
f2b60f7d FG |
1 | //! This diagnostic provides an assist for creating a struct definition from a JSON |
2 | //! example. | |
3 | ||
4 | use hir::{PathResolution, Semantics}; | |
5 | use ide_db::{ | |
6 | base_db::FileId, | |
7 | helpers::mod_path_to_ast, | |
8 | imports::insert_use::{insert_use, ImportScope}, | |
9 | source_change::SourceChangeBuilder, | |
10 | RootDatabase, | |
11 | }; | |
12 | use itertools::Itertools; | |
13 | use stdx::{format_to, never}; | |
14 | use syntax::{ | |
15 | ast::{self, make}, | |
16 | SyntaxKind, SyntaxNode, | |
17 | }; | |
18 | use text_edit::TextEdit; | |
19 | ||
20 | use crate::{fix, Diagnostic, DiagnosticsConfig, Severity}; | |
21 | ||
22 | #[derive(Default)] | |
23 | struct State { | |
24 | result: String, | |
25 | struct_counts: usize, | |
26 | has_serialize: bool, | |
27 | has_deserialize: bool, | |
28 | } | |
29 | ||
30 | impl State { | |
31 | fn generate_new_name(&mut self) -> ast::Name { | |
32 | self.struct_counts += 1; | |
33 | make::name(&format!("Struct{}", self.struct_counts)) | |
34 | } | |
35 | ||
36 | fn serde_derive(&self) -> String { | |
37 | let mut v = vec![]; | |
38 | if self.has_serialize { | |
39 | v.push("Serialize"); | |
40 | } | |
41 | if self.has_deserialize { | |
42 | v.push("Deserialize"); | |
43 | } | |
44 | match v.as_slice() { | |
45 | [] => "".to_string(), | |
46 | [x] => format!("#[derive({x})]\n"), | |
47 | [x, y] => format!("#[derive({x}, {y})]\n"), | |
48 | _ => { | |
49 | never!(); | |
50 | "".to_string() | |
51 | } | |
52 | } | |
53 | } | |
54 | ||
55 | fn build_struct(&mut self, value: &serde_json::Map<String, serde_json::Value>) -> ast::Type { | |
56 | let name = self.generate_new_name(); | |
57 | let ty = make::ty(&name.to_string()); | |
58 | let strukt = make::struct_( | |
59 | None, | |
60 | name, | |
61 | None, | |
62 | make::record_field_list(value.iter().sorted_unstable_by_key(|x| x.0).map( | |
63 | |(name, value)| make::record_field(None, make::name(name), self.type_of(value)), | |
64 | )) | |
65 | .into(), | |
66 | ); | |
67 | format_to!(self.result, "{}{}\n", self.serde_derive(), strukt); | |
68 | ty | |
69 | } | |
70 | ||
71 | fn type_of(&mut self, value: &serde_json::Value) -> ast::Type { | |
72 | match value { | |
73 | serde_json::Value::Null => make::ty_unit(), | |
74 | serde_json::Value::Bool(_) => make::ty("bool"), | |
75 | serde_json::Value::Number(it) => make::ty(if it.is_i64() { "i64" } else { "f64" }), | |
76 | serde_json::Value::String(_) => make::ty("String"), | |
77 | serde_json::Value::Array(it) => { | |
78 | let ty = match it.iter().next() { | |
79 | Some(x) => self.type_of(x), | |
80 | None => make::ty_placeholder(), | |
81 | }; | |
82 | make::ty(&format!("Vec<{ty}>")) | |
83 | } | |
84 | serde_json::Value::Object(x) => self.build_struct(x), | |
85 | } | |
86 | } | |
87 | } | |
88 | ||
89 | pub(crate) fn json_in_items( | |
90 | sema: &Semantics<'_, RootDatabase>, | |
91 | acc: &mut Vec<Diagnostic>, | |
92 | file_id: FileId, | |
93 | node: &SyntaxNode, | |
94 | config: &DiagnosticsConfig, | |
95 | ) { | |
96 | (|| { | |
97 | if node.kind() == SyntaxKind::ERROR | |
98 | && node.first_token().map(|x| x.kind()) == Some(SyntaxKind::L_CURLY) | |
99 | && node.last_token().map(|x| x.kind()) == Some(SyntaxKind::R_CURLY) | |
100 | { | |
101 | let node_string = node.to_string(); | |
102 | if let Ok(it) = serde_json::from_str(&node_string) { | |
103 | if let serde_json::Value::Object(it) = it { | |
104 | let import_scope = ImportScope::find_insert_use_container(node, sema)?; | |
105 | let range = node.text_range(); | |
106 | let mut edit = TextEdit::builder(); | |
107 | edit.delete(range); | |
108 | let mut state = State::default(); | |
109 | let semantics_scope = sema.scope(node)?; | |
110 | let scope_resolve = | |
111 | |it| semantics_scope.speculative_resolve(&make::path_from_text(it)); | |
112 | let scope_has = |it| scope_resolve(it).is_some(); | |
113 | let deserialize_resolved = scope_resolve("::serde::Deserialize"); | |
114 | let serialize_resolved = scope_resolve("::serde::Serialize"); | |
115 | state.has_deserialize = deserialize_resolved.is_some(); | |
116 | state.has_serialize = serialize_resolved.is_some(); | |
117 | state.build_struct(&it); | |
118 | edit.insert(range.start(), state.result); | |
119 | acc.push( | |
120 | Diagnostic::new( | |
121 | "json-is-not-rust", | |
122 | "JSON syntax is not valid as a Rust item", | |
123 | range, | |
124 | ) | |
125 | .severity(Severity::WeakWarning) | |
126 | .with_fixes(Some(vec![{ | |
127 | let mut scb = SourceChangeBuilder::new(file_id); | |
f25598a0 | 128 | let scope = match import_scope { |
f2b60f7d FG |
129 | ImportScope::File(it) => ImportScope::File(scb.make_mut(it)), |
130 | ImportScope::Module(it) => ImportScope::Module(scb.make_mut(it)), | |
131 | ImportScope::Block(it) => ImportScope::Block(scb.make_mut(it)), | |
132 | }; | |
133 | let current_module = semantics_scope.module(); | |
134 | if !scope_has("Serialize") { | |
135 | if let Some(PathResolution::Def(it)) = serialize_resolved { | |
136 | if let Some(it) = current_module.find_use_path_prefixed( | |
137 | sema.db, | |
138 | it, | |
139 | config.insert_use.prefix_kind, | |
2b03887a | 140 | config.prefer_no_std, |
f2b60f7d FG |
141 | ) { |
142 | insert_use( | |
143 | &scope, | |
144 | mod_path_to_ast(&it), | |
145 | &config.insert_use, | |
146 | ); | |
147 | } | |
148 | } | |
149 | } | |
150 | if !scope_has("Deserialize") { | |
151 | if let Some(PathResolution::Def(it)) = deserialize_resolved { | |
152 | if let Some(it) = current_module.find_use_path_prefixed( | |
153 | sema.db, | |
154 | it, | |
155 | config.insert_use.prefix_kind, | |
2b03887a | 156 | config.prefer_no_std, |
f2b60f7d FG |
157 | ) { |
158 | insert_use( | |
159 | &scope, | |
160 | mod_path_to_ast(&it), | |
161 | &config.insert_use, | |
162 | ); | |
163 | } | |
164 | } | |
165 | } | |
166 | let mut sc = scb.finish(); | |
167 | sc.insert_source_edit(file_id, edit.finish()); | |
168 | fix("convert_json_to_struct", "Convert JSON to struct", sc, range) | |
169 | }])), | |
170 | ); | |
171 | } | |
172 | } | |
173 | } | |
174 | Some(()) | |
175 | })(); | |
176 | } | |
177 | ||
178 | #[cfg(test)] | |
179 | mod tests { | |
180 | use crate::{ | |
181 | tests::{check_diagnostics_with_config, check_fix, check_no_fix}, | |
182 | DiagnosticsConfig, | |
183 | }; | |
184 | ||
185 | #[test] | |
186 | fn diagnostic_for_simple_case() { | |
187 | let mut config = DiagnosticsConfig::test_sample(); | |
188 | config.disabled.insert("syntax-error".to_string()); | |
189 | check_diagnostics_with_config( | |
190 | config, | |
191 | r#" | |
192 | { "foo": "bar" } | |
193 | // ^^^^^^^^^^^^^^^^ 💡 weak: JSON syntax is not valid as a Rust item | |
194 | "#, | |
195 | ); | |
196 | } | |
197 | ||
198 | #[test] | |
199 | fn types_of_primitives() { | |
200 | check_fix( | |
201 | r#" | |
202 | //- /lib.rs crate:lib deps:serde | |
203 | use serde::Serialize; | |
204 | ||
205 | fn some_garbage() { | |
206 | ||
207 | } | |
208 | ||
209 | {$0 | |
210 | "foo": "bar", | |
211 | "bar": 2.3, | |
212 | "baz": null, | |
213 | "bay": 57, | |
214 | "box": true | |
215 | } | |
216 | //- /serde.rs crate:serde | |
217 | ||
218 | pub trait Serialize { | |
219 | fn serialize() -> u8; | |
220 | } | |
221 | "#, | |
222 | r#" | |
223 | use serde::Serialize; | |
224 | ||
225 | fn some_garbage() { | |
226 | ||
227 | } | |
228 | ||
229 | #[derive(Serialize)] | |
230 | struct Struct1{ bar: f64, bay: i64, baz: (), r#box: bool, foo: String } | |
231 | ||
232 | "#, | |
233 | ); | |
234 | } | |
235 | ||
236 | #[test] | |
237 | fn nested_structs() { | |
238 | check_fix( | |
239 | r#" | |
240 | {$0 | |
241 | "foo": "bar", | |
242 | "bar": { | |
243 | "kind": "Object", | |
244 | "value": {} | |
245 | } | |
246 | } | |
247 | "#, | |
248 | r#" | |
249 | struct Struct3{ } | |
250 | struct Struct2{ kind: String, value: Struct3 } | |
251 | struct Struct1{ bar: Struct2, foo: String } | |
252 | ||
253 | "#, | |
254 | ); | |
255 | } | |
256 | ||
257 | #[test] | |
258 | fn arrays() { | |
259 | check_fix( | |
260 | r#" | |
261 | //- /lib.rs crate:lib deps:serde | |
262 | { | |
263 | "of_string": ["foo", "2", "x"], $0 | |
264 | "of_object": [{ | |
265 | "x": 10, | |
266 | "y": 20 | |
267 | }, { | |
268 | "x": 10, | |
269 | "y": 20 | |
270 | }], | |
271 | "nested": [[[2]]], | |
272 | "empty": [] | |
273 | } | |
274 | //- /serde.rs crate:serde | |
275 | ||
276 | pub trait Serialize { | |
277 | fn serialize() -> u8; | |
278 | } | |
279 | pub trait Deserialize { | |
280 | fn deserialize() -> u8; | |
281 | } | |
282 | "#, | |
283 | r#" | |
284 | use serde::Serialize; | |
285 | use serde::Deserialize; | |
286 | ||
287 | #[derive(Serialize, Deserialize)] | |
288 | struct Struct2{ x: i64, y: i64 } | |
289 | #[derive(Serialize, Deserialize)] | |
290 | struct Struct1{ empty: Vec<_>, nested: Vec<Vec<Vec<i64>>>, of_object: Vec<Struct2>, of_string: Vec<String> } | |
291 | ||
292 | "#, | |
293 | ); | |
294 | } | |
295 | ||
296 | #[test] | |
297 | fn no_emit_outside_of_item_position() { | |
298 | check_no_fix( | |
299 | r#" | |
300 | fn foo() { | |
301 | let json = {$0 | |
302 | "foo": "bar", | |
303 | "bar": { | |
304 | "kind": "Object", | |
305 | "value": {} | |
306 | } | |
307 | }; | |
308 | } | |
309 | "#, | |
310 | ); | |
311 | } | |
312 | } |