
/**
 * @import { DocumentNode, AnyNode, Token, MemberNode } from "@humanwhocodes/momoa"
 * @import { SourceLocation, FileProblem, DirectiveType, RulesConfig, Language, OkParseResult, ParseResult, File } from "@eslint/core"
 * @import { JSONSyntaxElement, JSONRuleDefinition } from "./types.ts"
 */
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var momoa = require('@humanwhocodes/momoa');
var pluginKit = require('@eslint/plugin-kit');
var naturalCompare = require('natural-compare');

/**
 * @fileoverview The JSONSourceCode class.
 * @author Nicholas C. Zakas
 */


//-----------------------------------------------------------------------------
// Types
//-----------------------------------------------------------------------------

/**
 */

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

const commentParser = new pluginKit.ConfigCommentParser();

const INLINE_CONFIG =
	/^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u;

/**
 * A class to represent a step in the traversal process.
 */
class JSONTraversalStep extends pluginKit.VisitNodeStep {
	/**
	 * The target of the step.
	 * @type {AnyNode}
	 */
	target = undefined;

	/**
	 * Creates a new instance.
	 * @param {Object} options The options for the step.
	 * @param {AnyNode} options.target The target of the step.
	 * @param {1|2} options.phase The phase of the step.
	 * @param {Array<any>} options.args The arguments of the step.
	 */
	constructor({ target, phase, args }) {
		super({ target, phase, args });

		this.target = target;
	}
}

/**
 * Processes tokens to extract comments and their starting tokens.
 * @param {Array<Token>} tokens The tokens to process.
 * @returns {{ comments: Array<Token>, starts: Map<number, number>, ends: Map<number, number>}}
 * An object containing an array of comments, a map of starting token range to token index, and
 * a map of ending token range to token index.
 */
function processTokens(tokens) {
	/** @type {Array<Token>} */
	const comments = [];

	/** @type {Map<number, number>} */
	const starts = new Map();

	/** @type {Map<number, number>} */
	const ends = new Map();

	for (let i = 0; i < tokens.length; i++) {
		const token = tokens[i];

		if (token.type.endsWith("Comment")) {
			comments.push(token);
		}

		starts.set(token.range[0], i);
		ends.set(token.range[1], i);
	}

	return { comments, starts, ends };
}

//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------

/**
 * JSON Source Code Object
 * @extends {TextSourceCodeBase<{LangOptions: JSONLanguageOptions, RootNode: DocumentNode, SyntaxElementWithLoc: JSONSyntaxElement, ConfigNode: Token}>}
 */
class JSONSourceCode extends pluginKit.TextSourceCodeBase {
	/**
	 * Cached traversal steps.
	 * @type {Array<JSONTraversalStep>|undefined}
	 */
	#steps;

	/**
	 * Cache of parent nodes.
	 * @type {WeakMap<AnyNode, AnyNode>}
	 */
	#parents = new WeakMap();

	/**
	 * Collection of inline configuration comments.
	 * @type {Array<Token>}
	 */
	#inlineConfigComments;

	/**
	 * The AST of the source code.
	 * @type {DocumentNode}
	 */
	ast = undefined;

	/**
	 * The comment tokens in the source code.
	 * @type {Array<Token>|undefined}
	 */
	comments;

	/**
	 * A map of token start positions to their corresponding index.
	 * @type {Map<number, number>}
	 */
	#tokenStarts;

	/**
	 * A map of token end positions to their corresponding index.
	 * @type {Map<number, number>}
	 */
	#tokenEnds;

	/**
	 * Creates a new instance.
	 * @param {Object} options The options for the instance.
	 * @param {string} options.text The source code text.
	 * @param {DocumentNode} options.ast The root AST node.
	 */
	constructor({ text, ast }) {
		super({ text, ast });
		this.ast = ast;

		const { comments, starts, ends } = processTokens(this.ast.tokens ?? []);
		this.comments = comments;
		this.#tokenStarts = starts;
		this.#tokenEnds = ends;
	}

