Skip to content

feat(core): CORRECT003 — $effect used as onMount - #76

Merged
oekazuma merged 8 commits into
mainfrom
feat/correct003-effect-onmount
Jul 2, 2026
Merged

oekazuma merged 8 commits into
mainfrom
feat/correct003-effect-onmount

Conversation

@oekazuma

@oekazuma oekazuma commented Jul 2, 2026 •

Copy link
Copy Markdown
Owner

The Correctness/reactivity slice of #69. Adds CORRECT003, flagging an $effect/$effect.pre whose non-empty body reads no reactive value — such an effect runs once after mount and never re-runs, so it is an onMount in disguise. warning severity, matching CORRECT002.

Distinct from CORRECT002 (an $effect that only assigns $state → use $derived): CORRECT003 is an $effect that neither reads nor derives reactive state → use onMount.

What's flagged

$effect(() => { el.focus(); });        // ✗ flag — member call, no reactive read
$effect(() => analytics.pageView());   // ✗ flag
$effect(() => { count; });             // ok — reads $state
$effect(() => localHelper());          // ok — bare call (may read state)
$effect(() => console.log($store));    // ok — store subscription
$effect(() => {});                     // ok — empty body

How it works (precision-first)

A new computed EffectFact.mountOnly boolean, set by the CLI parser. An effect is not flagged (conservative) when its body:

  1. references a reactive name (reactiveNames = $state ∪ $derived ∪ $props bindings, incl. destructured + rest),
  2. reads a $-prefixed non-rune identifier (store subscription), or
  3. contains a bare-identifier call foo() (a local/imported fn may read state internally).

Member calls (el.focus(), console.log('x')) are not suppressive on their own, but a reactive name in a call argument is caught by rule 1. Empty bodies are never flagged. The cardinal sin — flagging an effect that legitimately reacts — was scrutinized hardest in review; no false-positive path found.

The componentRule-built rule filters mountOnly and no-ops in rendered mode. assignsOnlyState/stateNames (CORRECT002) are untouched.

Docs & release

  • 2 rule reference pages (CORRECT003, en + ja).
  • @svelte-vitals/core + svelte-vitals + @svelte-vitals/mcp minor (CLI/static rule; not @svelte-vitals/vite).

Validation

pnpm -r test (554: core 250 / vite 76 / cli 219 / mcp 9), pnpm -r typecheck, pnpm lint, pnpm --filter docs build (111 pages incl. the 2 new) — all green.

Process

spec → plan → subagent-driven (3 impl tasks, each spec + quality reviewed) + a whole-branch review on Opus (verdict: ready to merge). Zero review-loop fixes needed.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a new warning (CORRECT003) that flags $effect / $effect.pre used as one-time, mount-like side effects, encouraging use of onMount.
  • Documentation
    • Added English and Japanese rule reference pages for CORRECT003, including examples and guidance on how to rewrite flagged code.
  • Tests
    • Expanded parsing and correctness-rule test coverage to detect and verify the new “mount-only” behavior, including cases that should not be flagged.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds CORRECT003, which flags $effect and $effect.pre bodies that read no reactive values. It introduces EffectFact.mountOnly, computes it in parsing, registers the rule, adds tests, and documents the rule.

Changes

CORRECT003 effect-as-onMount rule

