eslint-plugin-jsonc
Version:
ESLint plugin for JSON, JSONC and JSON5 files.
489 lines (488 loc) • 21.7 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const eslint_ast_utils_1 = require("../utils/eslint-ast-utils");
const eslint_utils_1 = require("@eslint-community/eslint-utils");
const eslint_string_utils_1 = require("../utils/eslint-string-utils");
function containsLineTerminator(str) {
return eslint_ast_utils_1.LINEBREAK_MATCHER.test(str);
}
function last(arr) {
return arr[arr.length - 1];
}
function isSingleLine(node) {
return node.loc.end.line === node.loc.start.line;
}
function isSingleLineProperties(properties) {
const [firstProp] = properties;
const lastProp = last(properties);
return firstProp.loc.start.line === lastProp.loc.end.line;
}
function initOptionProperty(toOptions, fromOptions) {
toOptions.mode = fromOptions.mode || "strict";
if (typeof fromOptions.beforeColon !== "undefined")
toOptions.beforeColon = Number(fromOptions.beforeColon);
else
toOptions.beforeColon = 0;
if (typeof fromOptions.afterColon !== "undefined")
toOptions.afterColon = Number(fromOptions.afterColon);
else
toOptions.afterColon = 1;
if (typeof fromOptions.align !== "undefined") {
if (typeof fromOptions.align === "object") {
toOptions.align = fromOptions.align;
}
else {
toOptions.align = {
on: fromOptions.align,
mode: toOptions.mode,
beforeColon: toOptions.beforeColon,
afterColon: toOptions.afterColon,
};
}
}
return toOptions;
}
function initOptions(toOptions, fromOptions) {
if (typeof fromOptions.align === "object") {
toOptions.align = initOptionProperty({}, fromOptions.align);
toOptions.align.on = fromOptions.align.on || "colon";
toOptions.align.mode = fromOptions.align.mode || "strict";
toOptions.multiLine = initOptionProperty({}, fromOptions.multiLine || fromOptions);
toOptions.singleLine = initOptionProperty({}, fromOptions.singleLine || fromOptions);
}
else {
toOptions.multiLine = initOptionProperty({}, fromOptions.multiLine || fromOptions);
toOptions.singleLine = initOptionProperty({}, fromOptions.singleLine || fromOptions);
if (toOptions.multiLine.align) {
toOptions.align = {
on: toOptions.multiLine.align.on,
mode: toOptions.multiLine.align.mode || toOptions.multiLine.mode,
beforeColon: toOptions.multiLine.align.beforeColon,
afterColon: toOptions.multiLine.align.afterColon,
};
}
}
return toOptions;
}
exports.default = (0, utils_1.createRule)("key-spacing", {
meta: {
docs: {
description: "enforce consistent spacing between keys and values in object literal properties",
recommended: null,
extensionRule: true,
layout: true,
},
type: "layout",
fixable: "whitespace",
schema: [
{
anyOf: [
{
type: "object",
properties: {
align: {
anyOf: [
{
type: "string",
enum: ["colon", "value"],
},
{
type: "object",
properties: {
mode: {
type: "string",
enum: ["strict", "minimum"],
},
on: {
type: "string",
enum: ["colon", "value"],
},
beforeColon: {
type: "boolean",
},
afterColon: {
type: "boolean",
},
},
additionalProperties: false,
},
],
},
mode: {
type: "string",
enum: ["strict", "minimum"],
},
beforeColon: {
type: "boolean",
},
afterColon: {
type: "boolean",
},
},
additionalProperties: false,
},
{
type: "object",
properties: {
singleLine: {
type: "object",
properties: {
mode: {
type: "string",
enum: ["strict", "minimum"],
},
beforeColon: {
type: "boolean",
},
afterColon: {
type: "boolean",
},
},
additionalProperties: false,
},
multiLine: {
type: "object",
properties: {
align: {
anyOf: [
{
type: "string",
enum: ["colon", "value"],
},
{
type: "object",
properties: {
mode: {
type: "string",
enum: ["strict", "minimum"],
},
on: {
type: "string",
enum: ["colon", "value"],
},
beforeColon: {
type: "boolean",
},
afterColon: {
type: "boolean",
},
},
additionalProperties: false,
},
],
},
mode: {
type: "string",
enum: ["strict", "minimum"],
},
beforeColon: {
type: "boolean",
},
afterColon: {
type: "boolean",
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
{
type: "object",
properties: {
singleLine: {
type: "object",
properties: {
mode: {
type: "string",
enum: ["strict", "minimum"],
},
beforeColon: {
type: "boolean",
},
afterColon: {
type: "boolean",
},
},
additionalProperties: false,
},
multiLine: {
type: "object",
properties: {
mode: {
type: "string",
enum: ["strict", "minimum"],
},
beforeColon: {
type: "boolean",
},
afterColon: {
type: "boolean",
},
},
additionalProperties: false,
},
align: {
type: "object",
properties: {
mode: {
type: "string",
enum: ["strict", "minimum"],
},
on: {
type: "string",
enum: ["colon", "value"],
},
beforeColon: {
type: "boolean",
},
afterColon: {
type: "boolean",
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
],
},
],
messages: {
extraKey: "Extra space after {{computed}}key '{{key}}'.",
extraValue: "Extra space before value for {{computed}}key '{{key}}'.",
missingKey: "Missing space after {{computed}}key '{{key}}'.",
missingValue: "Missing space before value for {{computed}}key '{{key}}'.",
},
},
create(context) {
const sourceCode = context.sourceCode;
if (!sourceCode.parserServices.isJSON) {
return {};
}
const options = context.options[0] || {};
const ruleOptions = initOptions({}, options);
const multiLineOptions = ruleOptions.multiLine;
const singleLineOptions = ruleOptions.singleLine;
const alignmentOptions = ruleOptions.align || null;
function isKeyValueProperty(property) {
return !((("method" in property && property.method) ||
("shorthand" in property && property.shorthand) ||
("kind" in property && property.kind !== "init") ||
property.type !== "JSONProperty"));
}
function getNextColon(node) {
return sourceCode.getTokenAfter(node, eslint_utils_1.isColonToken);
}
function getLastTokenBeforeColon(node) {
const colonToken = getNextColon(node);
return sourceCode.getTokenBefore(colonToken);
}
function getFirstTokenAfterColon(node) {
const colonToken = getNextColon(node);
return sourceCode.getTokenAfter(colonToken);
}
function continuesPropertyGroup(lastMember, candidate) {
const groupEndLine = lastMember.loc.start.line;
const candidateValueStartLine = (isKeyValueProperty(candidate)
? getFirstTokenAfterColon(candidate.key)
: candidate).loc.start.line;
if (candidateValueStartLine - groupEndLine <= 1)
return true;
const leadingComments = sourceCode.getCommentsBefore(candidate);
if (leadingComments.length &&
leadingComments[0].loc.start.line - groupEndLine <= 1 &&
candidateValueStartLine - last(leadingComments).loc.end.line <= 1) {
for (let i = 1; i < leadingComments.length; i++) {
if (leadingComments[i].loc.start.line -
leadingComments[i - 1].loc.end.line >
1)
return false;
}
return true;
}
return false;
}
function getKey(property) {
const key = property.key;
if (property.computed)
return sourceCode.getText().slice(key.range[0], key.range[1]);
return (0, eslint_ast_utils_1.getStaticPropertyName)(property);
}
function report(property, side, whitespace, expected, mode) {
const diff = whitespace.length - expected;
if (((diff && mode === "strict") ||
(diff < 0 && mode === "minimum") ||
(diff > 0 && !expected && mode === "minimum")) &&
!(expected && containsLineTerminator(whitespace))) {
const nextColon = getNextColon(property.key);
const tokenBeforeColon = sourceCode.getTokenBefore(nextColon, {
includeComments: true,
});
const tokenAfterColon = sourceCode.getTokenAfter(nextColon, {
includeComments: true,
});
const isKeySide = side === "key";
const isExtra = diff > 0;
const diffAbs = Math.abs(diff);
const spaces = Array(diffAbs + 1).join(" ");
const locStart = isKeySide
? tokenBeforeColon.loc.end
: nextColon.loc.start;
const locEnd = isKeySide
? nextColon.loc.start
: tokenAfterColon.loc.start;
const missingLoc = isKeySide
? tokenBeforeColon.loc
: tokenAfterColon.loc;
const loc = isExtra ? { start: locStart, end: locEnd } : missingLoc;
let fix;
if (isExtra) {
let range;
if (isKeySide)
range = [
tokenBeforeColon.range[1],
tokenBeforeColon.range[1] + diffAbs,
];
else
range = [
tokenAfterColon.range[0] - diffAbs,
tokenAfterColon.range[0],
];
fix = function (fixer) {
return fixer.removeRange(range);
};
}
else {
if (isKeySide) {
fix = function (fixer) {
return fixer.insertTextAfter(tokenBeforeColon, spaces);
};
}
else {
fix = function (fixer) {
return fixer.insertTextBefore(tokenAfterColon, spaces);
};
}
}
let messageId;
if (isExtra)
messageId = side === "key" ? "extraKey" : "extraValue";
else
messageId = side === "key" ? "missingKey" : "missingValue";
context.report({
node: property[side],
loc,
messageId,
data: {
computed: property.computed ? "computed " : "",
key: getKey(property),
},
fix,
});
}
}
function getKeyWidth(property) {
const startToken = sourceCode.getFirstToken(property);
const endToken = getLastTokenBeforeColon(property.key);
return (0, eslint_string_utils_1.getGraphemeCount)(sourceCode.getText().slice(startToken.range[0], endToken.range[1]));
}
function getPropertyWhitespace(property) {
const whitespace = /(\s*):(\s*)/u.exec(sourceCode
.getText()
.slice(property.key.range[1], property.value.range[0]));
if (whitespace) {
return {
beforeColon: whitespace[1],
afterColon: whitespace[2],
};
}
return null;
}
function createGroups(node) {
if (node.properties.length === 1)
return [node.properties];
return node.properties.reduce((groups, property) => {
const currentGroup = last(groups);
const prev = last(currentGroup);
if (!prev || continuesPropertyGroup(prev, property))
currentGroup.push(property);
else
groups.push([property]);
return groups;
}, [[]]);
}
function verifyGroupAlignment(properties) {
const length = properties.length;
const widths = properties.map(getKeyWidth);
const align = alignmentOptions.on;
let targetWidth = Math.max(...widths);
let beforeColon;
let afterColon;
let mode;
if (alignmentOptions && length > 1) {
beforeColon = alignmentOptions.beforeColon;
afterColon = alignmentOptions.afterColon;
mode = alignmentOptions.mode;
}
else {
beforeColon = multiLineOptions.beforeColon;
afterColon = multiLineOptions.afterColon;
mode = alignmentOptions.mode;
}
targetWidth += align === "colon" ? beforeColon : afterColon;
for (let i = 0; i < length; i++) {
const property = properties[i];
const whitespace = getPropertyWhitespace(property);
if (whitespace) {
const width = widths[i];
if (align === "value") {
report(property, "key", whitespace.beforeColon, beforeColon, mode);
report(property, "value", whitespace.afterColon, targetWidth - width, mode);
}
else {
report(property, "key", whitespace.beforeColon, targetWidth - width, mode);
report(property, "value", whitespace.afterColon, afterColon, mode);
}
}
}
}
function verifySpacing(node, lineOptions) {
const actual = getPropertyWhitespace(node);
if (actual) {
report(node, "key", actual.beforeColon, lineOptions.beforeColon, lineOptions.mode);
report(node, "value", actual.afterColon, lineOptions.afterColon, lineOptions.mode);
}
}
function verifyListSpacing(properties, lineOptions) {
const length = properties.length;
for (let i = 0; i < length; i++)
verifySpacing(properties[i], lineOptions);
}
function verifyAlignment(node) {
createGroups(node).forEach((group) => {
const properties = group.filter(isKeyValueProperty);
if (properties.length > 0 && isSingleLineProperties(properties))
verifyListSpacing(properties, multiLineOptions);
else
verifyGroupAlignment(properties);
});
}
if (alignmentOptions) {
return {
JSONObjectExpression(node) {
if (isSingleLine(node))
verifyListSpacing(node.properties.filter(isKeyValueProperty), singleLineOptions);
else
verifyAlignment(node);
},
};
}
return {
JSONProperty(node) {
verifySpacing(node, isSingleLine(node.parent) ? singleLineOptions : multiLineOptions);
},
};
},
});
;