From c82f63867e3122952ba5219e0473b5cd1f2ef76b Mon Sep 17 00:00:00 2001 From: infis Date: Fri, 7 Jun 2024 16:25:02 +0300 Subject: [PATCH 1/6] Add ALLOW_DUPLICATE_KEYS_TO_ARRAY flag for collect values from duplicate keys in reserved property object (key array) `__duplicates__` --- README.md | 1 + src/Seld/JsonLint/JsonParser.php | 29 +++++++++++++++-------------- tests/JsonParserTest.php | 7 ++++++- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ab4346d..5f70422 100644 --- a/README.md +++ b/README.md @@ -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 they will saved as array elements in object property (array key) `__duplicates__`. Example: diff --git a/src/Seld/JsonLint/JsonParser.php b/src/Seld/JsonLint/JsonParser.php index 420c0fd..080991e 100644 --- a/src/Seld/JsonLint/JsonParser.php +++ b/src/Seld/JsonLint/JsonParser.php @@ -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; @@ -484,14 +485,14 @@ 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])) { - $duplicateCount = 1; - do { - $duplicateKey = $key . '.' . $duplicateCount++; - } while (isset($this->vstack[$len-2][$duplicateKey])); - $key = $duplicateKey; + } 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] = ['__duplicates__' => [ $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]; @@ -505,14 +506,14 @@ 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})) { - $duplicateCount = 1; - do { - $duplicateKey = $key . '.' . $duplicateCount++; - } while (isset($this->vstack[$len-2]->$duplicateKey)); - $key = $duplicateKey; + } 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) ['__duplicates__' => [ $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: diff --git a/tests/JsonParserTest.php b/tests/JsonParserTest.php index 3eb73c2..bd823c2 100644 --- a/tests/JsonParserTest.php +++ b/tests/JsonParserTest.php @@ -231,7 +231,9 @@ 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'), @@ -239,6 +241,9 @@ public function testDuplicateKeys() $this->objectHasAttribute('a.2') ) ); + + $result = $parser->parse($str, JsonParser::ALLOW_DUPLICATE_KEYS_TO_ARRAY); + $this->assertTrue(isset($result->a->__duplicates__[2])); } public function testDuplicateKeysWithEmpty() From 222b56169011195226fecc1d88b98a8bc4f51055 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 14:21:47 +0200 Subject: [PATCH 2/6] Fix implementation and add some tests --- src/Seld/JsonLint/JsonParser.php | 14 ++++++++++++++ tests/JsonParserTest.php | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/Seld/JsonLint/JsonParser.php b/src/Seld/JsonLint/JsonParser.php index 080991e..c4d2765 100644 --- a/src/Seld/JsonLint/JsonParser.php +++ b/src/Seld/JsonLint/JsonParser.php @@ -485,6 +485,13 @@ 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)); + } + 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])); + $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] = ['__duplicates__' => [ $this->vstack[$len-2][$key] ]]; @@ -506,6 +513,13 @@ 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)); + } + 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)); + $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) ['__duplicates__' => [ $this->vstack[$len-2]->$key ]]; diff --git a/tests/JsonParserTest.php b/tests/JsonParserTest.php index bd823c2..1d8e9bd 100644 --- a/tests/JsonParserTest.php +++ b/tests/JsonParserTest.php @@ -242,8 +242,24 @@ public function testDuplicateKeys() ) ); + $result = $parser->parse($str, JsonParser::ALLOW_DUPLICATE_KEYS | JsonParser::PARSE_TO_ASSOC); + self::assertSame(['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->assertTrue(isset($result->a->__duplicates__[2])); + $this->assertThat($result, $this->objectHasAttribute('a')); + $this->assertThat($result->a, $this->objectHasAttribute('__duplicates__')); + self::assertSame(['b', 'c', 'd'], $result->a->__duplicates__); + + $result = $parser->parse($str, JsonParser::ALLOW_DUPLICATE_KEYS_TO_ARRAY | JsonParser::PARSE_TO_ASSOC); + self::assertSame(['a' => ['__duplicates__' => ['b', 'c', 'd']]], $result); } public function testDuplicateKeysWithEmpty() From 35d620f4539b4fb449d9dee106c88af5ff199750 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 15:16:33 +0200 Subject: [PATCH 3/6] Fix build issues --- .gitattributes | 1 + composer.json | 2 +- phpstan-baseline.neon | 16 ++++++++++++++++ phpstan.neon.dist | 6 ++++++ src/Seld/JsonLint/JsonParser.php | 8 ++++---- tests/JsonParserTest.php | 7 +++---- 6 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 phpstan-baseline.neon diff --git a/.gitattributes b/.gitattributes index 4e57adf..3dd7607 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/composer.json b/composer.json index 3f8afc5..dbc48e3 100644 --- a/composer.json +++ b/composer.json @@ -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/" } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..bce1ff3 --- /dev/null +++ b/phpstan-baseline.neon @@ -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\\\\) does not accept non\\-empty\\-array\\, array\\|bool\\|float\\|int\\|stdClass\\|string\\|null\\>\\.$#" + count: 4 + path: src/Seld/JsonLint/JsonParser.php + + - + message: "#^Property Seld\\\\JsonLint\\\\JsonParser\\:\\:\\$vstack \\(list\\\\) does not accept non\\-empty\\-array\\, mixed\\>\\.$#" + count: 1 + path: src/Seld/JsonLint/JsonParser.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index aa3323d..bd3a0ec 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,6 +1,12 @@ +includes: + - phpstan-baseline.neon + - vendor/phpstan/phpstan/conf/bleedingEdge.neon + parameters: level: 8 + treatPhpDocTypesAsCertain: false + paths: - src/ - tests/ diff --git a/src/Seld/JsonLint/JsonParser.php b/src/Seld/JsonLint/JsonParser.php index c4d2765..167b265 100644 --- a/src/Seld/JsonLint/JsonParser.php +++ b/src/Seld/JsonLint/JsonParser.php @@ -335,7 +335,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); } @@ -508,19 +508,19 @@ 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)); } - if (($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)); $this->vstack[$len-2]->$duplicateKey = $this->vstack[$len][1]; - } elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS_TO_ARRAY) && isset($this->vstack[$len-2]->{$key})) { + } 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) ['__duplicates__' => [ $this->vstack[$len-2]->$key ]]; } diff --git a/tests/JsonParserTest.php b/tests/JsonParserTest.php index 1d8e9bd..9b903bf 100644 --- a/tests/JsonParserTest.php +++ b/tests/JsonParserTest.php @@ -243,7 +243,7 @@ public function testDuplicateKeys() ); $result = $parser->parse($str, JsonParser::ALLOW_DUPLICATE_KEYS | JsonParser::PARSE_TO_ASSOC); - self::assertSame(['a' => 'b', 'a.1' => 'c', 'a.2' => 'd'], $result); + self::assertSame(array('a' => 'b', 'a.1' => 'c', 'a.2' => 'd'), $result); } public function testDuplicateKeysToArray() @@ -253,13 +253,12 @@ public function testDuplicateKeysToArray() $str = '{"a":"b", "a":"c", "a":"d"}'; $result = $parser->parse($str, JsonParser::ALLOW_DUPLICATE_KEYS_TO_ARRAY); - $this->assertTrue(isset($result->a->__duplicates__[2])); $this->assertThat($result, $this->objectHasAttribute('a')); $this->assertThat($result->a, $this->objectHasAttribute('__duplicates__')); - self::assertSame(['b', 'c', 'd'], $result->a->__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(['a' => ['__duplicates__' => ['b', 'c', 'd']]], $result); + self::assertSame(array('a' => array('__duplicates__' => array('b', 'c', 'd'))), $result); } public function testDuplicateKeysWithEmpty() From fc738024c99cf87869808421c3c90d10213f6970 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 15:21:48 +0200 Subject: [PATCH 4/6] Update docs, ensure conflicting flags cannot be combined --- README.md | 2 +- src/Seld/JsonLint/JsonParser.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f70422..e658f72 100644 --- a/README.md +++ b/README.md @@ -35,7 +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 they will saved as array elements in object property (array key) `__duplicates__`. +- `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: diff --git a/src/Seld/JsonLint/JsonParser.php b/src/Seld/JsonLint/JsonParser.php index 167b265..2c1db4c 100644 --- a/src/Seld/JsonLint/JsonParser.php +++ b/src/Seld/JsonLint/JsonParser.php @@ -202,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; From 7591529a33609b9c9a719c3756d77536da9abb7a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 15:57:22 +0200 Subject: [PATCH 5/6] Fix 5.3 support --- src/Seld/JsonLint/JsonParser.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Seld/JsonLint/JsonParser.php b/src/Seld/JsonLint/JsonParser.php index 2c1db4c..1d9e396 100644 --- a/src/Seld/JsonLint/JsonParser.php +++ b/src/Seld/JsonLint/JsonParser.php @@ -498,7 +498,7 @@ private function performAction($currentToken, $yytext, $yyleng, $yylineno, $yyst $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] = ['__duplicates__' => [ $this->vstack[$len-2][$key] ]]; + $this->vstack[$len-2][$key] = array('__duplicates__' => array($this->vstack[$len-2][$key])); } $this->vstack[$len-2][$key]['__duplicates__'][] = $this->vstack[$len][1]; } else { @@ -526,7 +526,7 @@ private function performAction($currentToken, $yytext, $yyleng, $yylineno, $yyst $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) ['__duplicates__' => [ $this->vstack[$len-2]->$key ]]; + $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 { From 5d850df807510ba5aaa8a701c97054eadbc6fe97 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 16:10:07 +0200 Subject: [PATCH 6/6] Try and fix 5.3 --- src/Seld/JsonLint/JsonParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Seld/JsonLint/JsonParser.php b/src/Seld/JsonLint/JsonParser.php index 1d9e396..cec4a87 100644 --- a/src/Seld/JsonLint/JsonParser.php +++ b/src/Seld/JsonLint/JsonParser.php @@ -497,7 +497,7 @@ private function performAction($currentToken, $yytext, $yyleng, $yylineno, $yyst } while (isset($this->vstack[$len-2][$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__'])) { + 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];