Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
117
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 360 lines 11 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 32/** @type {import('eslint').Rule.RuleModule} */ 33module.exports = { 34 meta: { 35 type: 'problem', 36 docs: { 37 description: 'Enforce text strings are wrapped in <Text> components', 38 }, 39 schema: [ 40 { 41 type: 'object', 42 properties: { 43 impliedTextComponents: { 44 type: 'array', 45 items: {type: 'string'}, 46 }, 47 impliedTextProps: { 48 type: 'array', 49 items: {type: 'string'}, 50 }, 51 suggestedTextWrappers: { 52 type: 'object', 53 additionalProperties: {type: 'string'}, 54 }, 55 }, 56 additionalProperties: false, 57 }, 58 ], 59 }, 60 create(context) { 61 const options = context.options[0] || {} 62 const impliedTextProps = options.impliedTextProps ?? [] 63 const impliedTextComponents = options.impliedTextComponents ?? [] 64 const suggestedTextWrappers = options.suggestedTextWrappers ?? {} 65 const textProps = [...impliedTextProps] 66 const textComponents = ['Text', ...impliedTextComponents] 67 68 function isTextComponent(tagName) { 69 return textComponents.includes(tagName) || tagName.endsWith('Text') 70 } 71 72 return { 73 JSXText(node) { 74 if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value)) { 75 return 76 } 77 let parent = node.parent 78 while (parent) { 79 if (parent.type === 'JSXElement') { 80 const tagName = getTagName(parent) 81 if (isTextComponent(tagName)) { 82 // We're good. 83 return 84 } 85 if (tagName === 'Trans') { 86 // Exit and rely on the traversal for <Trans> JSXElement (code below). 87 // TODO: Maybe validate that it's present. 88 return 89 } 90 const suggestedWrapper = suggestedTextWrappers[tagName] 91 let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.` 92 if (tagName !== 'View' && !suggestedWrapper) { 93 message += 94 ' If <' + 95 tagName + 96 '> is guaranteed to render <Text>, ' + 97 'rename it to <' + 98 tagName + 99 'Text> or add it to impliedTextComponents.' 100 } 101 context.report({ 102 node, 103 message, 104 }) 105 return 106 } 107 108 if ( 109 parent.type === 'JSXAttribute' && 110 parent.name.type === 'JSXIdentifier' && 111 parent.parent.type === 'JSXOpeningElement' && 112 parent.parent.parent.type === 'JSXElement' 113 ) { 114 const tagName = getTagName(parent.parent.parent) 115 const propName = parent.name.name 116 if ( 117 textProps.includes(tagName + ' ' + propName) || 118 propName === 'text' || 119 propName.endsWith('Text') 120 ) { 121 // We're good. 122 return 123 } 124 const message = 125 'Wrap this string in <Text>.' + 126 ' If `' + 127 propName + 128 '` is guaranteed to be wrapped in <Text>, ' + 129 'rename it to `' + 130 propName + 131 'Text' + 132 '` or add it to impliedTextProps.' 133 context.report({ 134 node, 135 message, 136 }) 137 return 138 } 139 140 parent = parent.parent 141 continue 142 } 143 }, 144 Literal(node) { 145 if (typeof node.value !== 'string' && typeof node.value !== 'number') { 146 return 147 } 148 let parent = node.parent 149 while (parent) { 150 if (parent.type === 'JSXElement') { 151 const tagName = getTagName(parent) 152 if (isTextComponent(tagName)) { 153 // We're good. 154 return 155 } 156 if (tagName === 'Trans') { 157 // Exit and rely on the traversal for <Trans> JSXElement (code below). 158 // TODO: Maybe validate that it's present. 159 return 160 } 161 const suggestedWrapper = suggestedTextWrappers[tagName] 162 let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.` 163 if (tagName !== 'View' && !suggestedWrapper) { 164 message += 165 ' If <' + 166 tagName + 167 '> is guaranteed to render <Text>, ' + 168 'rename it to <' + 169 tagName + 170 'Text> or add it to impliedTextComponents.' 171 } 172 context.report({ 173 node, 174 message, 175 }) 176 return 177 } 178 179 if (parent.type === 'BinaryExpression' && parent.operator === '+') { 180 parent = parent.parent 181 continue 182 } 183 184 if ( 185 parent.type === 'JSXExpressionContainer' || 186 parent.type === 'LogicalExpression' 187 ) { 188 parent = parent.parent 189 continue 190 } 191 192 // Be conservative for other types. 193 return 194 } 195 }, 196 TemplateLiteral(node) { 197 let parent = node.parent 198 while (parent) { 199 if (parent.type === 'JSXElement') { 200 const tagName = getTagName(parent) 201 if (isTextComponent(tagName)) { 202 // We're good. 203 return 204 } 205 if (tagName === 'Trans') { 206 // Exit and rely on the traversal for <Trans> JSXElement (code below). 207 // TODO: Maybe validate that it's present. 208 return 209 } 210 const suggestedWrapper = suggestedTextWrappers[tagName] 211 let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.` 212 if (tagName !== 'View' && !suggestedWrapper) { 213 message += 214 ' If <' + 215 tagName + 216 '> is guaranteed to render <Text>, ' + 217 'rename it to <' + 218 tagName + 219 'Text> or add it to impliedTextComponents.' 220 } 221 context.report({ 222 node, 223 message, 224 }) 225 return 226 } 227 228 if ( 229 parent.type === 'CallExpression' && 230 parent.callee.type === 'Identifier' && 231 parent.callee.name === '_' 232 ) { 233 // This is a user-facing string, keep going up. 234 parent = parent.parent 235 continue 236 } 237 238 if (parent.type === 'BinaryExpression' && parent.operator === '+') { 239 parent = parent.parent 240 continue 241 } 242 243 if ( 244 parent.type === 'JSXExpressionContainer' || 245 parent.type === 'LogicalExpression' || 246 parent.type === 'TaggedTemplateExpression' 247 ) { 248 parent = parent.parent 249 continue 250 } 251 252 // Be conservative for other types. 253 return 254 } 255 }, 256 JSXElement(node) { 257 if (getTagName(node) !== 'Trans') { 258 return 259 } 260 let parent = node.parent 261 while (parent) { 262 if (parent.type === 'JSXElement') { 263 const tagName = getTagName(parent) 264 if (isTextComponent(tagName)) { 265 // We're good. 266 return 267 } 268 if (tagName === 'Trans') { 269 // Exit and rely on the traversal for this JSXElement. 270 // TODO: Should nested <Trans> even be allowed? 271 return 272 } 273 const suggestedWrapper = suggestedTextWrappers[tagName] 274 let message = `Wrap this <Trans> in <${suggestedWrapper ?? 'Text'}>.` 275 if (tagName !== 'View' && !suggestedWrapper) { 276 message += 277 ' If <' + 278 tagName + 279 '> is guaranteed to render <Text>, ' + 280 'rename it to <' + 281 tagName + 282 'Text> or add it to impliedTextComponents.' 283 } 284 context.report({ 285 node, 286 message, 287 }) 288 return 289 } 290 291 if ( 292 parent.type === 'JSXAttribute' && 293 parent.name.type === 'JSXIdentifier' && 294 parent.parent.type === 'JSXOpeningElement' && 295 parent.parent.parent.type === 'JSXElement' 296 ) { 297 const tagName = getTagName(parent.parent.parent) 298 const propName = parent.name.name 299 if ( 300 textProps.includes(tagName + ' ' + propName) || 301 propName === 'text' || 302 propName.endsWith('Text') 303 ) { 304 // We're good. 305 return 306 } 307 const message = 308 'Wrap this <Trans> in <Text>.' + 309 ' If `' + 310 propName + 311 '` is guaranteed to be wrapped in <Text>, ' + 312 'rename it to `' + 313 propName + 314 'Text' + 315 '` or add it to impliedTextProps.' 316 context.report({ 317 node, 318 message, 319 }) 320 return 321 } 322 323 parent = parent.parent 324 continue 325 } 326 }, 327 ReturnStatement(node) { 328 let fnScope = context.sourceCode.getScope(node) 329 while (fnScope && fnScope.type !== 'function') { 330 fnScope = fnScope.upper 331 } 332 if (!fnScope) { 333 return 334 } 335 const fn = fnScope.block 336 if (!fn.id || fn.id.type !== 'Identifier' || !fn.id.name) { 337 return 338 } 339 if (!/^[A-Z]\w*Text$/.test(fn.id.name)) { 340 return 341 } 342 if (!node.argument || node.argument.type !== 'JSXElement') { 343 return 344 } 345 const openingEl = node.argument.openingElement 346 if (openingEl.name.type !== 'JSXIdentifier') { 347 return 348 } 349 const returnedComponentName = openingEl.name.name 350 if (!isTextComponent(returnedComponentName)) { 351 context.report({ 352 node, 353 message: 354 'Components ending with *Text must return <Text> or <SomeText>.', 355 }) 356 } 357 }, 358 } 359 }, 360}