no-restricted-component-options.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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 {Tester} test
  11. * @property {string|undefined} [message]
  12. */
  13. /**
  14. * @typedef {object} MatchResult
  15. * @property {Tester | undefined} [next]
  16. * @property {boolean} [wildcard]
  17. * @property {string} keyName
  18. */
  19. /**
  20. * @typedef { (name: string) => boolean } Matcher
  21. * @typedef { (node: Property | SpreadElement) => (MatchResult | null) } Tester
  22. */
  23. /**
  24. * @param {string} str
  25. * @returns {Matcher}
  26. */
  27. function buildMatcher(str) {
  28. if (regexp.isRegExp(str)) {
  29. const re = regexp.toRegExp(str)
  30. return (s) => {
  31. re.lastIndex = 0
  32. return re.test(s)
  33. }
  34. }
  35. return (s) => s === str
  36. }
  37. /**
  38. * @param {string | string[] | { name: string | string[], message?: string } } option
  39. * @returns {ParsedOption}
  40. */
  41. function parseOption(option) {
  42. if (typeof option === 'string' || Array.isArray(option)) {
  43. return parseOption({
  44. name: option
  45. })
  46. }
  47. /**
  48. * @typedef {object} Step
  49. * @property {Matcher} [test]
  50. * @property {boolean} [wildcard]
  51. */
  52. /** @type {Step[]} */
  53. const steps = []
  54. for (const name of Array.isArray(option.name) ? option.name : [option.name]) {
  55. if (name === '*') {
  56. steps.push({ wildcard: true })
  57. } else {
  58. steps.push({ test: buildMatcher(name) })
  59. }
  60. }
  61. const message = option.message
  62. return {
  63. test: buildTester(0),
  64. message
  65. }
  66. /**
  67. * @param {number} index
  68. * @returns {Tester}
  69. */
  70. function buildTester(index) {
  71. const { wildcard, test } = steps[index]
  72. const next = index + 1
  73. const needNext = steps.length > next
  74. return (node) => {
  75. /** @type {string} */
  76. let keyName
  77. if (wildcard) {
  78. keyName = '*'
  79. } else {
  80. if (node.type !== 'Property') {
  81. return null
  82. }
  83. const name = utils.getStaticPropertyName(node)
  84. if (!name || !test(name)) {
  85. return null
  86. }
  87. keyName = name
  88. }
  89. return {
  90. next: needNext ? buildTester(next) : undefined,
  91. wildcard,
  92. keyName
  93. }
  94. }
  95. }
  96. }
  97. module.exports = {
  98. meta: {
  99. type: 'suggestion',
  100. docs: {
  101. description: 'disallow specific component option',
  102. categories: undefined,
  103. url: 'https://eslint.vuejs.org/rules/no-restricted-component-options.html'
  104. },
  105. fixable: null,
  106. schema: {
  107. type: 'array',
  108. items: {
  109. oneOf: [
  110. { type: 'string' },
  111. {
  112. type: 'array',
  113. items: {
  114. type: 'string'
  115. }
  116. },
  117. {
  118. type: 'object',
  119. properties: {
  120. name: {
  121. anyOf: [
  122. { type: 'string' },
  123. {
  124. type: 'array',
  125. items: {
  126. type: 'string'
  127. }
  128. }
  129. ]
  130. },
  131. message: { type: 'string', minLength: 1 }
  132. },
  133. required: ['name'],
  134. additionalProperties: false
  135. }
  136. ]
  137. },
  138. uniqueItems: true,
  139. minItems: 0
  140. },
  141. messages: {
  142. // eslint-disable-next-line eslint-plugin/report-message-format
  143. restrictedOption: '{{message}}'
  144. }
  145. },
  146. /** @param {RuleContext} context */
  147. create(context) {
  148. if (!context.options || context.options.length === 0) {
  149. return {}
  150. }
  151. /** @type {ParsedOption[]} */
  152. const options = context.options.map(parseOption)
  153. return utils.defineVueVisitor(context, {
  154. onVueObjectEnter(node) {
  155. for (const option of options) {
  156. verify(node, option.test, option.message)
  157. }
  158. }
  159. })
  160. /**
  161. * @param {ObjectExpression} node
  162. * @param {Tester} test
  163. * @param {string | undefined} customMessage
  164. * @param {string[]} path
  165. */
  166. function verify(node, test, customMessage, path = []) {
  167. for (const prop of node.properties) {
  168. const result = test(prop)
  169. if (!result) {
  170. continue
  171. }
  172. if (result.next) {
  173. if (
  174. prop.type !== 'Property' ||
  175. prop.value.type !== 'ObjectExpression'
  176. ) {
  177. continue
  178. }
  179. verify(prop.value, result.next, customMessage, [
  180. ...path,
  181. result.keyName
  182. ])
  183. } else {
  184. const message =
  185. customMessage || defaultMessage([...path, result.keyName])
  186. context.report({
  187. node: prop.type === 'Property' ? prop.key : prop,
  188. messageId: 'restrictedOption',
  189. data: { message }
  190. })
  191. }
  192. }
  193. }
  194. /**
  195. * @param {string[]} path
  196. */
  197. function defaultMessage(path) {
  198. return `Using \`${path.join('.')}\` is not allowed.`
  199. }
  200. }
  201. }