jsx-no-literals.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /**
  2. * @fileoverview Prevent using string literals in React component definition
  3. * @author Caleb Morris
  4. * @author David Buchan-Swanson
  5. */
  6. 'use strict';
  7. const iterFrom = require('es-iterator-helpers/Iterator.from');
  8. const map = require('es-iterator-helpers/Iterator.prototype.map');
  9. const docsUrl = require('../util/docsUrl');
  10. const report = require('../util/report');
  11. // ------------------------------------------------------------------------------
  12. // Rule Definition
  13. // ------------------------------------------------------------------------------
  14. function trimIfString(val) {
  15. return typeof val === 'string' ? val.trim() : val;
  16. }
  17. const messages = {
  18. invalidPropValue: 'Invalid prop value: "{{text}}"',
  19. noStringsInAttributes: 'Strings not allowed in attributes: "{{text}}"',
  20. noStringsInJSX: 'Strings not allowed in JSX files: "{{text}}"',
  21. literalNotInJSXExpression: 'Missing JSX expression container around literal string: "{{text}}"',
  22. };
  23. module.exports = {
  24. meta: {
  25. docs: {
  26. description: 'Disallow usage of string literals in JSX',
  27. category: 'Stylistic Issues',
  28. recommended: false,
  29. url: docsUrl('jsx-no-literals'),
  30. },
  31. messages,
  32. schema: [{
  33. type: 'object',
  34. properties: {
  35. noStrings: {
  36. type: 'boolean',
  37. },
  38. allowedStrings: {
  39. type: 'array',
  40. uniqueItems: true,
  41. items: {
  42. type: 'string',
  43. },
  44. },
  45. ignoreProps: {
  46. type: 'boolean',
  47. },
  48. noAttributeStrings: {
  49. type: 'boolean',
  50. },
  51. },
  52. additionalProperties: false,
  53. }],
  54. },
  55. create(context) {
  56. const defaults = {
  57. noStrings: false,
  58. allowedStrings: [],
  59. ignoreProps: false,
  60. noAttributeStrings: false,
  61. };
  62. const config = Object.assign({}, defaults, context.options[0] || {});
  63. config.allowedStrings = new Set(map(iterFrom(config.allowedStrings), trimIfString));
  64. function defaultMessageId() {
  65. const ancestorIsJSXElement = arguments.length >= 1 && arguments[0];
  66. if (config.noAttributeStrings && !ancestorIsJSXElement) {
  67. return 'noStringsInAttributes';
  68. }
  69. if (config.noStrings) {
  70. return 'noStringsInJSX';
  71. }
  72. return 'literalNotInJSXExpression';
  73. }
  74. function getParentIgnoringBinaryExpressions(node) {
  75. let current = node;
  76. while (current.parent.type === 'BinaryExpression') {
  77. current = current.parent;
  78. }
  79. return current.parent;
  80. }
  81. function getValidation(node) {
  82. const values = [trimIfString(node.raw), trimIfString(node.value)];
  83. if (values.some((value) => config.allowedStrings.has(value))) {
  84. return false;
  85. }
  86. const parent = getParentIgnoringBinaryExpressions(node);
  87. function isParentNodeStandard() {
  88. if (!/^[\s]+$/.test(node.value) && typeof node.value === 'string' && parent.type.includes('JSX')) {
  89. if (config.noAttributeStrings) {
  90. return parent.type === 'JSXAttribute' || parent.type === 'JSXElement';
  91. }
  92. if (!config.noAttributeStrings) {
  93. return parent.type !== 'JSXAttribute';
  94. }
  95. }
  96. return false;
  97. }
  98. const standard = isParentNodeStandard();
  99. if (config.noStrings) {
  100. return standard;
  101. }
  102. return standard && parent.type !== 'JSXExpressionContainer';
  103. }
  104. function getParentAndGrandParentType(node) {
  105. const parent = getParentIgnoringBinaryExpressions(node);
  106. const parentType = parent.type;
  107. const grandParentType = parent.parent.type;
  108. return {
  109. parent,
  110. parentType,
  111. grandParentType,
  112. grandParent: parent.parent,
  113. };
  114. }
  115. function hasJSXElementParentOrGrandParent(node) {
  116. const parents = getParentAndGrandParentType(node);
  117. const parentType = parents.parentType;
  118. const grandParentType = parents.grandParentType;
  119. return parentType === 'JSXFragment' || parentType === 'JSXElement' || grandParentType === 'JSXElement';
  120. }
  121. function reportLiteralNode(node, messageId) {
  122. const ancestorIsJSXElement = hasJSXElementParentOrGrandParent(node);
  123. messageId = messageId || defaultMessageId(ancestorIsJSXElement);
  124. report(context, messages[messageId], messageId, {
  125. node,
  126. data: {
  127. text: context.getSourceCode().getText(node).trim(),
  128. },
  129. });
  130. }
  131. // --------------------------------------------------------------------------
  132. // Public
  133. // --------------------------------------------------------------------------
  134. return {
  135. Literal(node) {
  136. if (getValidation(node) && (hasJSXElementParentOrGrandParent(node) || !config.ignoreProps)) {
  137. reportLiteralNode(node);
  138. }
  139. },
  140. JSXAttribute(node) {
  141. const isNodeValueString = node && node.value && node.value.type === 'Literal' && typeof node.value.value === 'string' && !config.allowedStrings.has(node.value.value);
  142. if (config.noStrings && !config.ignoreProps && isNodeValueString) {
  143. const messageId = 'invalidPropValue';
  144. reportLiteralNode(node, messageId);
  145. }
  146. },
  147. JSXText(node) {
  148. if (getValidation(node)) {
  149. reportLiteralNode(node);
  150. }
  151. },
  152. TemplateLiteral(node) {
  153. const parents = getParentAndGrandParentType(node);
  154. const parentType = parents.parentType;
  155. const grandParentType = parents.grandParentType;
  156. const isParentJSXExpressionCont = parentType === 'JSXExpressionContainer';
  157. const isParentJSXElement = parentType === 'JSXElement' || grandParentType === 'JSXElement';
  158. if (isParentJSXExpressionCont && config.noStrings && (isParentJSXElement || !config.ignoreProps)) {
  159. reportLiteralNode(node);
  160. }
  161. },
  162. };
  163. },
  164. };