no-raw-text.js 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. /**
  2. * @fileoverview Detects raw text outside of Text component
  3. * @author Alex Zhukov
  4. */
  5. 'use strict';
  6. const { default: traverse } = require('@babel/traverse');
  7. const elementName = (node, scope) => {
  8. const identifiers = [];
  9. traverse(node, {
  10. JSXOpeningElement({ node: element }) {
  11. traverse(element, {
  12. JSXIdentifier({ node: identifier }) {
  13. if (identifier.parent.type === 'JSXOpeningElement'
  14. || identifier.parent.type === 'JSXMemberExpression') {
  15. identifiers.push(identifier.name);
  16. }
  17. },
  18. }, scope);
  19. },
  20. }, scope);
  21. return identifiers.join('.');
  22. };
  23. function create(context) {
  24. const options = context.options[0] || {};
  25. const report = (node) => {
  26. const errorValue = node.type === 'TemplateLiteral'
  27. ? `TemplateLiteral: ${node.expressions[0].name}`
  28. : node.value.trim();
  29. const formattedErrorValue = errorValue.length > 0
  30. ? `Raw text (${errorValue})`
  31. : 'Whitespace(s)';
  32. context.report({
  33. node,
  34. message: `${formattedErrorValue} cannot be used outside of a <Text> tag`,
  35. });
  36. };
  37. const skippedElements = options.skip ? options.skip : [];
  38. const allowedElements = ['Text', 'TSpan', 'StyledText', 'Animated.Text'].concat(skippedElements);
  39. const hasOnlyLineBreak = (value) => /^[\r\n\t\f\v]+$/.test(value.replace(/ /g, ''));
  40. const scope = context.getScope();
  41. const getValidation = (node) => !allowedElements.includes(elementName(node.parent, scope));
  42. return {
  43. Literal(node) {
  44. const parentType = node.parent.type;
  45. const onlyFor = ['JSXExpressionContainer', 'JSXElement'];
  46. if (typeof node.value !== 'string'
  47. || hasOnlyLineBreak(node.value)
  48. || !onlyFor.includes(parentType)
  49. || (node.parent.parent && node.parent.parent.type === 'JSXAttribute')
  50. ) return;
  51. const isStringLiteral = parentType === 'JSXExpressionContainer';
  52. if (getValidation(isStringLiteral ? node.parent : node)) {
  53. report(node);
  54. }
  55. },
  56. JSXText(node) {
  57. if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value)) return;
  58. if (getValidation(node)) {
  59. report(node);
  60. }
  61. },
  62. TemplateLiteral(node) {
  63. if (
  64. node.parent.type !== 'JSXExpressionContainer'
  65. || (node.parent.parent && node.parent.parent.type === 'JSXAttribute')
  66. ) return;
  67. if (getValidation(node.parent)) {
  68. report(node);
  69. }
  70. },
  71. };
  72. }
  73. module.exports = {
  74. meta: {
  75. schema: [
  76. {
  77. type: 'object',
  78. properties: {
  79. skip: {
  80. type: 'array',
  81. items: {
  82. type: 'string',
  83. },
  84. },
  85. },
  86. additionalProperties: false,
  87. },
  88. ],
  89. },
  90. create,
  91. };