json5-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 54db9905ae19b2ec82f8db3fb27732ce3332b04daf7515918939fcabb5fe2170
4
+ data.tar.gz: '05096708a51dd8e323c174164d7fa03e09079a05e21254e96caf3d9d8863ef64'
5
+ SHA512:
6
+ metadata.gz: 7c19286aaa870ced8fb58f3f5bf81214a0af14455ed30689f36e0dd47d373ab827b0aafe72c5c6b49f8b02a3cbaefbc5b7f8d874f8279f365ebb1958054691cd
7
+ data.tar.gz: 52ef17204f488be48a0d6b31773e2c65025337f76d24867a5582400376658784143b8271f22063ea38449cc69fb15f15d40d467394ef4e715b4e175e531d5608
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/NOTICE ADDED
@@ -0,0 +1,6 @@
1
+ This project includes generated parser artifacts produced with Ibex 0.3.0.
2
+ Ibex is used during development and is not a runtime dependency.
3
+
4
+ UnicodeData-3.0.0.txt is Copyright © 1991-2000 Unicode, Inc. and is
5
+ redistributed under the Unicode License V3 included in
6
+ tool/unicode/UNICODE-LICENSE.txt.
data/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # json5-ruby
2
+
3
+ `json5-ruby` is a Pure Ruby JSON5 1.0.0 parser. Its syntactic parser is
4
+ generated from `grammar/json5.y` with Ibex 0.3.0; Ibex and ibex-runtime are
5
+ development/build dependencies only.
6
+
7
+ ## Installation
8
+
9
+ ```ruby
10
+ gem "json5-ruby"
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ruby
16
+ require "json5"
17
+
18
+ JSON5.parse(<<~JSON5)
19
+ {
20
+ unquoted: 'value',
21
+ hex: 0x2a,
22
+ values: [true, null,],
23
+ }
24
+ JSON5
25
+ # {"unquoted"=>"value", "hex"=>42, "values"=>[true, nil]}
26
+ ```
27
+
28
+ The parser accepts comments, single-quoted strings, trailing commas,
29
+ IdentifierName keys, hexadecimal numbers, `Infinity`, and `NaN`. Input must be
30
+ a valid UTF-8 or US-ASCII Ruby `String`; other encodings are not implicitly
31
+ transcoded.
32
+
33
+ `JSON5.parse` returns Ruby values by default. Use `number_mode: :float` or
34
+ `:lexeme`, `string_mode: :code_units` for exact UTF-16 code units, and
35
+ `duplicate_keys: :first`, `:error`, or `:preserve` when the default last-value
36
+ policy is not appropriate. Lone surrogates in values require code-unit mode or
37
+ `parse_document`; `duplicate_keys: :preserve` also retains lone-surrogate
38
+ member names as `JSON5::ECMAString` keys because Ruby UTF-8 strings cannot
39
+ represent them losslessly.
40
+
41
+ Set `warn_duplicate_keys: true` to emit `duplicate_key` diagnostics for
42
+ duplicates discarded by the `:last` or `:first` policies.
43
+
44
+ ```ruby
45
+ document = JSON5.parse_document("{ /* keep me */ answer: 0x2a, }")
46
+ document.root.raw
47
+ # "{ /* keep me */ answer: 0x2a, }"
48
+ ```
49
+
50
+ `Document` nodes retain byte spans, raw lexemes, decoded code units, and
51
+ leading/trailing trivia. Member nodes expose trivia before `:` and around the
52
+ comma/closing separator; array nodes expose the corresponding element
53
+ separator trivia. The source is copied and frozen by default.
54
+
55
+ ## Limits and diagnostics
56
+
57
+ `JSON5::Limits.default` protects input size, nesting depth, string and number
58
+ length, comment length, and container entries. Pass `limits:
59
+ JSON5::Limits.unbounded` only for trusted input. Diagnostics can be collected in
60
+ an array or sent to a callable; by default warnings are sent to `Warning.warn`.
61
+ Unescaped U+2028 and U+2029 in strings produce diagnostics.
62
+
63
+ The defaults are 64 MiB input, 512 nesting levels, 16 Mi UTF-16 string code
64
+ units, 1 MiB number tokens, 16 MiB comments, and 10,000,000 entries per
65
+ container. Construct `JSON5::Limits` with individual keyword overrides when an
66
+ application needs a different boundary; a `nil` field disables that one limit.
67
+
68
+ ```ruby
69
+ warnings = []
70
+ JSON5.parse("'line\u2028separator'", diagnostics: warnings)
71
+ warnings.first.code
72
+ # "unescaped_line_separator_in_string"
73
+ ```
74
+
75
+ ## Development
76
+
77
+ ```sh
78
+ bundle install
79
+ bundle exec rake
80
+ bundle exec rake generated
81
+ bundle exec rake conformance
82
+ bundle exec rake fuzz
83
+ bundle exec rake security
84
+ bundle exec rake package
85
+ ```
86
+
87
+ `bundle exec rake release:check` runs all deterministic release gates above. The
88
+ generated and conformance tasks are read-only: intentional updates use
89
+ `bundle exec rake generated:update` and `bundle exec rake conformance:update`,
90
+ followed by the corresponding read-only check. `rake generated` regenerates into
91
+ a temporary directory, runs Ibex's own `--check` against that raw output, then
92
+ compares the canonicalized parser, manifest, automaton, verifier report, metrics,
93
+ and Unicode table byte-for-byte with the committed artifacts. The package gate verifies that
94
+ runtime dependencies are empty, inspects gem contents, installs into a clean
95
+ temporary gem home, and smoke-tests the installed artifact.
96
+
97
+ Performance thresholds are evaluated only between results captured on the same
98
+ fixture, Ruby runtime, and machine. Capture before/after files with
99
+ `benchmark/run.rb`, then run an explicit threshold gate:
100
+
101
+ ```sh
102
+ ruby benchmark/run.rb --iterations=25 --warmup=5 --output=before.json
103
+ # apply the candidate change, using the same host and Ruby
104
+ ruby benchmark/run.rb --iterations=25 --warmup=5 --output=after.json
105
+ bundle exec rake "benchmark:check[before.json,after.json,5,5]"
106
+ ```
107
+
108
+ The final two arguments are the maximum allowed median-runtime and allocation
109
+ regression percentages. The comparison requires identical fixture/result
110
+ checksums, Ruby/OS/CPU/compiler/host metadata, warmup, batch, and iteration
111
+ settings, YJIT state, and relevant environment knobs; incompatible or invalid
112
+ results are rejected. GitHub-hosted CI uploads a report-only artifact because
113
+ those runners do not provide a stable performance environment. Maintainers run
114
+ the `Benchmark Gate` workflow at the immutable `benchmark-builder-v12` tag with
115
+ a reviewed full candidate SHA. The base is fixed by
116
+ `benchmark/baseline.commit`; callers cannot substitute it.
117
+ `benchmark/trusted_builder.commit` pins the workflow revision that supplies the
118
+ trusted harness, comparator, manifest, and fixtures on a one-job `ephemeral` +
119
+ `json5-benchmark` self-hosted runner. It covers value, lexer, document, and error
120
+ paths, uses predeclared balanced before/after process rounds, enforces both 5%
121
+ thresholds on every round and their aggregate, and checks 1x/2x/4x linearity. Never attach
122
+ the label to a persistent runner: candidate parser code is executed, so the
123
+ runner must be isolated, one-time, and kept on a stable power/performance
124
+ profile. The runner must be Linux, provide Bubblewrap at `/usr/bin/bwrap`, and
125
+ provide Ruby from `/opt/hostedtoolcache`. Candidate code runs without network or
126
+ inherited credentials in isolated user, mount, PID, IPC, UTS, cgroup, and
127
+ network namespaces; its host inputs are read-only and only an ephemeral `/tmp`
128
+ is writable.
129
+
130
+ The workflow artifact is a tar archive containing aggregate `evidence.json`, a
131
+ SHA-256 file manifest, and all raw measurements. A separate GitHub-hosted job
132
+ revalidates it and creates a GitHub artifact attestation bound to the pinned
133
+ builder workflow. The signed contents bind the exact candidate release commit.
134
+ Before release, set `JSON5_BENCHMARK_EVIDENCE` to the downloaded `.tar.gz` file;
135
+ the release task verifies that attestation before it accepts the fixed base,
136
+ exact release/harness commit, complete manifest coverage, passing comparisons,
137
+ and passing linearity evidence. This verification requires GitHub CLI network
138
+ access.
139
+
140
+ `release:check` also refuses uncommitted or untracked files, so commit intended
141
+ generated evidence before running the final release gate.
142
+
143
+ The generated parser, Automaton IR, verification report, metrics, grammar,
144
+ source manifest, fixed Unicode 3.0.0 input, and conformance matrix are checked
145
+ in. See `docs/conformance-matrix.md` for the coverage map and the
146
+ design/work-plan documents under `.idea/` for the implementation boundary and
147
+ release checklist.
148
+
149
+ Security issues should be reported privately as described in [SECURITY.md](SECURITY.md).
150
+ Conformance scope and known limitations are recorded in
151
+ [docs/conformance-report.md](docs/conformance-report.md).
152
+
153
+ ## Compatibility
154
+
155
+ The public API is `JSON5`; `Json5` remains an alias for the initial gem
156
+ scaffold. The supported Ruby version is 3.2 or newer. This project does not
157
+ execute input as Ruby code and never converts member names to symbols.
158
+
159
+ ## License
160
+
161
+ MIT. See [LICENSE.txt](LICENSE.txt).
data/SECURITY.md ADDED
@@ -0,0 +1,9 @@
1
+ # Security policy
2
+
3
+ Please do not disclose security vulnerabilities in public issues or pull
4
+ requests. Report them privately to the project maintainer through the contact
5
+ address listed in the gem metadata, including a description, reproduction
6
+ steps, affected versions, and any suggested mitigation.
7
+
8
+ Please allow reasonable time for triage and coordinated disclosure. Do not
9
+ include secrets or unrelated personal data in a report.
@@ -0,0 +1,90 @@
1
+ # JSON5 1.0.0 conformance matrix
2
+
3
+ The matrix tracks the behavior required by the JSON5 1.0.0 specification and the
4
+ ECMAScript 5.1 lexical rules used by it.
5
+
6
+ Fixture IDs are stable evidence identifiers. Their source files, typed expected
7
+ results, and provenance are recorded in `spec/fixtures/conformance/cases.yml`.
8
+
9
+ | ID | Spec section | Requirement | Positive tests | Negative tests | Implementation | Status |
10
+ |---|---|---|---|---|---|---|
11
+ | TEXT-001 | JSON5Text | empty input is invalid because one value is required | - | `TEXT-EMPTY-001` | `grammar/json5.y` | covered |
12
+ | TEXT-002 | JSON5Text | trailing whitespace and comments are accepted | `TEXT-TRIVIA-001` | - | `lib/json5/lexer.rb` | covered |
13
+ | TEXT-003 | JSON5Text | a second top-level value is rejected | - | `TEXT-TRAILING-001` | `lib/json5/parser.rb` | covered |
14
+ | TEXT-004 | JSON5Text | punctuation suffix tokens after a value are rejected | - | `TEXT-SUFFIX-001` | `lib/json5/parser.rb` | covered |
15
+ | VALUE-001 | JSON5Value | object, array, string, number, Boolean, and null are top-level values | `TEXT-TOPLEVEL-001`<br>`VALUE-STRING-001`<br>`VALUE-BOOLEAN-001`<br>`VALUE-NULL-001`<br>`VALUE-TOPLEVEL-001` | - | `grammar/json5.y` | covered |
16
+ | OBJ-001 | JSON5Object / JSON5Array | empty containers are accepted | `OBJ-EMPTY-001` | - | `grammar/json5.y` | covered |
17
+ | OBJ-002 | JSON5Member / JSON5ElementList | one member or element is accepted | `OBJ-SINGLE-001` | - | `grammar/json5.y` | covered |
18
+ | OBJ-003 | JSON5MemberList / JSON5ElementList | multiple members or elements are accepted | `OBJ-MULTIPLE-001` | - | `grammar/json5.y` | covered |
19
+ | OBJ-004 | JSON5Object / JSON5Array | one trailing comma is accepted | `OBJECT-GRAMMAR-001`<br>`ARRAY-TRAILING-001` | - | `grammar/json5.y` | covered |
20
+ | OBJ-005 | JSON5Object / JSON5Array | a second trailing comma is rejected | - | `OBJECT-DOUBLE-COMMA-001`<br>`ARR-DOUBLE-COMMA-001` | `grammar/json5.y` | covered |
21
+ | ARR-001 | JSON5Array | array holes are rejected | - | `ARRAY-HOLE-001` | `grammar/json5.y` | covered |
22
+ | OBJ-006 | JSON5MemberName | quoted and unquoted member names are accepted | `OBJ-NAMES-001` | - | `grammar/json5.y` | covered |
23
+ | OBJ-007 | JSON5MemberName | literal keyword spellings are accepted as member names | `OBJECT-KEYWORDS-001` | - | `grammar/json5.y` | covered |
24
+ | DUP-001 | API duplicate policy | last, first, error, and preserve policies are fixed | `OBJECT-DUPLICATE-001`<br>`DUP-POLICIES-001` | - | `lib/json5/ruby_value_builder.rb` | covered |
25
+ | WS-001 | Whitespace | U+0009, U+000A, U+000B, U+000C, U+000D, and U+0020 are accepted | `WS-ASCII-001` | - | `lib/json5/lexer.rb` | covered |
26
+ | WS-002 | Whitespace | U+00A0, U+2028, U+2029, and U+FEFF are accepted | `WS-UNICODE-001` | - | `lib/json5/lexer.rb` | covered |
27
+ | WS-003 | Whitespace | every pinned Unicode 3.0 Zs character is accepted | `NUMBER-WHITESPACE-001`<br>`WS-ALL-ZS-001` | - | `lib/json5/unicode_tables.rb` | covered |
28
+ | COMMENT-001 | SingleLineComment | line comments terminate at LF, CRLF, CR, LS, PS, or EOF | `COMMENT-EOL-001` | - | `lib/json5/lexer.rb` | covered |
29
+ | COMMENT-002 | MultiLineComment | a closed block comment is trivia | `COMMENT-SIMPLE-001` | - | `lib/json5/lexer.rb` | covered |
30
+ | COMMENT-003 | MultiLineComment | nested-looking text closes at the first closing delimiter | `COMMENT-BLOCK-001` | - | `lib/json5/lexer.rb` | covered |
31
+ | COMMENT-004 | MultiLineComment | an unclosed block comment is rejected | - | `COMMENT-UNCLOSED-001` | `lib/json5/lexer.rb` | covered |
32
+ | COMMENT-005 | Comments | a slash that does not start a comment is rejected | - | `COMMENT-SLASH-001` | `lib/json5/lexer.rb` | covered |
33
+ | COMMENT-006 | Lexical token boundaries | comments inside identifiers and numbers are rejected | - | `COMMENT-TOKEN-BOUNDARY-001` | `lib/json5/lexer.rb` | covered |
34
+ | IDENT-001 | IdentifierStart | ASCII letters, dollar signs, and underscores are accepted | `IDENT-ASCII-001` | - | `lib/json5/lexer.rb` | covered |
35
+ | IDENT-002 | IdentifierPart | decimal digits are accepted only after the start | `IDENT-DIGIT-001` | - | `lib/json5/unicode_tables.rb` | covered |
36
+ | IDENT-003 | IdentifierPart | combining marks are accepted only after the start | `IDENT-COMBINING-001` | - | `lib/json5/unicode_tables.rb` | covered |
37
+ | IDENT-004 | IdentifierPart | ZWNJ and ZWJ are accepted only after the start | `IDENT-ZWNJ-ZWJ-001` | - | `lib/json5/unicode_tables.rb` | covered |
38
+ | IDENT-005 | UnicodeEscapeSequence | four-digit Unicode escapes are accepted by position | `IDENT-UNICODE-ESCAPE-001` | - | `lib/json5/lexer.rb` | covered |
39
+ | IDENT-006 | UnicodeEscapeSequence | invalid hex count and alternate escape forms are rejected | - | `IDENT-ESCAPE-LENGTH-001` | `lib/json5/lexer.rb` | covered |
40
+ | IDENT-007 | IdentifierName | decoded escape code units are revalidated as start or part | - | `IDENT-ESCAPE-BOUNDARY-001` | `lib/json5/unicode_tables.rb` | covered |
41
+ | IDENT-008 | IdentifierName / JSON5Value | escaped keywords are member names but not literal values | `IDENT-ESCAPED-KEYWORD-001` | `IDENT-ESCAPED-KEYWORD-002` | `lib/json5/lexer.rb` | covered |
42
+ | IDENT-009 | IdentifierName / JSON5Value | truex and Infinityx are identifiers by maximal munch | - | `IDENT-KEYWORD-SUFFIX-001`<br>`IDENT-KEYWORD-SUFFIX-COMPLETE-001` | `lib/json5/lexer.rb` | covered |
43
+ | IDENT-010 | JSON5MemberName | arbitrary strings are accepted as quoted keys | `IDENT-QUOTED-KEY-001` | - | `grammar/json5.y` | covered |
44
+ | IDENT-011 | IdentifierName | pinned Unicode letters are accepted as member names | `IDENT-UNICODE-001` | - | `lib/json5/unicode_tables.rb` | covered |
45
+ | STRING-001 | StringLiteral | single- and double-quoted strings are accepted | `STRING-QUOTES-001`<br>`STRING-QUOTE-PAIR-001` | - | `lib/json5/lexer.rb` | covered |
46
+ | STRING-002 | StringLiteral | mismatched delimiters are rejected | - | `STRING-MISMATCH-001` | `lib/json5/lexer.rb` | covered |
47
+ | STRING-003 | EscapeSequence | every named escape decodes to its specified code unit | `STRING-NAMED-ESCAPES-001` | - | `lib/json5/lexer.rb` | covered |
48
+ | STRING-004 | EscapeSequence | null escape is accepted and a following decimal digit is rejected | `STRING-ESCAPE-001` | `STRING-NULL-DIGIT-001` | `lib/json5/lexer.rb` | covered |
49
+ | STRING-005 | HexEscapeSequence | exactly two hexadecimal digits are required | `STRING-HEX-ESCAPE-001` | `STRING-ESCAPE-MALFORMED-001` | `lib/json5/lexer.rb` | covered |
50
+ | STRING-006 | UnicodeEscapeSequence | exactly four hexadecimal digits decode one UTF-16 code unit | `STRING-UNICODE-ESCAPE-001` | - | `lib/json5/lexer.rb` | covered |
51
+ | STRING-007 | NonEscapeCharacter | non-special escape characters and escaped slash are accepted | `STRING-NONESCAPE-001` | - | `lib/json5/lexer.rb` | covered |
52
+ | STRING-008 | EscapeSequence | decimal escapes one through nine are rejected | - | `STRING-DECIMAL-ESCAPE-001` | `lib/json5/lexer.rb` | covered |
53
+ | STRING-009 | LineContinuation | LF, CR, CRLF, LS, and PS continuations are accepted | `STRING-CONTINUATION-001` | - | `lib/json5/lexer.rb` | covered |
54
+ | STRING-010 | StringLiteral | unescaped LF and CR line terminators are rejected | - | `STRING-RAW-LINE-001` | `lib/json5/lexer.rb` | covered |
55
+ | STRING-011 | StringLiteral / diagnostics | unescaped U+2028 and U+2029 are accepted with warnings | `STRING-LS-001`<br>`STRING-PS-001` | - | `lib/json5/diagnostic.rb` | covered |
56
+ | STRING-012 | UTF-16 conversion | lone surrogates are preserved in code-unit mode and valid pairs combine | `STRING-LONE-SURROGATE-001`<br>`STRING-SURROGATE-PAIR-001` | `STRING-LONE-SURROGATE-002` | `lib/json5/ecma_string.rb` | covered |
57
+ | STRING-013 | StringLiteral | unescaped NUL and non-line control characters are preserved | `STRING-CONTROL-001` | - | `lib/json5/lexer.rb` | covered |
58
+ | STRING-014 | StringLiteral | unterminated quotes and terminal backslashes are rejected | - | `STRING-UNTERMINATED-001` | `lib/json5/lexer.rb` | covered |
59
+ | NUMBER-001 | DecimalIntegerLiteral | zero and non-zero integers are accepted | `NUMBER-INTEGER-001` | - | `lib/json5/lexer.rb` | covered |
60
+ | NUMBER-002 | DecimalIntegerLiteral | decimal integers with a leading zero are rejected | - | `NUMBER-LEADING-ZERO-001` | `lib/json5/lexer.rb` | covered |
61
+ | NUMBER-003 | DecimalLiteral | 1.0, .1, 1., and 1.e2 shapes are accepted | `NUMBER-DECIMAL-001`<br>`NUMBER-DECIMAL-SHAPES-001` | - | `lib/json5/lexer.rb` | covered |
62
+ | NUMBER-004 | ExponentPart | lower/upper E and absent/plus/minus exponent signs are accepted | `NUMBER-EXPONENT-CASE-001` | - | `lib/json5/lexer.rb` | covered |
63
+ | NUMBER-005 | ExponentPart | an exponent without decimal digits is rejected | - | `NUMBER-EXPONENT-001` | `lib/json5/lexer.rb` | covered |
64
+ | NUMBER-006 | HexIntegerLiteral | lower- and upper-case hexadecimal prefixes are accepted | `NUMBER-HEX-001` | - | `lib/json5/lexer.rb` | covered |
65
+ | NUMBER-007 | HexIntegerLiteral | a hexadecimal prefix without valid digits is rejected | - | `NUMBER-MALFORMED-HEX-001` | `lib/json5/lexer.rb` | covered |
66
+ | NUMBER-008 | NumericLiteral | explicit plus and minus are accepted on finite number forms | `NUMBER-FINITE-SIGNS-001` | - | `lib/json5/lexer.rb` | covered |
67
+ | NUMBER-009 | NumericLiteral | Infinity and NaN accept absent, plus, and minus signs atomically | `NUMBER-INFINITY-001`<br>`NUMBER-NAN-001`<br>`NUMBER-SIGNED-001`<br>`NUMBER-SPECIAL-SIGNS-COMPLETE-001` | - | `lib/json5/lexer.rb` | covered |
68
+ | NUMBER-010 | NumericLiteral | case-mismatched Infinity and NaN spellings are rejected | - | `NUMBER-SPECIAL-CASE-001` | `lib/json5/lexer.rb` | covered |
69
+ | NUMBER-011 | NumericLiteral | invalid identifier suffixes cannot follow numeric or special values | - | `NUMBER-TOKEN-BOUNDARY-001` | `lib/json5/lexer.rb` | covered |
70
+ | NUMBER-012 | NumericLiteral | whitespace or comments cannot separate a sign from its body | - | `NUMBER-COMMENT-SIGN-001`<br>`NUMBER-SIGN-TRIVIA-001` | `lib/json5/lexer.rb` | covered |
71
+ | NUMBER-013 | Numeric conversion | minus-zero sign and binary64 bit pattern are preserved | `NUMBER-NEGATIVE-ZERO-001`<br>`NUMBER-FLOAT-BITS-001` | - | `lib/json5/number_value.rb` | covered |
72
+ | NUMBER-014 | Numeric conversion / binary64 | rounding, overflow, and underflow follow fixed binary64 oracles | `NUMBER-HEX-ROUNDING-001`<br>`NUMBER-BINARY64-BOUNDARY-001` | - | `lib/json5/number_value.rb` | covered |
73
+ | ENC-001 | Input encoding | invalid UTF-8 is rejected | - | `ENC-INVALID-001` | `lib/json5/input.rb` | covered |
74
+ | UNICODE-001 | ECMAScript 5.1 IdentifierName | pinned Unicode 3.0.0 predicates match every BMP code point | `UNICODE-BMP-001` | - | `lib/json5/unicode_tables.rb` | covered |
75
+ | LIMIT-001 | API limits | configured limits fail early | - | `LIMIT-BOUNDARY-001` | `lib/json5/limits.rb` | covered |
76
+ | INPUT-001 | API load | load enforces limits even when a reader ignores chunk size | - | `INPUT-LIMIT-001` | `lib/json5/parser.rb` | covered |
77
+ | API-001 | API option validation | load and Document reject invalid or unsupported options | - | `API-OPTIONS-001` | `lib/json5/parser.rb` | covered |
78
+ | ERROR-001 | Error model | parser and builder errors expose source context | - | `ERROR-CONTEXT-001` | `lib/json5/errors.rb` | covered |
79
+ | ERROR-002 | Error model | EOF diagnostics never extend beyond the source | - | `ERROR-EOF-001` | `lib/json5/lexer.rb` | covered |
80
+ | PERF-001 | Resource safety | literal token context is released after reduction | - | `PERF-TOKEN-RETENTION-001` | `lib/json5/ruby_value_builder.rb` | covered |
81
+ | DOC-001 | Document mode | lossless document spans and trivia | `DOC-LOSSLESS-001` | - | `lib/json5/document_parser.rb` | covered |
82
+ | DIFF-001 | Strict JSON subset | values agree with the standard-library JSON parser | `JSON-DIFFERENTIAL-001` | - | `lib/json5/parser.rb` | covered |
83
+
84
+ ## Fuzz regressions
85
+
86
+ The deterministic generator configuration and invalid-mutation registry live in
87
+ `spec/fixtures/conformance/fuzz.yml`. When a seed finds a failure, follow the
88
+ minimization and stable-fixture procedure in `spec/fixtures/conformance/README.md`;
89
+ the generated source is never accepted or rejected solely because another JSON5
90
+ implementation behaves the same way.
@@ -0,0 +1,29 @@
1
+ # JSON5 conformance report
2
+
3
+ Generated from `docs/conformance-matrix.md`.
4
+
5
+ - JSON5 specification: 1.0.0 (`36f9418ed339580e362eb370fd084a235e361b8e`)
6
+ - ECMAScript lexical baseline: 5.1 / Unicode 3.0.0
7
+ - Ibex: 0.3.0, embedded Pure Ruby parser
8
+ - Supported Ruby CI matrix: 3.2, 3.3, 3.4, 4.0
9
+ - Matrix rows: 72
10
+ - Covered: 72
11
+ - Planned: 0
12
+ - Blocked: 0
13
+ - Generated automaton conflicts: 0
14
+ - Strict Automaton IR verification: valid
15
+ - Specification-derived fixtures: 33 (22 positive, 11 negative, 2 diagnostic-bearing)
16
+ - Linked executable evidence examples: 62
17
+ - Deterministic fuzz seeds: 12648430, 1592594996, 3735928559
18
+ - Deterministic fuzz iterations: 80 per seed (240 total)
19
+ - Deterministic fuzz corpus SHA-256: `52102866735c93ce7a3efbec11b512862c071ea43e2a30728903e4f8e3f0f156`
20
+ - Single-edit invalid mutations: 9
21
+ - Source provenance: `tool/sources.yml`; pinned Unicode SHA-256 verified
22
+ - Official JSON5 corpus: not vendored; coverage is specification-derived
23
+ - External differential: not used as the conformance oracle
24
+ - Known limitations: none within the documented strict JSON5 1.0.0 scope
25
+ - Performance baseline: `benchmark/results/baseline.json`
26
+
27
+ The report is evidence for the repository's current test suite. Performance
28
+ results remain machine-specific. The matrix does not by itself constitute a
29
+ claim that every third-party corpus or extension is supported.
data/grammar/json5.y ADDED
@@ -0,0 +1,74 @@
1
+ class JSON5::GeneratedParser
2
+ token STRING NUMBER IDENTIFIER_NAME TRUE FALSE NULL INFINITY NAN
3
+ start json5_text
4
+ expect 0
5
+ options no_result_var
6
+ rule
7
+ json5_text
8
+ : value { result = val[0] }
9
+
10
+ value
11
+ : object { result = val[0] }
12
+ | array { result = val[0] }
13
+ | STRING { result = @builder.string(val[0]) }
14
+ | NUMBER { result = @builder.number(val[0]) }
15
+ | TRUE { result = @builder.literal(val[0], true) }
16
+ | FALSE { result = @builder.literal(val[0], false) }
17
+ | NULL { result = @builder.literal(val[0], nil) }
18
+ | INFINITY { result = @builder.number(val[0]) }
19
+ | NAN { result = @builder.number(val[0]) }
20
+
21
+ object
22
+ : '{' '}' { result = @builder.object([], val[0], val[1]) }
23
+ | '{' members '}' { result = @builder.object(val[1], val[0], val[2]) }
24
+ | '{' members ',' '}' { result = @builder.object(val[1], val[0], val[3], val[2]) }
25
+
26
+ members
27
+ : member { result = @builder.member_list(val[0]) }
28
+ | members ',' member { result = @builder.append_member(val[0], val[2], val[1]) }
29
+
30
+ member
31
+ : member_name ':' value { result = @builder.member(val[0], val[1], val[2]) }
32
+
33
+ member_name
34
+ : STRING { result = @builder.member_name(val[0]) }
35
+ | IDENTIFIER_NAME { result = @builder.member_name(val[0]) }
36
+ | TRUE { result = @builder.member_name(val[0]) }
37
+ | FALSE { result = @builder.member_name(val[0]) }
38
+ | NULL { result = @builder.member_name(val[0]) }
39
+ | INFINITY { result = @builder.member_name(val[0]) }
40
+ | NAN { result = @builder.member_name(val[0]) }
41
+
42
+ array
43
+ : '[' ']' { result = @builder.array([], val[0], val[1]) }
44
+ | '[' elements ']' { result = @builder.array(val[1], val[0], val[2]) }
45
+ | '[' elements ',' ']' { result = @builder.array(val[1], val[0], val[3], val[2]) }
46
+
47
+ elements
48
+ : value { result = @builder.element_list(val[0]) }
49
+ | elements ',' value { result = @builder.append_element(val[0], val[2], val[1]) }
50
+ end
51
+ ---- inner
52
+ def initialize(lexer, builder)
53
+ @lexer = lexer
54
+ @builder = builder
55
+ super()
56
+ end
57
+
58
+ def parse_tokens
59
+ do_parse
60
+ end
61
+
62
+ def next_token
63
+ token = @lexer.next_token
64
+ @builder.observe_token(token)
65
+ return false if token.kind == :EOF
66
+
67
+ payload = if token.kind.is_a?(String)
68
+ @builder.retain_punctuation_tokens? ? token : nil
69
+ else
70
+ token.value
71
+ end
72
+ [token.kind, payload]
73
+ end
74
+ ---- footer
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSON5
4
+ module ASCIIClassification
5
+ WHITESPACE = 1 << 0
6
+ DIGIT = 1 << 1
7
+ HEX_DIGIT = 1 << 2
8
+ IDENTIFIER_START = 1 << 3
9
+ IDENTIFIER_PART = 1 << 4
10
+ QUOTE = 1 << 5
11
+ PUNCTUATOR = 1 << 6
12
+ SLASH = 1 << 7
13
+ BACKSLASH = 1 << 8
14
+ LINE_TERMINATOR = 1 << 9
15
+
16
+ TABLE = Array.new(256, 0).tap do |table|
17
+ [0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x20].each { |byte| table[byte] |= WHITESPACE }
18
+ (0x30..0x39).each { |byte| table[byte] |= DIGIT | HEX_DIGIT | IDENTIFIER_PART }
19
+ (0x41..0x46).each { |byte| table[byte] |= HEX_DIGIT }
20
+ (0x61..0x66).each { |byte| table[byte] |= HEX_DIGIT }
21
+ (0x41..0x5a).each { |byte| table[byte] |= IDENTIFIER_START | IDENTIFIER_PART }
22
+ (0x61..0x7a).each { |byte| table[byte] |= IDENTIFIER_START | IDENTIFIER_PART }
23
+ [0x24, 0x5f].each { |byte| table[byte] |= IDENTIFIER_START | IDENTIFIER_PART }
24
+ [0x22, 0x27].each { |byte| table[byte] |= QUOTE }
25
+ [0x7b, 0x7d, 0x5b, 0x5d, 0x3a, 0x2c].each { |byte| table[byte] |= PUNCTUATOR }
26
+ table[0x2f] |= SLASH
27
+ table[0x5c] |= BACKSLASH
28
+ [0x0a, 0x0d].each { |byte| table[byte] |= LINE_TERMINATOR }
29
+ end.freeze
30
+ end
31
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSON5
4
+ class Diagnostic
5
+ attr_reader :severity, :code, :message, :filename, :byte_offset,
6
+ :end_byte_offset, :line, :column
7
+
8
+ def initialize(code:, message:, severity: :warning, filename: nil,
9
+ byte_offset: nil, end_byte_offset: nil, line: nil,
10
+ column: nil)
11
+ @severity = severity.to_sym
12
+ @code = code.to_s.dup.freeze
13
+ @message = message.to_s.dup.freeze
14
+ @filename = filename.nil? ? nil : filename.to_s.dup.freeze
15
+ @byte_offset = byte_offset
16
+ @end_byte_offset = end_byte_offset
17
+ @line = line
18
+ @column = column
19
+ freeze
20
+ end
21
+
22
+ def to_h
23
+ {
24
+ severity: severity,
25
+ code: code,
26
+ message: message,
27
+ filename: filename,
28
+ byte_offset: byte_offset,
29
+ end_byte_offset: end_byte_offset,
30
+ line: line,
31
+ column: column
32
+ }
33
+ end
34
+ end
35
+
36
+ class DiagnosticSink
37
+ def self.build(target, filename: nil)
38
+ new(target, filename: filename)
39
+ end
40
+
41
+ def self.write_default_warning(diagnostic)
42
+ Warning.warn("#{diagnostic.message} (#{diagnostic.code})\n")
43
+ end
44
+
45
+ def initialize(target, filename: nil)
46
+ @target = target
47
+ @filename = filename
48
+ end
49
+
50
+ def emit(code:, message:, byte_offset:, end_byte_offset:, line: nil, column: nil)
51
+ diagnostic = Diagnostic.new(
52
+ code: code,
53
+ message: message,
54
+ filename: @filename,
55
+ byte_offset: byte_offset,
56
+ end_byte_offset: end_byte_offset,
57
+ line: line,
58
+ column: column
59
+ )
60
+ if @target.is_a?(Array)
61
+ @target << diagnostic
62
+ elsif @target.respond_to?(:call)
63
+ @target.call(diagnostic)
64
+ elsif @target != :ignore
65
+ self.class.write_default_warning(diagnostic)
66
+ end
67
+ diagnostic
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSON5
4
+ class Document
5
+ attr_reader :source, :root, :diagnostics, :trailing_trivia, :line_map
6
+
7
+ def initialize(source:, root:, diagnostics:, trailing_trivia: [])
8
+ @source = source
9
+ @root = root
10
+ @diagnostics = diagnostics.dup.freeze
11
+ @trailing_trivia = trailing_trivia.dup.freeze
12
+ @line_map = LineMap.new(source)
13
+ freeze
14
+ end
15
+
16
+ def raw
17
+ source
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSON5
4
+ ByteSpan = Data.define(:start_byte, :end_byte)
5
+
6
+ class Node
7
+ attr_reader :type, :source, :raw_span, :start_byte, :end_byte, :leading_trivia, :trailing_trivia
8
+
9
+ def initialize(type:, source:, start_byte:, end_byte:, leading_trivia: [], trailing_trivia: [])
10
+ @type = type.to_sym
11
+ @source = source
12
+ @start_byte = start_byte
13
+ @end_byte = end_byte
14
+ @raw_span = ByteSpan.new(start_byte, end_byte)
15
+ @leading_trivia = leading_trivia.dup.freeze
16
+ @trailing_trivia = trailing_trivia.dup.freeze
17
+ freeze
18
+ end
19
+
20
+ def raw
21
+ source.byteslice(start_byte...end_byte)
22
+ end
23
+ end
24
+
25
+ class StringNode < Node
26
+ attr_reader :code_units, :quote, :escape_spans
27
+
28
+ def initialize(code_units:, quote:, escape_spans: [], **attributes)
29
+ @code_units = code_units
30
+ @quote = quote
31
+ @escape_spans = escape_spans.dup.freeze
32
+ super(type: :string, **attributes)
33
+ end
34
+ end
35
+
36
+ class NumberNode < Node
37
+ attr_reader :classification, :sign
38
+
39
+ def initialize(classification:, sign:, **attributes)
40
+ @classification = classification
41
+ @sign = sign
42
+ super(type: :number, **attributes)
43
+ end
44
+
45
+ alias lexeme raw
46
+ end
47
+
48
+ class IdentifierNameNode < Node
49
+ attr_reader :code_units, :decoded_code_units
50
+
51
+ def initialize(code_units:, **attributes)
52
+ @code_units = code_units
53
+ @decoded_code_units = code_units
54
+ super(type: :identifier_name, **attributes)
55
+ end
56
+ end
57
+
58
+ class LiteralNode < Node
59
+ attr_reader :value
60
+
61
+ def initialize(value:, **attributes)
62
+ @value = value
63
+ super(type: :literal, **attributes)
64
+ end
65
+ end
66
+
67
+ class MemberNode < Node
68
+ attr_reader :name, :value, :colon_span, :colon_start_byte, :colon_end_byte, :colon_leading_trivia
69
+
70
+ def initialize(name:, value:, colon_start_byte:, colon_end_byte:, colon_leading_trivia: [], **attributes)
71
+ @name = name
72
+ @value = value
73
+ @colon_start_byte = colon_start_byte
74
+ @colon_end_byte = colon_end_byte
75
+ @colon_span = ByteSpan.new(colon_start_byte, colon_end_byte)
76
+ @colon_leading_trivia = colon_leading_trivia.dup.freeze
77
+ super(type: :member, **attributes)
78
+ end
79
+ end
80
+
81
+ class ObjectNode < Node
82
+ attr_reader :members
83
+
84
+ def initialize(members:, **attributes)
85
+ @members = members.dup.freeze
86
+ super(type: :object, **attributes)
87
+ end
88
+ end
89
+
90
+ class ArrayNode < Node
91
+ attr_reader :elements, :element_trailing_trivia
92
+
93
+ def initialize(elements:, element_trailing_trivia: [], **attributes)
94
+ @elements = elements.dup.freeze
95
+ @element_trailing_trivia = element_trailing_trivia.map { |trivia| trivia.dup.freeze }.freeze
96
+ super(type: :array, **attributes)
97
+ end
98
+ end
99
+ end