Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ALLOW_DUPLICATE_KEYS_TO_ARRAY flag for collect values from duplic… #88

Merged
merged 6 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading