Skip to content

Commit 2781670

Browse files
committed
feat: Replace the parser generated by Jison with a hand-built parser from JJU
BREAKING CHANGE: There is no `yy.parseError` to intercept error handling. Use the thrown error - it contains all available information. The error does not include the `hash` object with structured information. Look for the [documentd properties](/prantlf/jsonlint#error-handling). The location of the error occurrence is available as `location.start`, for example. DEPRECATION: The only exposed object to use from now on is the `parse` method as a named export. Other exports (`parser` and `Parser`) are deprecated and will be removed in future. The parser from ["Utilities to work with JSON/JSON5 documents"](/rlidwka/jju) is four times faster, than the prevous one, has approximatly the same size and can be easier enhanced, regarding both features and error handling.
1 parent 85d9fc6 commit 2781670

File tree

136 files changed

+2681
-549
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

136 files changed

+2681
-549
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ jsonlint*.js
44
test/fails
55
test/passes
66
test/recursive
7+
test/v8
78
web/*.min.*

.vscode/launch.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
{
55
"type": "node",
66
"request": "launch",
7-
"name": "bin",
7+
"name": "cli",
88
"program": "${workspaceRoot}/lib/cli.js",
99
"args": [
1010
"-C",
11-
"package.json"
11+
"test/passes/1.json"
1212
],
1313
"skipFiles": [
1414
"<node_internals>/**/*.js"
@@ -18,7 +18,7 @@
1818
"type": "node",
1919
"request": "launch",
2020
"name": "test",
21-
"program": "${workspaceRoot}/test/all-tests",
21+
"program": "${workspaceRoot}/test/parse1.js",
2222
"args": [
2323
"--native-parser"
2424
],

.vscode/settings.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
"search.exclude": {
88
"**/benchmarks/jison": true,
99
"**/benchmarks/pegjs": true,
10-
"**/coverage": true
11-
},
10+
"**/coverage": true,
11+
"**/test/fails": true,
12+
"**/test/passes": true,
13+
"**/test/recursive": true,
14+
"**/test/v8": true
15+
},
1216
"eslint.enable": true
1317
}

