UNPKG

eslint-plugin-svelte

Version:
238 lines (237 loc) 8.91 kB
import * as SV from './svelte.js'; import * as ES from './es.js'; import * as TS from './ts.js'; import { isNotWhitespace } from './ast.js'; import { isCommentToken } from '@eslint-community/eslint-utils'; import { OffsetContext } from './offset-context.js'; /** * Normalize options. * @param type The type of indentation. * @param options Other options. * @param defaultOptions The default value of options. * @returns Normalized options. */ function parseOptions(options, defaultOptions) { const ret = { indentChar: ' ', indentScript: true, indentSize: 2, switchCase: 1, alignAttributesVertically: false, ignoredNodes: [], ...defaultOptions }; if (Number.isSafeInteger(options.indent)) { ret.indentSize = Number(options.indent); } else if (options.indent === 'tab') { ret.indentChar = '\t'; ret.indentSize = 1; } if (typeof options.indentScript === 'boolean') { ret.indentScript = options.indentScript; } if (options.switchCase != null && Number.isSafeInteger(options.switchCase)) { ret.switchCase = options.switchCase; } if (options.ignoredNodes != null) { ret.ignoredNodes = options.ignoredNodes; } if (options.alignAttributesVertically && ret.indentChar === ' ') { ret.alignAttributesVertically = true; } else if (ret.indentChar !== ' ') { ret.alignAttributesVertically = false; } return ret; } /** * Creates AST event handlers for html-indent. * * @param context The rule context. * @param defaultOptions The default value of options. * @returns AST event handlers. */ export function defineVisitor(context, defaultOptions) { if (!context.filename.endsWith('.svelte')) return {}; const options = parseOptions(context.options[0] || {}, defaultOptions); const sourceCode = context.sourceCode; const offsets = new OffsetContext({ sourceCode, options }); /** * Get the text of the indentation part of the given location. */ function getIndentText({ line, column }) { return sourceCode.lines[line - 1].slice(0, column); } /** * Validate the given token with the pre-calculated expected indentation. */ function validateToken(token, expectedIndent) { const line = token.loc.start.line; const indentText = getIndentText(token.loc.start); // `indentText` contains non-whitespace characters. if (indentText.trim() !== '') { return; } const actualIndent = token.loc.start.column; const mismatchCharIndexes = []; for (let i = 0; i < indentText.length; ++i) { if (indentText[i] !== options.indentChar) { mismatchCharIndexes.push(i); } } if (actualIndent !== expectedIndent) { const loc = { start: { line, column: 0 }, end: { line, column: actualIndent } }; context.report({ loc, messageId: 'unexpectedIndentation', data: { expectedIndent: String(expectedIndent), actualIndent: String(actualIndent), expectedUnit: options.indentChar === '\t' ? 'tab' : 'space', actualUnit: mismatchCharIndexes.length ? 'whitespace' : options.indentChar === '\t' ? 'tab' : 'space', expectedIndentPlural: expectedIndent === 1 ? '' : 's', actualIndentPlural: actualIndent === 1 ? '' : 's' }, fix(fixer) { return fixer.replaceTextRange([sourceCode.getIndexFromLoc(loc.start), sourceCode.getIndexFromLoc(loc.end)], options.indentChar.repeat(expectedIndent)); } }); return; } for (const i of mismatchCharIndexes) { const loc = { start: { line, column: i }, end: { line, column: i + 1 } }; context.report({ loc, messageId: 'unexpectedChar', data: { expected: JSON.stringify(options.indentChar), actual: JSON.stringify(indentText[i]) }, fix(fixer) { return fixer.replaceTextRange([sourceCode.getIndexFromLoc(loc.start), sourceCode.getIndexFromLoc(loc.end)], options.indentChar); } }); } } /** Process line tokens */ function processLine(tokens, prevComments, prevToken, calculator) { const firstToken = tokens[0]; const actualIndent = firstToken.loc.start.column; const expectedIndent = calculator.getExpectedIndentFromTokens(tokens); if (expectedIndent == null) { calculator.saveExpectedIndent(tokens, actualIndent); return; } calculator.saveExpectedIndent(tokens, Math.min(...tokens .map((t) => calculator.getExpectedIndentFromToken(t)) .filter((i) => i != null))); let prev = prevToken; if (prevComments.length) { if (prev && prev.loc.end.line < prevComments[0].loc.start.line) { validateToken(prevComments[0], expectedIndent); } prev = prevComments[prevComments.length - 1]; } if (prev && prev.loc.end.line < tokens[0].loc.start.line) { validateToken(tokens[0], expectedIndent); } } const indentContext = { sourceCode, options, offsets }; const nodesVisitor = { ...ES.defineVisitor(indentContext), ...SV.defineVisitor(indentContext), ...TS.defineVisitor(indentContext) }; const knownNodes = new Set(Object.keys(nodesVisitor)); /** * Build a visitor combined with a visitor to handle the given ignore selector. */ function compositingIgnoresVisitor(visitor) { for (const ignoreSelector of options.ignoredNodes) { const key = `${ignoreSelector}:exit`; if (visitor[key]) { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore const handler = visitor[key]; visitor[key] = function (node, ...args) { const ret = handler.call(this, node, ...args); offsets.ignore(node); return ret; }; } else { visitor[key] = (node) => offsets.ignore(node); } } return visitor; } return compositingIgnoresVisitor({ ...nodesVisitor, '*:exit'(node) { // Ignore tokens of unknown nodes. if (!knownNodes.has(node.type)) { // debugger // console.log(node.type, node.loc!.start.line) offsets.ignore(node); } }, 'Program:exit'(node) { const calculator = offsets.getOffsetCalculator(); let prevToken = null; for (const { prevComments, tokens } of iterateLineTokens()) { processLine(tokens, prevComments, prevToken, calculator); prevToken = tokens[tokens.length - 1]; } /** Iterate line tokens */ function* iterateLineTokens() { let line = 0; let prevComments = []; let bufferTokens = []; for (const token of sourceCode.getTokens(node, { includeComments: true, filter: isNotWhitespace })) { const thisLine = token.loc.start.line; if (line === thisLine || bufferTokens.length === 0) { bufferTokens.push(token); } else { if (isCommentToken(bufferTokens[0]) && bufferTokens.every(isCommentToken)) { prevComments.push(bufferTokens[0]); } else { yield { prevComments, tokens: bufferTokens }; prevComments = []; } bufferTokens = [token]; } line = thisLine; } if (bufferTokens.length && !bufferTokens.every(isCommentToken)) { yield { prevComments, tokens: bufferTokens }; } } } }); }