no-restricted-component-options.js 5.0 KB

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