Skip to content

Add codama_program! macro to override the primary program's metadata - #132

Merged
lorisleiva merged 2 commits into
mainfrom
feat/codama-program-macro
Aug 18, 2026
Merged

Add codama_program! macro to override the primary program's metadata#132
lorisleiva merged 2 commits into
mainfrom
feat/codama-program-macro

Conversation

@lorisleiva

Copy link
Copy Markdown
Member

This PR adds a codama_program! function-like macro that overrides the primary program's metadata when generating an IDL, and reverts the crate-level #![codama(program(...))] directive support shipped in 0.12.0, which was unusable in practice: Rust does not allow attribute proc-macros in inner-attribute position, so no compiled crate could ever carry it.

Usage

Written as a free-standing declaration, conventionally next to declare_id!:

solana_address::declare_id!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");

#[cfg(feature = "codama")]
codama_program!(name = "associatedTokenAccount");

Both name and address are optional (at least one is required); any omitted field keeps the crate-derived default (Cargo.toml package name, declare_id! / package.metadata.solana.program-id address). The macro expands to nothing and validates its arguments at compile time; the values are read from the crate's source by SetProgramMetadataVisitor — the same mechanism used for declare_id! — and applied before the crate-derived fallbacks. Using codama_program! more than once per crate is an error.

Changes

  • codama-macros: new #[proc_macro] codama_program, re-exported as codama::codama_program!.
  • codama-attributes: new CodamaProgramMacro parser shared by the macro (compile-time validation) and the visitor (IDL-time extraction).
  • codama-korok-visitors: SetProgramMetadataVisitor reads the macro and applies name/address with precedence over the manifest defaults; errors on duplicate invocations.
  • Reverted from 0.12.0: AttributeContext::Crate, the scope-aware ProgramDirective (Option fields + ctx parameter) and the crate-korok/visitor changes that supported it. ProgramDirective is back to its 0.11.0 semantics: both name and address required, declaring a distinct program.

Tests

  • trybuild compile tests: name-only / address-only / both compile; empty and unrecognised-attribute invocations fail with snapshotted errors.
  • Unit tests for the parser; visitor tests for name/address overrides, manifest precedence and the duplicate-invocation error; an end-to-end fixture crate asserting the renamed IDL through Codama::load.
  • Manually verified against a crate mirroring the associated-token-account interface: compiles with and without the codama feature, and generate-idl emits the renamed program.

Note: the duplicate-invocation rule is enforced by the visitor (which sees the whole crate), not the proc macro (which expands each invocation independently), so it is covered by a visitor test rather than a trybuild case.

Add a codama_program! function-like macro that overrides the primary
program's metadata when generating an IDL, and revert the crate-level
#![codama(program(...))] directive support shipped in 0.12.0, which was
unusable in practice: Rust does not allow attribute proc-macros in
inner-attribute position, so no compiled crate could ever carry it.

Written as a free-standing declaration next to declare_id!:

    codama_program!(name = "associatedTokenAccount");

Both name and address are optional (at least one is required); any omitted
field keeps the crate-derived default. The macro expands to nothing and
validates its arguments at compile time; the values are read from the
crate's source by SetProgramMetadataVisitor (the same mechanism used for
declare_id!) and applied before the crate-derived fallbacks. Using
codama_program! more than once per crate is an error.

Reverts from 0.12.0: AttributeContext::Crate, the scope-aware
ProgramDirective (Option fields + ctx parameter) and the crate-korok /
visitor changes that supported it. ProgramDirective is back to its 0.11.0
semantics: both name and address required, declaring a distinct program.
@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

This PR replaces the crate-level #![codama(program(...))] directive (unshippable, since Rust forbids attribute proc-macros in inner-attribute position) with a function-like codama_program! macro that overrides the primary program's name and/or address. The macro expands to nothing, validates its args at compile time via the shared CodamaProgramMacro parser, and is read at IDL-time by SetProgramMetadataVisitor using the same unsupported-item mechanism as declare_id!. ProgramDirective is cleanly reverted to its 0.11.0 semantics (both fields required, distinct program).

The design is sound: sharing one parser between the proc macro and the visitor guarantees compile-time and IDL-time validation can't drift, and the precedence (macro → manifest → declare_id!) is clearly documented and well tested. The revert looks complete — AttributeContext::Crate, the Option fields, and the ctx parameter threading are all gone, and CrateKorok::parse is back to the plain From conversion. Test coverage is thorough: parser unit tests, trybuild pass/fail cases with snapshotted errors, visitor precedence tests, the duplicate-invocation error, and an end-to-end fixture through Codama::load.

Things to watch

  • Wrapping via parse_quote! in CodamaProgramMacro::parse: syn::parse_quote! panics on parse failure rather than returning an error, but this is safe here — codama_program(#tokens) always parses as a Meta::PathList since the arguments sit inside the delimiter group, and inner metas fall back to Meta::Verbatim. Just something to keep in mind if that wrapping trick gets reused elsewhere.
  • cfg-gated duplicates: source parsing doesn't evaluate cfg attributes, so two mutually exclusive invocations (e.g. one behind #[cfg(feature = "a")] and one behind #[cfg(not(feature = "a"))]) would still trip the once-per-crate error at IDL time. Edge-casey and arguably the right behavior, but worth knowing since the recommended usage pattern involves #[cfg(feature = "codama")].

Notes for subsequent reviewers

  • The PR description's usage example uses solana_address::declare_id!, but the (unchanged) declare_id! detection in SetProgramMetadataVisitor (around L105) only matches a bare path or a solana_program:: prefix — so in that exact example the address would come from package.metadata.solana.program-id, not declare_id!. Pre-existing behavior and out of scope here, but if solana_address::declare_id! is the pattern being promoted, recognizing that prefix may be worth a follow-up.
  • The duplicate-invocation rule is intentionally visitor-enforced (the proc macro expands each invocation independently), which is why _pass.rs compiles with three invocations in one file. This is called out in the PR description and covered by a visitor test.

Comment thread codama-korok-visitors/src/set_program_metadata_visitor.rs Outdated
Address review feedback on #132:

- Recognise `solana_address::` and `solana_pubkey::` prefixes for
  `declare_id!`, matching the crates used by the associated-token-account
  and stake interfaces.
- Restrict `codama_program!` detection to bare / `codama::` / `codama_macros::`
  prefixes, mirroring the `declare_id!` check, so a foreign
  `other_crate::codama_program!` is ignored rather than parsed (which could
  otherwise fail IDL generation).

@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

Both points from my previous review have been addressed — approving again.

  • Foreign-macro filtering: codama_program! detection is now restricted to bare, codama::, and codama_macros:: prefixes, mirroring the declare_id! check. The new it_ignores_codama_program_macros_from_other_crates test covers exactly the case I flagged (a foreign invocation whose arguments wouldn't parse as ours), and the duplicate-invocation check now correctly only counts recognized invocations.
  • solana_address::declare_id!: the detection now also accepts solana_address and solana_pubkey prefixes, so the usage example in the PR description resolves the address from declare_id! as advertised. Covered by it_gets_program_ids_from_the_solana_address_declare_id_macro.

Everything else is unchanged from my prior pass, and my earlier notes stand (the parse_quote! wrapping is safe here but panic-prone if reused elsewhere; cfg-gated duplicate invocations still trip the once-per-crate error at IDL time, which is arguably correct). No new concerns.

@lorisleiva
lorisleiva merged commit 0113c96 into main Aug 18, 2026
3 checks passed
@lorisleiva
lorisleiva deleted the feat/codama-program-macro branch August 27, 2026 15:41
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