├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── jsonlint ├── composer.json └── src └── Seld └── JsonLint ├── DuplicateKeyException.php ├── JsonParser.php ├── Lexer.php ├── ParsingException.php └── Undefined.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | You can find newer changelog entries in [GitHub releases](https://github.com/Seldaek/jsonlint/releases) 2 | 3 | ### 1.10.0 (2023-05-11) 4 | 5 | * Added ALLOW_COMMENTS flag to parse while allowing (and ignoring) inline `//` and multiline `/* */` comments in the JSON document (#81) 6 | 7 | ### 1.9.0 (2022-04-01) 8 | 9 | * Internal cleanups and type fixes 10 | 11 | ### 1.8.1 (2020-08-13) 12 | 13 | * Added type annotations 14 | 15 | ### 1.8.0 (2020-04-30) 16 | 17 | * Improved lexer performance 18 | * Added (tentative) support for PHP 8 19 | * Fixed wording of error reporting for invalid strings when the error happened after the 20th character 20 | 21 | ### 1.7.2 (2019-10-24) 22 | 23 | * Fixed issue decoding some unicode escaped characters (for " and ') 24 | 25 | ### 1.7.1 (2018-01-24) 26 | 27 | * Fixed PHP 5.3 compatibility in bin/jsonlint 28 | 29 | ### 1.7.0 (2018-01-03) 30 | 31 | * Added ability to lint multiple files at once using the jsonlint binary 32 | 33 | ### 1.6.2 (2017-11-30) 34 | 35 | * No meaningful public changes 36 | 37 | ### 1.6.1 (2017-06-18) 38 | 39 | * Fixed parsing of `0` as invalid 40 | 41 | ### 1.6.0 (2017-03-06) 42 | 43 | * Added $flags arg to JsonParser::lint() to take the same flag as parse() did 44 | * Fixed backtracking performance issues on long strings with a lot of escaped characters 45 | 46 | ### 1.5.0 (2016-11-14) 47 | 48 | * Added support for PHP 7.1 (which converts `{"":""}` to an object property called `""` and not `"_empty_"` like 7.0 and below). 49 | 50 | ### 1.4.0 (2015-11-21) 51 | 52 | * Added a DuplicateKeyException allowing for more specific error detection and handling 53 | 54 | ### 1.3.1 (2015-01-04) 55 | 56 | * Fixed segfault when parsing large JSON strings 57 | 58 | ### 1.3.0 (2014-09-05) 59 | 60 | * Added parsing to an associative array via JsonParser::PARSE_TO_ASSOC 61 | * Fixed a warning when rendering parse errors on empty lines 62 | 63 | ### 1.2.0 (2014-07-20) 64 | 65 | * Added support for linting multiple files at once in bin/jsonlint 66 | * Added a -q/--quiet flag to suppress the output 67 | * Fixed error output being on STDOUT instead of STDERR 68 | * Fixed parameter parsing 69 | 70 | ### 1.1.2 (2013-11-04) 71 | 72 | * Fixed handling of Unicode BOMs to give a better failure hint 73 | 74 | ### 1.1.1 (2013-02-12) 75 | 76 | * Fixed handling of empty keys in objects in certain cases 77 | 78 | ### 1.1.0 (2012-12-13) 79 | 80 | * Added optional parsing of duplicate keys into key.2, key.3, etc via JsonParser::ALLOW_DUPLICATE_KEYS 81 | * Improved error reporting for common mistakes 82 | 83 | ### 1.0.1 (2012-04-03) 84 | 85 | * Added optional detection and error reporting for duplicate keys via JsonParser::DETECT_KEY_CONFLICTS 86 | * Added ability to pipe content through stdin into bin/jsonlint 87 | 88 | ### 1.0.0 (2012-03-12) 89 | 90 | * Initial release 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Jordi Boggiano 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JSON Lint 2 | ========= 3 | 4 | [![Build Status](https://github.com/Seldaek/jsonlint/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/Seldaek/jsonlint/actions/workflows/continuous-integration.yml) 5 | 6 | Usage 7 | ----- 8 | 9 | ```php 10 | use Seld\JsonLint\JsonParser; 11 | 12 | $parser = new JsonParser(); 13 | 14 | // returns null if it's valid json, or a ParsingException object. 15 | $parser->lint($json); 16 | 17 | // Call getMessage() on the exception object to get 18 | // a well formatted error message error like this 19 | 20 | // Parse error on line 2: 21 | // ... "key": "value" "numbers": [1, 2, 3] 22 | // ----------------------^ 23 | // Expected one of: 'EOF', '}', ':', ',', ']' 24 | 25 | // Call getDetails() on the exception to get more info. 26 | 27 | // returns parsed json, like json_decode() does, but slower, throws 28 | // exceptions on failure. 29 | $parser->parse($json); 30 | ``` 31 | 32 | You can also pass additional flags to `JsonParser::lint/parse` that tweak the functionality: 33 | 34 | - `JsonParser::DETECT_KEY_CONFLICTS` throws an exception on duplicate keys. 35 | - `JsonParser::ALLOW_DUPLICATE_KEYS` collects duplicate keys. e.g. if you have two `foo` keys they will end up as `foo` and `foo.2`. 36 | - `JsonParser::PARSE_TO_ASSOC` parses to associative arrays instead of stdClass objects. 37 | - `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). 39 | 40 | Example: 41 | 42 | ```php 43 | $parser = new JsonParser; 44 | try { 45 | $parser->parse(file_get_contents($jsonFile), JsonParser::DETECT_KEY_CONFLICTS); 46 | } catch (DuplicateKeyException $e) { 47 | $details = $e->getDetails(); 48 | echo 'Key '.$details['key'].' is a duplicate in '.$jsonFile.' at line '.$details['line']; 49 | } 50 | ``` 51 | 52 | > **Note:** This library is meant to parse JSON while providing good error messages on failure. There is no way it can be as fast as php native `json_decode()`. 53 | > 54 | > It is recommended to parse with `json_decode`, and when it fails parse again with seld/jsonlint to get a proper error message back to the user. See for example [how Composer uses this library](https://github.com/composer/composer/blob/56edd53046fd697d32b2fd2fbaf45af5d7951671/src/Composer/Json/JsonFile.php#L283-L318): 55 | 56 | 57 | Installation 58 | ------------ 59 | 60 | For a quick install with Composer use: 61 | 62 | ```bash 63 | composer require seld/jsonlint 64 | ``` 65 | 66 | JSON Lint can easily be used within another app if you have a 67 | [PSR-4](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md) 68 | autoloader, or it can be installed through [Composer](https://getcomposer.org/) 69 | for use as a CLI util. 70 | Once installed via Composer you can run the following command to lint a json file or URL: 71 | 72 | $ bin/jsonlint file.json 73 | 74 | Requirements 75 | ------------ 76 | 77 | - PHP 5.3+ 78 | - [optional] PHPUnit 3.5+ to execute the test suite (phpunit --version) 79 | 80 | Submitting bugs and feature requests 81 | ------------------------------------ 82 | 83 | Bugs and feature request are tracked on [GitHub](https://github.com/Seldaek/jsonlint/issues) 84 | 85 | Author 86 | ------ 87 | 88 | Jordi Boggiano - - 89 | 90 | License 91 | ------- 92 | 93 | JSON Lint is licensed under the MIT License - see the LICENSE file for details 94 | 95 | Acknowledgements 96 | ---------------- 97 | 98 | This library is a port of the JavaScript [jsonlint](https://github.com/zaach/jsonlint) library. 99 | -------------------------------------------------------------------------------- /bin/jsonlint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | function includeIfExists($file) 14 | { 15 | if (file_exists($file)) { 16 | return include $file; 17 | } 18 | } 19 | 20 | if (!includeIfExists(__DIR__.'/../vendor/autoload.php') && !includeIfExists(__DIR__.'/../../../autoload.php')) { 21 | $msg = 'You must set up the project dependencies, run the following commands:'.PHP_EOL. 22 | 'curl -sS https://getcomposer.org/installer | php'.PHP_EOL. 23 | 'php composer.phar install'.PHP_EOL; 24 | fwrite(STDERR, $msg); 25 | exit(1); 26 | } 27 | 28 | use Seld\JsonLint\JsonParser; 29 | 30 | $files = array(); 31 | $quiet = false; 32 | 33 | if (isset($_SERVER['argc']) && $_SERVER['argc'] > 1) { 34 | for ($i = 1; $i < $_SERVER['argc']; $i++) { 35 | $arg = $_SERVER['argv'][$i]; 36 | if ($arg == '-q' || $arg == '--quiet') { 37 | $quiet = true; 38 | } else { 39 | if ($arg == '-h' || $arg == '--help') { 40 | showUsage($_SERVER['argv'][0]); 41 | } else { 42 | $files[] = $arg; 43 | } 44 | } 45 | } 46 | } 47 | 48 | if (!empty($files)) { 49 | // file linting 50 | $exitCode = 0; 51 | foreach ($files as $file) { 52 | $result = lintFile($file, $quiet); 53 | if ($result === false) { 54 | $exitCode = 1; 55 | } 56 | } 57 | exit($exitCode); 58 | } else { 59 | //stdin linting 60 | if ($contents = file_get_contents('php://stdin')) { 61 | lint($contents, $quiet); 62 | } else { 63 | fwrite(STDERR, 'No file name or json input given' . PHP_EOL); 64 | exit(1); 65 | } 66 | } 67 | 68 | // stdin lint function 69 | function lint($content, $quiet = false) 70 | { 71 | $parser = new JsonParser(); 72 | if ($err = $parser->lint($content)) { 73 | fwrite(STDERR, $err->getMessage() . ' (stdin)' . PHP_EOL); 74 | exit(1); 75 | } 76 | if (!$quiet) { 77 | echo 'Valid JSON (stdin)' . PHP_EOL; 78 | exit(0); 79 | } 80 | } 81 | 82 | // file lint function 83 | function lintFile($file, $quiet = false) 84 | { 85 | if (!preg_match('{^https?://}i', $file)) { 86 | if (!file_exists($file)) { 87 | fwrite(STDERR, 'File not found: ' . $file . PHP_EOL); 88 | return false; 89 | } 90 | if (!is_readable($file)) { 91 | fwrite(STDERR, 'File not readable: ' . $file . PHP_EOL); 92 | return false; 93 | } 94 | } 95 | 96 | $content = file_get_contents($file); 97 | $parser = new JsonParser(); 98 | if ($err = $parser->lint($content)) { 99 | fwrite(STDERR, $file . ': ' . $err->getMessage() . PHP_EOL); 100 | return false; 101 | } 102 | if (!$quiet) { 103 | echo 'Valid JSON (' . $file . ')' . PHP_EOL; 104 | } 105 | return true; 106 | } 107 | 108 | // usage text function 109 | function showUsage($programPath) 110 | { 111 | echo 'Usage: '.$programPath.' file [options]'.PHP_EOL; 112 | echo PHP_EOL; 113 | echo 'Options:'.PHP_EOL; 114 | echo ' -q, --quiet Cause jsonlint to be quiet when no errors are found'.PHP_EOL; 115 | echo ' -h, --help Show this message'.PHP_EOL; 116 | exit(0); 117 | } 118 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seld/jsonlint", 3 | "description": "JSON Linter", 4 | "keywords": ["json", "parser", "linter", "validator"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Jordi Boggiano", 10 | "email": "j.boggiano@seld.be", 11 | "homepage": "https://seld.be" 12 | } 13 | ], 14 | "require": { 15 | "php": "^5.3 || ^7.0 || ^8.0" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13", 19 | "phpstan/phpstan": "^1.11" 20 | }, 21 | "autoload": { 22 | "psr-4": { "Seld\\JsonLint\\": "src/Seld/JsonLint/" } 23 | }, 24 | "bin": ["bin/jsonlint"], 25 | "scripts": { 26 | "test": "vendor/bin/phpunit", 27 | "phpstan": "vendor/bin/phpstan analyse" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Seld/JsonLint/DuplicateKeyException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Seld\JsonLint; 13 | 14 | class DuplicateKeyException extends ParsingException 15 | { 16 | /** 17 | * @var array{key: string, line: int} 18 | */ 19 | protected $details; 20 | 21 | /** 22 | * @param string $message 23 | * @param string $key 24 | * @phpstan-param array{line: int} $details 25 | */ 26 | public function __construct($message, $key, array $details) 27 | { 28 | $details['key'] = $key; 29 | parent::__construct($message, $details); 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getKey() 36 | { 37 | return $this->details['key']; 38 | } 39 | 40 | /** 41 | * @phpstan-return array{key: string, line: int} 42 | */ 43 | public function getDetails() 44 | { 45 | return $this->details; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Seld/JsonLint/JsonParser.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Seld\JsonLint; 13 | use stdClass; 14 | 15 | /** 16 | * Parser class 17 | * 18 | * Example: 19 | * 20 | * $parser = new JsonParser(); 21 | * // returns null if it's valid json, or an error object 22 | * $parser->lint($json); 23 | * // returns parsed json, like json_decode does, but slower, throws exceptions on failure. 24 | * $parser->parse($json); 25 | * 26 | * Ported from https://github.com/zaach/jsonlint 27 | */ 28 | class JsonParser 29 | { 30 | const DETECT_KEY_CONFLICTS = 1; 31 | const ALLOW_DUPLICATE_KEYS = 2; 32 | const PARSE_TO_ASSOC = 4; 33 | const ALLOW_COMMENTS = 8; 34 | const ALLOW_DUPLICATE_KEYS_TO_ARRAY = 16; 35 | 36 | /** @var Lexer */ 37 | private $lexer; 38 | 39 | /** 40 | * @var int 41 | * @phpstan-var int-mask-of 42 | */ 43 | private $flags; 44 | /** @var list */ 45 | private $stack; 46 | /** @var list|int|bool|float|string|null> */ 47 | private $vstack; // semantic value stack 48 | /** @var list */ 49 | private $lstack; // location stack 50 | 51 | /** 52 | * @phpstan-var array 53 | */ 54 | private $symbols = array( 55 | 'error' => 2, 56 | 'JSONString' => 3, 57 | 'STRING' => 4, 58 | 'JSONNumber' => 5, 59 | 'NUMBER' => 6, 60 | 'JSONNullLiteral' => 7, 61 | 'NULL' => 8, 62 | 'JSONBooleanLiteral' => 9, 63 | 'TRUE' => 10, 64 | 'FALSE' => 11, 65 | 'JSONText' => 12, 66 | 'JSONValue' => 13, 67 | 'EOF' => 14, 68 | 'JSONObject' => 15, 69 | 'JSONArray' => 16, 70 | '{' => 17, 71 | '}' => 18, 72 | 'JSONMemberList' => 19, 73 | 'JSONMember' => 20, 74 | ':' => 21, 75 | ',' => 22, 76 | '[' => 23, 77 | ']' => 24, 78 | 'JSONElementList' => 25, 79 | '$accept' => 0, 80 | '$end' => 1, 81 | ); 82 | 83 | /** 84 | * @phpstan-var array 85 | * @const 86 | */ 87 | private $terminals_ = array( 88 | 2 => "error", 89 | 4 => "STRING", 90 | 6 => "NUMBER", 91 | 8 => "NULL", 92 | 10 => "TRUE", 93 | 11 => "FALSE", 94 | 14 => "EOF", 95 | 17 => "{", 96 | 18 => "}", 97 | 21 => ":", 98 | 22 => ",", 99 | 23 => "[", 100 | 24 => "]", 101 | ); 102 | 103 | /** 104 | * @phpstan-var array, array{int, int}> 105 | * @const 106 | */ 107 | private $productions_ = array( 108 | 1 => array(3, 1), 109 | 2 => array(5, 1), 110 | 3 => array(7, 1), 111 | 4 => array(9, 1), 112 | 5 => array(9, 1), 113 | 6 => array(12, 2), 114 | 7 => array(13, 1), 115 | 8 => array(13, 1), 116 | 9 => array(13, 1), 117 | 10 => array(13, 1), 118 | 11 => array(13, 1), 119 | 12 => array(13, 1), 120 | 13 => array(15, 2), 121 | 14 => array(15, 3), 122 | 15 => array(20, 3), 123 | 16 => array(19, 1), 124 | 17 => array(19, 3), 125 | 18 => array(16, 2), 126 | 19 => array(16, 3), 127 | 20 => array(25, 1), 128 | 21 => array(25, 3) 129 | ); 130 | 131 | /** 132 | * @var array, array|int>> List of stateID=>symbolID=>actionIDs|actionID 133 | * @const 134 | */ 135 | private $table = array( 136 | 0 => array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 12 => 1, 13 => 2, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), 137 | 1 => array( 1 => array(3)), 138 | 2 => array( 14 => array(1,16)), 139 | 3 => array( 14 => array(2,7), 18 => array(2,7), 22 => array(2,7), 24 => array(2,7)), 140 | 4 => array( 14 => array(2,8), 18 => array(2,8), 22 => array(2,8), 24 => array(2,8)), 141 | 5 => array( 14 => array(2,9), 18 => array(2,9), 22 => array(2,9), 24 => array(2,9)), 142 | 6 => array( 14 => array(2,10), 18 => array(2,10), 22 => array(2,10), 24 => array(2,10)), 143 | 7 => array( 14 => array(2,11), 18 => array(2,11), 22 => array(2,11), 24 => array(2,11)), 144 | 8 => array( 14 => array(2,12), 18 => array(2,12), 22 => array(2,12), 24 => array(2,12)), 145 | 9 => array( 14 => array(2,3), 18 => array(2,3), 22 => array(2,3), 24 => array(2,3)), 146 | 10 => array( 14 => array(2,4), 18 => array(2,4), 22 => array(2,4), 24 => array(2,4)), 147 | 11 => array( 14 => array(2,5), 18 => array(2,5), 22 => array(2,5), 24 => array(2,5)), 148 | 12 => array( 14 => array(2,1), 18 => array(2,1), 21 => array(2,1), 22 => array(2,1), 24 => array(2,1)), 149 | 13 => array( 14 => array(2,2), 18 => array(2,2), 22 => array(2,2), 24 => array(2,2)), 150 | 14 => array( 3 => 20, 4 => array(1,12), 18 => array(1,17), 19 => 18, 20 => 19 ), 151 | 15 => array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 23, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15), 24 => array(1,21), 25 => 22 ), 152 | 16 => array( 1 => array(2,6)), 153 | 17 => array( 14 => array(2,13), 18 => array(2,13), 22 => array(2,13), 24 => array(2,13)), 154 | 18 => array( 18 => array(1,24), 22 => array(1,25)), 155 | 19 => array( 18 => array(2,16), 22 => array(2,16)), 156 | 20 => array( 21 => array(1,26)), 157 | 21 => array( 14 => array(2,18), 18 => array(2,18), 22 => array(2,18), 24 => array(2,18)), 158 | 22 => array( 22 => array(1,28), 24 => array(1,27)), 159 | 23 => array( 22 => array(2,20), 24 => array(2,20)), 160 | 24 => array( 14 => array(2,14), 18 => array(2,14), 22 => array(2,14), 24 => array(2,14)), 161 | 25 => array( 3 => 20, 4 => array(1,12), 20 => 29 ), 162 | 26 => array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 30, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), 163 | 27 => array( 14 => array(2,19), 18 => array(2,19), 22 => array(2,19), 24 => array(2,19)), 164 | 28 => array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 31, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), 165 | 29 => array( 18 => array(2,17), 22 => array(2,17)), 166 | 30 => array( 18 => array(2,15), 22 => array(2,15)), 167 | 31 => array( 22 => array(2,21), 24 => array(2,21)), 168 | ); 169 | 170 | /** 171 | * @var array{16: array{2, 6}} 172 | * @const 173 | */ 174 | private $defaultActions = array( 175 | 16 => array(2, 6) 176 | ); 177 | 178 | /** 179 | * @param string $input JSON string 180 | * @param int $flags Bitmask of parse/lint options (see constants of this class) 181 | * @return null|ParsingException null if no error is found, a ParsingException containing all details otherwise 182 | * 183 | * @phpstan-param int-mask-of $flags 184 | */ 185 | public function lint($input, $flags = 0) 186 | { 187 | try { 188 | $this->parse($input, $flags); 189 | } catch (ParsingException $e) { 190 | return $e; 191 | } 192 | return null; 193 | } 194 | 195 | /** 196 | * @param string $input JSON string 197 | * @param int $flags Bitmask of parse/lint options (see constants of this class) 198 | * @return mixed 199 | * @throws ParsingException 200 | * 201 | * @phpstan-param int-mask-of $flags 202 | */ 203 | public function parse($input, $flags = 0) 204 | { 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 | 209 | $this->failOnBOM($input); 210 | 211 | $this->flags = $flags; 212 | 213 | $this->stack = array(0); 214 | $this->vstack = array(null); 215 | $this->lstack = array(); 216 | 217 | $yytext = ''; 218 | $yylineno = 0; 219 | $yyleng = 0; 220 | /** @var int<0,3> */ 221 | $recovering = 0; 222 | 223 | $this->lexer = new Lexer($flags); 224 | $this->lexer->setInput($input); 225 | 226 | $yyloc = $this->lexer->yylloc; 227 | $this->lstack[] = $yyloc; 228 | 229 | $symbol = null; 230 | $preErrorSymbol = null; 231 | $action = null; 232 | $a = null; 233 | $r = null; 234 | $p = null; 235 | $len = null; 236 | $newState = null; 237 | $expected = null; 238 | /** @var string|null */ 239 | $errStr = null; 240 | 241 | while (true) { 242 | // retrieve state number from top of stack 243 | $state = $this->stack[\count($this->stack)-1]; 244 | 245 | // use default actions if available 246 | if (isset($this->defaultActions[$state])) { 247 | $action = $this->defaultActions[$state]; 248 | } else { 249 | if ($symbol === null) { 250 | $symbol = $this->lexer->lex(); 251 | } 252 | // read action for current state and first input 253 | /** @var array|false */ 254 | $action = isset($this->table[$state][$symbol]) ? $this->table[$state][$symbol] : false; 255 | } 256 | 257 | // handle parse error 258 | if (!$action || !$action[0]) { 259 | assert(isset($symbol)); 260 | if (!$recovering) { 261 | // Report error 262 | $expected = array(); 263 | foreach ($this->table[$state] as $p => $ignore) { 264 | if (isset($this->terminals_[$p]) && $p > 2) { 265 | $expected[] = "'" . $this->terminals_[$p] . "'"; 266 | } 267 | } 268 | 269 | $message = null; 270 | if (\in_array("'STRING'", $expected) && \in_array(substr($this->lexer->match, 0, 1), array('"', "'"))) { 271 | $message = "Invalid string"; 272 | if ("'" === substr($this->lexer->match, 0, 1)) { 273 | $message .= ", it appears you used single quotes instead of double quotes"; 274 | } elseif (preg_match('{".+?(\\\\[^"bfnrt/\\\\u](...)?)}', $this->lexer->getFullUpcomingInput(), $match)) { 275 | $message .= ", it appears you have an unescaped backslash at: ".$match[1]; 276 | } elseif (preg_match('{"(?:[^"]+|\\\\")*$}m', $this->lexer->getFullUpcomingInput())) { 277 | $message .= ", it appears you forgot to terminate a string, or attempted to write a multiline string which is invalid"; 278 | } 279 | } 280 | 281 | $errStr = 'Parse error on line ' . ($yylineno+1) . ":\n"; 282 | $errStr .= $this->lexer->showPosition() . "\n"; 283 | if ($message) { 284 | $errStr .= $message; 285 | } else { 286 | $errStr .= (\count($expected) > 1) ? "Expected one of: " : "Expected: "; 287 | $errStr .= implode(', ', $expected); 288 | } 289 | 290 | if (',' === substr(trim($this->lexer->getPastInput()), -1)) { 291 | $errStr .= " - It appears you have an extra trailing comma"; 292 | } 293 | 294 | $this->parseError($errStr, array( 295 | 'text' => $this->lexer->match, 296 | 'token' => isset($this->terminals_[$symbol]) ? $this->terminals_[$symbol] : $symbol, 297 | 'line' => $this->lexer->yylineno, 298 | 'loc' => $yyloc, 299 | 'expected' => $expected, 300 | )); 301 | } 302 | 303 | // just recovered from another error 304 | if ($recovering == 3) { 305 | if ($symbol === Lexer::EOF) { 306 | throw new ParsingException($errStr ?: 'Parsing halted.'); 307 | } 308 | 309 | // discard current lookahead and grab another 310 | $yyleng = $this->lexer->yyleng; 311 | $yytext = $this->lexer->yytext; 312 | $yylineno = $this->lexer->yylineno; 313 | $yyloc = $this->lexer->yylloc; 314 | $symbol = $this->lexer->lex(); 315 | } 316 | 317 | // try to recover from error 318 | while (true) { 319 | // check for error recovery rule in this state 320 | if (\array_key_exists(Lexer::T_ERROR, $this->table[$state])) { 321 | break; 322 | } 323 | if ($state == 0) { 324 | throw new ParsingException($errStr ?: 'Parsing halted.'); 325 | } 326 | $this->popStack(1); 327 | $state = $this->stack[\count($this->stack)-1]; 328 | } 329 | 330 | $preErrorSymbol = $symbol; // save the lookahead token 331 | $symbol = Lexer::T_ERROR; // insert generic error symbol as new lookahead 332 | $state = $this->stack[\count($this->stack)-1]; 333 | /** @var array|false */ 334 | $action = isset($this->table[$state][Lexer::T_ERROR]) ? $this->table[$state][Lexer::T_ERROR] : false; 335 | if ($action === false) { 336 | throw new \LogicException('No table value found for '.$state.' => '.Lexer::T_ERROR); 337 | } 338 | $recovering = 3; // allow 3 real symbols to be shifted before reporting a new error 339 | } 340 | 341 | // this shouldn't happen, unless resolve defaults are off 342 | if (\is_array($action[0]) && \count($action) > 1) { 343 | throw new ParsingException('Parse Error: multiple actions possible at state: ' . $state . ', token: ' . $symbol); 344 | } 345 | 346 | switch ($action[0]) { 347 | case 1: // shift 348 | assert(isset($symbol)); 349 | $this->stack[] = $symbol; 350 | $this->vstack[] = $this->lexer->yytext; 351 | $this->lstack[] = $this->lexer->yylloc; 352 | $this->stack[] = $action[1]; // push state 353 | $symbol = null; 354 | if (!$preErrorSymbol) { // normal execution/no error 355 | $yyleng = $this->lexer->yyleng; 356 | $yytext = $this->lexer->yytext; 357 | $yylineno = $this->lexer->yylineno; 358 | $yyloc = $this->lexer->yylloc; 359 | if ($recovering > 0) { 360 | $recovering--; 361 | } 362 | } else { // error just occurred, resume old lookahead from before error 363 | $symbol = $preErrorSymbol; 364 | $preErrorSymbol = null; 365 | } 366 | break; 367 | 368 | case 2: // reduce 369 | $len = $this->productions_[$action[1]][1]; 370 | 371 | // perform semantic action 372 | $currentToken = $this->vstack[\count($this->vstack) - $len]; // default to $$ = $1 373 | // default location, uses first token for firsts, last for lasts 374 | $position = array( // _$ = store 375 | 'first_line' => $this->lstack[\count($this->lstack) - ($len ?: 1)]['first_line'], 376 | 'last_line' => $this->lstack[\count($this->lstack) - 1]['last_line'], 377 | 'first_column' => $this->lstack[\count($this->lstack) - ($len ?: 1)]['first_column'], 378 | 'last_column' => $this->lstack[\count($this->lstack) - 1]['last_column'], 379 | ); 380 | list($newToken, $actionResult) = $this->performAction($currentToken, $yytext, $yyleng, $yylineno, $action[1]); 381 | 382 | if (!$actionResult instanceof Undefined) { 383 | return $actionResult; 384 | } 385 | 386 | if ($len) { 387 | $this->popStack($len); 388 | } 389 | 390 | $this->stack[] = $this->productions_[$action[1]][0]; // push nonterminal (reduce) 391 | $this->vstack[] = $newToken; 392 | $this->lstack[] = $position; 393 | /** @var int */ 394 | $newState = $this->table[$this->stack[\count($this->stack)-2]][$this->stack[\count($this->stack)-1]]; 395 | $this->stack[] = $newState; 396 | break; 397 | 398 | case 3: // accept 399 | 400 | return true; 401 | } 402 | } 403 | } 404 | 405 | /** 406 | * @param string $str 407 | * @param array{text: string, token: string|int, line: int, loc: array{first_line: int, first_column: int, last_line: int, last_column: int}, expected: string[]}|null $hash 408 | * @return never 409 | */ 410 | protected function parseError($str, $hash = null) 411 | { 412 | throw new ParsingException($str, $hash ?: array()); 413 | } 414 | 415 | /** 416 | * @param stdClass|array|int|bool|float|string|null $currentToken 417 | * @param string $yytext 418 | * @param int $yyleng 419 | * @param int $yylineno 420 | * @param int $yystate 421 | * @return array{stdClass|array|int|bool|float|string|null, stdClass|array|int|bool|float|string|null|Undefined} 422 | */ 423 | private function performAction($currentToken, $yytext, $yyleng, $yylineno, $yystate) 424 | { 425 | $token = $currentToken; 426 | 427 | $len = \count($this->vstack) - 1; 428 | switch ($yystate) { 429 | case 1: 430 | $yytext = preg_replace_callback('{(?:\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4})}', array($this, 'stringInterpolation'), $yytext); 431 | $token = $yytext; 432 | break; 433 | case 2: 434 | if (strpos($yytext, 'e') !== false || strpos($yytext, 'E') !== false) { 435 | $token = \floatval($yytext); 436 | } else { 437 | $token = strpos($yytext, '.') === false ? \intval($yytext) : \floatval($yytext); 438 | } 439 | break; 440 | case 3: 441 | $token = null; 442 | break; 443 | case 4: 444 | $token = true; 445 | break; 446 | case 5: 447 | $token = false; 448 | break; 449 | case 6: 450 | $token = $this->vstack[$len-1]; 451 | 452 | return array($token, $token); 453 | case 13: 454 | if ($this->flags & self::PARSE_TO_ASSOC) { 455 | $token = array(); 456 | } else { 457 | $token = new stdClass; 458 | } 459 | break; 460 | case 14: 461 | $token = $this->vstack[$len-1]; 462 | break; 463 | case 15: 464 | $token = array($this->vstack[$len-2], $this->vstack[$len]); 465 | break; 466 | case 16: 467 | assert(\is_array($this->vstack[$len])); 468 | if (PHP_VERSION_ID < 70100) { 469 | $property = $this->vstack[$len][0] === '' ? '_empty_' : $this->vstack[$len][0]; 470 | } else { 471 | $property = $this->vstack[$len][0]; 472 | } 473 | if ($this->flags & self::PARSE_TO_ASSOC) { 474 | $token = array(); 475 | $token[$property] = $this->vstack[$len][1]; 476 | } else { 477 | $token = new stdClass; 478 | $token->$property = $this->vstack[$len][1]; 479 | } 480 | break; 481 | case 17: 482 | assert(\is_array($this->vstack[$len])); 483 | if ($this->flags & self::PARSE_TO_ASSOC) { 484 | assert(\is_array($this->vstack[$len-2])); 485 | $token =& $this->vstack[$len-2]; 486 | $key = $this->vstack[$len][0]; 487 | if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($this->vstack[$len-2][$key])) { 488 | $errStr = 'Parse error on line ' . ($yylineno+1) . ":\n"; 489 | $errStr .= $this->lexer->showPosition() . "\n"; 490 | $errStr .= "Duplicate key: ".$this->vstack[$len][0]; 491 | throw new DuplicateKeyException($errStr, $this->vstack[$len][0], array('line' => $yylineno+1)); 492 | } 493 | if (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($this->vstack[$len-2][$key])) { 494 | $duplicateCount = 1; 495 | do { 496 | $duplicateKey = $key . '.' . $duplicateCount++; 497 | } while (isset($this->vstack[$len-2][$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]; 506 | } 507 | } else { 508 | assert($this->vstack[$len-2] instanceof stdClass); 509 | $token = $this->vstack[$len-2]; 510 | if (PHP_VERSION_ID < 70100) { 511 | $key = $this->vstack[$len][0] === '' ? '_empty_' : $this->vstack[$len][0]; 512 | } else { 513 | $key = $this->vstack[$len][0]; 514 | } 515 | if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($this->vstack[$len-2]->$key)) { 516 | $errStr = 'Parse error on line ' . ($yylineno+1) . ":\n"; 517 | $errStr .= $this->lexer->showPosition() . "\n"; 518 | $errStr .= "Duplicate key: ".$this->vstack[$len][0]; 519 | throw new DuplicateKeyException($errStr, $this->vstack[$len][0], array('line' => $yylineno+1)); 520 | } 521 | if (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($this->vstack[$len-2]->$key)) { 522 | $duplicateCount = 1; 523 | do { 524 | $duplicateKey = $key . '.' . $duplicateCount++; 525 | } while (isset($this->vstack[$len-2]->$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]; 534 | } 535 | } 536 | break; 537 | case 18: 538 | $token = array(); 539 | break; 540 | case 19: 541 | $token = $this->vstack[$len-1]; 542 | break; 543 | case 20: 544 | $token = array($this->vstack[$len]); 545 | break; 546 | case 21: 547 | assert(\is_array($this->vstack[$len-2])); 548 | $this->vstack[$len-2][] = $this->vstack[$len]; 549 | $token = $this->vstack[$len-2]; 550 | break; 551 | } 552 | 553 | return array($token, new Undefined()); 554 | } 555 | 556 | /** 557 | * @param string $match 558 | * @return string 559 | */ 560 | private function stringInterpolation($match) 561 | { 562 | switch ($match[0]) { 563 | case '\\\\': 564 | return '\\'; 565 | case '\"': 566 | return '"'; 567 | case '\b': 568 | return \chr(8); 569 | case '\f': 570 | return \chr(12); 571 | case '\n': 572 | return "\n"; 573 | case '\r': 574 | return "\r"; 575 | case '\t': 576 | return "\t"; 577 | case '\/': 578 | return "/"; 579 | default: 580 | return html_entity_decode('&#x'.ltrim(substr($match[0], 2), '0').';', ENT_QUOTES, 'UTF-8'); 581 | } 582 | } 583 | 584 | /** 585 | * @param int $n 586 | * @return void 587 | */ 588 | private function popStack($n) 589 | { 590 | $this->stack = \array_slice($this->stack, 0, - (2 * $n)); 591 | $this->vstack = \array_slice($this->vstack, 0, - $n); 592 | $this->lstack = \array_slice($this->lstack, 0, - $n); 593 | } 594 | 595 | /** 596 | * @param string $input 597 | * @return void 598 | */ 599 | private function failOnBOM($input) 600 | { 601 | // UTF-8 ByteOrderMark sequence 602 | $bom = "\xEF\xBB\xBF"; 603 | 604 | if (substr($input, 0, 3) === $bom) { 605 | $this->parseError("BOM detected, make sure your input does not include a Unicode Byte-Order-Mark"); 606 | } 607 | } 608 | } 609 | -------------------------------------------------------------------------------- /src/Seld/JsonLint/Lexer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Seld\JsonLint; 13 | 14 | /** 15 | * Lexer class 16 | * 17 | * Ported from https://github.com/zaach/jsonlint 18 | */ 19 | class Lexer 20 | { 21 | /** @internal */ 22 | const EOF = 1; 23 | /** @internal */ 24 | const T_INVALID = -1; 25 | const T_SKIP_WHITESPACE = 0; 26 | const T_ERROR = 2; 27 | /** @internal */ 28 | const T_BREAK_LINE = 3; 29 | /** @internal */ 30 | const T_COMMENT = 30; 31 | /** @internal */ 32 | const T_OPEN_COMMENT = 31; 33 | /** @internal */ 34 | const T_CLOSE_COMMENT = 32; 35 | 36 | /** 37 | * @phpstan-var array, string> 38 | * @const 39 | */ 40 | private $rules = array( 41 | 0 => '/\G\s*\n\r?/', 42 | 1 => '/\G\s+/', 43 | 2 => '/\G-?([0-9]|[1-9][0-9]+)(\.[0-9]+)?([eE][+-]?[0-9]+)?\b/', 44 | 3 => '{\G"(?>\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4}|[^\0-\x1f\\\\"]++)*+"}', 45 | 4 => '/\G\{/', 46 | 5 => '/\G\}/', 47 | 6 => '/\G\[/', 48 | 7 => '/\G\]/', 49 | 8 => '/\G,/', 50 | 9 => '/\G:/', 51 | 10 => '/\Gtrue\b/', 52 | 11 => '/\Gfalse\b/', 53 | 12 => '/\Gnull\b/', 54 | 13 => '/\G$/', 55 | 14 => '/\G\/\//', 56 | 15 => '/\G\/\*/', 57 | 16 => '/\G\*\//', 58 | 17 => '/\G./', 59 | ); 60 | 61 | /** @var string */ 62 | private $input; 63 | /** @var bool */ 64 | private $more; 65 | /** @var bool */ 66 | private $done; 67 | /** @var 0|positive-int */ 68 | private $offset; 69 | /** @var int */ 70 | private $flags; 71 | 72 | /** @var string */ 73 | public $match; 74 | /** @var 0|positive-int */ 75 | public $yylineno; 76 | /** @var 0|positive-int */ 77 | public $yyleng; 78 | /** @var string */ 79 | public $yytext; 80 | /** @var array{first_line: 0|positive-int, first_column: 0|positive-int, last_line: 0|positive-int, last_column: 0|positive-int} */ 81 | public $yylloc; 82 | 83 | /** 84 | * @param int $flags 85 | */ 86 | public function __construct($flags = 0) 87 | { 88 | $this->flags = $flags; 89 | } 90 | 91 | /** 92 | * @return 0|1|4|6|8|10|11|14|17|18|21|22|23|24|30|-1 93 | */ 94 | public function lex() 95 | { 96 | while (true) { 97 | $symbol = $this->next(); 98 | switch ($symbol) { 99 | case self::T_SKIP_WHITESPACE: 100 | case self::T_BREAK_LINE: 101 | break; 102 | case self::T_COMMENT: 103 | case self::T_OPEN_COMMENT: 104 | if (!($this->flags & JsonParser::ALLOW_COMMENTS)) { 105 | $this->parseError('Lexical error on line ' . ($this->yylineno+1) . ". Comments are not allowed.\n" . $this->showPosition()); 106 | } 107 | $this->skipUntil($symbol === self::T_COMMENT ? self::T_BREAK_LINE : self::T_CLOSE_COMMENT); 108 | if ($this->done) { 109 | // last symbol '/\G$/' before EOF 110 | return 14; 111 | } 112 | break; 113 | case self::T_CLOSE_COMMENT: 114 | $this->parseError('Lexical error on line ' . ($this->yylineno+1) . ". Unexpected token.\n" . $this->showPosition()); 115 | default: 116 | return $symbol; 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * @param string $input 123 | * @return $this 124 | */ 125 | public function setInput($input) 126 | { 127 | $this->input = $input; 128 | $this->more = false; 129 | $this->done = false; 130 | $this->offset = 0; 131 | $this->yylineno = $this->yyleng = 0; 132 | $this->yytext = $this->match = ''; 133 | $this->yylloc = array('first_line' => 1, 'first_column' => 0, 'last_line' => 1, 'last_column' => 0); 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * @return string 140 | */ 141 | public function showPosition() 142 | { 143 | if ($this->yylineno === 0 && $this->offset === 1 && $this->match !== '{') { 144 | return $this->match.'...' . "\n^"; 145 | } 146 | 147 | $pre = str_replace("\n", '', $this->getPastInput()); 148 | $c = str_repeat('-', max(0, \strlen($pre) - 1)); // new Array(pre.length + 1).join("-"); 149 | 150 | return $pre . str_replace("\n", '', $this->getUpcomingInput()) . "\n" . $c . "^"; 151 | } 152 | 153 | /** 154 | * @return string 155 | */ 156 | public function getPastInput() 157 | { 158 | $pastLength = $this->offset - \strlen($this->match); 159 | 160 | return ($pastLength > 20 ? '...' : '') . substr($this->input, max(0, $pastLength - 20), min(20, $pastLength)); 161 | } 162 | 163 | /** 164 | * @return string 165 | */ 166 | public function getUpcomingInput() 167 | { 168 | $next = $this->match; 169 | if (\strlen($next) < 20) { 170 | $next .= substr($this->input, $this->offset, 20 - \strlen($next)); 171 | } 172 | 173 | return substr($next, 0, 20) . (\strlen($next) > 20 ? '...' : ''); 174 | } 175 | 176 | /** 177 | * @return string 178 | */ 179 | public function getFullUpcomingInput() 180 | { 181 | $next = $this->match; 182 | if (substr($next, 0, 1) === '"' && substr_count($next, '"') === 1) { 183 | $len = \strlen($this->input); 184 | if ($len === $this->offset) { 185 | $strEnd = $len; 186 | } else { 187 | $strEnd = min(strpos($this->input, '"', $this->offset + 1) ?: $len, strpos($this->input, "\n", $this->offset + 1) ?: $len); 188 | } 189 | $next .= substr($this->input, $this->offset, $strEnd - $this->offset); 190 | } elseif (\strlen($next) < 20) { 191 | $next .= substr($this->input, $this->offset, 20 - \strlen($next)); 192 | } 193 | 194 | return $next; 195 | } 196 | 197 | /** 198 | * @param string $str 199 | * @return never 200 | */ 201 | protected function parseError($str) 202 | { 203 | throw new ParsingException($str); 204 | } 205 | 206 | /** 207 | * @param int $token 208 | * @return void 209 | */ 210 | private function skipUntil($token) 211 | { 212 | $symbol = $this->next(); 213 | while ($symbol !== $token && false === $this->done) { 214 | $symbol = $this->next(); 215 | } 216 | } 217 | 218 | /** 219 | * @return 0|1|3|4|6|8|10|11|14|17|18|21|22|23|24|30|31|32|-1 220 | */ 221 | private function next() 222 | { 223 | if ($this->done) { 224 | return self::EOF; 225 | } 226 | if ($this->offset === \strlen($this->input)) { 227 | $this->done = true; 228 | } 229 | 230 | $token = null; 231 | $match = null; 232 | $col = null; 233 | $lines = null; 234 | 235 | if (!$this->more) { 236 | $this->yytext = ''; 237 | $this->match = ''; 238 | } 239 | 240 | $rulesLen = count($this->rules); 241 | 242 | for ($i=0; $i < $rulesLen; $i++) { 243 | if (preg_match($this->rules[$i], $this->input, $match, 0, $this->offset)) { 244 | $lines = explode("\n", $match[0]); 245 | array_shift($lines); 246 | $lineCount = \count($lines); 247 | $this->yylineno += $lineCount; 248 | $this->yylloc = array( 249 | 'first_line' => $this->yylloc['last_line'], 250 | 'last_line' => $this->yylineno+1, 251 | 'first_column' => $this->yylloc['last_column'], 252 | 'last_column' => $lineCount > 0 ? \strlen($lines[$lineCount - 1]) : $this->yylloc['last_column'] + \strlen($match[0]), 253 | ); 254 | $this->yytext .= $match[0]; 255 | $this->match .= $match[0]; 256 | $this->yyleng = \strlen($this->yytext); 257 | $this->more = false; 258 | $this->offset += \strlen($match[0]); 259 | return $this->performAction($i); 260 | } 261 | } 262 | 263 | if ($this->offset === \strlen($this->input)) { 264 | return self::EOF; 265 | } 266 | 267 | $this->parseError( 268 | 'Lexical error on line ' . ($this->yylineno+1) . ". Unrecognized text.\n" . $this->showPosition() 269 | ); 270 | } 271 | 272 | /** 273 | * @param int $rule 274 | * @return 0|3|4|6|8|10|11|14|17|18|21|22|23|24|30|31|32|-1 275 | */ 276 | private function performAction($rule) 277 | { 278 | switch ($rule) { 279 | case 0:/* skip break line */ 280 | return self::T_BREAK_LINE; 281 | case 1:/* skip whitespace */ 282 | return self::T_SKIP_WHITESPACE; 283 | case 2: 284 | return 6; 285 | case 3: 286 | $this->yytext = substr($this->yytext, 1, $this->yyleng-2); 287 | return 4; 288 | case 4: 289 | return 17; 290 | case 5: 291 | return 18; 292 | case 6: 293 | return 23; 294 | case 7: 295 | return 24; 296 | case 8: 297 | return 22; 298 | case 9: 299 | return 21; 300 | case 10: 301 | return 10; 302 | case 11: 303 | return 11; 304 | case 12: 305 | return 8; 306 | case 13: 307 | return 14; 308 | case 14: 309 | return self::T_COMMENT; 310 | case 15: 311 | return self::T_OPEN_COMMENT; 312 | case 16: 313 | return self::T_CLOSE_COMMENT; 314 | case 17: 315 | return self::T_INVALID; 316 | default: 317 | throw new \LogicException('Unsupported rule '.$rule); 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/Seld/JsonLint/ParsingException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Seld\JsonLint; 13 | 14 | class ParsingException extends \Exception 15 | { 16 | /** 17 | * @var array{text?: string, token?: string|int, line?: int, loc?: array{first_line: int, first_column: int, last_line: int, last_column: int}, expected?: string[]} 18 | */ 19 | protected $details; 20 | 21 | /** 22 | * @param string $message 23 | * @phpstan-param array{text?: string, token?: string|int, line?: int, loc?: array{first_line: int, first_column: int, last_line: int, last_column: int}, expected?: string[]} $details 24 | */ 25 | public function __construct($message, $details = array()) 26 | { 27 | $this->details = $details; 28 | parent::__construct($message); 29 | } 30 | 31 | /** 32 | * @phpstan-return array{text?: string, token?: string|int, line?: int, loc?: array{first_line: int, first_column: int, last_line: int, last_column: int}, expected?: string[]} 33 | */ 34 | public function getDetails() 35 | { 36 | return $this->details; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Seld/JsonLint/Undefined.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Seld\JsonLint; 13 | 14 | class Undefined 15 | { 16 | } 17 | --------------------------------------------------------------------------------