/**
 * @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;
