Skip to content

Commit 1748aaf

Browse files
infisamkSeldaek
andauthored
Add ALLOW_DUPLICATE_KEYS_TO_ARRAY flag for collect values from duplic… (#88)
* Add ALLOW_DUPLICATE_KEYS_TO_ARRAY flag for collect values from duplicate keys in reserved property object (key array) `__duplicates__` --------- Co-authored-by: Jordi Boggiano <[email protected]>
1 parent 9bb7db0 commit 1748aaf

7 files changed

+73
-10
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
.github export-ignore
33
.gitignore export-ignore
44
phpstan.neon.dist export-ignore
5+
phpstan-baseline.neon export-ignore
56
phpunit.xml.dist export-ignore
67
/tests export-ignore

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ You can also pass additional flags to `JsonParser::lint/parse` that tweak the fu
3535
- `JsonParser::ALLOW_DUPLICATE_KEYS` collects duplicate keys. e.g. if you have two `foo` keys they will end up as `foo` and `foo.2`.
3636
- `JsonParser::PARSE_TO_ASSOC` parses to associative arrays instead of stdClass objects.
3737
- `JsonParser::ALLOW_COMMENTS` parses while allowing (and ignoring) inline `//` and multiline `/* */` comments in the JSON document.
38+
- `JsonParser::ALLOW_DUPLICATE_KEYS_TO_ARRAY` collects duplicate keys. e.g. if you have two `foo` keys the `foo` key will become an object (or array in assoc mode) with all `foo` values accessible as an array in `$result->foo->__duplicates__` (or `$result['foo']['__duplicates__']` in assoc mode).
3839

3940
Example:
4041

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
},
1717
"require-dev": {
1818
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13",
19-
"phpstan/phpstan": "^1.5"
19+
"phpstan/phpstan": "^1.11"
2020
},
2121
"autoload": {
2222
"psr-4": { "Seld\\JsonLint\\": "src/Seld/JsonLint/" }

phpstan-baseline.neon

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: "#^Cannot access offset 1 on array\\|bool\\|float\\|int\\|stdClass\\|string\\|null\\.$#"
5+
count: 1
6+
path: src/Seld/JsonLint/JsonParser.php
7+
8+
-
9+
message: "#^Property Seld\\\\JsonLint\\\\JsonParser\\:\\:\\$vstack \\(list\\<array\\|bool\\|float\\|int\\|stdClass\\|string\\|null\\>\\) does not accept non\\-empty\\-array\\<int\\<\\-3, max\\>, array\\|bool\\|float\\|int\\|stdClass\\|string\\|null\\>\\.$#"
10+
count: 4
11+
path: src/Seld/JsonLint/JsonParser.php
12+
13+
-
14+
message: "#^Property Seld\\\\JsonLint\\\\JsonParser\\:\\:\\$vstack \\(list\\<array\\|bool\\|float\\|int\\|stdClass\\|string\\|null\\>\\) does not accept non\\-empty\\-array\\<int\\<\\-3, max\\>, mixed\\>\\.$#"
15+
count: 1
16+
path: src/Seld/JsonLint/JsonParser.php

phpstan.neon.dist

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
includes:
2+
- phpstan-baseline.neon
3+
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
4+
15
parameters:
26
level: 8
37

8+
treatPhpDocTypesAsCertain: false
9+
410
paths:
511
- src/
612
- tests/

src/Seld/JsonLint/JsonParser.php

+27-8
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class JsonParser
3131
const ALLOW_DUPLICATE_KEYS = 2;
3232
const PARSE_TO_ASSOC = 4;
3333
const ALLOW_COMMENTS = 8;
34+
const ALLOW_DUPLICATE_KEYS_TO_ARRAY = 16;
3435

3536
/** @var Lexer */
3637
private $lexer;
@@ -201,6 +202,10 @@ public function lint($input, $flags = 0)
201202
*/
202203
public function parse($input, $flags = 0)
203204
{
205+
if (($flags & self::ALLOW_DUPLICATE_KEYS_TO_ARRAY) && ($flags & self::ALLOW_DUPLICATE_KEYS)) {
206+
throw new \InvalidArgumentException('Only one of ALLOW_DUPLICATE_KEYS and ALLOW_DUPLICATE_KEYS_TO_ARRAY can be used, you passed in both.');
207+
}
208+
204209
$this->failOnBOM($input);
205210

206211
$this->flags = $flags;
@@ -334,7 +339,7 @@ public function parse($input, $flags = 0)
334339
}
335340

336341
// this shouldn't happen, unless resolve defaults are off
337-
if (\is_array($action[0]) && \count($action) > 1) { // @phpstan-ignore-line
342+
if (\is_array($action[0]) && \count($action) > 1) {
338343
throw new ParsingException('Parse Error: multiple actions possible at state: ' . $state . ', token: ' . $symbol);
339344
}
340345

@@ -484,14 +489,21 @@ private function performAction($currentToken, $yytext, $yyleng, $yylineno, $yyst
484489
$errStr .= $this->lexer->showPosition() . "\n";
485490
$errStr .= "Duplicate key: ".$this->vstack[$len][0];
486491
throw new DuplicateKeyException($errStr, $this->vstack[$len][0], array('line' => $yylineno+1));
487-
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($this->vstack[$len-2][$key])) {
492+
}
493+
if (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($this->vstack[$len-2][$key])) {
488494
$duplicateCount = 1;
489495
do {
490496
$duplicateKey = $key . '.' . $duplicateCount++;
491497
} while (isset($this->vstack[$len-2][$duplicateKey]));
492-
$key = $duplicateKey;
498+
$this->vstack[$len-2][$duplicateKey] = $this->vstack[$len][1];
499+
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS_TO_ARRAY) && isset($this->vstack[$len-2][$key])) {
500+
if (!isset($this->vstack[$len-2][$key]['__duplicates__']) || !is_array($this->vstack[$len-2][$key]['__duplicates__'])) {
501+
$this->vstack[$len-2][$key] = array('__duplicates__' => array($this->vstack[$len-2][$key]));
502+
}
503+
$this->vstack[$len-2][$key]['__duplicates__'][] = $this->vstack[$len][1];
504+
} else {
505+
$this->vstack[$len-2][$key] = $this->vstack[$len][1];
493506
}
494-
$this->vstack[$len-2][$key] = $this->vstack[$len][1];
495507
} else {
496508
assert($this->vstack[$len-2] instanceof stdClass);
497509
$token = $this->vstack[$len-2];
@@ -500,19 +512,26 @@ private function performAction($currentToken, $yytext, $yyleng, $yylineno, $yyst
500512
} else {
501513
$key = $this->vstack[$len][0];
502514
}
503-
if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($this->vstack[$len-2]->{$key})) {
515+
if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($this->vstack[$len-2]->$key)) {
504516
$errStr = 'Parse error on line ' . ($yylineno+1) . ":\n";
505517
$errStr .= $this->lexer->showPosition() . "\n";
506518
$errStr .= "Duplicate key: ".$this->vstack[$len][0];
507519
throw new DuplicateKeyException($errStr, $this->vstack[$len][0], array('line' => $yylineno+1));
508-
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($this->vstack[$len-2]->{$key})) {
520+
}
521+
if (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($this->vstack[$len-2]->$key)) {
509522
$duplicateCount = 1;
510523
do {
511524
$duplicateKey = $key . '.' . $duplicateCount++;
512525
} while (isset($this->vstack[$len-2]->$duplicateKey));
513-
$key = $duplicateKey;
526+
$this->vstack[$len-2]->$duplicateKey = $this->vstack[$len][1];
527+
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS_TO_ARRAY) && isset($this->vstack[$len-2]->$key)) {
528+
if (!isset($this->vstack[$len-2]->$key->__duplicates__)) {
529+
$this->vstack[$len-2]->$key = (object) array('__duplicates__' => array($this->vstack[$len-2]->$key));
530+
}
531+
$this->vstack[$len-2]->$key->__duplicates__[] = $this->vstack[$len][1];
532+
} else {
533+
$this->vstack[$len-2]->$key = $this->vstack[$len][1];
514534
}
515-
$this->vstack[$len-2]->$key = $this->vstack[$len][1];
516535
}
517536
break;
518537
case 18:

