eslint-plugin-yml
Version:
This ESLint plugin provides linting rules for YAML.
598 lines (597 loc) • 23.8 kB
JavaScript
;
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 index_1 = require("../utils/index");
const ast_utils_1 = require("../utils/ast-utils");
const compat_1 = require("../utils/compat");
function isNewLine(char) {
return (char === "\n" || char === "\r" || char === "\u2028" || char === "\u2029");
}
function getPropertyName(node, sourceCode) {
const prop = node.key;
if (prop == null) {
return "";
}
const target = prop.type === "YAMLWithMeta" ? prop.value : prop;
if (target == null) {
return "";
}
if (target.type === "YAMLScalar" && typeof target.value === "string") {
return target.value;
}
return sourceCode.text.slice(...target.range);
}
class YAMLPairData {
get reportLoc() {
var _a, _b;
return (_b = (_a = this.node.key) === null || _a === void 0 ? void 0 : _a.loc) !== null && _b !== void 0 ? _b : this.node.loc;
}
constructor(mapping, node, index, anchorAlias) {
this.cachedName = null;
this.mapping = mapping;
this.node = node;
this.index = index;
this.anchorAlias = anchorAlias;
}
get name() {
var _a;
return ((_a = this.cachedName) !== null && _a !== void 0 ? _a : (this.cachedName = getPropertyName(this.node, this.mapping.sourceCode)));
}
getPrev() {
const prevIndex = this.index - 1;
return prevIndex >= 0 ? this.mapping.pairs[prevIndex] : null;
}
}
class YAMLMappingData {
constructor(node, sourceCode, anchorAliasMap) {
this.cachedProperties = null;
this.node = node;
this.sourceCode = sourceCode;
this.anchorAliasMap = anchorAliasMap;
}
get pairs() {
var _a;
return ((_a = this.cachedProperties) !== null && _a !== void 0 ? _a : (this.cachedProperties = this.node.pairs.map((e, index) => new YAMLPairData(this, e, index, this.anchorAliasMap.get(e)))));
}
getPath(sourceCode) {
let path = "";
let curr = this.node;
let p = curr.parent;
while (p) {
if (p.type === "YAMLPair") {
const name = getPropertyName(p, sourceCode);
if (/^[$a-z_][\w$]*$/iu.test(name)) {
path = `.${name}${path}`;
}
else {
path = `[${JSON.stringify(name)}]${path}`;
}
}
else if (p.type === "YAMLSequence") {
const index = p.entries.indexOf(curr);
path = `[${index}]${path}`;
}
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, sourceCode) {
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 [
{
isTargetMapping: (data) => data.node.pairs.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 {
isTargetMapping,
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 {
isTargetMapping,
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 isTargetMapping(data) {
if (data.node.pairs.length < minKeys) {
return false;
}
if (hasProperties.length > 0) {
const names = new Set(data.pairs.map((p) => p.name));
if (!hasProperties.every((name) => names.has(name))) {
return false;
}
}
return pathPattern.test(data.getPath(sourceCode));
}
});
}
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, index_1.createRule)("sort-keys", {
meta: {
docs: {
description: "require mapping keys to be sorted",
categories: 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 mapping keys to be in {{orderText}} order. '{{thisName}}' should be before '{{prevName}}'.",
},
type: "suggestion",
},
create(context) {
var _a;
const sourceCode = (0, compat_1.getSourceCode)(context);
if (!((_a = sourceCode.parserServices) === null || _a === void 0 ? void 0 : _a.isYAML)) {
return {};
}
const parsedOptions = parseOptions(context.options, sourceCode);
function isValidOrder(prevData, thisData, option) {
if (option.isValidOrder(prevData, thisData)) {
return true;
}
for (const aliasName of thisData.anchorAlias.aliases) {
if (prevData.anchorAlias.anchors.has(aliasName)) {
return true;
}
}
for (const anchorName of thisData.anchorAlias.anchors) {
if (prevData.anchorAlias.aliases.has(anchorName)) {
return true;
}
}
return false;
}
function ignore(data, option) {
if (!data.node.key && !data.node.value) {
return true;
}
return option.ignore(data);
}
function verifyPair(data, option) {
if (ignore(data, option)) {
return;
}
const prevList = [];
let currTarget = data;
let prevTarget;
while ((prevTarget = currTarget.getPrev())) {
if (option.allowLineSeparatedGroups) {
if (hasBlankLine(prevTarget, currTarget)) {
break;
}
}
if (!ignore(prevTarget, option)) {
prevList.push(prevTarget);
}
currTarget = prevTarget;
}
if (prevList.length === 0) {
return;
}
const prev = prevList[0];
if (!isValidOrder(prev, data, option)) {
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 (isValidOrder(prev, data, option)) {
break;
}
else {
moveTarget = prev;
}
}
if (data.mapping.node.style === "flow") {
yield* fixForFlow(fixer, data, moveTarget);
}
else {
yield* fixForBlock(fixer, 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;
}
let pairStack = {
upper: null,
anchors: new Set(),
aliases: new Set(),
};
const anchorAliasMap = new Map();
return {
YAMLPair() {
pairStack = {
upper: pairStack,
anchors: new Set(),
aliases: new Set(),
};
},
YAMLAnchor(node) {
if (pairStack) {
pairStack.anchors.add(node.name);
}
},
YAMLAlias(node) {
if (pairStack) {
pairStack.aliases.add(node.name);
}
},
"YAMLPair:exit"(node) {
anchorAliasMap.set(node, pairStack);
const { anchors, aliases } = pairStack;
pairStack = pairStack.upper;
pairStack.anchors = new Set([...pairStack.anchors, ...anchors]);
pairStack.aliases = new Set([...pairStack.aliases, ...aliases]);
},
"YAMLMapping:exit"(node) {
const data = new YAMLMappingData(node, sourceCode, anchorAliasMap);
const option = parsedOptions.find((o) => o.isTargetMapping(data));
if (!option) {
return;
}
for (const pair of data.pairs) {
verifyPair(pair, option);
}
},
};
function* fixForFlow(fixer, data, moveTarget) {
const beforeCommaToken = sourceCode.getTokenBefore(data.node);
let insertCode, removeRange, insertTargetToken;
const afterCommaToken = sourceCode.getTokenAfter(data.node);
const moveTargetBeforeToken = sourceCode.getTokenBefore(moveTarget.node);
if ((0, ast_utils_1.isComma)(afterCommaToken)) {
removeRange = [beforeCommaToken.range[1], afterCommaToken.range[1]];
insertCode = sourceCode.text.slice(...removeRange);
insertTargetToken = moveTargetBeforeToken;
}
else {
removeRange = [beforeCommaToken.range[0], data.node.range[1]];
if ((0, ast_utils_1.isComma)(moveTargetBeforeToken)) {
insertCode = sourceCode.text.slice(...removeRange);
insertTargetToken = sourceCode.getTokenBefore(moveTargetBeforeToken);
}
else {
insertCode = `${sourceCode.text.slice(beforeCommaToken.range[1], data.node.range[1])},`;
insertTargetToken = moveTargetBeforeToken;
}
}
yield fixer.insertTextAfterRange(insertTargetToken.range, insertCode);
yield fixer.removeRange(removeRange);
}
function* fixForBlock(fixer, data, moveTarget) {
const nodeLocs = getPairRangeForBlock(data.node);
const moveTargetLocs = getPairRangeForBlock(moveTarget.node);
if (moveTargetLocs.loc.start.column === 0) {
const removeRange = [
getNewlineStartIndex(nodeLocs.range[0]),
nodeLocs.range[1],
];
const moveTargetRange = [
getNewlineStartIndex(moveTargetLocs.range[0]),
moveTargetLocs.range[1],
];
const insertCode = sourceCode.text.slice(...removeRange);
yield fixer.insertTextBeforeRange(moveTargetRange, `${insertCode}${moveTargetLocs.loc.start.line === 1 ? "\n" : ""}`);
yield fixer.removeRange(removeRange);
}
else {
const diffIndent = nodeLocs.indentColumn - moveTargetLocs.indentColumn;
const insertCode = `${sourceCode.text.slice(nodeLocs.range[0] + diffIndent, nodeLocs.range[1])}\n${sourceCode.text.slice(nodeLocs.range[0], nodeLocs.range[0] + diffIndent)}`;
yield fixer.insertTextBeforeRange(moveTargetLocs.range, insertCode);
const removeRange = [
getNewlineStartIndex(nodeLocs.range[0]),
nodeLocs.range[1],
];
yield fixer.removeRange(removeRange);
}
}
function getNewlineStartIndex(nextIndex) {
for (let index = nextIndex; index >= 0; index--) {
const char = sourceCode.text[index];
if (isNewLine(sourceCode.text[index])) {
const prev = sourceCode.text[index - 1];
if (prev === "\r" && char === "\n") {
return index - 1;
}
return index;
}
}
return 0;
}
function getPairRangeForBlock(node) {
let endOfRange, end;
const afterToken = sourceCode.getTokenAfter(node, {
includeComments: true,
filter: (t) => !(0, ast_utils_1.isCommentToken)(t) || node.loc.end.line < t.loc.start.line,
});
if (!afterToken || node.loc.end.line < afterToken.loc.start.line) {
const line = afterToken
? afterToken.loc.start.line - 1
: node.loc.end.line;
const lineText = sourceCode.lines[line - 1];
end = {
line,
column: lineText.length,
};
endOfRange = sourceCode.getIndexFromLoc(end);
}
else {
endOfRange = node.range[1];
end = node.loc.end;
}
const beforeToken = sourceCode.getTokenBefore(node);
if (beforeToken) {
const next = sourceCode.getTokenAfter(beforeToken, {
includeComments: true,
});
if (beforeToken.loc.end.line < next.loc.start.line ||
beforeToken.loc.end.line < node.loc.start.line) {
const start = {
line: beforeToken.loc.end.line < next.loc.start.line
? next.loc.start.line
: node.loc.start.line,
column: 0,
};
const startOfRange = sourceCode.getIndexFromLoc(start);
return {
range: [startOfRange, endOfRange],
loc: { start, end },
indentColumn: next.loc.start.column,
};
}
const start = beforeToken.loc.end;
const startOfRange = beforeToken.range[1];
return {
range: [startOfRange, endOfRange],
loc: { start, end },
indentColumn: node.range[0] - beforeToken.range[1],
};
}
let next = node;
for (const beforeComment of sourceCode
.getTokensBefore(node, {
includeComments: true,
})
.reverse()) {
if (beforeComment.loc.end.line + 1 < next.loc.start.line) {
const start = {
line: next.loc.start.line,
column: 0,
};
const startOfRange = sourceCode.getIndexFromLoc(start);
return {
range: [startOfRange, endOfRange],
loc: { start, end },
indentColumn: next.loc.start.column,
};
}
next = beforeComment;
}
const start = {
line: node.loc.start.line,
column: 0,
};
const startOfRange = sourceCode.getIndexFromLoc(start);
return {
range: [startOfRange, endOfRange],
loc: { start, end },
indentColumn: node.loc.start.column,
};
}
},
});