Bluesky app fork with some witchin' additions 馃挮
at main 9.7 kB view raw
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}