tests/JsonParserTest.php

+21-1
Original file line numberDiff line numberDiff line change
@@ -231,14 +231,34 @@ public function testDuplicateKeys()
231231
{
232232
$parser = new JsonParser();
233233

234-
$result = $parser->parse('{"a":"b", "a":"c", "a":"d"}', JsonParser::ALLOW_DUPLICATE_KEYS);
234+
$str = '{"a":"b", "a":"c", "a":"d"}';
235+
236+
$result = $parser->parse($str, JsonParser::ALLOW_DUPLICATE_KEYS);
235237
$this->assertThat($result,
236238
$this->logicalAnd(
237239
$this->objectHasAttribute('a'),
238240
$this->objectHasAttribute('a.1'),
239241
$this->objectHasAttribute('a.2')
240242
)
241243
);
244+
245+
$result = $parser->parse($str, JsonParser::ALLOW_DUPLICATE_KEYS | JsonParser::PARSE_TO_ASSOC);
246+
self::assertSame(array('a' => 'b', 'a.1' => 'c', 'a.2' => 'd'), $result);
247+
}
248+
249+
public function testDuplicateKeysToArray()
250+
{
251+
$parser = new JsonParser();
252+
253+
$str = '{"a":"b", "a":"c", "a":"d"}';
254+
255+
$result = $parser->parse($str, JsonParser::ALLOW_DUPLICATE_KEYS_TO_ARRAY);
256+
$this->assertThat($result, $this->objectHasAttribute('a'));
257+
$this->assertThat($result->a, $this->objectHasAttribute('__duplicates__'));
258+
self::assertSame(array('b', 'c', 'd'), $result->a->__duplicates__);
259+
260+
$result = $parser->parse($str, JsonParser::ALLOW_DUPLICATE_KEYS_TO_ARRAY | JsonParser::PARSE_TO_ASSOC);
261+
self::assertSame(array('a' => array('__duplicates__' => array('b', 'c', 'd'))), $result);
242262
}
243263

244264
public function testDuplicateKeysWithEmpty()

0 commit comments

Comments
 (0)