	/**
	 * Returns the value of the given comment.
	 * @param {Token} comment The comment to get the value of.
	 * @returns {string} The value of the comment.
	 * @throws {Error} When an unexpected comment type is passed.
	 */
	#getCommentValue(comment) {
		if (comment.type === "LineComment") {
			return this.getText(comment).slice(2); // strip leading `//`
		}

		if (comment.type === "BlockComment") {
			return this.getText(comment).slice(2, -2); // strip leading `/*` and trailing `*/`
		}

		throw new Error(`Unexpected comment type '${comment.type}'`);
	}

	/**
	 * Returns an array of all inline configuration nodes found in the
	 * source code.
	 * @returns {Array<Token>} An array of all inline configuration nodes.
	 */
	getInlineConfigNodes() {
		if (!this.#inlineConfigComments) {
			this.#inlineConfigComments = this.comments.filter(comment =>
				INLINE_CONFIG.test(this.#getCommentValue(comment)),
			);
		}

		return this.#inlineConfigComments ?? [];
	}

	/**
	 * Returns directives that enable or disable rules along with any problems
	 * encountered while parsing the directives.
	 * @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
	 *      that ESLint needs to further process the directives.
	 */
	getDisableDirectives() {
		/** @type {Array<FileProblem>} */
		const problems = [];
		/** @type {Array<Directive>} */
		const directives = [];

		this.getInlineConfigNodes().forEach(comment => {
			const { label, value, justification } =
				commentParser.parseDirective(this.#getCommentValue(comment));

			// `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
			if (
				label === "eslint-disable-line" &&
				comment.loc.start.line !== comment.loc.end.line
			) {
				const message = `${label} comment should not span multiple lines.`;

				problems.push({
					ruleId: null,
					message,
					loc: comment.loc,
				});
				return;
			}

			switch (label) {
				case "eslint-disable":
				case "eslint-enable":
				case "eslint-disable-next-line":
				case "eslint-disable-line": {
					const directiveType = label.slice("eslint-".length);

					directives.push(
						new pluginKit.Directive({
							type: /** @type {DirectiveType} */ (directiveType),
							node: comment,
							value,
							justification,
						}),
					);
				}

				// no default
			}
		});

		return { problems, directives };
	}

	/**
	 * Returns inline rule configurations along with any problems
	 * encountered while parsing the configurations.
	 * @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:SourceLocation}>}} Information
	 *      that ESLint needs to further process the rule configurations.
	 */
	applyInlineConfig() {
		/** @type {Array<FileProblem>} */
		const problems = [];
		/** @type {Array<{config:{rules:RulesConfig},loc:SourceLocation}>} */
		const configs = [];

		this.getInlineConfigNodes().forEach(comment => {
			const { label, value } = commentParser.parseDirective(
				this.#getCommentValue(comment),
			);

			if (label === "eslint") {
				const parseResult = commentParser.parseJSONLikeConfig(value);

				if (parseResult.ok) {
					configs.push({
						config: {
							rules: parseResult.config,
						},
						loc: comment.loc,
					});
				} else {
					problems.push({
						ruleId: null,
						message:
							/** @type {{ok: false, error: { message: string }}} */ (
								parseResult
							).error.message,
						loc: comment.loc,
					});
				}
			}
		});

		return {
			configs,
			problems,
		};
	}

	/**
	 * Returns the parent of the given node.
	 * @param {AnyNode} node The node to get the parent of.
	 * @returns {AnyNode|undefined} The parent of the node.
	 */
	getParent(node) {
		return this.#parents.get(node);
	}

	/**
	 * Traverse the source code and return the steps that were taken.
	 * @returns {Iterable<JSONTraversalStep>} The steps that were taken while traversing the source code.
	 */
	traverse() {
		// Because the AST doesn't mutate, we can cache the steps
		if (this.#steps) {
			return this.#steps.values();
		}

		/** @type {Array<JSONTraversalStep>} */
		const steps = (this.#steps = []);

		for (const { node, parent, phase } of momoa.iterator(this.ast)) {
			if (parent) {
				this.#parents.set(
					/** @type {AnyNode} */ (node),
					/** @type {AnyNode} */ (parent),
				);
			}

			steps.push(
				new JSONTraversalStep({
					target: /** @type {AnyNode} */ (node),
					phase: phase === "enter" ? 1 : 2,
					args: [node, parent],
				}),
			);
		}

		return steps;
	}

	/**
	 * Gets the token before the given node or token, optionally including comments.
	 * @param {AnyNode|Token} nodeOrToken The node or token to get the previous token for.
	 * @param {Object} [options] Options object.
	 * @param {boolean} [options.includeComments] If true, return comments when they are present.
	 * @returns {Token|null} The previous token or comment, or null if there is none.
	 */
	getTokenBefore(nodeOrToken, { includeComments = false } = {}) {
		const index = this.#tokenStarts.get(nodeOrToken.range[0]);

		if (index === undefined) {
			return null;
		}

		let previousIndex = index - 1;
		if (previousIndex < 0) {
			return null;
		}

		const tokens = this.ast.tokens;
		let tokenOrComment = tokens[previousIndex];

		if (includeComments) {
			return tokenOrComment;
		}

		// skip comments
		while (tokenOrComment?.type.endsWith("Comment")) {
			previousIndex--;

			if (previousIndex < 0) {
				return null;
			}

			tokenOrComment = tokens[previousIndex];
		}
		return tokenOrComment;
	}

	/**
	 * Gets the token after the given node or token, skipping any comments unless includeComments is true.
	 * @param {AnyNode|Token} nodeOrToken The node or token to get the next token for.
	 * @param {Object} [options] Options object.
	 * @param {boolean} [options.includeComments=false] If true, return comments when they are present.
	 * @returns {Token|null} The next token or comment, or null if there is none.
	 */
	getTokenAfter(nodeOrToken, { includeComments = false } = {}) {
		const index = this.#tokenEnds.get(nodeOrToken.range[1]);

		if (index === undefined) {
			return null;
		}

		let nextIndex = index + 1;
		const tokens = this.ast.tokens;
		if (nextIndex >= tokens.length) {
			return null;
		}

		let tokenOrComment = tokens[nextIndex];

		if (includeComments) {
			return tokenOrComment;
		}

		// skip comments
		while (tokenOrComment?.type.endsWith("Comment")) {
			nextIndex++;

			if (nextIndex >= tokens.length) {
				return null;
			}

			tokenOrComment = tokens[nextIndex];
		}

		return tokenOrComment;
	}
}

