no-restricted-props.js 5.8 KB

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