Skip to main content

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