397 lines
14 KiB
JavaScript
397 lines
14 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const utils_1 = require("../utils");
|
|
const ast_utils_1 = require("../utils/ast-utils");
|
|
const compat_1 = require("../utils/compat");
|
|
function containsLineTerminator(str) {
|
|
return /[\n\r\u2028\u2029]/u.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(fromOptions) {
|
|
const mode = fromOptions.mode || "strict";
|
|
let beforeEqual, afterEqual;
|
|
if (typeof fromOptions.beforeEqual !== "undefined") {
|
|
beforeEqual = fromOptions.beforeEqual;
|
|
}
|
|
else {
|
|
beforeEqual = true;
|
|
}
|
|
if (typeof fromOptions.afterEqual !== "undefined") {
|
|
afterEqual = fromOptions.afterEqual;
|
|
}
|
|
else {
|
|
afterEqual = true;
|
|
}
|
|
let align = undefined;
|
|
if (typeof fromOptions.align !== "undefined") {
|
|
if (typeof fromOptions.align === "object") {
|
|
align = fromOptions.align;
|
|
}
|
|
else {
|
|
align = {
|
|
on: fromOptions.align,
|
|
mode,
|
|
beforeEqual,
|
|
afterEqual,
|
|
};
|
|
}
|
|
}
|
|
return {
|
|
mode,
|
|
beforeEqual,
|
|
afterEqual,
|
|
align,
|
|
};
|
|
}
|
|
function initOptions(fromOptions) {
|
|
let align, multiLine, singleLine;
|
|
if (typeof fromOptions.align === "object") {
|
|
align = {
|
|
...initOptionProperty(fromOptions.align),
|
|
on: fromOptions.align.on || "equal",
|
|
mode: fromOptions.align.mode || "strict",
|
|
};
|
|
multiLine = initOptionProperty(fromOptions.multiLine || fromOptions);
|
|
singleLine = initOptionProperty(fromOptions.singleLine || fromOptions);
|
|
}
|
|
else {
|
|
multiLine = initOptionProperty(fromOptions.multiLine || fromOptions);
|
|
singleLine = initOptionProperty(fromOptions.singleLine || fromOptions);
|
|
if (multiLine.align) {
|
|
align = {
|
|
on: multiLine.align.on,
|
|
mode: multiLine.align.mode || multiLine.mode,
|
|
beforeEqual: multiLine.align.beforeEqual,
|
|
afterEqual: multiLine.align.afterEqual,
|
|
};
|
|
}
|
|
}
|
|
return {
|
|
align,
|
|
multiLine,
|
|
singleLine,
|
|
};
|
|
}
|
|
const ON_SCHEMA = {
|
|
enum: ["equal", "value"],
|
|
};
|
|
const OBJECT_WITHOUT_ON_SCHEMA = {
|
|
type: "object",
|
|
properties: {
|
|
mode: {
|
|
enum: ["strict", "minimum"],
|
|
},
|
|
beforeEqual: {
|
|
type: "boolean",
|
|
},
|
|
afterEqual: {
|
|
type: "boolean",
|
|
},
|
|
},
|
|
additionalProperties: false,
|
|
};
|
|
const ALIGN_OBJECT_SCHEMA = {
|
|
type: "object",
|
|
properties: {
|
|
on: ON_SCHEMA,
|
|
...OBJECT_WITHOUT_ON_SCHEMA.properties,
|
|
},
|
|
additionalProperties: false,
|
|
};
|
|
exports.default = (0, utils_1.createRule)("key-spacing", {
|
|
meta: {
|
|
docs: {
|
|
description: "enforce consistent spacing between keys and values in key/value pairs",
|
|
categories: ["standard"],
|
|
extensionRule: "key-spacing",
|
|
},
|
|
fixable: "whitespace",
|
|
schema: [
|
|
{
|
|
anyOf: [
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
align: {
|
|
anyOf: [ON_SCHEMA, ALIGN_OBJECT_SCHEMA],
|
|
},
|
|
...OBJECT_WITHOUT_ON_SCHEMA.properties,
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
singleLine: OBJECT_WITHOUT_ON_SCHEMA,
|
|
multiLine: {
|
|
type: "object",
|
|
properties: {
|
|
align: {
|
|
anyOf: [ON_SCHEMA, ALIGN_OBJECT_SCHEMA],
|
|
},
|
|
...OBJECT_WITHOUT_ON_SCHEMA.properties,
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
singleLine: OBJECT_WITHOUT_ON_SCHEMA,
|
|
multiLine: OBJECT_WITHOUT_ON_SCHEMA,
|
|
align: ALIGN_OBJECT_SCHEMA,
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
messages: {
|
|
extraKey: "Extra space after key '{{key}}'.",
|
|
extraValue: "Extra space before value for key '{{key}}'.",
|
|
missingKey: "Missing space after key '{{key}}'.",
|
|
missingValue: "Missing space before value for key '{{key}}'.",
|
|
},
|
|
type: "layout",
|
|
},
|
|
create,
|
|
});
|
|
function create(context) {
|
|
const sourceCode = (0, compat_1.getSourceCode)(context);
|
|
if (!sourceCode.parserServices?.isTOML) {
|
|
return {};
|
|
}
|
|
const options = context.options[0] || {};
|
|
const { multiLine: multiLineOptions, singleLine: singleLineOptions, align: alignmentOptions, } = initOptions(options);
|
|
function isKeyValueProperty(property) {
|
|
return property.type === "TOMLKeyValue";
|
|
}
|
|
function getLastTokenBeforeEqual(node) {
|
|
const equalToken = sourceCode.getTokenAfter(node, ast_utils_1.isEqualSign);
|
|
return sourceCode.getTokenBefore(equalToken);
|
|
}
|
|
function getNextEqual(node) {
|
|
return sourceCode.getTokenAfter(node, ast_utils_1.isEqualSign);
|
|
}
|
|
function getKey(property) {
|
|
const keys = property.key.keys;
|
|
return keys
|
|
.map((key) => sourceCode.getText().slice(key.range[0], key.range[1]))
|
|
.join(".");
|
|
}
|
|
function report(property, side, whitespace, expected, mode) {
|
|
const diff = whitespace.length - expected;
|
|
const nextEqual = getNextEqual(property.key);
|
|
const tokenBeforeEqual = sourceCode.getTokenBefore(nextEqual, {
|
|
includeComments: true,
|
|
});
|
|
const tokenAfterEqual = sourceCode.getTokenAfter(nextEqual, {
|
|
includeComments: true,
|
|
});
|
|
const invalid = (mode === "strict"
|
|
? diff !== 0
|
|
:
|
|
diff < 0 || (diff > 0 && expected === 0)) &&
|
|
!(expected && containsLineTerminator(whitespace));
|
|
if (!invalid) {
|
|
return;
|
|
}
|
|
const { locStart, locEnd, missingLoc } = side === "key"
|
|
? {
|
|
locStart: tokenBeforeEqual.loc.end,
|
|
locEnd: nextEqual.loc.start,
|
|
missingLoc: tokenBeforeEqual.loc,
|
|
}
|
|
: {
|
|
locStart: nextEqual.loc.start,
|
|
locEnd: tokenAfterEqual.loc.start,
|
|
missingLoc: tokenAfterEqual.loc,
|
|
};
|
|
const { loc, messageId } = diff > 0
|
|
? {
|
|
loc: { start: locStart, end: locEnd },
|
|
messageId: side === "key" ? "extraKey" : "extraValue",
|
|
}
|
|
: {
|
|
loc: missingLoc,
|
|
messageId: side === "key" ? "missingKey" : "missingValue",
|
|
};
|
|
context.report({
|
|
node: property[side],
|
|
loc,
|
|
messageId,
|
|
data: {
|
|
key: getKey(property),
|
|
},
|
|
fix(fixer) {
|
|
if (diff > 0) {
|
|
if (side === "key") {
|
|
return fixer.removeRange([
|
|
tokenBeforeEqual.range[1],
|
|
tokenBeforeEqual.range[1] + diff,
|
|
]);
|
|
}
|
|
return fixer.removeRange([
|
|
tokenAfterEqual.range[0] - diff,
|
|
tokenAfterEqual.range[0],
|
|
]);
|
|
}
|
|
const spaces = " ".repeat(-diff);
|
|
if (side === "key") {
|
|
return fixer.insertTextAfter(tokenBeforeEqual, spaces);
|
|
}
|
|
return fixer.insertTextBefore(tokenAfterEqual, spaces);
|
|
},
|
|
});
|
|
}
|
|
function getKeyWidth(pair) {
|
|
const startToken = sourceCode.getFirstToken(pair);
|
|
const endToken = getLastTokenBeforeEqual(pair.key);
|
|
return endToken.range[1] - startToken.range[0];
|
|
}
|
|
function getPropertyWhitespace(pair) {
|
|
const whitespace = /(\s*)=(\s*)/u.exec(sourceCode.getText().slice(pair.key.range[1], pair.value.range[0]));
|
|
if (whitespace) {
|
|
return {
|
|
beforeEqual: whitespace[1],
|
|
afterEqual: whitespace[2],
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
function verifySpacing(node, lineOptions) {
|
|
const actual = getPropertyWhitespace(node);
|
|
if (actual) {
|
|
report(node, "key", actual.beforeEqual, lineOptions.beforeEqual ? 1 : 0, lineOptions.mode);
|
|
report(node, "value", actual.afterEqual, lineOptions.afterEqual ? 1 : 0, lineOptions.mode);
|
|
}
|
|
}
|
|
function verifyListSpacing(properties, lineOptions) {
|
|
const length = properties.length;
|
|
for (let i = 0; i < length; i++) {
|
|
verifySpacing(properties[i], lineOptions);
|
|
}
|
|
}
|
|
if (alignmentOptions) {
|
|
return defineAlignmentVisitor(alignmentOptions);
|
|
}
|
|
return defineSpacingVisitor();
|
|
function defineAlignmentVisitor(alignmentOptions) {
|
|
return {
|
|
"TOMLTopLevelTable, TOMLTable, TOMLInlineTable"(node) {
|
|
if (isSingleLine(node)) {
|
|
const body = node.body;
|
|
verifyListSpacing(body.filter(isKeyValueProperty), singleLineOptions);
|
|
}
|
|
else {
|
|
verifyAlignment(node);
|
|
}
|
|
},
|
|
};
|
|
function verifyGroupAlignment(properties) {
|
|
const length = properties.length;
|
|
const widths = properties.map(getKeyWidth);
|
|
const align = alignmentOptions.on;
|
|
let targetWidth = Math.max(...widths);
|
|
let beforeEqual, afterEqual, mode;
|
|
if (alignmentOptions && length > 1) {
|
|
beforeEqual = alignmentOptions.beforeEqual ? 1 : 0;
|
|
afterEqual = alignmentOptions.afterEqual ? 1 : 0;
|
|
mode = alignmentOptions.mode;
|
|
}
|
|
else {
|
|
beforeEqual = multiLineOptions.beforeEqual ? 1 : 0;
|
|
afterEqual = multiLineOptions.afterEqual ? 1 : 0;
|
|
mode = alignmentOptions.mode;
|
|
}
|
|
targetWidth += align === "equal" ? beforeEqual : afterEqual;
|
|
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.beforeEqual, beforeEqual, mode);
|
|
report(property, "value", whitespace.afterEqual, targetWidth - width, mode);
|
|
}
|
|
else {
|
|
report(property, "key", whitespace.beforeEqual, targetWidth - width, mode);
|
|
report(property, "value", whitespace.afterEqual, afterEqual, mode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function continuesPropertyGroup(lastMember, candidate) {
|
|
const groupEndLine = lastMember.loc.start.line;
|
|
const candidateStartLine = candidate.loc.start.line;
|
|
if (candidateStartLine - groupEndLine <= 1) {
|
|
return true;
|
|
}
|
|
const leadingComments = sourceCode.getCommentsBefore(candidate);
|
|
if (leadingComments.length &&
|
|
leadingComments[0].loc.start.line - groupEndLine <= 1 &&
|
|
candidateStartLine - 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 createGroups(node) {
|
|
const body = node.body;
|
|
const pairs = body.filter(isKeyValueProperty);
|
|
if (pairs.length === 1) {
|
|
return [pairs];
|
|
}
|
|
return pairs.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 verifyAlignment(node) {
|
|
createGroups(node).forEach((group) => {
|
|
const properties = group;
|
|
if (properties.length > 0 && isSingleLineProperties(properties)) {
|
|
verifyListSpacing(properties, multiLineOptions);
|
|
}
|
|
else {
|
|
verifyGroupAlignment(properties);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
function defineSpacingVisitor() {
|
|
return {
|
|
TOMLKeyValue(node) {
|
|
if (!isKeyValueProperty(node))
|
|
return;
|
|
verifySpacing(node, isSingleLine(node.parent) ? singleLineOptions : multiLineOptions);
|
|
},
|
|
};
|
|
}
|
|
}
|