diff --git a/README.md b/README.md index 3a9a28d..c3d774f 100644 --- a/README.md +++ b/README.md @@ -211,13 +211,13 @@ b.step("check-config", "Check the config against Config").dependOn(&check.step); | implementation | against the spec | against typesafe/config 1.4.9 | |---|---|---| -| Java typesafe/config 1.4.9 | 90% (399/445) | 99% (442/445) | -| hocon2 hocon2 0.6.3 | 86% (384/445) | 80% (357/445) | -| pulse-pyhocon pulse-pyhocon 0.2.1 | 71% (314/445) | 68% (303/445) | -| py.hocon hocon-parser 1.14.2 (Python) | 89% (395/445) | 88% (392/445) | -| pyhocon pyhocon 0.3.63 | 70% (312/445) | 66% (295/445) | -| rs.hocon hocon-parser 1.14.2 (Rust) | 89% (395/445) | 86% (383/445) | -| zig-hocon zig-hocon code@fad796a | 99% (442/445) | 90% (399/445) | +| Java typesafe/config 1.4.9 | 90% (401/445) | 100% (444/445) | +| hocon2 hocon2 0.6.3 | 87% (386/445) | 81% (359/445) | +| pulse-pyhocon pulse-pyhocon 0.2.1 | 71% (316/445) | 69% (305/445) | +| py.hocon hocon-parser 1.14.2 (Python) | 89% (397/445) | 89% (394/445) | +| pyhocon pyhocon 0.3.63 | 71% (314/445) | 67% (297/445) | +| rs.hocon hocon-parser 1.14.2 (Rust) | 89% (397/445) | 87% (385/445) | +| zig-hocon zig-hocon code@73b5fad | 100% (444/445) | 90% (401/445) | The table keeps the best parsers; the others measured, and why they are not in it: [conformance/README.md](conformance/README.md#not-in-the-table). diff --git a/conformance/README.md b/conformance/README.md index b98c0ae..26e1ffa 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -81,6 +81,17 @@ A runner hands your adapter the path to `main.conf`; includes resolve beside it. A row where the reference implementation and the spec part ways carries `"java": "diverges" | "unsupported" | "lenient"` and, next to it, `"java_expect"` or `"java_error"` — what typesafe/config does with that same input. Those rows are the reason the suite exists; `maintaining/PROCESS.md` has the table of which kind means what. +Where the spec allows more than one answer, `"also_valid"` lists the others, and an output matching `expect` or any of them passes. `"rejected"` in the list lets an error pass too: + +```json +{ + "spec": "HOCON.md#examples-of-self-referential-substitutions", + "why": "the order a and b resolve in is undefined: both 1, both 2 or an error are all allowed, but a and b must agree", + "expect": {"a": 1, "b": 1}, + "also_valid": [{"a": 2, "b": 2}, "rejected"] +} +``` + A row carrying `"review"` is an open question for a human. It is neither a pass nor a failure. `--text` lists such rows apart from the failing ones, and they alone do not make `run.sh` exit 1. Two directories assert features the specification only permits: `include-file-formats` (other formats, basename probing: "implementations may support") and `substitution-fallback-to-environment` together with `list-values-from-environment-variables` ("implementations may search external sources"). An implementation without them is still conformant; it fails those rows knowingly, and `--only` can leave them out. @@ -102,9 +113,9 @@ Drop your own result into `reports/` and the local report picks it up. The published table keeps the best parsers: every Python parser of its own above 70 % against the spec, and the best Rust one. These were measured too and left out: - **hocon-rs 0.2.0** (Rust, mikai233/hocon-rs): 84 % against the spec, behind rs.hocon. -- **hocon 0.9.0** (Rust, mockersf/hocon.rs): 71 % with its strict loader, 58 % with the default one, which never fails; the repository is archived since 2023, and non-ASCII whitespace makes it panic. +- **hocon 0.9.0** (Rust, mockersf/hocon.rs): 72 % with its strict loader, 58 % with the default one, which never fails; the repository is archived since 2023, and non-ASCII whitespace makes it panic. - **hocon_** (Rust, pepa65): a republished fork of hocon 0.9.0, not measured on its own. -- **pyhocon-fix 0.0.4** (Python, 2018) and **dd-pyhocon 0.3.61** (Python, 2023): forks of pyhocon, 71 % and 70 %, within two cases of it; dd-pyhocon hangs on one merge-stack case. +- **pyhocon-fix 0.0.4** (Python, 2018) and **dd-pyhocon 0.3.61** (Python, 2023): forks of pyhocon, both 71 %, within two cases of it; dd-pyhocon hangs on one merge-stack case. - Not parsers of their own: `hocon` 0.3.0, `hocon-validator`, `pytyped-hocon`, `pyhocon-dantic` and `dataconf` wrap pyhocon; `atomr` (Rust/PyO3) builds its `Config` from TOML or a dict and parses no HOCON; chris-martin/hocon-python (2015) is an unfinished port of typesafe/config. ## Reusing this diff --git a/conformance/report.html b/conformance/report.html index 44c1344..ee90447 100644 --- a/conformance/report.html +++ b/conformance/report.html @@ -124,7 +124,7 @@
HOCON.md.
- | case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-arrays-concatenate | pass | pass | pass | pass | pass | pass | pass | arrays can be concatenated with arrays when only non-newline whitespace separates them |
| 002-objects-concatenate | pass | pass | pass | pass | pass | pass | pass | objects can be concatenated with objects when only non-newline whitespace separates them |
| 003-mixing-array-and-object-is-error | pass | pass | pass | pass | pass | pass | pass | it is an error to concatenate an array with an object |
| 004-substitution-counts-as-array-for-concatenation | pass | pass | pass | pass | pass | pass | pass | for purposes of concatenation a substitution that resolves to an array also means an array, so ${x} [ 1, 2 ] with x an array concatenates into one array |
| 005-substitution-counts-as-object-for-concatenation | pass | pass | pass | pass | pass | pass | pass | for purposes of concatenation a substitution that resolves to an object also means an object, so ${x} { c : 1 } with x an object merges into one object |
| 006-newline-between-values-prevents-concatenation | pass | pass | pass | pass | pass | pass | pass | newlines between a substitution and an array prevent concatenation, so inside an array they stay two separate elements instead of one concatenated array |
| 007-object-concatenation-merges-and-second-overrides | pass | pass | pass | pass | pass | pass | pass | for objects, concatenation means merging, and the second object overrides the first on overlapping keys |
| 008-array-cannot-be-field-key | pass | pass | pass | pass | pass | pass | pass | arrays cannot be field keys; nested inside an object so the rejection comes from the key position, not from the document root |
| 009-object-cannot-be-field-key | pass | pass | pass | pass | pass | pass | pass | objects cannot be field keys; nested inside an object so the rejection comes from the key position, not from the document root |
| 010-worked-example-object-inheritance | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: object concatenation with a substitution is a common way to express inheritance |
| 011-worked-example-array-path-append | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: array concatenation with a substitution is a common way to add to paths |
| 012-worked-example-self-referential-array-concat | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: a later array definition can concatenate a substitution referring to its own earlier value with a new array |
| 013-newlines-within-value-still-allow-concatenation | pass | pass | pass | pass | pass | pass | pass | newlines may occur within an array without preventing it from concatenating with a second array on the same line as its closing bracket |
| 014-substitution-object-with-literal-array-is-error | pass | pass | pass | pass | pass | pass | pass | a substitution that resolves to an object means an object for concatenation, and concatenating an object with an array is an error |
| 015-substitution-array-with-literal-object-is-error | pass | pass | pass | pass | pass | pass | pass | a substitution that resolves to an array means an array for concatenation, and concatenating an array with an object is an error |
| 016-mixing-object-then-array-is-error | pass | pass | pass | pass | pass | pass | pass | it is an error to concatenate an object with an array in this order too, not only array then object |
| 017-newline-between-arrays-in-field-value-prevents-concatenation | pass | pass | pass | pass | pass | pass | pass | a newline between two arrays in a field value prevents concatenation, so the second array begins a new field, and an array cannot be a field key |
| 018-object-with-inner-merge-concatenated-with-substitution | fail | pass | pass | pass | pass | pass | pass | an object whose field a is itself a merge of an object and a substitution concatenates with a substituted object like any other: the two merge. typesafe/config fails with an internal error (BugOrBroken) because the concatenation is not registered as the merge's parent; the same objects written as literals concatenate finejava: unsupported |
| 019-self-referential-object-concatenation-with-inner-merge | fail | pass | pass | pass | pass | pass | pass | ${a} in a's own value refers to the earlier a, an object, so the concatenation merges {p: ...} with {q: 1}; the inner p is itself a merge of an object and a substitution. typesafe/config fails with an internal error (BugOrBroken) when the merge inside a concatenation piece resolves, as in 018java: unsupported |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-newlines-instead-of-commas | pass | pass | pass | pass | pass | pass | pass | arrays allow newlines to separate elements instead of commas |
| 002-whitespace-instead-of-comma-concatenates-into-one-element | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: non-newline whitespace instead of a comma produces concatenation, so this array has one element, the string 1 2 3 4, not four integers |
| 003-newline-separates-two-arrays-into-elements | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: a newline between two bracketed arrays makes them two separate elements |
| 004-same-line-whitespace-concatenates-two-arrays-into-one-element | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: whitespace on the same line between two bracketed arrays concatenates them into a single element, the array [1, 2, 3, 4] |
| 005-unquoted-string-concatenation-with-substitutions-in-array | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: unquoted words and substitutions separated by whitespace concatenate into one string element, and only the comma separates elements |
| 006-two-elements-each-a-substitution-concatenation | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: whitespace between two substitutions concatenates them into one element, and the comma separates the two concatenations |
| 007-whitespace-never-separates-fields | pass | pass | fail | fail | pass | fail | pass | non-newline whitespace is never a field separator, so two field definitions cannot be written on one line separated only by spaces |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-array-trailing-comma | pass | pass | pass | pass | pass | pass | pass | the last element in an array may be followed by a single comma, which is ignored: [1,2,3,] and [1,2,3] are the same array |
| 002-array-newline-no-comma | pass | pass | pass | pass | pass | pass | pass | array elements need not have a comma between them as long as an ASCII newline separates them: [1\n2\n3] and [1,2,3] are the same array |
| 003-array-double-trailing-comma-invalid | pass | pass | fail | pass | fail | pass | pass | [1,2,3,,] is explicitly called invalid because it has two trailing commas |
| 004-array-initial-comma-invalid | pass | pass | fail | pass | fail | pass | pass | [,1,2,3] is explicitly called invalid because it has an initial comma |
| 005-array-double-comma-invalid | pass | pass | fail | pass | fail | pass | pass | [1,,2,3] is explicitly called invalid because it has two commas in a row |
| 006-object-trailing-comma | pass | pass | pass | pass | pass | pass | pass | these same comma rules apply to fields in objects, so a single trailing comma after the last field is ignored |
| 007-object-newline-no-comma | pass | pass | pass | pass | pass | pass | pass | fields in objects need not have a comma between them as long as an ASCII newline separates them, same as array elements |
| 008-object-double-comma-invalid | pass | pass | fail | pass | fail | pass | pass | these same comma rules apply to fields in objects, so two commas in a row between fields is invalid just as in an array |
| 009-fields-on-one-line-need-a-comma | pass | pass | fail | fail | pass | fail | pass | two fields need a comma or a newline between them; on one line with neither, 1 b becomes a value concatenation and the second = is a stray token |
| 011-trailing-comma-at-implicit-root | pass | pass | pass | pass | pass | pass | pass | the implicit root object follows the object comma rules, so its last field may carry a single trailing comma |
| 012-leading-comma-at-implicit-root-illegal | pass | pass | fail | pass | fail | pass | pass | an initial comma is invalid in an object, the implicit root included |
| 013-double-trailing-comma-in-object-illegal | pass | pass | fail | pass | fail | pass | pass | the array comma rules apply to object fields: two trailing commas are invalid |
| 014-leading-comma-in-object-illegal | pass | pass | fail | pass | fail | pass | pass | the array comma rules apply to object fields: an initial comma is invalid |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-hash-comment | pass | pass | pass | pass | pass | pass | pass | anything between # and the next newline is a comment and ignored |
| 002-double-slash-comment | pass | pass | pass | pass | pass | pass | pass | anything between // and the next newline is a comment and ignored |
| 003-whole-line-comment | pass | pass | pass | pass | pass | pass | pass | a comment may occupy an entire line by itself, ignored just like a trailing one |
| 004-comment-stops-at-newline | pass | pass | pass | pass | pass | pass | pass | a comment runs only to the next newline, so the following line is parsed normally |
| 005-hash-inside-quoted-string-is-not-a-comment | pass | pass | pass | pass | pass | pass | pass | a # inside a quoted string is not treated as starting a comment |
| 006-double-slash-inside-quoted-string-is-not-a-comment | pass | pass | pass | pass | pass | pass | pass | a // inside a quoted string is not treated as starting a comment |
| 007-comment-between-separator-and-value | pass | pass | pass | pass | pass | pass | pass | a comment runs to the end of its line and is then over; the value may follow on the next line just as it may after a bare = |
| 008-comment-then-blank-line-then-field | pass | pass | pass | pass | pass | pass | pass | a comment separated from the following field by a blank line is ignored like any other comment |
| 009-comment-inside-object | pass | pass | pass | pass | pass | pass | pass | comments and blank lines inside an object body are ignored |
| 010-comments-inside-array | pass | pass | pass | pass | pass | pass | pass | comments and blank lines between array elements are ignored, and do not join or separate elements |
| 011-comment-before-values-of-every-type | pass | pass | pass | pass | pass | pass | pass | a comment before a field is ignored whatever type its value has |
| 012-comment-before-explicit-root-object | pass | pass | pass | pass | pass | pass | pass | a comment and a blank line before an explicit root object are skipped before the implicit-braces rule looks at the first token; the spec's wording ("does not begin with") is loose here and this row records the consensus reading, as omit-root-braces/009 does |
| 013-line-comment-after-array | pass | pass | pass | pass | pass | pass | pass | a // comment after an array is not text in a concatenation; the value is just the array |
| 014-hash-comment-after-array-without-space | pass | pass | pass | pass | pass | pass | pass | a # comment directly after ] is a comment, not text in a concatenation |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-unquoted-whitespace-between-substitutions-is-significant-for-strings | pass | pass | pass | pass | pass | pass | pass | when the substitutions turn out to be strings, the unquoted whitespace between them is significant and survives into the concatenated string |
| 002-quoted-whitespace-between-substitutions-is-error | pass | pass | pass | pass | pass | pass | pass | between substitutions that resolve to objects, unquoted whitespace is ignored but quoted whitespace should be an error |
| 003-unquoted-whitespace-between-object-and-list-substitutions-is-ignored | pass | pass | pass | pass | pass | pass | pass | unquoted whitespace between substitutions that resolve to objects or lists must be ignored, so they merge and concatenate as if adjacent |
| 004-tab-between-list-substitutions-is-ignored | pass | pass | pass | pass | pass | pass | pass | whitespace between two list substitutions is ignored, tabs included, and the lists concatenate |
| 005-nonbreaking-space-between-list-substitutions-is-ignored | pass | pass | pass | pass | fail | pass | pass | U+00A0 is a Zs space and so whitespace in HOCON; between two list substitutions it is ignored (lightbend/config#862 review) |
| 006-bom-between-list-substitutions-is-ignored | pass | pass | fail | pass | fail | pass | pass | the BOM must be treated as whitespace, so between two list substitutions it is ignored |
| 007-zero-width-space-between-list-substitutions-is-error | fail | pass | pass | pass | pass | pass | pass | U+200B is format (Cf), not a space separator, so it is text, and text between two list substitutions is invalid. typesafe/config drops it and concatenates the lists; lightbend/config#862 rejects itjava: lenient |
| 008-list-then-undefined-optional-is-the-list | pass | pass | pass | pass | pass | pass | pass | an undefined optional substitution in a concatenation with an array becomes an empty array |
| 009-undefined-optional-then-list-is-the-list | fail | pass | pass | pass | pass | pass | pass | the mirror of 008: an undefined optional substitution before an array becomes an empty array too. typesafe/config keeps the whitespace before the list as text and rejects it, while accepting 008 (noted on lightbend/config#862)java: unsupported |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 002-file-merge-is-recursive-and-later-file-wins | pass | pass | pass | pass | pass | pass | pass | merging objects loaded from different files is exactly the same behavior as merging duplicate fields in the same file: object-valued fields present in both files merge recursively and a non-object field takes the value from the later file |
| 003-non-object-between-files-hides-earlier-object | pass | pass | pass | pass | fail | pass | pass | the spec's own example: an intermediate non-object value hides earlier object values because merging is done in pairs, so when 42 is paired with { y : 2 } it simply wins and loses all information about what it overrode, and the two objects are never merged |
| 004-adjacent-objects-across-files-merge | pass | pass | pass | pass | fail | pass | pass | the spec's re-ordered example: with the non-object 42 moved to the lowest priority the two objects are adjacent, so they merge to { a : { x : 1, y : 2 } } |
| 005-null-clears-included-object | pass | pass | pass | pass | pass | pass | pass | setting an object to null and then setting it back to a new object clears it and starts over, which is how to get rid of default fallback values from another file that you don't want |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-later-scalar-wins | pass | pass | pass | pass | pass | pass | pass | for a non-object value the later assignment simply replaces the earlier one |
| 002-objects-merge | pass | pass | pass | pass | pass | pass | pass | two objects under the same key merge key-by-key, they do not replace |
| 003-scalar-breaks-the-merge | pass | pass | pass | pass | fail | pass | pass | merging is always done two values at a time: the scalar replaces the first object (non-object always wins), then the second object replaces the scalar (no merging, object is the new value), so the two objects never see each other |
| 004-merge-scalar-field-later-wins | pass | pass | pass | pass | pass | pass | pass | for a non-object-valued field present in both merged objects, the field from the second object is used |
| 005-merge-nested-object-recursive | pass | pass | pass | pass | pass | pass | pass | for an object-valued field present in both objects, the object values are recursively merged by the same rules |
| 006-null-prevents-merge | pass | pass | pass | pass | pass | pass | pass | the spec's own example: an intermediate null is a non-object, so it replaces the first object and is in turn replaced by the second object with no merging, which prevents the two objects from ever seeing each other |
| 007-merge-keeps-fields-from-either-side | pass | pass | pass | pass | pass | pass | pass | fields present in only one of the two objects are added to the merged object as they are, whether the first or the second side has them and whether the value is a scalar or an object with nothing to merge against |
| 008-arrays-are-not-objects | pass | pass | pass | pass | pass | pass | pass | only two object values merge; an array is not an object, so a later array replaces an earlier array (no concatenation) and replaces an earlier object (no merge) |
| 009-later-null-overrides | pass | pass | pass | pass | pass | pass | pass | null is a value like any other, so a later null overrides an earlier scalar or object: the key stays, set to null, rather than being removed or the assignment skipped |
| 010-null-in-referenced-key-history-does-not-block-merge | fail | pass | fail | pass | pass | pass | pass | the null at r only stops merging at r; ${r} is the object {x:1}, and a later object merges with the earlier a, so helper stays. typesafe/config carries r's null through the substitution and drops helper (lightbend/config#864)java: diverges |
| 011-nested-null-in-referenced-object-does-not-block-merge | fail | pass | fail | pass | pass | pass | pass | the same rule one level down: the null in v.nested's history belongs to v.nested, so a.nested still merges with the substituted {x:1} (lightbend/config#864)java: diverges |
| 012-reference-to-merged-field-sees-the-same-value | fail | pass | pass | pass | pass | pass | pass | a substitution sees the resolved value of the field it names, so o24bbd equals a.nested, which keeps helper. The key name is chosen to sort before a in typesafe/config's root HashMap, the order in which the first version of lightbend/config#864 answered differently for the twojava: diverges |
| 013-top-level-null-before-object-with-substitution | fail | pass | fail | pass | pass | pass | pass | the null belongs to v's history even when v's object is still unresolved, so a merges and keeps helper (lightbend/config#864)java: diverges |
| 014-own-null-still-blocks-merge-with-referenced-object | pass | pass | pass | pass | pass | pass | pass | the receiver's own intermediate null still breaks the merge: a is {helper:0}, then null, then the substituted object, so helper is gone |
| 015-earlier-receiver-value-is-evaluated-once-merged | fail | pass | fail | pass | pass | pass | pass | ${r} is an object, so it merges with the earlier a and the ${missing} inside it is evaluated: an error. typesafe/config 1.4.5 agreed; 1.4.9 drops the earlier a and yields {y:1}, and lightbend/config#864 restores the errorjava: lenient |
| 016-null-deeper-in-a-merge-stack-stays-at-its-key | fail | pass | fail | pass | fail | pass | pass | v's value is a merge whose top is an object and whose null sits further down, under nested; that null is still v.nested's merge instruction, so a.nested keeps helper (lightbend/config#864 review, round 3)java: diverges |
| 017-lookup-into-nested-null-merges | fail | pass | pass | pass | pass | pass | pass | two nested nulls each break only their own key's merge, and a lookup of a.n.d sees the final {x:1}; typesafe/config throws BugOrBroken in this key order (reported on lightbend/config#864, same on main)java: unsupported |
| 018-object-then-empty-object | pass | pass | pass | pass | pass | pass | pass | two objects merge, so the empty later object keeps x; replacement would have given {} |
| 019-duplicate-keys-merge-inside-a-list-element | pass | pass | pass | pass | pass | pass | pass | an object that is a list element follows the same duplicate-key rules: the later value, here resolved from a substitution, wins |
| 020-objects-and-substitutions-alternate | pass | pass | fail | fail | pass | fail | pass | four values for a, two of them substitutions that resolve to objects: all four merge in order |
| 021-later-object-replaces-earlier-concatenation | pass | pass | pass | pass | fail | pass | pass | a string concatenation is a non-object value, so the later object replaces it rather than merging with it |
| 022-scalar-then-empty-object-then-object | pass | pass | pass | pass | fail | pass | pass | the number cuts the merge off from anything before it; the empty object after it and the object after that still merge, and the empty one contributes nothing |
| 023-empty-object-then-substitution | pass | pass | pass | pass | pass | pass | pass | an empty object followed by a substitution that resolves to an object merges into that object; the empty layer contributes nothing |
| 024-merge-stack-nested-inside-a-merge-stack | pass | pass | fail | pass | pass | pass | pass | the object layer of a's merge contains a field b that is itself a merge of a substitution and an object; both levels merge |
| 025-lookup-into-object-that-replaced-a-list | fail | pass | pass | pass | pass | pass | pass | the object replaces the earlier list, so a.m exists and p is 1 whatever order the keys are resolved in. typesafe/config fails when the reader p is resolved before a (it iterates p before a); the same file with the reader named zzz resolvesjava: unsupported |
| 026-missing-key-in-object-that-replaced-a-list | pass | pass | pass | pass | pass | pass | pass | the same shape with a key the object does not have: the list below it is not consulted, a.b is undefined and the optional p is not created |
| 027-nested-object-then-scalar-inside-merge | pass | pass | pass | pass | pass | pass | pass | inside a merge the same rules apply per field: b is an object in the first and a number in the second, so the later value replaces it |
| 028-null-in-second-object-wins | pass | pass | pass | pass | pass | pass | pass | null is a non-object value, so the second object's null is used for b rather than dropped |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-bare-number-is-milliseconds | pass | pass | pass | pass | pass | pass | pass | bare numbers are taken to be in milliseconds already |
| 002-uppercase-unit-is-illegal | pass | pass | pass | pass | pass | pass | pass | the supported unit strings for duration are case-sensitive and must be lowercase, so an uppercase unit is not one of the supported strings |
| 003-nanoseconds-spellings | pass | pass | pass | pass | fail | pass | pass | exactly ns, nano, nanos, nanosecond, nanoseconds are the supported unit strings for nanoseconds |
| 004-microseconds-spellings | pass | pass | pass | pass | fail | pass | pass | exactly us, micro, micros, microsecond, microseconds are the supported unit strings for microseconds |
| 005-milliseconds-spellings | pass | pass | pass | pass | fail | pass | pass | exactly ms, milli, millis, millisecond, milliseconds are the supported unit strings for milliseconds |
| 006-seconds-spellings | pass | pass | pass | pass | fail | pass | pass | exactly s, second, seconds are the supported unit strings for seconds |
| 007-minutes-spellings | pass | pass | pass | pass | fail | pass | pass | exactly m, minute, minutes are the supported unit strings for minutes |
| 008-hours-spellings | pass | pass | pass | pass | fail | pass | pass | exactly h, hour, hours are the supported unit strings for hours |
| 009-days-spellings | pass | pass | pass | pass | fail | pass | pass | exactly d, day, days are the supported unit strings for days in the duration format |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-basename-loads-every-extension | pass | fail | fail | pass | fail | pass | pass | a name without an extension is a basename: all of base.properties, base.json and base.conf exist, so all three are loaded and merged, properties first and HOCON last, so the .conf value wins where they overlap |
| 002-basename-with-only-conf | pass | fail | fail | pass | fail | pass | pass | a basename with only base.conf present loads just that file |
| 003-basename-with-only-properties | pass | fail | fail | pass | fail | pass | pass | a basename with only base.properties present loads it as Java properties, whose values are all strings |
| 004-basename-with-no-file-is-ignored | pass | pass | pass | pass | pass | pass | pass | a basename that exists with no extension at all is a missing include, and a missing include is ignored |
| 005-required-basename-with-no-file-is-error | pass | pass | pass | pass | pass | pass | pass | required() makes the missing basename an error |
| 006-json-extension-means-json-syntax | fail | fail | fail | fail | fail | fail | pass | a file named .json is in JSON format, and an unquoted value is not JSON. typesafe/config parses a .json named outright with the HOCON parser, so the file is accepted; the same file found through a basename is rejectedjava: lenient |
| 007-properties-extension-means-properties-syntax | fail | fail | fail | pass | fail | pass | pass | a file named .properties is in Java properties format, where the first = separates key from value and values are strings. typesafe/config parses a .properties named outright with the HOCON parser and fails on the second =; the same file found through a basename is read as propertiesjava: unsupported |
| 008-properties-keys-are-paths | pass | fail | fail | pass | fail | pass | pass | in a properties file a dotted key is a path, so a.b and a.c build an object at a; the spec's Java properties mapping says the object must always win when a key is both a value and a parent, whatever the order; values are strings |
| 010-json-file-with-every-value-type | pass | fail | fail | pass | fail | pass | pass | a .json file found through a basename is parsed as JSON and contributes its object |
| 011-json-rejects-unquoted-values | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; an unquoted value is a HOCON extension, not JSON |
| 012-json-rejects-substitutions | pass | fail | fail | pass | fail | pass | pass | the .json file is found through a basename, so it is parsed as JSON; ${} is a HOCON extension, not JSON |
| 013-json-rejects-trailing-comma | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; JSON has no trailing commas |
| 014-json-rejects-duplicate-keys | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; the spec's own footing is that duplicate keys are invalid JSON (L194-195), so the second a is an error rather than an override |
| 015-json-array-needs-commas | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; JSON separates array elements with commas only; whitespace does not concatenate or separate |
| 016-json-rejects-empty-document | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; an empty document is not JSON, although it is an empty HOCON object |
| 017-json-rejects-scalar-root | pass | fail | fail | pass | fail | pass | pass | the .json file is found through a basename, so it is parsed as JSON; a JSON document included here must be an object; a bare number is neither an object nor valid as an included root |
| 019-json-key-must-be-a-string | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; a JSON key is a quoted string; a number cannot be a key |
| 020-json-newline-is-not-a-separator | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; in JSON a newline between fields does not stand in for the comma |
| 022-json-dotted-key-is-one-key | pass | fail | fail | pass | fail | pass | pass | the .json file is found through a basename, so it is parsed as JSON; a JSON key is a plain string, not a path expression, so a.b stays a single key |
| 023-json-leading-zero-illegal | fail | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; JSON forbids a leading zero on a number. typesafe/config reads 01 as 1, the same leniency it shows in HOCONjava: lenient |
| 024-unknown-extension-is-not-a-basename | fail | pass | pass | fail | pass | pass | pass | x.txt has an extension, so it is not a basename to probe; it is a file adjacent to the including one and of the same type. The spec does not define what counts as an extension, and this row reads any dotted suffix as one. typesafe/config probes every name that does not end in .conf, .json or .properties as a basename and finds nothingjava: diverges |
| 025-json-is-parsed-after-properties | pass | fail | fail | pass | fail | pass | pass | base.properties and base.json both exist; JSON is parsed next-to-last, after properties, so its value for a wins |
| 026-conf-is-parsed-after-json | pass | fail | fail | pass | fail | pass | pass | base.json and base.conf both exist; HOCON is parsed last, after JSON, so its value for a wins |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-include-string-that-is-a-url-is-a-url | pass | pass | pass | pass | pass | pass | pass | a quoted include name with a known URL scheme is a URL, not a file adjacent to this one. With nothing behind the URL the include is ignored, so a parser that read the string as a filename would also get {}; the row records the rule, it cannot distinguish the two readings |
| 002-nested-include-is-relative-to-the-including-file | pass | pass | pass | pass | pass | pass | pass | inner.conf includes x.conf, which is found beside inner.conf, not beside main.conf and not in the process's working directory |
| 003-plain-name-is-a-file-beside-the-including-one | pass | pass | pass | pass | pass | pass | pass | x.conf is not a URL, so it names a file adjacent to main.conf |
| 004-absolute-path-stays-absolute | pass | pass | pass | pass | pass | pass | pass | an absolute path is loaded as such, so the file of the same name beside main.conf is not it; the absolute file does not exist and the include is ignored |
| 005-file-argument-is-not-relative-to-the-including-file | pass | fail | fail | pass | fail | pass | pass | a file() argument is not interpreted relative to the including file, so the marker beside sub/inner.conf is not loaded; nothing of that name exists elsewhere and the include is ignored |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-included-keys-merge-in-place | pass | pass | pass | pass | pass | pass | pass | the keys from the included object are conceptually substituted for the include statement in the including file, so the included field lands between the fields around it and merges by the usual duplicate-key rules |
| 002-included-conf-array-root-is-error | pass | pass | pass | pass | pass | pass | pass | an included file must contain an object, not an array; if it contains an array as the root value it is invalid and an error should be generated |
| 003-included-json-array-root-is-error | pass | pass | pass | pass | pass | pass | pass | both JSON and HOCON allow arrays as root values in a document, but an included file must contain an object, so a JSON file with an array root is an error when included |
| 004-include-inside-nested-object | pass | pass | pass | pass | pass | pass | pass | the keys from the included root object are conceptually substituted for the include statement in the including file, so an include inside a nested object contributes its keys to that object |
| 005-included-scalar-overrides-earlier-key | pass | pass | pass | pass | pass | pass | pass | if a key in the included object occurred prior to the include statement in the including object, the included key's value overrides the earlier value, exactly as with duplicate keys found in a single file |
| 006-included-object-merges-with-earlier-object | pass | pass | pass | pass | pass | pass | pass | if a key in the included object occurred prior to the include statement and both values are objects, the included value merges with the earlier value, exactly as with duplicate keys found in a single file |
| 007-included-scalar-replaces-earlier-object | pass | pass | pass | pass | pass | pass | pass | an included key's value overrides the earlier value exactly as with duplicate keys in a single file, so an included non-object replaces an earlier object rather than merging with it |
| 008-later-key-overrides-included-key | pass | pass | pass | pass | pass | pass | pass | if the including file repeats a key from an earlier-included object, the including file's value overrides the one from the included file |
| 009-later-object-merges-with-included-object | pass | pass | pass | pass | pass | pass | pass | if the including file repeats a key from an earlier-included object and both values are objects, the including file's value merges with the one from the included file |
| 010-include-inside-an-object-inside-a-list | pass | pass | pass | pass | pass | pass | pass | an include statement may stand wherever a field may, including in an object that is a list element |
| 011-include-inside-a-list-with-a-substitution | fail | pass | pass | pass | pass | pass | pass | the included file may contain substitutions like any other; typesafe/config refuses an include nested in a list when the included file is not fully resolvedjava: unsupported |
| 012-included-merge-stack-over-earlier-object | pass | pass | fail | fail | pass | fail | pass | the included file leaves a as an object merged with a substitution; that whole stack merges with the object the including file set earlier |
| 013-included-merge-stack-over-earlier-substitution | pass | pass | fail | fail | pass | fail | pass | the earlier value is itself a substitution; once everything resolves to objects, all three layers merge |
| 014-included-merge-stack-over-earlier-scalar | pass | pass | fail | pass | pass | pass | pass | the earlier value is a number, so the included object stack replaces it and merges only within itself |
| 015-included-object-on-top-of-substitution-over-earlier-substitution | pass | pass | fail | fail | pass | fail | pass | the same as 013 with the included stack the other way up, an object over a substitution; the result does not depend on which layer is on top |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-missing-include-ignored | pass | pass | pass | pass | pass | pass | pass | by default, if an included file does not exist the include statement is silently ignored, so the keys around it parse as if the line were not there |
| 002-missing-include-is-empty-object | pass | pass | pass | pass | pass | pass | pass | a missing include is treated as if the included file contained only an empty object, so an object whose only content is that include stays an empty object |
| 003-missing-file-include-ignored | pass | pass | pass | pass | pass | pass | pass | the silent-ignore default applies to an explicit file() include as well: a file that does not exist contributes nothing and parsing continues |
| 004-required-missing-is-error | pass | pass | pass | pass | pass | pass | pass | when the included resource name is wrapped with required(), file parsing fails with an error if the resource cannot be resolved |
| 005-required-file-missing-is-error | pass | pass | pass | pass | pass | pass | pass | required() may also wrap the file() form, and a required file that cannot be resolved is a parse error rather than an empty object |
| 006-required-present-is-included | pass | pass | pass | pass | pass | pass | pass | required() only makes the include mandatory; a resource that does resolve is included exactly as an unwrapped include would be |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-substitution-relativized-to-include-scope | pass | pass | fail | pass | fail | pass | pass | a substitution in an included file must be fixed up to be relative to the app's configuration root, so ${x} in a file included at key a becomes ${a.x} |
| 002-relativized-path-sees-later-redefinition | pass | pass | fail | pass | fail | pass | pass | substitution happens after parsing the whole configuration, so the ${x} fixed up to ${a.x} evaluates to the root's later redefinition 42 rather than the included file's 10 |
| 003-original-path-tried-as-fallback | pass | pass | pass | pass | pass | pass | pass | it is not enough to only look up the fixed-up path; when a.x is undefined the original path x is looked up relative to the root of the including configuration |
| 004-relativized-path-tried-before-original | pass | fail | fail | pass | fail | pass | pass | substitutions in included files are looked up first relative to the root of the included file and only second relative to the root of the including configuration, so a.x wins over x |
| 005-neither-path-defined-is-error | pass | pass | pass | pass | pass | pass | pass | only the fixed-up path a.x and the original path x are looked up; with neither defined the substitution is undefined and the configuration is invalid |
| 006-optional-substitution-with-neither-path-is-omitted | pass | pass | pass | pass | pass | pass | pass | an optional substitution in an included file is looked up at the fixed-up path a.x and at the original path x; with neither defined the field is omitted rather than an error |
| 007-nested-include-relativized-through-both-levels | pass | pass | fail | pass | fail | pass | pass | the fix-up is relative to the app's configuration root, so ${x} in a file included at b inside a file included at a becomes ${a.b.x} |
| 008-original-path-falls-back-to-environment | pass | fail | pass | pass | pass | pass | pass | an included file may intend to refer to the application's root config, for example to get a value from the environment; the original path CONFORMANCE_HOME is looked up and falls back to the environment variable |
| 009-undefined-optional-at-fixed-up-path-falls-back | fail | pass | pass | fail | pass | fail | pass | an undefined ${?nope} means common.x.y is never created, so ${x.y} is not found at the fixed-up path and is looked up at the original path x.y. typesafe/config treats the vanished value as a miss that ends the lookup -- except in some root key orders (see 014); lightbend/config#863 falls backjava: unsupported |
| 010-undefined-optional-at-fixed-up-path-with-no-original | pass | pass | pass | fail | pass | fail | pass | with neither common.x.y nor x.y defined, the required ${x.y} in the included file is unresolved |
| 011-optional-reference-falls-back-too | fail | pass | pass | fail | pass | fail | pass | the optional form of 009 falls back the same way; typesafe/config leaves a out instead (lightbend/config#863)java: diverges |
| 012-earlier-value-at-fixed-up-path-wins | pass | fail | fail | pass | fail | pass | pass | an undefined optional that would override keeps the previous value, so common.x.y is 9 and found at the fixed-up path; the original path is not consulted |
| 013-missing-leaf-under-existing-object-falls-back | pass | pass | pass | pass | pass | pass | pass | the metamorphic twin of 009 without the optional: common.x exists, common.x.y does not, so x.y is looked up at the original path |
| 014-unrelated-key-does-not-change-the-result | fail | pass | pass | fail | pass | fail | pass | an unrelated key that reads common.x must not change what common.a resolves to; in typesafe/config this input fails while the same input with the key named o24bbd resolves to 0 (lightbend/config#863)java: unsupported |
| 015-undefined-at-both-paths-is-error | pass | pass | fail | fail | pass | fail | pass | when the original path is itself an undefined optional, neither lookup finds a value and the required ${x.y} is an error |
| 016-substitution-inside-a-concatenation-is-fixed-up | pass | pass | pass | pass | pass | pass | pass | a substitution that is one piece of a concatenation in the included file is fixed up like a bare one: p.x is not set, so the original path x is used |
| 017-substitution-inside-a-list-is-fixed-up | pass | pass | pass | pass | pass | pass | pass | a substitution that is a list element in the included file is fixed up as well |
| 018-substitution-in-a-merge-stack-is-fixed-up | pass | pass | fail | pass | fail | pass | pass | an object merged with a substitution in the included file keeps working after the fix-up, whichever of the two is on top: q is found at p.q |
| 019-null-at-fixed-up-path-is-found | pass | fail | fail | pass | fail | pass | pass | a.x is present, set to null, so the lookup at the fixed-up path succeeds and the original path x is not consulted |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-include-later-in-key-is-literal | pass | pass | pass | pass | pass | pass | pass | unquoted include has no special meaning if it is not the start of a key's path expression; it may appear later in the key, so { foo include : 42 } is equivalent to { "foo include" : 42 } |
| 002-include-as-object-value-is-string | pass | pass | pass | pass | pass | pass | pass | unquoted include has no special meaning if it is not the start of a key's path expression; as an object value it is the string "include" |
| 003-include-as-array-element-is-string | pass | pass | pass | pass | pass | pass | pass | unquoted include has no special meaning if it is not the start of a key's path expression; as an array element it is the string "include". the spec's example is the root array [ include ]; it is wrapped in a field here so the row does not depend on array-root support |
| 004-no-concatenation-on-include-argument | pass | pass | fail | pass | pass | pass | pass | value concatenation is NOT performed on the argument to include; the argument must be a single quoted string, so a second quoted string after the resource name is an error whether or not the first resource exists |
| 005-no-substitution-in-include-argument | pass | pass | pass | pass | pass | pass | pass | no substitutions are allowed in the argument to include, which must be a single quoted string; ${x} after include is an error even though x is defined, so an implementation that resolves the argument first still fails this row |
| 006-include-argument-must-be-quoted | pass | pass | pass | pass | pass | pass | pass | if an unquoted include at the start of a key is followed by anything other than a single quoted string or the url()/file()/classpath() syntax, it is invalid and an error should be generated; the argument may not be an unquoted string |
| 007-quoted-include-is-ordinary-key | pass | pass | pass | pass | pass | pass | pass | only unquoted include is special; quoting it gives a key that starts with the word include, so { "include" : 42 } is an ordinary field named include |
| 008-file-argument-must-be-quoted | pass | pass | pass | pass | pass | pass | pass | the argument inside file() must be a single quoted string; an unquoted name is a syntax error |
| 009-file-missing-close-parenthesis | pass | pass | pass | fail | pass | pass | pass | file("...") must be closed; the line ends before the parenthesis |
| 010-required-missing-close-parenthesis | pass | pass | pass | fail | pass | fail | pass | required(...) must be closed too |
| 011-url-argument-must-be-a-url | fail | fail | fail | fail | fail | fail | fail | the string inside url() is interpreted as a URL, and x.conf has no scheme, so it is not one |
| 013-newlines-between-include-and-name | pass | pass | fail | pass | fail | pass | pass | any amount of whitespace, newlines included, may stand between include and the resource name |
| 014-whitespace-inside-parentheses | pass | fail | pass | pass | pass | pass | pass | whitespace inside the parentheses, outside the quotes, is allowed |
| 015-include-followed-by-separator-is-not-a-key | pass | pass | fail | pass | fail | pass | pass | an unquoted include at the start of a key is never a key; followed by a separator instead of a quoted name it is an include with an invalid argument |
| 016-comma-separates-include-from-next-field | pass | fail | pass | pass | pass | pass | pass | an include stands where a field would, so the usual comma separates it from the field that follows |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-equals-as-separator | pass | pass | pass | pass | pass | pass | pass | the = character can be used anywhere JSON allows :, to separate keys from values |
| 002-omitted-separator-before-object | pass | pass | pass | pass | pass | pass | pass | if a key is followed by {, the : or = may be omitted; the spec's own example is "foo" {} meaning "foo" : {} |
| 003-omitted-separator-unquoted-key | pass | pass | pass | pass | pass | pass | pass | the separator-omission rule before { applies to unquoted keys too, not just the quoted example in the spec |
| 004-value-on-the-line-after-the-separator | pass | pass | pass | pass | pass | pass | pass | = stands wherever JSON allows :, and JSON allows any whitespace, newlines included, between the separator and the value |
| 005-separator-cannot-be-omitted-before-array | pass | pass | pass | pass | pass | pass | pass | the separator may be omitted only before {; a key followed directly by [ is invalid |
| 006-newline-between-key-and-brace | fail | fail | fail | fail | fail | fail | fail | the separator may be omitted before {, and the spec does not say whether a newline may stand between the key and the brace; typesafe/config and pyhocon accept it, hocon-rs rejects it |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-list-from-indexed-variables | pass | fail | fail | pass | fail | pass | pass | the [] suffix collects CONFORMANCE_LIST_0, CONFORMANCE_LIST_1, ... until an index is missing, into a list of strings |
| 002-optional-list-with-no-elements-is-undefined | pass | fail | pass | pass | pass | pass | pass | with no CONFORMANCE_LIST_0 set, an optional ${?X[]} is undefined and the field is not created |
| 003-required-list-with-no-elements-is-error | pass | pass | pass | pass | pass | pass | pass | with no CONFORMANCE_LIST_0 set, a required ${X[]} is an error |
| 004-collection-stops-at-the-first-gap | pass | fail | fail | pass | fail | pass | pass | indices are collected from 0 until one is missing, so with _0 and _2 set the list holds only the first |
| 005-elements-are-strings | pass | fail | fail | pass | fail | pass | pass | an element taken from the environment is a string, so 1 stays "1" |
| 006-config-keys-are-not-collected | pass | fail | pass | pass | pass | pass | pass | the [] suffix collects environment variables only, so the config key a_0 is not gathered and the optional substitution is undefined |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-basic-triple-quote | pass | pass | pass | pass | pass | pass | pass | the three-character sequence """ starts a multi-line string whose unicode characters are used unmodified to create the string value |
| 002-newlines-and-whitespace-preserved | pass | pass | pass | pass | pass | pass | pass | newlines and whitespace inside a multi-line string receive no special treatment and are kept in the value |
| 003-unicode-escape-not-interpreted | pass | pass | pass | pass | pass | pass | pass | unlike JSON quoted strings, a triple-quoted string does not interpret escapes, so the six characters \u0041 stay as written rather than becoming A |
| 004-extra-quote-becomes-part-of-string | pass | pass | pass | pass | pass | pass | pass | any sequence of at least three quotes ends the multi-line string and any extra quotes are part of the string, so HOCON works like Scala's four-character string foo" rather than Python's syntax error |
| 005-five-closing-quotes-two-extra | pass | pass | fail | pass | pass | pass | pass | a closing run of five quotes still only needs three to end the string, so both extra quotes are appended to the string value |
| 006-unterminated-is-illegal | pass | pass | pass | pass | pass | pass | pass | a multi-line string opened with a three-character quote sequence that never reaches a closing """ is not a legal value |
| 007-fewer-than-three-quotes-does-not-close | pass | pass | fail | pass | pass | pass | pass | only a run of at least three quote characters ends the multi-line string, so the single embedded quote pairs around hi do not terminate it early |
| 008-backslash-kept-literally | pass | pass | fail | pass | pass | pass | pass | a backslash sequence inside a triple-quoted string is used unmodified, not as an escape |
| 009-six-quotes-are-an-empty-string | pass | pass | pass | pass | pass | pass | pass | the three opening quotes are followed at once by the closing run, so the string is empty |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-object-with-numeric-keys-stays-object | pass | pass | pass | pass | pass | pass | pass | the conversion should be done lazily when required to avoid a type error, not eagerly anytime an object has numeric keys, so a bare object with numeric keys stays an object |
| 003-concatenation-converts-numeric-object-to-array | pass | fail | fail | pass | fail | pass | pass | the conversion should be done in a concatenation when a list is expected and an object with numeric keys is found, so the trailing object concatenates onto the array as elements x, y |
| 004-concatenation-ignores-non-integer-keys | pass | fail | fail | pass | fail | pass | pass | the conversion should ignore any keys which do not parse as positive integers, so key "foo" is dropped and only "0" contributes to the resulting array |
| 005-empty-object-concatenation-not-converted | pass | pass | pass | pass | pass | pass | pass | the conversion should not occur if the object is empty, so an empty object cannot satisfy a list-expected concatenation and this is illegal |
| 006-object-without-integer-keys-not-converted | pass | pass | pass | pass | pass | pass | pass | the conversion should not occur if the object has no keys which parse as positive integers, so this object cannot satisfy a list-expected concatenation and this is illegal |
| 007-sparse-integer-keys-sorted-and-reindexed | pass | fail | fail | pass | fail | pass | pass | the conversion should sort by the integer value of each key and then build the array; missing indices such as "1" are eliminated rather than left as gaps, so keys "0" and "2" become a two-element array |
| 008-negative-key-ignored-in-conversion | pass | fail | fail | pass | fail | pass | pass | the conversion ignores any keys which do not parse as positive integers, and "-1" is not a positive integer, so it is dropped and only key "0" contributes |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-implicit-root-object | pass | pass | pass | pass | pass | pass | pass | if the file does not begin with [ or {, it is parsed as if enclosed with {} curly braces |
| 002-explicit-root-object-unaffected | pass | pass | pass | pass | pass | pass | pass | the implicit-wrap rule only applies when the file does not begin with [ or {; a file already beginning with { is parsed as ordinary JSON-style object |
| 003-missing-open-brace-with-close-brace-illegal | pass | pass | pass | fail | pass | fail | pass | a HOCON file is invalid if it omits the opening { but still has a closing }, since the curly braces must be balanced |
| 004-empty-file | pass | fail | pass | pass | pass | pass | pass | an empty file does not begin with a square bracket or a curly brace, so it is parsed as if enclosed in {}: the empty object |
| 005-bare-string-root-illegal | pass | pass | pass | pass | pass | pass | pass | the file does not begin with a bracket or brace, so it is wrapped in {} and becomes a key with no separator and no value |
| 006-array-root-unaffected | fail | pass | pass | fail | pass | fail | pass | the implicit {} wrap fires only when the file does not begin with a square bracket or curly brace, so a file starting with [ keeps its array root; the include section says so outright -- 'both JSON and HOCON allow arrays as root values in a document' (the reason an included file may not be one). typesafe/config has no array-rooted config at all and rejects the document as LIST rather than object.java: unsupported |
| 008-empty-array-root | fail | pass | pass | fail | pass | fail | pass | an empty array is still an array root, and the wrap rule looks at the opening bracket, not at whether the document has contentjava: unsupported |
| 009-comment-before-array-root | fail | pass | pass | fail | pass | fail | pass | the {} wrap fires only for a file that does not begin with a bracket or brace, and a leading comment does not make it one -- both typesafe/config and pyhocon read the first token, not the first byte. the spec's wording is loose here; this row records the consensus readingjava: unsupported |
| 010-blank-line-before-array-root | fail | pass | pass | fail | pass | fail | pass | leading whitespace does not trigger the {} wrap either, for the same reason as a leading commentjava: unsupported |
| 011-trailing-array-after-root-array | pass | pass | pass | pass | pass | pass | pass | two arrays side by side concatenate when only non-newline whitespace separates them, which would make this document [1,2,3]; but a document is one array or one object at the root, and nothing in the spec lets a root value be a concatenation of two. typesafe/config, pyhocon and hocon-rs all reject it |
| 012-close-brace-after-value-on-one-line-illegal | pass | pass | pass | fail | pass | fail | pass | a close brace with no open brace is invalid wherever it stands, on the same line as a value as much as on its own |
| 013-open-brace-never-closed-illegal | pass | pass | pass | pass | pass | pass | pass | an opening brace with no closing brace is unbalanced |
| 014-comment-only-file | pass | fail | pass | pass | pass | pass | pass | a file holding only a comment does not begin with a bracket or brace, so it is the empty object |
| 015-bare-number-root-illegal | pass | pass | pass | pass | pass | pass | pass | the file does not begin with { or [, so it is read as if wrapped in braces, and a bare number there is a key with no value |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-quoted-dot-has-no-meaning | pass | pass | pass | pass | pass | pass | pass | a dot inside a quoted string has no special meaning as a path separator, so a quoted "a.b" key is a single-element path |
| 002-mixed-quoted-and-unquoted-path | pass | pass | fail | pass | fail | pass | pass | foo.bar."hello.world" is a path with three elements: foo, bar, and hello.world, since unquoted dots are path separators but a quoted dot is not |
| 003-number-then-unquoted-string | pass | pass | pass | pass | pass | pass | pass | 10.0foo is a number then unquoted string foo, giving the two-element path 10 and 0foo, because a dot inside a number still counts as a path separator |
| 004-unquoted-string-with-dot | pass | pass | pass | pass | pass | pass | pass | foo10.0 is an unquoted string with a dot in it, giving the two-element path foo10 and 0 |
| 005-unquoted-concatenated-with-quoted-number | pass | pass | fail | fail | fail | fail | pass | foo followed by quoted "10.0" is an unquoted then a quoted string which concatenate, giving a single-element path |
| 006-all-numeric-path | pass | pass | pass | pass | pass | pass | pass | dots in numbers count as path separators, so 1.2.3 is the three-element path with elements 1, 2, 3 |
| 008-value-concatenation-keeps-boolean | pass | pass | pass | pass | pass | pass | pass | unlike a path expression, an array or element value consisting of the single value true is a value concatenation and retains its character as a boolean |
| 009-empty-path-element-quoted-is-valid | pass | pass | fail | pass | fail | pass | pass | a path element that is an empty string must be quoted; a."".b is a valid three-element path whose middle element is the empty string |
| 010-consecutive-dots-invalid | pass | pass | pass | pass | fail | pass | pass | an unquoted empty path element is invalid, so a..b must generate an error |
| 011-path-starting-with-dot-invalid | pass | pass | pass | pass | fail | pass | pass | a path that starts with a dot is invalid and should generate an error |
| 012-path-ending-with-dot-invalid | pass | fail | pass | pass | fail | pass | pass | a path that ends with a dot is invalid and should generate an error |
| 015-dotted-path-in-substitution | pass | pass | pass | pass | pass | pass | pass | path expressions appear in substitutions like ${foo.bar}, where the unquoted dot separates foo and bar into two path elements, so the lookup walks key foo then key bar |
| 016-number-keeps-original-text-in-path | pass | pass | pass | pass | pass | pass | pass | a number in a path expression must keep its original string representation as it appeared in the file, so 1.10 is the two-element path 1 and 10, not the 1 and 1 a generic number-to-string conversion would give |
| 017-whitespace-concatenation-in-path | pass | pass | pass | pass | pass | pass | pass | path expressions are syntactically identical to a value concatenation, so the whitespace-separated a b concatenates into one element and the unquoted dot then splits off c, giving the two-element path a b and c |
| 018-quoted-element-in-substitution-path | pass | pass | pass | pass | pass | pass | pass | path expressions appear in substitutions as well as keys, and inside quoted strings a dot has no special meaning there either, so ${foo."bar.baz"} looks up key foo then the single key bar.baz |
| 019-empty-element-in-substitution-path-invalid | pass | pass | fail | pass | fail | pass | pass | an unquoted empty path element is invalid in a substitution just as in a key, so ${a..b} must generate an error rather than collapsing to ${a.b} (a.b is defined so a lenient implementation would produce a value and fail) |
| 020-negative-leading-dot-key-is-a-path | pass | pass | pass | pass | pass | pass | pass | a dot in a key separates path elements whether or not the text around it would be a number, so -.33 names - then 33; numbers in paths keep their original text |
| 021-quoted-dot-in-substitution-path-is-literal | pass | pass | pass | pass | pass | pass | pass | inside quotes a dot is not a separator, so ${"b.c"} names one field called b.c, which does not exist; it does not reach b's field c |
| 022-keyword-in-substitution-path-is-a-string | pass | pass | pass | pass | pass | pass | pass | a path expression is always a string, so true in a substitution names the key true rather than a boolean |
| 023-substitution-in-dotted-key-illegal | pass | pass | pass | pass | pass | pass | pass | a key is a path expression, and a path expression may not contain a substitution, not even as a later element of a dotted key |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-two-element-path-expands | pass | pass | pass | pass | pass | pass | pass | a key that is a multi-element path expands to a nested object per element, the last element combined with the value becomes a field in the most-nested object: foo.bar:42 is equivalent to foo{bar:42} |
| 002-three-element-path-expands | pass | pass | pass | pass | pass | pass | pass | a three-element path key expands through two levels of nesting: foo.bar.baz:42 is equivalent to foo{bar{baz:42}} |
| 003-merge-sibling-paths | pass | pass | pass | pass | pass | pass | pass | the objects created by expanding path keys are merged in the usual way, so a.x:42, a.y:43 is equivalent to a{x:42,y:43} |
| 004-whitespace-in-key | pass | pass | pass | pass | pass | pass | pass | because path expressions work like value concatenations, whitespace is allowed in keys: a b c:42 is equivalent to "a b c":42 |
| 005-unquoted-true-key-becomes-string | pass | pass | pass | pass | pass | pass | pass | path expressions are always converted to strings, so the unquoted boolean-looking key true:42 is "true":42 |
| 006-unquoted-number-key-becomes-string | pass | pass | pass | pass | pass | pass | pass | path expressions are always converted to strings, so the unquoted numeric key 3:42 is "3":42 |
| 007-decimal-key-splits-on-dot | pass | pass | pass | pass | pass | pass | pass | a dot in an unquoted key is a path separator even when it looks like a decimal number, so 3.14:42 is "3":{"14":42} |
| 008-include-cannot-begin-key | fail | pass | fail | pass | fail | pass | pass | the unquoted string include may not begin a path expression in a key: where an object key is expected, include is not read as a key at all and must be followed by a quoted resource name, so include.foo is an error. typesafe/config recognises the keyword only as a bare include token -- include.foo tokenizes as one unquoted string, so it is accepted as the path include.foo, while include = 42 is rejected under the very rule the spec states.java: lenient |
| 009-include-not-at-start-is-ordinary | pass | fail | fail | pass | fail | pass | pass | only the unquoted string include that begins a path expression in a key is special: include later in a key (foo.include) and a quoted "include" beginning one are ordinary path elements |
| 010-path-key-merges-with-brace-object | pass | pass | pass | pass | pass | pass | pass | the object created by expanding a path key is merged in the usual way with an object written in braces at the same key, whichever form comes first: a{x:1}, a.y:2 and b.x:1, b{y:2} are both equivalent to {x:1,y:2} |
| 011-whitespace-around-a-dot-stays-in-the-elements | pass | pass | pass | pass | pass | pass | pass | whitespace inside a key is part of the key, and a dot splits the key into elements, so the elements here are 'a ' and ' b' with their spaces |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-bare-number-is-days | pass | pass | pass | pass | pass | pass | pass | for getPeriod(), bare numbers are taken to be in days, unlike getDuration() where bare numbers are milliseconds |
| 002-uppercase-unit-is-illegal | pass | pass | pass | pass | pass | pass | pass | the supported unit strings for period are case-sensitive and must be lowercase, so an uppercase unit is not one of the supported strings |
| 003-days-spellings | pass | pass | pass | pass | fail | pass | pass | exactly d, day, days are the supported unit strings for days in the period format |
| 004-weeks-spellings | pass | pass | pass | pass | fail | pass | pass | exactly w, week, weeks are the supported unit strings for weeks |
| 005-months-spellings | pass | pass | pass | pass | fail | pass | pass | exactly m, mo, month, months are the supported unit strings for months |
| 006-years-spellings | pass | pass | pass | pass | pass | pass | pass | exactly y, year, years are the supported unit strings for years |
| 007-month-m-ambiguous-with-duration-minutes | pass | pass | pass | pass | fail | pass | pass | the spec notes that getTemporal() callers should prefer mo over m for months, since m is also the duration unit for minutes and getTemporal() may return either a Duration or a Period |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-plus-equals-desugars-to-optional-self-ref-array | pass | pass | fail | pass | fail | pass | pass | a += b transforms into a = ${?a} [b], and because the fallback is optional (${?a} not ${a}) this is legal even as the first mention of a in the file |
| 002-plus-equals-appends-to-existing-array | pass | pass | fail | pass | fail | pass | pass | += appends an element to a previous array |
| 003-plus-equals-errors-on-non-array-previous-value | pass | pass | fail | pass | fail | pass | pass | if the previous value was not an array, += results in an error just as the long form a = ${?a} [b] would with a non-array a |
| 004-plus-equals-twice | pass | pass | fail | pass | fail | pass | pass | each += appends to the value the previous line left, so two of them append two elements |
| 005-plus-equals-in-nested-object-with-substitution | pass | fail | fail | pass | fail | pass | pass | inside an object, += desugars to a self-reference by the field's full path o.a; the appended element may itself be a substitution |
| 006-plus-equals-inside-a-list-element | fail | pass | fail | pass | fail | pass | pass | b has no previous value, so b += 1 is b = ${?b} [1] with the optional part undefined: [1]. typesafe/config refuses += inside a list because its desugared path cannot name a list elementjava: unsupported |
| 007-plus-equals-under-a-dotted-key | pass | fail | fail | pass | fail | pass | pass | the self-reference += desugars to uses the full path of the field, o.p.a, even when the enclosing key was written as the path o.p |
| 008-plus-equals-with-an-object-containing-a-merge | fail | pass | fail | pass | fail | pass | pass | l += v is l = ${?l} [v]; the appended object holds a merge of an object and a substitution, which resolves inside the new list. typesafe/config fails with an internal error (BugOrBroken) for the same reason as array-and-object-concatenation/018java: unsupported |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-isolated-self-reference-is-an-error | pass | pass | pass | pass | pass | pass | pass | in isolation, with no merges involved, a self-referential field is an error because the substitution cannot be resolved |
| 002-self-reference-resolves-to-earlier-merged-value | pass | pass | pass | pass | pass | pass | pass | when foo:${foo} is merged with an earlier value for foo, the self-reference resolves to that overridden value, so foo ends up { a : 1 } |
| 003-self-reference-before-any-value-is-undefined | pass | pass | fail | fail | pass | fail | pass | if the self-referential foo:${foo} comes before foo has any value, the reference is undefined, exactly as if it named a path not found in the document, so it is an error even though foo is given a value afterward |
| 004-optional-self-reference-disappears-silently | pass | pass | pass | pass | pass | pass | pass | because the self-reference error is treated as undefined rather than an intractable cycle, the optional syntax foo:${?foo} makes the field disappear silently instead of erroring |
| 005-substitution-hidden-by-later-non-mergeable-value-is-never-evaluated | pass | pass | pass | pass | pass | pass | pass | if a substitution is hidden by a later non-object value that could not be merged with it, it is never evaluated and no error is reported, no matter what it would have resolved to; foo ends up 42 |
| 006-self-reference-cycle-ignored-when-overridden-by-literal | pass | pass | pass | pass | pass | pass | pass | the same hiding rule applies to a self-reference cycle: once overridden by a non-mergeable literal, the initial foo:${foo} must simply be ignored, so foo ends up 42 with no error |
| 007-self-reference-in-path-expression-resolves-to-value-below | pass | pass | pass | pass | pass | pass | pass | a self-reference resolves to the value below it even as part of a path expression, so ${foo.a} refers to { c : 1 } rather than 2, and the final merge is { a : 2, c : 1 } |
| 008-object-may-refer-to-sibling-path-within-itself | pass | pass | pass | pass | pass | pass | pass | an implementation must allow an object to refer to a path within itself without treating it as a cycle, by resolving only the referenced field rather than recursing the whole enclosing object; bar.baz ends up 42 |
| 009-non-cycling-reference-looks-forward-across-merge | pass | pass | pass | pass | pass | pass | pass | because there is no inherent cycle here, the substitution must look forward including the field's own later merges, so bar.baz ends up 43 once foo is overridden to 43 |
| 010-mutually-referring-objects-are-not-self-referential | pass | pass | pass | pass | pass | pass | pass | mutually-referring objects should work and are not self-referential, so they look forward: bar.a ends up 4 and foo.c ends up 3 |
| 011-optional-self-reference-in-concatenation-looks-back | pass | pass | pass | pass | pass | pass | pass | an optional self-reference in a value concatenation has to look back to an undefined a, so a ends up "foo" rather than "foofoo" |
| 012-mutual-non-self-cycle-is-unresolvable | pass | pass | pass | pass | pass | pass | pass | the spec gives bar:${foo}, foo:${bar} as an example that is not possible to resolve, since neither lazy evaluation nor looking backward breaks the cycle and neither field is optional |
| 013-multi-step-cycle-is-invalid | pass | pass | pass | pass | pass | pass | pass | a multi-step loop across three fields (a:${b}, b:${c}, c:${a}) must also be detected as invalid, not just a direct two-field cycle |
| 014-order-dependent-resolution-must-agree-on-one-value | fail | fail | fail | fail | fail | fail | fail | this case has undefined behavior depending on resolution order: implementations are allowed to set both a and b to 1, both to 2, or to error, but must set both to the same value because substitutions are memoized by instance, so a and b must never diverge (e.g. a=1,b=2 is not permitted) |
| 015-list-hidden-by-substituted-object-is-never-evaluated | fail | pass | pass | fail | pass | fail | pass | a list merges with nothing, so any later value hides it and it is never evaluated; that the hiding object arrives through ${replacement} changes nothing, since the substitution is required and so either resolves or fails on its own. typesafe/config evaluates the hidden list (lightbend/config#865)java: unsupported |
| 016-object-hidden-by-substituted-scalar-is-never-evaluated | pass | pass | pass | pass | fail | pass | pass | ${replacement} resolves to 42, a non-object, so the earlier object is hidden and never evaluated, as with the literal in 005 (typesafe/config 1.4.5 evaluated it) |
| 017-object-hidden-by-substituted-concatenation-is-never-evaluated | pass | pass | pass | pass | fail | pass | pass | a string concatenation is a non-object too, so it hides the earlier object (typesafe/config 1.4.5 evaluated it) |
| 018-object-merged-with-substituted-object-is-evaluated | pass | pass | fail | pass | pass | pass | pass | a substituted object merges with the earlier object, so its fields are part of the result and ${does-not-exist} is an error |
| 019-list-hidden-by-literal-scalar-is-never-evaluated | pass | pass | pass | pass | pass | pass | pass | the literal counterpart of 015: a literal number could not be merged with the list, so the list is never evaluated and there is no error |
| 020-object-hidden-by-literal-list-is-never-evaluated | pass | pass | pass | pass | pass | pass | pass | a literal list is a non-object value, so it hides the earlier object and ${does-not-exist} is never evaluated |
| 021-hidden-list-does-not-depend-on-key-order | fail | pass | pass | fail | pass | fail | pass | a reader of w.x must not change whether the hidden list is evaluated; only objects merge, so the object hides the list whatever else refers to it. typesafe/config fails here, yet resolves the same file with the reader named o24bbd (lightbend/config#865)java: unsupported |
| 022-undefined-optional-does-not-hide | pass | pass | fail | pass | pass | pass | pass | an undefined optional that would override leaves the previous value in place, so the list is the result and ${does-not-exist} is evaluated: an error |
| 023-append-does-not-hide | pass | pass | fail | pass | fail | pass | pass | w += 1 is w = ${?w} [1], which reads the earlier list, so ${does-not-exist} is evaluated: an error |
| 024-substitution-target-is-fetched-lazily | pass | pass | pass | pass | pass | pass | pass | resolving ${b.c} fetches only b.c; resolving all of b first would reach ${a} and report a cycle that is not there |
| 025-optional-two-field-cycle-is-undefined | pass | pass | fail | pass | fail | pass | pass | a genuine cycle is an error for required substitutions; with the optional syntax it is a missing value, so neither field is created |
| 026-memoized-by-instance-not-by-path | pass | pass | pass | pass | pass | pass | pass | the text ${a} appears twice and resolves differently: to x in a's own self-reference and to the final xy in b; memoizing by path instead of by instance would give b the wrong value |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-path-builds-on-older-value | pass | pass | pass | pass | pass | pass | pass | a field may look up its own older value before being overridden with a new value based on it, letting path:${path}":d" extend the earlier path:"a:b:c" |
| 002-self-referential-concatenation | pass | pass | pass | pass | pass | pass | pass | a value concatenation containing a substitution to its own field is self-referential, so ${a} looks back to the earlier x and the result is xbc |
| 003-object-containing-self-reference-is-not-self-referential | pass | pass | pass | pass | pass | pass | pass | an object with a substitution inside it is not considered self-referential for this purpose, so a:{b:${a}} is an unbreakable cycle that must generate an error |
| 004-array-containing-self-reference-is-not-self-referential | pass | pass | pass | pass | pass | pass | pass | an array with a substitution inside it is not considered self-referential for this purpose, so a:[${a}] is an unbreakable cycle that must generate an error |
| 006-self-referential-array-concatenation | pass | pass | pass | pass | pass | pass | pass | the spec lists path:${path} [ /usr/bin ] among the examples of self-referential fields: a value concatenation of a substitution with an array literal is still self-referential |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-bare-number-is-bytes | pass | pass | pass | pass | pass | pass | pass | bare numbers are taken to be in bytes already |
| 002-single-byte-spellings | pass | pass | pass | pass | pass | pass | pass | for single bytes, exactly B, b, byte, bytes are supported |
| 003-powers-of-ten-suffixes | pass | pass | pass | pass | pass | pass | pass | every power-of-10 suffix and its long forms name the same unit family; one table, one rule |
| 004-powers-of-two-suffixes | pass | pass | pass | pass | fail | pass | pass | every power-of-2 suffix and its long forms name the same unit family; one table, one rule. The E and e forms are quoted because an unquoted 5E is a number token that is not a number (see unquoted-strings/022) |
| 019-single-letter-abbreviation-means-powers-of-two | pass | pass | pass | pass | pass | pass | pass | the single-letter abbreviations (like K) are ambiguous between powers of two and ten, and this spec follows the java -Xmx / GNU-tools precedent of mapping them to powers of two |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-substitution-in-string-concatenation | pass | pass | pass | pass | pass | pass | pass | string value concatenation supports substitutions inside the concatenated string |
| 002-only-simple-values-participate | pass | pass | pass | pass | pass | pass | pass | it is invalid for arrays or objects to appear in a string value concatenation, since only simple values participate |
| 003-leading-and-trailing-whitespace-trimmed | pass | pass | pass | pass | pass | pass | pass | whitespace before the first and after the last simple value is discarded; only the whitespace between the values is kept |
| 004-concatenation-never-spans-newline | pass | pass | pass | pass | pass | pass | pass | string value concatenations never span a newline, so a bare word on the following line is not appended to the value above it |
| 005-array-element-string-concatenation | pass | pass | pass | pass | pass | pass | pass | a string value concatenation may appear in any place a string may appear, including array elements |
| 006-boolean-becomes-string-in-concatenation | pass | pass | fail | pass | fail | pass | pass | true and false become the strings "true" and "false" when they participate in a concatenation |
| 007-null-becomes-string-in-concatenation | pass | pass | fail | pass | fail | pass | pass | null becomes the string "null" when it participates in a concatenation |
| 008-number-kept-as-written-in-concatenation | pass | pass | pass | pass | pass | pass | pass | numbers should be kept as they were originally written in the file when they participate in a concatenation, not renormalized |
| 009-single-boolean-not-converted-to-string | pass | pass | pass | pass | pass | pass | pass | a single value is never converted to a string; true by itself must be parsed as a boolean-typed value, not a string |
| 010-object-then-scalar-drops-the-scalar | fail | pass | pass | pass | pass | pass | pass | it is invalid for arrays or objects to appear in a string value concatenation; typesafe/config accepts this one silently and discards the trailing x, losing data with no diagnostic, while erroring on the same pair in the other orderjava: lenient |
| 011-array-then-scalar-drops-the-scalar | fail | pass | pass | pass | pass | pass | pass | the same rule for arrays; typesafe/config again accepts and discards the trailing x, where it errors when the scalar comes firstjava: lenient |
| 012-scalar-then-object-is-an-error | pass | pass | pass | pass | pass | pass | pass | the same rule with the scalar first, which typesafe/config does reject -- the half of the rule it enforces |
| 014-array-then-unquoted-text-without-space | fail | pass | pass | pass | pass | pass | pass | an array may not appear in a string value concatenation, with or without whitespace between them; typesafe/config silently drops suffix (lightbend/config#862)java: lenient |
| 015-object-then-unquoted-text-without-space | fail | pass | pass | pass | pass | pass | pass | the same rule for an object followed directly by text; typesafe/config silently drops suffix (lightbend/config#862)java: lenient |
| 016-text-between-arrays-is-not-dropped | fail | pass | pass | pass | pass | pass | pass | text between two arrays makes a concatenation of array, string and array, which is invalid; typesafe/config drops the text and concatenates the arrays (lightbend/config#862)java: lenient |
| 020-64-bit-number-kept-as-written | pass | pass | pass | pass | pass | pass | pass | a number in a concatenation is stringified as it appeared in the file, whatever its size |
| 021-false-becomes-string-in-concatenation | pass | pass | fail | pass | fail | pass | pass | false becomes the string "false" in a concatenation, as true does |
| 022-quoted-and-unquoted-strings-concatenate | pass | pass | pass | pass | pass | pass | pass | a quoted string is itself in a concatenation, and the whitespace between it and the unquoted string beside it is kept |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-env-fallback-for-undefined-path | pass | pass | pass | pass | pass | pass | pass | a substitution whose path is not defined in the configuration falls back to an environment variable of the same name |
| 002-required-substitution-resolved-from-env | pass | pass | pass | pass | pass | pass | pass | a required ${x} substitution that is not present in the configuration tree is searched for in the environment and resolves to the variable's value instead of being an error |
| 003-config-value-wins-over-env | pass | pass | pass | pass | pass | pass | pass | the environment is only searched when the substitution is not present within the configuration tree, so a value set in the configuration wins over an environment variable of the same name |
| 004-null-in-config-blocks-env-lookup | pass | pass | fail | pass | fail | pass | pass | setting a key to null in the configuration explicitly blocks looking that substitution up in the environment, so ${CONFORMANCE_X} resolves to null even though the variable is set |
| 005-config-lookup-is-case-sensitive | pass | pass | pass | pass | pass | pass | pass | the lookup is case-sensitive all the way until the env variable fallback is reached, so a lowercase key does not shadow an uppercase substitution and the environment variable is used |
| 006-empty-env-var-is-empty-string | pass | pass | pass | pass | pass | pass | pass | an env variable set to the empty string is kept as such, so a required substitution resolves to "" rather than being undefined |
| 007-empty-env-var-creates-optional-field | pass | pass | pass | pass | pass | pass | pass | an env variable set to the empty string is set to empty string rather than undefined, so an optional ${?x} substitution creates the field with "" instead of dropping it |
| 008-env-vars-always-become-strings | pass | pass | pass | pass | pass | pass | pass | environment variables always become a string value, so values that would parse as a number, a boolean or null in HOCON stay the strings "42", "true" and "null" |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-basic-substitution | pass | pass | pass | pass | pass | pass | pass | the syntax ${pathexpression} refers to another part of the configuration tree |
| 002-optional-substitution-syntax | pass | pass | pass | pass | pass | pass | pass | ${?pathexpression} is the optional substitution syntax; when the path is defined it resolves exactly like ${pathexpression}, keeping the value and its type |
| 003-optional-substitution-whitespace-illegal | pass | pass | pass | pass | pass | pass | pass | the ? in ${?pathexpression} must not have whitespace before it; the three characters ${? must be exactly grouped together |
| 004-substitution-not-parsed-in-quoted-string | pass | pass | pass | pass | pass | pass | pass | substitutions are not parsed inside quoted strings, so this stays the literal text |
| 005-substitution-value-concatenation-unquoted | pass | pass | pass | pass | pass | pass | pass | worked example: to get a string containing a substitution, use value concatenation with the substitution in the unquoted portion |
| 006-substitution-value-concatenation-quoted-tail | pass | pass | pass | pass | pass | pass | pass | worked example: quoting the non-substitution portion also forms a value concatenation with the substitution |
| 007-substitution-path-absolute | pass | pass | pass | pass | pass | pass | pass | substitutions are resolved by looking up the path from the root configuration object, absolute rather than relative, so ${a} inside b finds the root a and not the sibling b.a |
| 008-substitution-forward-reference | pass | pass | pass | pass | pass | pass | pass | substitution processing happens as the last parsing step, so a substitution can look forward in the configuration |
| 009-substitution-latest-duplicate-key | pass | pass | pass | pass | pass | pass | pass | if a key has been specified more than once, a substitution referring to it always evaluates to its latest-assigned value |
| 010-substitution-latest-duplicate-key-object-merge | pass | pass | pass | pass | pass | pass | pass | a substitution to a key set more than once evaluates to the merged object, not an earlier snapshot |
| 011-undefined-required-substitution-is-error | pass | pass | pass | pass | pass | pass | pass | an undefined substitution with the ${foo} syntax is invalid and should generate an error |
| 012-undefined-optional-substitution-field-not-created | pass | pass | pass | pass | pass | pass | pass | if an undefined ${?foo} substitution is the value of an object field, the field should not be created |
| 013-undefined-optional-substitution-keeps-previous-value | pass | pass | fail | pass | pass | pass | pass | if an undefined ${?foo} would override a previously-set value for the same field, the previous value remains |
| 014-undefined-optional-substitution-array-element-not-added | pass | pass | pass | pass | pass | pass | pass | if an undefined ${?foo} substitution is an array element, the element should not be added |
| 015-undefined-optional-substitution-concatenation-string-empty | pass | pass | pass | pass | pass | pass | pass | an undefined ${?foo} in a value concatenation with another string becomes an empty string |
| 016-undefined-optional-substitution-concatenation-object-empty | pass | pass | pass | pass | pass | pass | pass | an undefined ${?foo} in a value concatenation with an object becomes an empty object |
| 017-undefined-optional-substitution-concatenation-array-empty | pass | pass | pass | pass | pass | pass | pass | an undefined ${?foo} in a value concatenation with an array becomes an empty array |
| 018-two-optional-substitutions-both-undefined-field-not-created | pass | pass | pass | pass | pass | pass | pass | foo : ${?bar}${?baz} avoids creating the field only if both bar and baz are undefined |
| 019-two-optional-substitutions-one-defined-field-created | pass | pass | pass | pass | pass | pass | pass | foo : ${?bar}${?baz} creates the field once at least one of bar or baz is defined |
| 020-substitution-not-allowed-in-key | pass | pass | pass | pass | pass | pass | pass | substitutions are only allowed in field values and array elements, not in keys |
| 021-substitution-not-allowed-nested-in-path-expression | pass | pass | pass | pass | pass | pass | pass | substitutions are not allowed nested inside other substitutions (path expressions) |
| 022-substitution-preserves-type-array | pass | pass | pass | pass | pass | pass | pass | a substitution is replaced with any value type; if it is the only part of a value, the type (here an array) is preserved |
| 024-substitution-concatenation-forms-string | pass | pass | pass | pass | pass | pass | pass | when a substitution is not the only part of a value, it is value-concatenated to form a string rather than keeping the substituted type |
| 025-substitution-preserves-type-string-booleans-null | pass | pass | fail | pass | fail | pass | pass | a substitution is replaced with any value type and, when it is the only part of a value, the type is preserved: here string, true, false and null stay what they were rather than becoming strings |
| 026-unterminated-substitution-is-error | pass | pass | pass | pass | pass | pass | pass | the syntax is ${pathexpression}: a substitution opened with ${ and never closed with } is not that syntax, and $ cannot start an unquoted string either, so it must be rejected |
| 028-optional-substitution-to-null-value-is-defined | pass | pass | fail | pass | fail | pass | pass | a substitution is undefined only if it does not match any value present in the configuration, and null is among the value types a substitution is replaced with, so ${?a} with a = null is defined: the field is created with null rather than omitted |
| 030-quoted-key-is-not-a-substitution | pass | pass | pass | pass | pass | pass | pass | substitutions are not parsed inside quoted strings, and a quoted key is a quoted string: "${a}" is the literal key ${a}, not a substitution in a key (which would be illegal) |
| 031-whitespace-inside-braces-is-trimmed | pass | fail | pass | pass | fail | pass | pass | the path inside ${} has the syntax of an object key, and whitespace before the first and after the last element of a key is discarded, so ${ b } is ${b} |
| 032-empty-substitution-illegal | pass | pass | pass | pass | pass | pass | pass | ${} contains no path expression at all, so it is neither a substitution nor anything else |
| 033-path-through-a-non-object-is-undefined | pass | fail | pass | pass | pass | pass | pass | a is a number, so a.x names nothing; a required substitution to an undefined path is an error |
| 034-optional-path-through-a-non-object-is-omitted | pass | fail | pass | pass | fail | pass | pass | the same path with the optional form: undefined, so the field is not created |
| 035-quoted-path-element-with-space | pass | pass | pass | pass | pass | pass | pass | a substitution path has the syntax of an object key, so a quoted element may contain a space and names the field written the same way |
| 036-empty-quoted-path-element | pass | pass | fail | pass | fail | pass | pass | an empty path element is legal when quoted, in a substitution as in a key |
| 037-path-into-a-list-element-is-undefined | pass | fail | pass | pass | fail | pass | pass | a path names object fields only, so b.0 does not reach into the list b even though it has an element at index 0; the optional substitution is undefined and a is not created. The list is still unresolved when the lookup happens, which is what makes an implementation look inside it |
| 038-whitespace-after-question-mark | pass | fail | pass | pass | fail | pass | pass | only whitespace before the ? is forbidden; after it the path follows and its leading whitespace is discarded as in any key |
| 039-undefined-optional-with-number-is-a-string | fail | fail | pass | fail | fail | fail | pass | ${?x}42 is a concatenation of two parts, so the value is a string: the undefined optional becomes empty and the number keeps its written form. The spec states the empty-string rule for a concatenation 'with another string', so this row extends it to a number; typesafe/config instead drops the undefined part and keeps the lone number as a numberjava: diverges |
| 040-null-blocks-environment-lookup | pass | pass | fail | pass | fail | pass | pass | the configuration sets CONFORMANCE_NULLED to null, so the substitution resolves to that null and the environment variable of the same name is never consulted |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-quoted-string-json-format | pass | pass | fail | pass | fail | pass | pass | quoted strings are in the same format as JSON strings, so JSON escapes like \n, \t, \", \\ and \u unicode escapes are honored |
| 002-string-value-type | pass | pass | pass | pass | pass | pass | pass | string is one of the possible value types |
| 003-number-value-type | pass | pass | pass | pass | pass | pass | pass | number is one of the possible value types |
| 004-object-value-type | pass | pass | pass | pass | pass | pass | pass | object is one of the possible value types |
| 005-array-value-type | pass | pass | pass | pass | pass | pass | pass | array is one of the possible value types |
| 006-boolean-value-type | pass | pass | pass | pass | pass | pass | pass | boolean is one of the possible value types |
| 007-null-value-type | pass | pass | pass | pass | pass | pass | pass | null is one of the possible value types |
| 008-number-format-decimal-and-exponent | pass | pass | pass | pass | pass | pass | pass | allowed number formats match JSON, including negative numbers, decimals, and exponent notation |
| 009-number-leading-zero-illegal | fail | fail | fail | fail | fail | fail | pass | allowed number formats match JSON, and JSON forbids a zero followed by further digits; the initial number character plus the valid-in-JSON number characters after it must be parsed as a number value, so the whole token 01 has to be a number and is not one, with no fallback offered. typesafe/config parses it leniently as the number 1.java: lenient |
| 010-negative-number-without-integer-part-illegal | fail | fail | fail | fail | fail | fail | pass | allowed number formats match JSON, and JSON requires a digit before the decimal point, so -.33 is a number token that is not a valid number. typesafe/config releases up to 1.4.9 parse it as -0.33; upstream main since lightbend/config#861 (unreleased) reads it as the unquoted string "-.33", like .33 -- still accepted, still not the error the hyphen rule asks forjava: lenient |
| 011-negative-exponent-number-without-integer-part-illegal | fail | pass | fail | fail | fail | fail | pass | the same JSON rule with an exponent: -.33e+1 has no integer part. typesafe/config releases up to 1.4.9 parse it as -3.3; upstream main since lightbend/config#861 (unreleased) rejects it, on the reserved +java: lenient |
| 012-number-with-trailing-dot-illegal | fail | fail | fail | fail | fail | fail | pass | JSON requires a digit after the decimal point; 1. is a number token that is not a valid number. typesafe/config reads it as 1, and lightbend/config#861 leaves it so on purposejava: lenient |
| 013-trailing-dot-before-exponent-illegal | fail | fail | fail | fail | fail | fail | pass | the same rule with an exponent after the dot; typesafe/config reads it as -1000 (named out of scope in lightbend/config#861)java: lenient |
| 014-negative-leading-zeros-illegal | fail | fail | fail | fail | fail | fail | pass | JSON forbids leading zeros after the minus sign as well; typesafe/config reads it as -33 (named out of scope in lightbend/config#861)java: lenient |
| 015-remaining-json-escapes | pass | pass | fail | pass | fail | pass | pass | the JSON escapes \/ \b \f and \r are valid in a quoted string and mean slash, backspace, form feed and carriage return |
| 016-unknown-escape-illegal | pass | pass | fail | pass | fail | pass | pass | only the JSON escape sequences exist; \q is not one of them, so the string is invalid |
| 017-non-hex-unicode-escape-illegal | pass | pass | fail | pass | fail | pass | pass | \u must be followed by exactly four hex digits; zzzz are not hex digits |
| 018-unterminated-quoted-string-illegal | pass | pass | pass | pass | pass | pass | pass | a quoted string that is still open at the end of the file is invalid |
| 019-backslash-at-end-of-file-illegal | pass | pass | pass | pass | pass | pass | pass | a backslash with nothing after it is not an escape and the string is still open at the end of the file |
| 020-unicode-escape-cut-off-by-end-of-file-illegal | pass | pass | pass | pass | pass | pass | pass | the file ends after two of the four hex digits a \u escape requires |
| 021-raw-newline-in-quoted-string-illegal | pass | pass | fail | pass | pass | pass | pass | JSON forbids unescaped control characters in strings, and a raw newline is one; a quoted string cannot span lines |
| 022-raw-tab-in-quoted-string-illegal | pass | pass | fail | fail | fail | fail | pass | a raw tab is a control character too, so it must be written as \t inside quotes |
| 023-64-bit-integer | pass | pass | pass | pass | pass | pass | pass | a number beyond 32 bits is still a number and keeps its exact value |
| 024-raw-utf8-in-quoted-string | pass | pass | pass | pass | fail | pass | pass | the file is UTF-8, so a multi-byte sequence is one character |
| 025-nan-is-not-a-number | pass | fail | pass | pass | pass | pass | pass | NaN is not a JSON number, so the bare token is an unquoted string |
| 026-surrogate-pair-escape | pass | pass | fail | pass | fail | fail | pass | JSON writes a character outside the BMP as two \u escapes forming a surrogate pair, which decode to one character |
| 027-unpaired-surrogate-escape-is-kept | pass | pass | fail | pass | fail | fail | pass | quoted strings are JSON strings, and JSON's grammar accepts any \uXXXX escape, an unpaired surrogate included, so the string holds that one code unit |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-number-value-is-default-unit | pass | pass | pass | pass | pass | pass | pass | if the value is a number, it is taken to be a number in the default unit |
| 002-quoted-string-with-no-unit-uses-default-unit | pass | pass | pass | pass | pass | pass | pass | a string value with no unit name should be interpreted with the default unit, as if it were a number |
| 003-string-with-unit-name-specifies-interpretation | pass | pass | pass | pass | fail | pass | pass | a string value with a unit name has that name specify the value's interpretation |
| 004-whitespace-optional-around-number-and-unit | pass | pass | pass | pass | pass | pass | pass | the units-format grammar is optional whitespace, a number, optional whitespace, an optional unit name, optional whitespace |
| 005-unit-name-must-be-letters-only | pass | pass | pass | pass | pass | pass | pass | the unit name consists only of letters (Unicode L* categories, Java isLetter()), so a unit name with a trailing digit is not a legal units-format string |
| 006-unit-before-number-is-illegal | pass | pass | pass | pass | pass | pass | pass | the units-format grammar puts the number before the unit name, not after, so a unit-then-number string is not a legal units-format value |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-simple-unquoted-string | pass | pass | pass | pass | pass | pass | pass | a sequence of characters outside a quoted string is a string value when it contains none of the forbidden characters |
| 002-quoted-alternative-for-forbidden-char | pass | pass | pass | pass | pass | pass | pass | a quoted string may always be used as an alternative to write a character that is not permitted in an unquoted string, such as '@' |
| 003-unquoted-cannot-contain-at-sign | pass | pass | fail | pass | pass | pass | pass | '@' is one of the listed forbidden characters, so it cannot appear inside an unquoted string |
| 004-double-slash-starts-comment | pass | pass | fail | pass | fail | pass | pass | the two-character string '//' starts a comment, ending the unquoted string before it even mid-token |
| 005-single-slash-allowed | pass | pass | pass | pass | pass | pass | pass | only the two-character string '//' is special for comments; a lone '/' is not a forbidden character and stays part of the unquoted string |
| 006-truefoo-parses-as-true-then-foo | pass | fail | pass | pass | pass | pass | pass | worked example: truefoo parses as the boolean token true followed by the unquoted string foo, because its initial characters parse as true |
| 007-footrue-is-one-unquoted-string | pass | pass | pass | pass | pass | pass | pass | worked example: footrue does not begin with true, false, null, or a number, so it parses as a single unquoted string |
| 008-number-then-suffix-string | pass | pass | pass | pass | pass | pass | pass | worked example: 10.0bar is the number 10.0 followed by the unquoted string bar, since 10.0 parses as a number at the start |
| 009-digits-not-at-start-stay-string | pass | pass | pass | pass | pass | pass | pass | worked example: bar10.0 does not begin with a digit, so the whole thing is the single unquoted string bar10.0, not a number |
| 010-embedded-null-not-recognized | pass | pass | pass | pass | pass | pass | pass | embedded (non-initial) null, true, false, and numbers are not recognized as such: they are just part of the string, since only the leading characters are checked |
| 011-unquoted-cannot-contain-backslash | pass | pass | fail | pass | fail | pass | pass | backslash is a forbidden character, and unquoted strings support no escaping at all, so a literal backslash requires a quoted string instead |
| 012-unquoted-cannot-start-with-hyphen | fail | fail | fail | fail | fail | fail | pass | an unquoted string may not begin with a hyphen, and the initial number character plus any valid-in-JSON number characters that follow must be parsed as a number value -- here that is the lone '-', which is not a number. typesafe/config falls back to unquoted text and yields the string '-foo', the same leniency it shows for '1e'.java: lenient |
| 013-negative-number-then-string | pass | pass | pass | pass | pass | pass | pass | a hyphen followed by digits begins number parsing (-1), after which foo continues as a separate unquoted string token, mirroring the 10.0bar example for negative numbers |
| 014-hash-starts-comment-not-forbidden-char-in-string | pass | pass | fail | pass | pass | pass | pass | '#' is forbidden inside an unquoted string because it already has meaning in HOCON: it starts a comment |
| 015-reserved-forbidden-char-no-current-meaning | pass | pass | fail | pass | pass | pass | pass | some forbidden characters, such as '!', have no meaning in HOCON today and are reserved as keywords for future extensions to the spec |
| 016-control-char-allowed-unquoted | pass | pass | pass | pass | pass | pass | pass | unquoted strings place no restriction on control characters, other than the forbidden characters listed |
| 017-control-char-forbidden-in-quoted | pass | pass | fail | fail | fail | fail | pass | quoted JSON strings may not contain control characters, per the JSON spec, unlike unquoted strings |
| 018-leading-dot-is-not-a-number | pass | pass | fail | pass | fail | pass | pass | only 0-9 and - begin a number, so .33 is an ordinary unquoted string -- the reading lightbend/config#861 extends to -.33 |
| 019-dollar-must-start-a-substitution | pass | pass | fail | pass | pass | pass | pass | $ is a forbidden character in an unquoted string, and outside quotes it may only begin ${; $foo is neither |
| 020-plus-must-be-followed-by-equals | pass | pass | fail | pass | pass | pass | pass | + is a forbidden character in an unquoted string and outside quotes only forms the += separator; a + with nothing after it, here at the end of the file, is invalid |
| 021-plus-inside-a-number-illegal | pass | pass | fail | pass | fail | pass | pass | 1+2 begins like a number and is not one; + is a forbidden character, so the token cannot fall back to an unquoted string either |
| 022-digit-then-exponent-letter-illegal | fail | fail | fail | fail | fail | fail | pass | 1e begins with a digit, so the run of number characters 1e must be parsed as a number, and it is not one; typesafe/config falls back to the unquoted string "1e", the same leniency it shows for -foojava: lenient |
| 023-unquoted-string-ends-at-end-of-file | pass | pass | pass | pass | pass | pass | pass | an unquoted string runs to a forbidden character or whitespace, and the end of the file ends it as well; no final newline is needed |
| 024-escape-sequence-in-unquoted-string-illegal | pass | pass | fail | pass | fail | pass | pass | unquoted strings support no escaping, so \u00e9 is not decoded, and the backslash it needs is forbidden in an unquoted string; the quoted "caf\u00e9" is how to write it |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-simple-values-concatenate-to-string | pass | pass | pass | pass | pass | pass | pass | if all the values are simple values they are concatenated into a string |
| 004-string-concatenation-allowed-in-field-keys | pass | pass | pass | pass | pass | pass | pass | string value concatenation is allowed in field keys, in addition to field values and array elements |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-nonbreaking-space-as-whitespace | pass | pass | fail | pass | fail | pass | pass | a nonbreaking space (0x00A0) is a Unicode Zs separator and must be treated as whitespace |
| 002-bom-at-start-is-whitespace | pass | pass | fail | pass | fail | pass | pass | the BOM (0xFEFF) must be treated as whitespace, not as part of the key |
| 003-unicode-line-paragraph-separator | pass | pass | fail | fail | pass | fail | pass | the line and paragraph separators U+2028 and U+2029 are whitespace but not newlines, so they do not separate two fields: 1 b concatenates and the second = is a stray token |
| 004-vertical-tab-form-feed-whitespace | pass | pass | fail | pass | fail | pass | pass | vertical tab (0x000B) and form feed (0x000C) are whitespace characters |
| 005-ascii-separator-control-chars | pass | pass | pass | fail | pass | fail | pass | the ASCII separator controls 0x1C-0x1F are whitespace but not newlines, so they do not separate two fields |
| 006-special-nonbreaking-spaces | pass | pass | fail | pass | fail | pass | pass | the spec names figure space (0x2007) and narrow no-break space (0x202F) explicitly among the nonbreaking spaces treated as whitespace |
| 007-tab-is-whitespace | pass | pass | pass | pass | pass | pass | pass | a tab is whitespace around a key and a separator |
| 008-crlf-line-endings | pass | pass | pass | pass | pass | pass | pass | a carriage return is whitespace and the line feed beside it is the newline, so CRLF line endings separate fields; if the CR were text the first value would be "1\r" |
| 009-lone-carriage-return-is-not-a-newline | pass | fail | fail | fail | fail | fail | pass | a lone carriage return is whitespace but not a newline, so 1 b concatenates and the second = is a stray token |
| 010-line-separator-is-whitespace | pass | pass | fail | pass | fail | pass | pass | U+2028 around the separator is whitespace, not part of the key or the value |
| 011-ascii-separator-controls-are-whitespace | pass | pass | fail | pass | fail | pass | pass | the file, group, record and unit separators 0x1C-0x1F are whitespace around keys and separators |
| 012-ideographic-space-is-whitespace | pass | pass | fail | pass | fail | pass | pass | the ideographic space U+3000 is a Zs space separator and so whitespace, like every member of the category |
| 013-bom-inside-the-document-is-whitespace | pass | pass | fail | pass | fail | pass | pass | the BOM is whitespace wherever it appears, not only as a file-start marker |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-arrays-concatenate | pass | pass | pass | pass | pass | pass | pass | arrays can be concatenated with arrays when only non-newline whitespace separates them |
| 002-objects-concatenate | pass | pass | pass | pass | pass | pass | pass | objects can be concatenated with objects when only non-newline whitespace separates them |
| 003-mixing-array-and-object-is-error | pass | pass | pass | pass | pass | pass | pass | it is an error to concatenate an array with an object |
| 004-substitution-counts-as-array-for-concatenation | pass | pass | pass | pass | pass | pass | pass | for purposes of concatenation a substitution that resolves to an array also means an array, so ${x} [ 1, 2 ] with x an array concatenates into one array |
| 005-substitution-counts-as-object-for-concatenation | pass | pass | pass | pass | pass | pass | pass | for purposes of concatenation a substitution that resolves to an object also means an object, so ${x} { c : 1 } with x an object merges into one object |
| 006-newline-between-values-prevents-concatenation | pass | pass | pass | pass | pass | pass | pass | newlines between a substitution and an array prevent concatenation, so inside an array they stay two separate elements instead of one concatenated array |
| 007-object-concatenation-merges-and-second-overrides | pass | pass | pass | pass | pass | pass | pass | for objects, concatenation means merging, and the second object overrides the first on overlapping keys |
| 008-array-cannot-be-field-key | pass | pass | pass | pass | pass | pass | pass | arrays cannot be field keys; nested inside an object so the rejection comes from the key position, not from the document root |
| 009-object-cannot-be-field-key | pass | pass | pass | pass | pass | pass | pass | objects cannot be field keys; nested inside an object so the rejection comes from the key position, not from the document root |
| 010-worked-example-object-inheritance | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: object concatenation with a substitution is a common way to express inheritance |
| 011-worked-example-array-path-append | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: array concatenation with a substitution is a common way to add to paths |
| 012-worked-example-self-referential-array-concat | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: a later array definition can concatenate a substitution referring to its own earlier value with a new array |
| 013-newlines-within-value-still-allow-concatenation | pass | pass | pass | pass | pass | pass | pass | newlines may occur within an array without preventing it from concatenating with a second array on the same line as its closing bracket |
| 014-substitution-object-with-literal-array-is-error | pass | pass | pass | pass | pass | pass | pass | a substitution that resolves to an object means an object for concatenation, and concatenating an object with an array is an error |
| 015-substitution-array-with-literal-object-is-error | pass | pass | pass | pass | pass | pass | pass | a substitution that resolves to an array means an array for concatenation, and concatenating an array with an object is an error |
| 016-mixing-object-then-array-is-error | pass | pass | pass | pass | pass | pass | pass | it is an error to concatenate an object with an array in this order too, not only array then object |
| 017-newline-between-arrays-in-field-value-prevents-concatenation | pass | pass | pass | pass | pass | pass | pass | a newline between two arrays in a field value prevents concatenation, so the second array begins a new field, and an array cannot be a field key |
| 018-object-with-inner-merge-concatenated-with-substitution | fail | pass | pass | pass | pass | pass | pass | an object whose field a is itself a merge of an object and a substitution concatenates with a substituted object like any other: the two merge. typesafe/config fails with an internal error (BugOrBroken) because the concatenation is not registered as the merge's parent; the same objects written as literals concatenate finejava: unsupported |
| 019-self-referential-object-concatenation-with-inner-merge | fail | pass | pass | pass | pass | pass | pass | ${a} in a's own value refers to the earlier a, an object, so the concatenation merges {p: ...} with {q: 1}; the inner p is itself a merge of an object and a substitution. typesafe/config fails with an internal error (BugOrBroken) when the merge inside a concatenation piece resolves, as in 018java: unsupported |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-newlines-instead-of-commas | pass | pass | pass | pass | pass | pass | pass | arrays allow newlines to separate elements instead of commas |
| 002-whitespace-instead-of-comma-concatenates-into-one-element | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: non-newline whitespace instead of a comma produces concatenation, so this array has one element, the string 1 2 3 4, not four integers |
| 003-newline-separates-two-arrays-into-elements | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: a newline between two bracketed arrays makes them two separate elements |
| 004-same-line-whitespace-concatenates-two-arrays-into-one-element | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: whitespace on the same line between two bracketed arrays concatenates them into a single element, the array [1, 2, 3, 4] |
| 005-unquoted-string-concatenation-with-substitutions-in-array | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: unquoted words and substitutions separated by whitespace concatenate into one string element, and only the comma separates elements |
| 006-two-elements-each-a-substitution-concatenation | pass | pass | pass | pass | pass | pass | pass | the spec's worked example: whitespace between two substitutions concatenates them into one element, and the comma separates the two concatenations |
| 007-whitespace-never-separates-fields | pass | pass | fail | fail | pass | fail | pass | non-newline whitespace is never a field separator, so two field definitions cannot be written on one line separated only by spaces |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-array-trailing-comma | pass | pass | pass | pass | pass | pass | pass | the last element in an array may be followed by a single comma, which is ignored: [1,2,3,] and [1,2,3] are the same array |
| 002-array-newline-no-comma | pass | pass | pass | pass | pass | pass | pass | array elements need not have a comma between them as long as an ASCII newline separates them: [1\n2\n3] and [1,2,3] are the same array |
| 003-array-double-trailing-comma-invalid | pass | pass | fail | pass | fail | pass | pass | [1,2,3,,] is explicitly called invalid because it has two trailing commas |
| 004-array-initial-comma-invalid | pass | pass | fail | pass | fail | pass | pass | [,1,2,3] is explicitly called invalid because it has an initial comma |
| 005-array-double-comma-invalid | pass | pass | fail | pass | fail | pass | pass | [1,,2,3] is explicitly called invalid because it has two commas in a row |
| 006-object-trailing-comma | pass | pass | pass | pass | pass | pass | pass | these same comma rules apply to fields in objects, so a single trailing comma after the last field is ignored |
| 007-object-newline-no-comma | pass | pass | pass | pass | pass | pass | pass | fields in objects need not have a comma between them as long as an ASCII newline separates them, same as array elements |
| 008-object-double-comma-invalid | pass | pass | fail | pass | fail | pass | pass | these same comma rules apply to fields in objects, so two commas in a row between fields is invalid just as in an array |
| 009-fields-on-one-line-need-a-comma | pass | pass | fail | fail | pass | fail | pass | two fields need a comma or a newline between them; on one line with neither, 1 b becomes a value concatenation and the second = is a stray token |
| 011-trailing-comma-at-implicit-root | pass | pass | pass | pass | pass | pass | pass | the implicit root object follows the object comma rules, so its last field may carry a single trailing comma |
| 012-leading-comma-at-implicit-root-illegal | pass | pass | fail | pass | fail | pass | pass | an initial comma is invalid in an object, the implicit root included |
| 013-double-trailing-comma-in-object-illegal | pass | pass | fail | pass | fail | pass | pass | the array comma rules apply to object fields: two trailing commas are invalid |
| 014-leading-comma-in-object-illegal | pass | pass | fail | pass | fail | pass | pass | the array comma rules apply to object fields: an initial comma is invalid |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-hash-comment | pass | pass | pass | pass | pass | pass | pass | anything between # and the next newline is a comment and ignored |
| 002-double-slash-comment | pass | pass | pass | pass | pass | pass | pass | anything between // and the next newline is a comment and ignored |
| 003-whole-line-comment | pass | pass | pass | pass | pass | pass | pass | a comment may occupy an entire line by itself, ignored just like a trailing one |
| 004-comment-stops-at-newline | pass | pass | pass | pass | pass | pass | pass | a comment runs only to the next newline, so the following line is parsed normally |
| 005-hash-inside-quoted-string-is-not-a-comment | pass | pass | pass | pass | pass | pass | pass | a # inside a quoted string is not treated as starting a comment |
| 006-double-slash-inside-quoted-string-is-not-a-comment | pass | pass | pass | pass | pass | pass | pass | a // inside a quoted string is not treated as starting a comment |
| 007-comment-between-separator-and-value | pass | pass | pass | pass | pass | pass | pass | a comment runs to the end of its line and is then over; the value may follow on the next line just as it may after a bare = |
| 008-comment-then-blank-line-then-field | pass | pass | pass | pass | pass | pass | pass | a comment separated from the following field by a blank line is ignored like any other comment |
| 009-comment-inside-object | pass | pass | pass | pass | pass | pass | pass | comments and blank lines inside an object body are ignored |
| 010-comments-inside-array | pass | pass | pass | pass | pass | pass | pass | comments and blank lines between array elements are ignored, and do not join or separate elements |
| 011-comment-before-values-of-every-type | pass | pass | pass | pass | pass | pass | pass | a comment before a field is ignored whatever type its value has |
| 012-comment-before-explicit-root-object | pass | pass | pass | pass | pass | pass | pass | a comment and a blank line before an explicit root object are skipped before the implicit-braces rule looks at the first token; the spec's wording ("does not begin with") is loose here and this row records the consensus reading, as omit-root-braces/009 does |
| 013-line-comment-after-array | pass | pass | pass | pass | pass | pass | pass | a // comment after an array is not text in a concatenation; the value is just the array |
| 014-hash-comment-after-array-without-space | pass | pass | pass | pass | pass | pass | pass | a # comment directly after ] is a comment, not text in a concatenation |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-unquoted-whitespace-between-substitutions-is-significant-for-strings | pass | pass | pass | pass | pass | pass | pass | when the substitutions turn out to be strings, the unquoted whitespace between them is significant and survives into the concatenated string |
| 002-quoted-whitespace-between-substitutions-is-error | pass | pass | pass | pass | pass | pass | pass | between substitutions that resolve to objects, unquoted whitespace is ignored but quoted whitespace should be an error |
| 003-unquoted-whitespace-between-object-and-list-substitutions-is-ignored | pass | pass | pass | pass | pass | pass | pass | unquoted whitespace between substitutions that resolve to objects or lists must be ignored, so they merge and concatenate as if adjacent |
| 004-tab-between-list-substitutions-is-ignored | pass | pass | pass | pass | pass | pass | pass | whitespace between two list substitutions is ignored, tabs included, and the lists concatenate |
| 005-nonbreaking-space-between-list-substitutions-is-ignored | pass | pass | pass | pass | fail | pass | pass | U+00A0 is a Zs space and so whitespace in HOCON; between two list substitutions it is ignored (lightbend/config#862 review) |
| 006-bom-between-list-substitutions-is-ignored | pass | pass | fail | pass | fail | pass | pass | the BOM must be treated as whitespace, so between two list substitutions it is ignored |
| 007-zero-width-space-between-list-substitutions-is-error | fail | pass | pass | pass | pass | pass | pass | U+200B is format (Cf), not a space separator, so it is text, and text between two list substitutions is invalid. typesafe/config drops it and concatenates the lists; lightbend/config#862 rejects itjava: lenient |
| 008-list-then-undefined-optional-is-the-list | pass | pass | pass | pass | pass | pass | pass | an undefined optional substitution in a concatenation with an array becomes an empty array |
| 009-undefined-optional-then-list-is-the-list | fail | pass | pass | pass | pass | pass | pass | the mirror of 008: an undefined optional substitution before an array becomes an empty array too. typesafe/config keeps the whitespace before the list as text and rejects it, while accepting 008 (noted on lightbend/config#862)java: unsupported |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 002-file-merge-is-recursive-and-later-file-wins | pass | pass | pass | pass | pass | pass | pass | merging objects loaded from different files is exactly the same behavior as merging duplicate fields in the same file: object-valued fields present in both files merge recursively and a non-object field takes the value from the later file |
| 003-non-object-between-files-hides-earlier-object | pass | pass | pass | pass | fail | pass | pass | the spec's own example: an intermediate non-object value hides earlier object values because merging is done in pairs, so when 42 is paired with { y : 2 } it simply wins and loses all information about what it overrode, and the two objects are never merged |
| 004-adjacent-objects-across-files-merge | pass | pass | pass | pass | fail | pass | pass | the spec's re-ordered example: with the non-object 42 moved to the lowest priority the two objects are adjacent, so they merge to { a : { x : 1, y : 2 } } |
| 005-null-clears-included-object | pass | pass | pass | pass | pass | pass | pass | setting an object to null and then setting it back to a new object clears it and starts over, which is how to get rid of default fallback values from another file that you don't want |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-later-scalar-wins | pass | pass | pass | pass | pass | pass | pass | for a non-object value the later assignment simply replaces the earlier one |
| 002-objects-merge | pass | pass | pass | pass | pass | pass | pass | two objects under the same key merge key-by-key, they do not replace |
| 003-scalar-breaks-the-merge | pass | pass | pass | pass | fail | pass | pass | merging is always done two values at a time: the scalar replaces the first object (non-object always wins), then the second object replaces the scalar (no merging, object is the new value), so the two objects never see each other |
| 004-merge-scalar-field-later-wins | pass | pass | pass | pass | pass | pass | pass | for a non-object-valued field present in both merged objects, the field from the second object is used |
| 005-merge-nested-object-recursive | pass | pass | pass | pass | pass | pass | pass | for an object-valued field present in both objects, the object values are recursively merged by the same rules |
| 006-null-prevents-merge | pass | pass | pass | pass | pass | pass | pass | the spec's own example: an intermediate null is a non-object, so it replaces the first object and is in turn replaced by the second object with no merging, which prevents the two objects from ever seeing each other |
| 007-merge-keeps-fields-from-either-side | pass | pass | pass | pass | pass | pass | pass | fields present in only one of the two objects are added to the merged object as they are, whether the first or the second side has them and whether the value is a scalar or an object with nothing to merge against |
| 008-arrays-are-not-objects | pass | pass | pass | pass | pass | pass | pass | only two object values merge; an array is not an object, so a later array replaces an earlier array (no concatenation) and replaces an earlier object (no merge) |
| 009-later-null-overrides | pass | pass | pass | pass | pass | pass | pass | null is a value like any other, so a later null overrides an earlier scalar or object: the key stays, set to null, rather than being removed or the assignment skipped |
| 010-null-in-referenced-key-history-does-not-block-merge | fail | pass | fail | pass | pass | pass | pass | the null at r only stops merging at r; ${r} is the object {x:1}, and a later object merges with the earlier a, so helper stays. typesafe/config carries r's null through the substitution and drops helper (lightbend/config#864)java: diverges |
| 011-nested-null-in-referenced-object-does-not-block-merge | fail | pass | fail | pass | pass | pass | pass | the same rule one level down: the null in v.nested's history belongs to v.nested, so a.nested still merges with the substituted {x:1} (lightbend/config#864)java: diverges |
| 012-reference-to-merged-field-sees-the-same-value | fail | pass | pass | pass | pass | pass | pass | a substitution sees the resolved value of the field it names, so o24bbd equals a.nested, which keeps helper. The key name is chosen to sort before a in typesafe/config's root HashMap, the order in which the first version of lightbend/config#864 answered differently for the twojava: diverges |
| 013-top-level-null-before-object-with-substitution | fail | pass | fail | pass | pass | pass | pass | the null belongs to v's history even when v's object is still unresolved, so a merges and keeps helper (lightbend/config#864)java: diverges |
| 014-own-null-still-blocks-merge-with-referenced-object | pass | pass | pass | pass | pass | pass | pass | the receiver's own intermediate null still breaks the merge: a is {helper:0}, then null, then the substituted object, so helper is gone |
| 015-earlier-receiver-value-is-evaluated-once-merged | fail | pass | fail | pass | pass | pass | pass | ${r} is an object, so it merges with the earlier a and the ${missing} inside it is evaluated: an error. typesafe/config 1.4.5 agreed; 1.4.9 drops the earlier a and yields {y:1}, and lightbend/config#864 restores the errorjava: lenient |
| 016-null-deeper-in-a-merge-stack-stays-at-its-key | fail | pass | fail | pass | fail | pass | pass | v's value is a merge whose top is an object and whose null sits further down, under nested; that null is still v.nested's merge instruction, so a.nested keeps helper (lightbend/config#864 review, round 3)java: diverges |
| 017-lookup-into-nested-null-merges | fail | pass | pass | pass | pass | pass | pass | two nested nulls each break only their own key's merge, and a lookup of a.n.d sees the final {x:1}; typesafe/config throws BugOrBroken in this key order (reported on lightbend/config#864, same on main)java: unsupported |
| 018-object-then-empty-object | pass | pass | pass | pass | pass | pass | pass | two objects merge, so the empty later object keeps x; replacement would have given {} |
| 019-duplicate-keys-merge-inside-a-list-element | pass | pass | pass | pass | pass | pass | pass | an object that is a list element follows the same duplicate-key rules: the later value, here resolved from a substitution, wins |
| 020-objects-and-substitutions-alternate | pass | pass | fail | fail | pass | fail | pass | four values for a, two of them substitutions that resolve to objects: all four merge in order |
| 021-later-object-replaces-earlier-concatenation | pass | pass | pass | pass | fail | pass | pass | a string concatenation is a non-object value, so the later object replaces it rather than merging with it |
| 022-scalar-then-empty-object-then-object | pass | pass | pass | pass | fail | pass | pass | the number cuts the merge off from anything before it; the empty object after it and the object after that still merge, and the empty one contributes nothing |
| 023-empty-object-then-substitution | pass | pass | pass | pass | pass | pass | pass | an empty object followed by a substitution that resolves to an object merges into that object; the empty layer contributes nothing |
| 024-merge-stack-nested-inside-a-merge-stack | pass | pass | fail | pass | pass | pass | pass | the object layer of a's merge contains a field b that is itself a merge of a substitution and an object; both levels merge |
| 025-lookup-into-object-that-replaced-a-list | fail | pass | pass | pass | pass | pass | pass | the object replaces the earlier list, so a.m exists and p is 1 whatever order the keys are resolved in. typesafe/config fails when the reader p is resolved before a (it iterates p before a); the same file with the reader named zzz resolvesjava: unsupported |
| 026-missing-key-in-object-that-replaced-a-list | pass | pass | pass | pass | pass | pass | pass | the same shape with a key the object does not have: the list below it is not consulted, a.b is undefined and the optional p is not created |
| 027-nested-object-then-scalar-inside-merge | pass | pass | pass | pass | pass | pass | pass | inside a merge the same rules apply per field: b is an object in the first and a number in the second, so the later value replaces it |
| 028-null-in-second-object-wins | pass | pass | pass | pass | pass | pass | pass | null is a non-object value, so the second object's null is used for b rather than dropped |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-bare-number-is-milliseconds | pass | pass | pass | pass | pass | pass | pass | bare numbers are taken to be in milliseconds already |
| 002-uppercase-unit-is-illegal | pass | pass | pass | pass | pass | pass | pass | the supported unit strings for duration are case-sensitive and must be lowercase, so an uppercase unit is not one of the supported strings |
| 003-nanoseconds-spellings | pass | pass | pass | pass | fail | pass | pass | exactly ns, nano, nanos, nanosecond, nanoseconds are the supported unit strings for nanoseconds |
| 004-microseconds-spellings | pass | pass | pass | pass | fail | pass | pass | exactly us, micro, micros, microsecond, microseconds are the supported unit strings for microseconds |
| 005-milliseconds-spellings | pass | pass | pass | pass | fail | pass | pass | exactly ms, milli, millis, millisecond, milliseconds are the supported unit strings for milliseconds |
| 006-seconds-spellings | pass | pass | pass | pass | fail | pass | pass | exactly s, second, seconds are the supported unit strings for seconds |
| 007-minutes-spellings | pass | pass | pass | pass | fail | pass | pass | exactly m, minute, minutes are the supported unit strings for minutes |
| 008-hours-spellings | pass | pass | pass | pass | fail | pass | pass | exactly h, hour, hours are the supported unit strings for hours |
| 009-days-spellings | pass | pass | pass | pass | fail | pass | pass | exactly d, day, days are the supported unit strings for days in the duration format |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-basename-loads-every-extension | pass | fail | fail | pass | fail | pass | pass | a name without an extension is a basename: all of base.properties, base.json and base.conf exist, so all three are loaded and merged, properties first and HOCON last, so the .conf value wins where they overlap |
| 002-basename-with-only-conf | pass | fail | fail | pass | fail | pass | pass | a basename with only base.conf present loads just that file |
| 003-basename-with-only-properties | pass | fail | fail | pass | fail | pass | pass | a basename with only base.properties present loads it as Java properties, whose values are all strings |
| 004-basename-with-no-file-is-ignored | pass | pass | pass | pass | pass | pass | pass | a basename that exists with no extension at all is a missing include, and a missing include is ignored |
| 005-required-basename-with-no-file-is-error | pass | pass | pass | pass | pass | pass | pass | required() makes the missing basename an error |
| 006-json-extension-means-json-syntax | fail | fail | fail | fail | fail | fail | pass | a file named .json is in JSON format, and an unquoted value is not JSON. typesafe/config parses a .json named outright with the HOCON parser, so the file is accepted; the same file found through a basename is rejectedjava: lenient |
| 007-properties-extension-means-properties-syntax | fail | fail | fail | pass | fail | pass | pass | a file named .properties is in Java properties format, where the first = separates key from value and values are strings. typesafe/config parses a .properties named outright with the HOCON parser and fails on the second =; the same file found through a basename is read as propertiesjava: unsupported |
| 008-properties-keys-are-paths | pass | fail | fail | pass | fail | pass | pass | in a properties file a dotted key is a path, so a.b and a.c build an object at a; the spec's Java properties mapping says the object must always win when a key is both a value and a parent, whatever the order; values are strings |
| 010-json-file-with-every-value-type | pass | fail | fail | pass | fail | pass | pass | a .json file found through a basename is parsed as JSON and contributes its object |
| 011-json-rejects-unquoted-values | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; an unquoted value is a HOCON extension, not JSON |
| 012-json-rejects-substitutions | pass | fail | fail | pass | fail | pass | pass | the .json file is found through a basename, so it is parsed as JSON; ${} is a HOCON extension, not JSON |
| 013-json-rejects-trailing-comma | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; JSON has no trailing commas |
| 014-json-rejects-duplicate-keys | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; the spec's own footing is that duplicate keys are invalid JSON (L194-195), so the second a is an error rather than an override |
| 015-json-array-needs-commas | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; JSON separates array elements with commas only; whitespace does not concatenate or separate |
| 016-json-rejects-empty-document | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; an empty document is not JSON, although it is an empty HOCON object |
| 017-json-rejects-scalar-root | pass | fail | fail | pass | fail | pass | pass | the .json file is found through a basename, so it is parsed as JSON; a JSON document included here must be an object; a bare number is neither an object nor valid as an included root |
| 019-json-key-must-be-a-string | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; a JSON key is a quoted string; a number cannot be a key |
| 020-json-newline-is-not-a-separator | pass | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; in JSON a newline between fields does not stand in for the comma |
| 022-json-dotted-key-is-one-key | pass | fail | fail | pass | fail | pass | pass | the .json file is found through a basename, so it is parsed as JSON; a JSON key is a plain string, not a path expression, so a.b stays a single key |
| 023-json-leading-zero-illegal | fail | fail | fail | fail | fail | fail | pass | the .json file is found through a basename, so it is parsed as JSON; JSON forbids a leading zero on a number. typesafe/config reads 01 as 1, the same leniency it shows in HOCONjava: lenient |
| 024-unknown-extension-is-not-a-basename | fail | pass | pass | fail | pass | pass | pass | x.txt has an extension, so it is not a basename to probe; it is a file adjacent to the including one and of the same type. The spec does not define what counts as an extension, and this row reads any dotted suffix as one. typesafe/config probes every name that does not end in .conf, .json or .properties as a basename and finds nothingjava: diverges |
| 025-json-is-parsed-after-properties | pass | fail | fail | pass | fail | pass | pass | base.properties and base.json both exist; JSON is parsed next-to-last, after properties, so its value for a wins |
| 026-conf-is-parsed-after-json | pass | fail | fail | pass | fail | pass | pass | base.json and base.conf both exist; HOCON is parsed last, after JSON, so its value for a wins |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-include-string-that-is-a-url-is-a-url | pass | pass | pass | pass | pass | pass | pass | a quoted include name with a known URL scheme is a URL, not a file adjacent to this one. With nothing behind the URL the include is ignored, so a parser that read the string as a filename would also get {}; the row records the rule, it cannot distinguish the two readings |
| 002-nested-include-is-relative-to-the-including-file | pass | pass | pass | pass | pass | pass | pass | inner.conf includes x.conf, which is found beside inner.conf, not beside main.conf and not in the process's working directory |
| 003-plain-name-is-a-file-beside-the-including-one | pass | pass | pass | pass | pass | pass | pass | x.conf is not a URL, so it names a file adjacent to main.conf |
| 004-absolute-path-stays-absolute | pass | pass | pass | pass | pass | pass | pass | an absolute path is loaded as such, so the file of the same name beside main.conf is not it; the absolute file does not exist and the include is ignored |
| 005-file-argument-is-not-relative-to-the-including-file | pass | fail | fail | pass | fail | pass | pass | a file() argument is not interpreted relative to the including file, so the marker beside sub/inner.conf is not loaded; nothing of that name exists elsewhere and the include is ignored |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-included-keys-merge-in-place | pass | pass | pass | pass | pass | pass | pass | the keys from the included object are conceptually substituted for the include statement in the including file, so the included field lands between the fields around it and merges by the usual duplicate-key rules |
| 002-included-conf-array-root-is-error | pass | pass | pass | pass | pass | pass | pass | an included file must contain an object, not an array; if it contains an array as the root value it is invalid and an error should be generated |
| 003-included-json-array-root-is-error | pass | pass | pass | pass | pass | pass | pass | both JSON and HOCON allow arrays as root values in a document, but an included file must contain an object, so a JSON file with an array root is an error when included |
| 004-include-inside-nested-object | pass | pass | pass | pass | pass | pass | pass | the keys from the included root object are conceptually substituted for the include statement in the including file, so an include inside a nested object contributes its keys to that object |
| 005-included-scalar-overrides-earlier-key | pass | pass | pass | pass | pass | pass | pass | if a key in the included object occurred prior to the include statement in the including object, the included key's value overrides the earlier value, exactly as with duplicate keys found in a single file |
| 006-included-object-merges-with-earlier-object | pass | pass | pass | pass | pass | pass | pass | if a key in the included object occurred prior to the include statement and both values are objects, the included value merges with the earlier value, exactly as with duplicate keys found in a single file |
| 007-included-scalar-replaces-earlier-object | pass | pass | pass | pass | pass | pass | pass | an included key's value overrides the earlier value exactly as with duplicate keys in a single file, so an included non-object replaces an earlier object rather than merging with it |
| 008-later-key-overrides-included-key | pass | pass | pass | pass | pass | pass | pass | if the including file repeats a key from an earlier-included object, the including file's value overrides the one from the included file |
| 009-later-object-merges-with-included-object | pass | pass | pass | pass | pass | pass | pass | if the including file repeats a key from an earlier-included object and both values are objects, the including file's value merges with the one from the included file |
| 010-include-inside-an-object-inside-a-list | pass | pass | pass | pass | pass | pass | pass | an include statement may stand wherever a field may, including in an object that is a list element |
| 011-include-inside-a-list-with-a-substitution | fail | pass | pass | pass | pass | pass | pass | the included file may contain substitutions like any other; typesafe/config refuses an include nested in a list when the included file is not fully resolvedjava: unsupported |
| 012-included-merge-stack-over-earlier-object | pass | pass | fail | fail | pass | fail | pass | the included file leaves a as an object merged with a substitution; that whole stack merges with the object the including file set earlier |
| 013-included-merge-stack-over-earlier-substitution | pass | pass | fail | fail | pass | fail | pass | the earlier value is itself a substitution; once everything resolves to objects, all three layers merge |
| 014-included-merge-stack-over-earlier-scalar | pass | pass | fail | pass | pass | pass | pass | the earlier value is a number, so the included object stack replaces it and merges only within itself |
| 015-included-object-on-top-of-substitution-over-earlier-substitution | pass | pass | fail | fail | pass | fail | pass | the same as 013 with the included stack the other way up, an object over a substitution; the result does not depend on which layer is on top |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-missing-include-ignored | pass | pass | pass | pass | pass | pass | pass | by default, if an included file does not exist the include statement is silently ignored, so the keys around it parse as if the line were not there |
| 002-missing-include-is-empty-object | pass | pass | pass | pass | pass | pass | pass | a missing include is treated as if the included file contained only an empty object, so an object whose only content is that include stays an empty object |
| 003-missing-file-include-ignored | pass | pass | pass | pass | pass | pass | pass | the silent-ignore default applies to an explicit file() include as well: a file that does not exist contributes nothing and parsing continues |
| 004-required-missing-is-error | pass | pass | pass | pass | pass | pass | pass | when the included resource name is wrapped with required(), file parsing fails with an error if the resource cannot be resolved |
| 005-required-file-missing-is-error | pass | pass | pass | pass | pass | pass | pass | required() may also wrap the file() form, and a required file that cannot be resolved is a parse error rather than an empty object |
| 006-required-present-is-included | pass | pass | pass | pass | pass | pass | pass | required() only makes the include mandatory; a resource that does resolve is included exactly as an unwrapped include would be |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-substitution-relativized-to-include-scope | pass | pass | fail | pass | fail | pass | pass | a substitution in an included file must be fixed up to be relative to the app's configuration root, so ${x} in a file included at key a becomes ${a.x} |
| 002-relativized-path-sees-later-redefinition | pass | pass | fail | pass | fail | pass | pass | substitution happens after parsing the whole configuration, so the ${x} fixed up to ${a.x} evaluates to the root's later redefinition 42 rather than the included file's 10 |
| 003-original-path-tried-as-fallback | pass | pass | pass | pass | pass | pass | pass | it is not enough to only look up the fixed-up path; when a.x is undefined the original path x is looked up relative to the root of the including configuration |
| 004-relativized-path-tried-before-original | pass | fail | fail | pass | fail | pass | pass | substitutions in included files are looked up first relative to the root of the included file and only second relative to the root of the including configuration, so a.x wins over x |
| 005-neither-path-defined-is-error | pass | pass | pass | pass | pass | pass | pass | only the fixed-up path a.x and the original path x are looked up; with neither defined the substitution is undefined and the configuration is invalid |
| 006-optional-substitution-with-neither-path-is-omitted | pass | pass | pass | pass | pass | pass | pass | an optional substitution in an included file is looked up at the fixed-up path a.x and at the original path x; with neither defined the field is omitted rather than an error |
| 007-nested-include-relativized-through-both-levels | pass | pass | fail | pass | fail | pass | pass | the fix-up is relative to the app's configuration root, so ${x} in a file included at b inside a file included at a becomes ${a.b.x} |
| 008-original-path-falls-back-to-environment | pass | fail | pass | pass | pass | pass | pass | an included file may intend to refer to the application's root config, for example to get a value from the environment; the original path CONFORMANCE_HOME is looked up and falls back to the environment variable |
| 009-undefined-optional-at-fixed-up-path-falls-back | fail | pass | pass | fail | pass | fail | pass | an undefined ${?nope} means common.x.y is never created, so ${x.y} is not found at the fixed-up path and is looked up at the original path x.y. typesafe/config treats the vanished value as a miss that ends the lookup -- except in some root key orders (see 014); lightbend/config#863 falls backjava: unsupported |
| 010-undefined-optional-at-fixed-up-path-with-no-original | pass | pass | pass | fail | pass | fail | pass | with neither common.x.y nor x.y defined, the required ${x.y} in the included file is unresolved |
| 011-optional-reference-falls-back-too | fail | pass | pass | fail | pass | fail | pass | the optional form of 009 falls back the same way; typesafe/config leaves a out instead (lightbend/config#863)java: diverges |
| 012-earlier-value-at-fixed-up-path-wins | pass | fail | fail | pass | fail | pass | pass | an undefined optional that would override keeps the previous value, so common.x.y is 9 and found at the fixed-up path; the original path is not consulted |
| 013-missing-leaf-under-existing-object-falls-back | pass | pass | pass | pass | pass | pass | pass | the metamorphic twin of 009 without the optional: common.x exists, common.x.y does not, so x.y is looked up at the original path |
| 014-unrelated-key-does-not-change-the-result | fail | pass | pass | fail | pass | fail | pass | an unrelated key that reads common.x must not change what common.a resolves to; in typesafe/config this input fails while the same input with the key named o24bbd resolves to 0 (lightbend/config#863)java: unsupported |
| 015-undefined-at-both-paths-is-error | pass | pass | fail | fail | pass | fail | pass | when the original path is itself an undefined optional, neither lookup finds a value and the required ${x.y} is an error |
| 016-substitution-inside-a-concatenation-is-fixed-up | pass | pass | pass | pass | pass | pass | pass | a substitution that is one piece of a concatenation in the included file is fixed up like a bare one: p.x is not set, so the original path x is used |
| 017-substitution-inside-a-list-is-fixed-up | pass | pass | pass | pass | pass | pass | pass | a substitution that is a list element in the included file is fixed up as well |
| 018-substitution-in-a-merge-stack-is-fixed-up | pass | pass | fail | pass | fail | pass | pass | an object merged with a substitution in the included file keeps working after the fix-up, whichever of the two is on top: q is found at p.q |
| 019-null-at-fixed-up-path-is-found | pass | fail | fail | pass | fail | pass | pass | a.x is present, set to null, so the lookup at the fixed-up path succeeds and the original path x is not consulted |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-include-later-in-key-is-literal | pass | pass | pass | pass | pass | pass | pass | unquoted include has no special meaning if it is not the start of a key's path expression; it may appear later in the key, so { foo include : 42 } is equivalent to { "foo include" : 42 } |
| 002-include-as-object-value-is-string | pass | pass | pass | pass | pass | pass | pass | unquoted include has no special meaning if it is not the start of a key's path expression; as an object value it is the string "include" |
| 003-include-as-array-element-is-string | pass | pass | pass | pass | pass | pass | pass | unquoted include has no special meaning if it is not the start of a key's path expression; as an array element it is the string "include". the spec's example is the root array [ include ]; it is wrapped in a field here so the row does not depend on array-root support |
| 004-no-concatenation-on-include-argument | pass | pass | fail | pass | pass | pass | pass | value concatenation is NOT performed on the argument to include; the argument must be a single quoted string, so a second quoted string after the resource name is an error whether or not the first resource exists |
| 005-no-substitution-in-include-argument | pass | pass | pass | pass | pass | pass | pass | no substitutions are allowed in the argument to include, which must be a single quoted string; ${x} after include is an error even though x is defined, so an implementation that resolves the argument first still fails this row |
| 006-include-argument-must-be-quoted | pass | pass | pass | pass | pass | pass | pass | if an unquoted include at the start of a key is followed by anything other than a single quoted string or the url()/file()/classpath() syntax, it is invalid and an error should be generated; the argument may not be an unquoted string |
| 007-quoted-include-is-ordinary-key | pass | pass | pass | pass | pass | pass | pass | only unquoted include is special; quoting it gives a key that starts with the word include, so { "include" : 42 } is an ordinary field named include |
| 008-file-argument-must-be-quoted | pass | pass | pass | pass | pass | pass | pass | the argument inside file() must be a single quoted string; an unquoted name is a syntax error |
| 009-file-missing-close-parenthesis | pass | pass | pass | fail | pass | pass | pass | file("...") must be closed; the line ends before the parenthesis |
| 010-required-missing-close-parenthesis | pass | pass | pass | fail | pass | fail | pass | required(...) must be closed too |
| 011-url-argument-must-be-a-url | fail | fail | fail | fail | fail | fail | fail | the string inside url() is interpreted as a URL, and x.conf has no scheme, so it is not one |
| 013-newlines-between-include-and-name | pass | pass | fail | pass | fail | pass | pass | any amount of whitespace, newlines included, may stand between include and the resource name |
| 014-whitespace-inside-parentheses | pass | fail | pass | pass | pass | pass | pass | whitespace inside the parentheses, outside the quotes, is allowed |
| 015-include-followed-by-separator-is-not-a-key | pass | pass | fail | pass | fail | pass | pass | an unquoted include at the start of a key is never a key; followed by a separator instead of a quoted name it is an include with an invalid argument |
| 016-comma-separates-include-from-next-field | pass | fail | pass | pass | pass | pass | pass | an include stands where a field would, so the usual comma separates it from the field that follows |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-equals-as-separator | pass | pass | pass | pass | pass | pass | pass | the = character can be used anywhere JSON allows :, to separate keys from values |
| 002-omitted-separator-before-object | pass | pass | pass | pass | pass | pass | pass | if a key is followed by {, the : or = may be omitted; the spec's own example is "foo" {} meaning "foo" : {} |
| 003-omitted-separator-unquoted-key | pass | pass | pass | pass | pass | pass | pass | the separator-omission rule before { applies to unquoted keys too, not just the quoted example in the spec |
| 004-value-on-the-line-after-the-separator | pass | pass | pass | pass | pass | pass | pass | = stands wherever JSON allows :, and JSON allows any whitespace, newlines included, between the separator and the value |
| 005-separator-cannot-be-omitted-before-array | pass | pass | pass | pass | pass | pass | pass | the separator may be omitted only before {; a key followed directly by [ is invalid |
| 006-newline-between-key-and-brace | pass | pass | pass | pass | pass | pass | pass | the separator may be omitted before {, and a newline before the { is whitespace like any other between a key and its value, as JSON allows one before : |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-list-from-indexed-variables | pass | fail | fail | pass | fail | pass | pass | the [] suffix collects CONFORMANCE_LIST_0, CONFORMANCE_LIST_1, ... until an index is missing, into a list of strings |
| 002-optional-list-with-no-elements-is-undefined | pass | fail | pass | pass | pass | pass | pass | with no CONFORMANCE_LIST_0 set, an optional ${?X[]} is undefined and the field is not created |
| 003-required-list-with-no-elements-is-error | pass | pass | pass | pass | pass | pass | pass | with no CONFORMANCE_LIST_0 set, a required ${X[]} is an error |
| 004-collection-stops-at-the-first-gap | pass | fail | fail | pass | fail | pass | pass | indices are collected from 0 until one is missing, so with _0 and _2 set the list holds only the first |
| 005-elements-are-strings | pass | fail | fail | pass | fail | pass | pass | an element taken from the environment is a string, so 1 stays "1" |
| 006-config-keys-are-not-collected | pass | fail | pass | pass | pass | pass | pass | the [] suffix collects environment variables only, so the config key a_0 is not gathered and the optional substitution is undefined |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-basic-triple-quote | pass | pass | pass | pass | pass | pass | pass | the three-character sequence """ starts a multi-line string whose unicode characters are used unmodified to create the string value |
| 002-newlines-and-whitespace-preserved | pass | pass | pass | pass | pass | pass | pass | newlines and whitespace inside a multi-line string receive no special treatment and are kept in the value |
| 003-unicode-escape-not-interpreted | pass | pass | pass | pass | pass | pass | pass | unlike JSON quoted strings, a triple-quoted string does not interpret escapes, so the six characters \u0041 stay as written rather than becoming A |
| 004-extra-quote-becomes-part-of-string | pass | pass | pass | pass | pass | pass | pass | any sequence of at least three quotes ends the multi-line string and any extra quotes are part of the string, so HOCON works like Scala's four-character string foo" rather than Python's syntax error |
| 005-five-closing-quotes-two-extra | pass | pass | fail | pass | pass | pass | pass | a closing run of five quotes still only needs three to end the string, so both extra quotes are appended to the string value |
| 006-unterminated-is-illegal | pass | pass | pass | pass | pass | pass | pass | a multi-line string opened with a three-character quote sequence that never reaches a closing """ is not a legal value |
| 007-fewer-than-three-quotes-does-not-close | pass | pass | fail | pass | pass | pass | pass | only a run of at least three quote characters ends the multi-line string, so the single embedded quote pairs around hi do not terminate it early |
| 008-backslash-kept-literally | pass | pass | fail | pass | pass | pass | pass | a backslash sequence inside a triple-quoted string is used unmodified, not as an escape |
| 009-six-quotes-are-an-empty-string | pass | pass | pass | pass | pass | pass | pass | the three opening quotes are followed at once by the closing run, so the string is empty |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-object-with-numeric-keys-stays-object | pass | pass | pass | pass | pass | pass | pass | the conversion should be done lazily when required to avoid a type error, not eagerly anytime an object has numeric keys, so a bare object with numeric keys stays an object |
| 003-concatenation-converts-numeric-object-to-array | pass | fail | fail | pass | fail | pass | pass | the conversion should be done in a concatenation when a list is expected and an object with numeric keys is found, so the trailing object concatenates onto the array as elements x, y |
| 004-concatenation-ignores-non-integer-keys | pass | fail | fail | pass | fail | pass | pass | the conversion should ignore any keys which do not parse as positive integers, so key "foo" is dropped and only "0" contributes to the resulting array |
| 005-empty-object-concatenation-not-converted | pass | pass | pass | pass | pass | pass | pass | the conversion should not occur if the object is empty, so an empty object cannot satisfy a list-expected concatenation and this is illegal |
| 006-object-without-integer-keys-not-converted | pass | pass | pass | pass | pass | pass | pass | the conversion should not occur if the object has no keys which parse as positive integers, so this object cannot satisfy a list-expected concatenation and this is illegal |
| 007-sparse-integer-keys-sorted-and-reindexed | pass | fail | fail | pass | fail | pass | pass | the conversion should sort by the integer value of each key and then build the array; missing indices such as "1" are eliminated rather than left as gaps, so keys "0" and "2" become a two-element array |
| 008-negative-key-ignored-in-conversion | pass | fail | fail | pass | fail | pass | pass | the conversion ignores any keys which do not parse as positive integers, and "-1" is not a positive integer, so it is dropped and only key "0" contributes |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-implicit-root-object | pass | pass | pass | pass | pass | pass | pass | if the file does not begin with [ or {, it is parsed as if enclosed with {} curly braces |
| 002-explicit-root-object-unaffected | pass | pass | pass | pass | pass | pass | pass | the implicit-wrap rule only applies when the file does not begin with [ or {; a file already beginning with { is parsed as ordinary JSON-style object |
| 003-missing-open-brace-with-close-brace-illegal | pass | pass | pass | fail | pass | fail | pass | a HOCON file is invalid if it omits the opening { but still has a closing }, since the curly braces must be balanced |
| 004-empty-file | pass | fail | pass | pass | pass | pass | pass | an empty file does not begin with a square bracket or a curly brace, so it is parsed as if enclosed in {}: the empty object |
| 005-bare-string-root-illegal | pass | pass | pass | pass | pass | pass | pass | the file does not begin with a bracket or brace, so it is wrapped in {} and becomes a key with no separator and no value |
| 006-array-root-unaffected | fail | pass | pass | fail | pass | fail | pass | the implicit {} wrap fires only when the file does not begin with a square bracket or curly brace, so a file starting with [ keeps its array root; the include section says so outright -- 'both JSON and HOCON allow arrays as root values in a document' (the reason an included file may not be one). typesafe/config has no array-rooted config at all and rejects the document as LIST rather than object.java: unsupported |
| 008-empty-array-root | fail | pass | pass | fail | pass | fail | pass | an empty array is still an array root, and the wrap rule looks at the opening bracket, not at whether the document has contentjava: unsupported |
| 009-comment-before-array-root | fail | pass | pass | fail | pass | fail | pass | the {} wrap fires only for a file that does not begin with a bracket or brace, and a leading comment does not make it one -- both typesafe/config and pyhocon read the first token, not the first byte. the spec's wording is loose here; this row records the consensus readingjava: unsupported |
| 010-blank-line-before-array-root | fail | pass | pass | fail | pass | fail | pass | leading whitespace does not trigger the {} wrap either, for the same reason as a leading commentjava: unsupported |
| 011-trailing-array-after-root-array | pass | pass | pass | pass | pass | pass | pass | two arrays side by side concatenate when only non-newline whitespace separates them, which would make this document [1,2,3]; but a document is one array or one object at the root, and nothing in the spec lets a root value be a concatenation of two. typesafe/config, pyhocon and hocon-rs all reject it |
| 012-close-brace-after-value-on-one-line-illegal | pass | pass | pass | fail | pass | fail | pass | a close brace with no open brace is invalid wherever it stands, on the same line as a value as much as on its own |
| 013-open-brace-never-closed-illegal | pass | pass | pass | pass | pass | pass | pass | an opening brace with no closing brace is unbalanced |
| 014-comment-only-file | pass | fail | pass | pass | pass | pass | pass | a file holding only a comment does not begin with a bracket or brace, so it is the empty object |
| 015-bare-number-root-illegal | pass | pass | pass | pass | pass | pass | pass | the file does not begin with { or [, so it is read as if wrapped in braces, and a bare number there is a key with no value |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-quoted-dot-has-no-meaning | pass | pass | pass | pass | pass | pass | pass | a dot inside a quoted string has no special meaning as a path separator, so a quoted "a.b" key is a single-element path |
| 002-mixed-quoted-and-unquoted-path | pass | pass | fail | pass | fail | pass | pass | foo.bar."hello.world" is a path with three elements: foo, bar, and hello.world, since unquoted dots are path separators but a quoted dot is not |
| 003-number-then-unquoted-string | pass | pass | pass | pass | pass | pass | pass | 10.0foo is a number then unquoted string foo, giving the two-element path 10 and 0foo, because a dot inside a number still counts as a path separator |
| 004-unquoted-string-with-dot | pass | pass | pass | pass | pass | pass | pass | foo10.0 is an unquoted string with a dot in it, giving the two-element path foo10 and 0 |
| 005-unquoted-concatenated-with-quoted-number | pass | pass | fail | fail | fail | fail | pass | foo followed by quoted "10.0" is an unquoted then a quoted string which concatenate, giving a single-element path |
| 006-all-numeric-path | pass | pass | pass | pass | pass | pass | pass | dots in numbers count as path separators, so 1.2.3 is the three-element path with elements 1, 2, 3 |
| 008-value-concatenation-keeps-boolean | pass | pass | pass | pass | pass | pass | pass | unlike a path expression, an array or element value consisting of the single value true is a value concatenation and retains its character as a boolean |
| 009-empty-path-element-quoted-is-valid | pass | pass | fail | pass | fail | pass | pass | a path element that is an empty string must be quoted; a."".b is a valid three-element path whose middle element is the empty string |
| 010-consecutive-dots-invalid | pass | pass | pass | pass | fail | pass | pass | an unquoted empty path element is invalid, so a..b must generate an error |
| 011-path-starting-with-dot-invalid | pass | pass | pass | pass | fail | pass | pass | a path that starts with a dot is invalid and should generate an error |
| 012-path-ending-with-dot-invalid | pass | fail | pass | pass | fail | pass | pass | a path that ends with a dot is invalid and should generate an error |
| 015-dotted-path-in-substitution | pass | pass | pass | pass | pass | pass | pass | path expressions appear in substitutions like ${foo.bar}, where the unquoted dot separates foo and bar into two path elements, so the lookup walks key foo then key bar |
| 016-number-keeps-original-text-in-path | pass | pass | pass | pass | pass | pass | pass | a number in a path expression must keep its original string representation as it appeared in the file, so 1.10 is the two-element path 1 and 10, not the 1 and 1 a generic number-to-string conversion would give |
| 017-whitespace-concatenation-in-path | pass | pass | pass | pass | pass | pass | pass | path expressions are syntactically identical to a value concatenation, so the whitespace-separated a b concatenates into one element and the unquoted dot then splits off c, giving the two-element path a b and c |
| 018-quoted-element-in-substitution-path | pass | pass | pass | pass | pass | pass | pass | path expressions appear in substitutions as well as keys, and inside quoted strings a dot has no special meaning there either, so ${foo."bar.baz"} looks up key foo then the single key bar.baz |
| 019-empty-element-in-substitution-path-invalid | pass | pass | fail | pass | fail | pass | pass | an unquoted empty path element is invalid in a substitution just as in a key, so ${a..b} must generate an error rather than collapsing to ${a.b} (a.b is defined so a lenient implementation would produce a value and fail) |
| 020-negative-leading-dot-key-is-a-path | pass | pass | pass | pass | pass | pass | pass | a dot in a key separates path elements whether or not the text around it would be a number, so -.33 names - then 33; numbers in paths keep their original text |
| 021-quoted-dot-in-substitution-path-is-literal | pass | pass | pass | pass | pass | pass | pass | inside quotes a dot is not a separator, so ${"b.c"} names one field called b.c, which does not exist; it does not reach b's field c |
| 022-keyword-in-substitution-path-is-a-string | pass | pass | pass | pass | pass | pass | pass | a path expression is always a string, so true in a substitution names the key true rather than a boolean |
| 023-substitution-in-dotted-key-illegal | pass | pass | pass | pass | pass | pass | pass | a key is a path expression, and a path expression may not contain a substitution, not even as a later element of a dotted key |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-two-element-path-expands | pass | pass | pass | pass | pass | pass | pass | a key that is a multi-element path expands to a nested object per element, the last element combined with the value becomes a field in the most-nested object: foo.bar:42 is equivalent to foo{bar:42} |
| 002-three-element-path-expands | pass | pass | pass | pass | pass | pass | pass | a three-element path key expands through two levels of nesting: foo.bar.baz:42 is equivalent to foo{bar{baz:42}} |
| 003-merge-sibling-paths | pass | pass | pass | pass | pass | pass | pass | the objects created by expanding path keys are merged in the usual way, so a.x:42, a.y:43 is equivalent to a{x:42,y:43} |
| 004-whitespace-in-key | pass | pass | pass | pass | pass | pass | pass | because path expressions work like value concatenations, whitespace is allowed in keys: a b c:42 is equivalent to "a b c":42 |
| 005-unquoted-true-key-becomes-string | pass | pass | pass | pass | pass | pass | pass | path expressions are always converted to strings, so the unquoted boolean-looking key true:42 is "true":42 |
| 006-unquoted-number-key-becomes-string | pass | pass | pass | pass | pass | pass | pass | path expressions are always converted to strings, so the unquoted numeric key 3:42 is "3":42 |
| 007-decimal-key-splits-on-dot | pass | pass | pass | pass | pass | pass | pass | a dot in an unquoted key is a path separator even when it looks like a decimal number, so 3.14:42 is "3":{"14":42} |
| 008-include-cannot-begin-key | fail | pass | fail | pass | fail | pass | pass | the unquoted string include may not begin a path expression in a key: where an object key is expected, include is not read as a key at all and must be followed by a quoted resource name, so include.foo is an error. typesafe/config recognises the keyword only as a bare include token -- include.foo tokenizes as one unquoted string, so it is accepted as the path include.foo, while include = 42 is rejected under the very rule the spec states.java: lenient |
| 009-include-not-at-start-is-ordinary | pass | fail | fail | pass | fail | pass | pass | only the unquoted string include that begins a path expression in a key is special: include later in a key (foo.include) and a quoted "include" beginning one are ordinary path elements |
| 010-path-key-merges-with-brace-object | pass | pass | pass | pass | pass | pass | pass | the object created by expanding a path key is merged in the usual way with an object written in braces at the same key, whichever form comes first: a{x:1}, a.y:2 and b.x:1, b{y:2} are both equivalent to {x:1,y:2} |
| 011-whitespace-around-a-dot-stays-in-the-elements | pass | pass | pass | pass | pass | pass | pass | whitespace inside a key is part of the key, and a dot splits the key into elements, so the elements here are 'a ' and ' b' with their spaces |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-bare-number-is-days | pass | pass | pass | pass | pass | pass | pass | for getPeriod(), bare numbers are taken to be in days, unlike getDuration() where bare numbers are milliseconds |
| 002-uppercase-unit-is-illegal | pass | pass | pass | pass | pass | pass | pass | the supported unit strings for period are case-sensitive and must be lowercase, so an uppercase unit is not one of the supported strings |
| 003-days-spellings | pass | pass | pass | pass | fail | pass | pass | exactly d, day, days are the supported unit strings for days in the period format |
| 004-weeks-spellings | pass | pass | pass | pass | fail | pass | pass | exactly w, week, weeks are the supported unit strings for weeks |
| 005-months-spellings | pass | pass | pass | pass | fail | pass | pass | exactly m, mo, month, months are the supported unit strings for months |
| 006-years-spellings | pass | pass | pass | pass | pass | pass | pass | exactly y, year, years are the supported unit strings for years |
| 007-month-m-ambiguous-with-duration-minutes | pass | pass | pass | pass | fail | pass | pass | the spec notes that getTemporal() callers should prefer mo over m for months, since m is also the duration unit for minutes and getTemporal() may return either a Duration or a Period |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-plus-equals-desugars-to-optional-self-ref-array | pass | pass | fail | pass | fail | pass | pass | a += b transforms into a = ${?a} [b], and because the fallback is optional (${?a} not ${a}) this is legal even as the first mention of a in the file |
| 002-plus-equals-appends-to-existing-array | pass | pass | fail | pass | fail | pass | pass | += appends an element to a previous array |
| 003-plus-equals-errors-on-non-array-previous-value | pass | pass | fail | pass | fail | pass | pass | if the previous value was not an array, += results in an error just as the long form a = ${?a} [b] would with a non-array a |
| 004-plus-equals-twice | pass | pass | fail | pass | fail | pass | pass | each += appends to the value the previous line left, so two of them append two elements |
| 005-plus-equals-in-nested-object-with-substitution | pass | fail | fail | pass | fail | pass | pass | inside an object, += desugars to a self-reference by the field's full path o.a; the appended element may itself be a substitution |
| 006-plus-equals-inside-a-list-element | fail | pass | fail | pass | fail | pass | pass | b has no previous value, so b += 1 is b = ${?b} [1] with the optional part undefined: [1]. typesafe/config refuses += inside a list because its desugared path cannot name a list elementjava: unsupported |
| 007-plus-equals-under-a-dotted-key | pass | fail | fail | pass | fail | pass | pass | the self-reference += desugars to uses the full path of the field, o.p.a, even when the enclosing key was written as the path o.p |
| 008-plus-equals-with-an-object-containing-a-merge | fail | pass | fail | pass | fail | pass | pass | l += v is l = ${?l} [v]; the appended object holds a merge of an object and a substitution, which resolves inside the new list. typesafe/config fails with an internal error (BugOrBroken) for the same reason as array-and-object-concatenation/018java: unsupported |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-isolated-self-reference-is-an-error | pass | pass | pass | pass | pass | pass | pass | in isolation, with no merges involved, a self-referential field is an error because the substitution cannot be resolved |
| 002-self-reference-resolves-to-earlier-merged-value | pass | pass | pass | pass | pass | pass | pass | when foo:${foo} is merged with an earlier value for foo, the self-reference resolves to that overridden value, so foo ends up { a : 1 } |
| 003-self-reference-before-any-value-is-undefined | pass | pass | fail | fail | pass | fail | pass | if the self-referential foo:${foo} comes before foo has any value, the reference is undefined, exactly as if it named a path not found in the document, so it is an error even though foo is given a value afterward |
| 004-optional-self-reference-disappears-silently | pass | pass | pass | pass | pass | pass | pass | because the self-reference error is treated as undefined rather than an intractable cycle, the optional syntax foo:${?foo} makes the field disappear silently instead of erroring |
| 005-substitution-hidden-by-later-non-mergeable-value-is-never-evaluated | pass | pass | pass | pass | pass | pass | pass | if a substitution is hidden by a later non-object value that could not be merged with it, it is never evaluated and no error is reported, no matter what it would have resolved to; foo ends up 42 |
| 006-self-reference-cycle-ignored-when-overridden-by-literal | pass | pass | pass | pass | pass | pass | pass | the same hiding rule applies to a self-reference cycle: once overridden by a non-mergeable literal, the initial foo:${foo} must simply be ignored, so foo ends up 42 with no error |
| 007-self-reference-in-path-expression-resolves-to-value-below | pass | pass | pass | pass | pass | pass | pass | a self-reference resolves to the value below it even as part of a path expression, so ${foo.a} refers to { c : 1 } rather than 2, and the final merge is { a : 2, c : 1 } |
| 008-object-may-refer-to-sibling-path-within-itself | pass | pass | pass | pass | pass | pass | pass | an implementation must allow an object to refer to a path within itself without treating it as a cycle, by resolving only the referenced field rather than recursing the whole enclosing object; bar.baz ends up 42 |
| 009-non-cycling-reference-looks-forward-across-merge | pass | pass | pass | pass | pass | pass | pass | because there is no inherent cycle here, the substitution must look forward including the field's own later merges, so bar.baz ends up 43 once foo is overridden to 43 |
| 010-mutually-referring-objects-are-not-self-referential | pass | pass | pass | pass | pass | pass | pass | mutually-referring objects should work and are not self-referential, so they look forward: bar.a ends up 4 and foo.c ends up 3 |
| 011-optional-self-reference-in-concatenation-looks-back | pass | pass | pass | pass | pass | pass | pass | an optional self-reference in a value concatenation has to look back to an undefined a, so a ends up "foo" rather than "foofoo" |
| 012-mutual-non-self-cycle-is-unresolvable | pass | pass | pass | pass | pass | pass | pass | the spec gives bar:${foo}, foo:${bar} as an example that is not possible to resolve, since neither lazy evaluation nor looking backward breaks the cycle and neither field is optional |
| 013-multi-step-cycle-is-invalid | pass | pass | pass | pass | pass | pass | pass | a multi-step loop across three fields (a:${b}, b:${c}, c:${a}) must also be detected as invalid, not just a direct two-field cycle |
| 014-order-dependent-resolution-must-agree-on-one-value | pass | pass | pass | pass | pass | pass | pass | the order a and b resolve in is undefined: both 1, both 2 or an error are all allowed, but a and b must agree (a=1,b=2 is not permitted) |
| 015-list-hidden-by-substituted-object-is-never-evaluated | fail | pass | pass | fail | pass | fail | pass | a list merges with nothing, so any later value hides it and it is never evaluated; that the hiding object arrives through ${replacement} changes nothing, since the substitution is required and so either resolves or fails on its own. typesafe/config evaluates the hidden list (lightbend/config#865)java: unsupported |
| 016-object-hidden-by-substituted-scalar-is-never-evaluated | pass | pass | pass | pass | fail | pass | pass | ${replacement} resolves to 42, a non-object, so the earlier object is hidden and never evaluated, as with the literal in 005 (typesafe/config 1.4.5 evaluated it) |
| 017-object-hidden-by-substituted-concatenation-is-never-evaluated | pass | pass | pass | pass | fail | pass | pass | a string concatenation is a non-object too, so it hides the earlier object (typesafe/config 1.4.5 evaluated it) |
| 018-object-merged-with-substituted-object-is-evaluated | pass | pass | fail | pass | pass | pass | pass | a substituted object merges with the earlier object, so its fields are part of the result and ${does-not-exist} is an error |
| 019-list-hidden-by-literal-scalar-is-never-evaluated | pass | pass | pass | pass | pass | pass | pass | the literal counterpart of 015: a literal number could not be merged with the list, so the list is never evaluated and there is no error |
| 020-object-hidden-by-literal-list-is-never-evaluated | pass | pass | pass | pass | pass | pass | pass | a literal list is a non-object value, so it hides the earlier object and ${does-not-exist} is never evaluated |
| 021-hidden-list-does-not-depend-on-key-order | fail | pass | pass | fail | pass | fail | pass | a reader of w.x must not change whether the hidden list is evaluated; only objects merge, so the object hides the list whatever else refers to it. typesafe/config fails here, yet resolves the same file with the reader named o24bbd (lightbend/config#865)java: unsupported |
| 022-undefined-optional-does-not-hide | pass | pass | fail | pass | pass | pass | pass | an undefined optional that would override leaves the previous value in place, so the list is the result and ${does-not-exist} is evaluated: an error |
| 023-append-does-not-hide | pass | pass | fail | pass | fail | pass | pass | w += 1 is w = ${?w} [1], which reads the earlier list, so ${does-not-exist} is evaluated: an error |
| 024-substitution-target-is-fetched-lazily | pass | pass | pass | pass | pass | pass | pass | resolving ${b.c} fetches only b.c; resolving all of b first would reach ${a} and report a cycle that is not there |
| 025-optional-two-field-cycle-is-undefined | pass | pass | fail | pass | fail | pass | pass | a genuine cycle is an error for required substitutions; with the optional syntax it is a missing value, so neither field is created |
| 026-memoized-by-instance-not-by-path | pass | pass | pass | pass | pass | pass | pass | the text ${a} appears twice and resolves differently: to x in a's own self-reference and to the final xy in b; memoizing by path instead of by instance would give b the wrong value |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-path-builds-on-older-value | pass | pass | pass | pass | pass | pass | pass | a field may look up its own older value before being overridden with a new value based on it, letting path:${path}":d" extend the earlier path:"a:b:c" |
| 002-self-referential-concatenation | pass | pass | pass | pass | pass | pass | pass | a value concatenation containing a substitution to its own field is self-referential, so ${a} looks back to the earlier x and the result is xbc |
| 003-object-containing-self-reference-is-not-self-referential | pass | pass | pass | pass | pass | pass | pass | an object with a substitution inside it is not considered self-referential for this purpose, so a:{b:${a}} is an unbreakable cycle that must generate an error |
| 004-array-containing-self-reference-is-not-self-referential | pass | pass | pass | pass | pass | pass | pass | an array with a substitution inside it is not considered self-referential for this purpose, so a:[${a}] is an unbreakable cycle that must generate an error |
| 006-self-referential-array-concatenation | pass | pass | pass | pass | pass | pass | pass | the spec lists path:${path} [ /usr/bin ] among the examples of self-referential fields: a value concatenation of a substitution with an array literal is still self-referential |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-bare-number-is-bytes | pass | pass | pass | pass | pass | pass | pass | bare numbers are taken to be in bytes already |
| 002-single-byte-spellings | pass | pass | pass | pass | pass | pass | pass | for single bytes, exactly B, b, byte, bytes are supported |
| 003-powers-of-ten-suffixes | pass | pass | pass | pass | pass | pass | pass | every power-of-10 suffix and its long forms name the same unit family; one table, one rule |
| 004-powers-of-two-suffixes | pass | pass | pass | pass | fail | pass | pass | every power-of-2 suffix and its long forms name the same unit family; one table, one rule. The E and e forms are quoted because an unquoted 5E is a number token that is not a number (see unquoted-strings/022) |
| 019-single-letter-abbreviation-means-powers-of-two | pass | pass | pass | pass | pass | pass | pass | the single-letter abbreviations (like K) are ambiguous between powers of two and ten, and this spec follows the java -Xmx / GNU-tools precedent of mapping them to powers of two |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-substitution-in-string-concatenation | pass | pass | pass | pass | pass | pass | pass | string value concatenation supports substitutions inside the concatenated string |
| 002-only-simple-values-participate | pass | pass | pass | pass | pass | pass | pass | it is invalid for arrays or objects to appear in a string value concatenation, since only simple values participate |
| 003-leading-and-trailing-whitespace-trimmed | pass | pass | pass | pass | pass | pass | pass | whitespace before the first and after the last simple value is discarded; only the whitespace between the values is kept |
| 004-concatenation-never-spans-newline | pass | pass | pass | pass | pass | pass | pass | string value concatenations never span a newline, so a bare word on the following line is not appended to the value above it |
| 005-array-element-string-concatenation | pass | pass | pass | pass | pass | pass | pass | a string value concatenation may appear in any place a string may appear, including array elements |
| 006-boolean-becomes-string-in-concatenation | pass | pass | fail | pass | fail | pass | pass | true and false become the strings "true" and "false" when they participate in a concatenation |
| 007-null-becomes-string-in-concatenation | pass | pass | fail | pass | fail | pass | pass | null becomes the string "null" when it participates in a concatenation |
| 008-number-kept-as-written-in-concatenation | pass | pass | pass | pass | pass | pass | pass | numbers should be kept as they were originally written in the file when they participate in a concatenation, not renormalized |
| 009-single-boolean-not-converted-to-string | pass | pass | pass | pass | pass | pass | pass | a single value is never converted to a string; true by itself must be parsed as a boolean-typed value, not a string |
| 010-object-then-scalar-drops-the-scalar | fail | pass | pass | pass | pass | pass | pass | it is invalid for arrays or objects to appear in a string value concatenation; typesafe/config accepts this one silently and discards the trailing x, losing data with no diagnostic, while erroring on the same pair in the other orderjava: lenient |
| 011-array-then-scalar-drops-the-scalar | fail | pass | pass | pass | pass | pass | pass | the same rule for arrays; typesafe/config again accepts and discards the trailing x, where it errors when the scalar comes firstjava: lenient |
| 012-scalar-then-object-is-an-error | pass | pass | pass | pass | pass | pass | pass | the same rule with the scalar first, which typesafe/config does reject -- the half of the rule it enforces |
| 014-array-then-unquoted-text-without-space | fail | pass | pass | pass | pass | pass | pass | an array may not appear in a string value concatenation, with or without whitespace between them; typesafe/config silently drops suffix (lightbend/config#862)java: lenient |
| 015-object-then-unquoted-text-without-space | fail | pass | pass | pass | pass | pass | pass | the same rule for an object followed directly by text; typesafe/config silently drops suffix (lightbend/config#862)java: lenient |
| 016-text-between-arrays-is-not-dropped | fail | pass | pass | pass | pass | pass | pass | text between two arrays makes a concatenation of array, string and array, which is invalid; typesafe/config drops the text and concatenates the arrays (lightbend/config#862)java: lenient |
| 020-64-bit-number-kept-as-written | pass | pass | pass | pass | pass | pass | pass | a number in a concatenation is stringified as it appeared in the file, whatever its size |
| 021-false-becomes-string-in-concatenation | pass | pass | fail | pass | fail | pass | pass | false becomes the string "false" in a concatenation, as true does |
| 022-quoted-and-unquoted-strings-concatenate | pass | pass | pass | pass | pass | pass | pass | a quoted string is itself in a concatenation, and the whitespace between it and the unquoted string beside it is kept |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-env-fallback-for-undefined-path | pass | pass | pass | pass | pass | pass | pass | a substitution whose path is not defined in the configuration falls back to an environment variable of the same name |
| 002-required-substitution-resolved-from-env | pass | pass | pass | pass | pass | pass | pass | a required ${x} substitution that is not present in the configuration tree is searched for in the environment and resolves to the variable's value instead of being an error |
| 003-config-value-wins-over-env | pass | pass | pass | pass | pass | pass | pass | the environment is only searched when the substitution is not present within the configuration tree, so a value set in the configuration wins over an environment variable of the same name |
| 004-null-in-config-blocks-env-lookup | pass | pass | fail | pass | fail | pass | pass | setting a key to null in the configuration explicitly blocks looking that substitution up in the environment, so ${CONFORMANCE_X} resolves to null even though the variable is set |
| 005-config-lookup-is-case-sensitive | pass | pass | pass | pass | pass | pass | pass | the lookup is case-sensitive all the way until the env variable fallback is reached, so a lowercase key does not shadow an uppercase substitution and the environment variable is used |
| 006-empty-env-var-is-empty-string | pass | pass | pass | pass | pass | pass | pass | an env variable set to the empty string is kept as such, so a required substitution resolves to "" rather than being undefined |
| 007-empty-env-var-creates-optional-field | pass | pass | pass | pass | pass | pass | pass | an env variable set to the empty string is set to empty string rather than undefined, so an optional ${?x} substitution creates the field with "" instead of dropping it |
| 008-env-vars-always-become-strings | pass | pass | pass | pass | pass | pass | pass | environment variables always become a string value, so values that would parse as a number, a boolean or null in HOCON stay the strings "42", "true" and "null" |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-basic-substitution | pass | pass | pass | pass | pass | pass | pass | the syntax ${pathexpression} refers to another part of the configuration tree |
| 002-optional-substitution-syntax | pass | pass | pass | pass | pass | pass | pass | ${?pathexpression} is the optional substitution syntax; when the path is defined it resolves exactly like ${pathexpression}, keeping the value and its type |
| 003-optional-substitution-whitespace-illegal | pass | pass | pass | pass | pass | pass | pass | the ? in ${?pathexpression} must not have whitespace before it; the three characters ${? must be exactly grouped together |
| 004-substitution-not-parsed-in-quoted-string | pass | pass | pass | pass | pass | pass | pass | substitutions are not parsed inside quoted strings, so this stays the literal text |
| 005-substitution-value-concatenation-unquoted | pass | pass | pass | pass | pass | pass | pass | worked example: to get a string containing a substitution, use value concatenation with the substitution in the unquoted portion |
| 006-substitution-value-concatenation-quoted-tail | pass | pass | pass | pass | pass | pass | pass | worked example: quoting the non-substitution portion also forms a value concatenation with the substitution |
| 007-substitution-path-absolute | pass | pass | pass | pass | pass | pass | pass | substitutions are resolved by looking up the path from the root configuration object, absolute rather than relative, so ${a} inside b finds the root a and not the sibling b.a |
| 008-substitution-forward-reference | pass | pass | pass | pass | pass | pass | pass | substitution processing happens as the last parsing step, so a substitution can look forward in the configuration |
| 009-substitution-latest-duplicate-key | pass | pass | pass | pass | pass | pass | pass | if a key has been specified more than once, a substitution referring to it always evaluates to its latest-assigned value |
| 010-substitution-latest-duplicate-key-object-merge | pass | pass | pass | pass | pass | pass | pass | a substitution to a key set more than once evaluates to the merged object, not an earlier snapshot |
| 011-undefined-required-substitution-is-error | pass | pass | pass | pass | pass | pass | pass | an undefined substitution with the ${foo} syntax is invalid and should generate an error |
| 012-undefined-optional-substitution-field-not-created | pass | pass | pass | pass | pass | pass | pass | if an undefined ${?foo} substitution is the value of an object field, the field should not be created |
| 013-undefined-optional-substitution-keeps-previous-value | pass | pass | fail | pass | pass | pass | pass | if an undefined ${?foo} would override a previously-set value for the same field, the previous value remains |
| 014-undefined-optional-substitution-array-element-not-added | pass | pass | pass | pass | pass | pass | pass | if an undefined ${?foo} substitution is an array element, the element should not be added |
| 015-undefined-optional-substitution-concatenation-string-empty | pass | pass | pass | pass | pass | pass | pass | an undefined ${?foo} in a value concatenation with another string becomes an empty string |
| 016-undefined-optional-substitution-concatenation-object-empty | pass | pass | pass | pass | pass | pass | pass | an undefined ${?foo} in a value concatenation with an object becomes an empty object |
| 017-undefined-optional-substitution-concatenation-array-empty | pass | pass | pass | pass | pass | pass | pass | an undefined ${?foo} in a value concatenation with an array becomes an empty array |
| 018-two-optional-substitutions-both-undefined-field-not-created | pass | pass | pass | pass | pass | pass | pass | foo : ${?bar}${?baz} avoids creating the field only if both bar and baz are undefined |
| 019-two-optional-substitutions-one-defined-field-created | pass | pass | pass | pass | pass | pass | pass | foo : ${?bar}${?baz} creates the field once at least one of bar or baz is defined |
| 020-substitution-not-allowed-in-key | pass | pass | pass | pass | pass | pass | pass | substitutions are only allowed in field values and array elements, not in keys |
| 021-substitution-not-allowed-nested-in-path-expression | pass | pass | pass | pass | pass | pass | pass | substitutions are not allowed nested inside other substitutions (path expressions) |
| 022-substitution-preserves-type-array | pass | pass | pass | pass | pass | pass | pass | a substitution is replaced with any value type; if it is the only part of a value, the type (here an array) is preserved |
| 024-substitution-concatenation-forms-string | pass | pass | pass | pass | pass | pass | pass | when a substitution is not the only part of a value, it is value-concatenated to form a string rather than keeping the substituted type |
| 025-substitution-preserves-type-string-booleans-null | pass | pass | fail | pass | fail | pass | pass | a substitution is replaced with any value type and, when it is the only part of a value, the type is preserved: here string, true, false and null stay what they were rather than becoming strings |
| 026-unterminated-substitution-is-error | pass | pass | pass | pass | pass | pass | pass | the syntax is ${pathexpression}: a substitution opened with ${ and never closed with } is not that syntax, and $ cannot start an unquoted string either, so it must be rejected |
| 028-optional-substitution-to-null-value-is-defined | pass | pass | fail | pass | fail | pass | pass | a substitution is undefined only if it does not match any value present in the configuration, and null is among the value types a substitution is replaced with, so ${?a} with a = null is defined: the field is created with null rather than omitted |
| 030-quoted-key-is-not-a-substitution | pass | pass | pass | pass | pass | pass | pass | substitutions are not parsed inside quoted strings, and a quoted key is a quoted string: "${a}" is the literal key ${a}, not a substitution in a key (which would be illegal) |
| 031-whitespace-inside-braces-is-trimmed | pass | fail | pass | pass | fail | pass | pass | the path inside ${} has the syntax of an object key, and whitespace before the first and after the last element of a key is discarded, so ${ b } is ${b} |
| 032-empty-substitution-illegal | pass | pass | pass | pass | pass | pass | pass | ${} contains no path expression at all, so it is neither a substitution nor anything else |
| 033-path-through-a-non-object-is-undefined | pass | fail | pass | pass | pass | pass | pass | a is a number, so a.x names nothing; a required substitution to an undefined path is an error |
| 034-optional-path-through-a-non-object-is-omitted | pass | fail | pass | pass | fail | pass | pass | the same path with the optional form: undefined, so the field is not created |
| 035-quoted-path-element-with-space | pass | pass | pass | pass | pass | pass | pass | a substitution path has the syntax of an object key, so a quoted element may contain a space and names the field written the same way |
| 036-empty-quoted-path-element | pass | pass | fail | pass | fail | pass | pass | an empty path element is legal when quoted, in a substitution as in a key |
| 037-path-into-a-list-element-is-undefined | pass | fail | pass | pass | fail | pass | pass | a path names object fields only, so b.0 does not reach into the list b even though it has an element at index 0; the optional substitution is undefined and a is not created. The list is still unresolved when the lookup happens, which is what makes an implementation look inside it |
| 038-whitespace-after-question-mark | pass | fail | pass | pass | fail | pass | pass | only whitespace before the ? is forbidden; after it the path follows and its leading whitespace is discarded as in any key |
| 039-undefined-optional-with-number-is-a-string | fail | fail | pass | fail | fail | fail | pass | ${?x}42 is a concatenation of two parts, so the value is a string: the undefined optional becomes empty and the number keeps its written form. The spec states the empty-string rule for a concatenation 'with another string', so this row extends it to a number; typesafe/config instead drops the undefined part and keeps the lone number as a numberjava: diverges |
| 040-null-blocks-environment-lookup | pass | pass | fail | pass | fail | pass | pass | the configuration sets CONFORMANCE_NULLED to null, so the substitution resolves to that null and the environment variable of the same name is never consulted |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-quoted-string-json-format | pass | pass | fail | pass | fail | pass | pass | quoted strings are in the same format as JSON strings, so JSON escapes like \n, \t, \", \\ and \u unicode escapes are honored |
| 002-string-value-type | pass | pass | pass | pass | pass | pass | pass | string is one of the possible value types |
| 003-number-value-type | pass | pass | pass | pass | pass | pass | pass | number is one of the possible value types |
| 004-object-value-type | pass | pass | pass | pass | pass | pass | pass | object is one of the possible value types |
| 005-array-value-type | pass | pass | pass | pass | pass | pass | pass | array is one of the possible value types |
| 006-boolean-value-type | pass | pass | pass | pass | pass | pass | pass | boolean is one of the possible value types |
| 007-null-value-type | pass | pass | pass | pass | pass | pass | pass | null is one of the possible value types |
| 008-number-format-decimal-and-exponent | pass | pass | pass | pass | pass | pass | pass | allowed number formats match JSON, including negative numbers, decimals, and exponent notation |
| 009-number-leading-zero-illegal | fail | fail | fail | fail | fail | fail | pass | allowed number formats match JSON, and JSON forbids a zero followed by further digits; the initial number character plus the valid-in-JSON number characters after it must be parsed as a number value, so the whole token 01 has to be a number and is not one, with no fallback offered. typesafe/config parses it leniently as the number 1.java: lenient |
| 010-negative-number-without-integer-part-illegal | fail | fail | fail | fail | fail | fail | pass | allowed number formats match JSON, and JSON requires a digit before the decimal point, so -.33 is a number token that is not a valid number. typesafe/config releases up to 1.4.9 parse it as -0.33; upstream main since lightbend/config#861 (unreleased) reads it as the unquoted string "-.33", like .33 -- still accepted, still not the error the hyphen rule asks forjava: lenient |
| 011-negative-exponent-number-without-integer-part-illegal | fail | pass | fail | fail | fail | fail | pass | the same JSON rule with an exponent: -.33e+1 has no integer part. typesafe/config releases up to 1.4.9 parse it as -3.3; upstream main since lightbend/config#861 (unreleased) rejects it, on the reserved +java: lenient |
| 012-number-with-trailing-dot-illegal | fail | fail | fail | fail | fail | fail | pass | JSON requires a digit after the decimal point; 1. is a number token that is not a valid number. typesafe/config reads it as 1, and lightbend/config#861 leaves it so on purposejava: lenient |
| 013-trailing-dot-before-exponent-illegal | fail | fail | fail | fail | fail | fail | pass | the same rule with an exponent after the dot; typesafe/config reads it as -1000 (named out of scope in lightbend/config#861)java: lenient |
| 014-negative-leading-zeros-illegal | fail | fail | fail | fail | fail | fail | pass | JSON forbids leading zeros after the minus sign as well; typesafe/config reads it as -33 (named out of scope in lightbend/config#861)java: lenient |
| 015-remaining-json-escapes | pass | pass | fail | pass | fail | pass | pass | the JSON escapes \/ \b \f and \r are valid in a quoted string and mean slash, backspace, form feed and carriage return |
| 016-unknown-escape-illegal | pass | pass | fail | pass | fail | pass | pass | only the JSON escape sequences exist; \q is not one of them, so the string is invalid |
| 017-non-hex-unicode-escape-illegal | pass | pass | fail | pass | fail | pass | pass | \u must be followed by exactly four hex digits; zzzz are not hex digits |
| 018-unterminated-quoted-string-illegal | pass | pass | pass | pass | pass | pass | pass | a quoted string that is still open at the end of the file is invalid |
| 019-backslash-at-end-of-file-illegal | pass | pass | pass | pass | pass | pass | pass | a backslash with nothing after it is not an escape and the string is still open at the end of the file |
| 020-unicode-escape-cut-off-by-end-of-file-illegal | pass | pass | pass | pass | pass | pass | pass | the file ends after two of the four hex digits a \u escape requires |
| 021-raw-newline-in-quoted-string-illegal | pass | pass | fail | pass | pass | pass | pass | JSON forbids unescaped control characters in strings, and a raw newline is one; a quoted string cannot span lines |
| 022-raw-tab-in-quoted-string-illegal | pass | pass | fail | fail | fail | fail | pass | a raw tab is a control character too, so it must be written as \t inside quotes |
| 023-64-bit-integer | pass | pass | pass | pass | pass | pass | pass | a number beyond 32 bits is still a number and keeps its exact value |
| 024-raw-utf8-in-quoted-string | pass | pass | pass | pass | fail | pass | pass | the file is UTF-8, so a multi-byte sequence is one character |
| 025-nan-is-not-a-number | pass | fail | pass | pass | pass | pass | pass | NaN is not a JSON number, so the bare token is an unquoted string |
| 026-surrogate-pair-escape | pass | pass | fail | pass | fail | fail | pass | JSON writes a character outside the BMP as two \u escapes forming a surrogate pair, which decode to one character |
| 027-unpaired-surrogate-escape-is-kept | pass | pass | fail | pass | fail | fail | pass | quoted strings are JSON strings, and JSON's grammar accepts any \uXXXX escape, an unpaired surrogate included, so the string holds that one code unit |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-number-value-is-default-unit | pass | pass | pass | pass | pass | pass | pass | if the value is a number, it is taken to be a number in the default unit |
| 002-quoted-string-with-no-unit-uses-default-unit | pass | pass | pass | pass | pass | pass | pass | a string value with no unit name should be interpreted with the default unit, as if it were a number |
| 003-string-with-unit-name-specifies-interpretation | pass | pass | pass | pass | fail | pass | pass | a string value with a unit name has that name specify the value's interpretation |
| 004-whitespace-optional-around-number-and-unit | pass | pass | pass | pass | pass | pass | pass | the units-format grammar is optional whitespace, a number, optional whitespace, an optional unit name, optional whitespace |
| 005-unit-name-must-be-letters-only | pass | pass | pass | pass | pass | pass | pass | the unit name consists only of letters (Unicode L* categories, Java isLetter()), so a unit name with a trailing digit is not a legal units-format string |
| 006-unit-before-number-is-illegal | pass | pass | pass | pass | pass | pass | pass | the units-format grammar puts the number before the unit name, not after, so a unit-then-number string is not a legal units-format value |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-simple-unquoted-string | pass | pass | pass | pass | pass | pass | pass | a sequence of characters outside a quoted string is a string value when it contains none of the forbidden characters |
| 002-quoted-alternative-for-forbidden-char | pass | pass | pass | pass | pass | pass | pass | a quoted string may always be used as an alternative to write a character that is not permitted in an unquoted string, such as '@' |
| 003-unquoted-cannot-contain-at-sign | pass | pass | fail | pass | pass | pass | pass | '@' is one of the listed forbidden characters, so it cannot appear inside an unquoted string |
| 004-double-slash-starts-comment | pass | pass | fail | pass | fail | pass | pass | the two-character string '//' starts a comment, ending the unquoted string before it even mid-token |
| 005-single-slash-allowed | pass | pass | pass | pass | pass | pass | pass | only the two-character string '//' is special for comments; a lone '/' is not a forbidden character and stays part of the unquoted string |
| 006-truefoo-parses-as-true-then-foo | pass | fail | pass | pass | pass | pass | pass | worked example: truefoo parses as the boolean token true followed by the unquoted string foo, because its initial characters parse as true |
| 007-footrue-is-one-unquoted-string | pass | pass | pass | pass | pass | pass | pass | worked example: footrue does not begin with true, false, null, or a number, so it parses as a single unquoted string |
| 008-number-then-suffix-string | pass | pass | pass | pass | pass | pass | pass | worked example: 10.0bar is the number 10.0 followed by the unquoted string bar, since 10.0 parses as a number at the start |
| 009-digits-not-at-start-stay-string | pass | pass | pass | pass | pass | pass | pass | worked example: bar10.0 does not begin with a digit, so the whole thing is the single unquoted string bar10.0, not a number |
| 010-embedded-null-not-recognized | pass | pass | pass | pass | pass | pass | pass | embedded (non-initial) null, true, false, and numbers are not recognized as such: they are just part of the string, since only the leading characters are checked |
| 011-unquoted-cannot-contain-backslash | pass | pass | fail | pass | fail | pass | pass | backslash is a forbidden character, and unquoted strings support no escaping at all, so a literal backslash requires a quoted string instead |
| 012-unquoted-cannot-start-with-hyphen | fail | fail | fail | fail | fail | fail | pass | an unquoted string may not begin with a hyphen, and the initial number character plus any valid-in-JSON number characters that follow must be parsed as a number value -- here that is the lone '-', which is not a number. typesafe/config falls back to unquoted text and yields the string '-foo', the same leniency it shows for '1e'.java: lenient |
| 013-negative-number-then-string | pass | pass | pass | pass | pass | pass | pass | a hyphen followed by digits begins number parsing (-1), after which foo continues as a separate unquoted string token, mirroring the 10.0bar example for negative numbers |
| 014-hash-starts-comment-not-forbidden-char-in-string | pass | pass | fail | pass | pass | pass | pass | '#' is forbidden inside an unquoted string because it already has meaning in HOCON: it starts a comment |
| 015-reserved-forbidden-char-no-current-meaning | pass | pass | fail | pass | pass | pass | pass | some forbidden characters, such as '!', have no meaning in HOCON today and are reserved as keywords for future extensions to the spec |
| 016-control-char-allowed-unquoted | pass | pass | pass | pass | pass | pass | pass | unquoted strings place no restriction on control characters, other than the forbidden characters listed |
| 017-control-char-forbidden-in-quoted | pass | pass | fail | fail | fail | fail | pass | quoted JSON strings may not contain control characters, per the JSON spec, unlike unquoted strings |
| 018-leading-dot-is-not-a-number | pass | pass | fail | pass | fail | pass | pass | only 0-9 and - begin a number, so .33 is an ordinary unquoted string -- the reading lightbend/config#861 extends to -.33 |
| 019-dollar-must-start-a-substitution | pass | pass | fail | pass | pass | pass | pass | $ is a forbidden character in an unquoted string, and outside quotes it may only begin ${; $foo is neither |
| 020-plus-must-be-followed-by-equals | pass | pass | fail | pass | pass | pass | pass | + is a forbidden character in an unquoted string and outside quotes only forms the += separator; a + with nothing after it, here at the end of the file, is invalid |
| 021-plus-inside-a-number-illegal | pass | pass | fail | pass | fail | pass | pass | 1+2 begins like a number and is not one; + is a forbidden character, so the token cannot fall back to an unquoted string either |
| 022-digit-then-exponent-letter-illegal | fail | fail | fail | fail | fail | fail | pass | 1e begins with a digit, so the run of number characters 1e must be parsed as a number, and it is not one; typesafe/config falls back to the unquoted string "1e", the same leniency it shows for -foojava: lenient |
| 023-unquoted-string-ends-at-end-of-file | pass | pass | pass | pass | pass | pass | pass | an unquoted string runs to a forbidden character or whitespace, and the end of the file ends it as well; no final newline is needed |
| 024-escape-sequence-in-unquoted-string-illegal | pass | pass | fail | pass | fail | pass | pass | unquoted strings support no escaping, so \u00e9 is not decoded, and the backslash it needs is forbidden in an unquoted string; the quoted "caf\u00e9" is how to write it |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-simple-values-concatenate-to-string | pass | pass | pass | pass | pass | pass | pass | if all the values are simple values they are concatenated into a string |
| 004-string-concatenation-allowed-in-field-keys | pass | pass | pass | pass | pass | pass | pass | string value concatenation is allowed in field keys, in addition to field values and array elements |
| case | Java | hocon2 | pulse-pyhocon | py.hocon | pyhocon | rs.hocon | zig-hocon | rule |
|---|---|---|---|---|---|---|---|---|
| 001-nonbreaking-space-as-whitespace | pass | pass | fail | pass | fail | pass | pass | a nonbreaking space (0x00A0) is a Unicode Zs separator and must be treated as whitespace |
| 002-bom-at-start-is-whitespace | pass | pass | fail | pass | fail | pass | pass | the BOM (0xFEFF) must be treated as whitespace, not as part of the key |
| 003-unicode-line-paragraph-separator | pass | pass | fail | fail | pass | fail | pass | the line and paragraph separators U+2028 and U+2029 are whitespace but not newlines, so they do not separate two fields: 1 b concatenates and the second = is a stray token |
| 004-vertical-tab-form-feed-whitespace | pass | pass | fail | pass | fail | pass | pass | vertical tab (0x000B) and form feed (0x000C) are whitespace characters |
| 005-ascii-separator-control-chars | pass | pass | pass | fail | pass | fail | pass | the ASCII separator controls 0x1C-0x1F are whitespace but not newlines, so they do not separate two fields |
| 006-special-nonbreaking-spaces | pass | pass | fail | pass | fail | pass | pass | the spec names figure space (0x2007) and narrow no-break space (0x202F) explicitly among the nonbreaking spaces treated as whitespace |
| 007-tab-is-whitespace | pass | pass | pass | pass | pass | pass | pass | a tab is whitespace around a key and a separator |
| 008-crlf-line-endings | pass | pass | pass | pass | pass | pass | pass | a carriage return is whitespace and the line feed beside it is the newline, so CRLF line endings separate fields; if the CR were text the first value would be "1\r" |
| 009-lone-carriage-return-is-not-a-newline | pass | fail | fail | fail | fail | fail | pass | a lone carriage return is whitespace but not a newline, so 1 b concatenates and the second = is a stray token |
| 010-line-separator-is-whitespace | pass | pass | fail | pass | fail | pass | pass | U+2028 around the separator is whitespace, not part of the key or the value |
| 011-ascii-separator-controls-are-whitespace | pass | pass | fail | pass | fail | pass | pass | the file, group, record and unit separators 0x1C-0x1F are whitespace around keys and separators |
| 012-ideographic-space-is-whitespace | pass | pass | fail | pass | fail | pass | pass | the ideographic space U+3000 is a Zs space separator and so whitespace, like every member of the category |
| 013-bom-inside-the-document-is-whitespace | pass | pass | fail | pass | fail | pass | pass | the BOM is whitespace wherever it appears, not only as a file-start marker |