Layer / File(s) Summary
EffectFact.mountOnly contract
packages/core/src/component.ts
Adds mountOnly: boolean to EffectFact.
Parser mountOnly detection
packages/cli/src/providers/source/parse.ts, packages/cli/test/parse-component-facts.test.ts
Computes mountOnly from reactive-name scans and updates parser coverage for mount-only and non-mount-only effects.
CORRECT003 rule and registration
packages/core/src/rules/correctness/correct001-002.ts, packages/core/src/rules/index.ts, packages/core/src/index.ts, packages/core/test/correctness-rules.test.ts
Adds correct003EffectAsOnMount, registers and re-exports it, and adds rule tests.
Documentation, plan, spec, and changeset
docs/src/content/docs/rules/correct003.md, docs/src/content/docs/ja/rules/correct003.md, docs/superpowers/plans/..., docs/superpowers/specs/..., .changeset/correct003-effect-onmount.md
Adds rule docs, planning/design docs, and changelog metadata for CORRECT003.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • oekazuma/svelte-vitals#68: Introduces the same parseComponentFacts/EffectFact pipeline and $effect heuristics that this PR extends with mountOnly and CORRECT003.
🚥 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 change: adding CORRECT003 for $effect used as onMount.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/correct003-effect-onmount

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.

@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: 2

🧹 Nitpick comments (1)
packages/cli/src/providers/source/parse.ts (1)

446-462: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Non-computed member/property names can be misread as reactive references.

bodyReadsReactive relies on the generic walkEstree, which recurses into every object key including MemberExpression.property and Property.key even when computed is false. A property name that happens to match a reactive binding name (or starts with $) is then wrongly treated as a reactive read, suppressing a legitimate CORRECT003 flag.

Example: let title = $state('t'); $effect(() => { el.title = 'x'; }); — title here is just a property name, not a reference to the title state, yet it forces mountOnly to false.

This is a conservative-direction bug (under-flagging only), but worth tightening since common property names (value, id, name, title, count, …) are likely to collide with reactive bindings.

♻️ Possible fix sketch
 function bodyReadsReactive(fn: Node, reactiveNames: Set<string>): boolean {
   let reads = false;
-  walkEstree(fn.body, (n: Node) => {
-    if (reads) return;
-    if (n?.type === 'Identifier') {
-      if (reactiveNames.has(n.name)) reads = true;
-      else if (n.name.startsWith('$') && !RUNE_NAMES.has(n.name)) reads = true;
-    } else if (n?.type === 'CallExpression' && n.callee?.type === 'Identifier') {
-      reads = true;
-    }
-  });
+  const visit = (n: Node): void => {
+    if (reads || !n || typeof n !== 'object' || typeof n.type !== 'string') return;
+    if (n.type === 'Identifier') {
+      if (reactiveNames.has(n.name)) reads = true;
+      else if (n.name.startsWith('$') && !RUNE_NAMES.has(n.name)) reads = true;
+    } else if (n.type === 'CallExpression' && n.callee?.type === 'Identifier') {
+      reads = true;
+    } else if (n.type === 'MemberExpression') {
+      visit(n.object);
+      if (n.computed) visit(n.property);
+      return; // skip non-computed property key
+    } else if (n.type === 'Property' && !n.computed) {
+      visit(n.value);
+      return; // skip non-computed property key
+    }
+    for (const key of Object.keys(n)) {
+      if (key === 'type' || key === 'start' || key === 'end' || key === 'loc' || key === 'range') continue;
+      visit((n as any)[key]);
+    }
+  };
+  visit(fn.body);
   return reads;
 }
🤖 Prompt for 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.

