char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somfjson: remove members without leaving spurious whitespace75a6060

main
13.3 KiB363 linesraw
1use serde_fjson::{Document, PathSegment, parse};
2use serde_json::json;
3
4#[test]
5fn replacing_a_leaf_preserves_every_other_byte_and_quote_style() {
6    let source =
7        "// head\r\nimports: { 'de\\u0070': /* choice */ 'old' /* reason */ },\r\nother: 1e3\r\n";
8    let mut document = Document::parse(source).unwrap();
9    document
10        .set(&["imports".into(), "dep".into()], &json!("it's new\nšŸ˜€"))
11        .unwrap();
12    assert_eq!(
13        document.source(),
14        source.replace("'old'", "'it\\'s new\\nšŸ˜€'")
15    );
16    assert_eq!(
17        document.to_value().unwrap()["imports"]["dep"],
18        "it's new\nšŸ˜€"
19    );
20    assert_eq!(
21        &document.source()[document.root().span.clone()],
22        document.source()[source.find("imports").unwrap()..].trim_end_matches(['\r', '\n'])
23    );
24}
25
26#[test]
27fn unchanged_values_do_not_reformat_or_remove_internal_comments() {
28    let source = "items: [1, /* why */ 2,]\nname: 'same'\n";
29    let mut document = Document::parse(source).unwrap();
30    document.set(&["items".into()], &json!([1, 2])).unwrap();
31    document.set(&["name".into()], &json!("same")).unwrap();
32    assert_eq!(document.source(), source);
33}
34
35#[test]
36fn insertion_preserves_comments_line_endings_and_existing_layout() {
37    for newline in ["\n", "\r\n", "\r"] {
38        let source = format!(
39            "// head{newline}options: {{{newline}\told: 'kept' // reason{newline}}}{newline}"
40        );
41        let mut document = Document::parse(&source).unwrap();
42        document
43            .set(
44                &["options".into(), "new-key".into()],
45                &json!({"enabled": true}),
46            )
47            .unwrap();
48        assert!(
49            document
50                .source()
51                .contains(&format!("\told: 'kept' // reason{newline}"))
52        );
53        assert!(document.source().contains(&format!(
54            "\t\"new-key\": {{{newline}\t  enabled: true{newline}\t}}"
55        )));
56        assert_eq!(
57            document.to_value().unwrap(),
58            json!({"options": {"old": "kept", "new-key": {"enabled": true}}})
59        );
60        if newline != "\n" {
61            assert!(!document.source().replace(newline, "").contains('\n'));
62        }
63    }
64    for source in [
65        "",
66        "// eof",
67        "{/* empty */}",
68        "/* heading */\n",
69        "{\n  // empty\n}",
70    ] {
71        let mut document = Document::parse(source).unwrap();
72        document
73            .set(&["ę—„ęœ¬čŖž".into()], &json!([true, false]))
74            .unwrap();
75        assert_eq!(
76            document.to_value().unwrap(),
77            json!({"ę—„ęœ¬čŖž": [true, false]})
78        );
79        for comment in ["// eof", "/* empty */", "/* heading */", "// empty"] {
80            if source.contains(comment) {
81                assert!(document.source().contains(comment));
82            }
83        }
84    }
85}
86
87#[test]
88fn array_appends_and_sequential_edits_use_updated_paths() {
89    let mut document =
90        Document::parse("list: [/* first */ 1, // note\n 2,]\nother: 'keep'").unwrap();
91    document
92        .set(&["list".into(), 2.into()], &json!({"name": "new"}))
93        .unwrap();
94    document.remove(&["list".into(), 0.into()]).unwrap();
95    document
96        .set(&["list".into(), 1.into(), "name".into()], &json!("updated"))
97        .unwrap();
98    assert_eq!(
99        document.to_value().unwrap(),
100        json!({"list": [2, {"name": "updated"}], "other": "keep"})
101    );
102    for kept in ["/* first */", "// note", "other: 'keep'"] {
103        assert!(document.source().contains(kept));
104    }
105}
106
107#[test]
108fn removing_a_member_keeps_external_comments_but_deletes_its_contents() {
109    let source = "// head\na /* key : šŸ˜€ */ : /* before */ { nested: [/* inside */ 1] } /* after , */ , // line\nb: 'kept'";
110    let mut document = Document::parse(source).unwrap();
111    assert!(document.remove(&["a".into()]).unwrap());
112    assert_eq!(document.to_value().unwrap(), json!({"b": "kept"}));
113    for comment in [
114        "// head",
115        "/* key : šŸ˜€ */",
116        "/* before */",
117        "/* after , */",
118        "// line",
119    ] {
120        assert!(document.source().contains(comment), "{comment}");
121    }
122    assert!(!document.source().contains("/* inside */"));
123    assert!(document.source().ends_with("b: 'kept'"));
124    assert!(!document.remove(&["a".into()]).unwrap());
125}
126
127#[test]
128fn removing_a_member_alone_on_its_line_takes_the_whole_line() {
129    let source =
130        "tasks: {\n  a: {\n    command: \"x\",\n  },\n  b: {\n    command: \"y\",\n  },\n},\n";
131    let mut document = Document::parse(source).unwrap();
132    document.remove(&["tasks".into(), "a".into()]).unwrap();
133    assert_eq!(
134        document.source(),
135        "tasks: {\n  b: {\n    command: \"y\",\n  },\n},\n"
136    );
137    let mut document = Document::parse(source).unwrap();
138    document.remove(&["tasks".into(), "b".into()]).unwrap();
139    assert_eq!(
140        document.source(),
141        "tasks: {\n  a: {\n    command: \"x\",\n  },\n},\n"
142    );
143    for newline in ["\r\n", "\r"] {
144        let source = format!("a: 1{newline}  b: 2{newline}c: 3{newline}");
145        let mut document = Document::parse(&source).unwrap();
146        document.remove(&["b".into()]).unwrap();
147        assert_eq!(document.source(), format!("a: 1{newline}c: 3{newline}"));
148    }
149    let mut document = Document::parse("[\n  1,\n  2,\n  3\n]").unwrap();
150    document.remove(&[1.into()]).unwrap();
151    assert_eq!(document.source(), "[\n  1,\n  3\n]");
152}
153
154#[test]
155fn repeated_removal_and_reinsertion_does_not_accumulate_whitespace() {
156    let source =
157        "tasks: {\n  a: {\n    command: \"x\",\n  },\n  b: {\n    command: \"y\",\n  },\n},\n";
158    let mut document = Document::parse(source).unwrap();
159    for round in 0..3 {
160        document.remove(&["tasks".into(), "a".into()]).unwrap();
161        document.remove(&["tasks".into(), "b".into()]).unwrap();
162        document
163            .set(
164                &["tasks".into(), "a".into()],
165                &json!({"command": format!("x{round}")}),
166            )
167            .unwrap();
168        document
169            .set(&["tasks".into(), "b".into()], &json!({"command": "y"}))
170            .unwrap();
171    }
172    assert!(
173        !document
174            .source()
175            .lines()
176            .any(|line| !line.is_empty() && line.trim().is_empty()),
177        "{}",
178        document.source()
179    );
180    assert_eq!(
181        document.to_value().unwrap()["tasks"]["a"]["command"],
182        json!("x2")
183    );
184}
185
186#[test]
187fn removing_an_inline_member_keeps_its_line_and_comments() {
188    let mut document = Document::parse("tasks: { a: 1, b: 2 }, // done\n").unwrap();
189    document.remove(&["tasks".into(), "a".into()]).unwrap();
190    assert_eq!(document.source(), "tasks: {  b: 2 }, // done\n");
191    assert_eq!(document.to_value().unwrap(), json!({"tasks": {"b": 2}}));
192}
193
194#[test]
195fn removal_handles_all_separator_styles_and_positions() {
196    for separator in [",", ", ", "\n", " /* gap , šŸ˜€ */\n", ", // gap\n", "\r\n"] {
197        for trailing in ["", ",", "\n", ",\n"] {
198            for braced in [false, true] {
199                for index in 0..3 {
200                    let source = format!("a: 1{separator}b: 2{separator}c: 3{trailing}");
201                    let source = if braced {
202                        format!("{{{source}}}")
203                    } else {
204                        source
205                    };
206                    let mut document = Document::parse(source).unwrap();
207                    let keys = ["a", "b", "c"];
208                    assert!(document.remove(&[keys[index].into()]).unwrap());
209                    let mut expected = json!({"a": 1, "b": 2, "c": 3});
210                    expected.as_object_mut().unwrap().remove(keys[index]);
211                    assert_eq!(
212                        document.to_value().unwrap(),
213                        expected,
214                        "{}",
215                        document.source()
216                    );
217                    for key in keys {
218                        document.remove(&[key.into()]).unwrap();
219                    }
220                    assert_eq!(document.to_value().unwrap(), json!({}));
221                    document.set(&["new".into()], &json!(true)).unwrap();
222                    assert_eq!(parse(document.source()).unwrap(), json!({"new": true}));
223                }
224            }
225            for index in 0..3 {
226                let mut document =
227                    Document::parse(format!("[1{separator}2{separator}3{trailing}]")).unwrap();
228                document.remove(&[index.into()]).unwrap();
229                let mut expected = vec![1, 2, 3];
230                expected.remove(index);
231                assert_eq!(document.to_value().unwrap(), json!(expected));
232                document.remove(&[0.into()]).unwrap();
233                document.remove(&[0.into()]).unwrap();
234                assert_eq!(document.to_value().unwrap(), json!([]));
235                document.set(&[0.into()], &json!("again")).unwrap();
236                assert_eq!(document.to_value().unwrap(), json!(["again"]));
237            }
238        }
239    }
240}
241
242#[test]
243fn replacing_a_container_removes_only_comments_inside_it() {
244    let source = "keep: 1\nobject: /* before */ {x: 1 /* inside */} // after\n";
245    let mut document = Document::parse(source).unwrap();
246    document.set(&["object".into()], &json!(false)).unwrap();
247    assert_eq!(
248        document.source(),
249        "keep: 1\nobject: /* before */ false // after\n"
250    );
251    let mut document = Document::parse("// head\n[/* inside */ 1] // tail").unwrap();
252    document.set(&[], &json!({"new": true})).unwrap();
253    assert_eq!(document.source(), "// head\n{\n  new: true\n} // tail");
254}
255
256#[test]
257fn root_replacements_preserve_leading_and_trailing_trivia_without_becoming_comments() {
258    for source in [
259        "",
260        "  ",
261        "// heading",
262        "  // heading",
263        "/* heading */",
264        "// heading\n",
265        "// heading\n{} // tail",
266        "// heading\n[1] // tail",
267        "// heading\nfalse // tail",
268        "// heading\nold: true",
269        "// heading\nold: true // tail\n",
270        "// heading\nold: true,\n// tail\n",
271    ] {
272        for value in [
273            json!(true),
274            json!(null),
275            json!(42),
276            json!("new"),
277            json!({"new": true}),
278            json!([]),
279            json!([1, 2]),
280        ] {
281            let mut document = Document::parse(source).unwrap();
282            document.set(&[], &value).unwrap();
283            assert_eq!(document.to_value().unwrap(), value, "{}", document.source());
284            for kept in ["// heading", "/* heading */", "// tail"] {
285                if source.contains(kept) {
286                    assert!(document.source().contains(kept));
287                }
288            }
289        }
290    }
291}
292
293#[test]
294fn implicit_root_replacements_preserve_trailing_trivia_and_remove_only_a_real_comma() {
295    for newline in ["\n", "\r", "\r\n"] {
296        let prefix = format!("// heading{newline}");
297        let suffix =
298            format!(" /* outside, */{newline}// before comma,{newline}, // after comma{newline}");
299        let mut document = Document::parse(format!("{prefix}a: 1{suffix}")).unwrap();
300        let without_comma = suffix.replace(", // after comma", " // after comma");
301        for value in [json!({"b": 2}), json!([1, 2]), json!(false), json!({})] {
302            document.set(&[], &value).unwrap();
303            assert_eq!(document.to_value().unwrap(), value);
304            assert!(document.source().starts_with(&prefix));
305            assert!(document.source().ends_with(&without_comma));
306        }
307    }
308}
309
310#[test]
311fn removing_the_last_implicit_member_preserves_all_external_comments() {
312    let source = "a /* key */ : /* before */ { x: 1 /* inside */ } /* after, */ , // tail\n";
313    let mut document = Document::parse(source).unwrap();
314    document.remove(&["a".into()]).unwrap();
315    assert_eq!(document.to_value().unwrap(), json!({}));
316    for comment in ["/* key */", "/* before */", "/* after, */", "// tail"] {
317        assert!(document.source().contains(comment));
318    }
319    assert!(!document.source().contains("/* inside */"));
320    document.set(&["b".into()], &json!(2)).unwrap();
321    assert_eq!(document.to_value().unwrap(), json!({"b": 2}));
322}
323
324#[test]
325fn object_documents_require_object_roots_and_keep_missing_colon_diagnostics() {
326    assert_eq!(
327        Document::parse_object("imports {}")
328            .unwrap_err()
329            .render("config", "imports {}"),
330        "config:1:9: Expected `:`"
331    );
332    for source in ["false", "1", "[]", "'a'"] {
333        assert!(Document::parse_object(source).is_err());
334    }
335    let mut document = Document::parse_object("a: 1").unwrap();
336    document.set(&[], &json!(5)).unwrap();
337    assert_eq!(document.to_value().unwrap(), json!(5));
338}
339
340#[test]
341fn invalid_paths_and_unparseable_edits_are_atomic() {
342    let source = "items: [1]\nscalar: false";
343    let mut document = Document::parse(source).unwrap();
344    for path in [
345        vec!["missing".into(), "child".into()],
346        vec!["items".into(), 2.into()],
347        vec!["items".into(), "wrong".into()],
348        vec!["scalar".into(), "child".into()],
349        vec![PathSegment::Index(0)],
350    ] {
351        assert!(document.set(&path, &json!(true)).is_err());
352        assert_eq!(document.source(), source);
353    }
354    assert!(document.remove(&[]).is_err());
355    assert_eq!(document.source(), source);
356    let mut nested = json!(null);
357    for _ in 0..130 {
358        nested = json!([nested]);
359    }
360    let error = document.set(&["items".into()], &nested).unwrap_err();
361    error.render("test", document.source());
362    assert_eq!(document.source(), source);
363}