README.md

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ This is a fork of the original package with the following enhancements:
1717
* Can parse and skip JavaScript-style comments.
1818
* Can accept single quotes (apostrophes) as string delimiters.
1919
* Implements JavaScript modules using [UMD](https://github.com/umdjs/umd) to work everywhere.
20-
* Can use the native JSON parser to gain the [best performance](./benchmarks#json-parser-comparison), while showing error messages of the same quality.
20+
* Prefers using the native JSON parser to gain the [best performance](./benchmarks#json-parser-comparison), while showing error messages of the same quality.
2121
* Depends on up-to-date npm modules with no installation warnings.
22-
* Small size - 13.4 kB minified, 4.6 kB gzipped.
22+
* Small size - 17.6 kB minified, 6.1 kB gzipped.
2323

2424
## Command-line Interface
2525

@@ -91,51 +91,65 @@ parse("{'creative': true /* for creativity */}", {
9191
})
9292
```
9393

94-
Parsing methods return the parsed object or throw an error. If the data cam be parsed, you will be able to validate them against a JSON schema in addition:
94+
The parsing method returns the parsed object or throws an error. If the parsing succeeds, you can to validate the input against a JSON schema too:
9595

9696
```js
97-
const { parser } = require('@prantlf/jsonlint')
98-
const validator = require('@prantlf/jsonlint/lib/validator')
99-
const validate = validator.compile('string with JSON schema')
97+
const { parse } = require('@prantlf/jsonlint')
98+
const { compile } = require('@prantlf/jsonlint/lib/validator')
99+
const validate = compile('string with JSON schema')
100100
// Throws an error in case of failure.
101-
validate(parser.parse('string with JSON data'))
101+
validate(parse('string with JSON data'))
102102
```
103103

104-
Compiling JSON schema supports the same options for customisation and performance improvement as parsing JSON data (`ignoreComments`, `allowSingleQuotedStrings`, `limitedErrorInfo`). They can be passed as the second (object) parameter. The optional second `environment` parameter can be passed as an additional property in the options object:
104+
Compiling JSON schema supports the same options as parsing JSON data (`ignoreComments`, `allowSingleQuotedStrings`). They can be passed as the second (object) parameter. The optional second `environment` parameter can be passed as an additional property in the options object too:
105105

106106
```js
107-
const validator = require('@prantlf/jsonlint/lib/validator')
108-
const validate = validator.compile('string with JSON schema', {
109-
limitedErrorInfo: true,
107+
const validate = compile('string with JSON schema', {
110108
environment: 'json-schema-draft-04'
111109
})
112-
validate(jsonData)
113110
```
114111

115112
### Performance
116113

117114
This is a part of an output from the [parser benchmark](./benchmarks#json-parser-comparison), when parsing a 4.2 KB formatted string ([package.json](./package.json)) with Node.js 10.15.3:
118115

119116
the built-in parser x 61,588 ops/sec ±0.75% (80 runs sampled)
120-
the pure jison parser x 2,516 ops/sec ±1.31% (84 runs sampled)
121-
the extended jison parser x 2,434 ops/sec ±0.74% (89 runs sampled)
117+
the pure jju parser x 11,396 ops/sec ±1.05% (86 runs sampled)
118+
the extended jju parser x 8,221 ops/sec ±0.99% (87 runs sampled)
119+
120+
A custom JSON parser is [a lot slower](./benchmarks/results/performance.md#results) than the built-in one. However, it is more important to have a [clear error reporting](./benchmarks/results/errorReportingQuality.md#results) than the highest speed in scenarios like parsing configuration files. Extending the parser with the support for comments and single-quoted strings does not affect significantly the performance.
121+
122+
### Error Handling
123+
124+
If parsing fails, a `SyntaxError` will be thrown with the following properties:
125+
126+
| Property | Description |
127+
| ---------- | ----------------------------------------- |
128+
| `message` | the full multi-line error message |
129+
| `reason` | one-line explanation of the error |
130+
| `exzerpt` | part of the input string around the error |
131+
| `pointer` | "--^" pointing to the error in `exzerpt` |
132+
| `location` | object pointing to the error location |
133+
134+
The `location` object contains properties `line`, `column` and `offset`.
122135

123-
The custom JSON parser is [a lot slower](./benchmarks/results/performance.md#results) than the built-in one. However, it is more important to have a [clear error reporting](./benchmarks/results/errorReportingQuality.md#results) than the highest speed in scenarios like parsing configuration files. Extending the parser with the support for comments and single-quoted strings does not affect significantly the performance.
136+
The following code logs twice the following message:
124137

125-
You can enable the (fastest) native JSON parser by the `limitedErrorInfo` option, if you do not need the full structured error information provided by the custom parser. The error thrown by the native parser includes the same rich message, but some properties are missing, because the native parser does not return structured information. Parts of the message are returned instead to allow custom error reporting:
138+
Parse error on line 1, column 14:
139+
{"creative": ?}
140+
-------------^
141+
Unexpected token ?
126142

127143
```js
128144
const { parse } = require('@prantlf/jsonlint')
129145
try {
130-
parse('{"creative": ?}', { limitedErrorInfo: true })
146+
parse('{"creative": ?}')
131147
} catch (error) {
132148
const { message, reason, exzerpt, pointer, location } = error
133149
const { column, line, offset } = location.start
134-
// Logs the same text as is included in the `message` property:
135-
// Parse error on line 1, column 14:
136-
// {"creative": ?}
137-
// -------------^
138-
// Unexpected token ?
150+
// Logs the complete error message:
151+
console.log(message)
152+
// Logs the same text as included in the `message` property:
139153
console.log(`Parse error on line ${line}, ${column} column:
140154
${exzerpt}
141155
${pointer}

lib/cli.js

Lines changed: 17 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
var fs = require('fs')
44
var path = require('path')
5-
var parser = require('./jsonlint').parser
5+
var parser = require('./jsonlint')
66
var formatter = require('./formatter')
77
var sorter = require('./sorter')
88
var validator = require('./validator')
@@ -36,43 +36,22 @@ var options = require('commander')
3636
})
3737
.parse(process.argv)
3838

39-
// Workaround for missing column number in jison error output.
40-
function parseError (message, hash) {
41-
const match = /Parse error on line (\d+):/.exec(message)
42-
if (match) {
43-
const loc = parseError.yy.yylloc
44-
message = message.substr(0, match.index) +
45-
`Parse error on line ${match[1]}` +
46-
(loc ? `, column ${loc.first_column + 1}:` : ':') +
47-
message.substr(match.index + match[0].length)
48-
}
49-
if (hash.recoverable) {
50-
this.trace(message)
51-
} else {
52-
const error = new SyntaxError(message)
53-
error.hash = hash
54-
throw error
55-
}
39+
function logNormalError (error, file) {
40+
console.log('File:', file)
41+
console.error(error.message)
5642
}
5743

58-
parser.yy.parseError = parseError
59-
60-
var parsedFile
61-
if (options.compact) {
62-
parser.parseError = parser.yy.parseError = parser.lexer.parseError = function (str, hash) {
63-
console.error(parsedFile + ': line ' + hash.loc.first_line + ', col ' + hash.loc.last_column + ', found: \'' + hash.token + '\' - expected: ' + hash.expected.join(', ') + '.')
64-
throw new Error(str)
65-
}
44+
function logCompactError (error, file) {
45+
console.error(file + ': line ' + error.location.start.line +
46+
', col ' + error.location.start.column + ', ' + error.reason + '.')
6647
}
6748

6849
function parse (source, file) {
6950
var parserOptions, parsed, formatted
70-
parsedFile = file
7151
try {
7252
parserOptions = {
7353
ignoreComments: options.comments,
74-
allowSingleQuotedStrings: options.singleQuotedStrings,
75-
limitedErrorInfo: !(options.comments || options.singleQuotedStrings)
54+
allowSingleQuotedStrings: options.singleQuotedStrings
7655
}
7756
parsed = parser.parse(source, parserOptions)
7857
if (options.sortKeys) {
@@ -87,19 +66,9 @@ function parse (source, file) {
8766
} catch (error) {
8867
var message = 'Loading the JSON schema failed: "' +
8968
options.validate + '".\n' + error.message
90-
if (options.compact) {
91-
console.error(file + ':', error.message)
92-
}
9369
throw new Error(message)
9470
}
95-
try {
96-
validate(parsed)
97-
} catch (error) {
98-
if (options.compact) {
99-
console.error(file + ':', error.message)
100-
}
101-
throw error
102-
}
71+
validate(parsed)
10372
}
10473
return JSON.stringify(parsed, null, options.indent)
10574
} catch (e) {
@@ -114,17 +83,19 @@ function parse (source, file) {
11483
// Re-parse so exception output gets better line numbers
11584
parsed = parser.parse(formatted)
11685
} catch (e) {
117-
if (!options.compact) {
118-
console.log('File:', file)
119-
console.error(e)
86+
if (options.compact) {
87+
logCompactError(e, file)
88+
} else {
89+
logNormalError(e, file)
12090
}
12191
// force the pretty print before exiting
12292
console.log(formatted)
12393
}
12494
} else {
125-
if (!options.compact) {
126-
console.log('File:', file)
127-
console.error(e)
95+
if (options.compact) {
96+
logCompactError(e, file)
97+
} else {
98+
logNormalError(e, file)
12899
}
129100
}
130101
process.exit(1)

lib/validator.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@
5555
try {
5656
schema = jsonlint.parse(schema, {
5757
ignoreComments: options.ignoreComments,
58-
allowSingleQuotedStrings: options.allowSingleQuotedStrings,
59-
limitedErrorInfo: options.limitedErrorInfo
58+
allowSingleQuotedStrings: options.allowSingleQuotedStrings
6059
})
6160
validate = ajv.compile(schema)
6261
} catch (error) {

0 commit comments

Comments
 (0)