ide_diagnostics/handlers/
json_is_not_rust.rs

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