From 2c2be3268f3facaa9fe7fda21ae5439bf4256cfa Mon Sep 17 00:00:00 2001 From: Anton Piliugin Date: Mon, 23 Feb 2026 22:42:20 +0500 Subject: [PATCH 1/2] fix: include num-traits in Cargo.toml when num-derive is used --- src/utils/cargoToml.ts | 57 +++++++++++++++++++++++++++- test/utils/cargoToml.test.ts | 72 ++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/src/utils/cargoToml.ts b/src/utils/cargoToml.ts index f945e44..92e67cd 100644 --- a/src/utils/cargoToml.ts +++ b/src/utils/cargoToml.ts @@ -39,6 +39,19 @@ type CargoDependencyObject = { workspace?: boolean; }; +/** + * Proc-macro crates whose generated code depends on runtime crates that + * won't appear in the rendered source text. When a key crate is detected + * as used, all its peer crates are implicitly included. + * + * Example: `#[derive(num_derive::FromPrimitive)]` expands to code that + * references `num_traits::FromPrimitive`, so `num-traits` must be present + * in Cargo.toml whenever `num-derive` is used. + */ +export const PEER_DEPENDENCIES: Record = { + 'num-derive': ['num-traits'], +}; + export const DEFAULT_DEPENDENCY_VERSIONS: CargoDependencies = { 'anchor-lang': { optional: true, version: '~0.31' }, borsh: '^0.10', @@ -231,6 +244,12 @@ export function getUsedDependencyVersions( [{} as CargoDependencies, new Set()], ); + // Expand peer dependencies: proc-macro crates like `num-derive` generate + // code that depends on runtime crates like `num-traits` at compile time. + // Since these runtime crates never appear in the rendered source text, + // they won't be detected by import scanning — we must add them explicitly. + expandPeerDependencies(usedDependencyVersion, dependencyVersionsWithDefaults, missingDependencies); + if (missingDependencies.size > 0) { throw new CodamaError(CODAMA_ERROR__RENDERERS__MISSING_DEPENDENCY_VERSIONS, { dependencies: [...missingDependencies], @@ -241,6 +260,42 @@ export function getUsedDependencyVersions( return usedDependencyVersion; } +/** + * For each detected dependency, check if it has peer dependencies that must + * also be present (e.g. `num-derive` requires `num-traits` at runtime). + * If a peer is not already in the used set, look it up in the version map + * and add it. If it can't be resolved, add it to missingDependencies so + * the caller can surface the error. + */ +function expandPeerDependencies( + usedDependencyVersion: CargoDependencies, + dependencyVersionsWithDefaults: CargoDependencies, + missingDependencies: Set, +): void { + // Snapshot keys to avoid iterating over newly added entries. + const currentKeys = Object.keys(usedDependencyVersion); + + for (const depKey of currentKeys) { + const peers = PEER_DEPENDENCIES[depKey]; + if (!peers) continue; + + for (const peerCrateName of peers) { + // Skip if already resolved. + const peerImportName = getCargoDependencyImportName(peerCrateName); + const alreadyPresent = findCargoDependencyByImportName(usedDependencyVersion, peerImportName); + if (alreadyPresent) continue; + + // Look up the peer in the version map. + const peerDep = findCargoDependencyByImportName(dependencyVersionsWithDefaults, peerImportName); + if (peerDep) { + usedDependencyVersion[peerDep[0]] = peerDep[1]; + } else { + missingDependencies.add(peerCrateName); + } + } + } +} + function getUsedImportNames(renderMap: RenderMap, dependencyMap: Record): Set { const fragments = [...renderMap.values()]; const fromImportMap = new ImportMap() @@ -366,4 +421,4 @@ function findCargoDependencyByImportName( function readCargoToml(path: string): CargoToml { return parse(readFile(path)) as CargoToml; -} +} \ No newline at end of file diff --git a/test/utils/cargoToml.test.ts b/test/utils/cargoToml.test.ts index 93a5dfe..5e3a5aa 100644 --- a/test/utils/cargoToml.test.ts +++ b/test/utils/cargoToml.test.ts @@ -8,6 +8,7 @@ import { DEFAULT_DEPENDENCY_VERSIONS, Fragment, getUsedDependencyVersions, + PEER_DEPENDENCIES, shouldUpdateRange, updateExistingCargoToml, } from '../../src/utils'; @@ -282,3 +283,74 @@ describe('shouldUpdateRange', () => { expect(shouldUpdateRange('module', '=1.1.0', '=1.0.0')).toBe(false); }); }); + +describe('peer dependencies', () => { + test('it includes num-traits when num-derive is used via derive attribute', () => { + // Simulates generated code containing `#[derive(num_derive::FromPrimitive)]`. + // The content scanner detects `num_derive` but `num_traits` never appears + // in the rendered source — it's only referenced by the proc-macro expansion. + const renderMap = createRenderMap({ + 'my_enum.rs': fragment( + '#[derive(num_derive::FromPrimitive)]\npub enum MyEnum { A, B, C }', + ), + }); + + const result = getUsedDependencyVersions(renderMap, {}, {}); + expect(result).toHaveProperty('num-derive', DEFAULT_DEPENDENCY_VERSIONS['num-derive']); + expect(result).toHaveProperty('num-traits', DEFAULT_DEPENDENCY_VERSIONS['num-traits']); + }); + + test('it includes num-traits when num-derive is used via ImportMap', () => { + // Simulates the trait renderer adding `num_derive::FromPrimitive` to + // the ImportMap (when useFullyQualifiedName is false). + const renderMap = createRenderMap({ + 'my_enum.rs': use('num_derive::FromPrimitive'), + }); + + const result = getUsedDependencyVersions(renderMap, {}, {}); + expect(result).toHaveProperty('num-derive', DEFAULT_DEPENDENCY_VERSIONS['num-derive']); + expect(result).toHaveProperty('num-traits', DEFAULT_DEPENDENCY_VERSIONS['num-traits']); + }); + + test('it does not duplicate num-traits if already explicitly used', () => { + const renderMap = createRenderMap({ + 'my_enum.rs': { + content: '#[derive(num_derive::FromPrimitive)]\npub enum MyEnum { A }', + imports: new ImportMap().add('num_traits::FromPrimitive'), + }, + }); + + const result = getUsedDependencyVersions(renderMap, {}, {}); + expect(result).toHaveProperty('num-derive'); + expect(result).toHaveProperty('num-traits'); + // Ensure no duplicate keys — Object.keys is sufficient since keys are unique. + const numTraitsEntries = Object.keys(result).filter(k => k === 'num-traits'); + expect(numTraitsEntries).toHaveLength(1); + }); + + test('it respects user-provided dependencyVersions for peer dependencies', () => { + const renderMap = createRenderMap({ + 'my_enum.rs': fragment('#[derive(num_derive::FromPrimitive)]\npub enum MyEnum { A }'), + }); + const customVersions = { 'num-traits': '^0.3' }; + + const result = getUsedDependencyVersions(renderMap, {}, customVersions); + expect(result['num-traits']).toBe('^0.3'); + }); + + test('it does not include peer dependencies when the parent is not used', () => { + const renderMap = createRenderMap({ + 'my_struct.rs': use('borsh::BorshSerialize'), + }); + + const result = getUsedDependencyVersions(renderMap, {}, {}); + expect(result).toHaveProperty('borsh'); + expect(result).not.toHaveProperty('num-derive'); + expect(result).not.toHaveProperty('num-traits'); + }); + + test('PEER_DEPENDENCIES maps num-derive to num-traits', () => { + expect(PEER_DEPENDENCIES).toHaveProperty('num-derive'); + expect(PEER_DEPENDENCIES['num-derive']).toContain('num-traits'); + }); +}); \ No newline at end of file From 676d1e5658de18a5573d240dd1b1ff808894a783 Mon Sep 17 00:00:00 2001 From: Anton Piliugin Date: Mon, 23 Feb 2026 22:44:31 +0500 Subject: [PATCH 2/2] Perr dependency changeset --- .changeset/gold-parts-stare.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/gold-parts-stare.md diff --git a/.changeset/gold-parts-stare.md b/.changeset/gold-parts-stare.md new file mode 100644 index 0000000..cf350f0 --- /dev/null +++ b/.changeset/gold-parts-stare.md @@ -0,0 +1,5 @@ +--- +'@codama/renderers-rust': patch +--- + +fix: include num-traits in Cargo.toml when num-derive is used