no-restricted-static-attribute.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  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: VAttribute) => boolean } test
  11. * @property {boolean} [useValue]
  12. * @property {boolean} [useElement]
  13. * @property {string} [message]
  14. */
  15. /**
  16. * @param {string} str
  17. * @returns {(str: string) => boolean}
  18. */
  19. function buildMatcher(str) {
  20. if (regexp.isRegExp(str)) {
  21. const re = regexp.toRegExp(str)
  22. return (s) => {
  23. re.lastIndex = 0
  24. return re.test(s)
  25. }
  26. }
  27. return (s) => s === str
  28. }
  29. /**
  30. * @param {any} option
  31. * @returns {ParsedOption}
  32. */
  33. function parseOption(option) {
  34. if (typeof option === 'string') {
  35. const matcher = buildMatcher(option)
  36. return {
  37. test({ key }) {
  38. return matcher(key.rawName)
  39. }
  40. }
  41. }
  42. const parsed = parseOption(option.key)
  43. if (option.value) {
  44. const keyTest = parsed.test
  45. if (option.value === true) {
  46. parsed.test = (node) => {
  47. if (!keyTest(node)) {
  48. return false
  49. }
  50. return node.value == null || node.value.value === node.key.rawName
  51. }
  52. } else {
  53. const valueMatcher = buildMatcher(option.value)
  54. parsed.test = (node) => {
  55. if (!keyTest(node)) {
  56. return false
  57. }
  58. return node.value != null && valueMatcher(node.value.value)
  59. }
  60. }
  61. parsed.useValue = true
  62. }
  63. if (option.element) {
  64. const argTest = parsed.test
  65. const tagMatcher = buildMatcher(option.element)
  66. parsed.test = (node) => {
  67. if (!argTest(node)) {
  68. return false
  69. }
  70. const element = node.parent.parent
  71. return tagMatcher(element.rawName)
  72. }
  73. parsed.useElement = true
  74. }
  75. parsed.message = option.message
  76. return parsed
  77. }
  78. /**
  79. * @param {VAttribute} node
  80. * @param {ParsedOption} option
  81. */
  82. function defaultMessage(node, option) {
  83. const key = node.key.rawName
  84. let value = ''
  85. if (option.useValue) {
  86. value = node.value == null ? '` set to `true' : `="${node.value.value}"`
  87. }
  88. let on = ''
  89. if (option.useElement) {
  90. on = ` on \`<${node.parent.parent.rawName}>\``
  91. }
  92. return `Using \`${key + value}\`${on} is not allowed.`
  93. }
  94. module.exports = {
  95. meta: {
  96. type: 'suggestion',
  97. docs: {
  98. description: 'disallow specific attribute',
  99. categories: undefined,
  100. url: 'https://eslint.vuejs.org/rules/no-restricted-static-attribute.html'
  101. },
  102. fixable: null,
  103. schema: {
  104. type: 'array',
  105. items: {
  106. oneOf: [
  107. { type: 'string' },
  108. {
  109. type: 'object',
  110. properties: {
  111. key: { type: 'string' },
  112. value: { anyOf: [{ type: 'string' }, { enum: [true] }] },
  113. element: { type: 'string' },
  114. message: { type: 'string', minLength: 1 }
  115. },
  116. required: ['key'],
  117. additionalProperties: false
  118. }
  119. ]
  120. },
  121. uniqueItems: true,
  122. minItems: 0
  123. },
  124. messages: {
  125. // eslint-disable-next-line eslint-plugin/report-message-format
  126. restrictedAttr: '{{message}}'
  127. }
  128. },
  129. /** @param {RuleContext} context */
  130. create(context) {
  131. if (context.options.length === 0) {
  132. return {}
  133. }
  134. /** @type {ParsedOption[]} */
  135. const options = context.options.map(parseOption)
  136. return utils.defineTemplateBodyVisitor(context, {
  137. /**
  138. * @param {VAttribute} node
  139. */
  140. 'VAttribute[directive=false]'(node) {
  141. for (const option of options) {
  142. if (option.test(node)) {
  143. const message = option.message || defaultMessage(node, option)
  144. context.report({
  145. node,
  146. messageId: 'restrictedAttr',
  147. data: { message }
  148. })
  149. return
  150. }
  151. }
  152. }
  153. })
  154. }
  155. }