UNPKG

eslint-plugin-yml

Version:

This ESLint plugin provides linting rules for YAML.

1,382 lines (1,381 loc) 190 kB
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",