Skip to content

[2.x] Adapt update visitors to Codama v2 - #1178

Merged
lorisleiva merged 1 commit into
mainfrom
09-25-adapt_update_visitors_to_codama_v2
Sep 30, 2026
Merged

lorisleiva merged 1 commit into
mainfrom
09-25-adapt_update_visitors_to_codama_v2

Conversation

@lorisleiva

@lorisleiva lorisleiva commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

This PR adapts the update* visitors of @codama/visitors to the Codama v2 node model, along with fillDefaultPdaSeedValuesVisitor, which they depend on. Renames now repoint every reference to the renamed nodes, and update maps fail loudly on unknown keys or targets.

Update visitors

  • Exact identifiers. Keys and identifier updates are matched and applied as is, without camelCasing.
  • Unknown keys throw a new CODAMA_ERROR__VISITORS__UNRECOGNIZED_UPDATE_KEYS error when the visitor is created, so keys such as v1's name or arguments no longer silently do nothing.
  • updateInstructionsVisitor
    • data replaces arguments and updates the fields of the instruction's inline data, keyed by path (e.g. amount or config.fee, including tuple indices). Default values must be ValueNodes: contextual defaults are expressed with an injectedValueNode and a matching provides entry.
    • provides is a record merged by identifier with the instruction's provided nodes; null removes an entry.
    • defaultValue: null removes a default value.
    • Updating an account or data field that does not exist throws INSTRUCTION_ACCOUNT_NOT_FOUND or INSTRUCTION_DATA_FIELD_NOT_FOUND. Fields behind a definedTypeLinkNode cannot be updated since the type may be shared.
  • updateAccountsVisitor: renaming a missing data field throws ACCOUNT_FIELD_NOT_FOUND. Updating the seeds of an existing PDA keeps its docs and program ID, and a pda link to another program creates the PDA in that program.
  • updateDefinedTypesVisitor: renaming a missing field or variant throws a new DEFINED_TYPE_MEMBER_NOT_FOUND error. Renamed variants keep their discriminator and data, and renamed structs keep their transforms.
  • Program-scoped selectors now work for updateErrorsVisitor and updateProgramsVisitor.
  • Several matching entries. When several entries match the same node (e.g. transfer and myProgram.transfer), their updates are merged and applied at once against the original node, so they all refer to its original accounts, fields and members. A delete entry wins over any other update.
  • Supplied nodes use new identifiers. Default values and provided nodes given in an update refer to the updated instruction, so missing PDA seeds are filled against its new accounts and data fields. Renaming a data field and one of its nested fields in the same update works in any order.

Rename propagation

References are resolved through the LinkableDictionary to the node they point to, rather than matched by identifier, so links from other programs are handled correctly and links in program or transforms attributes are preserved.

Rename References repointed
Program, account, PDA, defined type, instruction Their link nodes (an account rename also renames the PDA of the same program sharing its identifier)
Instruction account accountValueNode, accountBumpValueNode, accountDataValueNode.account, instructionAccountLinkNode, ${accounts.…} placeholders
Data field (instruction, account or defined type struct) dataValueNode, accountDataValueNode.path, fieldDiscriminatorNode and ${data.…} placeholders going through it, following defined type links
Enum variant enumValueNode.variant of that enum

Other changes

  • fillDefaultPdaSeedValuesVisitor fills seeds matching top-level data fields with dataValueNodes, following linked data, and keeps the programId and plugins of PDA values.
  • @codama/visitors-core exports a new getInstructionDataFields helper listing an instruction's addressable data fields with their paths, now shared with getResolvedInstructionInputsVisitor. It follows same-named defined types from different programs.
  • LinkableDictionary gains a getRecordedPathsOfKind(kind) method listing the paths of every recorded linkable node of a given kind.
  • New error codes: UNRECOGNIZED_UPDATE_KEYS (1200015), INSTRUCTION_DATA_FIELD_NOT_FOUND (1200016), INSTRUCTION_ACCOUNT_NOT_FOUND (1200017) and DEFINED_TYPE_MEMBER_NOT_FOUND (1200018).
  • The READMEs of @codama/visitors and @codama/cli are updated accordingly.

