forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1'use strict'
2
3// Partially based on eslint-plugin-react-native.
4// Portions of code by Alex Zhukov, MIT license.
5
6function hasOnlyLineBreak(value) {
7 return /^[\r\n\t\f\v]+$/.test(value.replace(/ /g, ''))
8}
9
10function getTagName(node) {
11 const reversedIdentifiers = []
12 if (
13 node.type === 'JSXElement' &&
14 node.openingElement.type === 'JSXOpeningElement'
15 ) {
16 let object = node.openingElement.name
17 while (object.type === 'JSXMemberExpression') {
18 if (object.property.type === 'JSXIdentifier') {
19 reversedIdentifiers.push(object.property.name)
20 }
21 object = object.object
22 }
23
24 if (object.type === 'JSXIdentifier') {
25 reversedIdentifiers.push(object.name)
26 }
27 }
28
29 return reversedIdentifiers.reverse().join('.')
30}
31
32exports.create = function create(context) {
33 const options = context.options[0] || {}
34 const impliedTextProps = options.impliedTextProps ?? []
35 const impliedTextComponents = options.impliedTextComponents ?? []
36 const suggestedTextWrappers = options.suggestedTextWrappers ?? {}
37 const textProps = [...impliedTextProps]
38 const textComponents = ['Text', ...impliedTextComponents]
39
40 function isTextComponent(tagName) {
41 return textComponents.includes(tagName) || tagName.endsWith('Text')
42 }
43
44 return {
45 JSXText(node) {
46 if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value)) {
47 return
48 }
49 let parent = node.parent
50 while (parent) {
51 if (parent.type === 'JSXElement') {
52 const tagName = getTagName(parent)
53 if (isTextComponent(tagName)) {
54 // We're good.
55 return
56 }
57 if (tagName === 'Trans') {
58 // Exit and rely on the traversal for <Trans> JSXElement (code below).
59 // TODO: Maybe validate that it's present.
60 return
61 }
62 const suggestedWrapper = suggestedTextWrappers[tagName]
63 let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
64 if (tagName !== 'View' && !suggestedWrapper) {
65 message +=
66 ' If <' +
67 tagName +
68 '> is guaranteed to render <Text>, ' +
69 'rename it to <' +
70 tagName +
71 'Text> or add it to impliedTextComponents.'
72 }
73 context.report({
74 node,
75 message,
76 })
77 return
78 }
79
80 if (
81 parent.type === 'JSXAttribute' &&
82 parent.name.type === 'JSXIdentifier' &&
83 parent.parent.type === 'JSXOpeningElement' &&
84 parent.parent.parent.type === 'JSXElement'
85 ) {
86 const tagName = getTagName(parent.parent.parent)
87 const propName = parent.name.name
88 if (
89 textProps.includes(tagName + ' ' + propName) ||
90 propName === 'text' ||
91 propName.endsWith('Text')
92 ) {
93 // We're good.
94 return
95 }
96 const message =
97 'Wrap this string in <Text>.' +
98 ' If `' +
99 propName +
100 '` is guaranteed to be wrapped in <Text>, ' +
101 'rename it to `' +
102 propName +
103 'Text' +
104 '` or add it to impliedTextProps.'
105 context.report({
106 node,
107 message,
108 })
109 return
110 }
111
112 parent = parent.parent
113 continue
114 }
115 },
116 Literal(node) {
117 if (typeof node.value !== 'string' && typeof node.value !== 'number') {
118 return
119 }
120 let parent = node.parent
121 while (parent) {
122 if (parent.type === 'JSXElement') {
123 const tagName = getTagName(parent)
124 if (isTextComponent(tagName)) {
125 // We're good.
126 return
127 }
128 if (tagName === 'Trans') {
129 // Exit and rely on the traversal for <Trans> JSXElement (code below).
130 // TODO: Maybe validate that it's present.
131 return
132 }
133 const suggestedWrapper = suggestedTextWrappers[tagName]
134 let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
135 if (tagName !== 'View' && !suggestedWrapper) {
136 message +=
137 ' If <' +
138 tagName +
139 '> is guaranteed to render <Text>, ' +
140 'rename it to <' +
141 tagName +
142 'Text> or add it to impliedTextComponents.'
143 }
144 context.report({
145 node,
146 message,
147 })
148 return
149 }
150
151 if (parent.type === 'BinaryExpression' && parent.operator === '+') {
152 parent = parent.parent
153 continue
154 }
155
156 if (
157 parent.type === 'JSXExpressionContainer' ||
158 parent.type === 'LogicalExpression'
159 ) {
160 parent = parent.parent
161 continue
162 }
163
164 // Be conservative for other types.
165 return
166 }
167 },
168 TemplateLiteral(node) {
169 let parent = node.parent
170 while (parent) {
171 if (parent.type === 'JSXElement') {
172 const tagName = getTagName(parent)
173 if (isTextComponent(tagName)) {
174 // We're good.
175 return
176 }
177 if (tagName === 'Trans') {
178 // Exit and rely on the traversal for <Trans> JSXElement (code below).
179 // TODO: Maybe validate that it's present.
180 return
181 }
182 const suggestedWrapper = suggestedTextWrappers[tagName]
183 let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
184 if (tagName !== 'View' && !suggestedWrapper) {
185 message +=
186 ' If <' +
187 tagName +
188 '> is guaranteed to render <Text>, ' +
189 'rename it to <' +
190 tagName +
191 'Text> or add it to impliedTextComponents.'
192 }
193 context.report({
194 node,
195 message,
196 })
197 return
198 }
199
200 if (
201 parent.type === 'CallExpression' &&
202 parent.callee.type === 'Identifier' &&
203 parent.callee.name === '_'
204 ) {
205 // This is a user-facing string, keep going up.
206 parent = parent.parent
207 continue
208 }
209
210 if (parent.type === 'BinaryExpression' && parent.operator === '+') {
211 parent = parent.parent
212 continue
213 }
214
215 if (
216 parent.type === 'JSXExpressionContainer' ||
217 parent.type === 'LogicalExpression' ||
218 parent.type === 'TaggedTemplateExpression'
219 ) {
220 parent = parent.parent
221 continue
222 }
223
224 // Be conservative for other types.
225 return
226 }
227 },
228 JSXElement(node) {
229 if (getTagName(node) !== 'Trans') {
230 return
231 }
232 let parent = node.parent
233 while (parent) {
234 if (parent.type === 'JSXElement') {
235 const tagName = getTagName(parent)
236 if (isTextComponent(tagName)) {
237 // We're good.
238 return
239 }
240 if (tagName === 'Trans') {
241 // Exit and rely on the traversal for this JSXElement.
242 // TODO: Should nested <Trans> even be allowed?
243 return
244 }
245 const suggestedWrapper = suggestedTextWrappers[tagName]
246 let message = `Wrap this <Trans> in <${suggestedWrapper ?? 'Text'}>.`
247 if (tagName !== 'View' && !suggestedWrapper) {
248 message +=
249 ' If <' +
250 tagName +
251 '> is guaranteed to render <Text>, ' +
252 'rename it to <' +
253 tagName +
254 'Text> or add it to impliedTextComponents.'
255 }
256 context.report({
257 node,
258 message,
259 })
260 return
261 }
262
263 if (
264 parent.type === 'JSXAttribute' &&
265 parent.name.type === 'JSXIdentifier' &&
266 parent.parent.type === 'JSXOpeningElement' &&
267 parent.parent.parent.type === 'JSXElement'
268 ) {
269 const tagName = getTagName(parent.parent.parent)
270 const propName = parent.name.name
271 if (
272 textProps.includes(tagName + ' ' + propName) ||
273 propName === 'text' ||
274 propName.endsWith('Text')
275 ) {
276 // We're good.
277 return
278 }
279 const message =
280 'Wrap this <Trans> in <Text>.' +
281 ' If `' +
282 propName +
283 '` is guaranteed to be wrapped in <Text>, ' +
284 'rename it to `' +
285 propName +
286 'Text' +
287 '` or add it to impliedTextProps.'
288 context.report({
289 node,
290 message,
291 })
292 return
293 }
294
295 parent = parent.parent
296 continue
297 }
298 },
299 ReturnStatement(node) {
300 let fnScope = context.getScope()
301 while (fnScope && fnScope.type !== 'function') {
302 fnScope = fnScope.upper
303 }
304 if (!fnScope) {
305 return
306 }
307 const fn = fnScope.block
308 if (!fn.id || fn.id.type !== 'Identifier' || !fn.id.name) {
309 return
310 }
311 if (!/^[A-Z]\w*Text$/.test(fn.id.name)) {
312 return
313 }
314 if (!node.argument || node.argument.type !== 'JSXElement') {
315 return
316 }
317 const openingEl = node.argument.openingElement
318 if (openingEl.name.type !== 'JSXIdentifier') {
319 return
320 }
321 const returnedComponentName = openingEl.name.name
322 if (!isTextComponent(returnedComponentName)) {
323 context.report({
324 node,
325 message:
326 'Components ending with *Text must return <Text> or <SomeText>.',
327 })
328 }
329 },
330 }
331}