eslint-plugin-yml
Version:
This ESLint plugin provides linting rules for YAML.
1,382 lines (1,381 loc) • 190 kB
JavaScript
import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
import * as yamlESLintParser from "yaml-eslint-parser";
import { VisitorKeys, getStaticYAMLValue, parseForESLint, parseYAML, traverseNodes } from "yaml-eslint-parser";
import path from "node:path";
import naturalCompare from "natural-compare";
import diffModule from "diff-sequences";
import escapeStringRegexp from "escape-string-regexp";
import { CallMethodStep, ConfigCommentParser, Directive, TextSourceCodeBase, VisitNodeStep } from "@eslint/plugin-kit";
import { TokenStore } from "@ota-meshi/ast-token-store";
//#region src/utils/index.ts
/**
* Define the rule.
* @param ruleName ruleName
* @param rule rule module
*/
function createRule(ruleName, rule) {
return {
meta: {
...rule.meta,
docs: {
...rule.meta.docs,
url: `https://ota-meshi.github.io/eslint-plugin-yml/rules/${ruleName}.html`,
ruleId: `yml/${ruleName}`,
ruleName
}
},
create(context) {
const sourceCode = context.sourceCode;
if (typeof sourceCode.parserServices?.defineCustomBlocksVisitor === "function" && path.extname(context.filename) === ".vue") return sourceCode.parserServices.defineCustomBlocksVisitor(context, yamlESLintParser, {
target: ["yaml", "yml"],
create(blockContext) {
return rule.create(blockContext, { customBlock: true });
}
});
return rule.create(context, { customBlock: false });
}
};
}
//#endregion
//#region src/utils/ast-utils.ts
/**
* Checks if the given token is a comment token or not.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a comment token.
*/
function isCommentToken(token) {
return Boolean(token && (token.type === "Block" || token.type === "Line"));
}
/**
* Determines whether two adjacent tokens are on the same line.
* @param {Object} left The left token object.
* @param {Object} right The right token object.
* @returns {boolean} Whether or not the tokens are on the same line.
* @public
*/
function isTokenOnSameLine(left, right) {
return left.loc.end.line === right.loc.start.line;
}
/**
* Check whether the given token is a question.
* @param token The token to check.
* @returns `true` if the token is a question.
*/
function isQuestion(token) {
return token != null && token.type === "Punctuator" && token.value === "?";
}
/**
* Check whether the given token is a hyphen.
* @param token The token to check.
* @returns `true` if the token is a hyphen.
*/
function isHyphen(token) {
return token != null && token.type === "Punctuator" && token.value === "-";
}
/**
* Check whether the given token is a colon.
* @param token The token to check.
* @returns `true` if the token is a colon.
*/
function isColon(token) {
return token != null && token.type === "Punctuator" && token.value === ":";
}
/**
* Check whether the given token is a comma.
* @param token The token to check.
* @returns `true` if the token is a comma.
*/
function isComma(token) {
return token != null && token.type === "Punctuator" && token.value === ",";
}
/**
* Checks if the given token is an opening square bracket token or not.
* @param token The token to check.
* @returns `true` if the token is an opening square bracket token.
*/
function isOpeningBracketToken(token) {
return token != null && token.value === "[" && token.type === "Punctuator";
}
/**
* Checks if the given token is a closing square bracket token or not.
* @param token The token to check.
* @returns `true` if the token is a closing square bracket token.
*/
function isClosingBracketToken(token) {
return token != null && token.value === "]" && token.type === "Punctuator";
}
/**
* Checks if the given token is an opening brace token or not.
* @param token The token to check.
* @returns `true` if the token is an opening brace token.
*/
function isOpeningBraceToken(token) {
return token != null && token.value === "{" && token.type === "Punctuator";
}
/**
* Checks if the given token is a closing brace token or not.
* @param token The token to check.
* @returns `true` if the token is a closing brace token.
*/
function isClosingBraceToken(token) {
return token != null && token.value === "}" && token.type === "Punctuator";
}
//#endregion
//#region src/rules/block-mapping-colon-indicator-newline.ts
var block_mapping_colon_indicator_newline_default = createRule("block-mapping-colon-indicator-newline", {
meta: {
docs: {
description: "enforce consistent line breaks after `:` indicator",
categories: ["standard"],
extensionRule: false,
layout: true
},
fixable: "whitespace",
schema: [{ enum: ["always", "never"] }],
messages: {
unexpectedLinebreakAfterIndicator: "Unexpected line break after this `:` indicator.",
expectedLinebreakAfterIndicator: "Expected a line break after this `:` indicator."
},
type: "layout"
},
create(context) {
const sourceCode = context.sourceCode;
if (!sourceCode.parserServices?.isYAML) return {};
const option = context.options[0] || "never";
/**
* Get the colon token from the given pair node.
*/
function getColonToken(pair) {
const limitIndex = pair.key ? pair.key.range[1] : pair.range[0];
let candidateColon = sourceCode.getTokenBefore(pair.value);
while (candidateColon && !isColon(candidateColon)) {
candidateColon = sourceCode.getTokenBefore(candidateColon);
if (candidateColon && candidateColon.range[1] <= limitIndex) return null;
}
if (!candidateColon || !isColon(candidateColon)) return null;
return candidateColon;
}
/**
* Checks whether the newline between the given value node and the colon can be removed.
*/
function canRemoveNewline(value) {
const node = value.type === "YAMLWithMeta" ? value.value : value;
if (node && (node.type === "YAMLSequence" || node.type === "YAMLMapping") && node.style === "block") return false;
return true;
}
return { YAMLMapping(node) {
if (node.style !== "block") return;
for (const pair of node.pairs) {
const value = pair.value;
if (!value) continue;
const colon = getColonToken(pair);
if (!colon) return;
if (colon.loc.end.line < value.loc.start.line) {
if (option === "never") {
if (!canRemoveNewline(value)) return;
context.report({
loc: colon.loc,
messageId: "unexpectedLinebreakAfterIndicator",
fix(fixer) {
const spaceCount = value.loc.start.column - colon.loc.end.column;
if (spaceCount < 1 && value.loc.start.line < value.loc.end.line) return null;
const spaces = " ".repeat(Math.max(spaceCount, 1));
return fixer.replaceTextRange([colon.range[1], value.range[0]], spaces);
}
});
}
} else if (option === "always") context.report({
loc: colon.loc,
messageId: "expectedLinebreakAfterIndicator",
fix(fixer) {
const spaces = `\n${" ".repeat(value.loc.start.column)}`;
return fixer.replaceTextRange([colon.range[1], value.range[0]], spaces);
}
});
}
} };
}
});
//#endregion
//#region src/rules/block-mapping-question-indicator-newline.ts
var block_mapping_question_indicator_newline_default = createRule("block-mapping-question-indicator-newline", {
meta: {
docs: {
description: "enforce consistent line breaks after `?` indicator",
categories: ["standard"],
extensionRule: false,
layout: true
},
fixable: "whitespace",
schema: [{ enum: ["always", "never"] }],
messages: {
unexpectedLinebreakAfterIndicator: "Unexpected line break after this `?` indicator.",
expectedLinebreakAfterIndicator: "Expected a line break after this `?` indicator."
},
type: "layout"
},
create(context) {
const sourceCode = context.sourceCode;
if (!sourceCode.parserServices?.isYAML) return {};
const option = context.options[0] || "never";
return { YAMLMapping(node) {
if (node.style !== "block") return;
for (const pair of node.pairs) {
const key = pair.key;
if (!key) continue;
const question = sourceCode.getFirstToken(pair);
if (!question || !isQuestion(question)) continue;
if (question.loc.end.line < key.loc.start.line) {
if (option === "never") context.report({
loc: question.loc,
messageId: "unexpectedLinebreakAfterIndicator",
fix(fixer) {
const spaceCount = key.loc.start.column - question.loc.end.column;
if (spaceCount < 1 && key.loc.start.line < key.loc.end.line) return null;
const spaces = " ".repeat(Math.max(spaceCount, 1));
return fixer.replaceTextRange([question.range[1], key.range[0]], spaces);
}
});
} else if (option === "always") context.report({
loc: question.loc,
messageId: "expectedLinebreakAfterIndicator",
fix(fixer) {
const spaces = `\n${" ".repeat(key.loc.start.column)}`;
return fixer.replaceTextRange([question.range[1], key.range[0]], spaces);
}
});
}
} };
}
});
//#endregion
//#region src/utils/yaml.ts
/**
* Check if you are using tabs for indentation.
* If you're using tabs, you're not sure if your YAML was parsed successfully, so almost all rules stop auto-fix.
*/
function hasTabIndent(context) {
for (const line of context.sourceCode.getLines()) {
if (/^\s*\t/u.test(line)) return true;
if (/^\s*-\s*\t/u.test(line)) return true;
}
return false;
}
/**
* Calculate the required indentation for a given YAMLMapping pairs.
* Before calling this function, make sure that no flow style exists above the given mapping.
*/
function calcExpectIndentForPairs(mapping, context) {
const sourceCode = context.sourceCode;
let parentNode = mapping.parent;
if (parentNode.type === "YAMLWithMeta") {
const before = sourceCode.getTokenBefore(parentNode);
if (before == null || before.loc.end.line < parentNode.loc.start.line) return calcExpectIndentFromBaseNode(parentNode, mapping.pairs[0], context);
parentNode = parentNode.parent;
}
if (parentNode.type === "YAMLDocument") {
const mappingIndent = getActualIndent(mapping, context);
const firstPairIndent = getActualIndent(mapping.pairs[0], context);
if (mappingIndent == null) return firstPairIndent;
if (firstPairIndent != null && compareIndent(mappingIndent, firstPairIndent) < 0) return firstPairIndent;
return mappingIndent;
}
if (parentNode.type === "YAMLSequence") {
const hyphen = sourceCode.getTokenBefore(mapping);
if (!isHyphen(hyphen)) return null;
if (hyphen.loc.start.line === mapping.loc.start.line) {
const hyphenIndent = getActualIndent(hyphen, context);
if (hyphenIndent == null) return null;
return `${hyphenIndent} ${sourceCode.text.slice(hyphen.range[1], mapping.range[0])}`;
}
return getActualIndent(mapping, context);
}
if (parentNode.type !== "YAMLPair") return null;
return calcExpectIndentFromBaseNode(parentNode, mapping.pairs[0], context);
}
/**
* Calculate the required indentation for a given YAMLSequence entries.
*/
function calcExpectIndentForEntries(sequence, context) {
const sourceCode = context.sourceCode;
let parentNode = sequence.parent;
if (parentNode.type === "YAMLWithMeta") {
const before = sourceCode.getTokenBefore(parentNode);
if (before == null || before.loc.end.line < parentNode.loc.start.line) return calcExpectIndentFromBaseNode(parentNode, sequence.entries[0], context);
parentNode = parentNode.parent;
}
if (parentNode.type === "YAMLDocument") {
const sequenceIndent = getActualIndent(sequence, context);
const firstPairIndent = getActualIndent(sequence.entries[0], context);
if (sequenceIndent == null) return firstPairIndent;
if (firstPairIndent != null && compareIndent(sequenceIndent, firstPairIndent) < 0) return firstPairIndent;
return sequenceIndent;
}
if (parentNode.type === "YAMLSequence") {
const hyphen = sourceCode.getTokenBefore(sequence);
if (!isHyphen(hyphen)) return null;
if (hyphen.loc.start.line === sequence.loc.start.line) {
const hyphenIndent = getActualIndent(hyphen, context);
if (hyphenIndent == null) return null;
return `${hyphenIndent} ${sourceCode.text.slice(hyphen.range[1], sequence.range[0])}`;
}
return getActualIndent(sequence, context);
}
if (parentNode.type !== "YAMLPair") return null;
return calcExpectIndentFromBaseNode(parentNode, sequence.entries[0], context);
}
/**
* Calculate the required indentation from a given base node.
*/
function calcExpectIndentFromBaseNode(baseNode, node, context) {
const baseIndent = getActualIndent(baseNode, context);
if (baseIndent == null) return null;
const indent = getActualIndent(node, context);
if (indent != null && compareIndent(baseIndent, indent) < 0) return indent;
return incIndent(baseIndent, context);
}
/**
* Get the actual indentation for a given node.
*/
function getActualIndent(node, context) {
const before = context.sourceCode.getTokenBefore(node, { includeComments: true });
if (!before || before.loc.end.line < node.loc.start.line) return getActualIndentFromLine(node.loc.start.line, context);
return null;
}
/**
* Get the actual indentation for a given line.
*/
function getActualIndentFromLine(line, context) {
const lineText = context.sourceCode.getLines()[line - 1];
return /^[^\S\n\r\u2028\u2029]*/u.exec(lineText)[0];
}
/**
* Returns the indent that is incremented.
*/
function incIndent(indent, context) {
const numOfIndent = getNumOfIndent(context);
return `${indent}${numOfIndent === 2 ? " " : numOfIndent === 4 ? " " : " ".repeat(numOfIndent)}`;
}
/**
* Get the number of indentation offset
*/
function getNumOfIndent(context, optionValue) {
const num = optionValue ?? context.settings?.yml?.indent;
return num == null || num < 2 ? 2 : num;
}
/**
* Check if the indent is increasing.
*/
function compareIndent(a, b) {
const minLen = Math.min(a.length, b.length);
for (let index = 0; index < minLen; index++) if (a[index] !== b[index]) return NaN;
return a.length > b.length ? 1 : a.length < b.length ? -1 : 0;
}
/**
* Check if the given node is key node.
*/
function isKeyNode(node) {
if (node.parent.type === "YAMLWithMeta") return isKeyNode(node.parent);
return node.parent.type === "YAMLPair" && node.parent.key === node;
}
/**
* Unwrap meta
*/
function unwrapMeta(node) {
if (!node) return node;
if (node.type === "YAMLWithMeta") return node.value;
return node;
}
/**
* Adjust indent
*/
function* processIndentFix(fixer, baseIndent, targetNode, context) {
const sourceCode = context.sourceCode;
if (targetNode.type === "YAMLWithMeta") {
yield* metaIndent(targetNode);
return;
}
if (targetNode.type === "YAMLPair") {
yield* pairIndent(targetNode);
return;
}
yield* contentIndent(targetNode);
/**
* for YAMLContent
*/
function* contentIndent(contentNode) {
const actualIndent = getActualIndent(contentNode, context);
if (actualIndent != null && compareIndent(baseIndent, actualIndent) < 0) return;
let nextBaseIndent = baseIndent;
const expectValueIndent = incIndent(baseIndent, context);
if (actualIndent != null) {
yield fixIndent(fixer, sourceCode, expectValueIndent, contentNode);
nextBaseIndent = expectValueIndent;
}
if (contentNode.type === "YAMLMapping") {
for (const p of contentNode.pairs) yield* processIndentFix(fixer, nextBaseIndent, p, context);
if (contentNode.style === "flow") {
const close = sourceCode.getLastToken(contentNode);
if (close.value === "}") {
const closeActualIndent = getActualIndent(close, context);
if (closeActualIndent != null && compareIndent(closeActualIndent, nextBaseIndent) < 0) yield fixIndent(fixer, sourceCode, nextBaseIndent, close);
}
}
} else if (contentNode.type === "YAMLSequence") for (const e of contentNode.entries) {
if (!e) continue;
yield* processIndentFix(fixer, nextBaseIndent, e, context);
}
}
/**
* for YAMLWithMeta
*/
function* metaIndent(metaNode) {
let nextBaseIndent = baseIndent;
const actualIndent = getActualIndent(metaNode, context);
if (actualIndent != null) if (compareIndent(baseIndent, actualIndent) < 0) nextBaseIndent = actualIndent;
else {
const expectMetaIndent = incIndent(baseIndent, context);
yield fixIndent(fixer, sourceCode, expectMetaIndent, metaNode);
nextBaseIndent = expectMetaIndent;
}
if (metaNode.value) yield* processIndentFix(fixer, nextBaseIndent, metaNode.value, context);
}
/**
* for YAMLPair
*/
function* pairIndent(pairNode) {
let nextBaseIndent = baseIndent;
const actualIndent = getActualIndent(pairNode, context);
if (actualIndent != null) if (compareIndent(baseIndent, actualIndent) < 0) nextBaseIndent = actualIndent;
else {
const expectKeyIndent = incIndent(baseIndent, context);
yield fixIndent(fixer, sourceCode, expectKeyIndent, pairNode);
nextBaseIndent = expectKeyIndent;
}
if (pairNode.value) yield* processIndentFix(fixer, nextBaseIndent, pairNode.value, context);
}
}
/**
* Fix indent
*/
function fixIndent(fixer, sourceCode, indent, node) {
const prevToken = sourceCode.getTokenBefore(node, { includeComments: true });
return fixer.replaceTextRange([prevToken.range[1], node.range[0]], `\n${indent}`);
}
//#endregion
//#region src/rules/block-mapping.ts
const OPTIONS_ENUM$1 = [
"always",
"never",
"ignore"
];
/**
* Parse options
*/
function parseOptions$5(option) {
const opt = {
singleline: "ignore",
multiline: "always"
};
if (option) if (typeof option === "string") {
opt.singleline = option;
opt.multiline = option;
} else {
if (typeof option.singleline === "string") opt.singleline = option.singleline;
if (typeof option.multiline === "string") opt.multiline = option.multiline;
}
return opt;
}
var block_mapping_default = createRule("block-mapping", {
meta: {
docs: {
description: "require or disallow block style mappings.",
categories: ["standard"],
extensionRule: false,
layout: false
},
fixable: "code",
schema: [{ anyOf: [{ enum: ["always", "never"] }, {
type: "object",
properties: {
singleline: { enum: OPTIONS_ENUM$1 },
multiline: { enum: OPTIONS_ENUM$1 }
},
additionalProperties: false
}] }],
messages: {
required: "Must use block style mappings.",
disallow: "Must use flow style mappings."
},
type: "layout"
},
create(context) {
if (!context.sourceCode.parserServices?.isYAML) return {};
const options = parseOptions$5(context.options[0]);
let styleStack = null;
/**
* Moves the stack down.
*/
function downStack(node) {
if (styleStack) {
if (node.style === "flow") styleStack.hasFlowStyle = true;
else if (node.style === "block") styleStack.hasBlockStyle = true;
}
styleStack = {
upper: styleStack,
node,
flowStyle: node.style === "flow",
blockStyle: node.style === "block",
withinFlowStyle: styleStack && (styleStack.withinFlowStyle || styleStack.flowStyle) || false,
withinBlockStyle: styleStack && (styleStack.withinBlockStyle || styleStack.blockStyle) || false
};
}
/**
* Moves the stack up.
*/
function upStack() {
if (styleStack && styleStack.upper) {
styleStack.upper.hasNullPair = styleStack.upper.hasNullPair || styleStack.hasNullPair;
styleStack.upper.hasBlockLiteralOrFolded = styleStack.upper.hasBlockLiteralOrFolded || styleStack.hasBlockLiteralOrFolded;
styleStack.upper.hasBlockStyle = styleStack.upper.hasBlockStyle || styleStack.hasBlockStyle;
styleStack.upper.hasFlowStyle = styleStack.upper.hasFlowStyle || styleStack.hasFlowStyle;
}
styleStack = styleStack && styleStack.upper;
}
return {
YAMLSequence: downStack,
YAMLMapping: downStack,
YAMLPair(node) {
if (node.key == null || node.value == null) styleStack.hasNullPair = true;
},
YAMLScalar(node) {
if (styleStack && (node.style === "folded" || node.style === "literal")) styleStack.hasBlockLiteralOrFolded = true;
},
"YAMLSequence:exit": upStack,
"YAMLMapping:exit"(node) {
const mappingInfo = styleStack;
upStack();
if (node.pairs.length === 0) return;
const optionType = node.loc.start.line < node.loc.end.line ? options.multiline : options.singleline;
if (optionType === "ignore") return;
if (node.style === "flow") {
if (optionType === "never") return;
if (isKeyNode(node)) return;
const canFix = canFixToBlock$1(mappingInfo, node) && !hasTabIndent(context);
context.report({
loc: node.loc,
messageId: "required",
fix: canFix && buildFixFlowToBlock$1(node, context) || null
});
} else if (node.style === "block") {
if (optionType === "always") return;
const canFix = canFixToFlow$1(mappingInfo, node) && !hasTabIndent(context);
context.report({
loc: node.loc,
messageId: "disallow",
fix: canFix && buildFixBlockToFlow$1(node, context) || null
});
}
}
};
}
});
/**
* Check if it can be converted to block style.
*/
function canFixToBlock$1(mappingInfo, node) {
if (mappingInfo.hasNullPair || mappingInfo.hasBlockLiteralOrFolded) return false;
if (mappingInfo.withinFlowStyle) return false;
for (const pair of node.pairs) {
const key = pair.key;
if (key.loc.start.line < key.loc.end.line) return false;
}
return true;
}
/**
* Check if it can be converted to flow style.
*/
function canFixToFlow$1(mappingInfo, node) {
if (mappingInfo.hasNullPair || mappingInfo.hasBlockLiteralOrFolded) return false;
if (mappingInfo.hasBlockStyle) return false;
for (const pair of node.pairs) {
const value = unwrapMeta(pair.value);
const key = unwrapMeta(pair.key);
if (value && value.type === "YAMLScalar" && value.style === "plain") {
if (value.loc.start.line < value.loc.end.line) return false;
if (/[[\]{}]/u.test(value.strValue)) return false;
if (value.strValue.includes(",")) return false;
}
if (key && key.type === "YAMLScalar" && key.style === "plain") {
if (/[[\]{]/u.test(key.strValue)) return false;
if (/[,}]/u.test(key.strValue)) return false;
}
}
return true;
}
/**
* Build the fixer function that makes the flow style to block style.
*/
function buildFixFlowToBlock$1(node, context) {
return function* (fixer) {
const sourceCode = context.sourceCode;
const open = sourceCode.getFirstToken(node);
const close = sourceCode.getLastToken(node);
if (open?.value !== "{" || close?.value !== "}") return;
const expectIndent = calcExpectIndentForPairs(node, context);
if (expectIndent == null) return;
const openPrevToken = sourceCode.getTokenBefore(open, { includeComments: true });
if (!openPrevToken) yield fixer.removeRange([sourceCode.ast.range[0], open.range[1]]);
else if (openPrevToken.loc.end.line < open.loc.start.line) yield fixer.removeRange([openPrevToken.range[1], open.range[1]]);
else yield fixer.remove(open);
let prev = open;
for (const pair of node.pairs) {
const prevToken = sourceCode.getTokenBefore(pair, {
includeComments: true,
filter: (token) => !isComma(token)
});
yield* removeComma(prev, prevToken);
yield fixer.replaceTextRange([prevToken.range[1], pair.range[0]], `\n${expectIndent}`);
const colonToken = sourceCode.getTokenAfter(pair.key, isColon);
if (colonToken.range[1] === sourceCode.getTokenAfter(colonToken, { includeComments: true }).range[0]) yield fixer.insertTextAfter(colonToken, " ");
yield* processIndentFix(fixer, expectIndent, pair.value, context);
prev = pair;
}
yield* removeComma(prev, close);
yield fixer.remove(close);
/**
* Remove between commas
*/
function* removeComma(a, b) {
for (const token of sourceCode.getTokensBetween(a, b, { includeComments: true })) if (isComma(token)) yield fixer.remove(token);
}
};
}
/**
* Build the fixer function that makes the block style to flow style.
*/
function buildFixBlockToFlow$1(node, _context) {
return function* (fixer) {
yield fixer.insertTextBefore(node, "{");
const pairs = [...node.pairs];
const lastPair = pairs.pop();
for (const pair of pairs) yield fixer.insertTextAfter(pair, ",");
yield fixer.insertTextAfter(lastPair || node, "}");
};
}
//#endregion
//#region src/rules/block-sequence-hyphen-indicator-newline.ts
var block_sequence_hyphen_indicator_newline_default = createRule("block-sequence-hyphen-indicator-newline", {
meta: {
docs: {
description: "enforce consistent line breaks after `-` indicator",
categories: ["standard"],
extensionRule: false,
layout: true
},
fixable: "whitespace",
schema: [{ enum: ["always", "never"] }, {
type: "object",
properties: {
nestedHyphen: { enum: ["always", "never"] },
blockMapping: { enum: ["always", "never"] }
},
additionalProperties: false
}],
messages: {
unexpectedLinebreakAfterIndicator: "Unexpected line break after this `-` indicator.",
expectedLinebreakAfterIndicator: "Expected a line break after this `-` indicator."
},
type: "layout"
},
create(context) {
const sourceCode = context.sourceCode;
if (!sourceCode.parserServices?.isYAML) return {};
const style = context.options[0] || "never";
const nestedHyphenStyle = context.options[1]?.nestedHyphen || "always";
const blockMappingStyle = context.options[1]?.blockMapping || style;
/**
* Get style from given hyphen
*/
function getStyleOption(hyphen, entry) {
const next = sourceCode.getTokenAfter(hyphen);
if (next && isHyphen(next)) return nestedHyphenStyle;
if (entry.type === "YAMLMapping" && entry.style === "block") return blockMappingStyle;
return style;
}
return { YAMLSequence(node) {
if (node.style !== "block") return;
for (const entry of node.entries) {
if (!entry) continue;
const hyphen = sourceCode.getTokenBefore(entry);
if (!hyphen) continue;
if (hyphen.loc.end.line < entry.loc.start.line) {
if (getStyleOption(hyphen, entry) === "never") context.report({
loc: hyphen.loc,
messageId: "unexpectedLinebreakAfterIndicator",
fix(fixer) {
const spaceCount = entry.loc.start.column - hyphen.loc.end.column;
if (spaceCount < 1 && entry.loc.start.line < entry.loc.end.line) return null;
const spaces = " ".repeat(Math.max(spaceCount, 1));
return fixer.replaceTextRange([hyphen.range[1], entry.range[0]], spaces);
}
});
} else if (getStyleOption(hyphen, entry) === "always") context.report({
loc: hyphen.loc,
messageId: "expectedLinebreakAfterIndicator",
fix(fixer) {
const spaces = `\n${" ".repeat(entry.loc.start.column)}`;
return fixer.replaceTextRange([hyphen.range[1], entry.range[0]], spaces);
}
});
}
} };
}
});
//#endregion
//#region src/rules/block-sequence.ts
const OPTIONS_ENUM = [
"always",
"never",
"ignore"
];
/**
* Parse options
*/
function parseOptions$4(option) {
const opt = {
singleline: "ignore",
multiline: "always"
};
if (option) if (typeof option === "string") {
opt.singleline = option;
opt.multiline = option;
} else {
if (typeof option.singleline === "string") opt.singleline = option.singleline;
if (typeof option.multiline === "string") opt.multiline = option.multiline;
}
return opt;
}
var block_sequence_default = createRule("block-sequence", {
meta: {
docs: {
description: "require or disallow block style sequences.",
categories: ["standard"],
extensionRule: false,
layout: false
},
fixable: "code",
schema: [{ anyOf: [{ enum: ["always", "never"] }, {
type: "object",
properties: {
singleline: { enum: OPTIONS_ENUM },
multiline: { enum: OPTIONS_ENUM }
},
additionalProperties: false
}] }],
messages: {
required: "Must use block style sequences.",
disallow: "Must use flow style sequences."
},
type: "layout"
},
create(context) {
const sourceCode = context.sourceCode;
if (!sourceCode.parserServices?.isYAML) return {};
const options = parseOptions$4(context.options[0]);
let styleStack = null;
/**
* Moves the stack down.
*/
function downStack(node) {
if (styleStack) {
if (node.style === "flow") styleStack.hasFlowStyle = true;
else if (node.style === "block") styleStack.hasBlockStyle = true;
}
styleStack = {
upper: styleStack,
node,
flowStyle: node.style === "flow",
blockStyle: node.style === "block",
withinFlowStyle: styleStack && (styleStack.withinFlowStyle || styleStack.flowStyle) || false,
withinBlockStyle: styleStack && (styleStack.withinBlockStyle || styleStack.blockStyle) || false
};
}
/**
* Moves the stack up.
*/
function upStack() {
if (styleStack && styleStack.upper) {
styleStack.upper.hasNullPair = styleStack.upper.hasNullPair || styleStack.hasNullPair;
styleStack.upper.hasBlockLiteralOrFolded = styleStack.upper.hasBlockLiteralOrFolded || styleStack.hasBlockLiteralOrFolded;
styleStack.upper.hasBlockStyle = styleStack.upper.hasBlockStyle || styleStack.hasBlockStyle;
styleStack.upper.hasFlowStyle = styleStack.upper.hasFlowStyle || styleStack.hasFlowStyle;
}
styleStack = styleStack && styleStack.upper;
}
return {
YAMLMapping: downStack,
YAMLSequence: downStack,
YAMLPair(node) {
if (node.key == null || node.value == null) styleStack.hasNullPair = true;
},
YAMLScalar(node) {
if (styleStack && (node.style === "folded" || node.style === "literal")) styleStack.hasBlockLiteralOrFolded = true;
},
"YAMLMapping:exit": upStack,
"YAMLSequence:exit"(node) {
const sequenceInfo = styleStack;
upStack();
if (node.entries.length === 0) return;
const optionType = node.loc.start.line < node.loc.end.line ? options.multiline : options.singleline;
if (optionType === "ignore") return;
if (node.style === "flow") {
if (optionType === "never") return;
if (isKeyNode(node)) return;
const canFix = canFixToBlock(sequenceInfo, node, sourceCode) && !hasTabIndent(context);
context.report({
loc: node.loc,
messageId: "required",
fix: canFix && buildFixFlowToBlock(node, context) || null
});
} else if (node.style === "block") {
if (optionType === "always") return;
const canFix = canFixToFlow(sequenceInfo, node, context) && !hasTabIndent(context);
context.report({
loc: node.loc,
messageId: "disallow",
fix: canFix && buildFixBlockToFlow(node, context) || null
});
}
}
};
}
});
/**
* Check if it can be converted to block style.
*/
function canFixToBlock(sequenceInfo, node, sourceCode) {
if (sequenceInfo.hasNullPair || sequenceInfo.hasBlockLiteralOrFolded) return false;
if (sequenceInfo.withinFlowStyle) return false;
for (const entry of node.entries) if (entry.type === "YAMLMapping" && entry.style === "block") for (const pair of entry.pairs) {
if (pair.key) {
if (pair.key.loc.start.line < pair.key.loc.end.line) return false;
if (pair.key.type === "YAMLMapping") return false;
}
if (pair.value) {
const colon = sourceCode.getTokenBefore(pair.value);
if (colon?.value === ":") {
if (colon.range[1] === pair.value.range[0]) return false;
}
}
}
return true;
}
/**
* Check if it can be converted to flow style.
*/
function canFixToFlow(sequenceInfo, node, context) {
if (sequenceInfo.hasNullPair || sequenceInfo.hasBlockLiteralOrFolded) return false;
if (sequenceInfo.hasBlockStyle) return false;
if (node.parent.type === "YAMLWithMeta") {
const metaIndent = getActualIndent(node.parent, context);
if (metaIndent != null) {
for (let line = node.loc.start.line; line <= node.loc.end.line; line++) if (compareIndent(metaIndent, getActualIndentFromLine(line, context)) > 0) return false;
}
}
for (const entry of node.entries) {
const value = unwrapMeta(entry);
if (value && value.type === "YAMLScalar" && value.style === "plain") {
if (value.strValue.includes(",")) return false;
}
}
return true;
}
/**
* Build the fixer function that makes the flow style to block style.
*/
function buildFixFlowToBlock(node, context) {
return function* (fixer) {
const sourceCode = context.sourceCode;
const open = sourceCode.getFirstToken(node);
const close = sourceCode.getLastToken(node);
if (open?.value !== "[" || close?.value !== "]") return;
const expectIndent = calcExpectIndentForEntries(node, context);
if (expectIndent == null) return;
const openPrevToken = sourceCode.getTokenBefore(open, { includeComments: true });
if (!openPrevToken) yield fixer.removeRange([sourceCode.ast.range[0], open.range[1]]);
else if (openPrevToken.loc.end.line < open.loc.start.line) yield fixer.removeRange([openPrevToken.range[1], open.range[1]]);
else yield fixer.remove(open);
let prev = open;
for (const entry of node.entries) {
const prevToken = sourceCode.getTokenBefore(entry, {
includeComments: true,
filter: (token) => !isComma(token)
});
yield* removeComma(prev, prevToken);
yield fixer.replaceTextRange([prevToken.range[1], entry.range[0]], `\n${expectIndent}- `);
yield* processEntryIndent(`${expectIndent} `, entry);
prev = entry;
}
yield* removeComma(prev, close);
yield fixer.remove(close);
/**
* Remove between commas
*/
function* removeComma(a, b) {
for (const token of sourceCode.getTokensBetween(a, b, { includeComments: true })) if (isComma(token)) yield fixer.remove(token);
}
/**
* Indent
*/
function* processEntryIndent(baseIndent, entry) {
if (entry.type === "YAMLWithMeta" && entry.value) yield* processIndentFix(fixer, baseIndent, entry.value, context);
else if (entry.type === "YAMLMapping") {
for (const p of entry.pairs) if (p.range[0] === entry.range[0]) {
if (p.value) yield* processIndentFix(fixer, baseIndent, p.value, context);
} else yield* processIndentFix(fixer, baseIndent, p, context);
if (entry.style === "flow") {
const close = sourceCode.getLastToken(entry);
if (close.value === "}") {
const actualIndent = getActualIndent(close, context);
if (actualIndent != null && compareIndent(actualIndent, baseIndent) < 0) yield fixIndent(fixer, sourceCode, baseIndent, close);
}
}
} else if (entry.type === "YAMLSequence") for (const e of entry.entries) {
if (!e) continue;
yield* processIndentFix(fixer, baseIndent, e, context);
}
}
};
}
/**
* Build the fixer function that makes the block style to flow style.
*/
function buildFixBlockToFlow(node, context) {
const sourceCode = context.sourceCode;
return function* (fixer) {
const entries = node.entries.filter((e) => e != null);
if (entries.length !== node.entries.length) return;
const firstEntry = entries.shift();
const lastEntry = entries.pop();
const firstHyphen = sourceCode.getTokenBefore(firstEntry);
yield fixer.replaceText(firstHyphen, " ");
yield fixer.insertTextBefore(firstEntry, "[");
if (lastEntry) yield fixer.insertTextAfter(firstEntry, ",");
for (const entry of entries) {
const hyphen = sourceCode.getTokenBefore(entry);
yield fixer.replaceText(hyphen, " ");
yield fixer.insertTextAfter(entry, ",");
}
if (lastEntry) {
const lastHyphen = sourceCode.getTokenBefore(lastEntry);
yield fixer.replaceText(lastHyphen, " ");
}
yield fixer.insertTextAfter(lastEntry || firstEntry || node, "]");
};
}
//#endregion
//#region src/rules/file-extension.ts
var file_extension_default = createRule("file-extension", {
meta: {
docs: {
description: "enforce YAML file extension",
categories: [],
extensionRule: false,
layout: false
},
schema: [{
type: "object",
properties: {
extension: { enum: ["yaml", "yml"] },
caseSensitive: { type: "boolean" }
},
additionalProperties: false
}],
messages: { unexpected: `Expected extension '{{expected}}' but used extension '{{actual}}'.` },
type: "suggestion"
},
create(context) {
if (!context.sourceCode.parserServices?.isYAML) return {};
const expected = context.options[0]?.extension || "yaml";
const caseSensitive = context.options[0]?.caseSensitive ?? true;
return { Program(node) {
const filename = context.filename;
const actual = path.extname(filename);
if ((caseSensitive ? actual : actual.toLocaleLowerCase()) === `.${expected}`) return;
context.report({
node,
loc: node.loc.start,
messageId: "unexpected",
data: {
expected: `.${expected}`,
actual
}
});
} };
}
});
//#endregion
//#region src/rules/flow-mapping-curly-newline.ts
const OPTION_VALUE = { oneOf: [{ enum: ["always", "never"] }, {
type: "object",
properties: {
multiline: { type: "boolean" },
minProperties: {
type: "integer",
minimum: 0
},
consistent: { type: "boolean" }
},
additionalProperties: false,
minProperties: 1
}] };
/**
* Normalizes a given option value.
*/
function normalizeOptionValue(value) {
let multiline = false;
let minProperties = Number.POSITIVE_INFINITY;
let consistent = false;
if (value) if (value === "always") minProperties = 0;
else if (value === "never") minProperties = Number.POSITIVE_INFINITY;
else {
multiline = Boolean(value.multiline);
minProperties = value.minProperties || Number.POSITIVE_INFINITY;
consistent = Boolean(value.consistent);
}
else consistent = true;
return {
multiline,
minProperties,
consistent
};
}
/**
* Determines if ObjectExpression, ObjectPattern, ImportDeclaration or ExportNamedDeclaration
* node needs to be checked for missing line breaks
* @param {ASTNode} node Node under inspection
* @param {Object} options option specific to node type
* @param {Token} first First object property
* @param {Token} last Last object property
* @returns {boolean} `true` if node needs to be checked for missing line breaks
*/
function areLineBreaksRequired(node, options, first, last) {
const objectProperties = node.pairs;
return objectProperties.length >= options.minProperties || options.multiline && objectProperties.length > 0 && first.loc.start.line !== last.loc.end.line;
}
var flow_mapping_curly_newline_default = createRule("flow-mapping-curly-newline", {
meta: {
docs: {
description: "enforce consistent line breaks inside braces",
categories: ["standard"],
extensionRule: "object-curly-newline",
layout: true
},
fixable: "whitespace",
schema: [OPTION_VALUE],
messages: {
unexpectedLinebreakBeforeClosingBrace: "Unexpected line break before this closing brace.",
unexpectedLinebreakAfterOpeningBrace: "Unexpected line break after this opening brace.",
expectedLinebreakBeforeClosingBrace: "Expected a line break before this closing brace.",
expectedLinebreakAfterOpeningBrace: "Expected a line break after this opening brace."
},
type: "layout"
},
create(context) {
const sourceCode = context.sourceCode;
if (!sourceCode.parserServices?.isYAML) return {};
const options = normalizeOptionValue(context.options[0]);
/**
* Reports a given node if it violated this rule.
* @param {ASTNode} node A node to check. This is an ObjectExpression, ObjectPattern, ImportDeclaration or ExportNamedDeclaration node.
* @returns {void}
*/
function check(node) {
if (isKeyNode(node)) return;
const openBrace = sourceCode.getFirstToken(node, (token) => token.value === "{");
const closeBrace = sourceCode.getLastToken(node, (token) => token.value === "}");
let first = sourceCode.getTokenAfter(openBrace, { includeComments: true });
let last = sourceCode.getTokenBefore(closeBrace, { includeComments: true });
const needsLineBreaks = areLineBreaksRequired(node, options, first, last);
const hasCommentsFirstToken = isCommentToken(first);
const hasCommentsLastToken = isCommentToken(last);
const hasQuestionsLastToken = isQuestion(last);
first = sourceCode.getTokenAfter(openBrace);
last = sourceCode.getTokenBefore(closeBrace);
if (needsLineBreaks) {
if (isTokenOnSameLine(openBrace, first)) context.report({
messageId: "expectedLinebreakAfterOpeningBrace",
node,
loc: openBrace.loc,
fix(fixer) {
if (hasCommentsFirstToken || hasTabIndent(context)) return null;
const indent = incIndent(getActualIndentFromLine(openBrace.loc.start.line, context), context);
return fixer.insertTextAfter(openBrace, `\n${indent}`);
}
});
if (isTokenOnSameLine(last, closeBrace)) context.report({
messageId: "expectedLinebreakBeforeClosingBrace",
node,
loc: closeBrace.loc,
fix(fixer) {
if (hasCommentsLastToken || hasTabIndent(context)) return null;
const indent = getActualIndentFromLine(closeBrace.loc.start.line, context);
return fixer.insertTextBefore(closeBrace, `\n${indent}`);
}
});
} else {
const consistent = options.consistent;
const hasLineBreakBetweenOpenBraceAndFirst = !isTokenOnSameLine(openBrace, first);
const hasLineBreakBetweenCloseBraceAndLast = !isTokenOnSameLine(last, closeBrace);
if (!consistent && hasLineBreakBetweenOpenBraceAndFirst || consistent && hasLineBreakBetweenOpenBraceAndFirst && !hasLineBreakBetweenCloseBraceAndLast) context.report({
messageId: "unexpectedLinebreakAfterOpeningBrace",
node,
loc: openBrace.loc,
fix(fixer) {
if (hasCommentsFirstToken || hasTabIndent(context)) return null;
return fixer.removeRange([openBrace.range[1], first.range[0]]);
}
});
if (!consistent && hasLineBreakBetweenCloseBraceAndLast || consistent && !hasLineBreakBetweenOpenBraceAndFirst && hasLineBreakBetweenCloseBraceAndLast) {
if (hasQuestionsLastToken) return;
context.report({
messageId: "unexpectedLinebreakBeforeClosingBrace",
node,
loc: closeBrace.loc,
fix(fixer) {
if (hasCommentsLastToken || hasTabIndent(context)) return null;
return fixer.removeRange([last.range[1], closeBrace.range[0]]);
}
});
}
}
}
return { YAMLMapping(node) {
if (node.style === "flow") check(node);
} };
}
});
//#endregion
//#region src/rules/flow-mapping-curly-spacing.ts
/**
* Parse rule options and return helpers for spacing checks.
* @param options The options tuple from the rule configuration.
* @param sourceCode The sourceCode object for node lookup.
*/
function parseOptions$3(options, sourceCode) {
const spaced = options[0] ?? "never";
/**
* Determines whether an exception option is set relative to the base spacing.
* @param option The option to check.
*/
function isOptionSet(option) {
return options[1] ? options[1][option] === (spaced === "never") : false;
}
const arraysInObjectsException = isOptionSet("arraysInObjects");
const objectsInObjectsException = isOptionSet("objectsInObjects");
const emptyObjects = options[1]?.emptyObjects ?? "ignore";
/**
* Whether the opening brace must be spaced, considering exceptions.
* @param spaced The primary spaced option string.
* @param second The token after the opening brace.
*/
function isOpeningCurlyBraceMustBeSpaced(spaced, second) {
const targetPenultimateType = arraysInObjectsException && isOpeningBracketToken(second) ? "YAMLSequence" : objectsInObjectsException && isOpeningBraceToken(second) ? "YAMLMapping" : null;
const node = sourceCode.getNodeByRangeIndex(second.range[0]);
return targetPenultimateType && node?.type === targetPenultimateType ? spaced === "never" : spaced === "always";
}
/**
* Whether the closing brace must be spaced, considering exceptions.
* @param spaced The primary spaced option string.
* @param penultimate The token before the closing brace.
*/
function isClosingCurlyBraceMustBeSpaced(spaced, penultimate) {
const targetPenultimateType = arraysInObjectsException && isClosingBracketToken(penultimate) ? "YAMLSequence" : objectsInObjectsException && isClosingBraceToken(penultimate) ? "YAMLMapping" : null;
const node = sourceCode.getNodeByRangeIndex(penultimate.range[0]);
return targetPenultimateType && node?.type === targetPenultimateType ? spaced === "never" : spaced === "always";
}
return {
spaced,
emptyObjects,
isOpeningCurlyBraceMustBeSpaced,
isClosingCurlyBraceMustBeSpaced
};
}
var flow_mapping_curly_spacing_default = createRule("flow-mapping-curly-spacing", {
meta: {
docs: {
description: "enforce consistent spacing inside braces",
categories: ["standard"],
extensionRule: "object-curly-spacing",
layout: true
},
type: "layout",
fixable: "whitespace",
schema: [{
type: "string",
enum: ["always", "never"]
}, {
type: "object",
properties: {
arraysInObjects: { type: "boolean" },
objectsInObjects: { type: "boolean" },
emptyObjects: {
type: "string",
enum: [
"ignore",
"always",
"never"
]
}
},
additionalProperties: false
}],
messages: {
requireSpaceBefore: "A space is required before '{{token}}'.",
requireSpaceAfter: "A space is required after '{{token}}'.",
unexpectedSpaceBefore: "There should be no space before '{{token}}'.",
unexpectedSpaceAfter: "There should be no space after '{{token}}'.",
requiredSpaceInEmptyObject: "A space is required in empty flow mapping.",
unexpectedSpaceInEmptyObject: "There should be no space in empty flow mapping."
}
},
create(context) {
const sourceCode = context.sourceCode;
if (!sourceCode.parserServices?.isYAML) return {};
const options = parseOptions$3(context.options, sourceCode);
/**
* Reports that there shouldn't be a space after the first token
* @param node The node to report in the event of an error.
* @param token The token to use for the report.
*/
function reportNoBeginningSpace(node, token) {
const nextToken = sourceCode.getTokenAfter(token, { includeComments: true });
context.report({
node,
loc: {
start: token.loc.end,
end: nextToken.loc.start
},
messageId: "unexpectedSpaceAfter",
data: { token: token.value },
fix(fixer) {
return fixer.removeRange([token.range[1], nextToken.range[0]]);
}
});
}
/**
* Reports that there shouldn't be a space before the last token
* @param node The node to report in the event of an error.
* @param token The token to use for the report.
*/
function reportNoEndingSpace(node, token) {
const previousToken = sourceCode.getTokenBefore(token, { includeComments: true });
context.report({
node,
loc: {
start: previousToken.loc.end,
end: token.loc.start
},
messageId: "unexpectedSpaceBefore",
data: { token: token.value },
fix(fixer) {
return fixer.removeRange([previousToken.range[1], token.range[0]]);
}
});
}
/**
* Reports that there should be a space after the first token
* @param node The node to report in the event of an error.
* @param token The token to use for the report.
*/
function reportRequiredBeginningSpace(node, token) {
context.report({
node,
loc: token.loc,
messageId: "requireSpaceAfter",
data: { token: token.value },
fix(fixer) {
return fixer.insertTextAfter(token, " ");
}
});
}
/**
* Reports that there should be a space before the last token
* @param node The node to report in the event of an error.
* @param token The token to use for the report.
*/
function reportRequiredEndingSpace(node, token) {
context.report({
node,
loc: token.loc,
messageId: "requireSpaceBefore",
data: { token: token.value },
fix(fixer) {
return fixer.insertTextBefore(token, " ");
}
});
}
/**
* Determines if spacing in curly braces is valid.
* @param node The AST node to check.
* @param first The first token to check (should be the opening brace)
* @param second The second token to check (should be first after the opening brace)
* @param penultimate The penultimate token to check (should be last before closing brace)
* @param last The last token to check (should be closing brace)
*/
function validateBraceSpacing(node, spaced, openingToken, second, penultimate, closingToken) {
if (isTokenOnSameLine(openingToken, second)) {
const firstSpaced = sourceCode.isSpaceBetween(openingToken, second);
if (options.isOpeningCurlyBraceMustBeSpaced(spaced, second)) {
if (!firstSpaced) reportRequiredBeginningSpace(node, openingToken);
} else if (firstSpaced && second.type !== "Line") reportNoBeginningSpace(node, openingToken);
}
if (isTokenOnSameLine(penultimate, closingToken)) {
const lastSpaced = sourceCode.isSpaceBetween(penultimate, closingToken);
if (options.isClosingCurlyBraceMustBeSpaced(spaced, penultimate)) {
if (!lastSpaced) reportRequiredEndingSpace(node, closingToken);
} else if (lastSpaced) reportNoEndingSpace(node, closingToken);
}
}
/**
* Gets '}' token of an object node.
*
* Because the last token of object patterns might be a type annotation,
* this traverses tokens preceded by the last property, then returns the
* first '}' token.
* @param node The node to get. This node is an
* ObjectExpression or an ObjectPattern. And this node has one or
* more properties.
* @returns '}' token.
*/
function getClosingBraceOfObject(node) {
const lastProperty = node.pairs[node.pairs.length - 1];
return sourceCode.getTokenAfter(lastProperty, isClosingBraceToken);
}
/**
* Reports a given object node if spacing in curly braces is invalid.
* @param node An ObjectExpression or ObjectPattern node to check.
*/
function checkSpaceInEmptyObject(node) {
if (options.emptyObjects === "ignore") return;
const openingToken = sourceCode.getFirstToken(node);
const closingToken = sourceCode.getLastToken(node);
const second = sourceCode.getTokenAfter(openingToken, { includeComments: true });
if (second !== closingToken && isCommentToken(second)) {
const penultimate = sourceCode.getTokenBefore(closingToken, { includeComments: true });
validateBraceSpacing(node, options.emptyObjects, openingToken, second, penultimate, closingToken);
return;
}
if (!isTokenOnSameLine(openingToken, closingToken)) return;
const sourceBetween = sourceCode.text.slice(openingToken.range[1], closingToken.range[0]);
if (sourceBetween.trim() !== "") return;
if (options.emptyObjects === "always") {
if (sourceBetween) return;
context.report({
node,
loc: {
start: openingToken.loc.end,
end: closingToken.loc.start
},
messageId: "requiredSpaceInEmptyObject",