Add codama_program! macro to override the primary program's metadata - #132
Conversation
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.
trevor-cortex
left a comment
There was a problem hiding this comment.
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!inCodamaProgramMacro::parse:syn::parse_quote!panics on parse failure rather than returning an error, but this is safe here —codama_program(#tokens)always parses as aMeta::PathListsince the arguments sit inside the delimiter group, and inner metas fall back toMeta::Verbatim. Just something to keep in mind if that wrapping trick gets reused elsewhere. cfg-gated duplicates: source parsing doesn't evaluatecfgattributes, 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 inSetProgramMetadataVisitor(around L105) only matches a bare path or asolana_program::prefix — so in that exact example the address would come frompackage.metadata.solana.program-id, notdeclare_id!. Pre-existing behavior and out of scope here, but ifsolana_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.rscompiles with three invocations in one file. This is called out in the PR description and covered by a visitor test.
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
left a comment
There was a problem hiding this comment.
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::, andcodama_macros::prefixes, mirroring thedeclare_id!check. The newit_ignores_codama_program_macros_from_other_cratestest 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 acceptssolana_addressandsolana_pubkeyprefixes, so the usage example in the PR description resolves the address fromdeclare_id!as advertised. Covered byit_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.
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!:Both
nameandaddressare optional (at least one is required); any omitted field keeps the crate-derived default (Cargo.toml package name,declare_id!/package.metadata.solana.program-idaddress). The macro expands to nothing and validates its arguments at compile time; the values are read from the crate's source bySetProgramMetadataVisitor— the same mechanism used fordeclare_id!— and applied before the crate-derived fallbacks. Usingcodama_program!more than once per crate is an error.Changes
#[proc_macro] codama_program, re-exported ascodama::codama_program!.CodamaProgramMacroparser shared by the macro (compile-time validation) and the visitor (IDL-time extraction).SetProgramMetadataVisitorreads the macro and applies name/address with precedence over the manifest defaults; errors on duplicate invocations.AttributeContext::Crate, the scope-awareProgramDirective(Optionfields +ctxparameter) and the crate-korok/visitor changes that supported it.ProgramDirectiveis back to its 0.11.0 semantics: bothnameandaddressrequired, declaring a distinct program.Tests
Codama::load.codamafeature, andgenerate-idlemits the renamed program.