Skip to content

Commit 70bddf2

Browse files
committed
Add native JSONC support to ext/json
Introduce two new flags accepted by json_decode() and json_validate(): - JSON_ALLOW_COMMENTS: treats // single-line comments (terminated by newline or end of input) and /* ... */ block comments as whitespace anywhere whitespace is allowed. Block comments do not nest; an unterminated /* is reported as JSON_ERROR_SYNTAX at the comment opener. Newlines inside block comments advance the line counter so error locations stay accurate. Comment-like sequences inside strings are untouched. - JSON_ALLOW_TRAILING_COMMAS: permits exactly one trailing comma after the last object member or array element ([1,] and {"a":1,}), while [,], {,}, [1,,2] and [1,,] remain syntax errors. Comments are handled in the re2c scanner (rules match unconditionally, the flag is checked in the action so the flag-off error is byte identical to the previous behavior), trailing commas via new "member ','" / "element ','" productions in the bison grammar with the flag checked at reduce time, keeping the grammar conflict-free and the flag-off error code and line:column identical to before. The partially built container is freed explicitly on the YYERROR path since bison does not run destructors for the RHS of the failing rule. Using both flags together makes json_decode() a superset of JSONC as used by VS Code configuration files, tsconfig.json and similar formats. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TUEZ4aW8xZqzutZBAxX9UJ
1 parent de5a582 commit 70bddf2

13 files changed

Lines changed: 487 additions & 5 deletions

ext/json/json.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,8 +351,8 @@ PHP_FUNCTION(json_validate)
351351
ZEND_PARSE_PARAMETERS_END();
352352

353353

