char/avril

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

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

Charlotte Somrearrange libs into lib and extensions into extsf4a0f5b

main
13.5 KiB423 linesraw
1use serde_fjson::syntax::{self as config, Member, Value, ValueKind};
2
3fn members(value: &Value) -> &[Member] {
4    let ValueKind::Object(members) = &value.kind else {
5        panic!("Expected an object: {value:?}");
6    };
7    members
8}
9
10fn field<'a>(value: &'a Value, key: &str) -> &'a Value {
11    &members(value)
12        .iter()
13        .find(|member| member.key == key)
14        .unwrap()
15        .value
16}
17
18#[test]
19fn parses_a_package_config_with_or_without_root_braces() {
20    let source = r#"
21        // Dependencies are private to this package.
22        imports: {
23            assert: 'jsr:@std/assert@^1',
24            "@my/library": "../library/mod.ts",
25        },
26        options: {
27            enabled: true,
28            fallback: null,
29            targets: ["linux", /* another platform */ "macos",],
30        },
31    "#;
32    for source in [source.to_owned(), format!("{{{source}}}")] {
33        let value = config::parse_object(&source).unwrap();
34        let imports = field(&value, "imports");
35        assert_eq!(
36            field(imports, "assert").kind,
37            ValueKind::String("jsr:@std/assert@^1".into())
38        );
39        assert_eq!(
40            field(imports, "@my/library").kind,
41            ValueKind::String("../library/mod.ts".into())
42        );
43        let options = field(&value, "options");
44        assert_eq!(field(options, "enabled").kind, ValueKind::Bool(true));
45        assert_eq!(field(options, "fallback").kind, ValueKind::Null);
46        let ValueKind::Array(targets) = &field(options, "targets").kind else {
47            panic!()
48        };
49        assert_eq!(
50            targets.iter().map(|value| &value.kind).collect::<Vec<_>>(),
51            vec![
52                &ValueKind::String("linux".into()),
53                &ValueKind::String("macos".into()),
54            ]
55        );
56    }
57}
58
59#[test]
60fn handles_empty_containers_and_comments_at_token_boundaries() {
61    for source in [
62        "",
63        " \t\r\n",
64        "// only a comment",
65        "/* comment */",
66        "{}",
67        "{ /* comment */ }",
68    ] {
69        assert!(
70            members(&config::parse_object(source).unwrap()).is_empty(),
71            "{source}"
72        );
73    }
74    let value = config::parse_object("a/*1*/:/*2*/[/*3*/]/*4*/,/*5*/b: {/*6*/},// eof").unwrap();
75    assert_eq!(field(&value, "a").kind, ValueKind::Array(vec![]));
76    assert!(members(field(&value, "b")).is_empty());
77    assert_eq!(
78        field(&config::parse_object("a: false // eof").unwrap(), "a").kind,
79        ValueKind::Bool(false)
80    );
81}
82
83#[test]
84fn implicit_commas_separate_map_entries_and_array_items() {
85    for newline in ["\n", "\r\n", "\r"] {
86        for separator in [
87            newline.to_owned(),
88            format!(" // comment{newline}"),
89            format!(" /* comment */{newline}/* next */ "),
90            format!(" /* multiline{newline}comment */{newline}"),
91            format!("{newline}/* multiline{newline}comment */ "),
92            format!("{newline}// blank{newline}{newline}"),
93            format!(",{newline}"),
94            format!("{newline},{newline}"),
95        ] {
96            let source = format!(
97                "a: 1{separator}b: [true{separator}false{separator}null{newline}]{newline}"
98            );
99            for source in [source.clone(), format!("{{{newline}{source}}}")] {
100                let value = config::parse_object(&source).unwrap();
101                assert_eq!(members(&value).len(), 2, "{source:?}");
102                assert_eq!(field(&value, "a").kind, ValueKind::Number("1".into()));
103                assert_eq!(&source[field(&value, "a").span.clone()], "1");
104                let ValueKind::Array(items) = &field(&value, "b").kind else {
105                    panic!()
106                };
107                assert_eq!(
108                    items.iter().map(|item| &item.kind).collect::<Vec<_>>(),
109                    vec![
110                        &ValueKind::Bool(true),
111                        &ValueKind::Bool(false),
112                        &ValueKind::Null
113                    ]
114                );
115                for (item, spelling) in items.iter().zip(["true", "false", "null"]) {
116                    assert_eq!(&source[item.span.clone()], spelling);
117                }
118            }
119        }
120    }
121}
122
123#[test]
124fn newlines_only_separate_completed_entries_at_the_current_level() {
125    let source = "a\n:\n[\n{\nx\n:\n1\ny: 2\n}\n[\n3\n4\n]\n]\nb:\nfalse";
126    let value = config::parse_object(source).unwrap();
127    assert_eq!(members(&value).len(), 2);
128    assert_eq!(field(&value, "b").kind, ValueKind::Bool(false));
129    let ValueKind::Array(items) = &field(&value, "a").kind else {
130        panic!()
131    };
132    assert_eq!(items.len(), 2);
133    assert_eq!(field(&items[0], "x").kind, ValueKind::Number("1".into()));
134    assert_eq!(field(&items[0], "y").kind, ValueKind::Number("2".into()));
135    assert_eq!(&source[items[0].span.clone()], "{\nx\n:\n1\ny: 2\n}");
136    let ValueKind::Array(numbers) = &items[1].kind else {
137        panic!()
138    };
139    assert_eq!(
140        numbers
141            .iter()
142            .map(|number| &number.kind)
143            .collect::<Vec<_>>(),
144        vec![
145            &ValueKind::Number("3".into()),
146            &ValueKind::Number("4".into())
147        ]
148    );
149}
150
151#[test]
152fn requires_explicit_commas_without_a_separator_newline() {
153    for source in [
154        "a: 1 b: 2",
155        "{a: true b: false}",
156        "a: [1 2]",
157        "a: ['one' 'two']",
158        "a: 1 /* comment */ b: 2",
159        "a: [1 /* comment */ 2]",
160        "a: 1 /* multiline\ncomment */ b: 2",
161        "a: [1 /* multiline\r\ncomment */ 2]",
162        "a: [\n1\n] b: 2",
163        "a: [{\nx: 1\n} {}]",
164        "a\n:\n1 b: 2",
165        "a: ['one\\ntwo' 'three']",
166    ] {
167        let error = config::parse_object(source).unwrap_err();
168        assert!(
169            error.message.contains("Expected `,` or newline"),
170            "{source}: {error}"
171        );
172    }
173    for source in [
174        "a: 1,",
175        "{a: 1,}",
176        "a: [1,],",
177        "a: 1,\nb: 2,",
178        "a: 1\n,",
179        "{a: 1\n,}",
180        "a: [1\n,]",
181    ] {
182        config::parse_object(source).unwrap();
183    }
184}
185
186#[test]
187fn strings_decode_escapes_without_interpreting_comment_markers() {
188    let value = config::parse_object(
189        r#"
190        url: "https://example.com/*path*/",
191        text: '\"\'\\\/\b\f\n\r\t',
192        unicode: "α\u0061\uD83D\uDE00",
193        "quoted\u0020key": 'value',
194    "#,
195    )
196    .unwrap();
197    assert_eq!(
198        field(&value, "url").kind,
199        ValueKind::String("https://example.com/*path*/".into())
200    );
201    assert_eq!(
202        field(&value, "text").kind,
203        ValueKind::String("\"'\\/\u{0008}\u{000c}\n\r\t".into())
204    );
205    assert_eq!(
206        field(&value, "unicode").kind,
207        ValueKind::String("αa😀".into())
208    );
209    assert_eq!(
210        field(&value, "quoted key").kind,
211        ValueKind::String("value".into())
212    );
213}
214
215#[test]
216fn accepts_identifier_keys_but_not_bare_string_values() {
217    let value =
218        config::parse_object("_key: 1, $key: 2, key123: 3, true: false, '日本語': 'ok'").unwrap();
219    assert_eq!(
220        members(&value)
221            .iter()
222            .map(|member| member.key.as_str())
223            .collect::<Vec<_>>(),
224        vec!["_key", "$key", "key123", "true", "日本語"]
225    );
226    for source in ["a: bare", "a: undefined", "a: trueish", "a: Null"] {
227        assert!(
228            config::parse_object(source)
229                .unwrap_err()
230                .message
231                .contains("strings must be quoted")
232        );
233    }
234    for source in ["123: true", "日本語: 1", "@scope/pkg: 1", "some-key: 1"] {
235        assert!(config::parse_object(source).is_err(), "{source}");
236    }
237}
238
239#[test]
240fn decimal_numbers_keep_their_precision_and_spelling() {
241    for spelling in [
242        "0",
243        "-0",
244        "42",
245        "-12.34",
246        "1e3",
247        "2.5E-10",
248        "6e+2",
249        "9007199254740993",
250        "1e9999",
251    ] {
252        let source = format!("value: {spelling}");
253        assert_eq!(
254            field(&config::parse_object(&source).unwrap(), "value").kind,
255            ValueKind::Number(spelling.into())
256        );
257    }
258    for spelling in [
259        "-",
260        "01",
261        "-01",
262        "1.",
263        ".5",
264        "+1",
265        "1e",
266        "1e+",
267        "0x10",
268        "1_000",
269        "1n",
270        "NaN",
271        "Infinity",
272        "-Infinity",
273        "1.2.3",
274    ] {
275        assert!(
276            config::parse_object(&format!("value: {spelling}")).is_err(),
277            "{spelling}"
278        );
279    }
280}
281
282#[test]
283fn rejects_duplicate_decoded_keys_and_identifies_both_definitions() {
284    let source = "imports: {\n  assert: 'first'\n  '\\u0061ssert': 'second'\n}";
285    let error = config::parse_object(source).unwrap_err();
286    assert!(
287        error.message.contains("Duplicate key \"assert\""),
288        "{error}"
289    );
290    assert_eq!(&source[error.span.clone()], "'\\u0061ssert'");
291    assert_eq!(&source[error.previous.clone().unwrap()], "assert");
292    assert_eq!(
293        error.render("avril.conf", source),
294        "avril.conf:3:3: Duplicate key \"assert\"\navril.conf:2:3: First defined here"
295    );
296    config::parse_object("a: {key: 1}, b: {key: 2}").unwrap();
297}
298
299#[test]
300fn retains_original_source_spans_for_keys_and_values() {
301    let source = "/* lead */ imports: { '日本語': ['😀', -12.5], }, // tail";
302    let value = config::parse_object(source).unwrap();
303    let imports = field(&value, "imports");
304    assert_eq!(
305        &source[imports.span.clone()],
306        "{ '日本語': ['😀', -12.5], }"
307    );
308    let entry = &members(imports)[0];
309    assert_eq!(&source[entry.key_span.clone()], "'日本語'");
310    assert_eq!(&source[entry.value.span.clone()], "['😀', -12.5]");
311    let ValueKind::Array(elements) = &entry.value.kind else {
312        panic!()
313    };
314    assert_eq!(&source[elements[0].span.clone()], "'😀'");
315    assert_eq!(&source[elements[1].span.clone()], "-12.5");
316}
317
318#[test]
319fn diagnostics_use_original_lines_and_unicode_character_columns() {
320    let source = "label: '😀',\n  imports: {\n    x: 'α', y 'bad',\n  },\n";
321    let error = config::parse_object(source).unwrap_err();
322    assert_eq!(
323        error.render("avril.conf", source),
324        "avril.conf:3:15: Expected `:`"
325    );
326    assert_eq!(&source[error.span], "'");
327    for newline in ["\r", "\r\n", "\n"] {
328        let source = format!("a: true,{newline}// heading{newline}b:");
329        let error = config::parse_object(&source).unwrap_err();
330        assert_eq!(
331            error.render("avril.conf", &source),
332            "avril.conf:3:3: Expected a value"
333        );
334    }
335    let source = "a: 1,\nb:";
336    let error = config::parse_object(source).unwrap_err();
337    assert_eq!(error.span, source.len()..source.len());
338    assert_eq!(
339        error.render("avril.conf", source),
340        "avril.conf:2:3: Expected a value"
341    );
342}
343
344#[test]
345fn rejects_malformed_or_executable_input() {
346    for (source, message) in [
347        ("[]", "object key"),
348        ("42", "object key"),
349        ("{a: 1} b: 2", "after the root object"),
350        ("{a: 1},", "after the root object"),
351        ("a: {", "Expected `}`"),
352        ("a: [", "Expected `]`"),
353        ("a: [1", "Expected `]`"),
354        ("a: {b: 1", "Expected `}`"),
355        ("a: 1,,", "object key"),
356        ("a: [,]", "Expected a value"),
357        ("a: [1,,]", "Expected a value"),
358        ("a: 1;", "Expected `,` or newline"),
359        ("a = 1", "Expected `:`"),
360        ("a: () => 1", "Expected a value"),
361        ("a: `template`", "Expected a value"),
362        ("a: 'unterminated", "Unterminated string"),
363        ("a: 'new\nline'", "Control characters"),
364        ("a: '\0'", "Control characters"),
365        (r#"a: '\q'"#, "Invalid string escape"),
366        (r#"a: '\u12x4'"#, "hexadecimal digits"),
367        (r#"a: '\uDC00'"#, "Unexpected low Unicode surrogate"),
368        (r#"a: '\uD800\u0041'"#, "Expected a low Unicode surrogate"),
369        (r#"a: '\uD800'"#, "Unpaired Unicode surrogate"),
370        ("/* never closed", "Unterminated block comment"),
371        ("a: 1 / nope", "Expected `,` or newline"),
372    ] {
373        let error = config::parse_object(source).unwrap_err();
374        assert!(error.message.contains(message), "{source:?}: {error}");
375        assert!(source.get(error.span.clone()).is_some(), "{error:?}");
376    }
377}
378
379#[test]
380fn bounds_nesting_without_rejecting_large_flat_configs() {
381    let nested = format!("value: {}0{}", "[".repeat(129), "]".repeat(129));
382    assert!(
383        config::parse_object(&nested)
384            .unwrap_err()
385            .message
386            .contains("nesting")
387    );
388    let nested = format!("value: {}0{}", "[".repeat(128), "]".repeat(128));
389    config::parse_object(&nested).unwrap();
390    let objects = format!("value: {}0{}", "{a:".repeat(129), "}".repeat(129));
391    assert!(
392        config::parse_object(&objects)
393            .unwrap_err()
394            .message
395            .contains("nesting")
396    );
397    let flat = format!("value: [{}]", "0,".repeat(10_000));
398    let value = config::parse_object(&flat).unwrap();
399    let ValueKind::Array(elements) = &field(&value, "value").kind else {
400        panic!()
401    };
402    assert_eq!(elements.len(), 10_000);
403}
404
405#[test]
406fn truncated_input_always_produces_valid_diagnostic_spans() {
407    let source =
408        r#"/* lead */ imports: {'日本語': "😀\uD83D\uDE00",}, flags: [true, false, null, -1e+2],"#;
409    for offset in source
410        .char_indices()
411        .map(|(offset, _)| offset)
412        .chain([source.len()])
413    {
414        let prefix = &source[..offset];
415        if let Err(error) = config::parse_object(prefix) {
416            assert!(
417                prefix.get(error.span.clone()).is_some(),
418                "{prefix:?}: {error:?}"
419            );
420            error.render("avril.conf", prefix);
421        }
422    }
423}