Tests

The existing tests are ported to v2 nodes, new test files cover updateProgramsVisitor, updateErrorsVisitor, updateDefinedTypesVisitor, the internal update helpers and getInstructionDataFields, and every rename propagation case above is covered.

@changeset-bot

changeset-bot Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 8d4e2b1

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@lorisleiva

Copy link
Copy Markdown
Member Author

@trevor-cortex

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

Summary

Ports the update*Visitors and fillDefaultPdaSeedValuesVisitor to the v2 node model. The core of the change is updateHelpers.ts: a shared getUpdateVisitor that wraps each visitor's transformers with recordLinkablesOnFirstVisitVisitor and a set of reference-repointing transformers driven by a RenamePlan. References are resolved through the LinkableDictionary against the original tree (the NodeStack in bottomUpTransformerVisitor holds pre-transform nodes, so stack.getPath(...) + linkables.getPath(...) always land on original nodes), which is what makes cross-program links and program-scoped selectors work correctly. createPathResolver rewrites config.fee-style paths segment by segment while walking the original types and following defined type links, resetting to top-level-only renames once it crosses into a defined type — consistent with updateDefinedTypesVisitor only renaming top-level members.

The design holds together well and the test coverage is genuinely thorough (cross-program link resolution, tuple indices, self-referencing types, text-node intents, enum values scoped to the matching enum, PDA upserts in other programs). Error codes are appended to the registry without touching existing ones. I checked the node factories (accountLinkNode(newIdentifier, { ...node }), providedNode(identifier, value, { ...merged[index] }), etc.) — they take the identifier positionally and only read known option keys, so the spread-the-old-node pattern is safe.

Things to watch out for

Two correctness edge cases, both in updateInstructionsVisitor and both stemming from the same root: the instruction-level transform runs after the rename transformers have already processed the instruction's children, so anything the instruction transform introduces (or looks up by the old name) is not reconciled with the renames in the same update.

  1. Parent + nested data field renamed in one update (applyInstructionUpdates, data reduce). Updates are applied sequentially to the progressively-transformed type using the user's old paths, so { args: { identifier: 'params' }, 'args.amount': { identifier: 'lamports' } } silently skips the second update (no args field anymore), while createPathResolver — which builds prefixes from original identifiers — does rewrite references to params.lamports. Result: field is params.amount, references say params.lamports. Details inline.

  2. Auto-filled PDA seeds use pre-rename identifiers (applyAccountUpdates). fillDefaultPdaSeedValuesVisitor is fed the original instruction path, so a filled accountValueNode('owner') dangles if owner is renamed in the same update. Details inline.

Related, lower priority: when several entries match the same instruction (e.g. transfer and myProgram.transfer, or a [instructionNode] wildcard), the second entry's accounts[oldName] lookup runs against the already-renamed node and silently no-ops, even though assertUpdatedInputsExist passed on the original. Probably rare enough to leave, but worth knowing about.

Also worth stating in the README/docblock: user-supplied defaultValues in the same update are not repointed, so they must use the new identifiers (e.g. accounts: { owner: { identifier: 'authority' }, vault: { defaultValue: pdaValueNode('vault', { seeds: [pdaSeedValueNode('owner', accountValueNode('authority'))] }) } }). Reasonable behaviour, but it's a trap without a note.

Notes for subsequent reviewers

  • Changeset: none in the PR. CONTRIBUTING.md asks for one per user-facing change, but this is a [2.x] PR and the release notes say the seeded major changeset covers all public packages during the cut. Worth confirming that's the convention for this branch series and not an omission — the arguments → data and name → identifier key changes are the kind of thing users will grep changelogs for.
  • updateAccountsVisitor's pdas rename rule renames any PDA in the same program sharing the account's identifier, even if the account's pda link points elsewhere. Matches the description and v1 semantics, just flagging it's identifier-based rather than link-based (unlike everything else in this PR).
  • getResolvedInstructionInputsVisitor now calls stack.getPath('instructionNode') instead of passing node — equivalent given recordNodeStackVisitor has pushed the instruction, but if anyone reshuffles that visitor's pipe order it'll break subtly.
  • updateErrorsVisitor intentionally skips getUpdateVisitor since errors have no link nodes; nothing to repoint.