/**
 * @fileoverview The JSONLanguage class.
 * @author Nicholas C. Zakas
 */


//-----------------------------------------------------------------------------
// Types
//-----------------------------------------------------------------------------

/**
 *
 * @typedef {OkParseResult<DocumentNode>} JSONOkParseResult
 * @typedef {ParseResult<DocumentNode>} JSONParseResult
 *
 * @typedef {Object} JSONLanguageOptions
 * @property {boolean} [allowTrailingCommas] Whether to allow trailing commas in JSONC mode.
 */

//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------

/**
 * JSON Language Object
 * @implements {Language<{ LangOptions: JSONLanguageOptions; Code: JSONSourceCode; RootNode: DocumentNode; Node: AnyNode }>}
 */
class JSONLanguage {
	/**
	 * The type of file to read.
	 * @type {"text"}
	 */
	fileType = "text";

	/**
	 * The line number at which the parser starts counting.
	 * @type {0|1}
	 */
	lineStart = 1;

	/**
	 * The column number at which the parser starts counting.
	 * @type {0|1}
	 */
	columnStart = 1;

	/**
	 * The name of the key that holds the type of the node.
	 * @type {string}
	 */
	nodeTypeKey = "type";

	/**
	 * The parser mode.
	 * @type {"json"|"jsonc"|"json5"}
	 */
	#mode = "json";

