Skip to main content

provenant/assembly/
assemblers.rs

1use crate::models::{DatasourceId, FileInfo, Package, TopLevelDependency};
2use strum::EnumIter;
3
4use super::{
5    AssemblerConfig, AssemblyMode, DirectoryMergeOutput, cargo_resource_assign,
6    cargo_workspace_merge, composer_resource_assign, conda_rootfs_merge, file_ref_resolve,
7    hackage_merge, npm_resource_assign, npm_workspace_merge, nuget_cpm_resolve,
8    ruby_resource_assign, swift_merge,
9};
10
11#[derive(Clone, Copy)]
12pub(super) enum SpecialDirectoryMergerKind {
13    Skip,
14    Hackage,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, EnumIter)]
18pub(super) enum PostAssemblyPassKind {
19    SwiftMerge,
20    CondaRootfsMerge,
21    NpmResourceAssign,
22    FileReferenceResolve,
23    RpmYumdbMerge,
24    NpmWorkspaceMerge,
25    CargoWorkspaceMerge,
26    NugetCpmResolve,
27    CargoResourceAssign,
28    ComposerResourceAssign,
29    RubyResourceAssign,
30}
31
32pub(super) fn special_directory_merger_for(
33    config_key: DatasourceId,
34) -> Option<SpecialDirectoryMergerKind> {
35    match config_key {
36        DatasourceId::HackageCabal => Some(SpecialDirectoryMergerKind::Hackage),
37        DatasourceId::SwiftPackageManifestJson => Some(SpecialDirectoryMergerKind::Skip),
38        _ => None,
39    }
40}
41
42pub(super) static POST_ASSEMBLY_PASSES: &[PostAssemblyPassKind] = &[
43    PostAssemblyPassKind::SwiftMerge,
44    PostAssemblyPassKind::CondaRootfsMerge,
45    PostAssemblyPassKind::NpmResourceAssign,
46    PostAssemblyPassKind::FileReferenceResolve,
47    PostAssemblyPassKind::RpmYumdbMerge,
48    PostAssemblyPassKind::NpmWorkspaceMerge,
49    PostAssemblyPassKind::CargoWorkspaceMerge,
50    PostAssemblyPassKind::NugetCpmResolve,
51    PostAssemblyPassKind::CargoResourceAssign,
52    PostAssemblyPassKind::ComposerResourceAssign,
53    PostAssemblyPassKind::RubyResourceAssign,
54];
55
56pub(super) fn run_post_assembly_passes(
57    files: &mut [FileInfo],
58    packages: &mut Vec<Package>,
59    dependencies: &mut Vec<TopLevelDependency>,
60) {
61    for pass in POST_ASSEMBLY_PASSES {
62        pass.run(files, packages, dependencies);
63    }
64}
65
66impl SpecialDirectoryMergerKind {
67    pub(super) fn run(
68        self,
69        files: &[FileInfo],
70        file_indices: &[usize],
71    ) -> Vec<DirectoryMergeOutput> {
72        match self {
73            Self::Skip => Vec::new(),
74            Self::Hackage => hackage_merge::assemble_hackage_packages(files, file_indices),
75        }
76    }
77}
78
79impl PostAssemblyPassKind {
80    fn run(
81        self,
82        files: &mut [FileInfo],
83        packages: &mut Vec<Package>,
84        dependencies: &mut Vec<TopLevelDependency>,
85    ) {
86        match self {
87            Self::SwiftMerge => swift_merge::assemble_swift_packages(files, packages, dependencies),
88            Self::CondaRootfsMerge => {
89                conda_rootfs_merge::merge_conda_rootfs_metadata(files, packages, dependencies)
90            }
91            Self::NpmResourceAssign => {
92                npm_resource_assign::assign_npm_package_resources(files, packages)
93            }
94            Self::FileReferenceResolve => {
95                file_ref_resolve::resolve_file_references(files, packages, dependencies)
96            }
97            Self::RpmYumdbMerge => file_ref_resolve::merge_rpm_yumdb_metadata(files, packages),
98            Self::NpmWorkspaceMerge => {
99                npm_workspace_merge::assemble_npm_workspaces(files, packages, dependencies)
100            }
101            Self::CargoWorkspaceMerge => {
102                cargo_workspace_merge::assemble_cargo_workspaces(files, packages, dependencies)
103            }
104            Self::NugetCpmResolve => {
105                nuget_cpm_resolve::resolve_nuget_cpm_versions(files, dependencies)
106            }
107            Self::CargoResourceAssign => {
108                cargo_resource_assign::assign_cargo_package_resources(files, packages)
109            }
110            Self::ComposerResourceAssign => {
111                composer_resource_assign::assign_composer_package_resources(files, packages)
112            }
113            Self::RubyResourceAssign => {
114                ruby_resource_assign::assign_ruby_package_resources(files, packages)
115            }
116        }
117    }
118}
119
120pub static ASSEMBLERS: &[AssemblerConfig] = &[
121    // ── Sibling-merge assemblers ──
122    //
123    // npm ecosystem: package.json + lockfiles in same directory.
124    // NOTE: npm-shrinkwrap.json emits "npm_package_lock_json" as its datasource_id,
125    // so "npm_shrinkwrap_json" is NOT a real datasource_id.
126    AssemblerConfig {
127        datasource_ids: &[
128            DatasourceId::BunLock,
129            DatasourceId::BunLockb,
130            DatasourceId::NpmPackageJson,
131            DatasourceId::NpmPackageLockJson,
132            DatasourceId::YarnLock,
133            DatasourceId::PnpmLockYaml,
134            DatasourceId::PnpmWorkspaceYaml,
135        ],
136        sibling_file_patterns: &[
137            "package.json",
138            "bun.lock",
139            "bun.lockb",
140            "package-lock.json",
141            "npm-shrinkwrap.json",
142            "yarn.lock",
143            "pnpm-lock.yaml",
144            "pnpm-workspace.yaml",
145        ],
146        mode: AssemblyMode::SiblingMerge,
147    },
148    // Rust/Cargo ecosystem
149    AssemblerConfig {
150        datasource_ids: &[DatasourceId::CargoToml, DatasourceId::CargoLock],
151        sibling_file_patterns: &["Cargo.toml", "Cargo.lock"],
152        mode: AssemblyMode::SiblingMerge,
153    },
154    // CocoaPods ecosystem
155    AssemblerConfig {
156        datasource_ids: &[
157            DatasourceId::CocoapodsPodspec,
158            DatasourceId::CocoapodsPodspecJson,
159            DatasourceId::CocoapodsPodfile,
160            DatasourceId::CocoapodsPodfileLock,
161        ],
162        sibling_file_patterns: &["*.podspec", "*.podspec.json", "Podfile", "Podfile.lock"],
163        mode: AssemblyMode::SiblingMerge,
164    },
165    // PHP Composer ecosystem
166    AssemblerConfig {
167        datasource_ids: &[DatasourceId::PhpComposerJson, DatasourceId::PhpComposerLock],
168        sibling_file_patterns: &[
169            "*composer.json",
170            "composer.*.json",
171            "*composer.lock",
172            "composer.*.lock",
173        ],
174        mode: AssemblyMode::SiblingMerge,
175    },
176    // Go ecosystem (includes legacy Godeps)
177    AssemblerConfig {
178        datasource_ids: &[
179            DatasourceId::GoMod,
180            DatasourceId::GoModGraph,
181            DatasourceId::GoSum,
182            DatasourceId::GoWork,
183            DatasourceId::Godeps,
184        ],
185        sibling_file_patterns: &[
186            "go.mod",
187            "go.work",
188            "go.mod.graph",
189            "go.modgraph",
190            "go.sum",
191            "Godeps.json",
192        ],
193        mode: AssemblyMode::SiblingMerge,
194    },
195    // Dart/Flutter ecosystem
196    AssemblerConfig {
197        datasource_ids: &[DatasourceId::PubspecYaml, DatasourceId::PubspecLock],
198        sibling_file_patterns: &["pubspec.yaml", "pubspec.lock"],
199        mode: AssemblyMode::SiblingMerge,
200    },
201    // Pixi ecosystem
202    AssemblerConfig {
203        datasource_ids: &[DatasourceId::PixiToml, DatasourceId::PixiLock],
204        sibling_file_patterns: &["pixi.toml", "pixi.lock"],
205        mode: AssemblyMode::SiblingMerge,
206    },
207    AssemblerConfig {
208        datasource_ids: &[DatasourceId::NixFlakeNix, DatasourceId::NixFlakeLock],
209        sibling_file_patterns: &["flake.nix", "flake.lock"],
210        mode: AssemblyMode::SiblingMerge,
211    },
212    AssemblerConfig {
213        datasource_ids: &[DatasourceId::NixDefaultNix],
214        sibling_file_patterns: &["default.nix"],
215        mode: AssemblyMode::OnePerPackageData,
216    },
217    // Helm chart ecosystem
218    AssemblerConfig {
219        datasource_ids: &[DatasourceId::HelmChartYaml, DatasourceId::HelmChartLock],
220        sibling_file_patterns: &["Chart.yaml", "Chart.lock"],
221        mode: AssemblyMode::SiblingMerge,
222    },
223    AssemblerConfig {
224        datasource_ids: &[
225            DatasourceId::HackageCabal,
226            DatasourceId::HackageCabalProject,
227            DatasourceId::HackageStackYaml,
228        ],
229        sibling_file_patterns: &["*.cabal", "cabal.project", "stack.yaml"],
230        mode: AssemblyMode::SiblingMerge,
231    },
232    // Chef ecosystem
233    AssemblerConfig {
234        datasource_ids: &[
235            DatasourceId::ChefCookbookMetadataJson,
236            DatasourceId::ChefCookbookMetadataRb,
237        ],
238        sibling_file_patterns: &["metadata.json", "metadata.rb"],
239        mode: AssemblyMode::SiblingMerge,
240    },
241    // Conan (C/C++) ecosystem
242    AssemblerConfig {
243        datasource_ids: &[
244            DatasourceId::ConanConanFilePy,
245            DatasourceId::ConanConanFileTxt,
246            DatasourceId::ConanLock,
247            DatasourceId::ConanConanDataYml,
248        ],
249        sibling_file_patterns: &[
250            "conanfile.py",
251            "conanfile.txt",
252            "conan.lock",
253            "conandata.yml",
254        ],
255        mode: AssemblyMode::SiblingMerge,
256    },
257    // Maven/Java ecosystem (nested merge via META-INF)
258    AssemblerConfig {
259        datasource_ids: &[
260            DatasourceId::MavenPom,
261            DatasourceId::MavenPomProperties,
262            DatasourceId::JavaJarManifest,
263            DatasourceId::JavaOsgiManifest,
264        ],
265        sibling_file_patterns: &["pom.xml", "pom.properties", "**/META-INF/MANIFEST.MF"],
266        mode: AssemblyMode::SiblingMerge,
267    },
268    AssemblerConfig {
269        datasource_ids: &[DatasourceId::PypiWheel, DatasourceId::PypiPipOriginJson],
270        sibling_file_patterns: &["*.whl", "origin.json"],
271        mode: AssemblyMode::SiblingMerge,
272    },
273    // Python/PyPI ecosystem
274    AssemblerConfig {
275        datasource_ids: &[
276            DatasourceId::PypiPyprojectToml,
277            DatasourceId::PypiSetupPy,
278            DatasourceId::PypiSetupCfg,
279            DatasourceId::PypiWheel,
280            DatasourceId::PypiWheelMetadata,
281            DatasourceId::PypiEgg,
282            DatasourceId::PypiJson,
283            DatasourceId::PypiSdistPkginfo,
284            DatasourceId::PypiInspectDeplock,
285            DatasourceId::PipRequirements,
286            DatasourceId::PypiPoetryLock,
287            DatasourceId::PypiPylockToml,
288            DatasourceId::PypiUvLock,
289            DatasourceId::Pipfile,
290            DatasourceId::PipfileLock,
291        ],
292        sibling_file_patterns: &[
293            "pyproject.toml",
294            "setup.py",
295            "setup.cfg",
296            "PKG-INFO",
297            "METADATA",
298            "pypi.json",
299            "requirements*.txt",
300            "Pipfile",
301            "Pipfile.lock",
302            "poetry.lock",
303            "pylock.toml",
304            "pylock.*.toml",
305            "uv.lock",
306        ],
307        mode: AssemblyMode::SiblingMerge,
308    },
309    AssemblerConfig {
310        datasource_ids: &[DatasourceId::DenoJson, DatasourceId::DenoLock],
311        sibling_file_patterns: &["deno.json", "deno.jsonc", "deno.lock"],
312        mode: AssemblyMode::SiblingMerge,
313    },
314    // Ruby/RubyGems ecosystem
315    AssemblerConfig {
316        datasource_ids: &[
317            DatasourceId::GemArchiveExtracted,
318            DatasourceId::Gemspec,
319            DatasourceId::Gemfile,
320            DatasourceId::GemfileLock,
321            DatasourceId::GemArchive,
322        ],
323        sibling_file_patterns: &[
324            "metadata.gz-extract",
325            "**/data.gz-extract/*.gemspec",
326            "**/data.gz-extract/Gemfile",
327            "**/data.gz-extract/Gemfile.lock",
328            "*.gemspec",
329            "Gemfile",
330            "Gemfile.lock",
331        ],
332        mode: AssemblyMode::SiblingMerge,
333    },
334    // Conda ecosystem
335    AssemblerConfig {
336        datasource_ids: &[
337            DatasourceId::CondaMetaYaml,
338            DatasourceId::CondaYaml,
339            DatasourceId::CondaMetaJson,
340        ],
341        sibling_file_patterns: &[
342            "meta.yaml",
343            "meta.yml",
344            "environment.yml",
345            "environment.yaml",
346            "conda.yaml",
347            "env.yaml",
348            "*.json",
349        ],
350        mode: AssemblyMode::SiblingMerge,
351    },
352    // RPM specfile (source packages)
353    AssemblerConfig {
354        datasource_ids: &[DatasourceId::RpmSpecfile],
355        sibling_file_patterns: &["*.spec"],
356        mode: AssemblyMode::SiblingMerge,
357    },
358    // Debian source packages (nested merge via debian/ directory)
359    AssemblerConfig {
360        datasource_ids: &[
361            DatasourceId::DebianControlInSource,
362            DatasourceId::DebianCopyright,
363        ],
364        sibling_file_patterns: &["**/debian/control", "**/debian/copyright"],
365        mode: AssemblyMode::SiblingMerge,
366    },
367    // Gradle/Android ecosystem
368    AssemblerConfig {
369        datasource_ids: &[DatasourceId::BuildGradle, DatasourceId::GradleLockfile],
370        sibling_file_patterns: &["build.gradle", "build.gradle.kts", "gradle.lockfile"],
371        mode: AssemblyMode::SiblingMerge,
372    },
373    AssemblerConfig {
374        datasource_ids: &[DatasourceId::GradleModule],
375        sibling_file_patterns: &["*.module"],
376        mode: AssemblyMode::OnePerPackageData,
377    },
378    // CPAN/Perl ecosystem
379    AssemblerConfig {
380        datasource_ids: &[
381            DatasourceId::CpanMetaJson,
382            DatasourceId::CpanMetaYml,
383            DatasourceId::CpanManifest,
384            DatasourceId::CpanDistIni,
385            DatasourceId::CpanMakefile,
386        ],
387        sibling_file_patterns: &[
388            "META.json",
389            "META.yml",
390            "MANIFEST",
391            "dist.ini",
392            "Makefile.PL",
393        ],
394        mode: AssemblyMode::SiblingMerge,
395    },
396    // NuGet/.NET ecosystem
397    AssemblerConfig {
398        datasource_ids: &[
399            DatasourceId::NugetCsproj,
400            DatasourceId::NugetFsproj,
401            DatasourceId::NugetNuspec,
402            DatasourceId::NugetNupkg,
403            DatasourceId::NugetProjectJson,
404            DatasourceId::NugetProjectLockJson,
405            DatasourceId::NugetPackagesConfig,
406            DatasourceId::NugetPackagesLock,
407            DatasourceId::NugetVbproj,
408        ],
409        sibling_file_patterns: &[
410            "*.csproj",
411            "*.fsproj",
412            "*.nuspec",
413            "*.nupkg",
414            "project.json",
415            "project.lock.json",
416            "packages.config",
417            "packages.lock.json",
418            "*.vbproj",
419        ],
420        mode: AssemblyMode::SiblingMerge,
421    },
422    AssemblerConfig {
423        datasource_ids: &[DatasourceId::NugetDepsJson],
424        sibling_file_patterns: &["*.deps.json"],
425        mode: AssemblyMode::OnePerPackageData,
426    },
427    // Swift/SPM ecosystem
428    AssemblerConfig {
429        datasource_ids: &[
430            DatasourceId::SwiftPackageManifestJson,
431            DatasourceId::SwiftPackageResolved,
432            DatasourceId::SwiftPackageShowDependencies,
433        ],
434        sibling_file_patterns: &["Package.swift", "Package.resolved"],
435        mode: AssemblyMode::SiblingMerge,
436    },
437    // ── Standalone assemblers (single file → single package) ──
438    //
439    // These ecosystems have only one manifest file type with no sibling merging.
440    // They still need configs so their datasource_ids are recognized by the assembler.
441    //
442    // Bower (JavaScript)
443    AssemblerConfig {
444        datasource_ids: &[DatasourceId::BowerJson],
445        sibling_file_patterns: &["bower.json"],
446        mode: AssemblyMode::SiblingMerge,
447    },
448    // CRAN (R language)
449    AssemblerConfig {
450        datasource_ids: &[DatasourceId::CranDescription],
451        sibling_file_patterns: &["DESCRIPTION"],
452        mode: AssemblyMode::SiblingMerge,
453    },
454    // FreeBSD packages
455    AssemblerConfig {
456        datasource_ids: &[DatasourceId::FreebsdCompactManifest],
457        sibling_file_patterns: &["+COMPACT_MANIFEST"],
458        mode: AssemblyMode::SiblingMerge,
459    },
460    // Haxe ecosystem
461    AssemblerConfig {
462        datasource_ids: &[DatasourceId::HaxelibJson],
463        sibling_file_patterns: &["haxelib.json"],
464        mode: AssemblyMode::SiblingMerge,
465    },
466    // OCaml/opam ecosystem
467    AssemblerConfig {
468        datasource_ids: &[DatasourceId::OpamFile],
469        sibling_file_patterns: &["opam"],
470        mode: AssemblyMode::SiblingMerge,
471    },
472    // RPM Mariner manifest
473    AssemblerConfig {
474        datasource_ids: &[DatasourceId::RpmMarinerManifest],
475        sibling_file_patterns: &["*.rpm.manifest"],
476        mode: AssemblyMode::SiblingMerge,
477    },
478    AssemblerConfig {
479        datasource_ids: &[DatasourceId::RpmYumdb],
480        sibling_file_patterns: &["**/var/lib/yum/yumdb/*/*/from_repo"],
481        mode: AssemblyMode::OnePerPackageData,
482    },
483    // Microsoft Update Manifest
484    AssemblerConfig {
485        datasource_ids: &[DatasourceId::MicrosoftUpdateManifestMum],
486        sibling_file_patterns: &["*.mum"],
487        mode: AssemblyMode::SiblingMerge,
488    },
489    // Autotools (C/C++ build system)
490    AssemblerConfig {
491        datasource_ids: &[DatasourceId::AutotoolsConfigure],
492        sibling_file_patterns: &["configure", "configure.ac"],
493        mode: AssemblyMode::SiblingMerge,
494    },
495    // Bazel (build system)
496    AssemblerConfig {
497        datasource_ids: &[DatasourceId::BazelBuild],
498        sibling_file_patterns: &["BUILD"],
499        mode: AssemblyMode::SiblingMerge,
500    },
501    AssemblerConfig {
502        datasource_ids: &[DatasourceId::BazelModule],
503        sibling_file_patterns: &["MODULE.bazel"],
504        mode: AssemblyMode::OnePerPackageData,
505    },
506    // Buck (build system)
507    AssemblerConfig {
508        datasource_ids: &[DatasourceId::BuckFile, DatasourceId::BuckMetadata],
509        sibling_file_patterns: &["BUCK", ".buckconfig"],
510        mode: AssemblyMode::SiblingMerge,
511    },
512    // Ant/Ivy (Java dependency management)
513    AssemblerConfig {
514        datasource_ids: &[DatasourceId::AntIvyXml],
515        sibling_file_patterns: &["ivy.xml"],
516        mode: AssemblyMode::SiblingMerge,
517    },
518    // Meteor (JavaScript platform)
519    AssemblerConfig {
520        datasource_ids: &[DatasourceId::MeteorPackage],
521        sibling_file_patterns: &["package.js"],
522        mode: AssemblyMode::SiblingMerge,
523    },
524    // ── One-per-PackageData assemblers (database files with many packages) ──
525    //
526    // Alpine installed package database
527    AssemblerConfig {
528        datasource_ids: &[DatasourceId::AlpineInstalledDb],
529        sibling_file_patterns: &["installed"],
530        mode: AssemblyMode::OnePerPackageData,
531    },
532    AssemblerConfig {
533        datasource_ids: &[DatasourceId::AlpineApkbuild],
534        sibling_file_patterns: &["APKBUILD"],
535        mode: AssemblyMode::SiblingMerge,
536    },
537    // RPM installed package databases (BDB, NDB, SQLite)
538    AssemblerConfig {
539        datasource_ids: &[
540            DatasourceId::RpmInstalledDatabaseBdb,
541            DatasourceId::RpmInstalledDatabaseNdb,
542            DatasourceId::RpmInstalledDatabaseSqlite,
543        ],
544        sibling_file_patterns: &["Packages", "Packages.db", "rpmdb.sqlite"],
545        mode: AssemblyMode::OnePerPackageData,
546    },
547    // Debian installed package databases
548    AssemblerConfig {
549        datasource_ids: &[
550            DatasourceId::DebianInstalledStatusDb,
551            DatasourceId::DebianDistrolessInstalledDb,
552        ],
553        sibling_file_patterns: &["status"],
554        mode: AssemblyMode::OnePerPackageData,
555    },
556    AssemblerConfig {
557        datasource_ids: &[DatasourceId::AboutFile],
558        sibling_file_patterns: &["*.ABOUT"],
559        mode: AssemblyMode::OnePerPackageData,
560    },
561];
562
563/// Datasource IDs that are intentionally NOT assembled.
564///
565/// These are either:
566/// - Non-package metadata (readme, about, os_release)
567/// - Binary archives (require external extraction via ExtractCode before scanning)
568/// - Supplementary metadata files (not primary package definitions)
569///
570/// This list serves as documentation; it is not used at runtime.
571#[cfg(test)]
572pub static UNASSEMBLED_DATASOURCE_IDS: &[DatasourceId] = &[
573    // Non-package metadata
574    DatasourceId::Readme,
575    DatasourceId::EtcOsRelease,
576    DatasourceId::Gitmodules,
577    // Binary archives (require external extraction via ExtractCode before scanning)
578    DatasourceId::AlpineApkArchive,
579    DatasourceId::AndroidAarLibrary,
580    DatasourceId::AndroidApk,
581    DatasourceId::AppleDmg,
582    DatasourceId::Axis2Mar,
583    DatasourceId::ChromeCrx,
584    DatasourceId::DebianDeb,
585    DatasourceId::DebianOriginalSourceTarball,
586    DatasourceId::DebianSourceMetadataTarball,
587    DatasourceId::InstallshieldInstaller,
588    DatasourceId::IosIpa,
589    DatasourceId::IsoDiskImage,
590    DatasourceId::JavaEarArchive,
591    DatasourceId::JavaJar,
592    DatasourceId::JavaWarArchive,
593    DatasourceId::JbossSar,
594    DatasourceId::MicrosoftCabinet,
595    DatasourceId::MozillaXpi,
596    DatasourceId::NsisInstaller,
597    DatasourceId::RpmArchive,
598    DatasourceId::SharShellArchive,
599    DatasourceId::SquashfsDiskImage,
600    // Supplementary metadata (not primary package definitions)
601    DatasourceId::ArchAurinfo,
602    DatasourceId::ArchPkginfo,
603    DatasourceId::ArchSrcinfo,
604    DatasourceId::Axis2ModuleXml,
605    DatasourceId::ClojureDepsEdn,
606    DatasourceId::ClojureProjectClj,
607    DatasourceId::DebianControlExtractedDeb,
608    DatasourceId::DebianInstalledFilesList,
609    DatasourceId::DebianInstalledMd5Sums,
610    DatasourceId::DebianMd5SumsInExtractedDeb,
611    DatasourceId::DebianSourceControlDsc,
612    DatasourceId::Dockerfile,
613    DatasourceId::HexMixLock,
614    DatasourceId::JavaEarApplicationXml,
615    DatasourceId::JavaWarWebXml,
616    DatasourceId::JbossServiceXml,
617    DatasourceId::MesonBuild,
618    DatasourceId::NugetDirectoryBuildProps,
619    DatasourceId::NugetDirectoryPackagesProps,
620    DatasourceId::RpmPackageLicenses,
621    DatasourceId::SbtBuildSbt,
622    DatasourceId::VcpkgJson,
623];
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628    use std::collections::HashSet;
629    use strum::IntoEnumIterator;
630
631    #[test]
632    fn test_every_datasource_id_is_accounted_for() {
633        let mut assembled: HashSet<DatasourceId> = HashSet::new();
634        for config in ASSEMBLERS {
635            for &dsid in config.datasource_ids {
636                assembled.insert(dsid);
637            }
638        }
639
640        let unassembled: HashSet<DatasourceId> =
641            UNASSEMBLED_DATASOURCE_IDS.iter().copied().collect();
642
643        let overlap: Vec<_> = assembled.intersection(&unassembled).collect();
644        assert!(
645            overlap.is_empty(),
646            "Datasource IDs in BOTH ASSEMBLERS and UNASSEMBLED: {overlap:?}"
647        );
648
649        let missing: Vec<_> = DatasourceId::iter()
650            .filter(|dsid| !assembled.contains(dsid) && !unassembled.contains(dsid))
651            .collect();
652
653        assert!(
654            missing.is_empty(),
655            "Datasource IDs in NEITHER ASSEMBLERS nor UNASSEMBLED: {missing:?}\n\
656             Add each to an AssemblerConfig in ASSEMBLERS, or to UNASSEMBLED_DATASOURCE_IDS."
657        );
658    }
659
660    #[test]
661    fn test_post_assembly_passes_are_unique() {
662        let unique: HashSet<PostAssemblyPassKind> = POST_ASSEMBLY_PASSES.iter().copied().collect();
663
664        assert_eq!(
665            unique.len(),
666            POST_ASSEMBLY_PASSES.len(),
667            "POST_ASSEMBLY_PASSES contains duplicate entries"
668        );
669    }
670
671    #[test]
672    fn test_every_post_assembly_pass_kind_is_registered_once() {
673        let registered: HashSet<PostAssemblyPassKind> =
674            POST_ASSEMBLY_PASSES.iter().copied().collect();
675
676        let missing: Vec<_> = PostAssemblyPassKind::iter()
677            .filter(|pass| !registered.contains(pass))
678            .collect();
679
680        assert!(
681            missing.is_empty(),
682            "Post-assembly pass variants not registered in POST_ASSEMBLY_PASSES: {missing:?}"
683        );
684
685        for pass in PostAssemblyPassKind::iter() {
686            let count = POST_ASSEMBLY_PASSES
687                .iter()
688                .filter(|registered| **registered == pass)
689                .count();
690            assert_eq!(
691                count, 1,
692                "Post-assembly pass {pass:?} should be registered exactly once"
693            );
694        }
695    }
696}