You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

245 lines
7.2 KiB

/**
* @author Yosuke ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const htmlComments = require('../utils/html-comments')
/**
* Normalize options.
* @param {number|"tab"|undefined} type The type of indentation.
* @returns { { indentChar: string, indentSize: number, indentText: string } } Normalized options.
*/
function parseOptions(type) {
const ret = {
indentChar: ' ',
indentSize: 2,
indentText: ''
}
if (Number.isSafeInteger(type)) {
ret.indentSize = Number(type)
} else if (type === 'tab') {
ret.indentChar = '\t'
ret.indentSize = 1
}
ret.indentText = ret.indentChar.repeat(ret.indentSize)
return ret
}
/**
* @param {string} s
* @param {string} [unitChar]
*/
function toDisplay(s, unitChar) {
if (s.length === 0 && unitChar) {
return `0 ${toUnit(unitChar)}s`
}
const char = s[0]
if ((char === ' ' || char === '\t') && [...s].every((c) => c === char)) {
return `${s.length} ${toUnit(char)}${s.length === 1 ? '' : 's'}`
}
return JSON.stringify(s)
}
/** @param {string} char */
function toUnit(char) {
if (char === '\t') {
return 'tab'
}
if (char === ' ') {
return 'space'
}
return JSON.stringify(char)
}
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce consistent indentation in HTML comments',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/html-comment-indent.html'
},
fixable: 'whitespace',
schema: [
{
anyOf: [{ type: 'integer', minimum: 0 }, { enum: ['tab'] }]
}
],
messages: {
unexpectedBaseIndentation:
'Expected base point indentation of {{expected}}, but found {{actual}}.',
missingBaseIndentation:
'Expected base point indentation of {{expected}}, but not found.',
unexpectedIndentationCharacter:
'Expected {{expected}} character, but found {{actual}} character.',
unexpectedIndentation:
'Expected indentation of {{expected}} but found {{actual}}.',
unexpectedRelativeIndentation:
'Expected relative indentation of {{expected}} but found {{actual}}.'
}
},
/** @param {RuleContext} context */
create(context) {
const options = parseOptions(context.options[0])
const sourceCode = context.getSourceCode()
return htmlComments.defineVisitor(
context,
null,
(comment) => {
const baseIndentText = getLineIndentText(comment.open.loc.start.line)
let endLine
if (comment.value) {
const startLine = comment.value.loc.start.line
endLine = comment.value.loc.end.line
const checkStartLine =
comment.open.loc.end.line === startLine ? startLine + 1 : startLine
for (let line = checkStartLine; line <= endLine; line++) {
validateIndentForLine(line, baseIndentText, 1)
}
} else {
endLine = comment.open.loc.end.line
}
if (endLine < comment.close.loc.start.line) {
// `-->`
validateIndentForLine(comment.close.loc.start.line, baseIndentText, 0)
}
},
{ includeDirectives: true }
)
/**
* Checks whether the given line is a blank line.
* @param {number} line The number of line. Begins with 1.
* @returns {boolean} `true` if the given line is a blank line
*/
function isEmptyLine(line) {
const lineText = sourceCode.getLines()[line - 1]
return !lineText.trim()
}
/**
* Get the actual indentation of the given line.
* @param {number} line The number of line. Begins with 1.
* @returns {string} The actual indentation text
*/
function getLineIndentText(line) {
const lineText = sourceCode.getLines()[line - 1]
const charIndex = lineText.search(/\S/)
// already checked
// if (charIndex < 0) {
// return lineText
// }
return lineText.slice(0, charIndex)
}
/**
* Define the function which fixes the problem.
* @param {number} line The number of line.
* @param {string} actualIndentText The actual indentation text.
* @param {string} expectedIndentText The expected indentation text.
* @returns { (fixer: RuleFixer) => Fix } The defined function.
*/
function defineFix(line, actualIndentText, expectedIndentText) {
return (fixer) => {
const start = sourceCode.getIndexFromLoc({
line,
column: 0
})
return fixer.replaceTextRange(
[start, start + actualIndentText.length],
expectedIndentText
)
}
}
/**
* Validate the indentation of a line.
* @param {number} line The number of line. Begins with 1.
* @param {string} baseIndentText The expected base indentation text.
* @param {number} offset The number of the indentation offset.
*/
function validateIndentForLine(line, baseIndentText, offset) {
if (isEmptyLine(line)) {
return
}
const actualIndentText = getLineIndentText(line)
const expectedOffsetIndentText = options.indentText.repeat(offset)
const expectedIndentText = baseIndentText + expectedOffsetIndentText
// validate base indent
if (
baseIndentText &&
(actualIndentText.length < baseIndentText.length ||
!actualIndentText.startsWith(baseIndentText))
) {
context.report({
loc: {
start: { line, column: 0 },
end: { line, column: actualIndentText.length }
},
messageId: actualIndentText
? 'unexpectedBaseIndentation'
: 'missingBaseIndentation',
data: {
expected: toDisplay(baseIndentText),
actual: toDisplay(actualIndentText.slice(0, baseIndentText.length))
},
fix: defineFix(line, actualIndentText, expectedIndentText)
})
return
}
const actualOffsetIndentText = actualIndentText.slice(
baseIndentText.length
)
// validate indent charctor
for (const [i, char] of [...actualOffsetIndentText].entries()) {
if (char !== options.indentChar) {
context.report({
loc: {
start: { line, column: baseIndentText.length + i },
end: { line, column: baseIndentText.length + i + 1 }
},
messageId: 'unexpectedIndentationCharacter',
data: {
expected: toUnit(options.indentChar),
actual: toUnit(char)
},
fix: defineFix(line, actualIndentText, expectedIndentText)
})
return
}
}
// validate indent length
if (actualOffsetIndentText.length !== expectedOffsetIndentText.length) {
context.report({
loc: {
start: { line, column: baseIndentText.length },
end: { line, column: actualIndentText.length }
},
messageId: baseIndentText
? 'unexpectedRelativeIndentation'
: 'unexpectedIndentation',
data: {
expected: toDisplay(expectedOffsetIndentText, options.indentChar),
actual: toDisplay(actualOffsetIndentText, options.indentChar)
},
fix: defineFix(line, actualIndentText, expectedIndentText)
})
}
}
}
}