Comment thread packages/visitors/src/updateInstructionsVisitor.ts Outdated
Comment thread packages/visitors/src/updateInstructionsVisitor.ts Outdated
Comment thread packages/visitors-core/src/getInstructionDataFields.ts Outdated
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_update_visitors_to_codama_v2 branch from 3bb9769 to 67734b2 Compare September 25, 2026 10:14

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

Re-review

All three points from my first pass are fixed, each with a dedicated test, and I re-verified them against the current head rather than the description:

  • Parent + nested field renamed together — applyDataUpdates now does a single walk and recurses into field.type with the original path before the field itself is renamed, so { args: …, 'args.amount': … } works in either order. unusedPaths feeds INSTRUCTION_DATA_FIELD_NOT_FOUND, which also closes the silent-miss gap I flagged. Covered in updateHelpers.test.ts and updateInstructionsVisitor.test.ts.
  • Auto-filled PDA seeds using pre-rename identifiers — fillNewDefaultValues runs against [...path.slice(0, -1), updated], and defaults are keyed by identifier ?? accountName so they land on the renamed account. Both directions are tested (stale owner is not filled; authority is). The LinkableDictionary lookups inside still resolve through the original program node in the path, so links keep working.
  • Several entries matching the same node — createUpdateResolver merges in declaration order against the original node, delete wins, results cached per node. Tested for instructions and defined types.
  • Bonus: getInstructionDataFields cycle guard is now by node reference, with a same-name-across-programs test.

The updateAccountsVisitor restructure is also cleaner than before: PDA upserts are computed from linkables.getRecordedPathsOfKind('accountNode') per program, so cross-program upserts need no shared mutable state, and the pdas rename rule + programTransformer both use post-rename identifiers consistently (bottom-up order guarantees pdaTransformer has already run when the program is transformed).

Minor notes (non-blocking)

Two small edge cases inline — one on createUpdateResolver's cache identity, one on type + nested-path interplay in applyDataUpdates — plus a readability nit on getRecordedPathsOfKind's default branch. None of these warrant holding the PR.

For subsequent reviewers

  • Changeset: still none in the PR. I'm assuming the [2.x] series relies on a seeded major changeset as per the release notes; worth a one-line confirmation since arguments → data and name → identifier are exactly the kind of change people grep changelogs for.
  • getRecordedPathsOfKind is new public API on @codama/visitors-core (README updated). It only lists what was recorded — the docblock says so, but callers that construct a LinkableDictionary manually should be aware.
  • getResolvedInstructionInputsVisitor now relies on stack.getPath('instructionNode') rather than the passed node — equivalent given the recordNodeStackVisitor wrapping, and the inline comment now calls this out.

Comment thread packages/visitors/src/updateHelpers.ts Outdated
Comment thread packages/visitors/src/updateHelpers.ts
Comment thread packages/visitors-core/src/LinkableDictionary.ts Outdated
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_update_visitors_to_codama_v2 branch from 67734b2 to 46aec0a Compare September 25, 2026 10:31

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

Re-review (3rd pass)

Small delta since my approval, addressing the three non-blocking nits from last round. Re-fetched all changed files and verified each against the head:

  • createUpdateResolver cache identity — now keyed on the path (NodePath) rather than the node reference, so a frozen node instance shared between two programs resolves independently per location. Test added in updateHelpers.test.ts for the shared-instance case.
  • type + nested path interplay in applyDataUpdates — replacing a field's type while also targeting a path underneath it now throws INSTRUCTION_DATA_FIELD_NOT_FOUND for the nested path instead of silently discarding it. Tested in both updateHelpers.test.ts and updateInstructionsVisitor.test.ts.
  • getRecordedPathsOfKind default branch — the switch now spells out every LINKABLE_NODES kind explicitly, with an exhaustiveness guard so a future addition to LINKABLE_NODES fails to compile rather than falling through. The +45/-1 in LinkableDictionary.ts is this plus the docblock.

