Skip to content

feat(core): add CORRECT005 — flag mutation of a non-bindable $props - #139

Merged
oekazuma merged 2 commits into
mainfrom
feat/134-correct005-prop-mutation
Jul 7, 2026
Merged

oekazuma merged 2 commits into
mainfrom
feat/134-correct005-prop-mutation

Conversation

@oekazuma

@oekazuma oekazuma commented Jul 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes #134 — the last remaining slice from the code-health roadmap (#69). New Correctness rule flagging mutation of a non-$bindable prop destructured from $props().

Svelte's docs say plainly: "don't mutate props" unless they are $bindable. Two failure modes are invisible today, neither caught by the compiler:

  • Plain-object prop mutation is a silent no-op — the object isn't a state proxy, so not even the dev-time warning fires.
  • Reactive-state-proxy prop mutation works, but only triggers the ownership_invalid_mutation dev warning if that code path is exercised at runtime — static analysis catches it at review/CI time instead.

Plain reassignment of the prop itself (count = 5) is intentionally not flagged — the docs explicitly sanction that for ephemeral state ("the child component is able to temporarily override the prop value"); only mutation is prohibited. (An earlier draft of the issue proposed flagging reassignment too — corrected after checking the official docs via the Svelte MCP server, since that would have false-positived on documented, blessed code.)

Detection

  • collectNonBindableProps (new, component-parse.ts) walks $props() destructuring to build the set of non-$bindable local names: plain and renamed destructured props, the ...rest binding (rest props can never be individually $bindable), and the non-destructured let props = $props() case (no field there can be $bindable either). Ambiguous shapes (nested destructuring, more than one $props() call) are skipped conservatively — an empty set, not a guess.
  • collectPropMutations (new) flags member writes (prop.x = …, prop.x += …, prop.x++), delete prop.x, and calls to a conservative list of mutating methods (push, splice, set, …) rooted at one of those names — over both the instance script and the template (inline handlers mutate props too).
  • $bindable(...)-declared props are excluded by construction.

Docs gap fixed along the way

Wiring this up surfaced an undocumented fourth registration spot for a new rule, beyond the three AGENTS.md already lists: packages/core/src/index.ts keeps its own duplicate export { ... } from './rules/index.js' list, and a missed entry there doesn't fail typecheck (plain re-export, not a type error) — it silently drops the rule from the public API while allRules still finds it internally. Documented in AGENTS.md so this doesn't bite the next rule.

Test plan

  • New parse-level tests (component-parse.test.ts): member write / update expression / delete / mutating method call are flagged; plain reassignment and $bindable props are not; renamed and rest-prop bindings are tracked by their local name; the non-destructured $props() case; template inline-handler mutation; no false positive on a non-prop variable or a script with no $props().
  • New rule-level tests (correctness-rules.test.ts): one finding per mutation occurrence, no-signal when there are none.
  • packages/cli/test/docs-links.test.ts (existing) verifies both the en and ja CORRECT005 doc pages exist — it would fail the build if either were missing.
  • pnpm build && pnpm typecheck && pnpm test && pnpm lint: all pass (core 364, cli 345, vite 84, mcp 13).
  • pnpm --filter docs check: 0 errors.
  • Minor changeset for @svelte-vitals/core + svelte-vitals (new rule, user-facing).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new correctness rule to flag mutations of props from $props() unless they are marked $bindable.
    • Inline suppression now covers the expanded correctness rule range.
  • Documentation

    • Updated CLI guidance and added rule docs in English and Japanese for the new rule.
  • Tests

    • Expanded coverage for malformed input handling and new mutation-detection scenarios.
  • Chores

    • Bumped package versions to a minor release.

Svelte's docs say plainly: don't mutate props unless they are $bindable.
Two failure modes are invisible today: mutating a plain-object prop is a
silent no-op (the object isn't a state proxy — not even the dev-time
warning fires), and mutating a reactive-state-proxy prop only triggers
the ownership_invalid_mutation warning if that code path is exercised at
runtime. Neither is caught by the compiler. Plain reassignment of the
prop itself (count = 5) is intentionally NOT flagged — the docs
explicitly sanction that for ephemeral state; only mutation is
prohibited.

Detection (component-parse.ts): collectNonBindableProps walks $props()
destructuring to build the set of non-$bindable local names (handles
plain/renamed/rest bindings, and the non-destructured `let props =
$props()` case, where no field can be individually $bindable either).
collectPropMutations then flags member writes, `delete`, and a
conservative list of mutating methods (push/splice/set/...) rooted at
one of those names, over both the instance script and the template
(inline handlers).

