no-restricted-v-bind.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  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. const regexp = require('../utils/regexp')
  8. /**
  9. * @typedef {object} ParsedOption
  10. * @property { (key: VDirectiveKey) => boolean } test
  11. * @property {string[]} modifiers
  12. * @property {boolean} [useElement]
  13. * @property {string} [message]
  14. */
  15. const DEFAULT_OPTIONS = [
  16. {
  17. argument: '/^v-/',
  18. message:
  19. 'Using `:v-xxx` is not allowed. Instead, remove `:` and use it as directive.'
  20. }
  21. ]
  22. /**
  23. * @param {string} str
  24. * @returns {(str: string) => boolean}
  25. */
  26. function buildMatcher(str) {
  27. if (regexp.isRegExp(str)) {
  28. const re = regexp.toRegExp(str)
  29. return (s) => {
  30. re.lastIndex = 0
  31. return re.test(s)
  32. }
  33. }
  34. return (s) => s === str
  35. }
  36. /**
  37. * @param {any} option
  38. * @returns {ParsedOption}
  39. */
  40. function parseOption(option) {
  41. if (typeof option === 'string') {
  42. const matcher = buildMatcher(option)
  43. return {
  44. test(key) {
  45. return Boolean(
  46. key.argument &&
  47. key.argument.type === 'VIdentifier' &&
  48. matcher(key.argument.rawName)
  49. )
  50. },
  51. modifiers: []
  52. }
  53. }
  54. if (option === null) {
  55. return {
  56. test(key) {
  57. return key.argument === null
  58. },
  59. modifiers: []
  60. }
  61. }
  62. const parsed = parseOption(option.argument)
  63. if (option.modifiers) {
  64. const argTest = parsed.test
  65. parsed.test = (key) => {
  66. if (!argTest(key)) {
  67. return false
  68. }
  69. return /** @type {string[]} */ (option.modifiers).every((modName) => {
  70. return key.modifiers.some((mid) => mid.name === modName)
  71. })
  72. }
  73. parsed.modifiers = option.modifiers
  74. }
  75. if (option.element) {
  76. const argTest = parsed.test
  77. const tagMatcher = buildMatcher(option.element)
  78. parsed.test = (key) => {
  79. if (!argTest(key)) {
  80. return false
  81. }
  82. const element = key.parent.parent.parent
  83. return tagMatcher(element.rawName)
  84. }
  85. parsed.useElement = true
  86. }
  87. parsed.message = option.message
  88. return parsed
  89. }
  90. module.exports = {
  91. meta: {
  92. type: 'suggestion',
  93. docs: {
  94. description: 'disallow specific argument in `v-bind`',
  95. categories: undefined,
  96. url: 'https://eslint.vuejs.org/rules/no-restricted-v-bind.html'
  97. },
  98. fixable: null,
  99. schema: {
  100. type: 'array',
  101. items: {
  102. oneOf: [
  103. { type: ['string', 'null'] },
  104. {
  105. type: 'object',
  106. properties: {
  107. argument: { type: ['string', 'null'] },
  108. modifiers: {
  109. type: 'array',
  110. items: {
  111. type: 'string',
  112. enum: ['prop', 'camel', 'sync', 'attr']
  113. },
  114. uniqueItems: true
  115. },
  116. element: { type: 'string' },
  117. message: { type: 'string', minLength: 1 }
  118. },
  119. required: ['argument'],
  120. additionalProperties: false
  121. }
  122. ]
  123. },
  124. uniqueItems: true,
  125. minItems: 0
  126. },
  127. messages: {
  128. // eslint-disable-next-line eslint-plugin/report-message-format
  129. restrictedVBind: '{{message}}'
  130. }
  131. },
  132. /** @param {RuleContext} context */
  133. create(context) {
  134. /** @type {ParsedOption[]} */
  135. const options = (
  136. context.options.length === 0 ? DEFAULT_OPTIONS : context.options
  137. ).map(parseOption)
  138. return utils.defineTemplateBodyVisitor(context, {
  139. /**
  140. * @param {VDirectiveKey} node
  141. */
  142. "VAttribute[directive=true][key.name.name='bind'] > VDirectiveKey"(node) {
  143. for (const option of options) {
  144. if (option.test(node)) {
  145. const message = option.message || defaultMessage(node, option)
  146. context.report({
  147. node,
  148. messageId: 'restrictedVBind',
  149. data: { message }
  150. })
  151. return
  152. }
  153. }
  154. }
  155. })
  156. /**
  157. * @param {VDirectiveKey} key
  158. * @param {ParsedOption} option
  159. */
  160. function defaultMessage(key, option) {
  161. const vbind = key.name.rawName === ':' ? '' : 'v-bind'
  162. const arg =
  163. key.argument != null && key.argument.type === 'VIdentifier'
  164. ? `:${key.argument.rawName}`
  165. : ''
  166. const mod = option.modifiers.length
  167. ? `.${option.modifiers.join('.')}`
  168. : ''
  169. let on = ''
  170. if (option.useElement) {
  171. on = ` on \`<${key.parent.parent.parent.rawName}>\``
  172. }
  173. return `Using \`${vbind + arg + mod}\`${on} is not allowed.`
  174. }
  175. }
  176. }