UNPKG

@eslint/json

Version:

JSON linting plugin for ESLint

142 lines (141 loc) 6.49 kB
/** * @fileoverview Rule to detect unsafe values in JSON. * @author Bradley Meck Farias */ //----------------------------------------------------------------------------- // Type Definitions //----------------------------------------------------------------------------- /** * @import { JSONRuleDefinition } from "../types.js"; * @typedef {"unsafeNumber"|"unsafeInteger"|"unsafeZero"|"subnormal"|"loneSurrogate"} NoUnsafeValuesMessageIds * @typedef {JSONRuleDefinition<{ MessageIds: NoUnsafeValuesMessageIds }>} NoUnsafeValuesRuleDefinition */ //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- /* * This rule is based on the JSON grammar from RFC 8259, section 6. * https://tools.ietf.org/html/rfc8259#section-6 * * Also, this rule is based on the JSON5 grammar from json5.org, section 6. * https://spec.json5.org/#numbers * * We separately capture the integer and fractional parts of a number, so that * we can check for unsafe numbers that will evaluate to Infinity. */ const NUMBER = /^[+-]?(?<int>0|([1-9]\d*))?(?:\.(?<frac>\d*))?(?:e[+-]?\d+)?$/iu; const NON_ZERO = /[1-9]/u; //----------------------------------------------------------------------------- // Rule Definition //----------------------------------------------------------------------------- /** @type {NoUnsafeValuesRuleDefinition} */ const rule = { meta: { type: "problem", languages: ["json/json", "json/jsonc", "json/json5"], docs: { recommended: true, description: "Disallow JSON values that are unsafe for interchange", dialects: ["JSON", "JSONC", "JSON5"], url: "https://github.com/eslint/json/tree/main/docs/rules/no-unsafe-values.md", }, messages: { unsafeNumber: "The number '{{ value }}' will evaluate to Infinity.", unsafeInteger: "The integer '{{ value }}' is outside the safe integer range.", unsafeZero: "The number '{{ value }}' will evaluate to zero.", subnormal: "Unexpected subnormal number '{{ value }}' found, which may cause interoperability issues.", loneSurrogate: "Lone surrogate '{{ surrogate }}' found.", }, }, create(context) { return { Number(node) { const value = context.sourceCode.getText(node); if (Number.isFinite(node.value) !== true) { context.report({ loc: node.loc, messageId: "unsafeNumber", data: { value }, }); } else { // Also matches -0, intentionally if (node.value === 0) { // If the value has been rounded down to 0, but there was some // fraction or non-zero part before the e-, this is a very small // number that doesn't fit inside an f64. const match = value.match(NUMBER); if (match === null) { return; } // If any part of the number other than the exponent has a // non-zero digit in it, this number was not intended to be // evaluated down to a zero. if (NON_ZERO.test(match.groups.int) || NON_ZERO.test(match.groups.frac)) { context.report({ loc: node.loc, messageId: "unsafeZero", data: { value }, }); } } else if (!/[.e]/iu.test(value)) { // Intended to be an integer if (node.value > Number.MAX_SAFE_INTEGER || node.value < Number.MIN_SAFE_INTEGER) { context.report({ loc: node.loc, messageId: "unsafeInteger", data: { value }, }); } } else { // Floating point. Check for subnormal. const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setFloat64(0, node.value, false); const asBigInt = view.getBigUint64(0, false); // Subnormals have an 11-bit exponent of 0 and a non-zero mantissa. if ((asBigInt & 0x7ff0000000000000n) === 0n) { context.report({ loc: node.loc, messageId: "subnormal", // Value included so that it's seen in scientific notation data: { value, }, }); } } } }, String(node) { if (node.value.isWellFormed) { if (node.value.isWellFormed()) { return; } } // match any high surrogate and, if it exists, a paired low surrogate // match any low surrogate not already matched const surrogatePattern = /[\uD800-\uDBFF][\uDC00-\uDFFF]?|[\uDC00-\uDFFF]/gu; /** @type {RegExpExecArray | null} */ let match; while ((match = surrogatePattern.exec(node.value)) !== null) { // only need to report non-paired surrogates if (match[0].length < 2) { context.report({ loc: node.loc, messageId: "loneSurrogate", data: { surrogate: JSON.stringify(match[0]).slice(1, -1), }, }); } } }, }; }, }; export default rule;