UNPKG

eslint-plugin-san

Version:

Official ESLint plugin for San

1,320 lines (1,207 loc) 78.9 kB
/** * @author Toru Nagashima <https://github.com/mysticatea> * See LICENSE file in root directory for full license. */ 'use strict'; /* eslint-disable */ // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ /** @type {Set<ASTNode['type']>} */ const KNOWN_NODES = new Set([ 'ArrayExpression', 'ArrayPattern', 'ArrowFunctionExpression', 'AssignmentExpression', 'AssignmentPattern', 'AwaitExpression', 'BinaryExpression', 'BlockStatement', 'BreakStatement', 'CallExpression', 'CatchClause', 'ChainExpression', 'ClassBody', 'ClassDeclaration', 'ClassExpression', 'ConditionalExpression', 'ContinueStatement', 'DebuggerStatement', 'DoWhileStatement', 'EmptyStatement', 'ExportAllDeclaration', 'ExportDefaultDeclaration', 'ExportNamedDeclaration', 'ExportSpecifier', 'ExpressionStatement', 'ForInStatement', 'ForOfStatement', 'ForStatement', 'FunctionDeclaration', 'FunctionExpression', 'Identifier', 'IfStatement', 'ImportDeclaration', 'ImportDefaultSpecifier', 'ImportExpression', 'ImportNamespaceSpecifier', 'ImportSpecifier', 'LabeledStatement', 'Literal', 'LogicalExpression', 'MemberExpression', 'MetaProperty', 'MethodDefinition', 'NewExpression', 'ObjectExpression', 'ObjectPattern', 'Program', 'Property', 'RestElement', 'ReturnStatement', 'SequenceExpression', 'SpreadElement', 'Super', 'SwitchCase', 'SwitchStatement', 'TaggedTemplateExpression', 'TemplateElement', 'TemplateLiteral', 'ThisExpression', 'ThrowStatement', 'TryStatement', 'UnaryExpression', 'UpdateExpression', 'VariableDeclaration', 'VariableDeclarator', 'WhileStatement', 'WithStatement', 'YieldExpression', 'VAttribute', 'VDirectiveKey', 'VDocumentFragment', 'VElement', 'VEndTag', 'VExpressionContainer', 'VFilter', 'VFilterSequenceExpression', 'VForExpression', 'VIdentifier', 'VLiteral', 'VOnExpression', 'VSlotScopeExpression', 'VStartTag', 'VText' ]); const NON_STANDARD_KNOWN_NODES = new Set(['ExperimentalRestProperty', 'ExperimentalSpreadProperty']); const LT_CHAR = /[\r\n\u2028\u2029]/; const LINES = /[^\r\n\u2028\u2029]+(?:$|\r\n|[\r\n\u2028\u2029])/g; const BLOCK_COMMENT_PREFIX = /^\s*\*/; const ITERATION_OPTS = Object.freeze({ includeComments: true, filter: isNotWhitespace }); const PREFORMATTED_ELEMENT_NAMES = ['pre', 'textarea']; /** * @typedef {object} IndentOptions * @property { " " | "\t" } IndentOptions.indentChar * @property {number} IndentOptions.indentSize * @property {number} IndentOptions.baseIndent * @property {number} IndentOptions.attribute * @property {object} IndentOptions.closeBracket * @property {number} IndentOptions.closeBracket.startTag * @property {number} IndentOptions.closeBracket.endTag * @property {number} IndentOptions.closeBracket.selfClosingTag * @property {number} IndentOptions.switchCase * @property {boolean} IndentOptions.alignAttributesVertically * @property {string[]} IndentOptions.ignores */ /** * @typedef {object} IndentUserOptions * @property { " " | "\t" } [IndentUserOptions.indentChar] * @property {number} [IndentUserOptions.indentSize] * @property {number} [IndentUserOptions.baseIndent] * @property {number} [IndentUserOptions.attribute] * @property {IndentOptions['closeBracket'] | number} [IndentUserOptions.closeBracket] * @property {number} [IndentUserOptions.switchCase] * @property {boolean} [IndentUserOptions.alignAttributesVertically] * @property {string[]} [IndentUserOptions.ignores] */ /** * Normalize options. * @param {number|"tab"|undefined} type The type of indentation. * @param {IndentUserOptions} options Other options. * @param {Partial<IndentOptions>} defaultOptions The default value of options. * @returns {IndentOptions} Normalized options. */ function parseOptions(type, options, defaultOptions) { /** @type {IndentOptions} */ const ret = Object.assign( { indentChar: ' ', indentSize: 2, baseIndent: 0, attribute: 1, closeBracket: { startTag: 0, endTag: 0, selfClosingTag: 0 }, switchCase: 0, alignAttributesVertically: true, ignores: [] }, defaultOptions ); if (Number.isSafeInteger(type)) { ret.indentSize = Number(type); } else if (type === 'tab') { ret.indentChar = '\t'; ret.indentSize = 1; } if (Number.isSafeInteger(options.baseIndent)) { ret.baseIndent = options.baseIndent; } if (Number.isSafeInteger(options.attribute)) { ret.attribute = options.attribute; } if (Number.isSafeInteger(options.closeBracket)) { const num = Number(options.closeBracket); ret.closeBracket = { startTag: num, endTag: num, selfClosingTag: num }; } else if (options.closeBracket) { ret.closeBracket = Object.assign( { startTag: 0, endTag: 0, selfClosingTag: 0 }, options.closeBracket ); } if (Number.isSafeInteger(options.switchCase)) { ret.switchCase = options.switchCase; } if (options.alignAttributesVertically != null) { ret.alignAttributesVertically = options.alignAttributesVertically; } if (options.ignores != null) { ret.ignores = options.ignores; } return ret; } /** * Check whether the given token is an arrow. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `true` if the token is an arrow. */ function isArrow(token) { return token != null && token.type === 'Punctuator' && token.value === '=>'; } /** * Check whether the given token is a left parenthesis. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `true` if the token is a left parenthesis. */ function isLeftParen(token) { return token != null && token.type === 'Punctuator' && token.value === '('; } /** * Check whether the given token is a left parenthesis. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `false` if the token is a left parenthesis. */ function isNotLeftParen(token) { return token != null && (token.type !== 'Punctuator' || token.value !== '('); } /** * Check whether the given token is a right parenthesis. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `true` if the token is a right parenthesis. */ function isRightParen(token) { return token != null && token.type === 'Punctuator' && token.value === ')'; } /** * Check whether the given token is a right parenthesis. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `false` if the token is a right parenthesis. */ function isNotRightParen(token) { return token != null && (token.type !== 'Punctuator' || token.value !== ')'); } /** * Check whether the given token is a left brace. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `true` if the token is a left brace. */ function isLeftBrace(token) { return token != null && token.type === 'Punctuator' && token.value === '{'; } /** * Check whether the given token is a right brace. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `true` if the token is a right brace. */ function isRightBrace(token) { return token != null && token.type === 'Punctuator' && token.value === '}'; } /** * Check whether the given token is a left bracket. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `true` if the token is a left bracket. */ function isLeftBracket(token) { return token != null && token.type === 'Punctuator' && token.value === '['; } /** * Check whether the given token is a right bracket. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `true` if the token is a right bracket. */ function isRightBracket(token) { return token != null && token.type === 'Punctuator' && token.value === ']'; } /** * Check whether the given token is a semicolon. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `true` if the token is a semicolon. */ function isSemicolon(token) { return token != null && token.type === 'Punctuator' && token.value === ';'; } /** * Check whether the given token is a comma. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `true` if the token is a comma. */ function isComma(token) { return token != null && token.type === 'Punctuator' && token.value === ','; } /** * Check whether the given token is a wildcard. * @param {Token} token The token to check. * @returns {boolean} `true` if the token is a wildcard. */ function isWildcard(token) { return token != null && token.type === 'Punctuator' && token.value === '*'; } /** * Check whether the given token is a whitespace. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `true` if the token is a whitespace. */ function isNotWhitespace(token) { return token != null && token.type !== 'HTMLWhitespace'; } /** * Check whether the given token is a comment. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `true` if the token is a comment. */ function isComment(token) { return ( token != null && (token.type === 'Block' || token.type === 'Line' || token.type === 'Shebang' || (typeof token.type === 'string' /* Although acorn supports new tokens, espree may not yet support new tokens.*/ && token.type.endsWith('Comment'))) ); } /** * Check whether the given token is a comment. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `false` if the token is a comment. */ function isNotComment(token) { return ( token != null && token.type !== 'Block' && token.type !== 'Line' && token.type !== 'Shebang' && !( typeof token.type === 'string' /* Although acorn supports new tokens, espree may not yet support new tokens.*/ && token.type.endsWith('Comment') ) ); } /** * Check whether the given node is not an empty text node. * @param {ASTNode} node The node to check. * @returns {boolean} `false` if the token is empty text node. */ function isNotEmptyTextNode(node) { return !(node.type === 'VText' && node.value.trim() === ''); } /** * Check whether the given token is a pipe operator. * @param {Token|undefined|null} token The token to check. * @returns {boolean} `true` if the token is a pipe operator. */ function isPipeOperator(token) { return token != null && token.type === 'Punctuator' && token.value === '|'; } /** * Get the last element. * @template T * @param {T[]} xs The array to get the last element. * @returns {T | undefined} The last element or undefined. */ function last(xs) { return xs.length === 0 ? undefined : xs[xs.length - 1]; } /** * Check whether the node is at the beginning of line. * @param {ASTNode|null} node The node to check. * @param {number} index The index of the node in the nodes. * @param {(ASTNode|null)[]} nodes The array of nodes. * @returns {boolean} `true` if the node is at the beginning of line. */ function isBeginningOfLine(node, index, nodes) { if (node != null) { for (let i = index - 1; i >= 0; --i) { const prevNode = nodes[i]; if (prevNode == null) { continue; } return node.loc.start.line !== prevNode.loc.end.line; } } return false; } /** * Check whether a given token is a closing token which triggers unindent. * @param {Token} token The token to check. * @returns {boolean} `true` if the token is a closing token. */ function isClosingToken(token) { return ( token != null && (token.type === 'HTMLEndTagOpen' || token.type === 'VExpressionEnd' || (token.type === 'Punctuator' && (token.value === ')' || token.value === '}' || token.value === ']'))) ); } /** * Creates AST event handlers for html-indent. * * @param {RuleContext} context The rule context. * @param {ParserServices.TokenStore | SourceCode} tokenStore The token store object to get tokens. * @param {Partial<IndentOptions>} defaultOptions The default value of options. * @returns {NodeListener} AST event handlers. */ module.exports.defineVisitor = function create(context, tokenStore, defaultOptions) { // if (!context.getFilename().endsWith('.san')) return {}; const options = parseOptions(context.options[0], context.options[1] || {}, defaultOptions); const sourceCode = context.getSourceCode(); const offsets = new Map(); const ignoreTokens = new Set(); /** * Set offset to the given tokens. * @param {Token|Token[]|null|(Token|null)[]} token The token to set. * @param {number} offset The offset of the tokens. * @param {Token} baseToken The token of the base offset. * @returns {void} */ function setOffset(token, offset, baseToken) { if (!token) { return; } if (Array.isArray(token)) { for (const t of token) { offsets.set(t, { baseToken, offset, baseline: false, expectedIndent: undefined }); } } else { offsets.set(token, { baseToken, offset, baseline: false, expectedIndent: undefined }); } } /** * Set baseline flag to the given token. * @param {Token} token The token to set. * @returns {void} */ function setBaseline(token) { const offsetInfo = offsets.get(token); if (offsetInfo != null) { offsetInfo.baseline = true; } } /** * Sets preformatted tokens to the given element node. * @param {VElement} node The node to set. * @returns {void} */ function setPreformattedTokens(node) { const endToken = (node.endTag && tokenStore.getFirstToken(node.endTag)) || tokenStore.getTokenAfter(node); /** @type {SourceCode.CursorWithSkipOptions} */ const option = { includeComments: true, filter: token => token != null && (token.type === 'HTMLText' || token.type === 'HTMLRCDataText' || token.type === 'HTMLTagOpen' || token.type === 'HTMLEndTagOpen' || token.type === 'HTMLComment') }; for (const token of tokenStore.getTokensBetween(node.startTag, endToken, option)) { ignoreTokens.add(token); } ignoreTokens.add(endToken); } /** * Get the first and last tokens of the given node. * If the node is parenthesized, this gets the outermost parentheses. * @param {ASTNode} node The node to get. * @param {number} [borderOffset] The least offset of the first token. Defailt is 0. This value is used to prevent false positive in the following case: `(a) => {}` The parentheses are enclosing the whole parameter part rather than the first parameter, but this offset parameter is needed to distinguish. * @returns {{firstToken:Token,lastToken:Token}} The gotten tokens. */ function getFirstAndLastTokens(node, borderOffset = 0) { borderOffset |= 0; let firstToken = tokenStore.getFirstToken(node); let lastToken = tokenStore.getLastToken(node); // Get the outermost left parenthesis if it's parenthesized. let t, u; while ( (t = tokenStore.getTokenBefore(firstToken)) != null && (u = tokenStore.getTokenAfter(lastToken)) != null && isLeftParen(t) && isRightParen(u) && t.range[0] >= borderOffset ) { firstToken = t; lastToken = u; } return {firstToken, lastToken}; } /** * Process the given node list. * The first node is offsetted from the given left token. * Rest nodes are adjusted to the first node. * @param {(ASTNode|null)[]} nodeList The node to process. * @param {ASTNode|Token|null} left The left parenthesis token. * @param {ASTNode|Token|null} right The right parenthesis token. * @param {number} offset The offset to set. * @param {boolean} [alignVertically=true] The flag to align vertically. If `false`, this doesn't align vertically even if the first node is not at beginning of line. * @returns {void} */ function processNodeList(nodeList, left, right, offset, alignVertically) { let t; const leftToken = left && tokenStore.getFirstToken(left); const rightToken = right && tokenStore.getFirstToken(right); if (nodeList.length >= 1) { let baseToken = null; let lastToken = left; const alignTokensBeforeBaseToken = []; const alignTokens = []; for (let i = 0; i < nodeList.length; ++i) { const node = nodeList[i]; if (node == null) { // Holes of an array. continue; } const elementTokens = getFirstAndLastTokens(node, lastToken != null ? lastToken.range[1] : 0); // Collect comma/comment tokens between the last token of the previous node and the first token of this node. if (lastToken != null) { t = lastToken; while ( (t = tokenStore.getTokenAfter(t, ITERATION_OPTS)) != null && t.range[1] <= elementTokens.firstToken.range[0] ) { if (baseToken == null) { alignTokensBeforeBaseToken.push(t); } else { alignTokens.push(t); } } } if (baseToken == null) { baseToken = elementTokens.firstToken; } else { alignTokens.push(elementTokens.firstToken); } // Save the last token to find tokens between this node and the next node. lastToken = elementTokens.lastToken; } // Check trailing commas and comments. if (rightToken != null && lastToken != null) { t = lastToken; while ((t = tokenStore.getTokenAfter(t, ITERATION_OPTS)) != null && t.range[1] <= rightToken.range[0]) { if (baseToken == null) { alignTokensBeforeBaseToken.push(t); } else { alignTokens.push(t); } } } // Set offsets. if (leftToken != null) { setOffset(alignTokensBeforeBaseToken, offset, leftToken); } if (baseToken != null) { // Set offset to the first token. if (leftToken != null) { setOffset(baseToken, offset, leftToken); } // Set baseline. if (nodeList.some(isBeginningOfLine)) { setBaseline(baseToken); } if (alignVertically === false && leftToken != null) { // Align tokens relatively to the left token. setOffset(alignTokens, offset, leftToken); } else { // Align the rest tokens to the first token. setOffset(alignTokens, 0, baseToken); } } } if (rightToken != null && leftToken != null) { setOffset(rightToken, 0, leftToken); } } /** * Process the given node as body. * The body node maybe a block statement or an expression node. * @param {ASTNode} node The body node to process. * @param {Token} baseToken The base token. * @returns {void} */ function processMaybeBlock(node, baseToken) { const firstToken = getFirstAndLastTokens(node).firstToken; setOffset(firstToken, isLeftBrace(firstToken) ? 0 : 1, baseToken); } /** * Collect prefix tokens of the given property. * The prefix includes `async`, `get`, `set`, `static`, and `*`. * @param {Property|MethodDefinition} node The property node to collect prefix tokens. */ function getPrefixTokens(node) { const prefixes = []; /** @type {Token|null} */ let token = tokenStore.getFirstToken(node); while (token != null && token.range[1] <= node.key.range[0]) { prefixes.push(token); token = tokenStore.getTokenAfter(token); } while (isLeftParen(last(prefixes)) || isLeftBracket(last(prefixes))) { prefixes.pop(); } return prefixes; } /** * Find the head of chaining nodes. * @param {ASTNode} node The start node to find the head. * @returns {Token} The head token of the chain. */ function getChainHeadToken(node) { const type = node.type; while (node.parent && node.parent.type === type) { const prevToken = tokenStore.getTokenBefore(node); if (isLeftParen(prevToken)) { // The chaining is broken by parentheses. break; } node = node.parent; } return tokenStore.getFirstToken(node); } /** * Check whether a given token is the first token of: * * - ExpressionStatement * - VExpressionContainer * - A parameter of CallExpression/NewExpression * - An element of ArrayExpression * - An expression of SequenceExpression * * @param {Token} token The token to check. * @param {ASTNode} belongingNode The node that the token is belonging to. * @returns {boolean} `true` if the token is the first token of an element. */ function isBeginningOfElement(token, belongingNode) { let node = belongingNode; while (node != null && node.parent != null) { const parent = node.parent; if (parent.type.endsWith('Statement') || parent.type.endsWith('Declaration')) { return parent.range[0] === token.range[0]; } if (parent.type === 'VExpressionContainer') { if (node.range[0] !== token.range[0]) { return false; } const prevToken = tokenStore.getTokenBefore(belongingNode); if (isLeftParen(prevToken)) { // It is not the first token because it is enclosed in parentheses. return false; } return true; } if (parent.type === 'CallExpression' || parent.type === 'NewExpression') { const openParen = /** @type {Token} */ (tokenStore.getTokenAfter(parent.callee, isNotRightParen)); return parent.arguments.some( param => getFirstAndLastTokens(param, openParen.range[1]).firstToken.range[0] === token.range[0] ); } if (parent.type === 'ArrayExpression') { return parent.elements.some( element => element != null && getFirstAndLastTokens(element).firstToken.range[0] === token.range[0] ); } if (parent.type === 'SequenceExpression') { return parent.expressions.some( expr => getFirstAndLastTokens(expr).firstToken.range[0] === token.range[0] ); } node = parent; } return false; } /** * Set the base indentation to a given top-level AST node. * @param {ASTNode} node The node to set. * @param {number} expectedIndent The number of expected indent. * @returns {void} */ function processTopLevelNode(node, expectedIndent) { const token = tokenStore.getFirstToken(node); const offsetInfo = offsets.get(token); if (offsetInfo != null) { offsetInfo.expectedIndent = expectedIndent; } else { offsets.set(token, { baseToken: null, offset: 0, baseline: false, expectedIndent }); } } /** * Ignore all tokens of the given node. * @param {ASTNode} node The node to ignore. * @returns {void} */ function ignore(node) { for (const token of tokenStore.getTokens(node)) { offsets.delete(token); ignoreTokens.add(token); } } /** * Define functions to ignore nodes into the given visitor. * @param {NodeListener} visitor The visitor to define functions to ignore nodes. * @returns {NodeListener} The visitor. */ function processIgnores(visitor) { for (const ignorePattern of options.ignores) { const key = `${ignorePattern}:exit`; if (visitor.hasOwnProperty(key)) { const handler = visitor[key]; visitor[key] = function(node, ...args) { // @ts-expect-error const ret = handler.call(this, node, ...args); ignore(node); return ret; }; } else { visitor[key] = ignore; } } return visitor; } /** * Calculate correct indentation of the line of the given tokens. * @param {Token[]} tokens Tokens which are on the same line. * @returns { { expectedIndent: number, expectedBaseIndent: number } |null } Correct indentation. If it failed to calculate then `null`. */ function getExpectedIndents(tokens) { const expectedIndents = []; for (let i = 0; i < tokens.length; ++i) { const token = tokens[i]; const offsetInfo = offsets.get(token); if (offsetInfo != null) { if (offsetInfo.expectedIndent != null) { expectedIndents.push(offsetInfo.expectedIndent); } else { const baseOffsetInfo = offsets.get(offsetInfo.baseToken); if ( baseOffsetInfo != null && baseOffsetInfo.expectedIndent != null && (i === 0 || !baseOffsetInfo.baseline) ) { expectedIndents.push(baseOffsetInfo.expectedIndent + offsetInfo.offset * options.indentSize); if (baseOffsetInfo.baseline) { break; } } } } } if (!expectedIndents.length) { return null; } return { expectedIndent: expectedIndents[0], expectedBaseIndent: expectedIndents.reduce((a, b) => Math.min(a, b)) }; } /** * Get the text of the indentation part of the line which the given token is on. * @param {Token} firstToken The first token on a line. * @returns {string} The text of indentation part. */ function getIndentText(firstToken) { const text = sourceCode.text; let i = firstToken.range[0] - 1; while (i >= 0 && !LT_CHAR.test(text[i])) { i -= 1; } return text.slice(i + 1, firstToken.range[0]); } /** * Define the function which fixes the problem. * @param {Token} token The token to fix. * @param {number} actualIndent The number of actual indentation. * @param {number} expectedIndent The number of expected indentation. * @returns { (fixer: RuleFixer) => Fix } The defined function. */ function defineFix(token, actualIndent, expectedIndent) { if (token.type === 'Block' && token.loc.start.line !== token.loc.end.line) { // Fix indentation in multiline block comments. const lines = sourceCode.getText(token).match(LINES) || []; const firstLine = lines.shift(); if (lines.every(l => BLOCK_COMMENT_PREFIX.test(l))) { return fixer => { /** @type {Range} */ const range = [token.range[0] - actualIndent, token.range[1]]; const indent = options.indentChar.repeat(expectedIndent); return fixer.replaceTextRange( range, `${indent}${firstLine}${lines .map(l => l.replace(BLOCK_COMMENT_PREFIX, `${indent} *`)) .join('')}` ); }; } } return fixer => { /** @type {Range} */ const range = [token.range[0] - actualIndent, token.range[0]]; const indent = options.indentChar.repeat(expectedIndent); return fixer.replaceTextRange(range, indent); }; } /** * Validate the given token with the pre-calculated expected indentation. * @param {Token} token The token to validate. * @param {number} expectedIndent The expected indentation. * @param {number[]} [optionalExpectedIndents] The optional expected indentation. * @returns {void} */ function validateCore(token, expectedIndent, optionalExpectedIndents) { const line = token.loc.start.line; const indentText = getIndentText(token); // If there is no line terminator after the `<script>` start tag, // `indentText` contains non-whitespace characters. // In that case, do nothing in order to prevent removing the `<script>` tag. if (indentText.trim() !== '') { return; } const actualIndent = token.loc.start.column; const unit = options.indentChar === '\t' ? 'tab' : 'space'; for (let i = 0; i < indentText.length; ++i) { if (indentText[i] !== options.indentChar) { context.report({ loc: { start: {line, column: i}, end: {line, column: i + 1} }, message: 'Expected {{expected}} character, but found {{actual}} character.', data: { expected: JSON.stringify(options.indentChar), actual: JSON.stringify(indentText[i]) }, fix: defineFix(token, actualIndent, expectedIndent) }); return; } } if ( actualIndent !== expectedIndent && (optionalExpectedIndents == null || !optionalExpectedIndents.includes(actualIndent)) ) { context.report({ loc: { start: {line, column: 0}, end: {line, column: actualIndent} }, message: 'Expected indentation of {{expectedIndent}} {{unit}}{{expectedIndentPlural}} but found {{actualIndent}} {{unit}}{{actualIndentPlural}}.', data: { expectedIndent, actualIndent, unit, expectedIndentPlural: expectedIndent === 1 ? '' : 's', actualIndentPlural: actualIndent === 1 ? '' : 's' }, fix: defineFix(token, actualIndent, expectedIndent) }); } } /** * Get the expected indent of comments. * @param {Token} nextToken The next token of comments. * @param {number} nextExpectedIndent The expected indent of the next token. * @param {number} lastExpectedIndent The expected indent of the last token. * @returns {number[]} */ function getCommentExpectedIndents(nextToken, nextExpectedIndent, lastExpectedIndent) { if (typeof lastExpectedIndent === 'number' && isClosingToken(nextToken)) { if (nextExpectedIndent === lastExpectedIndent) { // For solo comment. E.g., // <div> // <!-- comment --> // </div> return [nextExpectedIndent + options.indentSize, nextExpectedIndent]; } // For last comment. E.g., // <div> // <div></div> // <!-- comment --> // </div> return [lastExpectedIndent, nextExpectedIndent]; } // Adjust to next normally. E.g., // <div> // <!-- comment --> // <div></div> // </div> return [nextExpectedIndent]; } /** * Validate indentation of the line that the given tokens are on. * @param {Token[]} tokens The tokens on the same line to validate. * @param {Token[]} comments The comments which are on the immediately previous lines of the tokens. * @param {Token|null} lastToken The last validated token. Comments can adjust to the token. * @returns {void} */ function validate(tokens, comments, lastToken) { // Calculate and save expected indentation. const firstToken = tokens[0]; const actualIndent = firstToken.loc.start.column; const expectedIndents = getExpectedIndents(tokens); if (!expectedIndents) { return; } const expectedBaseIndent = expectedIndents.expectedBaseIndent; const expectedIndent = expectedIndents.expectedIndent; // Debug log // console.log('line', firstToken.loc.start.line, '=', { actualIndent, expectedIndent }, 'from:') // for (const token of tokens) { // const offsetInfo = offsets.get(token) // if (offsetInfo == null) { // console.log(' ', JSON.stringify(sourceCode.getText(token)), 'is unknown.') // } else if (offsetInfo.expectedIndent != null) { // console.log(' ', JSON.stringify(sourceCode.getText(token)), 'is fixed at', offsetInfo.expectedIndent, '.') // } else { // const baseOffsetInfo = offsets.get(offsetInfo.baseToken) // console.log(' ', JSON.stringify(sourceCode.getText(token)), 'is', offsetInfo.offset, 'offset from ', JSON.stringify(sourceCode.getText(offsetInfo.baseToken)), '( line:', offsetInfo.baseToken && offsetInfo.baseToken.loc.start.line, ', indent:', baseOffsetInfo && baseOffsetInfo.expectedIndent, ', baseline:', baseOffsetInfo && baseOffsetInfo.baseline, ')') // } // } // Save. const baseline = new Set(); for (const token of tokens) { const offsetInfo = offsets.get(token); if (offsetInfo != null) { if (offsetInfo.baseline) { // This is a baseline token, so the expected indent is the column of this token. if (options.indentChar === ' ') { offsetInfo.expectedIndent = Math.max( 0, token.loc.start.column + expectedBaseIndent - actualIndent ); } else { // In hard-tabs mode, it cannot align tokens strictly, so use one additional offset. // But the additional offset isn't needed if it's at the beginning of the line. offsetInfo.expectedIndent = expectedBaseIndent + (token === tokens[0] ? 0 : 1); } baseline.add(token); } else if (baseline.has(offsetInfo.baseToken)) { // The base token is a baseline token on this line, so inherit it. offsetInfo.expectedIndent = offsets.get(offsetInfo.baseToken).expectedIndent; baseline.add(token); } else { // Otherwise, set the expected indent of this line. offsetInfo.expectedIndent = expectedBaseIndent; } } } // It does not validate ignore tokens. if (ignoreTokens.has(firstToken)) { return; } // Calculate the expected indents for comments. // It allows the same indent level with the previous line. const lastOffsetInfo = offsets.get(lastToken); const lastExpectedIndent = lastOffsetInfo && lastOffsetInfo.expectedIndent; const commentOptionalExpectedIndents = getCommentExpectedIndents( firstToken, expectedIndent, lastExpectedIndent ); // Validate. for (const comment of comments) { const commentExpectedIndents = getExpectedIndents([comment]); const commentExpectedIndent = commentExpectedIndents ? commentExpectedIndents.expectedIndent : commentOptionalExpectedIndents[0]; validateCore(comment, commentExpectedIndent, commentOptionalExpectedIndents); } validateCore(firstToken, expectedIndent); } // ------------------------------------------------------------------------------ // Main // ------------------------------------------------------------------------------ return processIgnores({ /** @param {VAttribute} node */ VAttribute(node) { const keyToken = tokenStore.getFirstToken(node); const eqToken = tokenStore.getTokenAfter(node.key); if (eqToken != null && eqToken.range[1] <= node.range[1]) { setOffset(eqToken, 1, keyToken); const valueToken = tokenStore.getTokenAfter(eqToken); if (valueToken != null && valueToken.range[1] <= node.range[1]) { setOffset(valueToken, 1, keyToken); } } }, /** @param {VElement} node */ VElement(node) { if (!PREFORMATTED_ELEMENT_NAMES.includes(node.name)) { const isTopLevel = node.parent.type !== 'VElement'; const offset = isTopLevel ? options.baseIndent : 1; processNodeList(node.children.filter(isNotEmptyTextNode), node.startTag, node.endTag, offset, false); } else { const startTagToken = tokenStore.getFirstToken(node); const endTagToken = node.endTag && tokenStore.getFirstToken(node.endTag); setOffset(endTagToken, 0, startTagToken); setPreformattedTokens(node); } }, /** @param {VEndTag} node */ VEndTag(node) { const element = node.parent; const startTagOpenToken = tokenStore.getFirstToken(element.startTag); const closeToken = tokenStore.getLastToken(node); if (closeToken.type.endsWith('TagClose')) { setOffset(closeToken, options.closeBracket.endTag, startTagOpenToken); } }, /** @param {VExpressionContainer} node */ VExpressionContainer(node) { if (node.expression != null && node.range[0] !== node.expression.range[0]) { const startQuoteToken = tokenStore.getFirstToken(node); const endQuoteToken = tokenStore.getLastToken(node); const childToken = tokenStore.getTokenAfter(startQuoteToken); setOffset(childToken, 1, startQuoteToken); setOffset(endQuoteToken, 0, startQuoteToken); } }, /** @param {VFilter} node */ VFilter(node) { const idToken = tokenStore.getFirstToken(node); const lastToken = tokenStore.getLastToken(node); if (isRightParen(lastToken)) { const leftParenToken = tokenStore.getTokenAfter(node.callee); setOffset(leftParenToken, 1, idToken); processNodeList(node.arguments, leftParenToken, lastToken, 1); } }, /** @param {VFilterSequenceExpression} node */ VFilterSequenceExpression(node) { if (node.filters.length === 0) { return; } const firstToken = tokenStore.getFirstToken(node); /** @type {(Token|null)[]} */ const tokens = []; for (const filter of node.filters) { tokens.push(tokenStore.getTokenBefore(filter, isPipeOperator), tokenStore.getFirstToken(filter)); } setOffset(tokens, 1, firstToken); }, /** @param {VForExpression} node */ VForExpression(node) { const firstToken = tokenStore.getFirstToken(node); const lastOfLeft = last(node.left) || firstToken; const inToken = /** @type {Token} */ (tokenStore.getTokenAfter(lastOfLeft, isNotRightParen)); const rightToken = tokenStore.getFirstToken(node.right); if (isLeftParen(firstToken)) { const rightToken = tokenStore.getTokenAfter(lastOfLeft, isRightParen); processNodeList(node.left, firstToken, rightToken, 1); } setOffset(inToken, 1, firstToken); setOffset(rightToken, 1, inToken); }, /** @param {VOnExpression} node */ VOnExpression(node) { processNodeList(node.body, null, null, 0); }, /** @param {VStartTag} node */ VStartTag(node) { const openToken = tokenStore.getFirstToken(node); const closeToken = tokenStore.getLastToken(node); processNodeList(node.attributes, openToken, null, options.attribute, options.alignAttributesVertically); if (closeToken != null && closeToken.type.endsWith('TagClose')) { const offset = closeToken.type !== 'HTMLSelfClosingTagClose' ? options.closeBracket.startTag : options.closeBracket.selfClosingTag; setOffset(closeToken, offset, openToken); } }, /** @param {VText} node */ VText(node) { const tokens = tokenStore.getTokens(node, isNotWhitespace); const firstTokenInfo = offsets.get(tokenStore.getFirstToken(node)); for (const token of tokens) { offsets.set(token, Object.assign({}, firstTokenInfo)); } }, /** @param {ArrayExpression | ArrayPattern} node */ 'ArrayExpression, ArrayPattern'(node) { processNodeList(node.elements, tokenStore.getFirstToken(node), tokenStore.getLastToken(node), 1); }, /** @param {ArrowFunctionExpression} node */ ArrowFunctionExpression(node) { const firstToken = tokenStore.getFirstToken(node); const secondToken = tokenStore.getTokenAfter(firstToken); const leftToken = node.async ? secondToken : firstToken; const arrowToken = tokenStore.getTokenBefore(node.body, isArrow); if (node.async) { setOffset(secondToken, 1, firstToken); } if (isLeftParen(leftToken)) { const rightToken = tokenStore.getTokenAfter(last(node.params) || leftToken, isRightParen); processNodeList(node.params, leftToken, rightToken, 1); } setOffset(arrowToken, 1, firstToken); processMaybeBlock(node.body, firstToken); }, /** @param {AssignmentExpression | AssignmentPattern | BinaryExpression | LogicalExpression} node */ 'AssignmentExpression, AssignmentPattern, BinaryExpression, LogicalExpression'(node) { const leftToken = getChainHeadToken(node); const opToken = /** @type {Token} */ (tokenStore.getTokenAfter(node.left, isNotRightParen)); const rightToken = tokenStore.getTokenAfter(opToken); const prevToken = tokenStore.getTokenBefore(leftToken); const shouldIndent = prevToken == null || prevToken.loc.end.line === leftToken.loc.start.line || isBeginningOfElement(leftToken, node); setOffset([opToken, rightToken], shouldIndent ? 1 : 0, leftToken); }, /** @param {AwaitExpression | RestElement | SpreadElement | UnaryExpression} node */ 'AwaitExpression, RestElement, SpreadElement, UnaryExpression'(node) { const firstToken = tokenStore.getFirstToken(node); const nextToken = tokenStore.getTokenAfter(firstToken); setOffset(nextToken, 1, firstToken); }, /** @param {BlockStatement | ClassBody} node */ 'BlockStatement, ClassBody'(node) { processNodeList(node.body, tokenStore.getFirstToken(node), tokenStore.getLastToken(node), 1); }, /** @param {BreakStatement | ContinueStatement | ReturnStatement | ThrowStatement} node */ 'BreakStatement, ContinueStatement, ReturnStatement, ThrowStatement'(node) { if ( ((node.type === 'ReturnStatement' || node.type === 'ThrowStatement') && node.argument != null) || ((node.type === 'BreakStatement' || node.type === 'ContinueStatement') && node.label != null) ) { const firstToken = tokenStore.getFirstToken(node); const nextToken = tokenStore.getTokenAfter(firstToken); setOffset(nextToken, 1, firstToken); } }, /** @param {CallExpression} node */ CallExpression(node) { const firstToken = tokenStore.getFirstToken(node); const rightToken = tokenStore.getLastToken(node); const leftToken = tokenStore.getTokenAfter(node.callee, isLeftParen); setOffset(leftToken, 1, firstToken); processNodeList(node.arguments, leftToken, rightToken, 1); }, /** @param {ImportExpression} node */ ImportExpression(node) { const firstToken = tokenStore.getFirstToken(node); const rightToken = tokenStore.getLastToken(node); const leftToken = tokenStore.getTokenAfter(firstToken, isLeftParen); setOffset(leftToken, 1, firstToken); processNodeList([node.source], leftToken, rightToken, 1); }, /** @param {CatchClause} node */ CatchClause(node) { const firstToken = tokenStore.getFirstToken(node); const bodyToken = tokenStore.getFirstToken(node.body); if (node.param != null) { const leftToken = tokenStore.getTokenAfter(firstToken); const rightToken = tokenStore.getTokenAfter(node.param); setOffset(leftToken, 1, firstToken); processNodeList([node.param], leftToken, rightToken, 1); } setOffset(bodyToken, 0, firstToken); }, /** @param {ClassDeclaration | ClassExpression} node */ 'ClassDeclaration, ClassExpression'(node) { const firstToken = tokenStore.getFirstToken(node); const bodyToken = tokenStore.getFirstToken(node.body); if (node.id != null) { setOffset(tokenStore.getFirstToken(node.id), 1, firstToken); } if (node.superClass != null) { const extendsToken = tokenStore.getTokenAfter(node.id || firstToken); const superClassToken = tokenStore.getTokenAfter(extendsToken); setOffset(extendsToken, 1, firstToken); setOffset(superClassToken, 1, extendsToken); } setOffset(bodyToken, 0, firstToken); }, /** @param {ConditionalExpression} node */ ConditionalExpression(node) { const prevToken = tokenStore.getTokenBefore(node); const firstToken = tokenStore.getFirstToken(node); const questionToken = /** @type {Token} */ (tokenStore.getTokenAfter(node.test, isNotRightParen)); const consequentToken = tokenStore.getTokenAfter(questionToken); const colonToken = /** @type {Token} */ (tokenStore.getTokenAfter(node.consequent, isNotRightParen)); const alternateToken = tokenStore.getTokenAfter(colonToken); const isFlat = prevToken && prevToken.loc.end.line !== node.loc.start.line && node.test.loc.end.line === node.consequent.loc.start.line; if (isFlat) { setOffset([questionToken, consequentToken, colonToken, alternateToken], 0, firstToken); } else { setOffset([questionToken, colonToken], 1, firstToken); setOffset([consequentToken, alternateToken], 1, questionToken); } }, /** @param {DoWhileStatement} node */ DoWhileState