UNPKG

eslint-plugin-jsonc

Version:

ESLint plugin for JSON, JSONC and JSON5 files.

389 lines (388 loc) 14.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const natural_compare_1 = __importDefault(require("natural-compare")); const utils_1 = require("../utils"); const jsonc_eslint_parser_1 = require("jsonc-eslint-parser"); const fix_sort_elements_1 = require("../utils/fix-sort-elements"); function getPropertyName(node) { const prop = node.key; if (prop.type === "JSONIdentifier") { return prop.name; } return String((0, jsonc_eslint_parser_1.getStaticJSONValue)(prop)); } class JSONPropertyData { get reportLoc() { return this.node.key.loc; } constructor(object, node, index) { this.cachedName = null; this.object = object; this.node = node; this.index = index; } get name() { var _a; return ((_a = this.cachedName) !== null && _a !== void 0 ? _a : (this.cachedName = getPropertyName(this.node))); } getPrev() { const prevIndex = this.index - 1; return prevIndex >= 0 ? this.object.properties[prevIndex] : null; } } class JSONObjectData { constructor(node) { this.cachedProperties = null; this.node = node; } get properties() { var _a; return ((_a = this.cachedProperties) !== null && _a !== void 0 ? _a : (this.cachedProperties = this.node.properties.map((e, index) => new JSONPropertyData(this, e, index)))); } getPath() { let path = ""; let curr = this.node; let p = curr.parent; while (p) { if (p.type === "JSONProperty") { const name = getPropertyName(p); if (/^[$a-z_][\w$]*$/iu.test(name)) { path = `.${name}${path}`; } else { path = `[${JSON.stringify(name)}]${path}`; } curr = p.parent; } else if (p.type === "JSONArrayExpression") { const index = p.elements.indexOf(curr); path = `[${index}]${path}`; curr = p; } else if (p.type === "JSONExpressionStatement") { break; } else { curr = p; } p = curr.parent; } if (path.startsWith(".")) { path = path.slice(1); } return path; } } function isCompatibleWithESLintOptions(options) { if (options.length === 0) { return true; } if (typeof options[0] === "string" || options[0] == null) { return true; } return false; } function buildValidatorFromType(order, insensitive, natural) { let compare = natural ? ([a, b]) => (0, natural_compare_1.default)(a, b) <= 0 : ([a, b]) => a <= b; if (insensitive) { const baseCompare = compare; compare = ([a, b]) => baseCompare([a.toLowerCase(), b.toLowerCase()]); } if (order === "desc") { const baseCompare = compare; compare = (args) => baseCompare(args.reverse()); } return (a, b) => compare([a.name, b.name]); } function parseOptions(options) { var _a, _b, _c; if (isCompatibleWithESLintOptions(options)) { const type = (_a = options[0]) !== null && _a !== void 0 ? _a : "asc"; const obj = (_b = options[1]) !== null && _b !== void 0 ? _b : {}; const insensitive = obj.caseSensitive === false; const natural = Boolean(obj.natural); const minKeys = (_c = obj.minKeys) !== null && _c !== void 0 ? _c : 2; const allowLineSeparatedGroups = obj.allowLineSeparatedGroups || false; return [ { isTargetObject: (node) => node.properties.length >= minKeys, ignore: () => false, isValidOrder: buildValidatorFromType(type, insensitive, natural), orderText: `${natural ? "natural " : ""}${insensitive ? "insensitive " : ""}${type}ending`, allowLineSeparatedGroups, }, ]; } return options.map((opt) => { var _a, _b, _c, _d, _e; const order = opt.order; const pathPattern = new RegExp(opt.pathPattern); const hasProperties = (_a = opt.hasProperties) !== null && _a !== void 0 ? _a : []; const minKeys = (_b = opt.minKeys) !== null && _b !== void 0 ? _b : 2; const allowLineSeparatedGroups = opt.allowLineSeparatedGroups || false; if (!Array.isArray(order)) { const type = (_c = order.type) !== null && _c !== void 0 ? _c : "asc"; const insensitive = order.caseSensitive === false; const natural = Boolean(order.natural); return { isTargetObject, ignore: () => false, isValidOrder: buildValidatorFromType(type, insensitive, natural), orderText: `${natural ? "natural " : ""}${insensitive ? "insensitive " : ""}${type}ending`, allowLineSeparatedGroups, }; } const parsedOrder = []; for (const o of order) { if (typeof o === "string") { parsedOrder.push({ test: (data) => data.name === o, isValidNestOrder: () => true, }); } else { const keyPattern = o.keyPattern ? new RegExp(o.keyPattern) : null; const nestOrder = (_d = o.order) !== null && _d !== void 0 ? _d : {}; const type = (_e = nestOrder.type) !== null && _e !== void 0 ? _e : "asc"; const insensitive = nestOrder.caseSensitive === false; const natural = Boolean(nestOrder.natural); parsedOrder.push({ test: (data) => (keyPattern ? keyPattern.test(data.name) : true), isValidNestOrder: buildValidatorFromType(type, insensitive, natural), }); } } return { isTargetObject, ignore: (data) => parsedOrder.every((p) => !p.test(data)), isValidOrder(a, b) { for (const p of parsedOrder) { const matchA = p.test(a); const matchB = p.test(b); if (!matchA || !matchB) { if (matchA) { return true; } if (matchB) { return false; } continue; } return p.isValidNestOrder(a, b); } return false; }, orderText: "specified", allowLineSeparatedGroups, }; function isTargetObject(data) { if (data.node.properties.length < minKeys) { return false; } if (hasProperties.length > 0) { const names = new Set(data.properties.map((p) => p.name)); if (!hasProperties.every((name) => names.has(name))) { return false; } } return pathPattern.test(data.getPath()); } }); } const ALLOW_ORDER_TYPES = ["asc", "desc"]; const ORDER_OBJECT_SCHEMA = { type: "object", properties: { type: { enum: ALLOW_ORDER_TYPES, }, caseSensitive: { type: "boolean", }, natural: { type: "boolean", }, }, additionalProperties: false, }; exports.default = (0, utils_1.createRule)("sort-keys", { meta: { docs: { description: "require object keys to be sorted", recommended: null, extensionRule: false, layout: false, }, fixable: "code", schema: { oneOf: [ { type: "array", items: { type: "object", properties: { pathPattern: { type: "string" }, hasProperties: { type: "array", items: { type: "string" }, }, order: { oneOf: [ { type: "array", items: { anyOf: [ { type: "string" }, { type: "object", properties: { keyPattern: { type: "string", }, order: ORDER_OBJECT_SCHEMA, }, additionalProperties: false, }, ], }, uniqueItems: true, }, ORDER_OBJECT_SCHEMA, ], }, minKeys: { type: "integer", minimum: 2, }, allowLineSeparatedGroups: { type: "boolean", }, }, required: ["pathPattern", "order"], additionalProperties: false, }, minItems: 1, }, { type: "array", items: [ { enum: ALLOW_ORDER_TYPES, }, { type: "object", properties: { caseSensitive: { type: "boolean", }, natural: { type: "boolean", }, minKeys: { type: "integer", minimum: 2, }, allowLineSeparatedGroups: { type: "boolean", }, }, additionalProperties: false, }, ], additionalItems: false, }, ], }, messages: { sortKeys: "Expected object keys to be in {{orderText}} order. '{{thisName}}' should be before '{{prevName}}'.", }, type: "suggestion", }, create(context) { const sourceCode = context.sourceCode; if (!sourceCode.parserServices.isJSON) { return {}; } const parsedOptions = parseOptions(context.options); function verifyProperty(data, option) { if (option.ignore(data)) { return; } const prevList = []; let currTarget = data; let prevTarget; while ((prevTarget = currTarget.getPrev())) { if (option.allowLineSeparatedGroups) { if (hasBlankLine(prevTarget, currTarget)) { break; } } if (!option.ignore(prevTarget)) { prevList.push(prevTarget); } currTarget = prevTarget; } if (prevList.length === 0) { return; } const prev = prevList[0]; if (!option.isValidOrder(prev, data)) { context.report({ loc: data.reportLoc, messageId: "sortKeys", data: { thisName: data.name, prevName: prev.name, orderText: option.orderText, }, fix(fixer) { let moveTarget = prevList[0]; for (const prev of prevList) { if (option.isValidOrder(prev, data)) { break; } else { moveTarget = prev; } } return (0, fix_sort_elements_1.fixForSorting)(fixer, sourceCode, data, moveTarget); }, }); } } function hasBlankLine(prev, next) { const tokenOrNodes = [ ...sourceCode.getTokensBetween(prev.node, next.node, { includeComments: true, }), next.node, ]; let prevLoc = prev.node.loc; for (const t of tokenOrNodes) { const loc = t.loc; if (loc.start.line - prevLoc.end.line > 1) { return true; } prevLoc = loc; } return false; } return { JSONObjectExpression(node) { const data = new JSONObjectData(node); const option = parsedOptions.find((o) => o.isTargetObject(data)); if (!option) { return; } for (const prop of data.properties) { verifyProperty(prop, option); } }, }; }, });