about summary refs log tree commit diff
path: root/eslint/avoid-unwrapped-text.js
blob: c9e72386eb4e6587e14464de4caa43e4e4e9b336 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
'use strict'

// Partially based on eslint-plugin-react-native.
// Portions of code by Alex Zhukov, MIT license.

function hasOnlyLineBreak(value) {
  return /^[\r\n\t\f\v]+$/.test(value.replace(/ /g, ''))
}

function getTagName(node) {
  const reversedIdentifiers = []
  if (
    node.type === 'JSXElement' &&
    node.openingElement.type === 'JSXOpeningElement'
  ) {
    let object = node.openingElement.name
    while (object.type === 'JSXMemberExpression') {
      if (object.property.type === 'JSXIdentifier') {
        reversedIdentifiers.push(object.property.name)
      }
      object = object.object
    }

    if (object.type === 'JSXIdentifier') {
      reversedIdentifiers.push(object.name)
    }
  }

  return reversedIdentifiers.reverse().join('.')
}

exports.create = function create(context) {
  const options = context.options[0] || {}
  const impliedTextProps = options.impliedTextProps ?? []
  const impliedTextComponents = options.impliedTextComponents ?? []
  const textProps = [...impliedTextProps]
  const textComponents = ['Text', ...impliedTextComponents]
  return {
    JSXText(node) {
      if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value)) {
        return
      }
      let parent = node.parent
      while (parent) {
        if (parent.type === 'JSXElement') {
          const tagName = getTagName(parent)
          if (textComponents.includes(tagName) || tagName.endsWith('Text')) {
            // We're good.
            return
          }
          if (tagName === 'Trans') {
            // Skip over it and check above.
            // TODO: Maybe validate that it's present.
            parent = parent.parent
            continue
          }
          let message = 'Wrap this string in <Text>.'
          if (tagName !== 'View') {
            message +=
              ' If <' +
              tagName +
              '> is guaranteed to render <Text>, ' +
              'rename it to <' +
              tagName +
              'Text> or add it to impliedTextComponents.'
          }
          context.report({
            node,
            message,
          })
          return
        }

        if (
          parent.type === 'JSXAttribute' &&
          parent.name.type === 'JSXIdentifier' &&
          parent.parent.type === 'JSXOpeningElement' &&
          parent.parent.parent.type === 'JSXElement'
        ) {
          const tagName = getTagName(parent.parent.parent)
          const propName = parent.name.name
          if (
            textProps.includes(tagName + ' ' + propName) ||
            propName === 'text' ||
            propName.endsWith('Text')
          ) {
            // We're good.
            return
          }
          const message =
            'Wrap this string in <Text>.' +
            ' If `' +
            propName +
            '` is guaranteed to be wrapped in <Text>, ' +
            'rename it to `' +
            propName +
            'Text' +
            '` or add it to impliedTextProps.'
          context.report({
            node,
            message,
          })
          return
        }

        parent = parent.parent
        continue
      }
    },
  }
}