forbid-component-props.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. /**
  2. * @fileoverview Forbid certain props on components
  3. * @author Joe Lencioni
  4. */
  5. 'use strict';
  6. const minimatch = require('minimatch');
  7. const docsUrl = require('../util/docsUrl');
  8. const report = require('../util/report');
  9. // ------------------------------------------------------------------------------
  10. // Constants
  11. // ------------------------------------------------------------------------------
  12. const DEFAULTS = ['className', 'style'];
  13. // ------------------------------------------------------------------------------
  14. // Rule Definition
  15. // ------------------------------------------------------------------------------
  16. const messages = {
  17. propIsForbidden: 'Prop "{{prop}}" is forbidden on Components',
  18. };
  19. /** @type {import('eslint').Rule.RuleModule} */
  20. module.exports = {
  21. meta: {
  22. docs: {
  23. description: 'Disallow certain props on components',
  24. category: 'Best Practices',
  25. recommended: false,
  26. url: docsUrl('forbid-component-props'),
  27. },
  28. messages,
  29. schema: [{
  30. type: 'object',
  31. properties: {
  32. forbid: {
  33. type: 'array',
  34. items: {
  35. anyOf: [
  36. { type: 'string' },
  37. {
  38. type: 'object',
  39. properties: {
  40. propName: { type: 'string' },
  41. allowedFor: {
  42. type: 'array',
  43. uniqueItems: true,
  44. items: { type: 'string' },
  45. },
  46. allowedForPatterns: {
  47. type: 'array',
  48. uniqueItems: true,
  49. items: { type: 'string' },
  50. },
  51. message: { type: 'string' },
  52. },
  53. additionalProperties: false,
  54. },
  55. {
  56. type: 'object',
  57. properties: {
  58. propName: { type: 'string' },
  59. disallowedFor: {
  60. type: 'array',
  61. uniqueItems: true,
  62. minItems: 1,
  63. items: { type: 'string' },
  64. },
  65. disallowedForPatterns: {
  66. type: 'array',
  67. uniqueItems: true,
  68. minItems: 1,
  69. items: { type: 'string' },
  70. },
  71. message: { type: 'string' },
  72. },
  73. anyOf: [
  74. { required: ['disallowedFor'] },
  75. { required: ['disallowedForPatterns'] },
  76. ],
  77. additionalProperties: false,
  78. },
  79. {
  80. type: 'object',
  81. properties: {
  82. propNamePattern: { type: 'string' },
  83. allowedFor: {
  84. type: 'array',
  85. uniqueItems: true,
  86. items: { type: 'string' },
  87. },
  88. allowedForPatterns: {
  89. type: 'array',
  90. uniqueItems: true,
  91. items: { type: 'string' },
  92. },
  93. message: { type: 'string' },
  94. },
  95. additionalProperties: false,
  96. },
  97. {
  98. type: 'object',
  99. properties: {
  100. propNamePattern: { type: 'string' },
  101. disallowedFor: {
  102. type: 'array',
  103. uniqueItems: true,
  104. minItems: 1,
  105. items: { type: 'string' },
  106. },
  107. disallowedForPatterns: {
  108. type: 'array',
  109. uniqueItems: true,
  110. minItems: 1,
  111. items: { type: 'string' },
  112. },
  113. message: { type: 'string' },
  114. },
  115. anyOf: [
  116. { required: ['disallowedFor'] },
  117. { required: ['disallowedForPatterns'] },
  118. ],
  119. additionalProperties: false,
  120. },
  121. ],
  122. },
  123. },
  124. },
  125. }],
  126. },
  127. create(context) {
  128. const configuration = context.options[0] || {};
  129. const forbid = new Map((configuration.forbid || DEFAULTS).map((value) => {
  130. const propName = typeof value === 'string' ? value : value.propName;
  131. const propPattern = value.propNamePattern;
  132. const prop = propName || propPattern;
  133. const options = {
  134. allowList: [].concat(value.allowedFor || []),
  135. allowPatternList: [].concat(value.allowedForPatterns || []),
  136. disallowList: [].concat(value.disallowedFor || []),
  137. disallowPatternList: [].concat(value.disallowedForPatterns || []),
  138. message: typeof value === 'string' ? null : value.message,
  139. isPattern: !!value.propNamePattern,
  140. };
  141. return [prop, options];
  142. }));
  143. function getPropOptions(prop) {
  144. // Get config options having pattern
  145. const propNamePatternArray = Array.from(forbid.entries()).filter((propEntry) => propEntry[1].isPattern);
  146. // Match current prop with pattern options, return if matched
  147. const propNamePattern = propNamePatternArray.find((propPatternVal) => minimatch(prop, propPatternVal[0]));
  148. // Get options for matched propNamePattern
  149. const propNamePatternOptions = propNamePattern && propNamePattern[1];
  150. const options = forbid.get(prop) || propNamePatternOptions;
  151. return options;
  152. }
  153. function isForbidden(prop, tagName) {
  154. const options = getPropOptions(prop);
  155. if (!options) {
  156. return false;
  157. }
  158. function checkIsTagForbiddenByAllowOptions() {
  159. if (options.allowList.indexOf(tagName) !== -1) {
  160. return false;
  161. }
  162. if (options.allowPatternList.length === 0) {
  163. return true;
  164. }
  165. return options.allowPatternList.every(
  166. (pattern) => !minimatch(tagName, pattern)
  167. );
  168. }
  169. function checkIsTagForbiddenByDisallowOptions() {
  170. if (options.disallowList.indexOf(tagName) !== -1) {
  171. return true;
  172. }
  173. if (options.disallowPatternList.length === 0) {
  174. return false;
  175. }
  176. return options.disallowPatternList.some(
  177. (pattern) => minimatch(tagName, pattern)
  178. );
  179. }
  180. const hasDisallowOptions = options.disallowList.length > 0 || options.disallowPatternList.length > 0;
  181. // disallowList should have a least one item (schema configuration)
  182. const isTagForbidden = hasDisallowOptions
  183. ? checkIsTagForbiddenByDisallowOptions()
  184. : checkIsTagForbiddenByAllowOptions();
  185. // if the tagName is undefined (`<this.something>`), we assume it's a forbidden element
  186. return typeof tagName === 'undefined' || isTagForbidden;
  187. }
  188. return {
  189. JSXAttribute(node) {
  190. const parentName = node.parent.name;
  191. // Extract a component name when using a "namespace", e.g. `<AntdLayout.Content />`.
  192. const tag = parentName.name || `${parentName.object.name}.${parentName.property.name}`;
  193. const componentName = parentName.name || parentName.property.name;
  194. if (componentName && typeof componentName[0] === 'string' && componentName[0] !== componentName[0].toUpperCase()) {
  195. // This is a DOM node, not a Component, so exit.
  196. return;
  197. }
  198. const prop = node.name.name;
  199. if (!isForbidden(prop, tag)) {
  200. return;
  201. }
  202. const customMessage = getPropOptions(prop).message;
  203. report(context, customMessage || messages.propIsForbidden, !customMessage && 'propIsForbidden', {
  204. node,
  205. data: {
  206. prop,
  207. },
  208. });
  209. },
  210. };
  211. },
  212. };