jsx-props-no-spreading.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. /**
  2. * @fileoverview Prevent JSX prop spreading
  3. * @author Ashish Gambhir
  4. */
  5. 'use strict';
  6. const docsUrl = require('../util/docsUrl');
  7. // ------------------------------------------------------------------------------
  8. // Constants
  9. // ------------------------------------------------------------------------------
  10. const OPTIONS = {ignore: 'ignore', enforce: 'enforce'};
  11. const DEFAULTS = {
  12. html: OPTIONS.enforce,
  13. custom: OPTIONS.enforce,
  14. explicitSpread: OPTIONS.enforce,
  15. exceptions: []
  16. };
  17. // ------------------------------------------------------------------------------
  18. // Rule Definition
  19. // ------------------------------------------------------------------------------
  20. module.exports = {
  21. meta: {
  22. docs: {
  23. description: 'Prevent JSX prop spreading',
  24. category: 'Best Practices',
  25. recommended: false,
  26. url: docsUrl('jsx-props-no-spreading')
  27. },
  28. messages: {
  29. noSpreading: 'Prop spreading is forbidden'
  30. },
  31. schema: [{
  32. allOf: [{
  33. type: 'object',
  34. properties: {
  35. html: {
  36. enum: [OPTIONS.enforce, OPTIONS.ignore]
  37. },
  38. custom: {
  39. enum: [OPTIONS.enforce, OPTIONS.ignore]
  40. },
  41. exceptions: {
  42. type: 'array',
  43. items: {
  44. type: 'string',
  45. uniqueItems: true
  46. }
  47. }
  48. }
  49. }, {
  50. not: {
  51. type: 'object',
  52. required: ['html', 'custom'],
  53. properties: {
  54. html: {
  55. enum: [OPTIONS.ignore]
  56. },
  57. custom: {
  58. enum: [OPTIONS.ignore]
  59. },
  60. exceptions: {
  61. type: 'array',
  62. minItems: 0,
  63. maxItems: 0
  64. }
  65. }
  66. }
  67. }]
  68. }]
  69. },
  70. create(context) {
  71. const configuration = context.options[0] || {};
  72. const ignoreHtmlTags = (configuration.html || DEFAULTS.html) === OPTIONS.ignore;
  73. const ignoreCustomTags = (configuration.custom || DEFAULTS.custom) === OPTIONS.ignore;
  74. const ignoreExplicitSpread = (configuration.explicitSpread || DEFAULTS.explicitSpread) === OPTIONS.ignore;
  75. const exceptions = configuration.exceptions || DEFAULTS.exceptions;
  76. const isException = (tag, allExceptions) => allExceptions.indexOf(tag) !== -1;
  77. const isProperty = (property) => property.type === 'Property';
  78. const getTagNameFromMemberExpression = (node) => `${node.property.parent.object.name}.${node.property.name}`;
  79. return {
  80. JSXSpreadAttribute(node) {
  81. const jsxOpeningElement = node.parent.name;
  82. const type = jsxOpeningElement.type;
  83. let tagName;
  84. if (type === 'JSXIdentifier') {
  85. tagName = jsxOpeningElement.name;
  86. } else if (type === 'JSXMemberExpression') {
  87. tagName = getTagNameFromMemberExpression(jsxOpeningElement);
  88. } else {
  89. tagName = undefined;
  90. }
  91. const isHTMLTag = tagName && tagName[0] !== tagName[0].toUpperCase();
  92. const isCustomTag = tagName && (tagName[0] === tagName[0].toUpperCase() || tagName.includes('.'));
  93. if (
  94. isHTMLTag
  95. && ((ignoreHtmlTags && !isException(tagName, exceptions))
  96. || (!ignoreHtmlTags && isException(tagName, exceptions)))
  97. ) {
  98. return;
  99. }
  100. if (
  101. isCustomTag
  102. && ((ignoreCustomTags && !isException(tagName, exceptions))
  103. || (!ignoreCustomTags && isException(tagName, exceptions)))
  104. ) {
  105. return;
  106. }
  107. if (
  108. ignoreExplicitSpread
  109. && node.argument.type === 'ObjectExpression'
  110. && node.argument.properties.every(isProperty)
  111. ) {
  112. return;
  113. }
  114. context.report({
  115. node,
  116. messageId: 'noSpreading'
  117. });
  118. }
  119. };
  120. }
  121. };