	/**
	 * The visitor keys.
	 * @type {Record<string, string[]>}
	 */
	visitorKeys = Object.fromEntries([...momoa.visitorKeys]);

	/**
	 * Creates a new instance.
	 * @param {Object} options The options to use for this instance.
	 * @param {"json"|"jsonc"|"json5"} options.mode The parser mode to use.
	 */
	constructor({ mode }) {
		this.#mode = mode;
	}

	/**
	 * Validates the language options.
	 * @param {JSONLanguageOptions} languageOptions The language options to validate.
	 * @returns {void}
	 * @throws {Error} When the language options are invalid.
	 */
	validateLanguageOptions(languageOptions) {
		if (languageOptions.allowTrailingCommas !== undefined) {
			if (typeof languageOptions.allowTrailingCommas !== "boolean") {
				throw new Error(
					"allowTrailingCommas must be a boolean if provided.",
				);
			}

			// we know that allowTrailingCommas is a boolean here

			// only allowed in JSONC mode
			if (this.#mode !== "jsonc") {
				throw new Error(
					"allowTrailingCommas option is only available in JSONC.",
				);
			}
		}
	}

	/**
	 * Parses the given file into an AST.
	 * @param {File} file The virtual file to parse.
	 * @param {{languageOptions: JSONLanguageOptions}} context The options to use for parsing.
	 * @returns {JSONParseResult} The result of parsing.
	 */
	parse(file, context) {
		// Note: BOM already removed
		const text = /** @type {string} */ (file.body);
		const allowTrailingCommas =
			context?.languageOptions?.allowTrailingCommas;

		/*
		 * Check for parsing errors first. If there's a parsing error, nothing
		 * else can happen. However, a parsing error does not throw an error
		 * from this method - it's just considered a fatal error message, a
		 * problem that ESLint identified just like any other.
		 */
		try {
			const root = momoa.parse(text, {
				mode: this.#mode,
				ranges: true,
				tokens: true,
				allowTrailingCommas,
			});

			return {
				ok: true,
				ast: root,
			};
		} catch (ex) {
			// error messages end with (line:column) so we strip that off for ESLint
			const message = ex.message
				.slice(0, ex.message.lastIndexOf("("))
				.trim();

			return {
				ok: false,
				errors: [
					{
						...ex,
						message,
					},
				],
			};
		}
	}

	/* eslint-disable class-methods-use-this -- Required to complete interface. */
	/**
	 * Creates a new `JSONSourceCode` object from the given information.
	 * @param {File} file The virtual file to create a `JSONSourceCode` object from.
	 * @param {JSONOkParseResult} parseResult The result returned from `parse()`.
	 * @returns {JSONSourceCode} The new `JSONSourceCode` object.
	 */
	createSourceCode(file, parseResult) {
		return new JSONSourceCode({
			text: /** @type {string} */ (file.body),
			ast: parseResult.ast,
		});
	}
	/* eslint-enable class-methods-use-this -- Required to complete interface. */
}

const rules$1 = /** @type {const} */ ({
    "json/no-duplicate-keys": "error",
    "json/no-empty-keys": "error",
    "json/no-unnormalized-keys": "error",
    "json/no-unsafe-values": "error"
});

/**
 * @fileoverview Rule to prevent duplicate keys in JSON.
 * @author Nicholas C. Zakas
 */

