eslint-plugin-yml
Version:
This ESLint plugin provides linting rules for YAML.
657 lines (656 loc) • 26.9 kB
JavaScript
"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 index_1 = require("../utils/index");
const ast_utils_1 = require("../utils/ast-utils");
const yaml_eslint_parser_1 = require("yaml-eslint-parser");
const compat_1 = require("../utils/compat");
const calc_shortest_edit_script_1 = require("../utils/calc-shortest-edit-script");
class YAMLEntryData {
get reportLoc() {
if (this.node) {
return this.node.loc;
}
const aroundTokens = this.aroundTokens;
return {
start: aroundTokens.before.loc.end,
end: aroundTokens.after.loc.start,
};
}
get range() {
if (this.node) {
return this.node.range;
}
if (this.cachedRange) {
return this.cachedRange;
}
const aroundTokens = this.aroundTokens;
return (this.cachedRange = [
aroundTokens.before.range[1],
aroundTokens.after.range[0],
]);
}
get aroundTokens() {
if (this.cachedAroundTokens) {
return this.cachedAroundTokens;
}
const sourceCode = this.sequence.sourceCode;
if (this.node) {
return (this.cachedAroundTokens = {
before: sourceCode.getTokenBefore(this.node),
after: sourceCode.getTokenAfter(this.node),
});
}
const before = this.index > 0
? this.sequence.entries[this.index - 1].aroundTokens.after
: sourceCode.getFirstToken(this.sequence.node);
const after = sourceCode.getTokenAfter(before);
return (this.cachedAroundTokens = { before, after });
}
constructor(sequence, node, index, anchorAlias) {
this.cached = null;
this.cachedRange = null;
this.cachedAroundTokens = null;
this.sequence = sequence;
this.node = node;
this.index = index;
this.anchorAlias = anchorAlias;
}
get value() {
var _a;
return ((_a = this.cached) !== null && _a !== void 0 ? _a : (this.cached = {
value: this.node == null ? null : (0, yaml_eslint_parser_1.getStaticYAMLValue)(this.node),
})).value;
}
}
class YAMLSequenceData {
constructor(node, sourceCode, anchorAliasMap) {
this.cachedEntries = null;
this.node = node;
this.sourceCode = sourceCode;
this.anchorAliasMap = anchorAliasMap;
}
get entries() {
var _a;
return ((_a = this.cachedEntries) !== null && _a !== void 0 ? _a : (this.cachedEntries = this.node.entries.map((e, index) => new YAMLEntryData(this, e, index, this.anchorAliasMap.get(e)))));
}
}
function buildValidatorFromType(order, insensitive, natural) {
let compareValue = ([a, b]) => a <= b;
let compareText = compareValue;
if (natural) {
compareText = ([a, b]) => (0, natural_compare_1.default)(a, b) <= 0;
}
if (insensitive) {
const baseCompareText = compareText;
compareText = ([a, b]) => baseCompareText([a.toLowerCase(), b.toLowerCase()]);
}
if (order === "desc") {
const baseCompareText = compareText;
compareText = (args) => baseCompareText(args.reverse());
const baseCompareValue = compareValue;
compareValue = (args) => baseCompareValue(args.reverse());
}
return (a, b) => {
if (typeof a.value === "string" && typeof b.value === "string") {
return compareText([a.value, b.value]);
}
const type = getYAMLPrimitiveType(a.value);
if (type && type === getYAMLPrimitiveType(b.value)) {
return compareValue([a.value, b.value]);
}
return true;
};
}
function parseOptions(options, sourceCode) {
return options.map((opt) => {
var _a, _b, _c, _d;
const order = opt.order;
const pathPattern = new RegExp(opt.pathPattern);
const minValues = (_a = opt.minValues) !== null && _a !== void 0 ? _a : 2;
if (!Array.isArray(order)) {
const type = (_b = order.type) !== null && _b !== void 0 ? _b : "asc";
const insensitive = order.caseSensitive === false;
const natural = Boolean(order.natural);
return {
isTargetArray,
ignore: () => false,
isValidOrder: buildValidatorFromType(type, insensitive, natural),
orderText(data) {
if (typeof data.value === "string") {
return `${natural ? "natural " : ""}${insensitive ? "insensitive " : ""}${type}ending`;
}
return `${type}ending`;
},
};
}
const parsedOrder = [];
for (const o of order) {
if (typeof o === "string") {
parsedOrder.push({
test: (v) => v.value === o,
isValidNestOrder: () => true,
});
}
else {
const valuePattern = o.valuePattern ? new RegExp(o.valuePattern) : null;
const nestOrder = (_c = o.order) !== null && _c !== void 0 ? _c : {};
const type = (_d = nestOrder.type) !== null && _d !== void 0 ? _d : "asc";
const insensitive = nestOrder.caseSensitive === false;
const natural = Boolean(nestOrder.natural);
parsedOrder.push({
test: (v) => valuePattern
? Boolean(getYAMLPrimitiveType(v.value)) &&
valuePattern.test(String(v.value))
: true,
isValidNestOrder: buildValidatorFromType(type, insensitive, natural),
});
}
}
return {
isTargetArray,
ignore: (v) => parsedOrder.every((p) => !p.test(v)),
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",
};
function isTargetArray(data) {
if (data.node.entries.length < minValues) {
return false;
}
let path = "";
let curr = data.node;
let p = curr.parent;
while (p) {
if (p.type === "YAMLPair") {
const name = getPropertyName(p);
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 pathPattern.test(path);
}
});
function getPropertyName(node) {
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);
}
}
function getYAMLPrimitiveType(val) {
const t = typeof val;
if (t === "string" || t === "number" || t === "boolean" || t === "bigint") {
return t;
}
if (val === null) {
return "null";
}
if (val === undefined) {
return "undefined";
}
if (val instanceof RegExp) {
return "regexp";
}
return null;
}
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-sequence-values", {
meta: {
docs: {
description: "require sequence values to be sorted",
categories: null,
extensionRule: false,
layout: false,
},
fixable: "code",
schema: {
type: "array",
items: {
type: "object",
properties: {
pathPattern: { type: "string" },
order: {
oneOf: [
{
type: "array",
items: {
anyOf: [
{ type: "string" },
{
type: "object",
properties: {
valuePattern: {
type: "string",
},
order: ORDER_OBJECT_SCHEMA,
},
additionalProperties: false,
},
],
},
uniqueItems: true,
},
ORDER_OBJECT_SCHEMA,
],
},
minValues: {
type: "integer",
minimum: 2,
},
},
required: ["pathPattern", "order"],
additionalProperties: false,
},
minItems: 1,
},
messages: {
shouldBeBefore: "Expected sequence values to be in {{orderText}} order. '{{thisValue}}' should be before '{{targetValue}}'.",
shouldBeAfter: "Expected sequence values to be in {{orderText}} order. '{{thisValue}}' should be after '{{targetValue}}'.",
},
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 shouldKeepOrder(prevData, nextData) {
if ((prevData.anchorAlias.aliases.size === 0 &&
prevData.anchorAlias.anchors.size === 0) ||
(nextData.anchorAlias.aliases.size === 0 &&
nextData.anchorAlias.anchors.size === 0))
return false;
for (const aliasName of nextData.anchorAlias.aliases) {
if (prevData.anchorAlias.anchors.has(aliasName)) {
return true;
}
}
for (const anchorName of nextData.anchorAlias.anchors) {
if (prevData.anchorAlias.aliases.has(anchorName)) {
return true;
}
}
return false;
}
function bubbleSort(entries, option) {
const l = entries.length;
const result = [...entries];
let swapped;
do {
swapped = false;
for (let nextIndex = 1; nextIndex < l; nextIndex++) {
const prevIndex = nextIndex - 1;
if (option.isValidOrder(result[prevIndex], result[nextIndex]) ||
shouldKeepOrder(result[prevIndex], result[nextIndex]))
continue;
[result[prevIndex], result[nextIndex]] = [
result[nextIndex],
result[prevIndex],
];
swapped = true;
}
} while (swapped);
return result;
}
function verifyArrayElements(entries, option) {
const sorted = bubbleSort(entries, option);
const editScript = (0, calc_shortest_edit_script_1.calcShortestEditScript)(entries, sorted);
for (let index = 0; index < editScript.length; index++) {
const edit = editScript[index];
if (edit.type !== "delete")
continue;
const insertEditIndex = editScript.findIndex((e) => e.type === "insert" && e.b === edit.a);
if (insertEditIndex === -1) {
continue;
}
if (index < insertEditIndex) {
const target = findInsertAfterTarget(edit.a, insertEditIndex);
if (!target) {
continue;
}
context.report({
loc: edit.a.reportLoc,
messageId: "shouldBeAfter",
data: {
thisValue: toText(edit.a),
targetValue: toText(target),
orderText: option.orderText(edit.a),
},
*fix(fixer) {
if (edit.a.sequence.node.style === "flow") {
yield* fixToMoveDownForFlow(fixer, edit.a, target);
}
else {
yield* fixToMoveDownForBlock(fixer, edit.a, target);
}
},
});
}
else {
const target = findInsertBeforeTarget(edit.a, insertEditIndex);
if (!target) {
continue;
}
context.report({
loc: edit.a.reportLoc,
messageId: "shouldBeBefore",
data: {
thisValue: toText(edit.a),
targetValue: toText(target),
orderText: option.orderText(edit.a),
},
*fix(fixer) {
if (edit.a.sequence.node.style === "flow") {
yield* fixToMoveUpForFlow(fixer, edit.a, target);
}
else {
yield* fixToMoveUpForBlock(fixer, edit.a, target);
}
},
});
}
}
function findInsertAfterTarget(entry, insertEditIndex) {
let candidate = null;
for (let index = insertEditIndex - 1; index >= 0; index--) {
const edit = editScript[index];
if (edit.type === "delete" && edit.a === entry)
break;
if (edit.type !== "common")
continue;
candidate = edit.a;
break;
}
const entryIndex = entries.indexOf(entry);
if (candidate) {
for (let index = entryIndex + 1; index < entries.length; index++) {
const element = entries[index];
if (element === candidate)
return candidate;
if (shouldKeepOrder(entry, element)) {
break;
}
}
}
let lastTarget = null;
for (let index = entryIndex + 1; index < entries.length; index++) {
const element = entries[index];
if (option.isValidOrder(element, entry) &&
!shouldKeepOrder(entry, element)) {
lastTarget = element;
continue;
}
return lastTarget;
}
return lastTarget;
}
function findInsertBeforeTarget(entry, insertEditIndex) {
let candidate = null;
for (let index = insertEditIndex + 1; index < editScript.length; index++) {
const edit = editScript[index];
if (edit.type === "delete" && edit.a === entry)
break;
if (edit.type !== "common")
continue;
candidate = edit.a;
break;
}
const entryIndex = entries.indexOf(entry);
if (candidate) {
for (let index = entryIndex - 1; index >= 0; index--) {
const element = entries[index];
if (element === candidate)
return candidate;
if (shouldKeepOrder(element, entry)) {
break;
}
}
}
let lastTarget = null;
for (let index = entryIndex - 1; index >= 0; index--) {
const element = entries[index];
if (option.isValidOrder(entry, element) &&
!shouldKeepOrder(element, entry)) {
lastTarget = element;
continue;
}
return lastTarget;
}
return lastTarget;
}
}
function toText(data) {
if (getYAMLPrimitiveType(data.value)) {
return String(data.value);
}
return sourceCode.getText(data.node);
}
let entryStack = {
upper: null,
anchors: new Set(),
aliases: new Set(),
};
const anchorAliasMap = new Map();
return {
"YAMLSequence > *"(node) {
if (!node.parent.entries.includes(node)) {
return;
}
entryStack = {
upper: entryStack,
anchors: new Set(),
aliases: new Set(),
};
if (node.type === "YAMLAlias") {
entryStack.aliases.add(node.name);
}
},
YAMLAnchor(node) {
if (entryStack) {
entryStack.anchors.add(node.name);
}
},
YAMLAlias(node) {
if (entryStack) {
entryStack.aliases.add(node.name);
}
},
"YAMLSequence > *:exit"(node) {
if (!node.parent.entries.includes(node)) {
return;
}
anchorAliasMap.set(node, entryStack);
const { anchors, aliases } = entryStack;
entryStack = entryStack.upper;
entryStack.anchors = new Set([...entryStack.anchors, ...anchors]);
entryStack.aliases = new Set([...entryStack.aliases, ...aliases]);
},
"YAMLSequence:exit"(node) {
const data = new YAMLSequenceData(node, sourceCode, anchorAliasMap);
const option = parsedOptions.find((o) => o.isTargetArray(data));
if (!option) {
return;
}
verifyArrayElements(data.entries.filter((d) => !option.ignore(d)), option);
},
};
function* fixToMoveDownForFlow(fixer, data, moveTarget) {
const beforeToken = data.aroundTokens.before;
const afterToken = data.aroundTokens.after;
let insertCode, removeRange, insertTargetToken;
if ((0, ast_utils_1.isComma)(afterToken)) {
removeRange = [beforeToken.range[1], afterToken.range[1]];
const moveTargetAfterToken = moveTarget.aroundTokens.after;
if ((0, ast_utils_1.isComma)(moveTargetAfterToken)) {
insertTargetToken = moveTargetAfterToken;
insertCode = sourceCode.text.slice(...removeRange);
}
else {
insertTargetToken = moveTarget.node
? sourceCode.getLastToken(moveTarget.node)
: moveTarget.aroundTokens.before;
insertCode = sourceCode.text.slice(beforeToken.range[1], afterToken.range[0]);
insertCode = `,${insertCode}`;
}
}
else {
if ((0, ast_utils_1.isComma)(beforeToken)) {
removeRange = [beforeToken.range[0], data.range[1]];
insertCode = sourceCode.text.slice(...removeRange);
insertTargetToken = moveTarget.node
? sourceCode.getLastToken(moveTarget.node)
: moveTarget.aroundTokens.before;
}
else {
removeRange = [beforeToken.range[1], data.range[1]];
insertCode = `,${sourceCode.text.slice(...removeRange)}`;
insertTargetToken = moveTarget.node
? sourceCode.getLastToken(moveTarget.node)
: moveTarget.aroundTokens.before;
}
}
yield fixer.removeRange(removeRange);
yield fixer.insertTextAfterRange(insertTargetToken.range, insertCode);
}
function* fixToMoveUpForFlow(fixer, data, moveTarget) {
const beforeToken = data.aroundTokens.before;
const afterToken = data.aroundTokens.after;
let insertCode, removeRange, insertTargetToken;
if ((0, ast_utils_1.isComma)(afterToken)) {
removeRange = [beforeToken.range[1], afterToken.range[1]];
insertCode = sourceCode.text.slice(...removeRange);
insertTargetToken = moveTarget.aroundTokens.before;
}
else {
removeRange = [beforeToken.range[0], data.range[1]];
if ((0, ast_utils_1.isComma)(moveTarget.aroundTokens.before)) {
insertCode = sourceCode.text.slice(...removeRange);
insertTargetToken = sourceCode.getTokenBefore(moveTarget.aroundTokens.before);
}
else {
insertCode = `${sourceCode.text.slice(beforeToken.range[1], data.range[1])},`;
insertTargetToken = moveTarget.aroundTokens.before;
}
}
yield fixer.insertTextAfterRange(insertTargetToken.range, insertCode);
yield fixer.removeRange(removeRange);
}
function* fixToMoveDownForBlock(fixer, data, moveTarget) {
const moveDataList = data.sequence.entries.slice(data.index, moveTarget.index + 1);
let replacementCodeRange = getBlockEntryRange(data);
for (const target of moveDataList.reverse()) {
const range = getBlockEntryRange(target);
yield fixer.replaceTextRange(range, sourceCode.text.slice(...replacementCodeRange));
replacementCodeRange = range;
}
}
function* fixToMoveUpForBlock(fixer, data, moveTarget) {
const moveDataList = data.sequence.entries.slice(moveTarget.index, data.index + 1);
let replacementCodeRange = getBlockEntryRange(data);
for (const target of moveDataList) {
const range = getBlockEntryRange(target);
yield fixer.replaceTextRange(range, sourceCode.text.slice(...replacementCodeRange));
replacementCodeRange = range;
}
}
function getBlockEntryRange(data) {
return [getBlockEntryStartOffset(data), getBlockEntryEndOffset(data)];
}
function getBlockEntryStartOffset(data) {
const beforeHyphenToken = sourceCode.getTokenBefore(data.aroundTokens.before);
if (!beforeHyphenToken) {
const comment = sourceCode.getTokenBefore(data.aroundTokens.before, {
includeComments: true,
});
if (comment &&
data.aroundTokens.before.loc.start.column <= comment.loc.start.column) {
return comment.range[0];
}
return data.aroundTokens.before.range[0];
}
let next = sourceCode.getTokenAfter(beforeHyphenToken, {
includeComments: true,
});
while (beforeHyphenToken.loc.end.line === next.loc.start.line &&
next.range[1] < data.aroundTokens.before.range[0]) {
next = sourceCode.getTokenAfter(next, {
includeComments: true,
});
}
return next.range[0];
}
function getBlockEntryEndOffset(data) {
var _a;
const valueEndToken = (_a = data.node) !== null && _a !== void 0 ? _a : data.aroundTokens.before;
let last = valueEndToken;
let afterToken = sourceCode.getTokenAfter(last, {
includeComments: true,
});
while (afterToken &&
valueEndToken.loc.end.line === afterToken.loc.start.line) {
last = afterToken;
afterToken = sourceCode.getTokenAfter(last, {
includeComments: true,
});
}
return last.range[1];
}
},
});