@prantlf/jsonlint 14.0.2 → 14.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,7 +1,7 @@
1
1
  MIT License
2
2
 
3
3
  Copyright (c) 2012-2018 Zachary Carter
4
- Copyright (c) 2019-2023 Ferdinand Prantl
4
+ Copyright (c) 2019-2024 Ferdinand Prantl
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -21,7 +21,7 @@ This is a fork of the original project ([zaach/jsonlint](https://github.com/zaac
21
21
  * Offers pretty-printing including comment-stripping and object keys without quotes (JSON5).
22
22
  * Prefers the native JSON parser if possible to run [10x faster than the custom parser].
23
23
  * Reports errors with rich additional information. From the JSON Schema validation too.
24
- * Consumes configuration from both command line and [configuration files](configuration).
24
+ * Consumes configuration from both command line and [configuration files](#configuration).
25
25
  * Implements JavaScript modules using [UMD] to work in Node.js, in a browser, everywhere.
26
26
  * Depends on up-to-date npm modules with no installation warnings.
27
27
  * Small size - 18.4 kB minified, 6.45 kB gzipped, 5.05 kB brotlied.
@@ -121,6 +121,12 @@ Usage: `jsonlint [options] [--] [<file, directory, pattern> ...]`
121
121
  -f, --config <file> read options from a custom configuration file
122
122
  -F, --no-config disable searching for configuration files
123
123
  -s, --sort-keys sort object keys (not when prettifying)
124
+ --sort-keys-ignore-case sort object keys ignoring the letter case
125
+ --sort-keys-locale <id> locale identifier to sort object keys with
126
+ (or "default" for the system default)
127
+ --sort-keys-case-first <id> order if only letter case is different
128
+ ("upper", "lower" and "false" are allowed)
129
+ --sort-keys-numeric sort by numbers recognised in object keys
124
130
  -E, --extensions <ext...> file extensions to process for directory walk
125
131
  (default: json, JSON)
126
132
  -i, --in-place overwrite the input files
@@ -154,6 +160,7 @@ Usage: `jsonlint [options] [--] [<file, directory, pattern> ...]`
154
160
  --enforce-double-quotes surrounds all strings with double quotes
155
161
  --enforce-single-quotes surrounds all strings with single quotes
156
162
  --trim-trailing-commas omit trailing commas from objects and arrays
163
+ --succeed-with-no-files succeed (exit code 0) if no files were found
157
164
  -v, --version output the version number
158
165
  -h, --help display help for command
159
166
 
@@ -193,6 +200,10 @@ The configuration is an object with the following properties, described above, w
193
200
  | --------- | ----- |
194
201
  | patterns | |
195
202
  | sort-keys | sortKeys |
203
+ | sort-keys-ignore-case | sortKeysIgnoreCase |
204
+ | sort-keys-locale | sortKeysLocale |
205
+ | sort-keys-case-first | sortKeysCaseFirst |
206
+ | sort-keys-numeric | sortKeysNumeric |
196
207
  | extensions | |
197
208
  | in-place | inPlace |
198
209
  | diff | |
@@ -428,7 +439,7 @@ ${reason}`)
428
439
 
429
440
  ## License
430
441
 
431
- Copyright (C) 2012-2023 Zachary Carter, Ferdinand Prantl
442
+ Copyright (C) 2012-2024 Zachary Carter, Ferdinand Prantl
432
443
 
433
444
  Licensed under the [MIT License].
434
445
 
package/lib/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { readdirSync, readFileSync, statSync, writeFileSync } = require('fs')
4
- const { extname, join } = require('path')
3
+ const { readdirSync, readFileSync, statSync, writeFileSync } = require('node:fs')
4
+ const { extname, join } = require('node:path')
5
5
  const { isDynamicPattern, sync } = require('fast-glob')
6
6
  const { parse, tokenize } = require('./jsonlint')
7
7
  const { format } = require('./formatter')
@@ -18,6 +18,12 @@ Options:
18
18
  -f, --config <file> read options from a custom configuration file
19
19
  -F, --no-config disable searching for configuration files
20
20
  -s, --sort-keys sort object keys (not when prettifying)
21
+ --sort-keys-ignore-case sort object keys ignoring the letter case
22
+ --sort-keys-locale <id> locale identifier to sort object keys with
23
+ (or "default" for the system default)
24
+ --sort-keys-case-first <id> order if only letter case is different
25
+ ("upper", "lower" and "false" are allowed)
26
+ --sort-keys-numeric sort by numbers recognised in object keys
21
27
  -E, --extensions <ext...> file extensions to process for directory walk
22
28
  (default: json, JSON)
23
29
  -i, --in-place overwrite the input files
@@ -51,6 +57,7 @@ Options:
51
57
  --enforce-double-quotes surrounds all strings with double quotes
52
58
  --enforce-single-quotes surrounds all strings with single quotes
53
59
  --trim-trailing-commas omit trailing commas from objects and arrays
60
+ --succeed-with-no-files succeed (exit code 0) if no files were found
54
61
  -v, --version output the version number
55
62
  -h, --help display help for command
56
63
 
@@ -85,6 +92,18 @@ for (let i = 2, l = argv.length; i < l; ++i) {
85
92
  case 's': case 'sort-keys':
86
93
  params.sortKeys = flag
87
94
  return
95
+ case 'sort-keys-ignore-case':
96
+ params.sortKeysIgnoreCase = flag
97
+ return
98
+ case 'sort-keys-locale':
99
+ params.sortKeysLocale = match[4] || argv[++i]
100
+ return
101
+ case 'sort-keys-case-first':
102
+ params.sortKeysCaseFirst = match[4] || argv[++i]
103
+ return
104
+ case 'sort-keys-numeric':
105
+ params.sortKeysNumeric = flag
106
+ return
88
107
  case 'E': case 'extensions':
89
108
  arg = match[4] || argv[++i]
90
109
  params.extensions.push(...arg.split(','))
@@ -100,7 +119,7 @@ for (let i = 2, l = argv.length; i < l; ++i) {
100
119
  return
101
120
  case 't': case 'indent':
102
121
  arg = match[4] || argv[++i]
103
- if (arg.trim().length > 0 && !isNaN(+arg)) arg = +arg
122
+ if (arg.trim().length > 0 && !Number.isNaN(+arg)) arg = +arg
104
123
  params.indent = arg
105
124
  return
106
125
  case 'c': case 'compact':
@@ -149,7 +168,7 @@ for (let i = 2, l = argv.length; i < l; ++i) {
149
168
  return
150
169
  case 'x': case 'context':
151
170
  arg = match[4] || argv[++i]
152
- if (isNaN(+arg)) {
171
+ if (Number.isNaN(+arg)) {
153
172
  throw new Error(`invalid diff context: "${arg}"`)
154
173
  }
155
174
  params.indent = +arg
@@ -190,6 +209,9 @@ for (let i = 2, l = argv.length; i < l; ++i) {
190
209
  case 'trim-trailing-commas':
191
210
  params.trimTrailingCommas = flag
192
211
  return
212
+ case 'succeed-with-no-files':
213
+ params.succeedWithNoFiles = flag
214
+ return
193
215
  case 'v': case 'version':
194
216
  console.log(require('../package.json').version)
195
217
  process.exit(0)
@@ -226,6 +248,10 @@ const paramNames = {
226
248
  'enforce-single-quotes': 'enforceSingleQuotes',
227
249
  'trim-trailing-commas': 'trimTrailingCommas',
228
250
  'sort-keys': 'sortKeys',
251
+ 'sort-keys-ignore-case': 'sortKeysIgnoreCase',
252
+ 'sort-keys-locale': 'sortKeysLocale',
253
+ 'sort-keys-case-first': 'sortKeysCaseFirst',
254
+ 'sort-keys-numeric': 'sortKeysNumeric',
229
255
  'pretty-print-invalid': 'prettyPrintInvalid',
230
256
  'log-files': 'logFiles',
231
257
  'in-place': 'inPlace',
@@ -283,18 +309,19 @@ function logNormalError (error, file) {
283
309
  }
284
310
 
285
311
  function logCompactError (error, file) {
286
- console.error(file + ': line ' + error.location.start.line +
287
- ', col ' + error.location.start.column + ', ' + error.reason + '.')
312
+ console.error(`${file}: line ${error.location.start.line}, col ${error.location.start.column}, ${error.reason}.`)
288
313
  }
289
314
 
290
315
  function processContents (source, file) {
291
- let parserOptions, parsed, formatted
316
+ let parserOptions
317
+ let parsed
318
+ let formatted
292
319
  try {
293
320
  parserOptions = {
294
321
  mode: params.mode,
295
322
  ignoreBOM: params.bom,
296
323
  ignoreComments: params.comments,
297
- ignoreTrailingCommas: params.trailingCommas,
324
+ ignoreTrailingCommas: params.trailingCommas || params.trimTrailingCommas,
298
325
  allowSingleQuotedStrings: params.singleQuotedStrings,
299
326
  allowDuplicateObjectKeys: params.duplicateKeys
300
327
  }
@@ -329,8 +356,29 @@ function processContents (source, file) {
329
356
  trimTrailingCommas: params.trimTrailingCommas
330
357
  })
331
358
  }
359
+ const sortOptions = {}
360
+ let sort
332
361
  if (params.sortKeys) {
333
- parsed = sortObject(parsed)
362
+ sort = true
363
+ }
364
+ if (params.sortKeysIgnoreCase) {
365
+ sortOptions.ignoreCase = true
366
+ sort = true
367
+ }
368
+ if (params.sortKeysLocale) {
369
+ sortOptions.locale = params.sortKeysLocale
370
+ sort = true
371
+ }
372
+ if (params.sortKeysCaseFirst) {
373
+ sortOptions.caseFirst = params.sortKeysCaseFirst
374
+ sort = true
375
+ }
376
+ if (params.sortKeysNumeric) {
377
+ sortOptions.numeric = true
378
+ sort = true
379
+ }
380
+ if (sort) {
381
+ parsed = sortObject(parsed, sortOptions)
334
382
  }
335
383
  return JSON.stringify(parsed, null, params.indent)
336
384
  } catch (e) {
@@ -381,7 +429,7 @@ function ensureLineBreak (parsed, source) {
381
429
  function checkContents (file, source, parsed) {
382
430
  const { createTwoFilesPatch, structuredPatch } = require('diff')
383
431
  const structured = structuredPatch(`${file}.orig`, file, source, parsed, '', '', { context: params.context })
384
- const length = structured.hunks && structured.hunks.length
432
+ const length = structured.hunks?.length
385
433
  if (length > 0) {
386
434
  const hunk = length === 1 ? 'hunk differs' : 'hunks differ'
387
435
  const message = `${length} ${hunk}`
@@ -413,10 +461,11 @@ function checkContents (file, source, parsed) {
413
461
  function diffContents (file, source, parsed) {
414
462
  const { createTwoFilesPatch, structuredPatch } = require('diff')
415
463
  const compact = params.quiet || params.compact
416
- let diff, length
464
+ let diff
465
+ let length
417
466
  if (compact) {
418
467
  diff = structuredPatch(`${file}.orig`, file, source, parsed, '', '', { context: params.context })
419
- length = diff.hunks && diff.hunks.length
468
+ length = diff.hunks?.length
420
469
  } else {
421
470
  diff = createTwoFilesPatch(`${file}.orig`, file, source, parsed, '', '', { context: params.context })
422
471
  length = diff.split(/\r?\n/).length - 4
@@ -487,7 +536,7 @@ function processPatterns (patterns) {
487
536
  const files = sync(patterns, { onlyFiles: true })
488
537
  if (!files.length) {
489
538
  console.error('no files found')
490
- process.exit(1)
539
+ process.exit(params.succeedWithNoFiles ? 0 : 1)
491
540
  }
492
541
  for (const file of files) {
493
542
  try {
package/lib/formatter.js CHANGED
@@ -36,7 +36,7 @@
36
36
  case '{':
37
37
  case '[':
38
38
  if (!inString) {
39
- outputString += currentChar + '\n' + repeat(indentString, indentLevel + 1)
39
+ outputString += `${currentChar}\n${repeat(indentString, indentLevel + 1)}`
40
40
  indentLevel += 1
41
41
  } else {
42
42
  outputString += currentChar
@@ -46,14 +46,14 @@
46
46
  case ']':
47
47
  if (!inString) {
48
48
  indentLevel -= 1
49
- outputString += '\n' + repeat(indentString, indentLevel) + currentChar
49
+ outputString += `\n${repeat(indentString, indentLevel)}${currentChar}`
50
50
  } else {
51
51
  outputString += currentChar
52
52
  }
53
53
  break
54
54
  case ',':
55
55
  if (!inString) {
56
- outputString += ',\n' + repeat(indentString, indentLevel)
56
+ outputString += `,\n${repeat(indentString, indentLevel)}`
57
57
  } else {
58
58
  outputString += currentChar
59
59
  }
package/lib/jsonlint.js CHANGED
@@ -103,6 +103,8 @@ const unescapeMap = {
103
103
  '/': '/'
104
104
  }
105
105
 
106
+ const ownsProperty = Object.prototype.hasOwnProperty
107
+
106
108
  function parseInternal (input, options) {
107
109
  if (typeof input !== 'string' || !(input instanceof String)) {
108
110
  input = String(input)
@@ -176,7 +178,7 @@ function parseInternal (input, options) {
176
178
  let message
177
179
  if (position < inputLength) {
178
180
  const token = JSON.stringify(input[position])
179
- message = 'Unexpected token ' + token
181
+ message = `Unexpected token ${token}`
180
182
  } else {
181
183
  message = 'Unexpected end of input'
182
184
  }
@@ -220,87 +222,84 @@ function parseInternal (input, options) {
220
222
 
221
223
  function parseGeneric () {
222
224
  if (position < inputLength) {
223
- startToken && startToken()
225
+ startToken?.()
224
226
  const char = input[position++]
225
227
  if (char === '"' || (char === '\'' && allowSingleQuotedStrings)) {
226
228
  const string = parseString(char)
227
- endToken && endToken('literal', string)
229
+ endToken?.('literal', string)
228
230
  return string
229
- } else if (char === '{') {
230
- endToken && endToken('symbol', '{')
231
+ }if (char === '{') {
232
+ endToken?.('symbol', '{')
231
233
  return parseObject()
232
- } else if (char === '[') {
233
- endToken && endToken('symbol', '[')
234
+ }if (char === '[') {
235
+ endToken?.('symbol', '[')
234
236
  return parseArray()
235
- } else if (char === '-' || char === '.' || isDecDigit(char) ||
237
+ }if (char === '-' || char === '.' || isDecDigit(char) ||
236
238
  (json5 && (char === '+' || char === 'I' || char === 'N'))) {
237
239
  const number = parseNumber()
238
- endToken && endToken('literal', number)
240
+ endToken?.('literal', number)
239
241
  return number
240
- } else if (char === 'n') {
242
+ }if (char === 'n') {
241
243
  parseKeyword('null')
242
- endToken && endToken('literal', null)
244
+ endToken?.('literal', null)
243
245
  return null
244
- } else if (char === 't') {
246
+ }if (char === 't') {
245
247
  parseKeyword('true')
246
- endToken && endToken('literal', true)
248
+ endToken?.('literal', true)
247
249
  return true
248
- } else if (char === 'f') {
250
+ }if (char === 'f') {
249
251
  parseKeyword('false')
250
- endToken && endToken('literal', false)
252
+ endToken?.('literal', false)
251
253
  return false
252
- } else {
254
+ }
253
255
  --position
254
- endToken && endToken()
256
+ endToken?.()
255
257
  return undefined
256
- }
257
258
  }
258
259
  }
259
260
 
260
261
  function parseKey () {
261
262
  let result
262
263
  if (position < inputLength) {
263
- startToken && startToken()
264
+ startToken?.()
264
265
  const char = input[position++]
265
266
  if (char === '"' || (char === '\'' && allowSingleQuotedStrings)) {
266
267
  const string = parseString(char)
267
- endToken && endToken('literal', string)
268
+ endToken?.('literal', string)
268
269
  return string
269
- } else if (char === '{') {
270
- endToken && endToken('symbol', '{')
270
+ }if (char === '{') {
271
+ endToken?.('symbol', '{')
271
272
  return parseObject()
272
- } else if (char === '[') {
273
- endToken && endToken('symbol', '[')
273
+ }if (char === '[') {
274
+ endToken?.('symbol', '[')
274
275
  return parseArray()
275
- } else if (char === '.' || isDecDigit(char)) {
276
+ }if (char === '.' || isDecDigit(char)) {
276
277
  const number = parseNumber(true)
277
- endToken && endToken('literal', number)
278
+ endToken?.('literal', number)
278
279
  return number
279
- } else if ((json5 && Uni.isIdentifierStart(char)) ||
280
+ }if ((json5 && Uni.isIdentifierStart(char)) ||
280
281
  (char === '\\' && input[position] === 'u')) {
281
282
  const rollback = position - 1
282
283
  result = parseIdentifier()
283
284
  if (result === undefined) {
284
285
  position = rollback
285
- endToken && endToken()
286
+ endToken?.()
286
287
  return undefined
287
- } else {
288
- endToken && endToken('literal', result)
289
- return result
290
288
  }
291
- } else {
289
+ endToken?.('literal', result)
290
+ return result
291
+ }
292
292
  --position
293
- endToken && endToken()
293
+ endToken?.()
294
294
  return undefined
295
- }
296
295
  }
297
296
  }
298
297
 
299
298
  function skipBOM () {
300
299
  if (isBOM(input)) {
301
- startToken && startToken()
300
+ startToken?.()
302
301
  ++position
303
- endToken && endToken('bom')
302
+ endToken?.('bom')
304
303
  }
305
304
  }
306
305
 
@@ -336,7 +335,7 @@ function parseInternal (input, options) {
336
335
  ++position
337
336
  }
338
337
  skipComment(input[position++] === '*')
339
- endToken && endToken('comment')
338
+ endToken?.('comment')
340
339
  } else {
341
340
  --position
342
341
  break
@@ -389,29 +388,29 @@ function parseInternal (input, options) {
389
388
  while (position < inputLength) {
390
389
  skipWhiteSpace()
391
390
  const key = parseKey()
392
- if (allowDuplicateObjectKeys === false && result[key]) {
393
- fail('Duplicate key: "' + key + '"')
391
+ if (allowDuplicateObjectKeys === false && ownsProperty.call(result, key)) {
392
+ fail(`Duplicate key: "${key}"`)
394
393
  }
395
394
  skipWhiteSpace()
396
- startToken && startToken()
395
+ startToken?.()
397
396
  let char = input[position++]
398
- endToken && endToken('symbol', char)
397
+ endToken?.('symbol', char)
399
398
  if (char === '}' && key === undefined) {
400
399
  if (!ignoreTrailingCommas && isNotEmpty) {
401
400
  --position
402
401
  fail('Trailing comma in object')
403
402
  }
404
403
  return result
405
- } else if (char === ':' && key !== undefined) {
404
+ }if (char === ':' && key !== undefined) {
406
405
  skipWhiteSpace()
407
- tokenPath && tokenPath.push(key)
406
+ tokenPath?.push(key)
408
407
  let value = parseGeneric()
409
- tokenPath && tokenPath.pop()
408
+ tokenPath?.pop()
410
409
 
411
- if (value === undefined) fail('No value found for key "' + key + '"')
410
+ if (value === undefined) fail(`No value found for key "${key}"`)
412
411
  if (typeof key !== 'string') {
413
412
  if (!json5 || typeof key !== 'number') {
414
- fail('Wrong key type: "' + key + '"')
413
+ fail(`Wrong key type: "${key}"`)
415
414
  }
416
415
  }
417
416
 
@@ -428,11 +427,10 @@ function parseInternal (input, options) {
428
427
  }
429
428
 
430
429
  skipWhiteSpace()
431
- startToken && startToken()
430
+ startToken?.()
432
431
  char = input[position++]
433
- endToken && endToken('symbol', char)
432
+ endToken?.('symbol', char)
434
433
  if (char === ',') {
435
- continue
436
434
  } else if (char === '}') {
437
435
  return result
438
436
  } else {
@@ -451,13 +449,13 @@ function parseInternal (input, options) {
451
449
  const result = []
452
450
  while (position < inputLength) {
453
451
  skipWhiteSpace()
454
- tokenPath && tokenPath.push(result.length)
452
+ tokenPath?.push(result.length)
455
453
  let item = parseGeneric()
456
- tokenPath && tokenPath.pop()
454
+ tokenPath?.pop()
457
455
  skipWhiteSpace()
458
- startToken && startToken()
456
+ startToken?.()
459
457
  const char = input[position++]
460
- endToken && endToken('symbol', char)
458
+ endToken?.('symbol', char)
461
459
  if (item !== undefined) {
462
460
  if (reviver) {
463
461
  item = reviver(String(result.length), item)
@@ -498,18 +496,18 @@ function parseInternal (input, options) {
498
496
  let result
499
497
 
500
498
  if (isOctal) {
501
- result = parseInt(string.replace(/^0o?/, ''), 8)
499
+ result = Number.parseInt(string.replace(/^0o?/, ''), 8)
502
500
  } else {
503
501
  result = Number(string)
504
502
  }
505
503
 
506
504
  if (Number.isNaN(result)) {
507
505
  --position
508
- fail('Bad numeric literal - "' + input.substr(start, position - start + 1) + '"')
506
+ fail(`Bad numeric literal - "${input.substr(start, position - start + 1)}"`)
509
507
  } else if (!json5 && !string.match(/^-?(0|[1-9][0-9]*)(\.[0-9]+)?(e[+-]?[0-9]+)?$/i)) {
510
508
  // additional restrictions imposed by json
511
509
  --position
512
- fail('Non-json numeric literal - "' + input.substr(start, position - start + 1) + '"')
510
+ fail(`Non-json numeric literal - "${input.substr(start, position - start + 1)}"`)
513
511
  } else {
514
512
  return result
515
513
  }
@@ -523,7 +521,7 @@ function parseInternal (input, options) {
523
521
 
524
522
  if (char === 'N' && json5) {
525
523
  parseKeyword('NaN')
526
- return NaN
524
+ return Number.NaN
527
525
  }
528
526
 
529
527
  if (char === 'I' && json5) {
@@ -608,7 +606,7 @@ function parseInternal (input, options) {
608
606
  isHexDigit(input[position + 3]) &&
609
607
  isHexDigit(input[position + 4])) {
610
608
  // UnicodeEscapeSequence
611
- char = String.fromCharCode(parseInt(input.substr(position + 1, 4), 16))
609
+ char = String.fromCharCode(Number.parseInt(input.substr(position + 1, 4), 16))
612
610
  position += 5
613
611
  }
614
612
 
@@ -639,7 +637,7 @@ function parseInternal (input, options) {
639
637
  let char = input[position++]
640
638
  if (char === endChar) {
641
639
  return result
642
- } else if (char === '\\') {
640
+ }if (char === '\\') {
643
641
  if (position >= inputLength) {
644
642
  fail()
645
643
  }
@@ -662,7 +660,7 @@ function parseInternal (input, options) {
662
660
  }
663
661
  position++
664
662
  }
665
- result += String.fromCharCode(parseInt(input.substr(position - count, count), 16))
663
+ result += String.fromCharCode(Number.parseInt(input.substr(position - count, count), 16))
666
664
  } else if (json5 && isOctDigit(char)) {
667
665
  let digits
668
666
  if (char < '4' && isOctDigit(input[position]) && isOctDigit(input[position + 1])) {
@@ -675,7 +673,7 @@ function parseInternal (input, options) {
675
673
  digits = 1
676
674
  }
677
675
  position += digits - 1
678
- result += String.fromCharCode(parseInt(input.substr(position - digits, digits), 8))
676
+ result += String.fromCharCode(Number.parseInt(input.substr(position - digits, digits), 8))
679
677
  } else if (json5) {
680
678
  // \X -> x
681
679
  result += char
@@ -710,9 +708,8 @@ function parseInternal (input, options) {
710
708
  returnValue = reviver('', returnValue)
711
709
  }
712
710
  return tokenize ? tokens : returnValue
713
- } else {
714
- fail()
715
711
  }
712
+ fail()
716
713
  } else {
717
714
  if (position) {
718
715
  fail('No data, only a whitespace')
@@ -759,9 +756,9 @@ function pathToPointer (tokens) {
759
756
  if (tokens.length === 0) {
760
757
  return ''
761
758
  }
762
- return '/' + tokens
759
+ return `/${tokens
763
760
  .map(escapePointerToken)
764
- .join('/')
761
+ .join('/')}`
765
762
  }
766
763
 
767
764
  function unescapePointerToken (token) {
@@ -825,7 +822,7 @@ function upcomingInput (input, offset) {
825
822
  function getPositionContext (input, offset) {
826
823
  const past = pastInput(input, offset)
827
824
  const upcoming = upcomingInput(input, offset)
828
- const pointer = new Array(past.length + 1).join('-') + '^'
825
+ const pointer = `${new Array(past.length + 1).join('-')}^`
829
826
  return {
830
827
  excerpt: past + upcoming,
831
828
  pointer
@@ -889,13 +886,13 @@ function getLocationOnSpiderMonkey (input, reason) {
889
886
  function getTexts (reason, input, offset, line, column) {
890
887
  const position = getPositionContext(input, offset)
891
888
  const excerpt = position.excerpt
892
- let message, pointer
889
+ let message
890
+ let pointer
893
891
  if (typeof line === 'number') {
894
892
  pointer = position.pointer
895
- message = 'Parse error on line ' + line + ', column ' +
896
- column + ':\n' + excerpt + '\n' + pointer + '\n' + reason
893
+ message = `Parse error on line ${line}, column ${column}:\n${excerpt}\n${pointer}\n${reason}`
897
894
  } else {
898
- message = 'Parse error in JSON input:\n' + excerpt + '\n' + reason
895
+ message = `Parse error in JSON input:\n${excerpt}\n${reason}`
899
896
  }
900
897
  return {
901
898
  message,
@@ -909,7 +906,9 @@ function improveNativeError (input, error) {
909
906
  const location = getLocationOnV8(input, reason) ||
910
907
  checkUnexpectedEndOnV8(input, reason) ||
911
908
  getLocationOnSpiderMonkey(input, reason)
912
- let offset, line, column
909
+ let offset
910
+ let line
911
+ let column
913
912
  if (location) {
914
913
  offset = location.offset
915
914
  line = location.line
@@ -940,7 +939,11 @@ function parseNative (input, reviver) {
940
939
  try {
941
940
  return JSON.parse(input, reviver)
942
941
  } catch (error) {
943
- throw improveNativeError(input, error)
942
+ const newError = improveNativeError(input, error)
943
+ if (error.location) throw newError
944
+ // If the native error didn't contain location, parse once more
945
+ // by the custom parser, which always provides the error location.
946
+ return parseCustom (input, reviver)
944
947
  }
945
948
  }
946
949
  /* globals navigator, process, parseCustom, parseNative */
@@ -957,7 +960,7 @@ function needsCustomParser (options) {
957
960
  function getReviver (options) {
958
961
  if (typeof options === 'function') {
959
962
  return options
960
- } else if (options) {
963
+ }if (options) {
961
964
  return options.reviver
962
965
  }
963
966
  }