//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/**
 *
 * @typedef {"duplicateKey"} NoDuplicateKeysMessageIds
 * @typedef {JSONRuleDefinition<{ MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateKeysRuleDefinition
 */

//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------

/** @type {NoDuplicateKeysRuleDefinition} */
const rule$5 = {
	meta: {
		type: "problem",

		docs: {
			recommended: true,
			description: "Disallow duplicate keys in JSON objects",
			url: "https://github.com/eslint/json/tree/main/docs/rules/no-duplicate-keys.md",
		},

		messages: {
			duplicateKey: 'Duplicate key "{{key}}" found.',
		},
	},

	create(context) {
		/** @type {Array<Map<string, MemberNode>|undefined>} */
		const objectKeys = [];

		/** @type {Map<string, MemberNode>|undefined} */
		let keys;

		return {
			Object() {
				objectKeys.push(keys);
				keys = new Map();
			},

			Member(node) {
				const key =
					node.name.type === "String"
						? node.name.value
						: node.name.name;

				if (keys.has(key)) {
					context.report({
						loc: node.name.loc,
						messageId: "duplicateKey",
						data: {
							key,
						},
					});
				} else {
					keys.set(key, node);
				}
			},
			"Object:exit"() {
				keys = objectKeys.pop();
			},
		};
	},
};

/**
 * @fileoverview Rule to prevent empty keys in JSON.
 * @author Nicholas C. Zakas
 */

//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/**
 *
 * @typedef {"emptyKey"} NoEmptyKeysMessageIds
 * @typedef {JSONRuleDefinition<{ MessageIds: NoEmptyKeysMessageIds }>} NoEmptyKeysRuleDefinition
 */

//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------

/** @type {NoEmptyKeysRuleDefinition} */
const rule$4 = {
	meta: {
		type: "problem",

		docs: {
			recommended: true,
			description: "Disallow empty keys in JSON objects",
			url: "https://github.com/eslint/json/tree/main/docs/rules/no-empty-keys.md",
		},

		messages: {
			emptyKey: "Empty key found.",
		},
	},

	create(context) {
		return {
			Member(node) {
				const key =
					node.name.type === "String"
						? node.name.value
						: node.name.name;

				if (key.trim() === "") {
					context.report({
						loc: node.name.loc,
						messageId: "emptyKey",
					});
				}
			},
		};
	},
};

/**
 * @fileoverview Rule to detect unnormalized keys in JSON.
 * @author Bradley Meck Farias
 */

//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/**
 *
 * @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
 * @typedef {{ form: string }} NoUnnormalizedKeysOptions
 * @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
 */

//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------

/** @type {NoUnnormalizedKeysRuleDefinition} */
const rule$3 = {
	meta: {
		type: "problem",

		docs: {
			recommended: true,
			description: "Disallow JSON keys that are not normalized",
			url: "https://github.com/eslint/json/tree/main/docs/rules/no-unnormalized-keys.md",
		},

		messages: {
			unnormalizedKey: "Unnormalized key '{{key}}' found.",
		},

		schema: [
			{
				type: "object",
				properties: {
					form: {
						enum: ["NFC", "NFD", "NFKC", "NFKD"],
					},
				},
				additionalProperties: false,
			},
		],
	},

	create(context) {
		const form = context.options.length
			? context.options[0].form
			: undefined;

		return {
			Member(node) {
				const key =
					node.name.type === "String"
						? node.name.value
						: node.name.name;

				if (key.normalize(form) !== key) {
					context.report({
						loc: node.name.loc,
						messageId: "unnormalizedKey",
						data: {
							key,
						},
					});
				}
			},
		};
	},
};

/**
 * @fileoverview Rule to detect unsafe values in JSON.
 * @author Bradley Meck Farias
 */

//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/**
 *
 * @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
 *
 * 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$2 = {
	meta: {
		type: "problem",

		docs: {
			recommended: true,
			description: "Disallow JSON values that are unsafe for interchange",
			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);
						// assert(match, "If the regex is right, match is always truthy")

						// 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;
				let match = surrogatePattern.exec(node.value);
				while (match) {
					// 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,
								),
							},
						});
					}
					match = surrogatePattern.exec(node.value);
				}
			},
		};
	},
};

/**
 * @fileoverview Rule to require JSON object keys to be sorted.
 * Copied largely from https://github.com/eslint/eslint/blob/main/lib/rules/sort-keys.js
 * @author Robin Thomas
 */


