oxlint-plugin-react-doctor 0.9.18-dev.a1df4e5 → 0.9.18-dev.a4bcd7f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,7 +22,7 @@ import*as e from"node:fs";import{readFileSync as t}from"node:fs";import*as n fro
22
22
  `;)t[n]=` `,n+=1;continue}if(r===`/`&&e[n+1]===`*`){for(;n<e.length;){if(e[n]===`*`&&e[n+1]===`/`){t[n]=` `,t[n+1]=` `,n+=2;break}e[n]!==`
23
23
  `&&(t[n]=` `),n+=1}continue}if(r===`'`){for(t[n]=` `,n+=1;n<e.length;){if(e[n]===`'`){if(e[n+1]===`'`){t[n]=` `,t[n+1]=` `,n+=2;continue}t[n]=` `,n+=1;break}e[n]!==`
24
24
  `&&(t[n]=` `),n+=1}continue}if(r===`$`){let r=Sg.exec(e.slice(n));if(r!==null){let i=r[0],a=e.indexOf(i,n+i.length),o=a<0?e.length:a+i.length,s=wg(e,n);if(Cg.has(s))Tg(e,t,n+i.length,o);else for(let r=n;r<o;r+=1)e[r]!==`
25
- `&&(t[r]=` `);n=o;continue}}if(r===`"`){for(n+=1;n<e.length;){if(e[n]===`"`){if(e[n+1]===`"`){n+=2;continue}n+=1;break}n+=1}continue}n+=1}return t.join(``)},Dg=/disable\s+row\s+level\s+security/i,Og=/auth\.role\(\)\s*=\s*["']service_role["']/i,kg=/create\s+policy/gi,Ag=/;|create\s+policy/i,jg=/\b(?:using|with\s+check)\s*\(\s*true\s*\)/i,Mg=/\bfor\s+select\b/i,Ng=/\bto\s+([\s\S]+?)(?=\s+(?:using|with\s+check|as|for)\b|;|$)/i,Pg=new Set([`service_role`,`postgres`,`supabase_admin`]),Fg=e=>{let t=Ng.exec(e);if(t===null)return!1;let n=t[1].split(`,`).map(e=>e.trim().replace(/["'`]/g,``).toLowerCase()).filter(Boolean);return n.length>0&&n.every(e=>Pg.has(e))},Ig=(e,t)=>Og.test(t)?!0:!jg.test(e)||Mg.test(e)?!1:!Fg(e),Lg=W({id:`supabase-rls-policy-risk`,title:`Permissive Supabase RLS policy`,severity:`error`,recommendation:"Keep public-read policies explicit, but gate inserts, updates, deletes, and service-role bypasses behind `auth.uid()` plus trusted tenant membership.",scan:e=>{if(!xg(e.relativePath))return[];let t=Eg(e.content),n=t.search(Dg);kg.lastIndex=0;for(let r=kg.exec(t);r!==null;r=kg.exec(t)){let i=r.index+r[0].length,a=t.slice(i).search(Ag),o=a<0?t.length:i+a;if(Ig(t.slice(r.index,o),e.content.slice(r.index,o))){(n<0||r.index<n)&&(n=r.index);break}}if(n<0)return[];let{line:r,column:i}=G(t,n);return[{message:`Supabase policy SQL disables RLS, permits writes broadly, or references a service-role bypass.`,line:r,column:i}]}}),Rg=/create\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(?!["`]?(?:auth|storage|realtime|vault|extensions|graphql|graphql_public|pgbouncer|net|supabase_functions|supabase_migrations|cron|pgsodium|pgmq|information_schema|pg_catalog|pg_temp|private|internal)["`]?\s*\.)(?:["`]?public["`]?\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?(?:\s*\(|\s+as\b)/gi,zg=/alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?(?:["`]?public["`]?\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?\s+(?:force\s+)?enable\s+row\s+level\s+security/gi,Bg=/\benable\s+row\s+level\s+security\b/gi,Vg=e=>{let t=new Map,n=0;for(let r of e.matchAll(zg)){let e=r[1];e!==void 0&&(n+=1,t.set(e.toLowerCase(),r.index))}return{lastEnableIndexByTable:t,staticEnableCount:n}},Hg=(e,t)=>(e.match(Bg)?.length??0)>t,Ug=W({id:`supabase-table-missing-rls`,title:`Supabase table created without Row Level Security`,severity:`error`,recommendation:"Enable RLS in the same migration (`alter table <name> enable row level security;`) and add `auth.uid()`-scoped policies for select/insert/update/delete. A public table without RLS is fully readable and writable with the public anon key.",scan:e=>{if(!bg(e.relativePath))return[];let t=Eg(e.content);if(!/create\s+(?:unlogged\s+)?table/i.test(t))return[];let n=[],{lastEnableIndexByTable:r,staticEnableCount:i}=Vg(t);if(Hg(t,i))return[];Rg.lastIndex=0;for(let e=Rg.exec(t);e!==null;e=Rg.exec(t)){let i=e[1];if(i===void 0)continue;let a=r.get(i.toLowerCase());if(a!==void 0&&a>=e.index)continue;let o=G(t,e.index);n.push({message:`Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.`,line:o.line,column:o.column})}return n}}),Wg=W({id:`svg-filter-clickjacking-risk`,title:`SVG-filtered iframe clickjacking primitive`,severity:`warn`,recommendation:"Avoid filtering cross-origin iframes. Use `frame-ancestors` on sensitive pages and keep SVG filters away from embedded privileged UI.",scan:q({shouldScan:e=>K(e.relativePath),pattern:/<iframe\b(?:=>|[^>]){0,300}\bfilter\s*:\s*["']?url\(#|filter\s*:\s*url\(#[\s\S]{0,700}<iframe\b|<fe(?:DisplacementMap|ColorMatrix|Composite|Tile|Morphology)\b[\s\S]{0,700}<iframe\b/i,message:`An iframe is rendered through an SVG/CSS filter, which can support advanced clickjacking or visual exfiltration tricks.`})}),Gg=e=>K(e)?nu.test(e)?!0:/(?:^|\/)(?:middleware|route)\.[cm]?[jt]sx?$/.test(e):!1,Kg=W({id:`tenant-static-proxy-risk`,title:`Tenant-controlled static asset proxy`,severity:`warn`,recommendation:`Bind tenant identity to the trusted host or authenticated org, canonicalize after decoding, reject traversal, and never let one tenant choose another tenant's asset prefix.`,scan:q({shouldScan:e=>Gg(e.relativePath),pattern:/\b(?:fetch|path\.join|getObject\w*|GetObjectCommand|getSignedUrl|createReadStream)\s*\([^;]{0,200}(?:\$\{[^}]{0,100}\b(?:tenant|subdomain|workspace|hostPattern|(?<!\.)organization(?:Id|Slug)?)\b|\b(?:tenant|subdomain|workspace)(?:Id|Slug|Name)?\b\s*[,)+\].])/i,message:`Route code appears to compose tenant or subdomain input into a static/CDN/object-store fetch path.`})}),qg=[/dangerouslySetInnerHTML\s*=\s*\{\{\s*__html\s*:[\s\S]{0,300}?\bJSON\.stringify\s*\(/gi,/<script\b[^>]*>(?:(?!<\/script>)[\s\S]){0,300}?\bJSON\.stringify\s*\(/gi],Jg=/\$\{|["'`]\s*\+\s*$/,Yg=/^\s*\{(?!\{)/,Xg=/"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'/g,Zg=/\b(?:true|false|null)\b|[\d.]+(?:e[+-]?\d+)?/gi,Qg=e=>{if(e.includes("`")||e.includes("${"))return!1;let t=e.replace(Xg,``).replace(Zg,``);return!/[A-Za-z_$]/.test(t)},$g=/^[\s)]*\.replace\s*\([^)]*(?:\\u003[cC]|&lt;|<)/,e_=/(?:\b(?:escapeHtml|escapeJSON|escapeJson|htmlEscape|jsesc)|(?<![.\w])(?:serialize|serializeJavascript|devalue|uneval|superjson))\s*\(\s*$/i,t_=/\bJSON\.stringify\s*\($/i,n_=W({id:`unsafe-json-in-html`,title:`Unescaped JSON in HTML or script sink`,severity:`warn`,recommendation:'JSON.stringify does not HTML-escape, so a `<\/script>` (or `<`) in the data breaks out and becomes XSS. Use an HTML-safe serializer (serialize-javascript, devalue) or escape `<`, `>`, and `&`, or pass data via a JSON `<script type="application/json">` read with JSON.parse.',scan:e=>{if(!K(e.relativePath))return[];let t=Bu(e);if(!t.includes(`JSON.stringify`))return[];let n=[],r=new Set;for(let[e,i]of qg.entries()){i.lastIndex=0;for(let a=i.exec(t);a!==null;a=i.exec(t)){let i=a[0].replace(t_,``);if(e_.test(i))continue;if(e===1){let e=t.indexOf(`>`,a.index),n=a[0].search(/\bJSON\.stringify\s*\($/i);if(e>=0&&n>=0){let r=t.slice(e+1,a.index+n);if(!Jg.test(r)||Yg.test(r))continue}}let o=a.index+a[0].length-1,s=vf(t,o);if(s>=0){let e=t.slice(s+1,s+1+160);if($g.test(e)||Qg(t.slice(o+1,s)))continue}if(r.has(a.index))continue;r.add(a.index);let c=G(t,a.index);n.push({message:"JSON.stringify is embedded in HTML/script markup without HTML-escaping; data containing `<\/script>` or `<` breaks out and becomes XSS.",line:c.line,column:c.column})}}return n}}),r_=/(?:(?<![.\w$])fetch|\baxios\.\s*(?:get|post|put|delete|head)|\bgot|\bgot\.\s*(?:get|post))\s*\(\s*([^,)]+)/,i_=/\b(?:url|targetUrl|callbackUrl|redirectUrl|webhookUrl|companyUrl|websiteUrl|domainUrl|imageUrl|fetchUrl|next|return_to|returnTo|destination|location)\b/i,a_=/\breq\.|\brequest\.(?:query|body|params|nextUrl)|\bsearchParams\b|\bparams\.|\bbody\.|\bquery\./,o_=/\bredirect\s*:\s*["'](?:manual|error)["']/,s_=/\b(params|body|query)\./g,c_=(e,t)=>RegExp(`(?:const|let|var)\\s+${e}\\s*(?::[^=;\\n]{0,80})?=\\s*new\\s+URLSearchParams\\b`).test(t),l_=(e,t)=>{let n=e.replace(s_,(e,n)=>c_(n,t)?``:e);return a_.test(n)},u_=(e,t)=>{if(l_(e,t))return!0;let n=/^[\w$]+$/.exec(e.trim());if(n===null)return!1;let r=RegExp(`(?:const|let|var)[^=;\\n]{0,80}\\b${n[0]}\\b[^=;\\n]{0,80}=([^;\\n]*)`,`g`);for(let e of t.matchAll(r)){let n=e[1]??``;if(/\breq\.|\brequest\.|\bsearchParams\b|\$_(?:GET|POST|REQUEST)/.test(n)||l_(n,t))return!0}return!1},d_=W({id:`untrusted-redirect-following`,title:`Server fetch follows redirects for caller-shaped URL`,severity:`warn`,recommendation:'Use `redirect: "manual"` or equivalent and re-validate every redirect target before following it to avoid SSRF redirect bypasses.',scan:e=>{if(!Gg(e.relativePath)||!r_.test(e.content))return[];let t=[],n=e.content.split(`
26
- `);for(let r=0;r<n.length;r+=1){let i=n[r]??``,a=i.match(r_),o=a?.[1]??``;if(!a||!i_.test(o)||!u_(o,e.content))continue;let s=n.slice(r,r+5).join(`
27
- `);o_.test(s)||t.push({message:`Server-side fetch code appears to follow redirects for a URL shaped like caller-controlled input.`,line:r+1,column:i.search(/\S/)+1})}return t}}),f_=W({id:`url-prefilled-privileged-action`,title:`URL pre-fills a privileged action`,severity:`warn`,recommendation:`Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.`,scan:q({shouldScan:e=>Cm(e.relativePath),pattern:/(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist|parse|normaliz)[\w$]*\(\s*(?:new\s+)?(?:[\w$]+\s*\.\s*){0,4})\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,message:`Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values.`})}),p_=/(?:^|\/)[^/]*webhook[^/]*\/|(?:^|\/)[^/]*webhook[^/]*\.[cm]?[jt]s$|\bwebhook\b/i,m_=/\b(?:export\s+(?:async\s+)?function\s+POST|export\s+const\s+(?:POST|handler|webhook)|webhookHandler|webhookRoute)\b/i,h_=RegExp(`verifySignature|verify.*signature|verify\\w*(?:Webhook|Auth)|constructEvent|createHmac|timingSafeEqual|svix|webhookSecret|stripe\\.webhooks|["'][\\w-]*signature["']|\\b[A-Za-z]{0,40}(?:verif|valid|check|assert|authenticat|compare|guard)[A-Za-z]{0,40}(?:secret|signature|hmac|webhook|digest)[A-Za-z]{0,40}\\s*\\(`,`i`),g_=/webhook[\s_-]?ur[il]\w*/gi,__=/process\.env\.\w*WEBHOOK_URL|\b(?:send|post|dispatch|publish|notify)\w*Webhook/,v_=/\b(?:req|request)\b/,y_=/\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|`(?:\\.|[^`\\])*`/g,b_=W({id:`webhook-signature-risk`,title:`Webhook handler lacks signature verification`,severity:`warn`,recommendation:`Verify provider signatures before parsing or acting on webhook bodies. Use provider SDK helpers or HMAC verification with timing-safe comparison.`,scan:q({shouldScan:e=>{if(!K(e.relativePath)||__.test(e.content))return!1;let t=e.content.replace(y_,``).replace(g_,``);return p_.test(e.relativePath)||p_.test(t)},pattern:m_,requireAll:[v_],suppressWhen:h_,message:`Webhook handler code does not show an obvious signature verification step.`})}),x_=/\.[cm]?[jt]sx?$/i,S_=/\.d\.[cm]?[jt]s$/i,C_=/\.mdx$/i,w_=/\.html?$/i,T_=new Set([`.angular`,`.astro`,`.cache`,`.contentlayer`,`.docusaurus`,`.expo`,`.git`,`.next`,`.nuxt`,`.output`,`.svelte-kit`,`.turbo`,`.vercel`,`build`,`coverage`,`dist`,`node_modules`,`out`,`storybook-static`]),E_=new WeakMap,D_=e=>{let t=E_.get(e);if(t)return t;let n=A(e);return E_.set(e,n),n},O_=t=>{let r=[],i=[],a=[t],o=!1;for(;a.length>0;){let t=a.pop();if(!t)continue;let s;try{s=e.readdirSync(t,{withFileTypes:!0})}catch{return null}for(let e of s){let s=n.join(t,e.name),c=T_.has(e.name)||e.name.startsWith(`.`)&&e.name!==`.dumi`&&e.name!==`.storybook`;if(!(e.isSymbolicLink()&&c)){if(e.isSymbolicLink())return null;if(e.isDirectory()){if(c)continue;a.push(s);continue}if(!(!e.isFile()||kl(s))){if(C_.test(e.name)){o=!0;continue}if(w_.test(e.name)){i.push(Xa(s));continue}x_.test(e.name)&&(S_.test(e.name)||r.push(Xa(s)))}}}}return{sourceFilePaths:r,htmlFilePaths:i,hasOpaqueMdxConsumerSurface:o}},k_=(t,n)=>{for(let r of t){let t;try{t=e.readFileSync(r,`utf8`)}catch{return!0}for(let e of n)if(t.includes(`"${e}"`)||t.includes(`'${e}'`))return!0}return!1},A_=e=>{if(b(e,`ImportDeclaration`))return cl(e)?null:typeof e.source.value==`string`?e.source.value:null;if(b(e,`ExportNamedDeclaration`))return e.exportKind===`type`||sl(e.specifiers,`ExportSpecifier`,`exportKind`)?null:e.source&&typeof e.source.value==`string`?e.source.value:null;if(b(e,`ExportAllDeclaration`))return e.exportKind===`type`?null:typeof e.source.value==`string`?e.source.value:null;if(b(e,`ImportExpression`))return b(e.source,`Literal`)&&typeof e.source.value==`string`?e.source.value:null;if(b(e,`CallExpression`)&&b(e.callee,`Identifier`)&&e.callee.name===`require`&&e.arguments.length===1){let t=e.arguments[0];return t&&b(t,`Literal`)&&typeof t.value==`string`?t.value:null}return null},j_=(e,t,n,r)=>{z(e.programNode,i=>{let a=A_(i);if(!a)return;let o=rr(e.filePath,a);if(!o){r.add(a);return}let s=Xa(o);n.set(i,s);let c=t.get(s)??new Set;c.add(e),t.set(s,c)})},M_=({rootDirectory:e,currentFilePath:t,currentProgramNode:n,currentScopes:r,requiredModuleSources:i=[],knownModuleSources:a})=>{if(i.length>0&&a!==void 0&&!i.some(e=>a.includes(e)))return null;let o=O_(e);if(!o||i.length>0&&a===void 0&&!k_(o.sourceFilePaths,i))return null;let s=new Map;for(let e of o.sourceFilePaths){if(e===t){s.set(e,{filePath:e,programNode:n,scopes:r});continue}let i=yn(e);if(!i||!b(i,`Program`))return null;s.set(e,{filePath:e,programNode:i,scopes:D_(i)})}let c=new Map,l=new WeakMap,u=new Set;for(let e of s.values())j_(e,c,l,u);return{modulesByFilePath:s,htmlFilePaths:o.htmlFilePaths,consumerModulesByFilePath:c,resolvedSourcePathByNode:l,unresolvedRuntimeSources:u,hasOpaqueMdxConsumerSurface:o.hasOpaqueMdxConsumerSurface}},N_=e=>{if(!e)return null;let t=w(e);return!b(t,`Literal`)||typeof t.value!=`boolean`?null:t.value},P_=(e,t,n={})=>{let r=n.maximumConstAliases===void 0?4:n.maximumConstAliases,i=n.shouldFoldStaticConditions??!1,a=n.shouldKeepKnownValues??!1,o=[],s=[{expression:e,remainingConstAliases:r,resolvingSymbols:new Set}];for(;s.length>0;){let e=s.pop();if(!e)continue;let n=w(e.expression);if(b(n,`Literal`)){if(typeof n.value!=`string`){if(a)continue;return null}o.push(n.value);continue}if(b(n,`TemplateLiteral`)){let e=Wt(n);if(e===null){if(a)continue;return null}o.push(e);continue}if(b(n,`ConditionalExpression`)){let t=i?N_(n.test):null;if(t!==null){s.push({expression:t?n.consequent:n.alternate,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:e.resolvingSymbols});continue}s.push({expression:n.alternate,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:new Set(e.resolvingSymbols)}),s.push({expression:n.consequent,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:new Set(e.resolvingSymbols)});continue}if(a&&b(n,`LogicalExpression`)&&(n.operator===`??`||n.operator===`||`)){s.push({expression:n.right,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:new Set(e.resolvingSymbols)}),s.push({expression:n.left,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:new Set(e.resolvingSymbols)});continue}if(b(n,`Identifier`)){let r=t.referenceFor(n)?.resolvedSymbol;if(!r||r.kind!==`const`||!r.initializer||!b(r.declarationNode,`VariableDeclarator`)||r.declarationNode.id!==r.bindingIdentifier||e.remainingConstAliases===0||e.resolvingSymbols.has(r)){if(a)continue;return null}e.resolvingSymbols.add(r),s.push({expression:r.initializer,remainingConstAliases:e.remainingConstAliases===null?null:e.remainingConstAliases-1,resolvingSymbols:e.resolvingSymbols});continue}if(!a)return null}return o.length>0?o:null},F_=(e,t,n={})=>P_(e,t,n),I_=(e,t)=>P_(e,t,{shouldKeepKnownValues:!0}),L_=(e,t,n,r,i=!1)=>{let a=e.value;return a?b(a,`Literal`)?typeof a.value==`string`?[a.value]:null:b(a,`JSXExpressionContainer`)?F_(a.expression,t,{maximumConstAliases:n,shouldFoldStaticConditions:r,shouldKeepKnownValues:i}):null:null},R_=(e,t)=>L_(e,t,4,!1),z_=(e,t)=>L_(e,t,null,!0),B_=(e,t)=>L_(e,t,4,!1,!0),V_=(e,t)=>{let r=n.relative(t,e);return r===``||!r.startsWith(`..${n.sep}`)&&r!==`..`&&!n.isAbsolute(r)},H_={attribute:null,expression:null,isPresent:!1,isUnknown:!1},U_={attribute:null,expression:null,isPresent:!1,isUnknown:!0},W_=(e,t,n)=>{for(let r=e.properties.length-1;r>=0;--r){let i=e.properties[r];if(!i)return U_;if(b(i,`SpreadElement`)){if(!b(i.argument,`ObjectExpression`))return U_;let e=W_(i.argument,t,n);if(e.isPresent||e.isUnknown)return e;continue}if(!b(i,`Property`))return U_;let a=S(i,{allowComputedString:!0});if(!a)return U_;if((n?a:a.toLowerCase())===t)return{attribute:null,expression:i.value,isPresent:!0,isUnknown:!1}}return H_},G_=(e,t,n=!0)=>{let r=n?t:t.toLowerCase();for(let t=e.length-1;t>=0;--t){let i=e[t];if(!i)return U_;if(b(i,`JSXSpreadAttribute`)){if(!b(i.argument,`ObjectExpression`))return U_;let e=W_(i.argument,r,n);if(e.isPresent||e.isUnknown)return e;continue}if(!b(i,`JSXAttribute`))continue;let a=Ds(i.name);if((n?a:a?.toLowerCase())===r)return{attribute:i,expression:null,isPresent:!0,isUnknown:!1}}return H_},K_=/(?<attributeName>[^\s"'<>/=]+)(?:\s*=\s*(?:"(?<doubleQuoted>[^"]*)"|'(?<singleQuoted>[^']*)'|(?<unquoted>[^\s"'=<>`]+)))?/g,q_=/<!--[\s\S]*?-->|(?<skippedContentOpeningTag><(?<skippedContentTag>script|style|textarea|title|xmp|iframe|noembed|noframes|plaintext|template)\b(?:"[^"]*"|'[^']*'|[^'"<>])*?>)[\s\S]*?(?:<\/\k<skippedContentTag>\s*>|$)|(?<openingTag><[A-Za-z][\w:-]*(?:"[^"]*"|'[^']*'|[^'"<>])*>)/gi,J_=new Map,Y_=(e,t,n)=>{if(!e.isPresent)return;let r=e.attribute?B_(e.attribute,t):e.expression?I_(e.expression,t):null;for(let e of r??[]){let t=e?.trim();t&&n.add(t)}},X_=(e,t,n)=>{for(let r of e.properties){if(b(r,`SpreadElement`)){b(r.argument,`ObjectExpression`)&&X_(r.argument,t,n);continue}if(b(r,`Property`)&&S(r,{allowComputedString:!0})?.toLowerCase()===`id`)for(let e of I_(r.value,t)??[]){let t=e.trim();t&&n.add(t)}}},Z_=e=>{let t=e.parent;for(b(t,`JSXElement`)&&t.openingElement===e&&(t=t.parent);t;){if(b(t,`JSXElement`)&&Ws(t.openingElement)===`template`)return!0;t=t.parent}return!1},Q_=(e,t,n)=>{z(e,e=>{if(!b(e,`JSXOpeningElement`)||Z_(e))return;let r=G_(e.attributes,`id`,!1);if(r.isUnknown){for(let r of e.attributes)Y_(G_([r],`id`,!1),t,n),b(r,`JSXSpreadAttribute`)&&b(r.argument,`ObjectExpression`)&&X_(r.argument,t,n);return}Y_(r,t,n)})},$_=(e,t)=>{for(let n of e.matchAll(q_)){let e=n.groups?.skippedContentOpeningTag??n.groups?.openingTag;if(e)for(let n of e.matchAll(K_)){if(n.groups?.attributeName?.toLowerCase()!==`id`)continue;let e=n.groups?.doubleQuoted??n.groups?.singleQuoted??n.groups?.unquoted??``;e.trim()&&t.add(e.trim())}}},ev=e=>{if(!n.isAbsolute(e.currentFilePath))return null;let t=e.configuredRootDirectory?Xa(e.configuredRootDirectory):null;return!t||!V_(e.currentFilePath,t)?null:t},tv=()=>{J_.clear()},nv=t=>{let n=new Set;Q_(t.currentProgramNode,t.currentScopes,n);let r=ev(t);if(!r)return n;if(J_.has(r)){let e=J_.get(r);return e?new Set([...e,...n]):null}let i=M_({rootDirectory:r,currentFilePath:Xa(t.currentFilePath),currentProgramNode:t.currentProgramNode,currentScopes:t.currentScopes});if(!i)return J_.set(r,null),null;let a=new Set;for(let e of i.modulesByFilePath.values())Q_(e.programNode,e.scopes,a);try{for(let t of i.htmlFilePaths)$_(e.readFileSync(t,`utf8`),a)}catch{return J_.set(r,null),null}return J_.set(r,a),new Set([...a,...n])},rv=new Map,iv=()=>{rv.clear()},av=t=>{let n=rv.get(t);if(n!==void 0)return n;let r;try{r=e.realpathSync(t)}catch{r=t}return rv.set(t,r),r},ov=(e,t,n)=>{if(t===void 0||t.length===0)return!1;let r=Xa(av(e)),i=Xa(t);if(n&&r===i)return!0;let a=i.endsWith(`/`)?i:`${i}/`;return r.startsWith(a)},sv=/webpackIgnore\s*:\s*true|@vite-ignore/,cv=new Map,lv=()=>{cv.clear()},uv=e=>{if(!e)return null;let n=cv.get(e);if(n!==void 0)return n===!1?null:n;let r=!1;try{let n=t(e,`utf8`);sv.test(n)&&(r=n)}catch{r=!1}return cv.set(e,r),r===!1?null:r},dv=()=>{lv(),ar(),Ae(),Vn(),iv(),tv(),nr()},fv=e=>Zl.test(e)||Ql.test(e),pv=e=>{let t=lu.test(e);return ug(e)||xg(e)||Su(e)||hu(e)?{bucket:`priority`,isGeneratedBundleByName:t}:mu(e,t)?{bucket:`artifact`,isGeneratedBundleByName:t}:fv(e)?{bucket:`other`,isGeneratedBundleByName:t}:null},mv=(e,t)=>t||fv(e)||hu(e)||ug(e),hv=(t,r)=>{let i=null;try{e.mkdirSync(n.dirname(t),{recursive:!0}),i=`${t}.${process.pid}.${c()}.tmp`,e.writeFileSync(i,r),e.renameSync(i,t),i=null}catch{return}finally{if(i!==null)try{e.rmSync(i,{force:!0})}catch{}}},gv=t=>{let r=n.resolve(t.filename);try{r=e.realpathSync(r)}catch{r=n.normalize(r)}let i=[r,t.rule,t.offset,t.message],a=s(`sha256`).update(JSON.stringify(i)).digest(`hex`);return n.join(t.directory,`${encodeURIComponent(t.rule)}-${a}.json`)},_v=[`nextjs`,`astro`,`vite`,`cra`,`remix`,`gatsby`,`expo`,`react-native`,`tanstack-start`,`preact`,`unknown`];export{Hf as $,Tt as $i,Ua as $n,Bi as $r,Nl as $t,lg as A,ae as Aa,z as Ai,is as An,Ea as Ar,pd as At,Th as B,ee as Ba,qt as Bi,yo as Bn,sa as Br,gd as Bt,Ug as C,b as Ca,_r as Ci,V as Cn,ua as Cr,Sd as Ct,hg as D,fe as Da,jr as Di,as as Dn,ha as Dr,Od as Dt,gg as E,ue as Ea,Er as Ei,ss as En,la as Er,fd as Et,Fh as F,le as Fa,rr as Fi,Fo as Fn,aa as Fr,wd as Ft,Bm as G,f as Ga,F as Gi,lo as Gn,Gi as Gr,xu as Gt,Sh as H,m as Ha,I as Hi,po as Hn,ta as Hr,md as Ht,Ph as I,me as Ia,Bn as Ii,B as In,ia as Ir,Td as It,Em as J,u as Ja,P as Ji,oo as Jn,Ki as Jr,Bl as Jt,Nm as K,p as Ka,Lt as Ki,ao as Kn,Ui as Kr,W as Kt,Mh as L,te as La,yn as Li,ko as Ln,Zi as Lr,_d as Lt,ig as M,se as Ma,sr as Mi,Io as Mn,ga as Mr,Cd as Mt,eg as N,de as Na,cr as Ni,No as Nn,$i as Nr,yd as Nt,mg as O,re as Oa,hr as Oi,os as On,ma as Or,bd as Ot,Uh as P,ce as Pa,R as Pi,Po as Pn,oa as Pr,vd as Pt,Qf as Q,kt as Qi,Xa as Qn,Hi as Qr,Ul as Qt,jh as R,v as Ra,ln as Ri,Oo as Rn,Qi as Rr,Ed as Rt,Wg as S,_e as Sa,Nr as Si,ls as Sn,Ra as Sr,ud as St,yg as T,he as Ta,Tr as Ti,rs as Tn,Ca as Tr,Dd as Tt,yh as U,g as Ua,Wt as Ui,fo as Un,na as Ur,Gu as Ut,wh as V,h as Va,Gt as Vi,vo as Vn,ea as Vr,dd as Vt,ch as W,y as Wa,zt as Wi,uo as Wn,Wi as Wr,Wu as Wt,Sm as X,Mt as Xi,eo as Xn,Yi as Xr,Il as Xt,Tm as Y,Ft as Yi,so as Yn,Xi as Yr,Vl as Yt,$f as Z,M as Zi,$a as Zn,Ji as Zr,Hl as Zt,b_ as _,ve as _a,xr as _i,Ds as _n,xa as _r,Ju as _t,mv as a,A as aa,Jr as ai,rl as an,Da as ar,yf as at,n_ as b,S as ba,Sr as bi,bs as bn,wa as br,$u as bt,uv as c,D as ca,Fr as ci,Pc as cn,Ia as cr,cf as ct,G_ as d,Me as da,Cr as di,Ks as dn,Ma as dr,Xd as dt,wt as ea,Ei as ei,kl as en,Ja as er,Tf as et,V_ as f,Ne as fa,Dr as fi,Gs as fn,Na as fr,Yu as ft,M_ as g,xe as ga,wr as gi,As as gn,ya as gr,Zu as gt,N_ as h,Se as ha,Mr as hi,Bs as hn,Oa as hr,td as ht,pv as i,Ct as ia,ci as ii,ol as in,Aa as ir,X as it,og as j,pe as ja,lr as ji,cs as jn,pa as jr,kd as jt,fg as k,ie as ka,ur as ki,ns as kn,da as kr,nd as kt,ov as l,Ge as la,Ir as li,Zs as ln,Pa as lr,sf as lt,R_ as m,E as ma,Ar as mi,Vs as mn,fa as mr,Xu as mt,gv as n,St as na,di as ni,sl as nn,ka as nr,wf as nt,dv as o,bt as oa,Gr as oi,il as on,Ta as or,_f as ot,z_ as p,Pe as pa,kr as pi,Ws as pn,ja as pr,Ku as pt,Dm as q,d as qa,It as qi,co as qn,qi as qr,Ll as qt,hv as r,xt as ra,ui as ri,cl as rn,Sa as rr,bf as rt,sv as s,O as sa,Kr as si,al as sn,ba as sr,gf as st,_v as t,j as ta,fi as ti,jl as tn,Ga as tr,Ef as tt,nv as u,Ue as ua,Rr as ui,qs as un,Fa as ur,Zd as ut,f_ as v,w as va,br as vi,Es as vn,va as vr,Qu as vt,Lg as w,ge as wa,vr as wi,us as wn,_a as wr,xd as wt,Kg as x,x as xa,gr as xi,ds as xn,La as xr,ed as xt,d_ as y,C as ya,Or as yi,Cs as yn,ca as yr,qu as yt,Ah as z,_ as za,on as zi,bo as zn,ra as zr,hd as zt};
28
- //# sourceMappingURL=capability-FtYEg12v.js.map
25
+ `&&(t[r]=` `);n=o;continue}}if(r===`"`){for(n+=1;n<e.length;){if(e[n]===`"`){if(e[n+1]===`"`){n+=2;continue}n+=1;break}n+=1}continue}n+=1}return t.join(``)},Dg=/disable\s+row\s+level\s+security/i,Og=/auth\.role\(\)\s*=\s*["']service_role["']/i,kg=/create\s+policy/gi,Ag=/;|create\s+policy/i,jg=/\b(?:using|with\s+check)\s*\(\s*true\s*\)/i,Mg=/\bfor\s+select\b/i,Ng=/\bto\s+([\s\S]+?)(?=\s+(?:using|with\s+check|as|for)\b|;|$)/i,Pg=new Set([`service_role`,`postgres`,`supabase_admin`]),Fg=e=>{let t=Ng.exec(e);if(t===null)return!1;let n=t[1].split(`,`).map(e=>e.trim().replace(/["'`]/g,``).toLowerCase()).filter(Boolean);return n.length>0&&n.every(e=>Pg.has(e))},Ig=(e,t)=>Og.test(t)?!0:!jg.test(e)||Mg.test(e)?!1:!Fg(e),Lg=W({id:`supabase-rls-policy-risk`,title:`Permissive Supabase RLS policy`,severity:`error`,recommendation:"Keep public-read policies explicit, but gate inserts, updates, deletes, and service-role bypasses behind `auth.uid()` plus trusted tenant membership.",scan:e=>{if(!xg(e.relativePath))return[];let t=Eg(e.content),n=t.search(Dg);kg.lastIndex=0;for(let r=kg.exec(t);r!==null;r=kg.exec(t)){let i=r.index+r[0].length,a=t.slice(i).search(Ag),o=a<0?t.length:i+a;if(Ig(t.slice(r.index,o),e.content.slice(r.index,o))){(n<0||r.index<n)&&(n=r.index);break}}if(n<0)return[];let{line:r,column:i}=G(t,n);return[{message:`Supabase policy SQL disables RLS, permits writes broadly, or references a service-role bypass.`,line:r,column:i}]}}),Rg=/create\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(?!["`]?(?:auth|storage|realtime|vault|extensions|graphql|graphql_public|pgbouncer|net|supabase_functions|supabase_migrations|cron|pgsodium|pgmq|information_schema|pg_catalog|pg_temp|private|internal)["`]?\s*\.)(?:["`]?public["`]?\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?(?:\s*\(|\s+as\b)/gi,zg=/alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?(?:["`]?public["`]?\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?\s+(?:force\s+)?enable\s+row\s+level\s+security/gi,Bg=/\benable\s+row\s+level\s+security\b/gi,Vg=e=>{let t=new Map,n=0;for(let r of e.matchAll(zg)){let e=r[1];e!==void 0&&(n+=1,t.set(e.toLowerCase(),r.index))}return{lastEnableIndexByTable:t,staticEnableCount:n}},Hg=(e,t)=>(e.match(Bg)?.length??0)>t,Ug=W({id:`supabase-table-missing-rls`,title:`Supabase table created without Row Level Security`,severity:`error`,recommendation:"Enable RLS in the same migration (`alter table <name> enable row level security;`) and add `auth.uid()`-scoped policies for select/insert/update/delete. A public table without RLS is fully readable and writable with the public anon key.",scan:e=>{if(!bg(e.relativePath))return[];let t=Eg(e.content);if(!/create\s+(?:unlogged\s+)?table/i.test(t))return[];let n=[],{lastEnableIndexByTable:r,staticEnableCount:i}=Vg(t);if(Hg(t,i))return[];Rg.lastIndex=0;for(let e=Rg.exec(t);e!==null;e=Rg.exec(t)){let i=e[1];if(i===void 0)continue;let a=r.get(i.toLowerCase());if(a!==void 0&&a>=e.index)continue;let o=G(t,e.index);n.push({message:`Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.`,line:o.line,column:o.column})}return n}}),Wg=W({id:`svg-filter-clickjacking-risk`,title:`SVG-filtered iframe clickjacking primitive`,severity:`warn`,recommendation:"Avoid filtering cross-origin iframes. Use `frame-ancestors` on sensitive pages and keep SVG filters away from embedded privileged UI.",scan:q({shouldScan:e=>K(e.relativePath),pattern:/<iframe\b(?:=>|[^>]){0,300}\bfilter\s*:\s*["']?url\(#|filter\s*:\s*url\(#[\s\S]{0,700}<iframe\b|<fe(?:DisplacementMap|ColorMatrix|Composite|Tile|Morphology)\b[\s\S]{0,700}<iframe\b/i,message:`An iframe is rendered through an SVG/CSS filter, which can support advanced clickjacking or visual exfiltration tricks.`})}),Gg=e=>K(e)?nu.test(e)?!0:/(?:^|\/)(?:middleware|route)\.[cm]?[jt]sx?$/.test(e):!1,Kg=W({id:`tenant-static-proxy-risk`,title:`Tenant-controlled static asset proxy`,severity:`warn`,recommendation:`Bind tenant identity to the trusted host or authenticated org, canonicalize after decoding, reject traversal, and never let one tenant choose another tenant's asset prefix.`,scan:q({shouldScan:e=>Gg(e.relativePath),pattern:/\b(?:fetch|path\.join|getObject\w*|GetObjectCommand|getSignedUrl|createReadStream)\s*\([^;]{0,200}(?:\$\{[^}]{0,100}\b(?:tenant|subdomain|workspace|hostPattern|(?<!\.)organization(?:Id|Slug)?)\b|\b(?:tenant|subdomain|workspace)(?:Id|Slug|Name)?\b\s*[,)+\].])/i,message:`Route code appears to compose tenant or subdomain input into a static/CDN/object-store fetch path.`})}),qg=[/dangerouslySetInnerHTML\s*=\s*\{\{\s*__html\s*:[\s\S]{0,300}?\bJSON\.stringify\s*\(/gi,/<script\b[^>]*>(?:(?!<\/script>)[\s\S]){0,300}?\bJSON\.stringify\s*\(/gi],Jg=/\$\{|["'`]\s*\+\s*$/,Yg=/^\s*\{(?!\{)/,Xg=/"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'/g,Zg=/\b(?:true|false|null)\b|[\d.]+(?:e[+-]?\d+)?/gi,Qg=e=>{if(e.includes("`")||e.includes("${"))return!1;let t=e.replace(Xg,``).replace(Zg,``);return!/[A-Za-z_$]/.test(t)},$g=/^[\s)]*\.replace\s*\([^)]*(?:\\u003[cC]|&lt;|<)/,e_=/^[\s)]*\.replaceAll\s*\(\s*["']<["']\s*,\s*["'](?:\\u003[cC]|&lt;)["']\s*\)/,t_=/(?:\b(?:escapeHtml|escapeJSON|escapeJson|htmlEscape|jsesc)|(?<![.\w])(?:serialize|serializeJavascript|devalue|uneval|superjson))\s*\(\s*$/i,n_=/\bJSON\.stringify\s*\($/i,r_=W({id:`unsafe-json-in-html`,title:`Unescaped JSON in HTML or script sink`,severity:`warn`,recommendation:'JSON.stringify does not HTML-escape, so a `<\/script>` (or `<`) in the data breaks out and becomes XSS. Use an HTML-safe serializer (serialize-javascript, devalue) or escape `<`, `>`, and `&`, or pass data via a JSON `<script type="application/json">` read with JSON.parse.',scan:e=>{if(!K(e.relativePath))return[];let t=Bu(e);if(!t.includes(`JSON.stringify`))return[];let n=[],r=new Set;for(let[e,i]of qg.entries()){i.lastIndex=0;for(let a=i.exec(t);a!==null;a=i.exec(t)){let i=a[0].replace(n_,``);if(t_.test(i))continue;if(e===1){let e=t.indexOf(`>`,a.index),n=a[0].search(/\bJSON\.stringify\s*\($/i);if(e>=0&&n>=0){let r=t.slice(e+1,a.index+n);if(!Jg.test(r)||Yg.test(r))continue}}let o=a.index+a[0].length-1,s=vf(t,o);if(s>=0){let e=t.slice(s+1,s+1+160);if($g.test(e)||e_.test(e)||Qg(t.slice(o+1,s)))continue}if(r.has(a.index))continue;r.add(a.index);let c=G(t,a.index);n.push({message:"JSON.stringify is embedded in HTML/script markup without HTML-escaping; data containing `<\/script>` or `<` breaks out and becomes XSS.",line:c.line,column:c.column})}}return n}}),i_=/(?:(?<![.\w$])fetch|\baxios\.\s*(?:get|post|put|delete|head)|\bgot|\bgot\.\s*(?:get|post))\s*\(\s*([^,)]+)/,a_=/\b(?:url|targetUrl|callbackUrl|redirectUrl|webhookUrl|companyUrl|websiteUrl|domainUrl|imageUrl|fetchUrl|next|return_to|returnTo|destination|location)\b/i,o_=/\breq\.|\brequest\.(?:query|body|params|nextUrl)|\bsearchParams\b|\bparams\.|\bbody\.|\bquery\./,s_=/\bredirect\s*:\s*["'](?:manual|error)["']/,c_=/\b(params|body|query)\./g,l_=(e,t)=>RegExp(`(?:const|let|var)\\s+${e}\\s*(?::[^=;\\n]{0,80})?=\\s*new\\s+URLSearchParams\\b`).test(t),u_=(e,t)=>{let n=e.replace(c_,(e,n)=>l_(n,t)?``:e);return o_.test(n)},d_=(e,t)=>{if(u_(e,t))return!0;let n=/^[\w$]+$/.exec(e.trim());if(n===null)return!1;let r=RegExp(`(?:const|let|var)[^=;\\n]{0,80}\\b${n[0]}\\b[^=;\\n]{0,80}=([^;\\n]*)`,`g`);for(let e of t.matchAll(r)){let n=e[1]??``;if(/\breq\.|\brequest\.|\bsearchParams\b|\$_(?:GET|POST|REQUEST)/.test(n)||u_(n,t))return!0}return!1},f_=W({id:`untrusted-redirect-following`,title:`Server fetch follows redirects for caller-shaped URL`,severity:`warn`,recommendation:'Use `redirect: "manual"` or equivalent and re-validate every redirect target before following it to avoid SSRF redirect bypasses.',scan:e=>{if(!Gg(e.relativePath)||!i_.test(e.content))return[];let t=[],n=e.content.split(`
26
+ `);for(let r=0;r<n.length;r+=1){let i=n[r]??``,a=i.match(i_),o=a?.[1]??``;if(!a||!a_.test(o)||!d_(o,e.content))continue;let s=n.slice(r,r+5).join(`
27
+ `);s_.test(s)||t.push({message:`Server-side fetch code appears to follow redirects for a URL shaped like caller-controlled input.`,line:r+1,column:i.search(/\S/)+1})}return t}}),p_=W({id:`url-prefilled-privileged-action`,title:`URL pre-fills a privileged action`,severity:`warn`,recommendation:`Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.`,scan:q({shouldScan:e=>Cm(e.relativePath),pattern:/(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist|parse|normaliz)[\w$]*\(\s*(?:new\s+)?(?:[\w$]+\s*\.\s*){0,4})\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,message:`Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values.`})}),m_=/(?:^|\/)[^/]*webhook[^/]*\/|(?:^|\/)[^/]*webhook[^/]*\.[cm]?[jt]s$|\bwebhook\b/i,h_=/\b(?:export\s+(?:async\s+)?function\s+POST|export\s+const\s+(?:POST|handler|webhook)|webhookHandler|webhookRoute)\b/i,g_=RegExp(`verifySignature|verify.*signature|verify\\w*(?:Webhook|Auth)|constructEvent|createHmac|timingSafeEqual|svix|webhookSecret|stripe\\.webhooks|["'][\\w-]*signature["']|\\b[A-Za-z]{0,40}(?:verif|valid|check|assert|authenticat|compare|guard)[A-Za-z]{0,40}(?:secret|signature|hmac|webhook|digest)[A-Za-z]{0,40}\\s*\\(`,`i`),__=/webhook[\s_-]?ur[il]\w*/gi,v_=/process\.env\.\w*WEBHOOK_URL|\b(?:send|post|dispatch|publish|notify)\w*Webhook/,y_=/\b(?:req|request)\b/,b_=/\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|`(?:\\.|[^`\\])*`/g,x_=W({id:`webhook-signature-risk`,title:`Webhook handler lacks signature verification`,severity:`warn`,recommendation:`Verify provider signatures before parsing or acting on webhook bodies. Use provider SDK helpers or HMAC verification with timing-safe comparison.`,scan:q({shouldScan:e=>{if(!K(e.relativePath)||v_.test(e.content))return!1;let t=e.content.replace(b_,``).replace(__,``);return m_.test(e.relativePath)||m_.test(t)},pattern:h_,requireAll:[y_],suppressWhen:g_,message:`Webhook handler code does not show an obvious signature verification step.`})}),S_=/\.[cm]?[jt]sx?$/i,C_=/\.d\.[cm]?[jt]s$/i,w_=/\.mdx$/i,T_=/\.html?$/i,E_=new Set([`.angular`,`.astro`,`.cache`,`.contentlayer`,`.docusaurus`,`.expo`,`.git`,`.next`,`.nuxt`,`.output`,`.svelte-kit`,`.turbo`,`.vercel`,`build`,`coverage`,`dist`,`node_modules`,`out`,`storybook-static`]),D_=new WeakMap,O_=e=>{let t=D_.get(e);if(t)return t;let n=A(e);return D_.set(e,n),n},k_=t=>{let r=[],i=[],a=[t],o=!1;for(;a.length>0;){let t=a.pop();if(!t)continue;let s;try{s=e.readdirSync(t,{withFileTypes:!0})}catch{return null}for(let e of s){let s=n.join(t,e.name),c=E_.has(e.name)||e.name.startsWith(`.`)&&e.name!==`.dumi`&&e.name!==`.storybook`;if(!(e.isSymbolicLink()&&c)){if(e.isSymbolicLink())return null;if(e.isDirectory()){if(c)continue;a.push(s);continue}if(!(!e.isFile()||kl(s))){if(w_.test(e.name)){o=!0;continue}if(T_.test(e.name)){i.push(Xa(s));continue}S_.test(e.name)&&(C_.test(e.name)||r.push(Xa(s)))}}}}return{sourceFilePaths:r,htmlFilePaths:i,hasOpaqueMdxConsumerSurface:o}},A_=(t,n)=>{for(let r of t){let t;try{t=e.readFileSync(r,`utf8`)}catch{return!0}for(let e of n)if(t.includes(`"${e}"`)||t.includes(`'${e}'`))return!0}return!1},j_=e=>{if(b(e,`ImportDeclaration`))return cl(e)?null:typeof e.source.value==`string`?e.source.value:null;if(b(e,`ExportNamedDeclaration`))return e.exportKind===`type`||sl(e.specifiers,`ExportSpecifier`,`exportKind`)?null:e.source&&typeof e.source.value==`string`?e.source.value:null;if(b(e,`ExportAllDeclaration`))return e.exportKind===`type`?null:typeof e.source.value==`string`?e.source.value:null;if(b(e,`ImportExpression`))return b(e.source,`Literal`)&&typeof e.source.value==`string`?e.source.value:null;if(b(e,`CallExpression`)&&b(e.callee,`Identifier`)&&e.callee.name===`require`&&e.arguments.length===1){let t=e.arguments[0];return t&&b(t,`Literal`)&&typeof t.value==`string`?t.value:null}return null},M_=(e,t,n,r)=>{z(e.programNode,i=>{let a=j_(i);if(!a)return;let o=rr(e.filePath,a);if(!o){r.add(a);return}let s=Xa(o);n.set(i,s);let c=t.get(s)??new Set;c.add(e),t.set(s,c)})},N_=({rootDirectory:e,currentFilePath:t,currentProgramNode:n,currentScopes:r,requiredModuleSources:i=[],knownModuleSources:a})=>{if(i.length>0&&a!==void 0&&!i.some(e=>a.includes(e)))return null;let o=k_(e);if(!o||i.length>0&&a===void 0&&!A_(o.sourceFilePaths,i))return null;let s=new Map;for(let e of o.sourceFilePaths){if(e===t){s.set(e,{filePath:e,programNode:n,scopes:r});continue}let i=yn(e);if(!i||!b(i,`Program`))return null;s.set(e,{filePath:e,programNode:i,scopes:O_(i)})}let c=new Map,l=new WeakMap,u=new Set;for(let e of s.values())M_(e,c,l,u);return{modulesByFilePath:s,htmlFilePaths:o.htmlFilePaths,consumerModulesByFilePath:c,resolvedSourcePathByNode:l,unresolvedRuntimeSources:u,hasOpaqueMdxConsumerSurface:o.hasOpaqueMdxConsumerSurface}},P_=e=>{if(!e)return null;let t=w(e);return!b(t,`Literal`)||typeof t.value!=`boolean`?null:t.value},F_=(e,t,n={})=>{let r=n.maximumConstAliases===void 0?4:n.maximumConstAliases,i=n.shouldFoldStaticConditions??!1,a=n.shouldKeepKnownValues??!1,o=[],s=[{expression:e,remainingConstAliases:r,resolvingSymbols:new Set}];for(;s.length>0;){let e=s.pop();if(!e)continue;let n=w(e.expression);if(b(n,`Literal`)){if(typeof n.value!=`string`){if(a)continue;return null}o.push(n.value);continue}if(b(n,`TemplateLiteral`)){let e=Wt(n);if(e===null){if(a)continue;return null}o.push(e);continue}if(b(n,`ConditionalExpression`)){let t=i?P_(n.test):null;if(t!==null){s.push({expression:t?n.consequent:n.alternate,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:e.resolvingSymbols});continue}s.push({expression:n.alternate,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:new Set(e.resolvingSymbols)}),s.push({expression:n.consequent,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:new Set(e.resolvingSymbols)});continue}if(a&&b(n,`LogicalExpression`)&&(n.operator===`??`||n.operator===`||`)){s.push({expression:n.right,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:new Set(e.resolvingSymbols)}),s.push({expression:n.left,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:new Set(e.resolvingSymbols)});continue}if(b(n,`Identifier`)){let r=t.referenceFor(n)?.resolvedSymbol;if(!r||r.kind!==`const`||!r.initializer||!b(r.declarationNode,`VariableDeclarator`)||r.declarationNode.id!==r.bindingIdentifier||e.remainingConstAliases===0||e.resolvingSymbols.has(r)){if(a)continue;return null}e.resolvingSymbols.add(r),s.push({expression:r.initializer,remainingConstAliases:e.remainingConstAliases===null?null:e.remainingConstAliases-1,resolvingSymbols:e.resolvingSymbols});continue}if(!a)return null}return o.length>0?o:null},I_=(e,t,n={})=>F_(e,t,n),L_=(e,t)=>F_(e,t,{shouldKeepKnownValues:!0}),R_=(e,t,n,r,i=!1)=>{let a=e.value;return a?b(a,`Literal`)?typeof a.value==`string`?[a.value]:null:b(a,`JSXExpressionContainer`)?I_(a.expression,t,{maximumConstAliases:n,shouldFoldStaticConditions:r,shouldKeepKnownValues:i}):null:null},z_=(e,t)=>R_(e,t,4,!1),B_=(e,t)=>R_(e,t,null,!0),V_=(e,t)=>R_(e,t,4,!1,!0),H_=(e,t)=>{let r=n.relative(t,e);return r===``||!r.startsWith(`..${n.sep}`)&&r!==`..`&&!n.isAbsolute(r)},U_={attribute:null,expression:null,isPresent:!1,isUnknown:!1},W_={attribute:null,expression:null,isPresent:!1,isUnknown:!0},G_=(e,t,n)=>{for(let r=e.properties.length-1;r>=0;--r){let i=e.properties[r];if(!i)return W_;if(b(i,`SpreadElement`)){if(!b(i.argument,`ObjectExpression`))return W_;let e=G_(i.argument,t,n);if(e.isPresent||e.isUnknown)return e;continue}if(!b(i,`Property`))return W_;let a=S(i,{allowComputedString:!0});if(!a)return W_;if((n?a:a.toLowerCase())===t)return{attribute:null,expression:i.value,isPresent:!0,isUnknown:!1}}return U_},K_=(e,t,n=!0)=>{let r=n?t:t.toLowerCase();for(let t=e.length-1;t>=0;--t){let i=e[t];if(!i)return W_;if(b(i,`JSXSpreadAttribute`)){if(!b(i.argument,`ObjectExpression`))return W_;let e=G_(i.argument,r,n);if(e.isPresent||e.isUnknown)return e;continue}if(!b(i,`JSXAttribute`))continue;let a=Ds(i.name);if((n?a:a?.toLowerCase())===r)return{attribute:i,expression:null,isPresent:!0,isUnknown:!1}}return U_},q_=/(?<attributeName>[^\s"'<>/=]+)(?:\s*=\s*(?:"(?<doubleQuoted>[^"]*)"|'(?<singleQuoted>[^']*)'|(?<unquoted>[^\s"'=<>`]+)))?/g,J_=/<!--[\s\S]*?-->|(?<skippedContentOpeningTag><(?<skippedContentTag>script|style|textarea|title|xmp|iframe|noembed|noframes|plaintext|template)\b(?:"[^"]*"|'[^']*'|[^'"<>])*?>)[\s\S]*?(?:<\/\k<skippedContentTag>\s*>|$)|(?<openingTag><[A-Za-z][\w:-]*(?:"[^"]*"|'[^']*'|[^'"<>])*>)/gi,Y_=new Map,X_=(e,t,n)=>{if(!e.isPresent)return;let r=e.attribute?V_(e.attribute,t):e.expression?L_(e.expression,t):null;for(let e of r??[]){let t=e?.trim();t&&n.add(t)}},Z_=(e,t,n)=>{for(let r of e.properties){if(b(r,`SpreadElement`)){b(r.argument,`ObjectExpression`)&&Z_(r.argument,t,n);continue}if(b(r,`Property`)&&S(r,{allowComputedString:!0})?.toLowerCase()===`id`)for(let e of L_(r.value,t)??[]){let t=e.trim();t&&n.add(t)}}},Q_=e=>{let t=e.parent;for(b(t,`JSXElement`)&&t.openingElement===e&&(t=t.parent);t;){if(b(t,`JSXElement`)&&Ws(t.openingElement)===`template`)return!0;t=t.parent}return!1},$_=(e,t,n)=>{z(e,e=>{if(!b(e,`JSXOpeningElement`)||Q_(e))return;let r=K_(e.attributes,`id`,!1);if(r.isUnknown){for(let r of e.attributes)X_(K_([r],`id`,!1),t,n),b(r,`JSXSpreadAttribute`)&&b(r.argument,`ObjectExpression`)&&Z_(r.argument,t,n);return}X_(r,t,n)})},ev=(e,t)=>{for(let n of e.matchAll(J_)){let e=n.groups?.skippedContentOpeningTag??n.groups?.openingTag;if(e)for(let n of e.matchAll(q_)){if(n.groups?.attributeName?.toLowerCase()!==`id`)continue;let e=n.groups?.doubleQuoted??n.groups?.singleQuoted??n.groups?.unquoted??``;e.trim()&&t.add(e.trim())}}},tv=e=>{if(!n.isAbsolute(e.currentFilePath))return null;let t=e.configuredRootDirectory?Xa(e.configuredRootDirectory):null;return!t||!H_(e.currentFilePath,t)?null:t},nv=()=>{Y_.clear()},rv=t=>{let n=new Set;$_(t.currentProgramNode,t.currentScopes,n);let r=tv(t);if(!r)return n;if(Y_.has(r)){let e=Y_.get(r);return e?new Set([...e,...n]):null}let i=N_({rootDirectory:r,currentFilePath:Xa(t.currentFilePath),currentProgramNode:t.currentProgramNode,currentScopes:t.currentScopes});if(!i)return Y_.set(r,null),null;let a=new Set;for(let e of i.modulesByFilePath.values())$_(e.programNode,e.scopes,a);try{for(let t of i.htmlFilePaths)ev(e.readFileSync(t,`utf8`),a)}catch{return Y_.set(r,null),null}return Y_.set(r,a),new Set([...a,...n])},iv=new Map,av=()=>{iv.clear()},ov=t=>{let n=iv.get(t);if(n!==void 0)return n;let r;try{r=e.realpathSync(t)}catch{r=t}return iv.set(t,r),r},sv=(e,t,n)=>{if(t===void 0||t.length===0)return!1;let r=Xa(ov(e)),i=Xa(t);if(n&&r===i)return!0;let a=i.endsWith(`/`)?i:`${i}/`;return r.startsWith(a)},cv=/webpackIgnore\s*:\s*true|@vite-ignore/,lv=new Map,uv=()=>{lv.clear()},dv=e=>{if(!e)return null;let n=lv.get(e);if(n!==void 0)return n===!1?null:n;let r=!1;try{let n=t(e,`utf8`);cv.test(n)&&(r=n)}catch{r=!1}return lv.set(e,r),r===!1?null:r},fv=()=>{uv(),ar(),Ae(),Vn(),av(),nv(),nr()},pv=e=>Zl.test(e)||Ql.test(e),mv=e=>{let t=lu.test(e);return ug(e)||xg(e)||Su(e)||hu(e)?{bucket:`priority`,isGeneratedBundleByName:t}:mu(e,t)?{bucket:`artifact`,isGeneratedBundleByName:t}:pv(e)?{bucket:`other`,isGeneratedBundleByName:t}:null},hv=(e,t)=>t||pv(e)||hu(e)||ug(e),gv=(t,r)=>{let i=null;try{e.mkdirSync(n.dirname(t),{recursive:!0}),i=`${t}.${process.pid}.${c()}.tmp`,e.writeFileSync(i,r),e.renameSync(i,t),i=null}catch{return}finally{if(i!==null)try{e.rmSync(i,{force:!0})}catch{}}},_v=t=>{let r=n.resolve(t.filename);try{r=e.realpathSync(r)}catch{r=n.normalize(r)}let i=[r,t.rule,t.offset,t.message],a=s(`sha256`).update(JSON.stringify(i)).digest(`hex`);return n.join(t.directory,`${encodeURIComponent(t.rule)}-${a}.json`)},vv=[`nextjs`,`astro`,`vite`,`cra`,`remix`,`gatsby`,`expo`,`react-native`,`tanstack-start`,`preact`,`unknown`];export{Hf as $,Tt as $i,Ua as $n,Bi as $r,Nl as $t,lg as A,ae as Aa,z as Ai,is as An,Ea as Ar,pd as At,Th as B,ee as Ba,qt as Bi,yo as Bn,sa as Br,gd as Bt,Ug as C,b as Ca,_r as Ci,V as Cn,ua as Cr,Sd as Ct,hg as D,fe as Da,jr as Di,as as Dn,ha as Dr,Od as Dt,gg as E,ue as Ea,Er as Ei,ss as En,la as Er,fd as Et,Fh as F,le as Fa,rr as Fi,Fo as Fn,aa as Fr,wd as Ft,Bm as G,f as Ga,F as Gi,lo as Gn,Gi as Gr,xu as Gt,Sh as H,m as Ha,I as Hi,po as Hn,ta as Hr,md as Ht,Ph as I,me as Ia,Bn as Ii,B as In,ia as Ir,Td as It,Em as J,u as Ja,P as Ji,oo as Jn,Ki as Jr,Bl as Jt,Nm as K,p as Ka,Lt as Ki,ao as Kn,Ui as Kr,W as Kt,Mh as L,te as La,yn as Li,ko as Ln,Zi as Lr,_d as Lt,ig as M,se as Ma,sr as Mi,Io as Mn,ga as Mr,Cd as Mt,eg as N,de as Na,cr as Ni,No as Nn,$i as Nr,yd as Nt,mg as O,re as Oa,hr as Oi,os as On,ma as Or,bd as Ot,Uh as P,ce as Pa,R as Pi,Po as Pn,oa as Pr,vd as Pt,Qf as Q,kt as Qi,Xa as Qn,Hi as Qr,Ul as Qt,jh as R,v as Ra,ln as Ri,Oo as Rn,Qi as Rr,Ed as Rt,Wg as S,_e as Sa,Nr as Si,ls as Sn,Ra as Sr,ud as St,yg as T,he as Ta,Tr as Ti,rs as Tn,Ca as Tr,Dd as Tt,yh as U,g as Ua,Wt as Ui,fo as Un,na as Ur,Gu as Ut,wh as V,h as Va,Gt as Vi,vo as Vn,ea as Vr,dd as Vt,ch as W,y as Wa,zt as Wi,uo as Wn,Wi as Wr,Wu as Wt,Sm as X,Mt as Xi,eo as Xn,Yi as Xr,Il as Xt,Tm as Y,Ft as Yi,so as Yn,Xi as Yr,Vl as Yt,$f as Z,M as Zi,$a as Zn,Ji as Zr,Hl as Zt,x_ as _,ve as _a,xr as _i,Ds as _n,xa as _r,Ju as _t,hv as a,A as aa,Jr as ai,rl as an,Da as ar,yf as at,r_ as b,S as ba,Sr as bi,bs as bn,wa as br,$u as bt,dv as c,D as ca,Fr as ci,Pc as cn,Ia as cr,cf as ct,K_ as d,Me as da,Cr as di,Ks as dn,Ma as dr,Xd as dt,wt as ea,Ei as ei,kl as en,Ja as er,Tf as et,H_ as f,Ne as fa,Dr as fi,Gs as fn,Na as fr,Yu as ft,N_ as g,xe as ga,wr as gi,As as gn,ya as gr,Zu as gt,P_ as h,Se as ha,Mr as hi,Bs as hn,Oa as hr,td as ht,mv as i,Ct as ia,ci as ii,ol as in,Aa as ir,X as it,og as j,pe as ja,lr as ji,cs as jn,pa as jr,kd as jt,fg as k,ie as ka,ur as ki,ns as kn,da as kr,nd as kt,sv as l,Ge as la,Ir as li,Zs as ln,Pa as lr,sf as lt,z_ as m,E as ma,Ar as mi,Vs as mn,fa as mr,Xu as mt,_v as n,St as na,di as ni,sl as nn,ka as nr,wf as nt,fv as o,bt as oa,Gr as oi,il as on,Ta as or,_f as ot,B_ as p,Pe as pa,kr as pi,Ws as pn,ja as pr,Ku as pt,Dm as q,d as qa,It as qi,co as qn,qi as qr,Ll as qt,gv as r,xt as ra,ui as ri,cl as rn,Sa as rr,bf as rt,cv as s,O as sa,Kr as si,al as sn,ba as sr,gf as st,vv as t,j as ta,fi as ti,jl as tn,Ga as tr,Ef as tt,rv as u,Ue as ua,Rr as ui,qs as un,Fa as ur,Zd as ut,p_ as v,w as va,br as vi,Es as vn,va as vr,Qu as vt,Lg as w,ge as wa,vr as wi,us as wn,_a as wr,xd as wt,Kg as x,x as xa,gr as xi,ds as xn,La as xr,ed as xt,f_ as y,C as ya,Or as yi,Cs as yn,ca as yr,qu as yt,Ah as z,_ as za,on as zi,bo as zn,ra as zr,hd as zt};
28
+ //# sourceMappingURL=capability-COsjgFAJ.js.map
package/dist/core.js CHANGED
@@ -1,4 +1,4 @@
1
- import{A as e,B as t,C as n,D as r,F as i,G as a,Ga as ee,Gt as o,H as s,I as c,J as l,Ja as u,K as d,Ka as f,L as p,M as m,N as h,O as g,P as _,R as v,S as y,T as b,U as x,Ut as S,V as C,W as w,Wa as T,Wt as E,X as D,Y as O,_ as k,a as te,an as ne,b as A,ci as j,ct as M,dt as N,i as re,j as P,k as F,li as ie,lt as I,n as ae,o as oe,on as L,ot as R,q as z,qa as B,r as V,sn as H,st as U,t as W,ui as G,ut as K,v as q,w as J,wa as se,x as Y,y as X,z as Z}from"./capability-FtYEg12v.js";var ce=[{key:`react-doctor/active-static-asset`,id:`active-static-asset`,source:`react-doctor`,originallyExternal:!1,rule:{id:`active-static-asset`,title:`Executable SVG exposure`,severity:`warn`,recommendation:"Prefer `<img>` for SVG images; if SVG must be served directly, use attachment disposition and a CSP that blocks scripts and objects.",category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/activity-wraps-effect-heavy-subtree`,id:`activity-wraps-effect-heavy-subtree`,source:`react-doctor`,originallyExternal:!1,rule:{id:`activity-wraps-effect-heavy-subtree`,title:`Activity wraps an effect-heavy subtree`,severity:`warn`,recommendation:`Retired: Activity intentionally cleans up effects while hidden and preserves component state.`,category:`Bugs`,framework:`global`,requires:[`react`,`react:19.2`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/advanced-event-handler-refs`,id:`advanced-event-handler-refs`,source:`react-doctor`,originallyExternal:!1,rule:{id:`advanced-event-handler-refs`,title:`Listener re-subscribes on every handler change`,severity:`warn`,recommendation:"Store the handler in a ref and have the listener read `handlerRef.current()`. The subscription stays put while the latest handler still runs.",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/agent-tool-capability-risk`,id:`agent-tool-capability-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`agent-tool-capability-risk`,title:`Agent tool exposes dangerous capability`,severity:`warn`,recommendation:`Treat tool inputs as prompt-injection controlled. Validate arguments, scope permissions per call, and avoid exposing shell/file/network primitives directly to agents.`,category:`Security`,framework:`global`,tags:[`security-scan`],defaultEnabled:!1,isScanRule:!0}},{key:`react-doctor/alt-text`,id:`alt-text`,source:`react-doctor`,originallyExternal:!0,rule:{id:`alt-text`,title:`Image missing alt text`,severity:`error`,recommendation:"Give every meaningful image an `alt`, `aria-label`, or `aria-labelledby`.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],diffScope:`jsx-element`,isScanRule:!1}},{key:`react-doctor/anchor-ambiguous-text`,id:`anchor-ambiguous-text`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-ambiguous-text`,title:`Ambiguous link text`,severity:`warn`,recommendation:`Name where a link goes. Avoid 'click here', 'learn more', and 'link'.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-has-content`,id:`anchor-has-content`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-has-content`,title:`Anchor has no content`,severity:`warn`,recommendation:`Put readable text inside every link.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-is-valid`,id:`anchor-is-valid`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-is-valid`,title:`Anchor used as a button`,severity:`warn`,recommendation:"Give links a real destination. Use `<button>` for in-page actions.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-target-exists`,id:`anchor-target-exists`,source:`react-doctor`,originallyExternal:!1,rule:{id:`anchor-target-exists`,title:`Fragment link target is missing`,severity:`warn`,recommendation:`Add an element with the referenced id, or update the fragment link to point to an existing target.`,category:`Accessibility`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/aria-activedescendant-has-tabindex`,id:`aria-activedescendant-has-tabindex`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-activedescendant-has-tabindex`,title:`aria-activedescendant missing tabindex`,severity:`warn`,recommendation:"Add `tabIndex` so keyboard users can reach elements that use `aria-activedescendant`.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-braille-equivalent`,id:`aria-braille-equivalent`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-braille-equivalent`,title:`Braille-only accessible description`,severity:`warn`,recommendation:`Pair braille-specific labels and role descriptions with an equivalent accessible name or role description.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/aria-props`,id:`aria-props`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-props`,title:`Invalid ARIA attribute`,severity:`error`,recommendation:"Only use `aria-*` attributes that actually exist.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-proptypes`,id:`aria-proptypes`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-proptypes`,title:`Invalid ARIA attribute value`,severity:`error`,recommendation:"Give each `aria-*` attribute the kind of value it expects.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-role`,id:`aria-role`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-role`,title:`Invalid ARIA role`,severity:`error`,recommendation:`Use a real, non-abstract ARIA role so assistive tech can expose the element correctly.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-unsupported-elements`,id:`aria-unsupported-elements`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-unsupported-elements`,title:`ARIA on unsupported element`,severity:`error`,recommendation:"Do not put `role` or `aria-*` on reserved HTML elements.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/artifact-baas-authority-surface`,id:`artifact-baas-authority-surface`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-baas-authority-surface`,title:`BaaS authority map shipped in browser artifact`,severity:`warn`,recommendation:`Client BaaS config is often public, but shipped collection names plus owner, role, tenant, or admin fields give attackers a precise authorization map. Verify rules/RLS enforce every boundary server-side.`,category:`Security`,framework:`global`,tags:[`security-scan`],defaultEnabled:!1,isScanRule:!0}},{key:`react-doctor/artifact-env-leak`,id:`artifact-env-leak`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-env-leak`,title:`Server env leaked to browser artifact`,severity:`error`,recommendation:`Treat public env prefixes as publication, not secrecy; keep secret env vars server-only and rebuild after rotating leaked keys.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/artifact-secret-leak`,id:`artifact-secret-leak`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-secret-leak`,title:`Secret shipped in browser artifact`,severity:`error`,recommendation:`Remove the secret from client bundles/static assets, rotate it, and route privileged service calls through server-only code.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/async-await-in-loop`,id:`async-await-in-loop`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-await-in-loop`,title:`await inside a loop`,severity:`warn`,recommendation:`Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.`,category:`Performance`,framework:`global`,tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/async-defer-await`,id:`async-defer-await`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-defer-await`,title:`await before an early-return guard`,severity:`warn`,recommendation:"Move the `await` below the early-return guard so the skip path stays fast and avoids unnecessary async work.",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/async-parallel`,id:`async-parallel`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-parallel`,title:`Independent awaits run sequentially`,severity:`warn`,recommendation:"Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` so independent calls run at the same time",category:`Performance`,framework:`global`,tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/auth-token-in-web-storage`,id:`auth-token-in-web-storage`,source:`react-doctor`,originallyExternal:!1,rule:{id:`auth-token-in-web-storage`,title:`Auth token in web storage`,severity:`warn`,recommendation:"Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",category:`Security`,framework:`global`,isScanRule:!1}},{key:`react-doctor/autocomplete-valid`,id:`autocomplete-valid`,source:`react-doctor`,originallyExternal:!0,rule:{id:`autocomplete-valid`,title:`Invalid autocomplete value`,severity:`warn`,recommendation:"Use a valid autofill token in `autoComplete` so browsers can fill the right field reliably.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/base-ui-dialog-popup-requires-title`,id:`base-ui-dialog-popup-requires-title`,source:`react-doctor`,originallyExternal:!1,rule:{id:`base-ui-dialog-popup-requires-title`,title:`Base UI dialog popup without a title`,severity:`warn`,recommendation:`Give every Base UI Dialog.Popup and AlertDialog.Popup a Title part (visually hidden when the design shows no heading) or name the dialog with aria-label.`,category:`Accessibility`,framework:`global`,requires:[`react`,`base-ui`],isScanRule:!1}},{key:`react-doctor/base-ui-field-requires-label`,id:`base-ui-field-requires-label`,source:`react-doctor`,originallyExternal:!1,rule:{id:`base-ui-field-requires-label`,title:`Base UI field control without a label`,severity:`warn`,recommendation:`Give every Field.Root that wraps a Field.Control a Field.Label (visually hidden when the design shows no label) or name the control directly with aria-label.`,category:`Accessibility`,framework:`global`,requires:[`react`,`base-ui`],isScanRule:!1}},{key:`react-doctor/base-ui-tabs-tab-requires-list`,id:`base-ui-tabs-tab-requires-list`,source:`react-doctor`,originallyExternal:!1,rule:{id:`base-ui-tabs-tab-requires-list`,title:`Base UI tab is outside Tabs.List`,severity:`warn`,recommendation:`Render each Base UI Tabs.Tab inside Tabs.List so the tablist role and arrow-key focus handling cover every tab.`,category:`Bugs`,framework:`global`,requires:[`base-ui`],matchByOccurrence:!0,isScanRule:!1}},{key:`react-doctor/build-pipeline-secret-boundary`,id:`build-pipeline-secret-boundary`,source:`react-doctor`,originallyExternal:!1,rule:{id:`build-pipeline-secret-boundary`,title:`Build pipeline runs code near secrets`,severity:`warn`,recommendation:`Run dependency installs with scripts disabled before exposing secrets, isolate untrusted build code, and move signing/deploy authority into a narrow privileged step.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/button-has-type`,id:`button-has-type`,source:`react-doctor`,originallyExternal:!0,rule:{id:`button-has-type`,title:`Button missing explicit type`,severity:`warn`,recommendation:'Set an explicit button `type` so plain buttons do not submit forms by accident: `type="button"`, `"submit"`, or `"reset"`.',category:`Bugs`,framework:`global`,requires:[`react`],diffScope:`jsx-element`,isScanRule:!1}},{key:`react-doctor/checked-requires-onchange-or-readonly`,id:`checked-requires-onchange-or-readonly`,source:`react-doctor`,originallyExternal:!0,rule:{id:`checked-requires-onchange-or-readonly`,title:`Checked input without onChange`,severity:`warn`,recommendation:"Add `onChange`, `readOnly`, or `defaultChecked` so React knows whether the checkbox is editable, display-only, or uncontrolled.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/circular-dependency`,id:`circular-dependency`,source:`react-doctor`,originallyExternal:!1,rule:{id:`circular-dependency`,title:`Runtime import cycle`,severity:`warn`,recommendation:`Break the runtime cycle by extracting shared code into a lower-level module or inverting one dependency.`,category:`Maintainability`,framework:`global`,tags:[`project-analysis`],defaultEnabled:!1,isScanRule:!1,isProjectRule:!0}},{key:`react-doctor/class-component-missing-component-will-unmount-teardown`,id:`class-component-missing-component-will-unmount-teardown`,source:`react-doctor`,originallyExternal:!1,rule:{id:`class-component-missing-component-will-unmount-teardown`,title:`Class component acquires a resource with no teardown`,severity:`warn`,recommendation:"Release listeners and timers acquired in `componentDidMount`/`constructor` by adding a `componentWillUnmount` that removes them (or use MobX `disposeOnUnmount`).",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/click-events-have-key-events`,id:`click-events-have-key-events`,source:`react-doctor`,originallyExternal:!0,rule:{id:`click-events-have-key-events`,title:`Click handler missing keyboard handler`,severity:`warn`,recommendation:"Pair `onClick` with a key handler so keyboard users can trigger it.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],diffScope:`jsx-element`,isScanRule:!1}},{key:`react-doctor/clickjacking-redirect-risk`,id:`clickjacking-redirect-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`clickjacking-redirect-risk`,title:`Redirect or frame boundary risk`,severity:`warn`,recommendation:"Allowlist redirect origins/paths, set `frame-ancestors` for privileged pages, and avoid URL-prefilled privileged dialogs.",category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/client-localstorage-no-version`,id:`client-localstorage-no-version`,source:`react-doctor`,originallyExternal:!1,rule:{id:`client-localstorage-no-version`,title:`Unversioned localStorage key`,severity:`warn`,recommendation:`Retired: Storage can keep its version in the payload or use a separate migration path.`,category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/client-passive-event-listeners`,id:`client-passive-event-listeners`,source:`react-doctor`,originallyExternal:!1,rule:{id:`client-passive-event-listeners`,title:`Non-passive scroll listener`,severity:`warn`,recommendation:"Add `{ passive: true }` as the third argument: `addEventListener('touchmove', handler, { passive: true })`. Only do this if the handler doesn't call `event.preventDefault()`, since passive listeners ignore it (which breaks pull-to-refresh, custom gestures, and nested scrolling).",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/command-execution-input-risk`,id:`command-execution-input-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`command-execution-input-risk`,title:`Command execution uses caller-shaped input`,severity:`error`,recommendation:`Avoid shell execution for caller-controlled values. Use fixed commands, argument arrays, strict allowlists, and no shell interpolation.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/context-provider-value-from-unmemoized-local-literal`,id:`context-provider-value-from-unmemoized-local-literal`,source:`react-doctor`,originallyExternal:!1,rule:{id:`context-provider-value-from-unmemoized-local-literal`,title:`Context value from an unmemoized local literal`,severity:`warn`,recommendation:`Memoize the context value in component scope so consumers do not redraw every render. For mapped providers, extract each item into a child component and memoize there.`,category:`Performance`,framework:`global`,requires:[`react`],disabledWhen:[`react-compiler`],tags:[`react-jsx-only`,`test-noise`],isScanRule:!1}},{key:`react-doctor/control-has-associated-label`,id:`control-has-associated-label`,source:`react-doctor`,originallyExternal:!0,rule:{id:`control-has-associated-label`,title:`Control missing accessible label`,severity:`warn`,recommendation:`Give every interactive control a label screen readers can read.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/cors-cookie-trust-risk`,id:`cors-cookie-trust-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`cors-cookie-trust-risk`,title:`Broad cookie or credentialed CORS trust`,severity:`warn`,recommendation:`Keep auth cookies host-only and HttpOnly, avoid credentialed CORS for less-trusted docs/vendor origins, and isolate documentation domains from app sessions.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/dangerous-html-sink`,id:`dangerous-html-sink`,source:`react-doctor`,originallyExternal:!1,rule:{id:`dangerous-html-sink`,title:`HTML injection sink with dynamic content`,severity:`warn`,recommendation:`Prefer rendering structured React nodes. If HTML is required, sanitize with a well-reviewed sanitizer and keep the trust boundary close to the sink.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/data-table-requires-accessible-name`,id:`data-table-requires-accessible-name`,source:`react-doctor`,originallyExternal:!1,rule:{id:`data-table-requires-accessible-name`,title:`Data table has no accessible name`,severity:`warn`,recommendation:`Give data tables a concise caption, or reference an existing visible title with aria-labelledby.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/debounce-no-cleanup`,id:`debounce-no-cleanup`,source:`react-doctor`,originallyExternal:!1,rule:{id:`debounce-no-cleanup`,title:`Memoized debounce never cancelled on unmount`,severity:`warn`,recommendation:"A debounced/throttled callback holds a pending timer that still fires after unmount, so add `useEffect(() => () => debounced.cancel(), [debounced])` to cancel the trailing invocation when the component tears down.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/design-no-em-dash-in-jsx-text`,id:`design-no-em-dash-in-jsx-text`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-em-dash-in-jsx-text`,title:`Em dash in JSX text`,severity:`warn`,recommendation:`Retired: Visual and writing preferences belong in an explicit design guide, not a general defect check.`,category:`Bugs`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-redundant-padding-axes`,id:`design-no-redundant-padding-axes`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-redundant-padding-axes`,title:`Redundant padding axes`,severity:`warn`,recommendation:"Collapse matching padding axes to `p-N` so duplicated classes do not make spacing harder to scan; keep split axes only when breakpoints differ.",category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-redundant-size-axes`,id:`design-no-redundant-size-axes`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-redundant-size-axes`,title:`Redundant width and height axes`,severity:`warn`,recommendation:"Collapse matching width and height to `size-N` so duplicated classes do not make layout harder to scan.",category:`Maintainability`,framework:`global`,requires:[`react`,`tailwind:3.4`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-space-on-flex-children`,id:`design-no-space-on-flex-children`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-space-on-flex-children`,title:`space-* utility on flex children`,severity:`warn`,recommendation:"Use `gap-*` on the flex or grid parent. `space-x-*` and `space-y-*` leave gaps when a child is hidden, miss spacing on wrapped lines, and don't flip in right-to-left layouts.",category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-three-period-ellipsis`,id:`design-no-three-period-ellipsis`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-three-period-ellipsis`,title:`Three dots instead of ellipsis`,severity:`warn`,recommendation:`Retired: Visual and writing preferences belong in an explicit design guide, not a general defect check.`,category:`Bugs`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-vague-button-label`,id:`design-no-vague-button-label`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-vague-button-label`,title:`Vague button label`,severity:`warn`,recommendation:`Name the action: "Save changes" instead of "Continue", "Send invite" instead of "Submit". The label is the button's accessible name.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/details-requires-summary`,id:`details-requires-summary`,source:`react-doctor`,originallyExternal:!1,rule:{id:`details-requires-summary`,title:`Details disclosure has no explicit summary`,severity:`warn`,recommendation:`Make a meaningful summary the first child of details so users can understand and operate the disclosure.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/dialog-has-accessible-name`,id:`dialog-has-accessible-name`,source:`react-doctor`,originallyExternal:!1,rule:{id:`dialog-has-accessible-name`,title:`Dialog without accessible name`,severity:`warn`,recommendation:'Give every `<dialog>` / `role="dialog"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog\'s title element).',category:`Accessibility`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/display-name`,id:`display-name`,source:`react-doctor`,originallyExternal:!0,rule:{id:`display-name`,title:`Component missing display name`,severity:`warn`,recommendation:"Give each component a `displayName` so DevTools shows a clear name.",category:`Maintainability`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/duplicate-jsx-subtree`,id:`duplicate-jsx-subtree`,source:`react-doctor`,originallyExternal:!1,rule:{id:`duplicate-jsx-subtree`,title:`Duplicated JSX structure`,severity:`warn`,recommendation:`Extract a shared component when the repeated JSX trees represent the same UI concept and should evolve together.`,category:`Maintainability`,framework:`global`,tags:[`project-analysis`],isScanRule:!1,isProjectRule:!0}},{key:`react-doctor/effect-listener-cleanup-mismatch`,id:`effect-listener-cleanup-mismatch`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-listener-cleanup-mismatch`,title:`Effect cleanup does not match its event listener`,severity:`error`,recommendation:"Pass the same callback binding and capture flag to `addEventListener` and `removeEventListener`, or abort the registration's local AbortController during cleanup.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-listener-cleanup-reference-mismatch`,id:`effect-listener-cleanup-reference-mismatch`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-listener-cleanup-reference-mismatch`,title:`Effect cleanup removes the wrong listener reference`,severity:`error`,recommendation:`Removal APIs match by reference identity, so the second inline function passed to the remove call can never equal the one you added; hoist the handler into a single named const (or useCallback) and pass that same reference to both the add and remove calls.`,category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-needs-cleanup`,id:`effect-needs-cleanup`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-needs-cleanup`,title:`Effect subscription or timer never cleaned up`,severity:`error`,recommendation:"Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/effect-observer-needs-disconnect`,id:`effect-observer-needs-disconnect`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-observer-needs-disconnect`,title:`Observer created in an effect never disconnected`,severity:`error`,recommendation:"Return a cleanup function that calls `observer.disconnect()` (or `observer.unobserve(node)`) so the observer stops firing callbacks against detached nodes after unmount instead of leaking on every mount.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-raf-loop-needs-cancel`,id:`effect-raf-loop-needs-cancel`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-raf-loop-needs-cancel`,title:`requestAnimationFrame loop never cancelled`,severity:`warn`,recommendation:"Store the frame id and return a cleanup that calls `cancelAnimationFrame(id)` so self-scheduling work cannot continue after unmount.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-remove-listener-inline-handler`,id:`effect-remove-listener-inline-handler`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-remove-listener-inline-handler`,title:`removeEventListener called with a fresh inline handler`,severity:`error`,recommendation:"Removal APIs match the listener by reference equality, so a fresh inline arrow, function expression, or `.bind(...)` result can never equal the registered handler; hoist the handler into a named const and pass that same reference to both the add and remove calls.",category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/empty-table-header`,id:`empty-table-header`,source:`react-doctor`,originallyExternal:!0,rule:{id:`empty-table-header`,title:`Table header has no accessible text`,severity:`warn`,recommendation:`Give every table header concise text or an explicit accessible name.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/exhaustive-deps`,id:`exhaustive-deps`,source:`react-doctor`,originallyExternal:!0,rule:{id:`exhaustive-deps`,title:`Missing effect dependencies`,severity:`warn`,recommendation:`Don't blindly add missing dependencies. Read the hook callback first.
1
+ import{A as e,B as t,C as n,D as r,F as i,G as a,Ga as ee,Gt as o,H as s,I as c,J as l,Ja as u,K as d,Ka as f,L as p,M as m,N as h,O as g,P as _,R as v,S as y,T as b,U as x,Ut as S,V as C,W as w,Wa as T,Wt as E,X as D,Y as O,_ as k,a as te,an as ne,b as A,ci as j,ct as M,dt as N,i as re,j as P,k as F,li as ie,lt as I,n as ae,o as oe,on as L,ot as R,q as z,qa as B,r as V,sn as H,st as U,t as W,ui as G,ut as K,v as q,w as J,wa as se,x as Y,y as X,z as Z}from"./capability-COsjgFAJ.js";var ce=[{key:`react-doctor/active-static-asset`,id:`active-static-asset`,source:`react-doctor`,originallyExternal:!1,rule:{id:`active-static-asset`,title:`Executable SVG exposure`,severity:`warn`,recommendation:"Prefer `<img>` for SVG images; if SVG must be served directly, use attachment disposition and a CSP that blocks scripts and objects.",category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/activity-wraps-effect-heavy-subtree`,id:`activity-wraps-effect-heavy-subtree`,source:`react-doctor`,originallyExternal:!1,rule:{id:`activity-wraps-effect-heavy-subtree`,title:`Activity wraps an effect-heavy subtree`,severity:`warn`,recommendation:`Retired: Activity intentionally cleans up effects while hidden and preserves component state.`,category:`Bugs`,framework:`global`,requires:[`react`,`react:19.2`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/advanced-event-handler-refs`,id:`advanced-event-handler-refs`,source:`react-doctor`,originallyExternal:!1,rule:{id:`advanced-event-handler-refs`,title:`Listener re-subscribes on every handler change`,severity:`warn`,recommendation:"Store the handler in a ref and have the listener read `handlerRef.current()`. The subscription stays put while the latest handler still runs.",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/agent-tool-capability-risk`,id:`agent-tool-capability-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`agent-tool-capability-risk`,title:`Agent tool exposes dangerous capability`,severity:`warn`,recommendation:`Treat tool inputs as prompt-injection controlled. Validate arguments, scope permissions per call, and avoid exposing shell/file/network primitives directly to agents.`,category:`Security`,framework:`global`,tags:[`security-scan`],defaultEnabled:!1,isScanRule:!0}},{key:`react-doctor/alt-text`,id:`alt-text`,source:`react-doctor`,originallyExternal:!0,rule:{id:`alt-text`,title:`Image missing alt text`,severity:`error`,recommendation:"Give every meaningful image an `alt`, `aria-label`, or `aria-labelledby`.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],diffScope:`jsx-element`,isScanRule:!1}},{key:`react-doctor/anchor-ambiguous-text`,id:`anchor-ambiguous-text`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-ambiguous-text`,title:`Ambiguous link text`,severity:`warn`,recommendation:`Name where a link goes. Avoid 'click here', 'learn more', and 'link'.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-has-content`,id:`anchor-has-content`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-has-content`,title:`Anchor has no content`,severity:`warn`,recommendation:`Put readable text inside every link.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-is-valid`,id:`anchor-is-valid`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-is-valid`,title:`Anchor used as a button`,severity:`warn`,recommendation:"Give links a real destination. Use `<button>` for in-page actions.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-target-exists`,id:`anchor-target-exists`,source:`react-doctor`,originallyExternal:!1,rule:{id:`anchor-target-exists`,title:`Fragment link target is missing`,severity:`warn`,recommendation:`Add an element with the referenced id, or update the fragment link to point to an existing target.`,category:`Accessibility`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/aria-activedescendant-has-tabindex`,id:`aria-activedescendant-has-tabindex`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-activedescendant-has-tabindex`,title:`aria-activedescendant missing tabindex`,severity:`warn`,recommendation:"Add `tabIndex` so keyboard users can reach elements that use `aria-activedescendant`.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-braille-equivalent`,id:`aria-braille-equivalent`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-braille-equivalent`,title:`Braille-only accessible description`,severity:`warn`,recommendation:`Pair braille-specific labels and role descriptions with an equivalent accessible name or role description.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/aria-props`,id:`aria-props`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-props`,title:`Invalid ARIA attribute`,severity:`error`,recommendation:"Only use `aria-*` attributes that actually exist.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-proptypes`,id:`aria-proptypes`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-proptypes`,title:`Invalid ARIA attribute value`,severity:`error`,recommendation:"Give each `aria-*` attribute the kind of value it expects.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-role`,id:`aria-role`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-role`,title:`Invalid ARIA role`,severity:`error`,recommendation:`Use a real, non-abstract ARIA role so assistive tech can expose the element correctly.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-unsupported-elements`,id:`aria-unsupported-elements`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-unsupported-elements`,title:`ARIA on unsupported element`,severity:`error`,recommendation:"Do not put `role` or `aria-*` on reserved HTML elements.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/artifact-baas-authority-surface`,id:`artifact-baas-authority-surface`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-baas-authority-surface`,title:`BaaS authority map shipped in browser artifact`,severity:`warn`,recommendation:`Client BaaS config is often public, but shipped collection names plus owner, role, tenant, or admin fields give attackers a precise authorization map. Verify rules/RLS enforce every boundary server-side.`,category:`Security`,framework:`global`,tags:[`security-scan`],defaultEnabled:!1,isScanRule:!0}},{key:`react-doctor/artifact-env-leak`,id:`artifact-env-leak`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-env-leak`,title:`Server env leaked to browser artifact`,severity:`error`,recommendation:`Treat public env prefixes as publication, not secrecy; keep secret env vars server-only and rebuild after rotating leaked keys.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/artifact-secret-leak`,id:`artifact-secret-leak`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-secret-leak`,title:`Secret shipped in browser artifact`,severity:`error`,recommendation:`Remove the secret from client bundles/static assets, rotate it, and route privileged service calls through server-only code.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/async-await-in-loop`,id:`async-await-in-loop`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-await-in-loop`,title:`await inside a loop`,severity:`warn`,recommendation:`Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.`,category:`Performance`,framework:`global`,tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/async-defer-await`,id:`async-defer-await`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-defer-await`,title:`await before an early-return guard`,severity:`warn`,recommendation:"Move the `await` below the early-return guard so the skip path stays fast and avoids unnecessary async work.",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/async-parallel`,id:`async-parallel`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-parallel`,title:`Independent awaits run sequentially`,severity:`warn`,recommendation:"Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` so independent calls run at the same time",category:`Performance`,framework:`global`,tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/auth-token-in-web-storage`,id:`auth-token-in-web-storage`,source:`react-doctor`,originallyExternal:!1,rule:{id:`auth-token-in-web-storage`,title:`Auth token in web storage`,severity:`warn`,recommendation:"Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",category:`Security`,framework:`global`,isScanRule:!1}},{key:`react-doctor/autocomplete-valid`,id:`autocomplete-valid`,source:`react-doctor`,originallyExternal:!0,rule:{id:`autocomplete-valid`,title:`Invalid autocomplete value`,severity:`warn`,recommendation:"Use a valid autofill token in `autoComplete` so browsers can fill the right field reliably.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/base-ui-dialog-popup-requires-title`,id:`base-ui-dialog-popup-requires-title`,source:`react-doctor`,originallyExternal:!1,rule:{id:`base-ui-dialog-popup-requires-title`,title:`Base UI dialog popup without a title`,severity:`warn`,recommendation:`Give every Base UI Dialog.Popup and AlertDialog.Popup a Title part (visually hidden when the design shows no heading) or name the dialog with aria-label.`,category:`Accessibility`,framework:`global`,requires:[`react`,`base-ui`],isScanRule:!1}},{key:`react-doctor/base-ui-field-requires-label`,id:`base-ui-field-requires-label`,source:`react-doctor`,originallyExternal:!1,rule:{id:`base-ui-field-requires-label`,title:`Base UI field control without a label`,severity:`warn`,recommendation:`Give every Field.Root that wraps a Field.Control a Field.Label (visually hidden when the design shows no label) or name the control directly with aria-label.`,category:`Accessibility`,framework:`global`,requires:[`react`,`base-ui`],isScanRule:!1}},{key:`react-doctor/base-ui-tabs-tab-requires-list`,id:`base-ui-tabs-tab-requires-list`,source:`react-doctor`,originallyExternal:!1,rule:{id:`base-ui-tabs-tab-requires-list`,title:`Base UI tab is outside Tabs.List`,severity:`warn`,recommendation:`Render each Base UI Tabs.Tab inside Tabs.List so the tablist role and arrow-key focus handling cover every tab.`,category:`Bugs`,framework:`global`,requires:[`base-ui`],matchByOccurrence:!0,isScanRule:!1}},{key:`react-doctor/build-pipeline-secret-boundary`,id:`build-pipeline-secret-boundary`,source:`react-doctor`,originallyExternal:!1,rule:{id:`build-pipeline-secret-boundary`,title:`Build pipeline runs code near secrets`,severity:`warn`,recommendation:`Run dependency installs with scripts disabled before exposing secrets, isolate untrusted build code, and move signing/deploy authority into a narrow privileged step.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/button-has-type`,id:`button-has-type`,source:`react-doctor`,originallyExternal:!0,rule:{id:`button-has-type`,title:`Button missing explicit type`,severity:`warn`,recommendation:'Set an explicit button `type` so plain buttons do not submit forms by accident: `type="button"`, `"submit"`, or `"reset"`.',category:`Bugs`,framework:`global`,requires:[`react`],diffScope:`jsx-element`,isScanRule:!1}},{key:`react-doctor/checked-requires-onchange-or-readonly`,id:`checked-requires-onchange-or-readonly`,source:`react-doctor`,originallyExternal:!0,rule:{id:`checked-requires-onchange-or-readonly`,title:`Checked input without onChange`,severity:`warn`,recommendation:"Add `onChange`, `readOnly`, or `defaultChecked` so React knows whether the checkbox is editable, display-only, or uncontrolled.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/circular-dependency`,id:`circular-dependency`,source:`react-doctor`,originallyExternal:!1,rule:{id:`circular-dependency`,title:`Runtime import cycle`,severity:`warn`,recommendation:`Break the runtime cycle by extracting shared code into a lower-level module or inverting one dependency.`,category:`Maintainability`,framework:`global`,tags:[`project-analysis`],defaultEnabled:!1,isScanRule:!1,isProjectRule:!0}},{key:`react-doctor/class-component-missing-component-will-unmount-teardown`,id:`class-component-missing-component-will-unmount-teardown`,source:`react-doctor`,originallyExternal:!1,rule:{id:`class-component-missing-component-will-unmount-teardown`,title:`Class component acquires a resource with no teardown`,severity:`warn`,recommendation:"Release listeners and timers acquired in `componentDidMount`/`constructor` by adding a `componentWillUnmount` that removes them (or use MobX `disposeOnUnmount`).",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/click-events-have-key-events`,id:`click-events-have-key-events`,source:`react-doctor`,originallyExternal:!0,rule:{id:`click-events-have-key-events`,title:`Click handler missing keyboard handler`,severity:`warn`,recommendation:"Pair `onClick` with a key handler so keyboard users can trigger it.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],diffScope:`jsx-element`,isScanRule:!1}},{key:`react-doctor/clickjacking-redirect-risk`,id:`clickjacking-redirect-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`clickjacking-redirect-risk`,title:`Redirect or frame boundary risk`,severity:`warn`,recommendation:"Allowlist redirect origins/paths, set `frame-ancestors` for privileged pages, and avoid URL-prefilled privileged dialogs.",category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/client-localstorage-no-version`,id:`client-localstorage-no-version`,source:`react-doctor`,originallyExternal:!1,rule:{id:`client-localstorage-no-version`,title:`Unversioned localStorage key`,severity:`warn`,recommendation:`Retired: Storage can keep its version in the payload or use a separate migration path.`,category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/client-passive-event-listeners`,id:`client-passive-event-listeners`,source:`react-doctor`,originallyExternal:!1,rule:{id:`client-passive-event-listeners`,title:`Non-passive scroll listener`,severity:`warn`,recommendation:"Add `{ passive: true }` as the third argument: `addEventListener('touchmove', handler, { passive: true })`. Only do this if the handler doesn't call `event.preventDefault()`, since passive listeners ignore it (which breaks pull-to-refresh, custom gestures, and nested scrolling).",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/command-execution-input-risk`,id:`command-execution-input-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`command-execution-input-risk`,title:`Command execution uses caller-shaped input`,severity:`error`,recommendation:`Avoid shell execution for caller-controlled values. Use fixed commands, argument arrays, strict allowlists, and no shell interpolation.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/context-provider-value-from-unmemoized-local-literal`,id:`context-provider-value-from-unmemoized-local-literal`,source:`react-doctor`,originallyExternal:!1,rule:{id:`context-provider-value-from-unmemoized-local-literal`,title:`Context value from an unmemoized local literal`,severity:`warn`,recommendation:`Memoize the context value in component scope so consumers do not redraw every render. For mapped providers, extract each item into a child component and memoize there.`,category:`Performance`,framework:`global`,requires:[`react`],disabledWhen:[`react-compiler`],tags:[`react-jsx-only`,`test-noise`],isScanRule:!1}},{key:`react-doctor/control-has-associated-label`,id:`control-has-associated-label`,source:`react-doctor`,originallyExternal:!0,rule:{id:`control-has-associated-label`,title:`Control missing accessible label`,severity:`warn`,recommendation:`Give every interactive control a label screen readers can read.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/cors-cookie-trust-risk`,id:`cors-cookie-trust-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`cors-cookie-trust-risk`,title:`Broad cookie or credentialed CORS trust`,severity:`warn`,recommendation:`Keep auth cookies host-only and HttpOnly, avoid credentialed CORS for less-trusted docs/vendor origins, and isolate documentation domains from app sessions.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/dangerous-html-sink`,id:`dangerous-html-sink`,source:`react-doctor`,originallyExternal:!1,rule:{id:`dangerous-html-sink`,title:`HTML injection sink with dynamic content`,severity:`warn`,recommendation:`Prefer rendering structured React nodes. If HTML is required, sanitize with a well-reviewed sanitizer and keep the trust boundary close to the sink.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/data-table-requires-accessible-name`,id:`data-table-requires-accessible-name`,source:`react-doctor`,originallyExternal:!1,rule:{id:`data-table-requires-accessible-name`,title:`Data table has no accessible name`,severity:`warn`,recommendation:`Give data tables a concise caption, or reference an existing visible title with aria-labelledby.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/debounce-no-cleanup`,id:`debounce-no-cleanup`,source:`react-doctor`,originallyExternal:!1,rule:{id:`debounce-no-cleanup`,title:`Memoized debounce never cancelled on unmount`,severity:`warn`,recommendation:"A debounced/throttled callback holds a pending timer that still fires after unmount, so add `useEffect(() => () => debounced.cancel(), [debounced])` to cancel the trailing invocation when the component tears down.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/design-no-em-dash-in-jsx-text`,id:`design-no-em-dash-in-jsx-text`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-em-dash-in-jsx-text`,title:`Em dash in JSX text`,severity:`warn`,recommendation:`Retired: Visual and writing preferences belong in an explicit design guide, not a general defect check.`,category:`Bugs`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-redundant-padding-axes`,id:`design-no-redundant-padding-axes`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-redundant-padding-axes`,title:`Redundant padding axes`,severity:`warn`,recommendation:"Collapse matching padding axes to `p-N` so duplicated classes do not make spacing harder to scan; keep split axes only when breakpoints differ.",category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-redundant-size-axes`,id:`design-no-redundant-size-axes`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-redundant-size-axes`,title:`Redundant width and height axes`,severity:`warn`,recommendation:"Collapse matching width and height to `size-N` so duplicated classes do not make layout harder to scan.",category:`Maintainability`,framework:`global`,requires:[`react`,`tailwind:3.4`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-space-on-flex-children`,id:`design-no-space-on-flex-children`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-space-on-flex-children`,title:`space-* utility on flex children`,severity:`warn`,recommendation:"Use `gap-*` on the flex or grid parent. `space-x-*` and `space-y-*` leave gaps when a child is hidden, miss spacing on wrapped lines, and don't flip in right-to-left layouts.",category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-three-period-ellipsis`,id:`design-no-three-period-ellipsis`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-three-period-ellipsis`,title:`Three dots instead of ellipsis`,severity:`warn`,recommendation:`Retired: Visual and writing preferences belong in an explicit design guide, not a general defect check.`,category:`Bugs`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-vague-button-label`,id:`design-no-vague-button-label`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-vague-button-label`,title:`Vague button label`,severity:`warn`,recommendation:`Name the action: "Save changes" instead of "Continue", "Send invite" instead of "Submit". The label is the button's accessible name.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/details-requires-summary`,id:`details-requires-summary`,source:`react-doctor`,originallyExternal:!1,rule:{id:`details-requires-summary`,title:`Details disclosure has no explicit summary`,severity:`warn`,recommendation:`Make a meaningful summary the first child of details so users can understand and operate the disclosure.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/dialog-has-accessible-name`,id:`dialog-has-accessible-name`,source:`react-doctor`,originallyExternal:!1,rule:{id:`dialog-has-accessible-name`,title:`Dialog without accessible name`,severity:`warn`,recommendation:'Give every `<dialog>` / `role="dialog"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog\'s title element).',category:`Accessibility`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/display-name`,id:`display-name`,source:`react-doctor`,originallyExternal:!0,rule:{id:`display-name`,title:`Component missing display name`,severity:`warn`,recommendation:"Give each component a `displayName` so DevTools shows a clear name.",category:`Maintainability`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/duplicate-jsx-subtree`,id:`duplicate-jsx-subtree`,source:`react-doctor`,originallyExternal:!1,rule:{id:`duplicate-jsx-subtree`,title:`Duplicated JSX structure`,severity:`warn`,recommendation:`Extract a shared component when the repeated JSX trees represent the same UI concept and should evolve together.`,category:`Maintainability`,framework:`global`,tags:[`project-analysis`],isScanRule:!1,isProjectRule:!0}},{key:`react-doctor/effect-listener-cleanup-mismatch`,id:`effect-listener-cleanup-mismatch`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-listener-cleanup-mismatch`,title:`Effect cleanup does not match its event listener`,severity:`error`,recommendation:"Pass the same callback binding and capture flag to `addEventListener` and `removeEventListener`, or abort the registration's local AbortController during cleanup.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-listener-cleanup-reference-mismatch`,id:`effect-listener-cleanup-reference-mismatch`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-listener-cleanup-reference-mismatch`,title:`Effect cleanup removes the wrong listener reference`,severity:`error`,recommendation:`Removal APIs match by reference identity, so the second inline function passed to the remove call can never equal the one you added; hoist the handler into a single named const (or useCallback) and pass that same reference to both the add and remove calls.`,category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-needs-cleanup`,id:`effect-needs-cleanup`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-needs-cleanup`,title:`Effect subscription or timer never cleaned up`,severity:`error`,recommendation:"Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/effect-observer-needs-disconnect`,id:`effect-observer-needs-disconnect`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-observer-needs-disconnect`,title:`Observer created in an effect never disconnected`,severity:`error`,recommendation:"Return a cleanup function that calls `observer.disconnect()` (or `observer.unobserve(node)`) so the observer stops firing callbacks against detached nodes after unmount instead of leaking on every mount.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-raf-loop-needs-cancel`,id:`effect-raf-loop-needs-cancel`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-raf-loop-needs-cancel`,title:`requestAnimationFrame loop never cancelled`,severity:`warn`,recommendation:"Store the frame id and return a cleanup that calls `cancelAnimationFrame(id)` so self-scheduling work cannot continue after unmount.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-remove-listener-inline-handler`,id:`effect-remove-listener-inline-handler`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-remove-listener-inline-handler`,title:`removeEventListener called with a fresh inline handler`,severity:`error`,recommendation:"Removal APIs match the listener by reference equality, so a fresh inline arrow, function expression, or `.bind(...)` result can never equal the registered handler; hoist the handler into a named const and pass that same reference to both the add and remove calls.",category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/empty-table-header`,id:`empty-table-header`,source:`react-doctor`,originallyExternal:!0,rule:{id:`empty-table-header`,title:`Table header has no accessible text`,severity:`warn`,recommendation:`Give every table header concise text or an explicit accessible name.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/exhaustive-deps`,id:`exhaustive-deps`,source:`react-doctor`,originallyExternal:!0,rule:{id:`exhaustive-deps`,title:`Missing effect dependencies`,severity:`warn`,recommendation:`Don't blindly add missing dependencies. Read the hook callback first.
2
2
 
3
3
  Bad:
4
4
  useEffect(() => {