354-
if ((options != 0) && (options != PHP_JSON_INVALID_UTF8_IGNORE)) {
355-
zend_argument_value_error(3, "must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE)");
354+
if ((options & ~(PHP_JSON_INVALID_UTF8_IGNORE | PHP_JSON_ALLOW_COMMENTS | PHP_JSON_ALLOW_TRAILING_COMMAS)) != 0) {
355+
zend_argument_value_error(3, "must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE, JSON_ALLOW_COMMENTS, JSON_ALLOW_TRAILING_COMMAS)");
356356
RETURN_THROWS();
357357
}
358358

ext/json/json.stub.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,17 @@
9090
*/
9191
const JSON_THROW_ON_ERROR = UNKNOWN;
9292

93+
/**
94+
* @var int
95+
* @cvalue PHP_JSON_ALLOW_COMMENTS
96+
*/
97+
const JSON_ALLOW_COMMENTS = UNKNOWN;
98+
/**
99+
* @var int
100+
* @cvalue PHP_JSON_ALLOW_TRAILING_COMMAS
101+
*/
102+
const JSON_ALLOW_TRAILING_COMMAS = UNKNOWN;
103+
93104
/**
94105
* @var int
95106
* @cvalue PHP_JSON_ERROR_NONE

ext/json/json_arginfo.h

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/json/json_parser.y

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ members:
119119
}
120120
}
121121
| member
122+
| member ','
123+
{
124+
if (!(parser->scanner.options & PHP_JSON_ALLOW_TRAILING_COMMAS)) {
125+
parser->scanner.errcode = PHP_JSON_ERROR_SYNTAX;
126+
zval_ptr_dtor_nogc(&$1);
127+
YYERROR;
128+
}
129+
ZVAL_COPY_VALUE(&$$, &$1);
130+
}
122131
;
123132

124133
member:
@@ -175,6 +184,15 @@ elements:
175184
}
176185
}
177186
| element
187+
| element ','
188+
{
189+
if (!(parser->scanner.options & PHP_JSON_ALLOW_TRAILING_COMMAS)) {
190+
parser->scanner.errcode = PHP_JSON_ERROR_SYNTAX;
191+
zval_ptr_dtor_nogc(&$1);
192+
YYERROR;
193+
}
194+
ZVAL_COPY_VALUE(&$$, &$1);
195+
}
178196
;
179197

180198
element:

ext/json/json_scanner.re

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ std:
123123
EXP = ( INT | FLOAT ) [eE] [+-]? DIGIT+ ;
124124
NL = "\r"? "\n" ;
125125
WS = [ \t\r]+ ;
126+
CMT_SL = "//" [^\n\x00]* ;
127+
CMT_ML = "/*" ( [^*\x00] | ( "*"+ [^*/\x00] ) )* "*"+ "/" ;
126128
EOI = "\000";
127129
CTRL = [\x00-\x1F] ;
128130
UTF8T = [\x80-\xBF] ;
@@ -203,6 +205,27 @@ std:
203205
goto std;
204206
}
205207
<JS>WS { goto std; }
208+
<JS>CMT_SL {
209+
if (!(s->options & PHP_JSON_ALLOW_COMMENTS)) {
210+
s->errcode = PHP_JSON_ERROR_SYNTAX;
211+
return PHP_JSON_T_ERROR;
212+
}
213+
goto std;
214+
}
215+
<JS>CMT_ML {
216+
php_json_ctype *p;
217+
if (!(s->options & PHP_JSON_ALLOW_COMMENTS)) {
218+
s->errcode = PHP_JSON_ERROR_SYNTAX;
219+
return PHP_JSON_T_ERROR;
220+
}
221+
for (p = s->token; p < s->cursor; p++) {
222+
if (*p == '\n') {
223+
s->line++;
224+
s->line_start = p + 1;
225+
}
226+
}
227+
goto std;
228+
}
206229
<JS>EOI {
207230
if (s->limit < s->cursor) {
208231
return PHP_JSON_T_EOI;

ext/json/php_json.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ static inline void php_json_error_details_clear(php_json_error_details *out) {
8989
#define PHP_JSON_INVALID_UTF8_SUBSTITUTE (1<<21)
9090
#define PHP_JSON_THROW_ON_ERROR (1<<22)
9191

92+
/* json_validate() and json_decode() common options */
93+
#define PHP_JSON_ALLOW_COMMENTS (1<<23)
94+
#define PHP_JSON_ALLOW_TRAILING_COMMAS (1<<24)
95+
9296
/* default depth */
9397
#define PHP_JSON_PARSER_DEFAULT_DEPTH 512
9498

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
--TEST--
2+
json_decode() with JSON_ALLOW_COMMENTS - valid comment usage
3+
--FILE--
4+
<?php
5+
6+
function decode_dump(string $json) {
7+
var_dump(json_decode($json, true, 512, JSON_ALLOW_COMMENTS));
8+
if (json_last_error() !== JSON_ERROR_NONE) {
9+
echo "error: ", json_last_error_msg(), "\n";
10+
}
11+
}
12+
13+
decode_dump("// comment\n1");
14+
decode_dump("1 // comment at EOF without newline");
15+
decode_dump("/* comment */ 1");
16+
decode_dump("1/*c*/");
17+
decode_dump("/**/1");
18+
decode_dump("/***/1");
19+
decode_dump("/*/*/1");
20+
decode_dump("/* /* */1");
21+
decode_dump("[1, // a\n2]");
22+
decode_dump("{\"a\" /*x*/ : /*y*/ 1 /*z*/, // t\n \"b\":2}");
23+
decode_dump('"// not a comment"');
24+
decode_dump('"/* not a comment */"');
25+
decode_dump("//only\n// comments\n[]");
26+
var_dump(json_validate("// c\n{\"a\": [1, 2]}", 512, JSON_ALLOW_COMMENTS));
27+
28+
?>
29+
--EXPECT--
30+
int(1)
31+
int(1)
32+
int(1)
33+
int(1)
34+
int(1)
35+
int(1)
36+
int(1)
37+
int(1)
38+
array(2) {
39+
[0]=>
40+
int(1)
41+
[1]=>
42+
int(2)
43+
}
44+
array(2) {
45+
["a"]=>
46+
int(1)
47+
["b"]=>
48+
int(2)
49+
}
50+
string(16) "// not a comment"
51+
string(19) "/* not a comment */"
52+
array(0) {
53+
}
54+
bool(true)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
--TEST--
2+
json_decode() with JSON_ALLOW_COMMENTS - malformed comments and other errors
3+
--FILE--
4+
<?php
5+
6+
function decode_dump(string $json, int $flags = JSON_ALLOW_COMMENTS) {
7+
var_dump(json_decode($json, true, 512, $flags));
8+
echo json_last_error(), ": ", json_last_error_msg(), "\n";
9+
}
10+
11+
// unterminated block comments: error at the comment opener
12+
decode_dump("/*");
13+
decode_dump("/* text");
14+
decode_dump("{\n /* x\n y");
15+
// lone slashes are still syntax errors
16+
decode_dump("/");
17+
decode_dump("/ /");
18+
// the first */ closes the comment, the stray one is an error
19+
decode_dump("/* /* */ */1");
20+
// hash comments are not supported
21+
decode_dump("# comment\n1");
22+
// comment-only documents behave like whitespace-only documents
23+
decode_dump(" ", 0);
24+
decode_dump("//x");
25+
decode_dump("/*x*/");
26+
decode_dump("// x\n");
27+
// comments separate tokens but do not join values
28+
decode_dump("1 2", 0);
29+
decode_dump("1/*c*/2");
30+
31+
try {
32+
json_decode("/*", true, 512, JSON_ALLOW_COMMENTS | JSON_THROW_ON_ERROR);
33+
} catch (JsonException $e) {
34+
echo "JsonException: ", $e->getCode(), " ", $e->getMessage(), "\n";
35+
}
36+
37+
?>
38+
--EXPECT--
39+
NULL
40+
4: Syntax error near location 1:1
41+
NULL
42+
4: Syntax error near location 1:1
43+
NULL
44+
4: Syntax error near location 2:2
45+
NULL
46+
4: Syntax error near location 1:1
47+
NULL
48+
4: Syntax error near location 1:1
49+
NULL
50+
4: Syntax error near location 1:10
51+
NULL
52+
4: Syntax error near location 1:1
53+
NULL
54+
4: Syntax error near location 1:2
55+
NULL
56+
4: Syntax error near location 1:4
57+
NULL
58+
4: Syntax error near location 1:6
59+
NULL
60+
4: Syntax error near location 2:1
61+
NULL
62+
4: Syntax error near location 1:3
63+
NULL
64+
4: Syntax error near location 1:7
65+
JsonException: 4 Syntax error near location 1:1
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
--TEST--
2+
JSONC syntax is rejected when JSON_ALLOW_COMMENTS / JSON_ALLOW_TRAILING_COMMAS are not set
3+
--FILE--
4+
<?php
5+
6+
function decode_dump(string $json, int $flags = 0) {
7+
var_dump(json_decode($json, true, 512, $flags));
8+
echo json_last_error(), ": ", json_last_error_msg(), "\n";
9+
}
10+
11+
// no flags: comments are syntax errors at the first slash
12+
decode_dump("// x");
13+
decode_dump("/* x */ 1");
14+
decode_dump("/**/");
15+
decode_dump("[1, 2] // t");
16+
// no flags: trailing commas are syntax errors at the closer
17+
decode_dump("[1,]");
18+
decode_dump("{\"a\":1,}");
19+
decode_dump("[1, 2,]");
20+
// one flag does not enable the other relaxation
21+
decode_dump("[1,]", JSON_ALLOW_COMMENTS);
22+
decode_dump("[1, /*c*/ 2]", JSON_ALLOW_TRAILING_COMMAS);
23+
// validate behaves identically
24+
var_dump(json_validate("// x\n1", 512, JSON_ALLOW_TRAILING_COMMAS));
25+
var_dump(json_validate("[1,]", 512, JSON_ALLOW_COMMENTS));
26+
27+
?>
28+
--EXPECT--
29+
NULL
30+
4: Syntax error near location 1:1
31+
NULL
32+
4: Syntax error near location 1:1
33+
NULL
34+
4: Syntax error near location 1:1
35+
NULL
36+
4: Syntax error near location 1:8
37+
NULL
38+
4: Syntax error near location 1:4
39+
NULL
40+
4: Syntax error near location 1:8
41+
NULL
42+
4: Syntax error near location 1:7
43+
NULL
44+
4: Syntax error near location 1:4
45+
NULL
46+
4: Syntax error near location 1:5
47+
bool(false)
48+
bool(false)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
--TEST--
2+
json_decode() with JSON_ALLOW_COMMENTS | JSON_ALLOW_TRAILING_COMMAS - full JSONC documents
3+
--FILE--
4+
<?php
5+
6+
const JSONC_FLAGS = JSON_ALLOW_COMMENTS | JSON_ALLOW_TRAILING_COMMAS;
7+
8+
$config = '{
9+
// server settings
10+
"host": "localhost", /* inline comment */
11+
"port": 8080,
12+
/*
13+
* feature list
14+
*/
15+
"features": [
16+
"alpha",
17+
"beta", // most recent
18+
],
19+
"debug": true,
20+
}';
21+
22+
var_dump(json_decode($config, true, 512, JSONC_FLAGS));
23+
var_dump(json_validate($config, 512, JSONC_FLAGS));
24+
25+
var_dump(json_decode("[1, /* c */ ]", true, 512, JSONC_FLAGS));
26+
var_dump(json_decode("{\"a\":1, // t\n}", true, 512, JSONC_FLAGS));
27+
28+
// a comment between two commas does not hide the error
29+
var_dump(json_decode("[1, // c\n,]", true, 512, JSONC_FLAGS));
30+
echo json_last_error(), ": ", json_last_error_msg(), "\n";
31+
32+
?>
33+
--EXPECT--
34+
array(4) {
35+
["host"]=>
36+
string(9) "localhost"
37+
["port"]=>
38+
int(8080)
39+
["features"]=>
40+
array(2) {
41+
[0]=>
42+
string(5) "alpha"
43+
[1]=>
44+
string(4) "beta"
45+
}
46+
["debug"]=>
47+
bool(true)
48+
}
49+
bool(true)
50+
array(1) {
51+
[0]=>
52+
int(1)
53+
}
54+
array(1) {
55+
["a"]=>
56+
int(1)
57+
}
58+
NULL
59+
4: Syntax error near location 2:1

0 commit comments

Comments
 (0)