//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/**
 *
 * @typedef {Object} SortOptions
 * @property {boolean} caseSensitive
 * @property {boolean} natural
 * @property {number} minKeys
 * @property {boolean} allowLineSeparatedGroups
 *
 * @typedef {"sortKeys"} SortKeysMessageIds
 * @typedef {"asc"|"desc"} SortDirection
 * @typedef {[SortDirection, SortOptions]} SortKeysRuleOptions
 * @typedef {JSONRuleDefinition<{ RuleOptions: SortKeysRuleOptions, MessageIds: SortKeysMessageIds }>} SortKeysRuleDefinition
 * @typedef {(a:string,b:string) => boolean} Comparator
 */

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

const hasNonWhitespace = /\S/u;

const comparators = {
	ascending: {
		alphanumeric: {
			/** @type {Comparator} */
			sensitive: (a, b) => a <= b,

			/** @type {Comparator} */
			insensitive: (a, b) => a.toLowerCase() <= b.toLowerCase(),
		},
		natural: {
			/** @type {Comparator} */
			sensitive: (a, b) => naturalCompare(a, b) <= 0,

			/** @type {Comparator} */
			insensitive: (a, b) =>
				naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0,
		},
	},
	descending: {
		alphanumeric: {
			/** @type {Comparator} */
			sensitive: (a, b) =>
				comparators.ascending.alphanumeric.sensitive(b, a),

			/** @type {Comparator} */
			insensitive: (a, b) =>
				comparators.ascending.alphanumeric.insensitive(b, a),
		},
		natural: {
			/** @type {Comparator} */
			sensitive: (a, b) => comparators.ascending.natural.sensitive(b, a),

			/** @type {Comparator} */
			insensitive: (a, b) =>
				comparators.ascending.natural.insensitive(b, a),
		},
	},
};

/**
 * Gets the MemberNode's string key value.
 * @param {MemberNode} member
 * @return {string}
 */
function getKey(member) {
	return member.name.type === "Identifier"
		? member.name.name
		: member.name.value;
}

//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------

/** @type {SortKeysRuleDefinition} */
const rule$1 = {
	meta: {
		type: "suggestion",

		defaultOptions: [
			"asc",
			{
				allowLineSeparatedGroups: false,
				caseSensitive: true,
				minKeys: 2,
				natural: false,
			},
		],

		docs: {
			recommended: false,
			description: `Require JSON object keys to be sorted`,
			url: "https://github.com/eslint/json/tree/main/docs/rules/sort-keys.md",
		},

		messages: {
			sortKeys:
				"Expected object keys to be in {{sortName}} case-{{sensitivity}} {{direction}} order. '{{thisName}}' should be before '{{prevName}}'.",
		},

		schema: [
			{
				enum: ["asc", "desc"],
			},
			{
				type: "object",
				properties: {
					caseSensitive: {
						type: "boolean",
					},
					natural: {
						type: "boolean",
					},
					minKeys: {
						type: "integer",
						minimum: 2,
					},
					allowLineSeparatedGroups: {
						type: "boolean",
					},
				},
				additionalProperties: false,
			},
		],
	},

	create(context) {
		const [
			directionShort,
			{ allowLineSeparatedGroups, caseSensitive, natural, minKeys },
		] = context.options;

		const direction = directionShort === "asc" ? "ascending" : "descending";
		const sortName = natural ? "natural" : "alphanumeric";
		const sensitivity = caseSensitive ? "sensitive" : "insensitive";
		const isValidOrder = comparators[direction][sortName][sensitivity];

		// Note that @humanwhocodes/momoa doesn't include comments in the object.members tree, so we can't just see if a member is preceded by a comment
		const commentLineNums = new Set();
		for (const comment of context.sourceCode.comments) {
			for (
				let lineNum = comment.loc.start.line;
				lineNum <= comment.loc.end.line;
				lineNum += 1
			) {
				commentLineNums.add(lineNum);
			}
		}

		/**
		 * Checks if two members are line-separated.
		 * @param {MemberNode} prevMember The previous member.
		 * @param {MemberNode} member The current member.
		 * @return {boolean}
		 */
		function isLineSeparated(prevMember, member) {
			// Note that there can be comments *inside* members, e.g. `{"foo: /* comment *\/ "bar"}`, but these are ignored when calculating line-separated groups
			const prevMemberEndLine = prevMember.loc.end.line;
			const thisStartLine = member.loc.start.line;
			if (thisStartLine - prevMemberEndLine < 2) {
				return false;
			}

			for (
				let lineNum = prevMemberEndLine + 1;
				lineNum < thisStartLine;
				lineNum += 1
			) {
				if (
					!commentLineNums.has(lineNum) &&
					!hasNonWhitespace.test(
						context.sourceCode.lines[lineNum - 1],
					)
				) {
					return true;
				}
			}

			return false;
		}

		return {
			Object(node) {
				let prevMember;
				let prevName;

				if (node.members.length < minKeys) {
					return;
				}

				for (const member of node.members) {
					const thisName = getKey(member);

					if (
						prevMember &&
						!isValidOrder(prevName, thisName) &&
						(!allowLineSeparatedGroups ||
							!isLineSeparated(prevMember, member))
					) {
						context.report({
							loc: member.name.loc,
							messageId: "sortKeys",
							data: {
								thisName,
								prevName,
								direction,
								sensitivity,
								sortName,
							},
						});
					}

					prevMember = member;
					prevName = thisName;
				}
			},
		};
	},
};

/**
 * @fileoverview Rule to ensure top-level items are either an array or object.
 * @author Joe Hildebrand
 */

//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/**
 *
 * @typedef {"topLevel"} TopLevelInteropMessageIds
 * @typedef {JSONRuleDefinition<{ MessageIds: TopLevelInteropMessageIds }>} TopLevelInteropRuleDefinition
 */

//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------

/** @type {TopLevelInteropRuleDefinition} */
const rule = {
	meta: {
		type: "problem",

		docs: {
			recommended: false,
			description:
				"Require the JSON top-level value to be an array or object",
			url: "https://github.com/eslint/json/tree/main/docs/rules/top-level-interop.md",
		},

		messages: {
			topLevel:
				"Top level item should be array or object, got '{{type}}'.",
		},
	},

	create(context) {
		return {
			Document(node) {
				const { type } = node.body;
				if (type !== "Object" && type !== "Array") {
					context.report({
						loc: node.loc,
						messageId: "topLevel",
						data: { type },
					});
				}
			},
		};
	},
};

var rules = {
    "no-duplicate-keys": rule$5,
    "no-empty-keys": rule$4,
    "no-unnormalized-keys": rule$3,
    "no-unsafe-values": rule$2,
    "sort-keys": rule$1,
    "top-level-interop": rule,
};

/**
 * @fileoverview JSON plugin.
 * @author Nicholas C. Zakas
 */


//-----------------------------------------------------------------------------
// Plugin
//-----------------------------------------------------------------------------

const plugin = {
	meta: {
		name: "@eslint/json",
		version: "0.13.2", // x-release-please-version
	},
	languages: {
		json: new JSONLanguage({ mode: "json" }),
		jsonc: new JSONLanguage({ mode: "jsonc" }),
		json5: new JSONLanguage({ mode: "json5" }),
	},
	rules,
	configs: {
		recommended: {
			plugins: {},
			rules: rules$1,
		},
	},
};

// eslint-disable-next-line no-lone-blocks -- The block syntax { ... } ensures that TypeScript does not get confused about the type of `plugin`.
{
	plugin.configs.recommended.plugins.json = plugin;
}

exports.JSONLanguage = JSONLanguage;
exports.JSONSourceCode = JSONSourceCode;
exports.default = plugin;
