1mod about;
2#[cfg(test)]
3mod about_scan_test;
4#[cfg(test)]
5mod about_test;
6mod alpine;
7#[cfg(test)]
8mod alpine_scan_test;
9mod arch;
10#[cfg(test)]
11mod arch_scan_test;
12#[cfg(test)]
13mod arch_test;
14mod autotools;
15#[cfg(test)]
16mod autotools_test;
17mod bazel;
18#[cfg(test)]
19mod bazel_module_test;
20#[cfg(test)]
21mod bazel_test;
22mod bower;
23#[cfg(test)]
24mod bower_scan_test;
25#[cfg(test)]
26mod bower_test;
27mod buck;
28#[cfg(test)]
29mod buck_test;
30mod bun_lock;
31#[cfg(test)]
32mod bun_lock_test;
33mod bun_lockb;
34#[cfg(test)]
35mod bun_lockb_test;
36mod cargo;
37mod cargo_lock;
38#[cfg(test)]
39mod cargo_lock_test;
40#[cfg(test)]
41mod cargo_scan_test;
42#[cfg(test)]
43mod cargo_test;
44mod chef;
45#[cfg(test)]
46mod chef_scan_test;
47#[cfg(test)]
48mod chef_test;
49mod citation;
50#[cfg(test)]
51mod citation_test;
52mod clojure;
53#[cfg(test)]
54mod clojure_test;
55#[cfg(test)]
56mod cocoapods_scan_test;
57pub(crate) mod compiled_binary;
58mod composer;
59#[cfg(test)]
60mod composer_scan_test;
61#[cfg(test)]
62mod composer_test;
63mod conan;
64mod conan_data;
65#[cfg(test)]
66mod conan_data_test;
67#[cfg(test)]
68mod conan_scan_test;
69#[cfg(test)]
70mod conan_test;
71mod conda;
72mod conda_meta_json;
73#[cfg(test)]
74mod conda_meta_json_test;
75#[cfg(test)]
76mod conda_scan_test;
77#[cfg(test)]
78mod conda_test;
79mod cpan;
80mod cpan_dist_ini;
81#[cfg(test)]
82mod cpan_dist_ini_test;
83mod cpan_makefile_pl;
84#[cfg(test)]
85mod cpan_makefile_pl_test;
86#[cfg(test)]
87mod cpan_scan_test;
88#[cfg(test)]
89mod cpan_test;
90mod cran;
91#[cfg(test)]
92mod cran_scan_test;
93#[cfg(test)]
94mod cran_test;
95mod dart;
96#[cfg(test)]
97mod dart_scan_test;
98#[cfg(test)]
99mod dart_test;
100mod debian;
101#[cfg(test)]
102mod debian_scan_test;
103#[cfg(test)]
104mod debian_test;
105mod deno;
106mod deno_lock;
107#[cfg(test)]
108mod deno_lock_test;
109#[cfg(test)]
110mod deno_scan_test;
111#[cfg(test)]
112mod deno_test;
113mod docker;
114#[cfg(test)]
115mod docker_scan_test;
116#[cfg(test)]
117mod docker_test;
118mod freebsd;
119#[cfg(test)]
120mod freebsd_scan_test;
121#[cfg(test)]
122mod freebsd_test;
123mod gitmodules;
124#[cfg(test)]
125mod gitmodules_scan_test;
126mod go;
127mod go_mod_graph;
128#[cfg(test)]
129mod go_scan_test;
130#[cfg(test)]
131mod go_test;
132#[cfg(test)]
133mod go_work_test;
134#[cfg(all(test, feature = "golden-tests"))]
135pub(crate) mod golden_test_utils;
136mod gradle;
137mod gradle_lock;
138#[cfg(test)]
139mod gradle_lock_test;
140mod gradle_module;
141#[cfg(test)]
142mod gradle_module_scan_test;
143#[cfg(test)]
144mod gradle_module_test;
145#[cfg(test)]
146mod gradle_scan_test;
147mod hackage;
148#[cfg(test)]
149mod hackage_scan_test;
150#[cfg(test)]
151mod hackage_test;
152mod haxe;
153#[cfg(test)]
154mod haxe_scan_test;
155#[cfg(test)]
156mod haxe_test;
157mod helm;
158#[cfg(test)]
159mod helm_scan_test;
160#[cfg(test)]
161mod helm_test;
162mod hex_lock;
163#[cfg(test)]
164mod hex_lock_test;
165mod license_normalization;
166mod maven;
167#[cfg(test)]
168mod maven_scan_test;
169#[cfg(test)]
170mod maven_test;
171mod meson;
172#[cfg(test)]
173mod meson_scan_test;
174#[cfg(test)]
175mod meson_test;
176pub mod metadata;
177mod microsoft_update_manifest;
178#[cfg(test)]
179mod microsoft_update_manifest_test;
180mod misc;
181#[cfg(test)]
182mod misc_test;
183mod nix;
184#[cfg(test)]
185mod nix_scan_test;
186#[cfg(test)]
187mod nix_test;
188mod npm;
189mod npm_lock;
190#[cfg(test)]
191mod npm_lock_test;
192#[cfg(test)]
193mod npm_scan_test;
194#[cfg(test)]
195mod npm_test;
196mod npm_workspace;
197#[cfg(test)]
198mod npm_workspace_test;
199mod nuget;
200#[cfg(test)]
201mod nuget_scan_test;
202#[cfg(test)]
203mod nuget_test;
204mod opam;
205#[cfg(test)]
206mod opam_scan_test;
207mod os_release;
208#[cfg(test)]
209mod os_release_test;
210#[cfg(test)]
211mod osgi_test;
212mod pep508;
213mod pip_inspect_deplock;
214#[cfg(test)]
215mod pip_inspect_deplock_test;
216mod pipfile_lock;
217#[cfg(test)]
218mod pipfile_lock_test;
219mod pixi;
220#[cfg(test)]
221mod pixi_scan_test;
222#[cfg(test)]
223mod pixi_test;
224mod pnpm_lock;
225#[cfg(test)]
226mod pnpm_lock_test;
227mod podfile;
228mod podfile_lock;
229#[cfg(test)]
230mod podfile_lock_test;
231mod podspec;
232mod podspec_json;
233#[cfg(test)]
234mod podspec_json_test;
235mod poetry_lock;
236#[cfg(test)]
237mod poetry_lock_test;
238mod publiccode;
239#[cfg(test)]
240mod publiccode_test;
241mod pylock_toml;
242#[cfg(test)]
243mod pylock_toml_test;
244mod python;
245#[cfg(test)]
246mod python_scan_test;
247#[cfg(test)]
248mod python_test;
249mod readme;
250#[cfg(test)]
251mod readme_test;
252mod requirements_txt;
253#[cfg(test)]
254mod requirements_txt_test;
255pub(crate) mod rfc822;
256mod rpm_db;
257mod rpm_db_native;
258#[cfg(test)]
259mod rpm_db_scan_test;
260mod rpm_license_files;
261#[cfg(test)]
262mod rpm_license_files_test;
263mod rpm_mariner_manifest;
264#[cfg(test)]
265mod rpm_mariner_manifest_test;
266mod rpm_parser;
267#[cfg(test)]
268mod rpm_scan_test;
269mod rpm_specfile;
270#[cfg(test)]
271mod rpm_specfile_test;
272mod rpm_yumdb;
273mod ruby;
274#[cfg(test)]
275mod ruby_scan_test;
276#[cfg(test)]
277mod ruby_test;
278mod sbt;
279#[cfg(test)]
280mod sbt_test;
281#[cfg(test)]
282mod scan_test_utils;
283mod swift_manifest_json;
284#[cfg(test)]
285mod swift_manifest_json_test;
286mod swift_resolved;
287#[cfg(test)]
288mod swift_resolved_test;
289#[cfg(test)]
290mod swift_scan_test;
291mod swift_show_dependencies;
292#[cfg(test)]
293mod swift_show_dependencies_test;
294pub mod utils;
295mod uv_lock;
296#[cfg(test)]
297mod uv_lock_test;
298mod vcpkg;
299#[cfg(test)]
300mod vcpkg_scan_test;
301#[cfg(test)]
302mod vcpkg_test;
303pub(crate) mod windows_executable;
304#[cfg(test)]
305mod windows_executable_golden_test;
306mod yarn_lock;
307#[cfg(test)]
308mod yarn_lock_test;
309mod yarn_pnp;
310#[cfg(test)]
311mod yarn_pnp_test;
312
313#[cfg(all(test, feature = "golden-tests"))]
314mod golden_test;
315
316use std::cell::RefCell;
317use std::panic::{AssertUnwindSafe, catch_unwind};
318use std::path::Path;
319
320use crate::models::{PackageData, PackageType};
321use crate::parsers::license_normalization::finalize_package_declared_license_references;
322
323thread_local! {
324 static PARSER_DIAGNOSTIC_STACK: RefCell<Vec<Vec<String>>> = const { RefCell::new(Vec::new()) };
325}
326
327#[derive(Debug, Default)]
328pub struct ParsePackagesResult {
329 pub packages: Vec<PackageData>,
330 pub scan_errors: Vec<String>,
331}
332
333fn panic_payload_to_string(payload: &(dyn std::any::Any + Send)) -> String {
334 if let Some(message) = payload.downcast_ref::<&str>() {
335 (*message).to_string()
336 } else if let Some(message) = payload.downcast_ref::<String>() {
337 message.clone()
338 } else {
339 "unknown panic payload".to_string()
340 }
341}
342
343pub(crate) fn capture_parser_diagnostics<F>(
344 extract: F,
345 handler_name: &str,
346 path: &Path,
347) -> ParsePackagesResult
348where
349 F: FnOnce() -> Vec<PackageData>,
350{
351 PARSER_DIAGNOSTIC_STACK.with(|stack| {
352 stack.borrow_mut().push(Vec::new());
353 });
354
355 let extract_result = catch_unwind(AssertUnwindSafe(extract));
356 let mut scan_errors =
357 PARSER_DIAGNOSTIC_STACK.with(|stack| stack.borrow_mut().pop().unwrap_or_default());
358
359 match extract_result {
360 Ok(packages) => ParsePackagesResult {
361 packages: packages
362 .into_iter()
363 .map(|mut package| {
364 finalize_package_declared_license_references(&mut package);
365 package
366 })
367 .collect(),
368 scan_errors,
369 },
370 Err(payload) => {
371 scan_errors.push(format!(
372 "{} panicked while parsing {}: {}",
373 handler_name,
374 path.display(),
375 panic_payload_to_string(payload.as_ref())
376 ));
377 ParsePackagesResult {
378 packages: Vec::new(),
379 scan_errors,
380 }
381 }
382 }
383}
384
385pub(crate) fn record_parser_diagnostic(message: String) -> bool {
386 PARSER_DIAGNOSTIC_STACK.with(|stack| {
387 let mut stack = stack.borrow_mut();
388 let Some(active) = stack.last_mut() else {
389 return false;
390 };
391 active.push(message);
392 true
393 })
394}
395
396#[macro_export]
397macro_rules! parser_warn {
398 ($($arg:tt)*) => {{
399 let message = format!($($arg)*);
400 if !$crate::parsers::record_parser_diagnostic(message.clone()) {
401 log::warn!("{message}");
402 }
403 }};
404}
405
406pub trait PackageParser {
450 const PACKAGE_TYPE: PackageType;
452
453 fn extract_packages(path: &Path) -> Vec<PackageData>;
463
464 fn is_match(path: &Path) -> bool;
469
470 fn extract_first_package(path: &Path) -> PackageData {
473 Self::extract_packages(path)
474 .into_iter()
475 .map(|mut package| {
476 finalize_package_declared_license_references(&mut package);
477 package
478 })
479 .next()
480 .unwrap_or_default()
481 }
482}
483
484pub use self::about::AboutFileParser;
485pub use self::alpine::{AlpineApkParser, AlpineApkbuildParser, AlpineInstalledParser};
486pub use self::arch::{ArchPkginfoParser, ArchSrcinfoParser};
487pub use self::autotools::AutotoolsConfigureParser;
488pub use self::bazel::{BazelBuildParser, BazelModuleParser};
489pub use self::bower::BowerJsonParser;
490pub use self::buck::{BuckBuildParser, BuckMetadataBzlParser};
491pub use self::bun_lock::BunLockParser;
492pub use self::bun_lockb::BunLockbParser;
493pub use self::cargo::CargoParser;
494#[cfg_attr(not(test), allow(unused_imports))]
495pub use self::cargo_lock::CargoLockParser;
496pub use self::chef::{ChefMetadataJsonParser, ChefMetadataRbParser};
497pub use self::citation::CitationCffParser;
498pub use self::clojure::{ClojureDepsEdnParser, ClojureProjectCljParser};
499pub use self::composer::{ComposerJsonParser, ComposerLockParser};
500pub use self::conan::{ConanFilePyParser, ConanLockParser, ConanfileTxtParser};
501pub use self::conan_data::ConanDataParser;
502pub use self::conda::{CondaEnvironmentYmlParser, CondaMetaYamlParser};
503pub use self::conda_meta_json::CondaMetaJsonParser;
504pub use self::cpan::{CpanManifestParser, CpanMetaJsonParser, CpanMetaYmlParser};
505pub use self::cpan_dist_ini::CpanDistIniParser;
506pub use self::cpan_makefile_pl::CpanMakefilePlParser;
507pub use self::cran::CranParser;
508pub use self::dart::{PubspecLockParser, PubspecYamlParser};
509pub use self::debian::{
510 DebianControlInExtractedDebParser, DebianControlParser, DebianCopyrightParser, DebianDebParser,
511 DebianDebianTarParser, DebianDistrolessInstalledParser, DebianDscParser,
512 DebianInstalledListParser, DebianInstalledMd5sumsParser, DebianInstalledParser,
513 DebianMd5sumInPackageParser, DebianOrigTarParser,
514};
515pub use self::deno::DenoParser;
516pub use self::deno_lock::DenoLockParser;
517pub use self::docker::DockerfileParser;
518pub use self::freebsd::FreebsdCompactManifestParser;
519pub use self::gitmodules::GitmodulesParser;
520pub use self::go::{GoModParser, GoSumParser, GoWorkParser, GodepsParser};
521pub use self::go_mod_graph::GoModGraphParser;
522pub use self::gradle::GradleParser;
523pub use self::gradle_lock::GradleLockfileParser;
524pub use self::gradle_module::GradleModuleParser;
525pub use self::hackage::{HackageCabalParser, HackageCabalProjectParser, HackageStackYamlParser};
526pub use self::haxe::HaxeParser;
527pub use self::helm::{HelmChartLockParser, HelmChartYamlParser};
528pub use self::hex_lock::HexLockParser;
529pub use self::maven::MavenParser;
530pub use self::meson::MesonParser;
531pub use self::microsoft_update_manifest::MicrosoftUpdateManifestParser;
532pub use self::misc::{
533 AndroidApkRecognizer, AndroidLibraryRecognizer, AppleDmgRecognizer, Axis2MarRecognizer,
534 Axis2ModuleXmlRecognizer, CabArchiveRecognizer, ChromeCrxRecognizer, InstallShieldRecognizer,
535 IosIpaRecognizer, IsoImageRecognizer, IvyXmlRecognizer, JBossSarRecognizer,
536 JBossServiceXmlRecognizer, JavaEarAppXmlRecognizer, JavaEarRecognizer, JavaJarRecognizer,
537 JavaWarRecognizer, JavaWarWebXmlRecognizer, MeteorPackageRecognizer, MozillaXpiRecognizer,
538 NsisRecognizer, SharArchiveRecognizer, SquashfsRecognizer,
539};
540pub use self::nix::{NixDefaultParser, NixFlakeLockParser, NixFlakeParser};
541pub use self::npm::NpmParser;
542pub use self::npm_lock::NpmLockParser;
543pub use self::npm_workspace::NpmWorkspaceParser;
544pub use self::nuget::{
545 CentralPackageManagementPropsParser, DirectoryBuildPropsParser, DotNetDepsJsonParser,
546 NupkgParser, NuspecParser, PackageReferenceProjectParser, PackagesConfigParser,
547 PackagesLockParser, ProjectJsonParser, ProjectLockJsonParser,
548};
549pub use self::opam::OpamParser;
550pub use self::os_release::OsReleaseParser;
551pub use self::pip_inspect_deplock::PipInspectDeplockParser;
552pub use self::pipfile_lock::PipfileLockParser;
553pub use self::pixi::{PixiLockParser, PixiTomlParser};
554pub use self::pnpm_lock::PnpmLockParser;
555pub use self::podfile::PodfileParser;
556pub use self::podfile_lock::PodfileLockParser;
557pub use self::podspec::PodspecParser;
558pub use self::podspec_json::PodspecJsonParser;
559pub use self::poetry_lock::PoetryLockParser;
560pub use self::publiccode::PubliccodeParser;
561pub use self::pylock_toml::PylockTomlParser;
562pub use self::python::PythonParser;
563pub use self::readme::ReadmeParser;
564pub use self::requirements_txt::RequirementsTxtParser;
565#[cfg(feature = "rpm-sqlite")]
566pub use self::rpm_db::RpmSqliteDatabaseParser;
567pub use self::rpm_db::{RpmBdbDatabaseParser, RpmNdbDatabaseParser};
568pub use self::rpm_license_files::RpmLicenseFilesParser;
569pub use self::rpm_mariner_manifest::RpmMarinerManifestParser;
570pub use self::rpm_parser::RpmParser;
571pub use self::rpm_specfile::RpmSpecfileParser;
572pub use self::rpm_yumdb::RpmYumdbParser;
573pub use self::ruby::{
574 GemArchiveParser, GemMetadataExtractedParser, GemfileLockParser, GemfileParser, GemspecParser,
575};
576pub use self::sbt::SbtParser;
577pub use self::swift_manifest_json::SwiftManifestJsonParser;
578pub use self::swift_resolved::SwiftPackageResolvedParser;
579pub use self::swift_show_dependencies::SwiftShowDependenciesParser;
580pub use self::uv_lock::UvLockParser;
581pub use self::vcpkg::VcpkgManifestParser;
582pub use self::yarn_lock::YarnLockParser;
583pub use self::yarn_pnp::YarnPnpParser;
584
585macro_rules! register_package_handlers {
591 (
592 parsers: [$($(#[$parser_meta:meta])* $parser:ty),* $(,)?],
593 recognizers: [$($recognizer:ty),* $(,)?] $(,)?
594 ) => {
595 pub fn try_parse_file(path: &Path) -> Option<ParsePackagesResult> {
596 $(
597 $(#[$parser_meta])*
598 if <$parser>::is_match(path) {
599 return Some(capture_parser_diagnostics(
600 || <$parser>::extract_packages(path),
601 stringify!($parser),
602 path,
603 ));
604 }
605 )*
606 $(
607 if <$recognizer>::is_match(path) {
608 return Some(capture_parser_diagnostics(
609 || <$recognizer>::extract_packages(path),
610 stringify!($recognizer),
611 path,
612 ));
613 }
614 )*
615 None
616 }
617
618 #[allow(dead_code)]
621 pub fn parse_by_type_name(type_name: &str, path: &Path) -> Option<PackageData> {
622 match type_name {
623 $(
624 $(#[$parser_meta])*
625 stringify!($parser) => Some(<$parser>::extract_first_package(path)),
626 )*
627 $(
628 stringify!($recognizer) => Some(<$recognizer>::extract_first_package(path)),
629 )*
630 _ => None
631 }
632 }
633
634 #[allow(dead_code)]
637 pub fn list_parser_types() -> Vec<&'static str> {
638 vec![
639 $(
640 $(#[$parser_meta])*
641 stringify!($parser),
642 )*
643 $(
644 stringify!($recognizer),
645 )*
646 ]
647 }
648 };
649}
650
651register_package_handlers! {
652 parsers: [
653 AboutFileParser,
654 AlpineApkParser,
655 AlpineApkbuildParser,
656 AlpineInstalledParser,
657 ArchPkginfoParser,
658 ArchSrcinfoParser,
659 AutotoolsConfigureParser,
660 BazelBuildParser,
661 BazelModuleParser,
662 BowerJsonParser,
663 BunLockParser,
664 BunLockbParser,
665 BuckBuildParser,
666 BuckMetadataBzlParser,
667 CargoLockParser,
668 CargoParser,
669 ChefMetadataJsonParser,
670 ChefMetadataRbParser,
671 CitationCffParser,
672 ClojureDepsEdnParser,
673 ClojureProjectCljParser,
674 ComposerJsonParser,
675 ComposerLockParser,
676 ConanDataParser,
677 ConanFilePyParser,
678 ConanfileTxtParser,
679 ConanLockParser,
680 CondaEnvironmentYmlParser,
681 CondaMetaJsonParser,
682 CondaMetaYamlParser,
683 CpanDistIniParser,
684 CpanMakefilePlParser,
685 CpanManifestParser,
686 CpanMetaJsonParser,
687 CpanMetaYmlParser,
688 CranParser,
689 DebianControlInExtractedDebParser,
690 DebianControlParser,
691 DebianCopyrightParser,
692 DebianDebianTarParser,
693 DebianDebParser,
694 DebianDistrolessInstalledParser,
695 DebianDscParser,
696 DebianInstalledListParser,
697 DebianInstalledMd5sumsParser,
698 DebianInstalledParser,
699 DebianMd5sumInPackageParser,
700 DebianOrigTarParser,
701 DenoParser,
702 DenoLockParser,
703 DockerfileParser,
704 FreebsdCompactManifestParser,
705 GemArchiveParser,
706 GemfileLockParser,
707 GemfileParser,
708 GemMetadataExtractedParser,
709 GemspecParser,
710 GitmodulesParser,
711 GodepsParser,
712 GoModParser,
713 GoModGraphParser,
714 GoSumParser,
715 GoWorkParser,
716 GradleLockfileParser,
717 GradleParser,
718 GradleModuleParser,
719 HackageCabalParser,
720 HackageCabalProjectParser,
721 HackageStackYamlParser,
722 HelmChartYamlParser,
723 HelmChartLockParser,
724 HaxeParser,
725 HexLockParser,
726 MavenParser,
727 MesonParser,
728 MicrosoftUpdateManifestParser,
729 NixDefaultParser,
730 NixFlakeLockParser,
731 NixFlakeParser,
732 NpmLockParser,
733 NpmParser,
734 NpmWorkspaceParser,
735 DotNetDepsJsonParser,
736 CentralPackageManagementPropsParser,
737 DirectoryBuildPropsParser,
738 NupkgParser,
739 NuspecParser,
740 PackageReferenceProjectParser,
741 OpamParser,
742 OsReleaseParser,
743 PackagesConfigParser,
744 PackagesLockParser,
745 ProjectJsonParser,
746 ProjectLockJsonParser,
747 PipfileLockParser,
748 PipInspectDeplockParser,
749 PixiTomlParser,
750 PixiLockParser,
751 PnpmLockParser,
752 PodfileLockParser,
753 PodfileParser,
754 PodspecJsonParser,
755 PodspecParser,
756 PoetryLockParser,
757 PubliccodeParser,
758 PylockTomlParser,
759 PubspecLockParser,
760 PubspecYamlParser,
761 PythonParser,
762 UvLockParser,
763 VcpkgManifestParser,
764 ReadmeParser,
765 RequirementsTxtParser,
766 RpmBdbDatabaseParser,
767 RpmLicenseFilesParser,
768 RpmMarinerManifestParser,
769 RpmNdbDatabaseParser,
770 RpmParser,
771 RpmSpecfileParser,
772 #[cfg(feature = "rpm-sqlite")]
773 RpmSqliteDatabaseParser,
774 RpmYumdbParser,
775 SbtParser,
776 SwiftManifestJsonParser,
777 SwiftPackageResolvedParser,
778 SwiftShowDependenciesParser,
779 YarnLockParser,
780 YarnPnpParser,
781 ],
782 recognizers: [
783 AndroidApkRecognizer,
784 AndroidLibraryRecognizer,
785 AppleDmgRecognizer,
786 Axis2MarRecognizer,
787 Axis2ModuleXmlRecognizer,
788 CabArchiveRecognizer,
789 ChromeCrxRecognizer,
790 InstallShieldRecognizer,
791 IosIpaRecognizer,
792 IsoImageRecognizer,
793 IvyXmlRecognizer,
794 JavaEarAppXmlRecognizer,
795 JavaEarRecognizer,
796 JavaJarRecognizer,
797 JavaWarRecognizer,
798 JavaWarWebXmlRecognizer,
799 JBossSarRecognizer,
800 JBossServiceXmlRecognizer,
801 MeteorPackageRecognizer,
802 MozillaXpiRecognizer,
803 NsisRecognizer,
804 SharArchiveRecognizer,
805 SquashfsRecognizer,
806 ],
807}
808
809#[cfg(test)]
810mod panic_isolation_tests {
811 use super::*;
812
813 #[test]
814 fn capture_parser_diagnostics_turns_panics_into_scan_errors() {
815 let path = Path::new("fixtures/panic-package.json");
816 let result = capture_parser_diagnostics(
817 || -> Vec<PackageData> { panic!("panic boom") },
818 "PanicParser",
819 path,
820 );
821
822 assert!(result.packages.is_empty());
823 assert_eq!(result.scan_errors.len(), 1);
824 assert!(result.scan_errors[0].contains("PanicParser"));
825 assert!(result.scan_errors[0].contains("fixtures/panic-package.json"));
826 assert!(result.scan_errors[0].contains("panic boom"));
827 }
828
829 #[test]
830 fn capture_parser_diagnostics_recovers_after_panic() {
831 let panic_path = Path::new("fixtures/panic-package.json");
832 let _ = capture_parser_diagnostics(
833 || -> Vec<PackageData> { panic!("panic boom") },
834 "PanicParser",
835 panic_path,
836 );
837
838 let ok_path = Path::new("fixtures/recovered-package.json");
839 let result = capture_parser_diagnostics(
840 || {
841 crate::parser_warn!("recoverable parser warning");
842 vec![PackageData {
843 package_type: Some(PackageType::Npm),
844 ..Default::default()
845 }]
846 },
847 "RecoveringParser",
848 ok_path,
849 );
850
851 assert_eq!(result.packages.len(), 1);
852 assert_eq!(result.scan_errors, vec!["recoverable parser warning"]);
853 }
854}