Also fixes an AGENTS.md gap found while wiring this up: adding a rule
has a fourth registration spot beyond the three already documented —
packages/core/src/index.ts keeps its own duplicate re-export list from
rules/index.js, which a missed entry doesn't fail typecheck on.

Fixes #134

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 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

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

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

Run ID: 53ae5819-3a72-49be-8bd8-7fa6367c946c

📥 Commits

Reviewing files that changed from the base of the PR and between 7f1697d and 5583376.

📒 Files selected for processing (4)
  • docs/src/content/docs/ja/rules/correct005.md
  • docs/src/content/docs/rules/correct005.md
  • packages/core/src/component-parse.ts
  • packages/core/test/component-parse.test.ts
📝 Walkthrough

Walkthrough

Adds a new CORRECT005 correctness rule detecting mutations of non-$bindable props destructured from $props(). Extends ComponentFacts and parseComponentFacts with a mutatedProps field, registers the rule, updates dependent test fixtures, and adds English/Japanese documentation plus a changeset.

Changes

CORRECT005 Rule Implementation

Layer / File(s) Summary
ComponentFacts contract and empty-facts baseline
packages/core/src/component.ts, packages/core/src/component-collect.ts
Adds mutatedProps: { name, line }[] to the ComponentFacts interface and to emptyComponentFacts.
Mutation detection in component parsing
packages/core/src/component-parse.ts, packages/core/test/component-parse.test.ts
Analyzes $props() destructuring to classify non-bindable props, detects member writes, delete, and mutating method calls in instance code and templates, and populates mutatedProps in the parse result.
Rule definition and registration
packages/core/src/rules/correctness/correct005-prop-mutation.ts, packages/core/src/rules/index.ts, packages/core/src/index.ts, AGENTS.md
Defines correct005PropMutation emitting one diagnostic per mutated prop, wires it into allRules/exports, and updates rule-registration guidance to four steps.
Test fixture updates and rule test coverage
packages/core/test/*.ts, packages/cli/test/malformed-svelte.test.ts, packages/cli/test/suppression-e2e.test.ts
Adds mutatedProps: [] to existing comp()/empty-facts fixtures across rule and CLI tests, and adds CORRECT005 findings coverage.
Rule documentation and changeset
docs/src/content/docs/rules/correct005.md, docs/src/content/docs/ja/rules/correct005.md, docs/src/content/docs/guides/cli.md, docs/src/content/docs/ja/guides/cli.md, .changeset/correct005-prop-mutation.md
Adds English/Japanese rule docs, updates the CLI inline-suppression rule ID range from CORRECT001–004 to CORRECT001–005, and adds a minor-version changeset.

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

Sequence Diagram(s)

sequenceDiagram
  participant Linter
  participant parseComponentFacts
  participant PropsAnalyzer
  participant correct005PropMutation

  Linter->>parseComponentFacts: parse(source, filename)
  parseComponentFacts->>PropsAnalyzer: derive non-bindable props from $props()
  PropsAnalyzer->>PropsAnalyzer: scan for member writes, delete, mutating calls
  PropsAnalyzer-->>parseComponentFacts: mutatedProps [{name, line}]
  parseComponentFacts-->>Linter: ComponentFacts with mutatedProps
  Linter->>correct005PropMutation: applies(ComponentFacts)
  correct005PropMutation-->>Linter: diagnostic per mutated prop
Loading

Possibly related PRs

  • oekazuma/svelte-vitals#68: Both PRs build on the same ComponentFacts/parseComponentFacts correctness rule pipeline, this PR extends it with mutatedProps and CORRECT005.
  • oekazuma/svelte-vitals#112: Both PRs modify packages/core/src/component-parse.ts and the ComponentFacts shape, layering the mutatedProps addition on prior parsing work.
  • oekazuma/svelte-vitals#116: Both PRs update the same CLI malformed .svelte test expectations for the empty-facts object shape.
🚥 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 states the new CORRECT005 rule and its focus on mutating non-bindable $props.
Linked Issues check ✅ Passed The PR implements CORRECT005 as specified, including mutation-only detection, $bindable exclusions, docs, registry wiring, and conservative handling of $props() forms.
Out of Scope Changes check ✅ Passed The changes stay within the rule implementation, parser support, docs, tests, and registration needed for CORRECT005.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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.

@oekazuma

oekazuma commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🧹 Nitpick comments (1)
packages/core/src/component-parse.ts (1)

360-375: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Generic mutating-method name list can false-positive on non-collection objects.

MUTATING_METHODS flags any call to push/set/add/delete/clear/etc. on a prop-rooted expression, regardless of the actual runtime type. A prop holding a custom object with an immutable .set(key, val) builder method (returning a new instance, common in some functional-style APIs) would be flagged even though nothing is mutated. This is an inherent tradeoff of a type-unaware static heuristic and is explicitly framed as "conservative" in the PR description, so it may be acceptable, but worth being aware it trades false negatives for occasional false positives on generically-named methods.

🤖 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/core/src/component-parse.ts` around lines 360 - 375, The
`MUTATING_METHODS` heuristic in `component-parse.ts` is too broad because it
matches method names like `set` and `add` even on non-collection objects; either
narrow the check in the prop-rooted call analysis to only known collection-like
receivers in the relevant parser logic, or explicitly document in the
`MUTATING_METHODS`/CORRECT005 comment that this is a conservative static rule
and may false-positive on builder-style APIs.
🤖 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 `@packages/core/src/component-parse.ts`:
- Around line 385-410: collectPropMutations currently flags mutations based only
on the base identifier returned by rootObjectName, so shadowed locals/parameters
can be mistaken for prop writes. Update the logic in collectPropMutations (and
any helper it relies on) to resolve bindings in scope before pushing to acc, or
add a guard that skips identifiers redeclared by nested function params, let,
const, or catch bindings. Make sure the check still handles
AssignmentExpression, UpdateExpression, UnaryExpression delete, and mutating
CallExpression cases correctly while distinguishing the real prop reference from
a shadowed name.

---

Nitpick comments:
In `@packages/core/src/component-parse.ts`:
- Around line 360-375: The `MUTATING_METHODS` heuristic in `component-parse.ts`
is too broad because it matches method names like `set` and `add` even on
non-collection objects; either narrow the check in the prop-rooted call analysis
to only known collection-like receivers in the relevant parser logic, or
explicitly document in the `MUTATING_METHODS`/CORRECT005 comment that this is a
conservative static rule and may false-positive on builder-style APIs.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 215057b7-40f8-405d-9ca1-fdcd87ba24b0

📥 Commits

Reviewing files that changed from the base of the PR and between 535904b and 7f1697d.

📒 Files selected for processing (21)
  • .changeset/correct005-prop-mutation.md
  • AGENTS.md
  • docs/src/content/docs/guides/cli.md
  • docs/src/content/docs/ja/guides/cli.md
  • docs/src/content/docs/ja/rules/correct005.md
  • docs/src/content/docs/rules/correct005.md
  • packages/cli/test/malformed-svelte.test.ts
  • packages/cli/test/suppression-e2e.test.ts
  • packages/core/src/component-collect.ts
  • packages/core/src/component-parse.ts
  • packages/core/src/component.ts
  • packages/core/src/index.ts
  • packages/core/src/rules/correctness/correct005-prop-mutation.ts
  • packages/core/src/rules/index.ts
  • packages/core/test/architecture-rules.test.ts
  • packages/core/test/bundle-rules.test.ts
  • packages/core/test/component-collect.test.ts
  • packages/core/test/component-parse.test.ts
  • packages/core/test/component-rule.test.ts
  • packages/core/test/correctness-rules.test.ts
  • packages/core/test/security-rules.test.ts

Comment thread packages/core/src/component-parse.ts
…(review)

Matching mutations by base identifier name alone (rootObjectName, no
scope resolution) means a nested function parameter or {#each ... as x}
loop variable reusing a prop's name would be misattributed as a prop
mutation — a real false-positive risk CodeRabbit flagged (Major).

collectPropMutations now tracks the two realistic shadow sources in a
Svelte component (function parameters, each-block context bindings)
while descending the AST, and skips a match whose resolved root name is
shadowed at that point. Full lexical scope resolution (block-scoped
let/const redeclaration, {#snippet}/{:then}/{:catch} bindings) is out of
scope here — this mirrors the identifier-only matching CORRECT004's
collectStateWrites already ships with, just tightened for the two most
likely collisions given the asymmetric cost (a false positive here,
unlike CORRECT004's false negative, contradicts the project's
stated precision principle). Documented as a deliberate partial
mitigation in the rule docs (en/ja) and in code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

feat(correctness): CORRECT005 — flag mutation of non-bindable $props

1 participant