Nothing else moved in the non-test source. Everything I raised across three passes is now resolved and covered by tests; approval stands.

For subsequent reviewers

  • Changeset: still absent — flagging one last time so it's a conscious decision on the [2.x] series rather than an oversight. If the seeded major changeset covers it, all good.
  • No further concerns from me. The updateHelpers.ts module is the one to read closely if you're coming in fresh: getUpdateVisitor (orchestration), createUpdateResolver (merging multiple matching entries), createPathResolver (dotted-path rewriting through defined type links), and applyDataUpdates (the recursive walk) are the four load-bearing pieces.

@trevor-cortex

Copy link
Copy Markdown

One correction to my review above: I described the createUpdateResolver fix as "now keyed on the path" — that's not accurate. Looking at the code again, the per-node cache was removed entirely; resolve now just filters the matchers against the given path on every call. Same outcome (the shared-instance test passes because each path is evaluated independently), just a simpler mechanism than I described. Everything else in the review stands.

@lorisleiva
lorisleiva removed this pull request from stack #1182 September 30, 2026 13:53
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_update_visitors_to_codama_v2 branch from 46aec0a to 9bb6ed0 Compare September 30, 2026 13:54
@lorisleiva
lorisleiva force-pushed the 09-24-adapt_structure_visitors_to_codama_v2 branch from 68455c3 to 599fa34 Compare September 30, 2026 13:54
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_update_visitors_to_codama_v2 branch from 9bb6ed0 to f74457a Compare September 30, 2026 14:07
@lorisleiva
lorisleiva force-pushed the 09-24-adapt_structure_visitors_to_codama_v2 branch 2 times, most recently from 82d679f to e48e185 Compare September 30, 2026 14:08
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_update_visitors_to_codama_v2 branch from f74457a to 0e134a7 Compare September 30, 2026 14:08
@lorisleiva
lorisleiva force-pushed the 09-24-adapt_structure_visitors_to_codama_v2 branch from e48e185 to 66f05b8 Compare September 30, 2026 14:09
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_update_visitors_to_codama_v2 branch from 0e134a7 to 0159613 Compare September 30, 2026 14:09
@lorisleiva
lorisleiva force-pushed the 09-24-adapt_structure_visitors_to_codama_v2 branch from 66f05b8 to c4f8eac Compare September 30, 2026 14:11
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_update_visitors_to_codama_v2 branch 2 times, most recently from 0d6f7f5 to f0cba6f Compare September 30, 2026 14:12
@lorisleiva
lorisleiva force-pushed the 09-24-adapt_structure_visitors_to_codama_v2 branch 2 times, most recently from ae160c2 to be4576d Compare September 30, 2026 14:13
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_update_visitors_to_codama_v2 branch 2 times, most recently from f0cba6f to 4c2d13f Compare September 30, 2026 14:13
@lorisleiva
lorisleiva force-pushed the 09-24-adapt_structure_visitors_to_codama_v2 branch from ae160c2 to be4576d Compare September 30, 2026 14:13
Base automatically changed from 09-24-adapt_structure_visitors_to_codama_v2 to main September 30, 2026 14:14
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_update_visitors_to_codama_v2 branch from 4c2d13f to 8d4e2b1 Compare September 30, 2026 14:14
@lorisleiva
lorisleiva marked this pull request as ready for review September 30, 2026 14:15
@lorisleiva
lorisleiva merged commit 3246a22 into main Sep 30, 2026
2 of 4 checks passed
@lorisleiva
lorisleiva deleted the 09-25-adapt_update_visitors_to_codama_v2 branch September 30, 2026 14: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