In `@packages/cli/src/providers/source/parse.ts` around lines 446 - 462, The
bodyReadsReactive helper is overcounting reactive reads because walkEstree also
visits non-computed member/property keys, so identifiers used as plain property
names can falsely match reactive bindings. Update bodyReadsReactive in parse.ts
to ignore MemberExpression.property and Property.key when computed is false (or
otherwise skip non-value positions) while still detecting real reactive
identifiers, $-prefixed stores, and bare-identifier calls. Use the existing
bodyReadsReactive and walkEstree logic as the main place to tighten this check.
🤖 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/correct003.md`:
- Line 10: The description in the `$effect`/`$effect.pre` rule is too narrow
because it says only “local function calls,” while the parser treats any bare
identifier call as a reactive read. Update the wording in the rule doc to match
the behavior from `parse.ts` by referring to “bare function calls” or
equivalent, and keep the rest of the reactive-read examples (`$state`,
`$derived`, `$props`, store subscriptions) unchanged.

In `@docs/src/content/docs/rules/correct003.md`:
- Line 10: The CORRECT003 description is too narrow because it says “local
function call,” but the parser in parse.ts treats any bare identifier call as a
valid reactive read. Update the rule text in correct003.md to describe a “bare
function call” instead of only local helpers, and keep the rest of the
effect/readability wording aligned with the static analysis behavior.

---

Nitpick comments:
In `@packages/cli/src/providers/source/parse.ts`:
- Around line 446-462: The bodyReadsReactive helper is overcounting reactive
reads because walkEstree also visits non-computed member/property keys, so
identifiers used as plain property names can falsely match reactive bindings.
Update bodyReadsReactive in parse.ts to ignore MemberExpression.property and
Property.key when computed is false (or otherwise skip non-value positions)
while still detecting real reactive identifiers, $-prefixed stores, and
bare-identifier calls. Use the existing bodyReadsReactive and walkEstree logic
as the main place to tighten this check.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02aefdbe-f2d7-4b56-8c20-859a16fe716f

📥 Commits

Reviewing files that changed from the base of the PR and between 61132a5 and 7f994aa.

📒 Files selected for processing (12)
  • .changeset/correct003-effect-onmount.md
  • docs/src/content/docs/ja/rules/correct003.md
  • docs/src/content/docs/rules/correct003.md
  • docs/superpowers/plans/2026-07-02-correct003-effect-as-onmount.md
  • docs/superpowers/specs/2026-07-02-correct003-effect-as-onmount-design.md
  • packages/cli/src/providers/source/parse.ts
  • packages/cli/test/parse-component-facts.test.ts
  • packages/core/src/component.ts
  • packages/core/src/index.ts
  • packages/core/src/rules/correctness/correct001-002.ts
  • packages/core/src/rules/index.ts
  • packages/core/test/correctness-rules.test.ts

Comment thread docs/src/content/docs/ja/rules/correct003.md Outdated
Comment thread docs/src/content/docs/rules/correct003.md Outdated
…re call (PR #76 review)

bodyReadsReactive now uses a dedicated walk that skips non-computed
MemberExpression.property and Property.key, so a property named like a reactive
binding (obj.count) no longer suppresses a legitimate flag; computed access
(obj[count]) and reactive names in call arguments still count as reads.
Docs (en/ja) reworded 'local function call' -> 'bare function call'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@oekazuma

oekazuma commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

Addressed the review in 3c58162:

  • Docs (en/ja): "local function call" → "bare function call (foo())" — matches the parser, which flags any bare-identifier call, not just local helpers.
  • bodyReadsReactive non-computed property names (nitpick): replaced the generic walkEstree with a dedicated walk that skips non-computed MemberExpression.property and Property.key. So obj.count = 5 (property named like a reactive binding) is no longer misread as a reactive read and is now correctly flagged, while computed access obj[count] and reactive names in call arguments (console.log(count)) still count as reads. Added a regression test for both.

Green: parse-component-facts 39/39, correctness-rules 10/10, typecheck + prettier clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a new correctness rule CORRECT003 to detect $effect/$effect.pre bodies that never read reactive values (i.e., they effectively run once after mount), and recommends using onMount to make intent explicit. It extends the CLI’s component-fact extraction with a new EffectFact.mountOnly classification, registers the rule in core exports, and documents/releases it.

Changes:

  • Add EffectFact.mountOnly computed by the CLI parser via a conservative “reads reactive” scan.
  • Introduce and register CORRECT003 (correct003EffectAsOnMount) and add core/cli tests covering the new behavior.
  • Add docs (en/ja), spec/plan artifacts, and a changeset bumping @svelte-vitals/core, svelte-vitals, and @svelte-vitals/mcp (minor).

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/cli/src/providers/source/parse.ts Computes mountOnly by scanning effect bodies for reactive reads / store subs / bare calls.
packages/core/src/component.ts Extends EffectFact with mountOnly.
packages/core/src/rules/correctness/correct001-002.ts Adds correct003EffectAsOnMount rule based on mountOnly.
packages/core/src/rules/index.ts Registers and re-exports CORRECT003 in allRules and exports.
packages/core/src/index.ts Re-exports CORRECT003 from the package entrypoint.
packages/cli/test/parse-component-facts.test.ts Updates existing effect expectations and adds CORRECT003 capture tests for mountOnly.
packages/core/test/correctness-rules.test.ts Updates CORRECT002 fixtures for the new field and adds CORRECT003 rule tests.
docs/src/content/docs/rules/correct003.md Adds English rule reference page for CORRECT003.
docs/src/content/docs/ja/rules/correct003.md Adds Japanese rule reference page for CORRECT003.
.changeset/correct003-effect-onmount.md Minor version changeset for core/app/mcp plus release notes for CORRECT003.
docs/superpowers/specs/2026-07-02-correct003-effect-as-onmount-design.md Design spec for the new mount-only effect detection and rule.
docs/superpowers/plans/2026-07-02-correct003-effect-as-onmount.md Implementation plan covering capture, rule, docs, and verification steps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/cli/src/providers/source/parse.ts
Props/state/derived with defaults ({ title = 'x' }), nested patterns
({ a: { b } }), arrays, and rest were dropped from reactiveNames, which could
mark an  reading such a binding as mountOnly — a false-positive CORRECT003.
addBoundNames now recurses the full binding-pattern grammar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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.

🧹 Nitpick comments (1)
packages/cli/test/parse-component-facts.test.ts (1)

202-209: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding array/rest destructuring coverage.

The commit message states the destructuring fix tracks reactive bindings "inside defaults, nested patterns, arrays, and rest," but this block only tests default values and nested object destructuring ({ a: { b } }). No test exercises array destructuring (let [a] = $props()-style, if applicable) or rest patterns (let { ...rest } = $props()). Given addBoundNames presumably now handles these per the commit description, adding assertions here would close the gap between the stated fix scope and test coverage.

🤖 Prompt for 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.

In `@packages/cli/test/parse-component-facts.test.ts` around lines 202 - 209, The
destructuring coverage test is missing cases for array and rest bindings even
though the fix is meant to handle them. Extend the existing facts() assertions
in parse-component-facts.test.ts to cover reactive names produced by
addBoundNames from array patterns and rest patterns, alongside the current
default and nested object destructuring checks, so the behavior is verified for
all mentioned binding forms.
🤖 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.

Nitpick comments:
In `@packages/cli/test/parse-component-facts.test.ts`:
- Around line 202-209: The destructuring coverage test is missing cases for
array and rest bindings even though the fix is meant to handle them. Extend the
existing facts() assertions in parse-component-facts.test.ts to cover reactive
names produced by addBoundNames from array patterns and rest patterns, alongside
the current default and nested object destructuring checks, so the behavior is
verified for all mentioned binding forms.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5520b204-8e18-4337-933f-90ef50355c0a

📥 Commits

Reviewing files that changed from the base of the PR and between 7f994aa and 6e32c2d.

📒 Files selected for processing (4)
  • docs/src/content/docs/ja/rules/correct003.md
  • docs/src/content/docs/rules/correct003.md
  • packages/cli/src/providers/source/parse.ts
  • packages/cli/test/parse-component-facts.test.ts
✅ Files skipped from review due to trivial changes (2)
  • docs/src/content/docs/ja/rules/correct003.md
  • docs/src/content/docs/rules/correct003.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/providers/source/parse.ts

@oekazuma
oekazuma merged commit a90d563 into main Jul 2, 2026
5 checks passed
@oekazuma
oekazuma deleted the feat/correct003-effect-onmount branch July 2, 2026 07:08
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