| 239 | if (/setInterval\s*\([^,]+,\s*\d+\s*\)/.test(content)) { |
| 240 | violations.push({ file, lens: 'performance', rule: 'no-magic-intervals', message: 'Hardcoded setInterval — use a named constant for the interval' }); |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | return violations; |
| 245 | } |
| 246 | |
| 247 | // ── Accessibility Lens ────────────────────────────────────────────────────── |
| 248 | |
| 249 | function lensAccessibility(file, content) { |
| 250 | if (!/\.(tsx|jsx)$/.test(file)) return []; |
| 251 | |
| 252 | const violations = []; |
| 253 | |
| 254 | // Check for onClick on non-button elements without role/tabIndex |
| 255 | const onClickDivRegex = /<(?:div|span|a(?!\s+href))\s[^>]*onClick[^>]*>/g; |
| 256 | let match; |
| 257 | while ((match = onClickDivRegex.exec(content)) !== null) { |
| 258 | const tag = match[0]; |
| 259 | if (!tag.includes('role=') && !tag.includes('tabIndex')) { |
| 260 | violations.push({ |
| 261 | file, |
| 262 | lens: 'accessibility', |
| 263 | message: 'Interactive element with onClick but missing role and tabIndex', |
| 264 | }); |
| 265 | break; // One warning per file to avoid noise |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | // Check for img tags without alt attribute |
| 270 | const imgNoAlt = /<img\s(?![^>]*alt=)[^>]*>/; |
| 271 | if (imgNoAlt.test(content)) { |
| 272 | violations.push({ |
| 273 | file, |
| 274 | lens: 'accessibility', |
| 275 | message: 'img element without alt attribute', |
| 276 | }); |
| 277 | } |
| 278 | |
| 279 | // Check for icon-only buttons without aria-label |
| 280 | const iconButtonRegex = /<button\s(?![^>]*aria-label)[^>]*>[\s]*<(?:svg|Icon|img)/; |
| 281 | if (iconButtonRegex.test(content)) { |
| 282 | violations.push({ |
| 283 | file, |
| 284 | lens: 'accessibility', |
| 285 | message: 'Icon-only button without aria-label', |