no-useless-mustaches.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. /**
  2. * @author Yosuke Ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. /**
  8. * Strip quotes string
  9. * @param {string} text
  10. * @returns {string|null}
  11. */
  12. function stripQuotesForHTML(text) {
  13. if (
  14. (text[0] === '"' || text[0] === "'" || text[0] === '`') &&
  15. text[0] === text[text.length - 1]
  16. ) {
  17. return text.slice(1, -1)
  18. }
  19. const re = /^(?:&(?:quot|apos|#\d+|#x[\da-f]+);|["'`])([\s\S]*)(?:&(?:quot|apos|#\d+|#x[\da-f]+);|["'`])$/u.exec(
  20. text
  21. )
  22. if (!re) {
  23. return null
  24. }
  25. return re[1]
  26. }
  27. module.exports = {
  28. meta: {
  29. docs: {
  30. description: 'disallow unnecessary mustache interpolations',
  31. categories: undefined,
  32. url: 'https://eslint.vuejs.org/rules/no-useless-mustaches.html'
  33. },
  34. fixable: 'code',
  35. messages: {
  36. unexpected:
  37. 'Unexpected mustache interpolation with a string literal value.'
  38. },
  39. schema: [
  40. {
  41. type: 'object',
  42. properties: {
  43. ignoreIncludesComment: {
  44. type: 'boolean'
  45. },
  46. ignoreStringEscape: {
  47. type: 'boolean'
  48. }
  49. }
  50. }
  51. ],
  52. type: 'suggestion'
  53. },
  54. /** @param {RuleContext} context */
  55. create(context) {
  56. const opts = context.options[0] || {}
  57. const ignoreIncludesComment = opts.ignoreIncludesComment
  58. const ignoreStringEscape = opts.ignoreStringEscape
  59. const sourceCode = context.getSourceCode()
  60. /**
  61. * Report if the value expression is string literals
  62. * @param {VExpressionContainer} node the node to check
  63. */
  64. function verify(node) {
  65. const { expression } = node
  66. if (!expression) {
  67. return
  68. }
  69. /** @type {string} */
  70. let strValue
  71. /** @type {string} */
  72. let rawValue
  73. if (expression.type === 'Literal') {
  74. if (typeof expression.value !== 'string') {
  75. return
  76. }
  77. strValue = expression.value
  78. rawValue = sourceCode.getText(expression).slice(1, -1)
  79. } else if (expression.type === 'TemplateLiteral') {
  80. if (expression.expressions.length > 0) {
  81. return
  82. }
  83. strValue = expression.quasis[0].value.cooked
  84. rawValue = expression.quasis[0].value.raw
  85. } else {
  86. return
  87. }
  88. const tokenStore = context.parserServices.getTemplateBodyTokenStore()
  89. const hasComment = tokenStore
  90. .getTokens(node, { includeComments: true })
  91. .some((t) => t.type === 'Block' || t.type === 'Line')
  92. if (ignoreIncludesComment && hasComment) {
  93. return
  94. }
  95. let hasEscape = false
  96. if (rawValue !== strValue) {
  97. // check escapes
  98. const chars = [...rawValue]
  99. let c = chars.shift()
  100. while (c) {
  101. if (c === '\\') {
  102. c = chars.shift()
  103. if (
  104. c == null ||
  105. // ignore "\\", '"', "'", "`" and "$"
  106. 'nrvtbfux'.includes(c)
  107. ) {
  108. // has useful escape.
  109. hasEscape = true
  110. break
  111. }
  112. }
  113. c = chars.shift()
  114. }
  115. }
  116. if (ignoreStringEscape && hasEscape) {
  117. return
  118. }
  119. context.report({
  120. node,
  121. messageId: 'unexpected',
  122. fix(fixer) {
  123. if (hasComment || hasEscape) {
  124. // cannot fix
  125. return null
  126. }
  127. const text = stripQuotesForHTML(sourceCode.getText(expression))
  128. if (text == null) {
  129. // unknowns
  130. return null
  131. }
  132. if (text.includes('\n') || /^\s|\s$/u.test(text)) {
  133. // It doesn't autofix because another rule like indent or eol space might remove spaces.
  134. return null
  135. }
  136. return fixer.replaceText(node, text.replace(/\\([\s\S])/g, '$1'))
  137. }
  138. })
  139. }
  140. return utils.defineTemplateBodyVisitor(context, {
  141. 'VElement > VExpressionContainer': verify
  142. })
  143. }
  144. }