Skip to content

fix(core): stop effect-as-onmount flagging member-read reactivity; align effect-rule docs - #419

Merged
oekazuma merged 3 commits into
mainfrom
fix/effect-rules-validity
Aug 8, 2026
Merged

oekazuma merged 3 commits into
mainfrom
fix/effect-rules-validity

Conversation

@oekazuma

@oekazuma oekazuma commented Aug 8, 2026 •

Copy link
Copy Markdown
Owner

From the 2026-08-09 v1.0 rule-validity review (Priority-1 rows 1 and 5) — the review's highest-impact finding.

Finding 1: correctness/effect-as-onmount's advice broke working code

bodyReadsReactive recognized reactivity only through same-file rune declarators, so four officially-recommended reactive idioms — class instances with $state fields, SvelteMap/SvelteSet reads, imported runes-module state, svelte/reactivity/window — were indistinguishable from the intended true positive. The "reads no reactive value — use onMount instead" advice, applied to any of them, converts a re-running effect into run-once: a stale-UI bug. Verified empirically by two independent reviewers before this fix.

Fix (detection narrowing, deliberately conservative): local names bound by non-type imports and locals initialized with new expressions are folded into reactiveNames — an imported binding or class instance is opaque to static analysis and may carry hidden reactivity, so it suppresses the finding wherever referenced. The trade (member-form access on an imported binding — e.g. analytics.track() — now escapes detection; bare imported calls already did on both sides) is intentional: a missed finding is cheaper than wrong advice. The remaining blind spot (const c = createCounter() — no syntactic marker) is admitted in the code comment and the doc's new Known-limitation section; the design doc carries a dated addendum retiring its "no false positives" claim. Rule wording now names {@attach}/event handlers alongside onMount per current Svelte guidance (MCP-verified).

Finding 5: interlocking docs contradiction

The documented fixes in server-browser-global.md / instance-browser-global.md ($effect(() => { x = localStorage/window… })) were themselves flagged by effect-as-derived, whose "$derived" advice would reintroduce the SSR ReferenceError those rules prevent. Both docs' fixes are now onMount (SSR-inert, flagged by neither effect rule; svelte/reactivity/window shown as the modern idiom for instance-browser-global), effect-as-derived.md gains the browser-global-capture limitation beside its existing one, and its "updates synchronously" claim is corrected to push-pull/lazy. All shipped fix snippets autofixer-clean; the one deliberately-flagged illustration in the limitation section is labeled as the documented trap, not a fix.

Verification

  • 5 of 6 new tests confirmed failing pre-fix (4 fact-level idioms + 1 rule-level end-to-end); the true-positive pin passed both sides as designed.
  • Zero existing tests broken by the narrowing. pnpm -r typecheck / pnpm test (core 1360, cli 875, vite 213) / pnpm lint / pnpm -r build / check:publish all green; rules-index regenerated (frontmatter description changed).
  • Changeset: @svelte-vitals/core patch (false-positive removals + docs corrections).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved effect analysis to avoid incorrect onMount recommendations for reactive class instances, collections, imported state, and browser-reactivity bindings.
    • Clarified handling of browser globals and server-side rendering risks.
  • Documentation

    • Updated guidance for choosing event handlers, {@Attach}, onMount, or $derived.
    • Added limitations, remediation examples, and clearer explanations of lazy derived values and hydration behavior.
  • Tests

    • Added regression coverage for reactive member access and mount-only effects.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@oekazuma, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4fb6ad11-d1fe-47b6-ad31-bde56bd75920

📥 Commits

Reviewing files that changed from the base of the PR and between dee9ea0 and 824d695.

📒 Files selected for processing (8)
  • .changeset/effect-rules-validity.md
  • docs/src/content/docs/ja/rules/correctness/effect-as-derived.md
  • docs/src/content/docs/ja/rules/correctness/effect-as-onmount.md
  • docs/src/content/docs/ja/rules/correctness/instance-browser-global.md
  • docs/src/content/docs/rules/correctness/effect-as-derived.md
  • docs/src/content/docs/rules/correctness/effect-as-onmount.md
  • docs/src/content/docs/rules/correctness/instance-browser-global.md
  • docs/superpowers/specs/2026-07-02-correct003-effect-as-onmount-design.md
