html-comment-indent.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. /**
  2. * @author Yosuke ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. // -----------------------------------------------------------------------------
  7. // Requirements
  8. // -----------------------------------------------------------------------------
  9. const htmlComments = require('../utils/html-comments')
  10. // ------------------------------------------------------------------------------
  11. // Helpers
  12. // ------------------------------------------------------------------------------
  13. /**
  14. * Normalize options.
  15. * @param {number|"tab"|undefined} type The type of indentation.
  16. * @returns { { indentChar: string, indentSize: number, indentText: string } } Normalized options.
  17. */
  18. function parseOptions(type) {
  19. const ret = {
  20. indentChar: ' ',
  21. indentSize: 2,
  22. indentText: ''
  23. }
  24. if (Number.isSafeInteger(type)) {
  25. ret.indentSize = Number(type)
  26. } else if (type === 'tab') {
  27. ret.indentChar = '\t'
  28. ret.indentSize = 1
  29. }
  30. ret.indentText = ret.indentChar.repeat(ret.indentSize)
  31. return ret
  32. }
  33. /**
  34. * @param {string} s
  35. * @param {string} [unitChar]
  36. */
  37. function toDisplay(s, unitChar) {
  38. if (s.length === 0 && unitChar) {
  39. return `0 ${toUnit(unitChar)}s`
  40. }
  41. const char = s[0]
  42. if (char === ' ' || char === '\t') {
  43. if (s.split('').every((c) => c === char)) {
  44. return `${s.length} ${toUnit(char)}${s.length === 1 ? '' : 's'}`
  45. }
  46. }
  47. return JSON.stringify(s)
  48. }
  49. /** @param {string} char */
  50. function toUnit(char) {
  51. if (char === '\t') {
  52. return 'tab'
  53. }
  54. if (char === ' ') {
  55. return 'space'
  56. }
  57. return JSON.stringify(char)
  58. }
  59. // ------------------------------------------------------------------------------
  60. // Rule Definition
  61. // ------------------------------------------------------------------------------
  62. module.exports = {
  63. meta: {
  64. type: 'layout',
  65. docs: {
  66. description: 'enforce consistent indentation in HTML comments',
  67. categories: undefined,
  68. url: 'https://eslint.vuejs.org/rules/html-comment-indent.html'
  69. },
  70. fixable: 'whitespace',
  71. schema: [
  72. {
  73. anyOf: [{ type: 'integer', minimum: 0 }, { enum: ['tab'] }]
  74. }
  75. ],
  76. messages: {
  77. unexpectedBaseIndentation:
  78. 'Expected base point indentation of {{expected}}, but found {{actual}}.',
  79. missingBaseIndentation:
  80. 'Expected base point indentation of {{expected}}, but not found.',
  81. unexpectedIndentationCharacter:
  82. 'Expected {{expected}} character, but found {{actual}} character.',
  83. unexpectedIndentation:
  84. 'Expected indentation of {{expected}} but found {{actual}}.',
  85. unexpectedRelativeIndentation:
  86. 'Expected relative indentation of {{expected}} but found {{actual}}.'
  87. }
  88. },
  89. /** @param {RuleContext} context */
  90. create(context) {
  91. const options = parseOptions(context.options[0])
  92. const sourceCode = context.getSourceCode()
  93. return htmlComments.defineVisitor(
  94. context,
  95. null,
  96. (comment) => {
  97. const baseIndentText = getLineIndentText(comment.open.loc.start.line)
  98. let endLine
  99. if (comment.value) {
  100. const startLine = comment.value.loc.start.line
  101. endLine = comment.value.loc.end.line
  102. const checkStartLine =
  103. comment.open.loc.end.line === startLine ? startLine + 1 : startLine
  104. for (let line = checkStartLine; line <= endLine; line++) {
  105. validateIndentForLine(line, baseIndentText, 1)
  106. }
  107. } else {
  108. endLine = comment.open.loc.end.line
  109. }
  110. if (endLine < comment.close.loc.start.line) {
  111. // `-->`
  112. validateIndentForLine(comment.close.loc.start.line, baseIndentText, 0)
  113. }
  114. },
  115. { includeDirectives: true }
  116. )
  117. /**
  118. * Checks whether the given line is a blank line.
  119. * @param {number} line The number of line. Begins with 1.
  120. * @returns {boolean} `true` if the given line is a blank line
  121. */
  122. function isEmptyLine(line) {
  123. const lineText = sourceCode.getLines()[line - 1]
  124. return !lineText.trim()
  125. }
  126. /**
  127. * Get the actual indentation of the given line.
  128. * @param {number} line The number of line. Begins with 1.
  129. * @returns {string} The actual indentation text
  130. */
  131. function getLineIndentText(line) {
  132. const lineText = sourceCode.getLines()[line - 1]
  133. const charIndex = lineText.search(/\S/)
  134. // already checked
  135. // if (charIndex < 0) {
  136. // return lineText
  137. // }
  138. return lineText.slice(0, charIndex)
  139. }
  140. /**
  141. * Define the function which fixes the problem.
  142. * @param {number} line The number of line.
  143. * @param {string} actualIndentText The actual indentation text.
  144. * @param {string} expectedIndentText The expected indentation text.
  145. * @returns { (fixer: RuleFixer) => Fix } The defined function.
  146. */
  147. function defineFix(line, actualIndentText, expectedIndentText) {
  148. return (fixer) => {
  149. const start = sourceCode.getIndexFromLoc({
  150. line,
  151. column: 0
  152. })
  153. return fixer.replaceTextRange(
  154. [start, start + actualIndentText.length],
  155. expectedIndentText
  156. )
  157. }
  158. }
  159. /**
  160. * Validate the indentation of a line.
  161. * @param {number} line The number of line. Begins with 1.
  162. * @param {string} baseIndentText The expected base indentation text.
  163. * @param {number} offset The number of the indentation offset.
  164. */
  165. function validateIndentForLine(line, baseIndentText, offset) {
  166. if (isEmptyLine(line)) {
  167. return
  168. }
  169. const actualIndentText = getLineIndentText(line)
  170. const expectedOffsetIndentText = options.indentText.repeat(offset)
  171. const expectedIndentText = baseIndentText + expectedOffsetIndentText
  172. // validate base indent
  173. if (
  174. baseIndentText &&
  175. (actualIndentText.length < baseIndentText.length ||
  176. !actualIndentText.startsWith(baseIndentText))
  177. ) {
  178. context.report({
  179. loc: {
  180. start: { line, column: 0 },
  181. end: { line, column: actualIndentText.length }
  182. },
  183. messageId: actualIndentText
  184. ? 'unexpectedBaseIndentation'
  185. : 'missingBaseIndentation',
  186. data: {
  187. expected: toDisplay(baseIndentText),
  188. actual: toDisplay(actualIndentText.slice(0, baseIndentText.length))
  189. },
  190. fix: defineFix(line, actualIndentText, expectedIndentText)
  191. })
  192. return
  193. }
  194. const actualOffsetIndentText = actualIndentText.slice(
  195. baseIndentText.length
  196. )
  197. // validate indent charctor
  198. for (let i = 0; i < actualOffsetIndentText.length; ++i) {
  199. if (actualOffsetIndentText[i] !== options.indentChar) {
  200. context.report({
  201. loc: {
  202. start: { line, column: baseIndentText.length + i },
  203. end: { line, column: baseIndentText.length + i + 1 }
  204. },
  205. messageId: 'unexpectedIndentationCharacter',
  206. data: {
  207. expected: toUnit(options.indentChar),
  208. actual: toUnit(actualOffsetIndentText[i])
  209. },
  210. fix: defineFix(line, actualIndentText, expectedIndentText)
  211. })
  212. return
  213. }
  214. }
  215. // validate indent length
  216. if (actualOffsetIndentText.length !== expectedOffsetIndentText.length) {
  217. context.report({
  218. loc: {
  219. start: { line, column: baseIndentText.length },
  220. end: { line, column: actualIndentText.length }
  221. },
  222. messageId: baseIndentText
  223. ? 'unexpectedRelativeIndentation'
  224. : 'unexpectedIndentation',
  225. data: {
  226. expected: toDisplay(expectedOffsetIndentText, options.indentChar),
  227. actual: toDisplay(actualOffsetIndentText, options.indentChar)
  228. },
  229. fix: defineFix(line, actualIndentText, expectedIndentText)
  230. })
  231. }
  232. }
  233. }
  234. }