Skip to content

feat: add no-redirect-to-route-group and require-auth-initiate-call rules#62

Open
danielchen0 wants to merge 5 commits intomainfrom
danielchen0/expo-routing-and-auth-rules
Open

feat: add no-redirect-to-route-group and require-auth-initiate-call rules#62
danielchen0 wants to merge 5 commits intomainfrom
danielchen0/expo-routing-and-auth-rules

Conversation

@danielchen0
Copy link
Copy Markdown
Collaborator

Summary

Two new expo-tagged rules driven by recurring V2 mobile preview regressions where the agent generated code that compiled clean but rendered a blank screen. Both bugs were traced live in escher#fix_mobile_v2_previews before this PR.

no-redirect-to-route-group (error)

In Expo Router, segments wrapped in parens like (tabs) are route groups — they're stripped from URL resolution. So <Redirect href="/(tabs)" /> targets a path that doesn't resolve to any concrete route: app/(tabs)/index.tsx maps to /, not /(tabs). When both app/index.tsx (with the redirect) and app/(tabs)/index.tsx exist, you get a route conflict or a silent no-op redirect, and the screen renders blank.

The bug is encouraged by V2's own system prompt at stacks/v2/prompts/system.ts:460-472 which literally shows <Redirect href="/(tabs)" /> as the canonical example. The companion fix is to either correct or delete that example, but until then this lint rule fails the commit so the agent self-corrects.

Flag any href whose segments are all route-group segments with no concrete path beyond them. /(tabs)/explore is fine (strips to /explore); /(tabs) alone is the bug.

require-auth-initiate-call (error)

The shipped V2 mobile useAuth() exposes isReady (gates render until the persisted JWT loads from SecureStore) and initiate() (the function that does the load). The gate doesn't flip on its own. If a layout gates render on isReady but never calls initiate(), the gate stays closed forever and the app renders null/blank — the exact regression we hit in production project group c607f7e2-c087-4c3f-8015-5a26b522da58, where rev 5 of _layout.tsx stripped useAuth().initiate() while keeping the isReady gate.

Fires when:

  • useAuth() is destructured to pull isReady (or aliased: { isReady: authReady })
  • That name is used in a negated render-gate (if (!isReady) return ...)
  • initiate is neither destructured-and-invoked nor called via member access on an auth return

Includes a "non-gate" exception: a file that uses isReady only inside JSX (e.g. {isReady ? <Profile /> : null}) doesn't fire — that's a sibling of the layout that's allowed to consume the global auth state without bootstrapping it.

Tests

  • 11 cases for no-redirect-to-route-group: literal + JSXExpressionContainer href forms, leading/trailing slashes, multiple group segments, dynamic hrefs, non-Redirect components.
  • 10 cases for require-auth-initiate-call: happy path, aliased isReady, member-access initiate, non-gate isReady reads (negative case), and the literal agent-stripped layout that triggered this PR.
  • Rule count assertion bumped from 50 → 52.

Risks

  • require-auth-initiate-call looks at any useAuth() symbol. If a different module also exports useAuth with different semantics, the rule still fires. In practice the V2 stack only has the one shipped useAuth, and the rule has a strict "must be used as a render gate" filter that should keep false positives close to zero. If it surfaces noise, narrowing to useAuth from a specific import path is a one-line follow-up.
  • no-redirect-to-route-group only checks string-literal hrefs. A redirect built from a template literal or variable bypasses the rule — but those are rare in agent-generated code, and adding template-literal support is straightforward if needed.
  • No risk to existing rules: the new rules are additive and registered alongside the rest. npm test passes 422/422.

Test plan

  • npm test — 422/422
  • npm run lint — 0 errors (20 pre-existing fs warnings unchanged)
  • npm run build — green
  • npx prettier --check . — clean
  • npx knip — clean
  • After merge + publish, bump laint in escher and confirm the agent receives lint feedback for the two patterns end-to-end

@daniel-chen-1
Copy link
Copy Markdown

daniel-chen-1 Bot commented May 1, 2026

Auto-fix: ran prettier --write on src/rules/require-auth-initiate-call.ts to address the failing Lint & Format CI job. The fix added a trailing comma on a multi-line function-call argument list in the IfStatement visitor (line 112). Pushed as commit ff783e3. CI should re-run and pass.

— danielchen0-pr-monitor

JSXOpeningElement(path) {
const { name, attributes } = path.node;

if (name.type !== 'JSXIdentifier' || name.name !== 'Redirect') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same bug class via imperative API isn't caught (verified):

router.replace('/(tabs)'); router.push('/(tabs)'); router.navigate('/(tabs)');

Add a CallExpression visitor for <id>.replace/.push/.navigate with a string-literal first arg, reusing hrefIsOnlyRouteGroups. If out of scope here, document the gap in the rule's JSDoc.

Comment on lines +108 to +122
IfStatement(path) {
const { test, consequent } = path.node;
const matchedName = matchesNegatedIdentifier(
test,
useAuthDestructures.map((d) => d.isReadyName).filter((n): n is string => n !== null),
);
if (matchedName === null) return;
if (containsReturn(consequent)) {
usedAsRenderGate = true;
}
},
});

