Skip to content

Commit

Permalink
Add ALLOW_DUPLICATE_KEYS_TO_ARRAY flag for collect values from duplic… (
Browse files Browse the repository at this point in the history
#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 <j.boggiano@seld.be>
  • Loading branch information
infisamk and Seldaek committed Jul 11, 2024
1 parent 9bb7db0 commit 1748aaf
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 10 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
.github export-ignore
.gitignore export-ignore
phpstan.neon.dist export-ignore
phpstan-baseline.neon export-ignore
phpunit.xml.dist export-ignore
/tests export-ignore
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ You can also pass additional flags to `JsonParser::lint/parse` that tweak the fu
- `JsonParser::ALLOW_DUPLICATE_KEYS` collects duplicate keys. e.g. if you have two `foo` keys they will end up as `foo` and `foo.2`.
- `JsonParser::PARSE_TO_ASSOC` parses to associative arrays instead of stdClass objects.
- `JsonParser::ALLOW_COMMENTS` parses while allowing (and ignoring) inline `//` and multiline `/* */` comments in the JSON document.
- `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).

Example:

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13",
"phpstan/phpstan": "^1.5"
"phpstan/phpstan": "^1.11"
},
"autoload": {
"psr-4": { "Seld\\JsonLint\\": "src/Seld/JsonLint/" }
Expand Down
16 changes: 16 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
parameters:
ignoreErrors:
-
message: "#^Cannot access offset 1 on array\\|bool\\|float\\|int\\|stdClass\\|string\\|null\\.$#"
count: 1
path: src/Seld/JsonLint/JsonParser.php

-
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\\>\\.$#"
count: 4
path: src/Seld/JsonLint/JsonParser.php

-
message: "#^Property Seld\\\\JsonLint\\\\JsonParser\\:\\:\\$vstack \\(list\\<array\\|bool\\|float\\|int\\|stdClass\\|string\\|null\\>\\) does not accept non\\-empty\\-array\\<int\\<\\-3, max\\>, mixed\\>\\.$#"
count: 1
path: src/Seld/JsonLint/JsonParser.php
6 changes: 6 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
includes:
- phpstan-baseline.neon
- vendor/phpstan/phpstan/conf/bleedingEdge.neon

parameters:
level: 8

treatPhpDocTypesAsCertain: false

paths:
- src/
- tests/
Expand Down
35 changes: 27 additions & 8 deletions src/Seld/JsonLint/JsonParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class JsonParser
const ALLOW_DUPLICATE_KEYS = 2;
const PARSE_TO_ASSOC = 4;
const ALLOW_COMMENTS = 8;
const ALLOW_DUPLICATE_KEYS_TO_ARRAY = 16;

/** @var Lexer */
private $lexer;
Expand Down Expand Up @@ -201,6 +202,10 @@ public function lint($input, $flags = 0)
*/
public function parse($input, $flags = 0)
{
if (($flags & self::ALLOW_DUPLICATE_KEYS_TO_ARRAY) && ($flags & self::ALLOW_DUPLICATE_KEYS)) {
throw new \InvalidArgumentException('Only one of ALLOW_DUPLICATE_KEYS and ALLOW_DUPLICATE_KEYS_TO_ARRAY can be used, you passed in both.');
}

$this->failOnBOM($input);

$this->flags = $flags;
Expand Down Expand Up @@ -334,7 +339,7 @@ public function parse($input, $flags = 0)
}

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

Expand Down Expand Up @@ -484,14 +489,21 @@ private function performAction($currentToken, $yytext, $yyleng, $yylineno, $yyst
$errStr .= $this->lexer->showPosition() . "\n";
$errStr .= "Duplicate key: ".$this->vstack[$len][0];
throw new DuplicateKeyException($errStr, $this->vstack[$len][0], array('line' => $yylineno+1));
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($this->vstack[$len-2][$key])) {
}
if (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($this->vstack[$len-2][$key])) {
$duplicateCount = 1;
do {
$duplicateKey = $key . '.' . $duplicateCount++;
} while (isset($this->vstack[$len-2][$duplicateKey]));
$key = $duplicateKey;
$this->vstack[$len-2][$duplicateKey] = $this->vstack[$len][1];
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS_TO_ARRAY) && isset($this->vstack[$len-2][$key])) {
if (!isset($this->vstack[$len-2][$key]['__duplicates__']) || !is_array($this->vstack[$len-2][$key]['__duplicates__'])) {
$this->vstack[$len-2][$key] = array('__duplicates__' => array($this->vstack[$len-2][$key]));
}
$this->vstack[$len-2][$key]['__duplicates__'][] = $this->vstack[$len][1];
} else {
$this->vstack[$len-2][$key] = $this->vstack[$len][1];
}
$this->vstack[$len-2][$key] = $this->vstack[$len][1];
} else {
assert($this->vstack[$len-2] instanceof stdClass);
$token = $this->vstack[$len-2];
Expand All @@ -500,19 +512,26 @@ private function performAction($currentToken, $yytext, $yyleng, $yylineno, $yyst
} else {
$key = $this->vstack[$len][0];
}
if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($this->vstack[$len-2]->{$key})) {
if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($this->vstack[$len-2]->$key)) {
$errStr = 'Parse error on line ' . ($yylineno+1) . ":\n";
$errStr .= $this->lexer->showPosition() . "\n";
$errStr .= "Duplicate key: ".$this->vstack[$len][0];
throw new DuplicateKeyException($errStr, $this->vstack[$len][0], array('line' => $yylineno+1));
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($this->vstack[$len-2]->{$key})) {
}
if (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($this->vstack[$len-2]->$key)) {
$duplicateCount = 1;
do {
$duplicateKey = $key . '.' . $duplicateCount++;
} while (isset($this->vstack[$len-2]->$duplicateKey));
$key = $duplicateKey;
$this->vstack[$len-2]->$duplicateKey = $this->vstack[$len][1];
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS_TO_ARRAY) && isset($this->vstack[$len-2]->$key)) {
if (!isset($this->vstack[$len-2]->$key->__duplicates__)) {
$this->vstack[$len-2]->$key = (object) array('__duplicates__' => array($this->vstack[$len-2]->$key));
}
$this->vstack[$len-2]->$key->__duplicates__[] = $this->vstack[$len][1];
} else {
$this->vstack[$len-2]->$key = $this->vstack[$len][1];
}
$this->vstack[$len-2]->$key = $this->vstack[$len][1];
}
break;
case 18:
Expand Down
22 changes: 21 additions & 1 deletion tests/JsonParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,34 @@ public function testDuplicateKeys()
{
$parser = new JsonParser();

$result = $parser->parse('{"a":"b", "a":"c", "a":"d"}', JsonParser::ALLOW_DUPLICATE_KEYS);
$str = '{"a":"b", "a":"c", "a":"d"}';

$result = $parser->parse($str, JsonParser::ALLOW_DUPLICATE_KEYS);
$this->assertThat($result,
$this->logicalAnd(
$this->objectHasAttribute('a'),
$this->objectHasAttribute('a.1'),
$this->objectHasAttribute('a.2')
)
);

$result = $parser->parse($str, JsonParser::ALLOW_DUPLICATE_KEYS | JsonParser::PARSE_TO_ASSOC);
self::assertSame(array('a' => 'b', 'a.1' => 'c', 'a.2' => 'd'), $result);
}

public function testDuplicateKeysToArray()
{
$parser = new JsonParser();

$str = '{"a":"b", "a":"c", "a":"d"}';

$result = $parser->parse($str, JsonParser::ALLOW_DUPLICATE_KEYS_TO_ARRAY);
$this->assertThat($result, $this->objectHasAttribute('a'));
$this->assertThat($result->a, $this->objectHasAttribute('__duplicates__'));
self::assertSame(array('b', 'c', 'd'), $result->a->__duplicates__);

$result = $parser->parse($str, JsonParser::ALLOW_DUPLICATE_KEYS_TO_ARRAY | JsonParser::PARSE_TO_ASSOC);
self::assertSame(array('a' => array('__duplicates__' => array('b', 'c', 'd'))), $result);
}

public function testDuplicateKeysWithEmpty()
Expand Down

0 comments on commit 1748aaf

Please sign in to comment.