UNPKG

@eslint/json

Version:

JSON linting plugin for ESLint

1,268 lines (1,087 loc) 32.4 kB
/** * @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;