📝 Walkthrough

Walkthrough

The PR expands correctness/effect-as-onmount reactive binding detection, adds regression tests, updates rule guidance, and revises English and Japanese documentation for effect alternatives, $derived, and browser-global access.

Changes

CORRECT003 refinement

Layer / File(s) Summary
Reactive binding detection and regression coverage
packages/core/src/component-parse.ts, packages/core/test/*
Effect analysis now recognizes imported bindings and locals initialized with new expressions as reactive. Tests cover class instances, Svelte collections, imported state, window bindings, and module imports.
Rule guidance and design record
packages/core/src/rules/correctness/effect-as-onmount.ts, docs/superpowers/specs/..., docs/src/content/docs/{,ja/}rules/correctness/effect-as-onmount.md
Guidance now distinguishes event handlers, {@Attach}, and onMount. The documentation records supported reactive sources and remaining false-positive cases.
Rule and browser-global documentation
.changeset/*, docs/src/content/docs/{,ja}/rules/*
Documentation updates describe lazy $derived recomputation, browser-global limitations, onMount, and svelte/reactivity/window fixes. The changeset records a patch release.
Estimated code review effort: 3 (Moderate) ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main detection fix and the related documentation updates.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…t wording, module-script test, {@Attach} version gate

Tightens "new-ed local" phrasing to "declared with a new …() initializer" everywhere it
appears (assignment-form `let m; m = new SvelteMap();` is still undetected, now documented
as a second known limitation instead of implied covered). Adds a fact-level test pinning the
<script module> import-folding path (mutation-confirmed: fails when the moduleProgram
collectImportedLocalNames/collectNewExprLocalNames calls are removed). Adds the {@Attach}
5.29+ version gate to the docs, matching the sibling reactivity/window 5.11.0+ note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oekazuma

oekazuma commented Aug 8, 2026

Copy link
Copy Markdown
Owner Author

An independent fresh-context review was run on this PR (the maintainer requested extra caution against regressions). Verdict: APPROVE — merging cannot make the tool worse for any rule other than the intended one.

Key verifications by the reviewer:

  • The reactiveNames widening is structurally confined to effect-as-onmount's mountOnly fact (every consumer traced; effect-as-derived output byte-identical pre/post on the doc-snippet E2E).
  • Pre-fix reproduction exact (5 of 6 new tests fail on main, as the PR claims); both new collectors are mutation-covered.
  • A 16-probe boundary table classifying retained true positives (browser globals, plain locals, literals), fixed false positives (the four documented idioms), and the declared new misses (member-form access on imported bindings) — suppression-only by construction.
  • All MCP/autofixer/docs checks pass, en/ja parity full.

Four non-blocking notes, all addressed in the latest commit: the "initialized with new" wording tightened to declarator-form (the assignment-form residual is now a documented limitation), the previously-untested <script module> folding path gained a mutation-verified test, {@attach} is version-gated "(5.29+)" per the official docs, and the PR body's trade sentence was corrected (bare imported calls escaped on both sides; the new misses are member-form only).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/src/content/docs/ja/rules/correctness/effect-as-derived.md`:
- Around line 43-44: Update the documentation around $derived, $effect, and
canVibrate: state that $effect runs after component mount, and clarify that the
$derived expression is evaluated when read, including during SSR when it is
referenced. Preserve the guidance to suppress the check with
svelte-vitals-disable-next-line rather than replacing $effect with $derived.

In `@docs/src/content/docs/ja/rules/correctness/instance-browser-global.md`:
- Around line 22-32: Remove the SSR-unsafe `const width = window.innerWidth`
statement from the safe example in the Japanese and English
`instance-browser-global` documentation; keep it only in the preceding unsafe
example (or comment it out), leaving the fix block with the
`svelte/reactivity/window` import and `innerWidth.current` usage.

In `@docs/superpowers/specs/2026-07-02-correct003-effect-as-onmount-design.md`:
- Around line 162-165: Update the 2026-08-09 addendum in the specification to
use the correct review date and revise the related past-tense statements,
including “was refuted” and “Fixed,” so the document does not present a future
event as completed history.
- Around line 164-178: The earlier rule section covering the recommendation,
rationale, diagnostic, and “conservative — no false positives” claim must be
synchronized with the current contract in effect-as-onmount.ts. Update it to
include the current detection behavior and remaining false-positive cases, or
explicitly mark the outdated statements as historical; ensure the section no
longer presents onMount-only guidance or guarantees zero false positives.

In `@packages/core/src/component-parse.ts`:
- Around line 932-954: Replace text-only reactiveNames tracking with
lexical-scope binding resolution in the component analysis flow, including
collectImportedLocalNames and collectNewExprLocalNames. Mark identifier reads
reactive only when their resolved declaration is a selected import or a
new-expression declarator in an enclosing scope, excluding shadowing and
bindings declared inside the current effect callback. Add regression coverage
for effect-local shadowing and effect-local new bindings.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f28944e-0247-4308-affd-ef8d60504080

📥 Commits

Reviewing files that changed from the base of the PR and between d2e29c0 and dee9ea0.

📒 Files selected for processing (18)
  • .changeset/effect-rules-validity.md
  • docs/src/content/docs/ja/rules/correctness/effect-as-derived.md
  • docs/src/content/docs/ja/rules/correctness/effect-as-onmount.md
  • docs/src/content/docs/ja/rules/correctness/index.mdx
  • docs/src/content/docs/ja/rules/correctness/instance-browser-global.md
  • docs/src/content/docs/ja/rules/correctness/server-browser-global.md
  • docs/src/content/docs/ja/rules/index.mdx
  • docs/src/content/docs/rules/correctness/effect-as-derived.md
  • docs/src/content/docs/rules/correctness/effect-as-onmount.md
  • docs/src/content/docs/rules/correctness/index.mdx
  • docs/src/content/docs/rules/correctness/instance-browser-global.md
  • docs/src/content/docs/rules/correctness/server-browser-global.md
  • docs/src/content/docs/rules/index.mdx
  • docs/superpowers/specs/2026-07-02-correct003-effect-as-onmount-design.md
  • packages/core/src/component-parse.ts
  • packages/core/src/rules/correctness/effect-as-onmount.ts
  • packages/core/test/component-parse.test.ts
  • packages/core/test/correctness-rules.test.ts

Comment thread docs/src/content/docs/ja/rules/correctness/effect-as-derived.md Outdated
Comment thread docs/src/content/docs/ja/rules/correctness/instance-browser-global.md Outdated
Comment thread packages/core/src/component-parse.ts
…ble fix block split, shadowing clause

- effect-as-derived.md (en+ja): "$effect runs one tick after mount" -> "runs only after the
  component has mounted to the DOM" (matches the official docs' framing); "$derived evaluates
  during SSR" unconditional claim qualified to "evaluates when read, and a template read happens
  during SSR too" — same imprecision existed in both locales, both fixed.
- instance-browser-global.md (en+ja): the svelte/reactivity/window fix fence contained the ❌
  window.innerWidth line ahead of the ✅ import — a verbatim copy would still crash SSR. Split
  into a standalone ❌ example and a fix fence containing only the import + usage. Both
  autofixer-verified clean.
- 2026-07-02-correct003-effect-as-onmount-design.md: one-line inline pointer at the refuted
  "conservative — no false positives" claim; no other historical-section rewrites, per convention.
- effect-as-onmount.md (en+ja) + changeset: added the shadowing-granularity clause — matching is
  by identifier text, not lexical scope (same as the rule's pre-existing rune-name handling), so a
  callback-local binding that shadows an imported/new-declared name still reads as reactive; this
  can only suppress a finding, never wrongly flag one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oekazuma
oekazuma merged commit 72d908d into main Aug 8, 2026
8 checks passed
@oekazuma
oekazuma deleted the fix/effect-rules-validity branch August 8, 2026 18:24
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.

1 participant