/** * ESLint Rule: no-incorrect-provider-order * * Validates that React context providers are nested in the correct order * according to the BanGUI provider dependency contract. * * Provider Order (enforced by this rule): * 1. ThemeProvider (OUTERMOST — must be first) * 2. FluentProvider (wraps all Fluent UI consumers) * 3. NotificationProvider (must wrap error boundaries) * 4. ErrorBoundary (top-level error boundary) * 5. BrowserRouter (enables routing) * 6. NavigationCancellationProvider (manages route cancellation) * 7. AuthProvider (session validation) * 8. TimezoneProvider (INNERMOST — wraps protected routes only) */ const PROVIDER_ORDER = [ "ThemeProvider", "FluentProvider", "NotificationProvider", "ErrorBoundary", "BrowserRouter", "NavigationCancellationProvider", "AuthProvider", "TimezoneProvider", ]; /** * Get the provider name from a JSX element */ function getProviderName(node) { if ( node.name && node.name.type === "JSXIdentifier" && PROVIDER_ORDER.includes(node.name.name) ) { return node.name.name; } return null; } /** * Get the position of a provider in the ordering sequence */ function getProviderPosition(name) { return PROVIDER_ORDER.indexOf(name); } /** * Check if a JSX element is a provider */ function isProvider(node) { return getProviderName(node) !== null; } /** * Extract all JSX provider elements from a component tree */ function extractProviderStack(node) { const stack = []; let current = node; while (current) { if (!current.openingElement) break; const providerName = getProviderName(current.openingElement); if (providerName) { stack.push({ name: providerName, node: current.openingElement, }); } // Find the next JSX element by looking at children if (current.children && current.children.length > 0) { const firstChild = current.children[0]; if (firstChild && firstChild.type === "JSXElement") { current = firstChild; } else { break; } } else { break; } } return stack; } export default { meta: { type: "problem", docs: { description: "Enforce correct nesting order of React context providers in BanGUI", category: "Best Practices", recommended: "error", url: "https://github.com/bangui/frontend/blob/main/src/providers/PROVIDER_ORDER.md", }, fixable: undefined, schema: [], messages: { incorrectOrder: "Provider '{{ outer }}' (position {{ outerPos }}) must wrap '{{ inner }}' (position {{ innerPos }}). Current nesting: {{ outer }} → {{ inner }}", missingParent: "Provider '{{ current }}' (position {{ currentPos }}) requires parent provider '{{ required }}' (position {{ requiredPos }}). Expected: {{ expected }}", }, }, create(context) { return { JSXElement(node) { // Skip non-provider elements if (!node.openingElement || !isProvider(node.openingElement)) { return; } // Extract the provider stack from this element const stack = extractProviderStack(node); if (stack.length < 2) { // No nesting to validate return; } // Check ordering from outermost to innermost for (let i = 0; i < stack.length - 1; i++) { const outer = stack[i]; const inner = stack[i + 1]; const outerPos = getProviderPosition(outer.name); const innerPos = getProviderPosition(inner.name); // Inner provider must have a higher position (come later in the sequence) if (innerPos <= outerPos) { context.report({ node: inner.node, messageId: "incorrectOrder", data: { outer: outer.name, outerPos: String(outerPos), inner: inner.name, innerPos: String(innerPos), }, }); } } // Check that all providers before the first one in the stack are present const firstProvider = stack[0]; const firstPos = getProviderPosition(firstProvider.name); if (firstPos > 0) { const required = PROVIDER_ORDER[firstPos - 1]; context.report({ node: firstProvider.node, messageId: "missingParent", data: { current: firstProvider.name, currentPos: String(firstPos), required, requiredPos: String(firstPos - 1), expected: PROVIDER_ORDER.slice(0, firstPos).join(" → "), }, }); } }, }; }, };