UNPKG

eslint-plugin-yml

Version:

This ESLint plugin provides linting rules for YAML.

763 lines (762 loc) 33.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const index_1 = require("../utils/index"); const yaml_1 = require("../utils/yaml"); const ast_utils_1 = require("../utils/ast-utils"); const compat_1 = require("../utils/compat"); const ITERATION_OPTS = Object.freeze({ includeComments: true, }); function parseOptions(context) { const [indentOption, objectOptions] = context.options; const numOfIndent = (0, yaml_1.getNumOfIndent)(context, indentOption); let indentBlockSequences = true; let indicatorValueIndent = numOfIndent; let alignMultilineFlowScalars = false; if (objectOptions) { if (objectOptions.indentBlockSequences === false) { indentBlockSequences = false; } if (objectOptions.indicatorValueIndent != null) { indicatorValueIndent = objectOptions.indicatorValueIndent; } if (objectOptions.alignMultilineFlowScalars != null) { alignMultilineFlowScalars = objectOptions.alignMultilineFlowScalars; } } return { numOfIndent, indentBlockSequences, indicatorValueIndent, alignMultilineFlowScalars, }; } exports.default = (0, index_1.createRule)("indent", { meta: { docs: { description: "enforce consistent indentation", categories: ["standard"], extensionRule: false, layout: true, }, fixable: "whitespace", schema: [ { type: "integer", minimum: 2, }, { type: "object", properties: { indentBlockSequences: { type: "boolean" }, indicatorValueIndent: { type: "integer", minimum: 2, }, alignMultilineFlowScalars: { type: "boolean", }, }, additionalProperties: false, }, ], messages: { wrongIndentation: "Expected indentation of {{expected}} spaces but found {{actual}} spaces.", }, type: "layout", }, create(context) { var _a; const sourceCode = (0, compat_1.getSourceCode)(context); if (!((_a = sourceCode.parserServices) === null || _a === void 0 ? void 0 : _a.isYAML)) { return {}; } if ((0, yaml_1.hasTabIndent)(context)) { return {}; } const { numOfIndent, indentBlockSequences, indicatorValueIndent, alignMultilineFlowScalars, } = parseOptions(context); const indents = new Map(); const indicators = new Set(); const blockLiteralMarks = new Set(); const scalars = new Map(); function setOffset(token, offset, baseToken, options) { setIndent(token, offset * numOfIndent, baseToken, options && { indentWhenBaseIsNotFirst: options.offsetWhenBaseIsNotFirst && options.offsetWhenBaseIsNotFirst * numOfIndent, }); } function setIndent(token, indent, baseToken, options) { var _a; if (token == null) { return; } if (Array.isArray(token)) { for (const t of token) { setIndent(t, indent, baseToken, options); } } else { indents.set(token, { baseToken, indent, indentWhenBaseIsNotFirst: (_a = options === null || options === void 0 ? void 0 : options.indentWhenBaseIsNotFirst) !== null && _a !== void 0 ? _a : null, }); } } function processNodeList(nodeList, left, right, offset) { let lastToken = left; const alignTokens = 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); } } function calcMappingPairValueIndent(node, indent) { if (indentBlockSequences) { return indent; } if (node.type === "YAMLSequence" && node.style === "block") { return 0; } return indent; } function calcIndicatorValueIndent(token) { return isBeginningToken(token) ? indicatorValueIndent : numOfIndent; } function isBeginningToken(token) { const before = sourceCode.getTokenBefore(token, (t) => !indicators.has(t)); if (!before) return true; return before.loc.end.line < token.loc.start.line; } const documents = []; return { YAMLDocument(node) { documents.push(node); const first = sourceCode.getFirstToken(node, ITERATION_OPTS); if (!first) { return; } indents.set(first, { baseToken: null, indentWhenBaseIsNotFirst: null, indent: 0, expectedIndent: 0, }); processNodeList([...node.directives, node.content], first, null, 0); }, YAMLMapping(node) { if (node.style === "flow") { const open = sourceCode.getFirstToken(node); const close = sourceCode.getLastToken(node); processNodeList(node.pairs, open, close, 1); } else if (node.style === "block") { const first = sourceCode.getFirstToken(node); processNodeList(node.pairs, first, null, 0); } }, YAMLSequence(node) { if (node.style === "flow") { const open = sourceCode.getFirstToken(node); const close = sourceCode.getLastToken(node); processNodeList(node.entries, open, close, 1); } else if (node.style === "block") { const first = sourceCode.getFirstToken(node); processNodeList(node.entries, first, null, 0); for (const entry of node.entries) { if (!entry) { continue; } const hyphen = sourceCode.getTokenBefore(entry, ast_utils_1.isHyphen); indicators.add(hyphen); const entryToken = sourceCode.getFirstToken(entry); setIndent(entryToken, calcIndicatorValueIndent(hyphen), hyphen); } } }, YAMLPair(node) { const pairFirst = sourceCode.getFirstToken(node); const keyToken = node.key && sourceCode.getFirstToken(node.key); const colonToken = findColonToken(); const questionToken = (0, ast_utils_1.isQuestion)(pairFirst) ? pairFirst : null; if (questionToken) { indicators.add(questionToken); if (node.key) { setIndent(keyToken, calcMappingPairValueIndent(node.key, calcIndicatorValueIndent(questionToken)), questionToken); } } if (colonToken) { indicators.add(colonToken); if (questionToken) { setOffset(colonToken, 0, questionToken, { offsetWhenBaseIsNotFirst: 1, }); } else if (keyToken) { setOffset(colonToken, 1, keyToken); } } if (node.value) { const valueToken = sourceCode.getFirstToken(node.value); if (colonToken) { setIndent(valueToken, calcMappingPairValueIndent(node.value, calcIndicatorValueIndent(colonToken)), colonToken); } else if (keyToken) { setOffset(valueToken, 1, keyToken); } } function findColonToken() { if (node.value) { return sourceCode.getTokenBefore(node.value, ast_utils_1.isColon); } if (node.key) { const token = sourceCode.getTokenAfter(node.key, ast_utils_1.isColon); if (token && token.range[0] < node.range[1]) { return token; } } const tokens = sourceCode.getTokens(node, ast_utils_1.isColon); if (tokens.length) { return tokens[0]; } return null; } }, YAMLWithMeta(node) { const anchorToken = node.anchor && sourceCode.getFirstToken(node.anchor); const tagToken = node.tag && sourceCode.getFirstToken(node.tag); let baseToken; if (anchorToken && tagToken) { if (anchorToken.range[0] < tagToken.range[0]) { setOffset(tagToken, 0, anchorToken, { offsetWhenBaseIsNotFirst: 1, }); baseToken = anchorToken; } else { setOffset(anchorToken, 0, tagToken, { offsetWhenBaseIsNotFirst: 1, }); baseToken = tagToken; } } else { baseToken = (anchorToken || tagToken); } if (node.value) { const valueToken = sourceCode.getFirstToken(node.value); setOffset(valueToken, 1, baseToken); } }, YAMLScalar(node) { if (node.style === "folded" || node.style === "literal") { if (!node.value.trim()) { return; } const mark = sourceCode.getFirstToken(node); const literal = sourceCode.getLastToken(node); setOffset(literal, 1, mark); scalars.set(literal, node); blockLiteralMarks.add(mark); } else { scalars.set(sourceCode.getFirstToken(node), node); } }, "Program:exit"(node) { const lineIndentsWk = []; 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); lineIndentsWk[lineIndent.line] = lineIndent; tokensOnSameLine = [token]; } } if (tokensOnSameLine.length >= 1) { const lineIndent = processExpectedIndent(tokensOnSameLine); lineIndentsWk[lineIndent.line] = lineIndent; } const lineIndents = processMissingLines(lineIndentsWk); validateLines(lineIndents); }, }; function processExpectedIndent(lineTokens) { const lastToken = lineTokens[lineTokens.length - 1]; let lineExpectedIndent = null; let cacheExpectedIndent = null; const indicatorData = []; const firstToken = lineTokens.shift(); let token = firstToken; let expectedIndent = getExpectedIndent(token); if (expectedIndent != null) { lineExpectedIndent = expectedIndent; cacheExpectedIndent = expectedIndent; } while (token && indicators.has(token) && expectedIndent != null) { const nextToken = lineTokens.shift(); if (!nextToken) { break; } const nextExpectedIndent = getExpectedIndent(nextToken); if (nextExpectedIndent == null || expectedIndent >= nextExpectedIndent) { lineTokens.unshift(nextToken); break; } indicatorData.push({ indicator: token, next: nextToken, expectedOffset: nextExpectedIndent - expectedIndent - 1, actualOffset: nextToken.range[0] - token.range[1], }); if (blockLiteralMarks.has(nextToken)) { lineTokens.unshift(nextToken); break; } token = nextToken; expectedIndent = nextExpectedIndent; cacheExpectedIndent = expectedIndent; } if (lineExpectedIndent == null) { while ((token = lineTokens.shift()) != null) { lineExpectedIndent = getExpectedIndent(token); if (lineExpectedIndent != null) { break; } } } const scalarNode = scalars.get(lastToken); if (scalarNode) { lineTokens.pop(); } if (cacheExpectedIndent != null) { while ((token = lineTokens.shift()) != null) { const indent = indents.get(token); if (indent) { indent.expectedIndent = cacheExpectedIndent; } } } let lastScalar = null; if (scalarNode) { const expectedScalarIndent = getExpectedIndent(lastToken); if (expectedScalarIndent != null) { lastScalar = { expectedIndent: expectedScalarIndent, token: lastToken, node: scalarNode, }; } } const { line, column } = firstToken.loc.start; return { expectedIndent: lineExpectedIndent, actualIndent: column, firstToken, line, indicatorData, lastScalar, }; } function getExpectedIndent(token) { if (token.type === "Marker") { return 0; } const indent = indents.get(token); if (!indent) { return null; } if (indent.expectedIndent != null) { return indent.expectedIndent; } if (indent.baseToken == null) { return null; } const baseIndent = getExpectedIndent(indent.baseToken); if (baseIndent == null) { return null; } let offsetIndent = indent.indent; if (offsetIndent === 0 && indent.indentWhenBaseIsNotFirst != null) { let before = indent.baseToken; while ((before = sourceCode.getTokenBefore(before, ITERATION_OPTS)) != null) { if (!indicators.has(before)) { break; } } if ((before === null || before === void 0 ? void 0 : before.loc.end.line) === indent.baseToken.loc.start.line) { offsetIndent = indent.indentWhenBaseIsNotFirst; } } return (indent.expectedIndent = baseIndent + offsetIndent); } function processMissingLines(lineIndents) { const results = []; const commentLines = []; for (const lineIndent of lineIndents) { if (!lineIndent) { continue; } const line = lineIndent.line; if (lineIndent.firstToken.type === "Block") { 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, indicatorData: lineIndent.indicatorData, }; if (!results[line]) { results[line] = indent; } if (lineIndent.lastScalar) { const scalarNode = lineIndent.lastScalar.node; if (scalarNode.style === "literal" || scalarNode.style === "folded") { processBlockLiteral(indent, scalarNode, lineIndent.lastScalar.expectedIndent); } else { let expectedIndent = lineIndent.lastScalar.expectedIndent; if (alignMultilineFlowScalars) { if (!isBeginningToken(lineIndent.lastScalar.token)) { expectedIndent = lineIndent.lastScalar.node.loc.start.column; } } processMultilineScalar(indent, scalarNode, expectedIndent); } } } } processComments(commentLines, lineIndents); return results; function processComments(commentLines, lineIndents) { var _a; for (const { range, commentLineIndents } of commentLines) { let prev = results .slice(0, range[0]) .filter((data) => data) .pop(); const next = results .slice(range[1] + 1) .filter((data) => data) .shift(); if (isBlockLiteral(prev)) { prev = undefined; } const expectedIndents = []; let either; if (prev && next) { expectedIndents.unshift(next.expectedIndent); if (next.expectedIndent < prev.expectedIndent) { let indent = next.expectedIndent + numOfIndent; while (indent <= prev.expectedIndent) { expectedIndents.unshift(indent); indent += numOfIndent; } } } else if ((either = prev || next)) { expectedIndents.unshift(either.expectedIndent); if (!next) { let indent = either.expectedIndent - numOfIndent; while (indent >= 0) { expectedIndents.push(indent); indent -= numOfIndent; } } } if (!expectedIndents.length) { continue; } let expectedIndent = expectedIndents[0]; for (const commentLineIndent of commentLineIndents) { if (results[commentLineIndent.line]) { continue; } expectedIndent = Math.min((_a = expectedIndents.find((indent, index) => { var _a; if (indent <= commentLineIndent.actualIndent) { return true; } const prev = (_a = expectedIndents[index + 1]) !== null && _a !== void 0 ? _a : -1; return (prev < commentLineIndent.actualIndent && commentLineIndent.actualIndent < indent); })) !== null && _a !== void 0 ? _a : expectedIndent, expectedIndent); results[commentLineIndent.line] = { line: commentLineIndent.line, expectedIndent, actualIndent: commentLineIndent.actualIndent, indicatorData: commentLineIndent.indicatorData, }; } } function isBlockLiteral(prev) { if (!prev) { return false; } for (let prevLine = prev.line; prevLine >= 0; prevLine--) { const prevLineIndent = lineIndents[prev.line]; if (!prevLineIndent) { continue; } if (prevLineIndent.lastScalar) { const scalarNode = prevLineIndent.lastScalar.node; if (scalarNode.style === "literal" || scalarNode.style === "folded") { if (scalarNode.loc.start.line <= prev.line && prev.line <= scalarNode.loc.end.line) { return true; } } } return false; } return false; } } function processBlockLiteral(lineIndent, scalarNode, expectedIndent) { if (scalarNode.indent != null) { if (lineIndent.expectedIndent < lineIndent.actualIndent) { lineIndent.expectedIndent = lineIndent.actualIndent; return; } lineIndent.indentBlockScalar = { node: scalarNode, }; } const firstLineActualIndent = lineIndent.actualIndent; for (let scalarLine = lineIndent.line + 1; scalarLine <= scalarNode.loc.end.line; scalarLine++) { const actualLineIndent = getActualLineIndent(scalarLine); if (actualLineIndent == null) { continue; } const scalarActualIndent = Math.min(firstLineActualIndent, actualLineIndent); results[scalarLine] = { line: scalarLine, expectedIndent, actualIndent: scalarActualIndent, indicatorData: [], }; } } function processMultilineScalar(lineIndent, scalarNode, expectedIndent) { for (let scalarLine = lineIndent.line + 1; scalarLine <= scalarNode.loc.end.line; scalarLine++) { const scalarActualIndent = getActualLineIndent(scalarLine); if (scalarActualIndent == null) { continue; } results[scalarLine] = { line: scalarLine, expectedIndent, actualIndent: scalarActualIndent, indicatorData: [], }; } } } function validateLines(lineIndents) { for (const lineIndent of lineIndents) { if (!lineIndent) { continue; } if (lineIndent.actualIndent !== lineIndent.expectedIndent) { context.report({ loc: { start: { line: lineIndent.line, column: 0, }, end: { line: lineIndent.line, column: lineIndent.actualIndent, }, }, messageId: "wrongIndentation", data: { expected: String(lineIndent.expectedIndent), actual: String(lineIndent.actualIndent), }, fix: buildFix(lineIndent, lineIndents), }); } else if (lineIndent.indicatorData.length) { for (const indicatorData of lineIndent.indicatorData) { if (indicatorData.actualOffset !== indicatorData.expectedOffset) { const indicatorLoc = indicatorData.indicator.loc.end; const loc = indicatorData.next.loc.start; context.report({ loc: { start: indicatorLoc, end: loc, }, messageId: "wrongIndentation", data: { expected: String(indicatorData.expectedOffset), actual: String(indicatorData.actualOffset), }, fix: buildFix(lineIndent, lineIndents), }); } } } } } function buildFix(lineIndent, lineIndents) { var _a; const { line, expectedIndent } = lineIndent; const document = documents.find((doc) => doc.loc.start.line <= line && line <= doc.loc.end.line) || sourceCode.ast; let startLine = document.loc.start.line; let endLine = document.loc.end.line; for (let lineIndex = line - 1; lineIndex >= document.loc.start.line; lineIndex--) { const li = lineIndents[lineIndex]; if (!li) { continue; } if (li.expectedIndent < expectedIndent) { if (expectedIndent <= li.actualIndent) { return null; } for (const indicator of li.indicatorData) { if (indicator.actualOffset !== indicator.expectedOffset) { return null; } } startLine = lineIndex + 1; break; } } for (let lineIndex = line + 1; lineIndex <= document.loc.end.line; lineIndex++) { const li = lineIndents[lineIndex]; if (!li) { continue; } if (li && li.expectedIndent < expectedIndent) { if (expectedIndent <= li.actualIndent) { return null; } endLine = lineIndex - 1; break; } } for (let lineIndex = startLine; lineIndex <= endLine; lineIndex++) { const li = lineIndents[lineIndex]; if (li === null || li === void 0 ? void 0 : li.indentBlockScalar) { const blockLiteral = li.indentBlockScalar.node; const diff = li.expectedIndent - li.actualIndent; const mark = sourceCode.getFirstToken(blockLiteral); const num = (_a = /\d+/u.exec(mark.value)) === null || _a === void 0 ? void 0 : _a[0]; if (num != null) { const newIndent = Number(num) + diff; if (newIndent >= 10) { return null; } } } } return function* (fixer) { let stacks = null; for (let lineIndex = startLine; lineIndex <= endLine; lineIndex++) { const li = lineIndents[lineIndex]; if (!li) { continue; } const lineExpectedIndent = li.expectedIndent; if (stacks == null) { if (li.expectedIndent !== li.actualIndent) { yield* fixLine(fixer, li); } } else { if (stacks.indent < lineExpectedIndent) { stacks = { indent: lineExpectedIndent, parentIndent: stacks.indent, upper: stacks, }; } else if (lineExpectedIndent < stacks.indent) { stacks = stacks.upper; } if (li.actualIndent <= stacks.parentIndent) { yield* fixLine(fixer, li); } } if (li.indicatorData) { for (const indicatorData of li.indicatorData) { yield fixer.replaceTextRange([indicatorData.indicator.range[1], indicatorData.next.range[0]], " ".repeat(indicatorData.expectedOffset)); } } } }; } function* fixLine(fixer, li) { if (li.indentBlockScalar) { const blockLiteral = li.indentBlockScalar.node; const diff = li.expectedIndent - li.actualIndent; const mark = sourceCode.getFirstToken(blockLiteral); yield fixer.replaceText(mark, mark.value.replace(/\d+/u, (num) => `${Number(num) + diff}`)); } const expectedIndent = li.expectedIndent; yield fixer.replaceTextRange([ sourceCode.getIndexFromLoc({ line: li.line, column: 0, }), sourceCode.getIndexFromLoc({ line: li.line, column: li.actualIndent, }), ], " ".repeat(expectedIndent)); } function getActualLineIndent(line) { const lineText = sourceCode.lines[line - 1]; if (!lineText.length) { return null; } return /^\s*/u.exec(lineText)[0].length; } }, });