UNPKG

eslint-plugin-jsonc

Version:

ESLint plugin for JSON, JSONC and JSON5 files.

619 lines (618 loc) 27.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../utils"); const eslint_ast_utils_1 = require("../utils/eslint-ast-utils"); const eslint_utils_1 = require("@eslint-community/eslint-utils"); const KNOWN_NODES = new Set([ "JSONArrayExpression", "JSONBinaryExpression", "JSONExpressionStatement", "JSONIdentifier", "JSONLiteral", "JSONObjectExpression", "Program", "JSONProperty", "JSONTemplateElement", "JSONTemplateLiteral", "JSONUnaryExpression", ]); class IndexMap { constructor(maxKey) { this._values = Array(maxKey + 1); } insert(key, value) { this._values[key] = value; } findLastNotAfter(key) { const values = this._values; for (let index = key; index >= 0; index--) { const value = values[index]; if (value) return value; } return undefined; } deleteRange(start, end) { this._values.fill(undefined, start, end); } } class TokenInfo { constructor(sourceCode) { this.sourceCode = sourceCode; this.firstTokensByLineNumber = new Map(); const tokens = sourceCode.getTokens(sourceCode.ast, { includeComments: true, }); for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (!this.firstTokensByLineNumber.has(token.loc.start.line)) this.firstTokensByLineNumber.set(token.loc.start.line, token); if (!this.firstTokensByLineNumber.has(token.loc.end.line) && sourceCode.text .slice(token.range[1] - token.loc.end.column, token.range[1]) .trim()) this.firstTokensByLineNumber.set(token.loc.end.line, token); } } getFirstTokenOfLine(token) { return this.firstTokensByLineNumber.get(token.loc.start.line); } isFirstTokenOfLine(token) { return this.getFirstTokenOfLine(token) === token; } getTokenIndent(token) { return this.sourceCode.text.slice(token.range[0] - token.loc.start.column, token.range[0]); } } class OffsetStorage { constructor(tokenInfo, indentSize, indentType, maxIndex) { this._lockedFirstTokens = new WeakMap(); this._desiredIndentCache = new WeakMap(); this._ignoredTokens = new WeakSet(); this._tokenInfo = tokenInfo; this._indentSize = indentSize; this._indentType = indentType; this._indexMap = new IndexMap(maxIndex); this._indexMap.insert(0, { offset: 0, from: null, force: false }); } _getOffsetDescriptor(token) { return this._indexMap.findLastNotAfter(token.range[0]); } matchOffsetOf(baseToken, offsetToken) { this._lockedFirstTokens.set(offsetToken, baseToken); } setDesiredOffset(token, fromToken, offset) { if (token) this.setDesiredOffsets(token.range, fromToken, offset); } setDesiredOffsets(range, fromToken, offset, force = false) { const descriptorToInsert = { offset, from: fromToken, force }; const descriptorAfterRange = this._indexMap.findLastNotAfter(range[1]); const fromTokenIsInRange = fromToken && fromToken.range[0] >= range[0] && fromToken.range[1] <= range[1]; const fromTokenDescriptor = fromTokenIsInRange && this._getOffsetDescriptor(fromToken); this._indexMap.deleteRange(range[0] + 1, range[1]); this._indexMap.insert(range[0], descriptorToInsert); if (fromTokenIsInRange) { this._indexMap.insert(fromToken.range[0], fromTokenDescriptor); this._indexMap.insert(fromToken.range[1], descriptorToInsert); } this._indexMap.insert(range[1], descriptorAfterRange); } getDesiredIndent(token) { if (!this._desiredIndentCache.has(token)) { if (this._ignoredTokens.has(token)) { this._desiredIndentCache.set(token, this._tokenInfo.getTokenIndent(token)); } else if (this._lockedFirstTokens.has(token)) { const firstToken = this._lockedFirstTokens.get(token); this._desiredIndentCache.set(token, this.getDesiredIndent(this._tokenInfo.getFirstTokenOfLine(firstToken)) + this._indentType.repeat(firstToken.loc.start.column - this._tokenInfo.getFirstTokenOfLine(firstToken).loc.start .column)); } else { const offsetInfo = this._getOffsetDescriptor(token); const offset = offsetInfo.from && offsetInfo.from.loc.start.line === token.loc.start.line && !/^\s*?\n/u.test(token.value) && !offsetInfo.force ? 0 : offsetInfo.offset * this._indentSize; this._desiredIndentCache.set(token, (offsetInfo.from ? this.getDesiredIndent(offsetInfo.from) : "") + this._indentType.repeat(offset)); } } return this._desiredIndentCache.get(token); } ignoreToken(token) { if (this._tokenInfo.isFirstTokenOfLine(token)) this._ignoredTokens.add(token); } getFirstDependency(token) { return this._getOffsetDescriptor(token).from; } } const ELEMENT_LIST_SCHEMA = { oneOf: [ { type: "integer", minimum: 0, }, { type: "string", enum: ["first", "off"], }, ], }; exports.default = (0, utils_1.createRule)("indent", { meta: { docs: { description: "enforce consistent indentation", recommended: null, extensionRule: true, layout: true, }, type: "layout", fixable: "whitespace", schema: [ { oneOf: [ { type: "string", enum: ["tab"], }, { type: "integer", minimum: 0, }, ], }, { type: "object", properties: { SwitchCase: { type: "integer", minimum: 0, default: 0, }, VariableDeclarator: { oneOf: [ ELEMENT_LIST_SCHEMA, { type: "object", properties: { var: ELEMENT_LIST_SCHEMA, let: ELEMENT_LIST_SCHEMA, const: ELEMENT_LIST_SCHEMA, }, additionalProperties: false, }, ], }, outerIIFEBody: { oneOf: [ { type: "integer", minimum: 0, }, { type: "string", enum: ["off"], }, ], }, MemberExpression: { oneOf: [ { type: "integer", minimum: 0, }, { type: "string", enum: ["off"], }, ], }, FunctionDeclaration: { type: "object", properties: { parameters: ELEMENT_LIST_SCHEMA, body: { type: "integer", minimum: 0, }, }, additionalProperties: false, }, FunctionExpression: { type: "object", properties: { parameters: ELEMENT_LIST_SCHEMA, body: { type: "integer", minimum: 0, }, }, additionalProperties: false, }, StaticBlock: { type: "object", properties: { body: { type: "integer", minimum: 0, }, }, additionalProperties: false, }, CallExpression: { type: "object", properties: { arguments: ELEMENT_LIST_SCHEMA, }, additionalProperties: false, }, ArrayExpression: ELEMENT_LIST_SCHEMA, ObjectExpression: ELEMENT_LIST_SCHEMA, ImportDeclaration: ELEMENT_LIST_SCHEMA, flatTernaryExpressions: { type: "boolean", default: false, }, offsetTernaryExpressions: { type: "boolean", default: false, }, ignoredNodes: { type: "array", items: { type: "string", not: { pattern: ":exit$", }, }, }, ignoreComments: { type: "boolean", default: false, }, }, additionalProperties: false, }, ], messages: { wrongIndentation: "Expected indentation of {{expected}} but found {{actual}}.", }, }, create(context) { var _a; const sourceCode = context.sourceCode; if (!sourceCode.parserServices.isJSON) { return {}; } const DEFAULT_VARIABLE_INDENT = 1; const DEFAULT_PARAMETER_INDENT = 1; const DEFAULT_FUNCTION_BODY_INDENT = 1; let indentType = "space"; let indentSize = 4; const options = { SwitchCase: 0, VariableDeclarator: { var: DEFAULT_VARIABLE_INDENT, let: DEFAULT_VARIABLE_INDENT, const: DEFAULT_VARIABLE_INDENT, }, outerIIFEBody: 1, FunctionDeclaration: { parameters: DEFAULT_PARAMETER_INDENT, body: DEFAULT_FUNCTION_BODY_INDENT, }, FunctionExpression: { parameters: DEFAULT_PARAMETER_INDENT, body: DEFAULT_FUNCTION_BODY_INDENT, }, StaticBlock: { body: DEFAULT_FUNCTION_BODY_INDENT, }, CallExpression: { arguments: DEFAULT_PARAMETER_INDENT, }, MemberExpression: 1, ArrayExpression: 1, ObjectExpression: 1, ImportDeclaration: 1, flatTernaryExpressions: false, ignoredNodes: [], ignoreComments: false, offsetTernaryExpressions: false, }; if (context.options.length) { if (context.options[0] === "tab") { indentSize = 1; indentType = "tab"; } else { indentSize = (_a = context.options[0]) !== null && _a !== void 0 ? _a : indentSize; indentType = "space"; } const userOptions = context.options[1]; if (userOptions) { Object.assign(options, userOptions); if (typeof userOptions.VariableDeclarator === "number" || userOptions.VariableDeclarator === "first") { options.VariableDeclarator = { var: userOptions.VariableDeclarator, let: userOptions.VariableDeclarator, const: userOptions.VariableDeclarator, }; } } } const tokenInfo = new TokenInfo(sourceCode); const offsets = new OffsetStorage(tokenInfo, indentSize, indentType === "space" ? " " : "\t", sourceCode.text.length); const parameterParens = new WeakSet(); function createErrorMessageData(expectedAmount, actualSpaces, actualTabs) { const expectedStatement = `${expectedAmount} ${indentType}${expectedAmount === 1 ? "" : "s"}`; const foundSpacesWord = `space${actualSpaces === 1 ? "" : "s"}`; const foundTabsWord = `tab${actualTabs === 1 ? "" : "s"}`; let foundStatement; if (actualSpaces > 0) { foundStatement = indentType === "space" ? actualSpaces : `${actualSpaces} ${foundSpacesWord}`; } else if (actualTabs > 0) { foundStatement = indentType === "tab" ? actualTabs : `${actualTabs} ${foundTabsWord}`; } else { foundStatement = "0"; } return { expected: expectedStatement, actual: String(foundStatement), }; } function report(token, neededIndent) { const actualIndent = Array.from(tokenInfo.getTokenIndent(token)); const numSpaces = actualIndent.filter((char) => char === " ").length; const numTabs = actualIndent.filter((char) => char === "\t").length; context.report({ node: token, messageId: "wrongIndentation", data: createErrorMessageData(neededIndent.length, numSpaces, numTabs), loc: { start: { line: token.loc.start.line, column: 0 }, end: { line: token.loc.start.line, column: token.loc.start.column }, }, fix(fixer) { const range = [ token.range[0] - token.loc.start.column, token.range[0], ]; const newText = neededIndent; return fixer.replaceTextRange(range, newText); }, }); } function validateTokenIndent(token, desiredIndent) { const indentation = tokenInfo.getTokenIndent(token); return (indentation === desiredIndent || (indentation.includes(" ") && indentation.includes("\t"))); } function countTrailingLinebreaks(string) { const trailingWhitespace = /\s*$/u.exec(string)[0]; const linebreakMatches = (0, eslint_ast_utils_1.createGlobalLinebreakMatcher)().exec(trailingWhitespace); return linebreakMatches === null ? 0 : linebreakMatches.length; } function addElementListIndent(elements, startToken, endToken, offset) { function getFirstToken(element) { let token = sourceCode.getTokenBefore(element); while ((0, eslint_utils_1.isOpeningParenToken)(token) && token !== startToken) token = sourceCode.getTokenBefore(token); return sourceCode.getTokenAfter(token); } offsets.setDesiredOffsets([startToken.range[1], endToken.range[0]], startToken, typeof offset === "number" ? offset : 1); offsets.setDesiredOffset(endToken, startToken, 0); if (offset === "first" && elements.length && !elements[0]) return; elements.forEach((element, index) => { if (!element) { return; } if (offset === "off") { offsets.ignoreToken(getFirstToken(element)); } if (index === 0) return; if (offset === "first" && tokenInfo.isFirstTokenOfLine(getFirstToken(element))) { offsets.matchOffsetOf(getFirstToken(elements[0]), getFirstToken(element)); } else { const previousElement = elements[index - 1]; const firstTokenOfPreviousElement = previousElement && getFirstToken(previousElement); const previousElementLastToken = previousElement && sourceCode.getLastToken(previousElement); if (previousElement && previousElementLastToken.loc.end.line - countTrailingLinebreaks(previousElementLastToken.value) > startToken.loc.end.line) { offsets.setDesiredOffsets([previousElement.range[1], element.range[1]], firstTokenOfPreviousElement, 0); } } }); } function addParensIndent(tokens) { const parenStack = []; const parenPairs = []; for (let i = 0; i < tokens.length; i++) { const nextToken = tokens[i]; if ((0, eslint_utils_1.isOpeningParenToken)(nextToken)) parenStack.push(nextToken); else if ((0, eslint_utils_1.isClosingParenToken)(nextToken)) parenPairs.push({ left: parenStack.pop(), right: nextToken }); } for (let i = parenPairs.length - 1; i >= 0; i--) { const leftParen = parenPairs[i].left; const rightParen = parenPairs[i].right; if (!parameterParens.has(leftParen) && !parameterParens.has(rightParen)) { const parenthesizedTokens = new Set(sourceCode.getTokensBetween(leftParen, rightParen)); parenthesizedTokens.forEach((token) => { if (!parenthesizedTokens.has(offsets.getFirstDependency(token))) offsets.setDesiredOffset(token, leftParen, 1); }); } offsets.setDesiredOffset(rightParen, leftParen, 0); } } function ignoreNode(node) { const unknownNodeTokens = new Set(sourceCode.getTokens(node, { includeComments: true })); unknownNodeTokens.forEach((token) => { if (!unknownNodeTokens.has(offsets.getFirstDependency(token))) { const firstTokenOfLine = tokenInfo.getFirstTokenOfLine(token); if (token === firstTokenOfLine) offsets.ignoreToken(token); else offsets.setDesiredOffset(token, firstTokenOfLine, 0); } }); } function hasBlankLinesBetween(firstToken, secondToken) { const firstTokenLine = firstToken.loc.end.line; const secondTokenLine = secondToken.loc.start.line; if (firstTokenLine === secondTokenLine || firstTokenLine === secondTokenLine - 1) return false; for (let line = firstTokenLine + 1; line < secondTokenLine; ++line) { if (!tokenInfo.firstTokensByLineNumber.has(line)) return true; } return false; } const ignoredNodeFirstTokens = new Set(); const baseOffsetListeners = { JSONArrayExpression(node) { const openingBracket = sourceCode.getFirstToken(node); const closingBracket = sourceCode.getTokenAfter([...node.elements].reverse().find((_) => _) || openingBracket, eslint_utils_1.isClosingBracketToken); addElementListIndent(node.elements, openingBracket, closingBracket, options.ArrayExpression); }, JSONObjectExpression(node) { const openingCurly = sourceCode.getFirstToken(node); const closingCurly = sourceCode.getTokenAfter(node.properties.length ? node.properties[node.properties.length - 1] : openingCurly, eslint_utils_1.isClosingBraceToken); addElementListIndent(node.properties, openingCurly, closingCurly, options.ObjectExpression); }, JSONBinaryExpression(node) { const operator = sourceCode.getFirstTokenBetween(node.left, node.right, (token) => token.value === node.operator); const tokenAfterOperator = sourceCode.getTokenAfter(operator); offsets.ignoreToken(operator); offsets.ignoreToken(tokenAfterOperator); offsets.setDesiredOffset(tokenAfterOperator, operator, 0); }, JSONProperty(node) { if (!node.shorthand && !node.method && node.kind === "init") { const colon = sourceCode.getFirstTokenBetween(node.key, node.value, eslint_utils_1.isColonToken); offsets.ignoreToken(sourceCode.getTokenAfter(colon)); } }, JSONTemplateLiteral(node) { node.expressions.forEach((_expression, index) => { const previousQuasi = node.quasis[index]; const nextQuasi = node.quasis[index + 1]; const tokenToAlignFrom = previousQuasi.loc.start.line === previousQuasi.loc.end.line ? sourceCode.getFirstToken(previousQuasi) : null; offsets.setDesiredOffsets([previousQuasi.range[1], nextQuasi.range[0]], tokenToAlignFrom, 1); offsets.setDesiredOffset(sourceCode.getFirstToken(nextQuasi), tokenToAlignFrom, 0); }); }, "*"(node) { const firstToken = sourceCode.getFirstToken(node); if (firstToken && !ignoredNodeFirstTokens.has(firstToken)) offsets.setDesiredOffsets(node.range, firstToken, 0); }, }; const listenerCallQueue = []; const offsetListeners = {}; for (const [selector, listener] of Object.entries(baseOffsetListeners)) { offsetListeners[selector] = (node) => listenerCallQueue.push({ listener: listener, node, }); } const ignoredNodes = new Set(); function addToIgnoredNodes(node) { ignoredNodes.add(node); ignoredNodeFirstTokens.add(sourceCode.getFirstToken(node)); } const ignoredNodeListeners = options.ignoredNodes.reduce((listeners, ignoredSelector) => Object.assign(listeners, { [ignoredSelector]: addToIgnoredNodes }), {}); return Object.assign(offsetListeners, ignoredNodeListeners, { "*:exit"(node) { if (!KNOWN_NODES.has(node.type)) addToIgnoredNodes(node); }, "Program:exit"() { var _a; if (options.ignoreComments) { sourceCode .getAllComments() .forEach((comment) => offsets.ignoreToken(comment)); } for (let i = 0; i < listenerCallQueue.length; i++) { const nodeInfo = listenerCallQueue[i]; if (!ignoredNodes.has(nodeInfo.node)) (_a = nodeInfo.listener) === null || _a === void 0 ? void 0 : _a.call(nodeInfo, nodeInfo.node); } ignoredNodes.forEach(ignoreNode); addParensIndent(sourceCode.ast.tokens); const precedingTokens = new WeakMap(); for (let i = 0; i < sourceCode.ast.comments.length; i++) { const comment = sourceCode.ast.comments[i]; const tokenOrCommentBefore = sourceCode.getTokenBefore(comment, { includeComments: true, }); const hasToken = precedingTokens.has(tokenOrCommentBefore) ? precedingTokens.get(tokenOrCommentBefore) : tokenOrCommentBefore; precedingTokens.set(comment, hasToken); } for (let i = 1; i < sourceCode.lines.length + 1; i++) { if (!tokenInfo.firstTokensByLineNumber.has(i)) { continue; } const firstTokenOfLine = tokenInfo.firstTokensByLineNumber.get(i); if (firstTokenOfLine.loc.start.line !== i) { continue; } if ((0, eslint_utils_1.isCommentToken)(firstTokenOfLine)) { const tokenBefore = precedingTokens.get(firstTokenOfLine); const tokenAfter = tokenBefore ? sourceCode.getTokenAfter(tokenBefore) : sourceCode.ast.tokens[0]; const mayAlignWithBefore = tokenBefore && !hasBlankLinesBetween(tokenBefore, firstTokenOfLine); const mayAlignWithAfter = tokenAfter && !hasBlankLinesBetween(firstTokenOfLine, tokenAfter); if (tokenAfter && (0, eslint_utils_1.isSemicolonToken)(tokenAfter) && !(0, eslint_ast_utils_1.isTokenOnSameLine)(firstTokenOfLine, tokenAfter)) offsets.setDesiredOffset(firstTokenOfLine, tokenAfter, 0); if ((mayAlignWithBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore))) || (mayAlignWithAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter)))) continue; } if (validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine))) continue; report(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine)); } }, }); }, });