if (!usedAsRenderGate) {
return results;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Render-gate detection misses two common forms (verified []):

return isReady ? <Stack /> : null;          // ternary
if (isReady) return <Stack />; return null; // positive early-return

Same bug from a codegen perspective. Extend usedAsRenderGate to recognize:

  • ReturnStatement whose argument is a ConditionalExpression testing the destructured name;
  • IfStatement with a positive identifier test (no !) whose consequent returns.

return results;
}

function matchesNegatedIdentifier(node: unknown, names: string[]): string | null {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unknown + ad-hoc casts in matchesNegatedIdentifier/containsReturn (and extractStringLiteral in the sibling rule) fight CLAUDE.md's "use Babel's type system directly". Use t.isUnaryExpression/t.isIdentifier/t.isReturnStatement/t.isBlockStatement to narrow.

@daniel-chen-1
Copy link
Copy Markdown

daniel-chen-1 Bot commented May 2, 2026

@danielchen0 dx5v just approved this PR (3:36Z) with 3 non-blocking inline review comments worth a look before merging:

  1. src/rules/no-redirect-to-route-group.ts L26 — rule misses imperative API forms (router.replace('/(tabs)'), .push, .navigate); suggestion: add a CallExpression visitor or document the gap in JSDoc.
  2. src/rules/require-auth-initiate-call.ts L122 — render-gate detection misses ternary returns and positive early-returns (verified false-negatives); suggestion: extend usedAsRenderGate to recognize ReturnStatement+ConditionalExpression and IfStatement with positive identifier test.
  3. src/rules/require-auth-initiate-call.ts L141 — unknown + ad-hoc casts in matchesNegatedIdentifier/containsReturn/extractStringLiteral conflict with CLAUDE.md's "use Babel's type system directly"; suggestion: use t.isUnaryExpression/t.isIdentifier/t.isReturnStatement/t.isBlockStatement predicates.

All three are substantive logic/typing changes I won't apply autonomously. Also reminder: branch is still dirty (merge conflict, rebaseable=false) since 01:21Z May 1 — needs your rebase before merge.

— danielchen0-pr-monitor

@daniel-chen-1
Copy link
Copy Markdown

daniel-chen-1 Bot commented May 3, 2026

Auto-fix: Lint & Format CI was failing on this rebase because README.md needed prettier formatting. Pushed prettier --write README.md as a follow-up commit. CI should go green on the next run.

…ules

Rebased onto main to resolve conflicts after #51 merge.
@daniel-chen-1 daniel-chen-1 Bot force-pushed the danielchen0/expo-routing-and-auth-rules branch from f560084 to ee04f11 Compare May 4, 2026 16:24
@daniel-chen-1
Copy link
Copy Markdown

daniel-chen-1 Bot commented May 4, 2026

Rebased onto main after #51 merged — the conflicts were mechanical (rule-registration list, README rule table, rule-count assertion, meta.ts platform map), no logic touched. New head is ee04f11, all 7 CI checks green, mergeable=true. Ready to merge when you are.

@danielchen0 danielchen0 enabled auto-merge (squash) May 4, 2026 17:51
lainterr Bot and others added 2 commits May 4, 2026 18:00
Auto-resolved trivial conflicts in shared files. Resolved by Lainter (steward).
@daniel-chen-1
Copy link
Copy Markdown

daniel-chen-1 Bot commented May 4, 2026

Auto-fix: lainterr's main-merge bumped this branch's head and the README rule-table alignment drifted (prettier flagged it on the new head). Pushed prettier --write README.md as commit fe53900 to clear the Lint & Format check.

dx5v's three inline review comments on this PR (router.replace/push/navigate variants, ternary + positive-if render gates, and Babel-typed narrowing) are still open — flagging since you also asked me to address review comments on #57. Want me to apply the same pattern here? They're real false-negative gaps but the fixes are larger than a one-line auto-fix, so I'd rather get a thumbs-up before touching the rule logic on this one.

lainterr Bot and others added 2 commits May 4, 2026 18:30
Auto-resolved trivial conflicts in shared files. Resolved by Lainter (steward).
@daniel-chen-1
Copy link
Copy Markdown

daniel-chen-1 Bot commented May 4, 2026

Auto-fix: lainterr's 18:30 main-merge (head 2a9e06d) again drifted the General Rules table column alignment in README.md and left a stray blank line above ## Adding a New Rule. Pushed prettier --write README.md as commit c09ee23 to clear Lint & Format. CI should go green again on the next run.

laint/55 README came back clean from this main-merge — no fix needed there. dx5v's three inline review comments on this PR are still unaddressed; let me know if you want me to take them on or if you'd rather handle them yourself.

@danielchen0 danielchen0 disabled auto-merge May 5, 2026 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants