449 lines
18 KiB
JavaScript
449 lines
18 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const toml_eslint_parser_1 = require("toml-eslint-parser");
|
|
const utils_1 = require("../utils");
|
|
const ast_utils_1 = require("../utils/ast-utils");
|
|
const compat_1 = require("../utils/compat");
|
|
const ITERATION_OPTS = Object.freeze({
|
|
includeComments: true,
|
|
});
|
|
function buildIndentUtility(optionValue) {
|
|
const indent = optionValue ?? 2;
|
|
const textIndent = typeof indent === "number" ? " ".repeat(indent) : "\t";
|
|
return {
|
|
getIndentText: (offset) => offset === 1 ? textIndent : textIndent.repeat(offset),
|
|
outdent(indent) {
|
|
return indent.slice(0, -textIndent.length);
|
|
},
|
|
};
|
|
}
|
|
exports.default = (0, utils_1.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 = (0, compat_1.getSourceCode)(context);
|
|
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 = new Map();
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
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 = [];
|
|
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 = (0, toml_eslint_parser_1.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") {
|
|
const openBracketNext = sourceCode.getTokenAfter(openBracket);
|
|
setOffset(openBracketNext, 0, openBracket);
|
|
}
|
|
const key = sourceCode.getFirstToken(node.key);
|
|
setOffset(key, 1, openBracket);
|
|
const closeBracket = sourceCode.getTokenAfter(node.key);
|
|
setOffset(closeBracket, 0, openBracket);
|
|
if (node.kind === "array") {
|
|
const closeBracketNext = sourceCode.getTokenAfter(closeBracket);
|
|
setOffset(closeBracketNext, 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, ast_utils_1.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;
|
|
}
|
|
const lineIndents = processMissingLines(lineIndentsStep1);
|
|
validateLines(lineIndents);
|
|
},
|
|
};
|
|
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,
|
|
};
|
|
}
|
|
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));
|
|
}
|
|
function processMissingLines(lineIndents) {
|
|
const results = [];
|
|
const commentLines = [];
|
|
for (const lineIndent of lineIndents) {
|
|
if (!lineIndent) {
|
|
continue;
|
|
}
|
|
const line = lineIndent.line;
|
|
if ((0, ast_utils_1.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;
|
|
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;
|
|
}
|
|
const prev = expectedIndents[index + 1]?.length ?? -1;
|
|
return (prev < 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,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
function getIndentData(lineIndent) {
|
|
return {
|
|
expected: toDisplayText(lineIndent.expectedIndent),
|
|
actual: toDisplayText(lineIndent.actualIndent),
|
|
};
|
|
function toDisplayText(indent) {
|
|
if (indent.length === 0) {
|
|
return "0 spaces";
|
|
}
|
|
const char = indent[0];
|
|
if (char === " " || char === "\t") {
|
|
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)}"`;
|
|
}
|
|
function replaceToDisplay(indent) {
|
|
return indent.replace(/\s/gu, (c) => {
|
|
if (c === "\t")
|
|
return "\\t";
|
|
if (c === " ")
|
|
return " ";
|
|
const hex = c.codePointAt(0).toString(16);
|
|
return `\\u${`000${hex}`.slice(-4)}`;
|
|
});
|
|
}
|
|
}
|
|
},
|
|
});
|