no-restricted-props.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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 {import('../utils').ComponentArrayProp} ComponentArrayProp
  10. * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
  11. * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp
  12. */
  13. /**
  14. * @typedef {object} ParsedOption
  15. * @property { (name: string) => boolean } test
  16. * @property {string|undefined} [message]
  17. * @property {string|undefined} [suggest]
  18. */
  19. /**
  20. * @param {string} str
  21. * @returns {(str: string) => boolean}
  22. */
  23. function buildMatcher(str) {
  24. if (regexp.isRegExp(str)) {
  25. const re = regexp.toRegExp(str)
  26. return (s) => {
  27. re.lastIndex = 0
  28. return re.test(s)
  29. }
  30. }
  31. return (s) => s === str
  32. }
  33. /**
  34. * @param {string|{name:string, message?: string, suggest?:string}} option
  35. * @returns {ParsedOption}
  36. */
  37. function parseOption(option) {
  38. if (typeof option === 'string') {
  39. const matcher = buildMatcher(option)
  40. return {
  41. test(name) {
  42. return matcher(name)
  43. }
  44. }
  45. }
  46. const parsed = parseOption(option.name)
  47. parsed.message = option.message
  48. parsed.suggest = option.suggest
  49. return parsed
  50. }
  51. module.exports = {
  52. meta: {
  53. hasSuggestions: true,
  54. type: 'suggestion',
  55. docs: {
  56. description: 'disallow specific props',
  57. categories: undefined,
  58. url: 'https://eslint.vuejs.org/rules/no-restricted-props.html'
  59. },
  60. fixable: null,
  61. schema: {
  62. type: 'array',
  63. items: {
  64. oneOf: [
  65. { type: ['string'] },
  66. {
  67. type: 'object',
  68. properties: {
  69. name: { type: 'string' },
  70. message: { type: 'string', minLength: 1 },
  71. suggest: { type: 'string' }
  72. },
  73. required: ['name'],
  74. additionalProperties: false
  75. }
  76. ]
  77. },
  78. uniqueItems: true,
  79. minItems: 0
  80. },
  81. messages: {
  82. // eslint-disable-next-line eslint-plugin/report-message-format
  83. restrictedProp: '{{message}}',
  84. instead: 'Instead, change to `{{suggest}}`.'
  85. }
  86. },
  87. /** @param {RuleContext} context */
  88. create(context) {
  89. /** @type {ParsedOption[]} */
  90. const options = context.options.map(parseOption)
  91. /**
  92. * @param {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[]} props
  93. * @param { { [key: string]: Property | undefined } } [withDefaultsProps]
  94. */
  95. function processProps(props, withDefaultsProps) {
  96. for (const prop of props) {
  97. if (!prop.propName) {
  98. continue
  99. }
  100. for (const option of options) {
  101. if (option.test(prop.propName)) {
  102. const message =
  103. option.message ||
  104. `Using \`${prop.propName}\` props is not allowed.`
  105. context.report({
  106. node: prop.key,
  107. messageId: 'restrictedProp',
  108. data: { message },
  109. suggest: createSuggest(
  110. prop.key,
  111. option,
  112. withDefaultsProps && withDefaultsProps[prop.propName]
  113. )
  114. })
  115. break
  116. }
  117. }
  118. }
  119. }
  120. return utils.compositingVisitors(
  121. utils.defineScriptSetupVisitor(context, {
  122. onDefinePropsEnter(node, props) {
  123. processProps(props, utils.getWithDefaultsProps(node))
  124. }
  125. }),
  126. utils.defineVueVisitor(context, {
  127. onVueObjectEnter(node) {
  128. processProps(utils.getComponentProps(node))
  129. }
  130. })
  131. )
  132. }
  133. }
  134. /**
  135. * @param {Expression} node
  136. * @param {ParsedOption} option
  137. * @param {Property} [withDefault]
  138. * @returns {Rule.SuggestionReportDescriptor[]}
  139. */
  140. function createSuggest(node, option, withDefault) {
  141. if (!option.suggest) {
  142. return []
  143. }
  144. /** @type {string} */
  145. let replaceText
  146. if (node.type === 'Literal' || node.type === 'TemplateLiteral') {
  147. replaceText = JSON.stringify(option.suggest)
  148. } else if (node.type === 'Identifier') {
  149. replaceText = /^[a-z]\w*$/iu.exec(option.suggest)
  150. ? option.suggest
  151. : JSON.stringify(option.suggest)
  152. } else {
  153. return []
  154. }
  155. return [
  156. {
  157. fix(fixer) {
  158. const fixes = [fixer.replaceText(node, replaceText)]
  159. if (withDefault) {
  160. if (withDefault.shorthand) {
  161. fixes.push(
  162. fixer.insertTextBefore(withDefault.value, `${replaceText}:`)
  163. )
  164. } else {
  165. fixes.push(fixer.replaceText(withDefault.key, replaceText))
  166. }
  167. }
  168. return fixes.sort((a, b) => a.range[0] - b.range[0])
  169. },
  170. messageId: 'instead',
  171. data: { suggest: option.suggest }
  172. }
  173. ]
  174. }