UNPKG

eslint-plugin-toml

Version:

This ESLint plugin provides linting rules for TOML.

1,375 lines 124 kB
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs"; import * as tomlESLintParser from "toml-eslint-parser"; import { VisitorKeys, getStaticTOMLValue, parseTOML, traverseNodes } from "toml-eslint-parser"; import path from "node:path"; 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-toml/rules/${ruleName}.html`, ruleId: `toml/${ruleName}`, ruleName } }, create(context) { const parserServices = context.sourceCode.parserServices || {}; if (typeof parserServices.defineCustomBlocksVisitor === "function" && path.extname(context.filename) === ".vue") return parserServices.defineCustomBlocksVisitor(context, tomlESLintParser, { target: ["toml", "toml"], create(blockContext) { return rule.create(blockContext, { customBlock: true }); } }); return rule.create(context, { customBlock: false }); } }; } //#endregion //#region src/utils/ast-utils.ts const LINEBREAK_MATCHER = /\r\n|[\n\r\u2028\u2029]/u; /** * 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"); } /** * Checks if the given token is a comma token or not. * @param token The token to check. * @returns `true` if the token is a comma token. */ function isCommaToken(token) { return token != null && token.value === "," && token.type === "Punctuator"; } /** * Check whether the given token is a equal sign. * @param token The token to check. * @returns `true` if the token is a equal sign. */ function isEqualSign(token) { return token != null && token.type === "Punctuator" && token.value === "="; } /** * Checks if the given token is a closing parenthesis token or not. * @param token The token to check. * @returns `true` if the token is a closing parenthesis token. */ function isClosingParenToken(token) { return token != null && token.value === ")" && token.type === "Punctuator"; } const isNotClosingParenToken = negate(isClosingParenToken); /** * 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 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"; } /** * Determines whether two adjacent tokens are on the same line. * @param left The left token object. * @param right The right token object. * @returns Whether or not the tokens are on the same line. * @public */ function isTokenOnSameLine(left, right) { return left?.loc?.end.line === right?.loc?.start.line; } /** * Creates the negate function of the given function. * @param f The function to negate. * @returns Negated function. */ function negate(f) { return ((token) => !f(token)); } //#endregion //#region src/rules/array-bracket-newline.ts var array_bracket_newline_default = createRule("array-bracket-newline", { meta: { docs: { description: "enforce linebreaks after opening and before closing array brackets", categories: ["standard"], extensionRule: "array-bracket-newline" }, type: "layout", fixable: "whitespace", schema: [{ oneOf: [{ type: "string", enum: [ "always", "never", "consistent" ] }, { type: "object", properties: { multiline: { type: "boolean" }, minItems: { type: ["integer", "null"], minimum: 0 } }, additionalProperties: false }] }], messages: { unexpectedOpeningLinebreak: "There should be no linebreak after '['.", unexpectedClosingLinebreak: "There should be no linebreak before ']'.", missingOpeningLinebreak: "A linebreak is required after '['.", missingClosingLinebreak: "A linebreak is required before ']'." } }, create(context) { const sourceCode = context.sourceCode; if (!sourceCode.parserServices?.isTOML) return {}; /** * Normalizes a given option value. * @param option An option value to parse. * @returns Normalized option object. */ function normalizeOptionValue(option) { let consistent = false; let multiline = false; let minItems = 0; if (option) if (option === "consistent") { consistent = true; minItems = Number.POSITIVE_INFINITY; } else if (option === "always" || typeof option !== "string" && option.minItems === 0) minItems = 0; else if (option === "never") minItems = Number.POSITIVE_INFINITY; else { multiline = Boolean(option.multiline); minItems = option.minItems || Number.POSITIVE_INFINITY; } else { consistent = false; multiline = true; minItems = Number.POSITIVE_INFINITY; } return { consistent, multiline, minItems }; } /** * Normalizes a given option value. * @param options An option value to parse. * @returns Normalized option object. */ function normalizeOptions(options) { return { TOMLArray: normalizeOptionValue(options) }; } /** * Reports that there shouldn't be a linebreak 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 reportNoBeginningLinebreak(node, token) { context.report({ node, loc: token.loc, messageId: "unexpectedOpeningLinebreak", fix(fixer) { const nextToken = sourceCode.getTokenAfter(token, { includeComments: true }); if (!nextToken || isCommentToken(nextToken)) return null; return fixer.removeRange([token.range[1], nextToken.range[0]]); } }); } /** * Reports that there shouldn't be a linebreak 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 reportNoEndingLinebreak(node, token) { context.report({ node, loc: token.loc, messageId: "unexpectedClosingLinebreak", fix(fixer) { const previousToken = sourceCode.getTokenBefore(token, { includeComments: true }); if (!previousToken || isCommentToken(previousToken)) return null; return fixer.removeRange([previousToken.range[1], token.range[0]]); } }); } /** * Reports that there should be a linebreak 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 reportRequiredBeginningLinebreak(node, token) { context.report({ node, loc: token.loc, messageId: "missingOpeningLinebreak", fix(fixer) { return fixer.insertTextAfter(token, "\n"); } }); } /** * Reports that there should be a linebreak 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 reportRequiredEndingLinebreak(node, token) { context.report({ node, loc: token.loc, messageId: "missingClosingLinebreak", fix(fixer) { return fixer.insertTextBefore(token, "\n"); } }); } /** * Reports a given node if it violated this rule. * @param node A node to check. This is an ArrayExpression node or an ArrayPattern node. */ function check(node) { const elements = node.elements; const options = normalizeOptions(context.options[0])[node.type]; const openBracket = sourceCode.getFirstToken(node); const closeBracket = sourceCode.getLastToken(node); const firstIncComment = sourceCode.getTokenAfter(openBracket, { includeComments: true }); const lastIncComment = sourceCode.getTokenBefore(closeBracket, { includeComments: true }); const first = sourceCode.getTokenAfter(openBracket); const last = sourceCode.getTokenBefore(closeBracket); /** * Use tokens or comments to check multiline or not. * But use only tokens to check whether linebreaks are needed. * This allows: * var arr = [ // eslint-disable-line foo * 'a' * ] */ if (elements.length >= options.minItems || options.multiline && elements.length > 0 && firstIncComment.loc.start.line !== lastIncComment.loc.end.line || elements.length === 0 && firstIncComment.type === "Block" && firstIncComment.loc.start.line !== lastIncComment.loc.end.line && firstIncComment === lastIncComment || options.consistent && openBracket.loc.end.line !== first.loc.start.line) { if (isTokenOnSameLine(openBracket, first)) reportRequiredBeginningLinebreak(node, openBracket); if (isTokenOnSameLine(last, closeBracket)) reportRequiredEndingLinebreak(node, closeBracket); } else { if (!isTokenOnSameLine(openBracket, first)) reportNoBeginningLinebreak(node, openBracket); if (!isTokenOnSameLine(last, closeBracket)) reportNoEndingLinebreak(node, closeBracket); } } return { TOMLArray: check }; } }); //#endregion //#region src/rules/array-bracket-spacing.ts var array_bracket_spacing_default = createRule("array-bracket-spacing", { meta: { docs: { description: "enforce consistent spacing inside array brackets", categories: ["standard"], extensionRule: "array-bracket-spacing" }, type: "layout", fixable: "whitespace", schema: [{ type: "string", enum: ["always", "never"] }, { type: "object", properties: { singleValue: { type: "boolean" }, objectsInArrays: { type: "boolean" }, arraysInArrays: { type: "boolean" } }, additionalProperties: false }], messages: { unexpectedSpaceAfter: "There should be no space after '{{tokenValue}}'.", unexpectedSpaceBefore: "There should be no space before '{{tokenValue}}'.", missingSpaceAfter: "A space is required after '{{tokenValue}}'.", missingSpaceBefore: "A space is required before '{{tokenValue}}'." } }, create(context) { const sourceCode = context.sourceCode; if (!sourceCode.parserServices?.isTOML) return {}; const spaced = (context.options[0] || "always") === "always"; /** * Determines whether an option is set, relative to the spacing option. * If spaced is "always", then check whether option is set to false. * If spaced is "never", then check whether option is set to true. * @param option The option to exclude. * @returns Whether or not the property is excluded. */ function isOptionSet(option) { return context.options[1] ? context.options[1][option] === !spaced : false; } const options = { spaced, singleElementException: isOptionSet("singleValue"), objectsInArraysException: isOptionSet("objectsInArrays"), arraysInArraysException: isOptionSet("arraysInArrays"), isOpeningBracketMustBeSpaced(node) { if (options.singleElementException && node.elements.length === 1) return !options.spaced; const firstElement = node.elements[0]; return firstElement && (options.objectsInArraysException && isObjectType(firstElement) || options.arraysInArraysException && isArrayType(firstElement)) ? !options.spaced : options.spaced; }, isClosingBracketMustBeSpaced(node) { if (options.singleElementException && node.elements.length === 1) return !options.spaced; const lastElement = node.elements[node.elements.length - 1]; return lastElement && (options.objectsInArraysException && isObjectType(lastElement) || options.arraysInArraysException && isArrayType(lastElement)) ? !options.spaced : options.spaced; } }; /** * 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); context.report({ node, loc: { start: token.loc.end, end: nextToken.loc.start }, messageId: "unexpectedSpaceAfter", data: { tokenValue: 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); context.report({ node, loc: { start: previousToken.loc.end, end: token.loc.start }, messageId: "unexpectedSpaceBefore", data: { tokenValue: 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: "missingSpaceAfter", data: { tokenValue: 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: "missingSpaceBefore", data: { tokenValue: token.value }, fix(fixer) { return fixer.insertTextBefore(token, " "); } }); } /** * Determines if a node is an object type * @param node The node to check. * @returns Whether or not the node is an object type. */ function isObjectType(node) { return node && node.type === "TOMLInlineTable"; } /** * Determines if a node is an array type * @param node The node to check. * @returns Whether or not the node is an array type. */ function isArrayType(node) { return node && node.type === "TOMLArray"; } /** * Validates the spacing around array brackets * @param node The node we're checking for spacing */ function validateArraySpacing(node) { if (options.spaced && node.elements.length === 0) return; const first = sourceCode.getFirstToken(node); const last = sourceCode.getLastToken(node); const second = sourceCode.getTokenAfter(first, { includeComments: true }); const penultimate = sourceCode.getTokenBefore(last, { includeComments: true }); if (isTokenOnSameLine(first, second)) { if (options.isOpeningBracketMustBeSpaced(node)) { if (!sourceCode.isSpaceBetween(first, second)) reportRequiredBeginningSpace(node, first); } else if (sourceCode.isSpaceBetween(first, second)) reportNoBeginningSpace(node, first); } if (first !== penultimate && isTokenOnSameLine(penultimate, last)) { if (options.isClosingBracketMustBeSpaced(node)) { if (!sourceCode.isSpaceBetween(penultimate, last)) reportRequiredEndingSpace(node, last); } else if (sourceCode.isSpaceBetween(penultimate, last)) reportNoEndingSpace(node, last); } } return { TOMLArray: validateArraySpacing }; } }); //#endregion //#region src/rules/array-element-newline.ts var array_element_newline_default = createRule("array-element-newline", { meta: { docs: { description: "enforce line breaks between array elements", categories: ["standard"], extensionRule: "array-element-newline" }, type: "layout", fixable: "whitespace", schema: { definitions: { basicConfig: { oneOf: [{ type: "string", enum: [ "always", "never", "consistent" ] }, { type: "object", properties: { multiline: { type: "boolean" }, minItems: { type: ["integer", "null"], minimum: 0 } }, additionalProperties: false }] } }, type: "array", items: [{ oneOf: [{ $ref: "#/definitions/basicConfig" }, { type: "object", properties: { ArrayExpression: { $ref: "#/definitions/basicConfig" }, ArrayPattern: { $ref: "#/definitions/basicConfig" }, TOMLArray: { $ref: "#/definitions/basicConfig" } }, additionalProperties: false, minProperties: 1 }] }] }, messages: { unexpectedLineBreak: "There should be no linebreak here.", missingLineBreak: "There should be a linebreak after this element." } }, create(context) { const sourceCode = context.sourceCode; if (!sourceCode.parserServices?.isTOML) return {}; /** * Normalizes a given option value. * @param providedOption An option value to parse. * @returns Normalized option object. */ function normalizeOptionValue(providedOption) { let consistent = false; let multiline = false; let minItems; const option = providedOption || "always"; if (!option || option === "always" || typeof option === "object" && option.minItems === 0) minItems = 0; else if (option === "never") minItems = Number.POSITIVE_INFINITY; else if (option === "consistent") { consistent = true; minItems = Number.POSITIVE_INFINITY; } else { multiline = Boolean(option.multiline); minItems = option.minItems || Number.POSITIVE_INFINITY; } return { consistent, multiline, minItems }; } /** * Normalizes a given option value. * @param options An option value to parse. * @returns Normalized option object. */ function normalizeOptions(options) { if (options && (options.ArrayExpression || options.TOMLArray)) { let expressionOptions; if (options.ArrayExpression) expressionOptions = normalizeOptionValue(options.ArrayExpression); if (options.TOMLArray) expressionOptions = normalizeOptionValue(options.TOMLArray); return { TOMLArray: expressionOptions }; } return { TOMLArray: normalizeOptionValue(options) }; } /** * Reports that there shouldn't be a line break after the first token * @param token The token to use for the report. */ function reportNoLineBreak(token) { const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true }); context.report({ loc: { start: tokenBefore.loc.end, end: token.loc.start }, messageId: "unexpectedLineBreak", fix(fixer) { if (isCommentToken(tokenBefore)) return null; if (!isTokenOnSameLine(tokenBefore, token)) return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], " "); /** * This will check if the comma is on the same line as the next element * Following array: * [ * 1 * , 2 * , 3 * ] * * will be fixed to: * [ * 1, 2, 3 * ] */ const twoTokensBefore = sourceCode.getTokenBefore(tokenBefore, { includeComments: true }); if (isCommentToken(twoTokensBefore)) return null; return fixer.replaceTextRange([twoTokensBefore.range[1], tokenBefore.range[0]], ""); } }); } /** * Reports that there should be a line break after the first token * @param token The token to use for the report. */ function reportRequiredLineBreak(token) { const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true }); context.report({ loc: { start: tokenBefore.loc.end, end: token.loc.start }, messageId: "missingLineBreak", fix(fixer) { return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], "\n"); } }); } /** * Reports a given node if it violated this rule. * @param node A node to check. This is an ObjectExpression node or an ObjectPattern node. */ function check(node) { const elements = node.elements; const options = normalizeOptions(context.options[0])[node.type]; if (!options) return; let elementBreak = false; /** * MULTILINE: true * loop through every element and check * if at least one element has linebreaks inside * this ensures that following is not valid (due to elements are on the same line): * * [ * 1, * 2, * 3 * ] */ if (options.multiline) elementBreak = elements.filter((element) => element !== null).some((element) => element.loc.start.line !== element.loc.end.line); let linebreaksCount = 0; for (let i = 0; i < node.elements.length; i++) { const element = node.elements[i]; const previousElement = elements[i - 1]; if (i === 0 || element === null || previousElement === null) continue; const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, isCommaToken); if (!isTokenOnSameLine(sourceCode.getTokenBefore(commaToken), sourceCode.getTokenAfter(commaToken))) linebreaksCount++; } const needsLinebreaks = elements.length >= options.minItems || options.multiline && elementBreak || options.consistent && linebreaksCount > 0 && linebreaksCount < node.elements.length; elements.forEach((element, i) => { const previousElement = elements[i - 1]; if (i === 0 || element === null || previousElement === null) return; const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, isCommaToken); const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken); const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken); if (needsLinebreaks) { if (isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) reportRequiredLineBreak(firstTokenOfCurrentElement); } else if (!isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) reportNoLineBreak(firstTokenOfCurrentElement); }); } return { TOMLArray: check }; } }); //#endregion //#region src/rules/comma-style.ts var comma_style_default = createRule("comma-style", { meta: { docs: { description: "enforce consistent comma style in array", categories: ["standard"], extensionRule: "comma-style" }, type: "layout", fixable: "code", schema: [{ type: "string", enum: ["first", "last"] }, { type: "object", properties: { exceptions: { type: "object", additionalProperties: { type: "boolean" } } }, additionalProperties: false }], messages: { unexpectedLineBeforeAndAfterComma: "Bad line breaking before and after ','.", expectedCommaFirst: "',' should be placed first.", expectedCommaLast: "',' should be placed last." } }, create(context) { const sourceCode = context.sourceCode; if (!sourceCode.parserServices?.isTOML) return {}; const style = context.options[0] || "last"; const exceptions = {}; if (context.options.length === 2 && Object.prototype.hasOwnProperty.call(context.options[1], "exceptions")) { context.options[1] ??= { exceptions: {} }; const rawExceptions = context.options[1].exceptions; const keys = Object.keys(rawExceptions); for (let i = 0; i < keys.length; i++) exceptions[keys[i]] = rawExceptions[keys[i]]; } /** * Modified text based on the style * @param styleType Style type * @param text Source code text * @returns modified text * @private */ function getReplacedText(styleType, text) { switch (styleType) { case "between": return `,${text.replace(LINEBREAK_MATCHER, "")}`; case "first": return `${text},`; case "last": return `,${text}`; default: return ""; } } /** * Determines the fixer function for a given style. * @param styleType comma style * @param previousItemToken The token to check. * @param commaToken The token to check. * @param currentItemToken The token to check. * @returns Fixer function * @private */ function getFixerFunction(styleType, previousItemToken, commaToken, currentItemToken) { const text = sourceCode.text.slice(previousItemToken.range[1], commaToken.range[0]) + sourceCode.text.slice(commaToken.range[1], currentItemToken.range[0]); const range = [previousItemToken.range[1], currentItemToken.range[0]]; return function(fixer) { return fixer.replaceTextRange(range, getReplacedText(styleType, text)); }; } /** * Validates the spacing around single items in lists. * @param previousItemToken The last token from the previous item. * @param commaToken The token representing the comma. * @param currentItemToken The first token of the current item. * @param reportItem The item to use when reporting an error. * @private */ function validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem) { if (isTokenOnSameLine(commaToken, currentItemToken) && isTokenOnSameLine(previousItemToken, commaToken)) {} else if (!isTokenOnSameLine(commaToken, currentItemToken) && !isTokenOnSameLine(previousItemToken, commaToken)) { const comment = sourceCode.getCommentsAfter(commaToken)[0]; const styleType = comment && comment.type === "Block" && isTokenOnSameLine(commaToken, comment) ? style : "between"; context.report({ node: reportItem, loc: commaToken.loc, messageId: "unexpectedLineBeforeAndAfterComma", fix: getFixerFunction(styleType, previousItemToken, commaToken, currentItemToken) }); } else if (style === "first" && !isTokenOnSameLine(commaToken, currentItemToken)) context.report({ node: reportItem, loc: commaToken.loc, messageId: "expectedCommaFirst", fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken) }); else if (style === "last" && isTokenOnSameLine(commaToken, currentItemToken)) context.report({ node: reportItem, loc: commaToken.loc, messageId: "expectedCommaLast", fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken) }); } /** * Checks the comma placement with regards to a declaration/property/element * @param node The binary expression node to check * @param property The property of the node containing child nodes. * @private */ function validateComma(node, property) { const items = node[property]; const arrayLiteral = node.type === "TOMLArray"; if (items.length > 1 || arrayLiteral) { let previousItemToken = sourceCode.getFirstToken(node); items.forEach((item) => { const commaToken = item ? sourceCode.getTokenBefore(item) : previousItemToken; const currentItemToken = item ? sourceCode.getFirstToken(item) : sourceCode.getTokenAfter(commaToken); const reportItem = item || currentItemToken; /** * This works by comparing three token locations: * - previousItemToken is the last token of the previous item * - commaToken is the location of the comma before the current item * - currentItemToken is the first token of the current item * * These values get switched around if item is undefined. * previousItemToken will refer to the last token not belonging * to the current item, which could be a comma or an opening * square bracket. currentItemToken could be a comma. * * All comparisons are done based on these tokens directly, so * they are always valid regardless of an undefined item. */ if (isCommaToken(commaToken)) validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem); if (item) { const tokenAfterItem = sourceCode.getTokenAfter(item, isNotClosingParenToken); previousItemToken = tokenAfterItem ? sourceCode.getTokenBefore(tokenAfterItem) : sourceCode.ast.tokens[sourceCode.ast.tokens.length - 1]; } else previousItemToken = currentItemToken; }); /** * Special case for array literals that have empty last items, such * as [ 1, 2, ]. These arrays only have two items show up in the * AST, so we need to look at the token to verify that there's no * dangling comma. */ if (arrayLiteral) { const lastToken = sourceCode.getLastToken(node); const nextToLastToken = sourceCode.getTokenBefore(lastToken); if (isCommaToken(nextToLastToken)) validateCommaItemSpacing(sourceCode.getTokenBefore(nextToLastToken), nextToLastToken, lastToken, lastToken); } } } const nodes = {}; if (!exceptions.ObjectExpression && !exceptions.TOMLInlineTable) nodes.TOMLInlineTable = function(node) { validateComma(node, "body"); }; if (!exceptions.ArrayExpression && !exceptions.TOMLArray) nodes.TOMLArray = function(node) { validateComma(node, "elements"); }; return nodes; } }); //#endregion //#region src/rules/indent.ts const ITERATION_OPTS = Object.freeze({ includeComments: true }); /** * Create get indentation function */ function buildIndentUtility(optionValue) { const indent = optionValue ?? 2; const textIndent = typeof indent === "number" ? " ".repeat(indent) : " "; return { getIndentText: (offset) => offset === 1 ? textIndent : textIndent.repeat(offset), outdent(indent) { return indent.slice(0, -textIndent.length); } }; } var indent_default = createRule("indent", { meta: { docs: { description: "enforce consistent indentation", categories: ["standard"], extensionRule: false }, fixable: "whitespace", schema: [{ oneOf: [{ enum: ["tab"] }, { type: "integer", minimum: 0 }] }, { type: "object", properties: { subTables: { type: "integer", minimum: 0 }, keyValuePairs: { type: "integer", minimum: 0 } }, additionalProperties: false }], messages: { wrongIndentation: "Expected indentation of {{expected}} but found {{actual}}." }, type: "layout" }, create(context) { const sourceCode = context.sourceCode; if (!sourceCode.parserServices?.isTOML) return {}; const { getIndentText, outdent } = buildIndentUtility(context.options[0]); const subTablesOffset = context.options[1]?.subTables ?? 0; const keyValuePairsOffset = context.options[1]?.keyValuePairs ?? 0; const offsets = /* @__PURE__ */ new Map(); /** * Set offset to the given tokens. * @param token The token to set. * @param offset The offset of the tokens. * @param baseToken The token of the base offset. */ function setOffset(token, offset, baseToken) { if (token == null) return; if (Array.isArray(token)) for (const t of token) setOffset(t, offset, baseToken); else offsets.set(token, { baseToken, offset }); } /** * Process the given node list. * @param {(AST.YAMLNode|null)[]} nodeList The node to process. * @param {number} offset The offset to set. * @returns {void} */ function processNodeList(nodeList, left, right, offset) { let lastToken = left; const alignTokens = /* @__PURE__ */ new Set(); for (const node of nodeList) { if (node == null) continue; const elementTokens = { firstToken: sourceCode.getFirstToken(node), lastToken: sourceCode.getLastToken(node) }; let t = lastToken; while ((t = sourceCode.getTokenAfter(t, ITERATION_OPTS)) != null && t.range[1] <= elementTokens.firstToken.range[0]) alignTokens.add(t); alignTokens.add(elementTokens.firstToken); lastToken = elementTokens.lastToken; } if (right != null) { let t = lastToken; while ((t = sourceCode.getTokenAfter(t, ITERATION_OPTS)) != null && t.range[1] <= right.range[0]) alignTokens.add(t); } alignTokens.delete(left); setOffset([...alignTokens], offset, left); if (right != null) setOffset(right, 0, left); } return { TOMLTopLevelTable(node) { const first = sourceCode.getFirstToken(node, ITERATION_OPTS); if (!first) return; const beforeTokens = sourceCode.getTokensBefore(first, ITERATION_OPTS); if (beforeTokens.length) { const firstOfAllTokens = beforeTokens[0]; offsets.set(firstOfAllTokens, { baseToken: null, offset: 0, expectedIndent: "" }); setOffset(beforeTokens.slice(1), 0, firstOfAllTokens); setOffset(first, 0, firstOfAllTokens); } else offsets.set(first, { baseToken: null, offset: 0, expectedIndent: "" }); let tableKeyStack = []; /** Get offset from given table keys */ function getTableOffset(keys) { let last = tableKeyStack.pop(); while (last) { if (last.keys.length && last.keys.length <= keys.length && last.keys.every((k, i) => k === keys[i])) { if (last.keys.length < keys.length) { tableKeyStack.push(last); return last.offset + subTablesOffset; } return last.offset; } last = tableKeyStack.pop(); } return 0; } for (const body of node.body) { const bodyFirstToken = sourceCode.getFirstToken(body); if (body.type === "TOMLKeyValue") { if (bodyFirstToken !== first) setOffset(bodyFirstToken, 0, first); } if (body.type === "TOMLTable") { const keys = getStaticTOMLValue(body.key); const offset = getTableOffset(keys); tableKeyStack.push({ keys, offset }); if (bodyFirstToken !== first) setOffset(bodyFirstToken, offset, first); } else tableKeyStack = []; } }, TOMLTable(node) { const openBracket = sourceCode.getFirstToken(node); if (node.kind === "array") setOffset(sourceCode.getTokenAfter(openBracket), 0, openBracket); setOffset(sourceCode.getFirstToken(node.key), 1, openBracket); const closeBracket = sourceCode.getTokenAfter(node.key); setOffset(closeBracket, 0, openBracket); if (node.kind === "array") setOffset(sourceCode.getTokenAfter(closeBracket), 0, closeBracket); processNodeList(node.body, openBracket, null, keyValuePairsOffset); }, TOMLKeyValue(node) { const keyToken = sourceCode.getFirstToken(node.key); const valueToken = sourceCode.getFirstToken(node.value); const eqToken = sourceCode.getTokenBefore(node.value, isEqualSign); setOffset(eqToken, 1, keyToken); setOffset(valueToken, 1, eqToken); }, TOMLKey(node) { const first = sourceCode.getFirstToken(node, ITERATION_OPTS); processNodeList(node.keys, first, null, 1); }, TOMLValue() {}, TOMLBare() {}, TOMLQuoted() {}, TOMLArray(node) { const openBracket = sourceCode.getFirstToken(node); const closeBracket = sourceCode.getLastToken(node); processNodeList(node.elements, openBracket, closeBracket, 1); }, TOMLInlineTable(node) { const openBrace = sourceCode.getFirstToken(node); const closeBrace = sourceCode.getLastToken(node); processNodeList(node.body, openBrace, closeBrace, 1); }, "Program:exit"(node) { const lineIndentsStep1 = []; let tokensOnSameLine = []; for (const token of sourceCode.getTokens(node, ITERATION_OPTS)) if (tokensOnSameLine.length === 0 || tokensOnSameLine[0].loc.start.line === token.loc.start.line) tokensOnSameLine.push(token); else { const lineIndent = processExpectedIndent(tokensOnSameLine); lineIndentsStep1[lineIndent.line] = lineIndent; tokensOnSameLine = [token]; } if (tokensOnSameLine.length >= 1) { const lineIndent = processExpectedIndent(tokensOnSameLine); lineIndentsStep1[lineIndent.line] = lineIndent; } validateLines(processMissingLines(lineIndentsStep1)); } }; /** * Process the expected indent for given line tokens */ function processExpectedIndent(lineTokens) { const firstToken = lineTokens.shift(); let token = firstToken; const expectedIndent = getExpectedIndent(token); let lineExpectedIndent = expectedIndent; if (lineExpectedIndent == null) while ((token = lineTokens.shift()) != null) { lineExpectedIndent = getExpectedIndent(token); if (lineExpectedIndent != null) break; } if (expectedIndent != null) while ((token = lineTokens.shift()) != null) { const offset = offsets.get(token); if (offset) offset.expectedIndent = expectedIndent; } const { line, column } = firstToken.loc.start; return { expectedIndent: lineExpectedIndent, actualIndent: sourceCode.lines[line - 1].slice(0, column), firstToken, line }; } /** * Get the expected indent from given token */ function getExpectedIndent(token) { const offset = offsets.get(token); if (!offset) return null; if (offset.expectedIndent != null) return offset.expectedIndent; if (offset.baseToken == null) return null; const baseIndent = getExpectedIndent(offset.baseToken); if (baseIndent == null) return null; const offsetIndent = offset.offset; return offset.expectedIndent = baseIndent + getIndentText(offsetIndent); } /** * Calculates the indent for lines with missing indent information. */ function processMissingLines(lineIndents) { const results = []; const commentLines = []; for (const lineIndent of lineIndents) { if (!lineIndent) continue; const line = lineIndent.line; if (isCommentToken(lineIndent.firstToken)) { const last = commentLines[commentLines.length - 1]; if (last && last.range[1] === line - 1) { last.range[1] = line; last.commentLineIndents.push(lineIndent); } else commentLines.push({ range: [line, line], commentLineIndents: [lineIndent] }); } else if (lineIndent.expectedIndent != null) { const indent = { line, expectedIndent: lineIndent.expectedIndent, actualIndent: lineIndent.actualIndent, firstToken: lineIndent.firstToken }; if (!results[line]) results[line] = indent; } } processComments(commentLines); return results; /** * Process comments. */ function processComments(commentLines) { for (const { range, commentLineIndents } of commentLines) { const prev = results.slice(0, range[0]).filter((data) => data).pop(); const next = results.slice(range[1] + 1).filter((data) => data).shift(); const expectedIndents = []; let either; if (prev && next) { expectedIndents.unshift(next.expectedIndent); if (next.expectedIndent < prev.expectedIndent) { let indent = next.expectedIndent + getIndentText(1); while (indent.length <= prev.expectedIndent.length) { expectedIndents.unshift(indent); indent += getIndentText(1); } } } else if (either = prev || next) { expectedIndents.unshift(either.expectedIndent); if (!next) { let indent = outdent(either.expectedIndent); while (indent.length > 0) { expectedIndents.push(indent); indent = outdent(indent); if (indent.length <= 0) { expectedIndents.push(indent); break; } } } } if (!expectedIndents.length) continue; let expectedIndent = expectedIndents[0]; for (const commentLineIndent of commentLineIndents) { if (results[commentLineIndent.line]) continue; const indentCandidate = expectedIndents.find((indent, index) => { if (indent.length <= commentLineIndent.actualIndent.length) return true; return (expectedIndents[index + 1]?.length ?? -1) < commentLineIndent.actualIndent.length && commentLineIndent.actualIndent.length < indent.length; }); if (indentCandidate != null && indentCandidate.length < expectedIndent.length) expectedIndent = indentCandidate; results[commentLineIndent.line] = { line: commentLineIndent.line, expectedIndent, actualIndent: commentLineIndent.actualIndent, firstToken: commentLineIndent.firstToken }; } } } } /** * Validate lines */ function validateLines(lineIndents) { for (const lineIndent of lineIndents) { if (!lineIndent) continue; if (lineIndent.actualIndent !== lineIndent.expectedIndent) { const startLoc = { line: lineIndent.line, column: 0 }; context.report({ loc: { start: startLoc, end: lineIndent.firstToken.loc.start }, messageId: "wrongIndentation", data: getIndentData(lineIndent), fix(fixer) { return fixer.replaceTextRange([sourceCode.getIndexFromLoc(startLoc), lineIndent.firstToken.range[0]], lineIndent.expectedIndent); } }); } } } /** * Gets the indentation information to display. */ function getIndentData(lineIndent) { return { expected: toDisplayText(lineIndent.expectedIndent), actual: toDisplayText(lineIndent.actualIndent) }; /** * indentation to display text */ function toDisplayText(indent) { if (indent.length === 0) return "0 spaces"; const char = indent[0]; if (char === " " || char === " ") { let uni = true; for (const c of indent) if (c !== char) uni = false; if (uni) { const unit = char === " " ? "spaces" : "tabs"; return `${indent.length} ${unit}`; } } return `"${replaceToDisplay(indent)}"`; } /** * Space replacer */ function replaceToDisplay(indent) { return indent.replace(/\s/gu, (c) => { if (c === " ") return "\\t"; if (c === " ") return " "; return `\\u${`000${c.codePointAt(0).toString(16)}`.slice(-4)}`; }); } } } }); //#endregion //#region src/rules/inline-table-curly-newline.ts var inline_table_curly_newline_default = createRule("inline-table-curly-newline", { meta: { docs: { description: "enforce linebreaks after opening and before closing braces", categories: ["standard"], extensionRule: "object-curly-newline" }, type: "layout", fixable: "whitespace", schema: [{ oneOf: [{ type: "string", enum: ["always", "never"] }, { type: "object", properties: { multiline: { type: "boolean" }, minProperties: { type: "integer", minimum: 0 }, consistent: { type: "boolean" } }, additionalProperties: false, minProperties: 1 }] }], 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." } }, create(context) { const sourceCode = context.sourceCode; if (!sourceCode.parserServices?.isTOML) return {}; if (context.languageOptions.parserOptions?.tomlVersion) { const tomlVersion = context.languageOptions.parserOptions.tomlVersion.includes(".") && context.languageOptions.parserOptions.tomlVersion.split("."); if (tomlVersion && tomlVersion[0] === "1" && tomlVersion[1] === "0") return {}; } /** * Normalizes a given option. * @param value An option value to parse. * @returns Normalized option object. */ function normalizeOptions(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 }; } const options = normalizeOptions(context.options[0]); /** * Determines if ObjectExpression, ObjectPattern, ImportDeclaration, ExportNamedDeclaration, TSTypeLiteral or TSInterfaceBody * node needs to be checked for missing line breaks * @param node Node under inspection * @param options option specific to node type * @param first First object property * @param last Last object property * @returns `true` if node needs to be checked for missing line breaks */ function areLineBreaksRequired(node, options, first, last) { const objectProperties = node.body; return objectProperties.length >= options.minProperties || options.multiline && objectProperties.length > 0 && !isTokenOnSameLine(last, first); } /** * Reports a given node if it violated this rule. * @param node A node to check. This is an ObjectExpression, ObjectPattern, ImportDeclaration, ExportNamedDeclaration, TSTypeLiteral or TSInterfaceBody node. */ function check(node) { const openBrace = sourceCode.getFirstToken(node, (token) => token.value === "{"); const closeBrace = sourceCode.getLastToken(node, (token) => token.value === "}"); const firstTokenOrComment = sourceCode.getTokenAfter(openBrace, { includeComments: true }); const lastTokenOrComment = sourceCode.getTokenBefore(closeBrace, { includeComments: true }); const needsLineBreaks = areLineBreaksRequired(node, options, firstTokenOrComment, lastTokenOrComment); const hasCommentsFirstToken = isCommentToken(firstTokenOrComment); const hasCommentsLastToken = isCommentToken(lastTokenOrComment); /** * Use tokens or comments to check multiline or not. * But use only tokens to check whether line breaks are needed. * This allows: * obj = { # eslint-disable-line foo * a: 1 * } */ const firstToken = sourceCode.getTokenAfter(openBrace); const lastToken = sourceCode.getTokenBefore(closeBrace); if (needsLineBreaks) { if (isTokenOnSameLine(openBrace, firstToken)) context.report({ messageId: "expectedLinebreakAfterOpeningBrace", node, loc: openBrace.loc, fix(fixer) { if (hasCommentsFirstToken) return null; return fixer.insertTextAfter(openBrace, "\n"); } }); if (isTokenOnSameLine(lastToken, closeBrace)) context.report({ messageId: "expectedLinebreakBeforeClosingBrace", node, loc: closeBrace.loc, fix(fixer) { if (hasCommentsLastToken) return null; return fixer.insertTextBefore(closeBrace, "\n"); } }); } else { const consistent = options.consistent; const hasLineBreakBetweenOpenBraceAndFirst = !isTokenOnSameLine(openBrace, firstToken); const hasLineBreakBetweenCloseBraceAndLast = !isTokenOnSameLine(lastToken, closeBrace); if (!consistent && hasLineBreakBetweenOpenBraceAndFirst || consistent && hasLineBreakBetweenOpenBraceAndFirst && !hasLineBreakBetweenCloseBraceAndLast) context.report({ messageId: "unexpectedLinebreakAfterOpeningBrace", node, loc: openBrace.loc, fix(fixer) { if (hasCommentsFirstToken) return null; return fixer.removeRange([openBrace.range[1], firstToken.range[0]]); } }); if (!consistent && hasLineBreakBetweenCloseBraceAndLast || consistent && !hasLineBreakBetweenOpenBraceAndFirst && hasLineBreakBetweenCloseBraceAndLast) context.report({ messageId: "unexpectedLinebreakBeforeClosingBrace", node, loc: closeBrace.loc, fix(fixer) { if (hasCommentsLastToken) return null; return fixer.removeRange([lastToken.range[1], closeBrace.range[0]]); } }); } } return { TOMLInlineTable: check }; } }); //#endregion //#region src/rules/inline-table-curly-spacing.ts /** * Parses the options for this rule and returns an object containing the spacing option, * the emptyObjects option, and two functions to determine if there should be spaces * after the opening curly brace and before the closing curly brace, based on the options and the surrounding tokens. * @param options The options passed to the rule. * @param sourceCode The source code object, used to get nodes by range index. * @returns An object containing the spacing option, the emptyObjects option, and two functions to determine if there should be spaces after the opening curly brace and before the closing curly brace. */ function parseOptions$1(options, sourceCode) { const spaced = options[0] ?? "always"; /** * Determines whether an option is set, relative to the spacing option. * If spaced is "always", then check whether option is set to false. * If spaced is "never", then check whether option is set to true. * @param option The option to exclude. * @returns Whether or not the property is excluded. */ 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"; /** * Determines if there should be a space after the opening curly brace, * based on the spacing option and the second token. * @param spaced The spacing option ("always" or "never"). * @param second The second token after the opening curly brace. * @returns Whether or not there should be a space after the opening curly brace. */ function isOpeningCurlyBraceMustBeSpaced(spaced, _second) { return spaced === "always"; } /** * Determines if there should be a space before the closing curly brace, * based on the spacing option and the penultimate token. * @param spaced The spacing option ("always" or "never"). * @param penultimate The penultimate token before the closing curly brace. * @returns Whether or not there should be a space before the closing curly brace. */ function isClosingCurlyBraceMustBeSpaced(spaced, penultimate) { const targetPenultimateType = arraysInObjectsException && isClosingBracketToken(penultimate) ? "TOMLArray" : objectsInObjectsException && isClosingBraceToken(penultimate) ? "TOMLInlineTable" : null; const node = sourceCode.getNodeByRangeIndex(penultimate.range[0]); return targetPenultimateType && node?.type === targetPenultimateType ? spaced === "never" : spaced === "always"; } return { spaced, emptyObjects, isOpeningCurlyBraceMustBeSpaced, isClosingCurlyBraceMustBeSpaced }; } var inline_table_curly_spacing_default = createRule("inline-table-curly-spacing", { meta: { docs: { description: "enforce consistent spacing inside braces", categories: ["standard"], extensionRule: "object-curly-spacing" }, type: