1use std::collections::{HashMap, HashSet, VecDeque};
24use std::path::Path;
25
26use crate::parser_warn as warn;
27use crate::parsers::utils::{
28 MAX_ITERATION_COUNT, RecursionGuard, read_file_to_string, truncate_field,
29};
30use packageurl::PackageUrl;
31use yaml_serde::{Mapping, Value};
32
33use crate::models::{
34 DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage, Sha256Digest,
35};
36
37use super::PackageParser;
38
39const FIELD_NAME: &str = "name";
40const FIELD_VERSION: &str = "version";
41const FIELD_DESCRIPTION: &str = "description";
42const FIELD_HOMEPAGE: &str = "homepage";
43const FIELD_LICENSE: &str = "license";
44const FIELD_REPOSITORY: &str = "repository";
45const FIELD_AUTHOR: &str = "author";
46const FIELD_AUTHORS: &str = "authors";
47const FIELD_DEPENDENCIES: &str = "dependencies";
48const FIELD_DEV_DEPENDENCIES: &str = "dev_dependencies";
49const FIELD_DEPENDENCY_OVERRIDES: &str = "dependency_overrides";
50const FIELD_ENVIRONMENT: &str = "environment";
51const FIELD_ISSUE_TRACKER: &str = "issue_tracker";
52const FIELD_DOCUMENTATION: &str = "documentation";
53const FIELD_EXECUTABLES: &str = "executables";
54const FIELD_PUBLISH_TO: &str = "publish_to";
55const FIELD_ARCHIVE_URL: &str = "archive_url";
56const FIELD_PLATFORMS: &str = "platforms";
57const FIELD_FUNDING: &str = "funding";
58const FIELD_FALSE_SECRETS: &str = "false_secrets";
59const FIELD_SCREENSHOTS: &str = "screenshots";
60const FIELD_TOPICS: &str = "topics";
61const FIELD_IGNORED_ADVISORIES: &str = "ignored_advisories";
62const FIELD_PACKAGES: &str = "packages";
63const FIELD_SDKS: &str = "sdks";
64const FIELD_SDK: &str = "sdk";
65const FIELD_DEPENDENCY: &str = "dependency";
66const FIELD_SHA256: &str = "sha256";
67
68pub struct PubspecYamlParser;
70
71impl PackageParser for PubspecYamlParser {
72 const PACKAGE_TYPE: PackageType = PackageType::Dart;
73
74 fn extract_packages(path: &Path) -> Vec<PackageData> {
75 let yaml_content = match read_yaml_file(path) {
76 Ok(content) => content,
77 Err(e) => {
78 warn!("Failed to read pubspec.yaml at {:?}: {}", path, e);
79 let mut package_data = default_package_data();
80 package_data.datasource_id = Some(DatasourceId::PubspecYaml);
81 return vec![package_data];
82 }
83 };
84
85 vec![parse_pubspec_yaml(&yaml_content)]
86 }
87
88 fn is_match(path: &Path) -> bool {
89 path.file_name().is_some_and(|name| name == "pubspec.yaml")
90 }
91}
92
93pub struct PubspecLockParser;
95
96impl PackageParser for PubspecLockParser {
97 const PACKAGE_TYPE: PackageType = PackageType::Pubspec;
98
99 fn extract_packages(path: &Path) -> Vec<PackageData> {
100 let yaml_content = match read_yaml_file(path) {
101 Ok(content) => content,
102 Err(e) => {
103 warn!("Failed to read pubspec.lock at {:?}: {}", path, e);
104 let mut package_data =
105 default_package_data_with_type(PubspecLockParser::PACKAGE_TYPE.as_str());
106 package_data.datasource_id = Some(DatasourceId::PubspecLock);
107 return vec![package_data];
108 }
109 };
110
111 vec![parse_pubspec_lock(&yaml_content)]
112 }
113
114 fn is_match(path: &Path) -> bool {
115 path.file_name().is_some_and(|name| name == "pubspec.lock")
116 }
117}
118
119fn read_yaml_file(path: &Path) -> Result<Value, String> {
120 let content =
121 read_file_to_string(path, None).map_err(|e| format!("Failed to read file: {}", e))?;
122 yaml_serde::from_str(&content).map_err(|e| format!("Failed to parse YAML: {}", e))
123}
124
125fn parse_pubspec_yaml(yaml_content: &Value) -> PackageData {
126 let name = extract_string_field(yaml_content, FIELD_NAME).map(truncate_field);
127 let version = extract_string_field(yaml_content, FIELD_VERSION).map(truncate_field);
128 let description = extract_description_field(yaml_content).map(truncate_field);
129 let homepage_url = extract_string_field(yaml_content, FIELD_HOMEPAGE).map(truncate_field);
130 let raw_license = extract_string_field(yaml_content, FIELD_LICENSE).map(truncate_field);
131 let vcs_url = extract_string_field(yaml_content, FIELD_REPOSITORY).map(truncate_field);
132 let bug_tracking_url =
133 extract_string_field(yaml_content, FIELD_ISSUE_TRACKER).map(truncate_field);
134 let archive_url = extract_string_field(yaml_content, FIELD_ARCHIVE_URL).map(truncate_field);
135
136 let parties = extract_authors(yaml_content);
137
138 let declared_license_expression = None;
139 let declared_license_expression_spdx = None;
140 let license_detections = Vec::new();
141
142 let dependencies = [
143 collect_dependencies(
144 yaml_content,
145 FIELD_DEPENDENCIES,
146 Some("dependencies"),
147 true,
148 false,
149 ),
150 collect_dependencies(
151 yaml_content,
152 FIELD_DEV_DEPENDENCIES,
153 Some("dev_dependencies"),
154 false,
155 true,
156 ),
157 collect_dependencies(
158 yaml_content,
159 FIELD_DEPENDENCY_OVERRIDES,
160 Some("dependency_overrides"),
161 true,
162 false,
163 ),
164 collect_dependencies(
165 yaml_content,
166 FIELD_ENVIRONMENT,
167 Some("environment"),
168 true,
169 false,
170 ),
171 ]
172 .concat();
173
174 let extra_data = build_extra_data(yaml_content);
175 let keywords = extract_string_list_field(yaml_content, FIELD_TOPICS);
176
177 let purl = name
178 .as_ref()
179 .and_then(|name| build_purl(name, version.as_deref()))
180 .map(truncate_field);
181
182 let (api_data_url, repository_homepage_url, repository_download_url) =
183 if let (Some(name_val), Some(version_val)) = (&name, &version) {
184 (
185 Some(truncate_field(format!(
186 "https://pub.dev/api/packages/{}/versions/{}",
187 name_val, version_val
188 ))),
189 Some(truncate_field(format!(
190 "https://pub.dev/packages/{}/versions/{}",
191 name_val, version_val
192 ))),
193 Some(truncate_field(format!(
194 "https://pub.dartlang.org/packages/{}/versions/{}.tar.gz",
195 name_val, version_val
196 ))),
197 )
198 } else {
199 (None, None, None)
200 };
201
202 let download_url = archive_url.or_else(|| repository_download_url.clone());
203
204 PackageData {
205 package_type: Some(PubspecYamlParser::PACKAGE_TYPE),
206 namespace: None,
207 name,
208 version,
209 qualifiers: None,
210 subpath: None,
211 primary_language: Some("dart".to_string()),
212 description,
213 release_date: None,
214 parties,
215 keywords,
216 homepage_url,
217 download_url,
218 size: None,
219 sha1: None,
220 md5: None,
221 sha256: None,
222 sha512: None,
223 bug_tracking_url,
224 code_view_url: None,
225 vcs_url,
226 copyright: None,
227 holder: None,
228 declared_license_expression,
229 declared_license_expression_spdx,
230 license_detections,
231 other_license_expression: None,
232 other_license_expression_spdx: None,
233 other_license_detections: Vec::new(),
234 extracted_license_statement: raw_license,
235 notice_text: None,
236 source_packages: Vec::new(),
237 file_references: Vec::new(),
238 is_private: yaml_content
239 .get(FIELD_PUBLISH_TO)
240 .and_then(Value::as_str)
241 .is_some_and(|value| value.trim() == "none"),
242 is_virtual: false,
243 extra_data,
244 dependencies,
245 repository_homepage_url,
246 repository_download_url,
247 api_data_url,
248 datasource_id: Some(DatasourceId::PubspecYaml),
249 purl,
250 }
251}
252
253fn parse_pubspec_lock(yaml_content: &Value) -> PackageData {
254 let dependencies = extract_lock_dependencies(yaml_content);
255
256 let mut package_data = default_package_data_with_type(PubspecLockParser::PACKAGE_TYPE.as_str());
257 package_data.dependencies = dependencies;
258 package_data.datasource_id = Some(DatasourceId::PubspecLock);
259 package_data
260}
261
262fn extract_lock_dependencies(lock_data: &Value) -> Vec<Dependency> {
263 let mut dependencies = Vec::new();
264
265 if let Some(sdks) = lock_data.get(FIELD_SDKS).and_then(Value::as_mapping) {
266 for (name_value, version_value) in sdks.iter().take(MAX_ITERATION_COUNT) {
267 if let (Some(name), Some(version_str)) = (name_value.as_str(), version_value.as_str()) {
268 let purl = build_dependency_purl(name, None).map(truncate_field);
269 dependencies.push(Dependency {
270 purl,
271 extracted_requirement: Some(truncate_field(version_str.to_string())),
272 scope: Some("sdk".to_string()),
273 is_runtime: Some(true),
274 is_optional: Some(false),
275 is_pinned: Some(false),
276 is_direct: Some(true),
277 resolved_package: None,
278 extra_data: None,
279 });
280 }
281 }
282 } else if let Some(version_str) = lock_data.get(FIELD_SDK).and_then(Value::as_str) {
283 let purl = build_dependency_purl("dart", None).map(truncate_field);
284 dependencies.push(Dependency {
285 purl,
286 extracted_requirement: Some(truncate_field(version_str.to_string())),
287 scope: Some("sdk".to_string()),
288 is_runtime: Some(true),
289 is_optional: Some(false),
290 is_pinned: Some(false),
291 is_direct: Some(true),
292 resolved_package: None,
293 extra_data: None,
294 });
295 }
296
297 let Some(packages) = lock_data.get(FIELD_PACKAGES).and_then(Value::as_mapping) else {
298 return dependencies;
299 };
300
301 let runtime_reachable =
302 reachable_lock_packages(packages, &["direct main", "direct overridden"]);
303 let dev_only_reachable = reachable_lock_packages(packages, &["direct dev"]);
304
305 for (name_value, details_value) in packages.iter().take(MAX_ITERATION_COUNT) {
306 let name = match name_value.as_str() {
307 Some(value) => value,
308 None => continue,
309 };
310 let Some(details) = details_value.as_mapping() else {
311 continue;
312 };
313
314 let version = mapping_get(details, FIELD_VERSION)
315 .and_then(Value::as_str)
316 .map(|value| truncate_field(value.to_string()));
317 let dependency_kind = mapping_get(details, FIELD_DEPENDENCY)
318 .and_then(Value::as_str)
319 .map(|value| truncate_field(value.to_string()));
320 let (is_runtime, is_optional, is_direct) = classify_lock_dependency(
321 name,
322 dependency_kind.as_deref(),
323 &runtime_reachable,
324 &dev_only_reachable,
325 );
326
327 let is_pinned = version
328 .as_ref()
329 .is_some_and(|value| !value.trim().is_empty());
330
331 let purl = build_dependency_purl(name, version.as_deref()).map(truncate_field);
332 let sha256 = extract_sha256(details).and_then(|h| Sha256Digest::from_hex(&h).ok());
333 let resolved_dependencies = extract_lock_package_dependencies(details);
334 let resolved_package = build_resolved_package(
335 name,
336 &version,
337 sha256,
338 extract_lock_descriptor_extra_data(details),
339 resolved_dependencies,
340 );
341
342 dependencies.push(Dependency {
343 purl,
344 extracted_requirement: version.clone().map(truncate_field),
345 scope: dependency_kind,
346 is_runtime: Some(is_runtime),
347 is_optional: Some(is_optional),
348 is_pinned: Some(is_pinned),
349 is_direct: Some(is_direct),
350 resolved_package: Some(Box::new(resolved_package)),
351 extra_data: extract_lock_descriptor_extra_data(details),
352 });
353 }
354
355 dependencies
356}
357
358fn extract_lock_package_dependencies(details: &Mapping) -> Vec<Dependency> {
359 let mut dependencies = Vec::new();
360
361 let Some(dep_map) = mapping_get(details, FIELD_DEPENDENCIES).and_then(Value::as_mapping) else {
362 return dependencies;
363 };
364
365 for (name_value, requirement_value) in dep_map.iter().take(MAX_ITERATION_COUNT) {
366 let name = match name_value.as_str() {
367 Some(value) => value,
368 None => continue,
369 };
370
371 let requirement = match dependency_requirement_from_value(requirement_value) {
372 Some(value) => value,
373 None => continue,
374 };
375 let is_pinned = is_pubspec_version_pinned(&requirement);
376 let purl = if is_pinned {
377 build_dependency_purl(name, Some(requirement.as_str()))
378 } else {
379 build_dependency_purl(name, None)
380 };
381
382 dependencies.push(Dependency {
383 purl: purl.map(truncate_field),
384 extracted_requirement: Some(truncate_field(requirement)),
385 scope: Some(FIELD_DEPENDENCIES.to_string()),
386 is_runtime: Some(true),
387 is_optional: Some(false),
388 is_pinned: Some(is_pinned),
389 is_direct: Some(false),
390 resolved_package: None,
391 extra_data: None,
392 });
393 }
394
395 dependencies
396}
397
398fn extract_sha256(details: &Mapping) -> Option<String> {
399 let direct = mapping_get(details, FIELD_SHA256)
400 .and_then(Value::as_str)
401 .map(|value| value.to_string());
402
403 if direct.is_some() {
404 return direct;
405 }
406
407 mapping_get(details, FIELD_DESCRIPTION)
408 .and_then(Value::as_mapping)
409 .and_then(|desc_map| mapping_get(desc_map, FIELD_SHA256))
410 .and_then(Value::as_str)
411 .map(|value| value.to_string())
412}
413
414fn build_resolved_package(
415 name: &str,
416 version: &Option<String>,
417 sha256: Option<Sha256Digest>,
418 extra_data: Option<HashMap<String, serde_json::Value>>,
419 dependencies: Vec<Dependency>,
420) -> ResolvedPackage {
421 ResolvedPackage {
422 primary_language: Some("dart".to_string()),
423 download_url: None,
424 sha1: None,
425 sha256,
426 sha512: None,
427 md5: None,
428 is_virtual: true,
429 extra_data,
430 dependencies,
431 repository_homepage_url: None,
432 repository_download_url: None,
433 api_data_url: None,
434 datasource_id: None,
435 purl: None,
436 ..ResolvedPackage::new(
437 PubspecLockParser::PACKAGE_TYPE,
438 String::new(),
439 truncate_field(name.to_string()),
440 truncate_field(version.clone().unwrap_or_default()),
441 )
442 }
443}
444
445fn collect_dependencies(
446 yaml_content: &Value,
447 field: &str,
448 scope: Option<&str>,
449 is_runtime: bool,
450 is_optional: bool,
451) -> Vec<Dependency> {
452 let mut dependencies = Vec::new();
453
454 let Some(dep_map) = yaml_content.get(field).and_then(Value::as_mapping) else {
455 return dependencies;
456 };
457
458 for (name_value, requirement_value) in dep_map.iter().take(MAX_ITERATION_COUNT) {
459 let name = match name_value.as_str() {
460 Some(value) => value,
461 None => continue,
462 };
463 let requirement = dependency_requirement_from_value(requirement_value).map(truncate_field);
464 let is_pinned = requirement
465 .as_deref()
466 .is_some_and(is_pubspec_version_pinned);
467 let purl = if is_pinned {
468 build_dependency_purl(name, requirement.as_deref())
469 } else {
470 build_dependency_purl(name, None)
471 };
472
473 dependencies.push(Dependency {
474 purl: purl.map(truncate_field),
475 extracted_requirement: requirement,
476 scope: scope.map(|value| value.to_string()),
477 is_runtime: Some(is_runtime),
478 is_optional: Some(is_optional),
479 is_pinned: Some(is_pinned),
480 is_direct: Some(true),
481 resolved_package: None,
482 extra_data: extract_manifest_dependency_extra_data(requirement_value),
483 });
484 }
485
486 dependencies
487}
488
489fn dependency_requirement_from_value(value: &Value) -> Option<String> {
490 if let Some(value) = value.as_str() {
491 let trimmed = value.trim();
492 if trimmed.is_empty() {
493 return None;
494 }
495 return Some(trimmed.to_string());
496 }
497
498 if let Some(value) = value.as_i64() {
499 return Some(value.to_string());
500 }
501
502 if let Some(value) = value.as_f64() {
503 return Some(value.to_string());
504 }
505
506 if let Some(map) = value.as_mapping() {
507 return format_dependency_mapping(map, &mut RecursionGuard::depth_only());
508 }
509
510 None
511}
512
513fn format_dependency_mapping(map: &Mapping, guard: &mut RecursionGuard<()>) -> Option<String> {
514 if guard.descend() {
515 warn!("Recursion depth exceeded in format_dependency_mapping");
516 return None;
517 }
518
519 let mut parts = Vec::new();
520
521 for (key, value) in map.iter().take(MAX_ITERATION_COUNT) {
522 let Some(key_str) = key.as_str() else {
523 continue;
524 };
525
526 let value_str = if let Some(value) = value.as_str() {
527 value.to_string()
528 } else if let Some(value) = value.as_i64() {
529 value.to_string()
530 } else if let Some(value) = value.as_f64() {
531 value.to_string()
532 } else if let Some(nested) = value.as_mapping() {
533 format_dependency_mapping(nested, guard)?
534 } else {
535 continue;
536 };
537
538 parts.push(format!("{}: {}", key_str, value_str));
539 }
540
541 guard.ascend();
542
543 if parts.is_empty() {
544 None
545 } else {
546 Some(parts.join(", "))
547 }
548}
549
550fn is_pubspec_version_pinned(version: &str) -> bool {
551 let trimmed = version.trim();
552 if trimmed.is_empty() {
553 return false;
554 }
555
556 trimmed
557 .chars()
558 .all(|character| character.is_ascii_digit() || character == '.')
559}
560
561fn build_purl(name: &str, version: Option<&str>) -> Option<String> {
562 build_purl_with_type(PubspecYamlParser::PACKAGE_TYPE.as_str(), name, version)
563}
564
565fn build_dependency_purl(name: &str, version: Option<&str>) -> Option<String> {
566 build_purl_with_type("pubspec", name, version)
567}
568
569fn build_purl_with_type(package_type: &str, name: &str, version: Option<&str>) -> Option<String> {
570 let mut package_url = match PackageUrl::new(package_type, name) {
571 Ok(purl) => purl,
572 Err(e) => {
573 warn!(
574 "Failed to create PackageUrl for {} dependency '{}': {}",
575 package_type, name, e
576 );
577 return None;
578 }
579 };
580
581 if let Some(version) = version
582 && let Err(e) = package_url.with_version(version)
583 {
584 warn!(
585 "Failed to set version '{}' for {} dependency '{}': {}",
586 version, package_type, name, e
587 );
588 return None;
589 }
590
591 Some(package_url.to_string())
592}
593
594fn extract_string_field(yaml_content: &Value, field: &str) -> Option<String> {
595 yaml_content
596 .get(field)
597 .and_then(Value::as_str)
598 .map(|value| value.trim().to_string())
599 .filter(|value| !value.is_empty())
600}
601
602fn extract_description_field(yaml_content: &Value) -> Option<String> {
603 yaml_content
606 .get(FIELD_DESCRIPTION)
607 .and_then(Value::as_str)
608 .and_then(|value| {
609 let trimmed = value.trim_start();
611 if trimmed.is_empty() {
612 None
613 } else {
614 Some(trimmed.to_string())
615 }
616 })
617}
618
619fn mapping_get<'a>(map: &'a Mapping, key: &str) -> Option<&'a Value> {
620 map.get(Value::String(key.to_string()))
621}
622
623fn default_package_data() -> PackageData {
624 default_package_data_with_type(PubspecYamlParser::PACKAGE_TYPE.as_str())
625}
626
627fn default_package_data_with_type(package_type: &str) -> PackageData {
628 PackageData {
629 package_type: package_type.parse::<PackageType>().ok(),
630 primary_language: Some("dart".to_string()),
631 ..Default::default()
632 }
633}
634
635fn extract_authors(yaml_content: &Value) -> Vec<crate::models::Party> {
636 use crate::models::Party;
637 let mut parties = Vec::new();
638
639 if let Some(author) = extract_string_field(yaml_content, FIELD_AUTHOR).map(truncate_field) {
640 parties.push(Party {
641 r#type: None,
642 role: Some("author".to_string()),
643 name: Some(author),
644 email: None,
645 url: None,
646 organization: None,
647 organization_url: None,
648 timezone: None,
649 });
650 }
651
652 if let Some(authors_value) = yaml_content.get(FIELD_AUTHORS)
653 && let Some(authors_array) = authors_value.as_sequence()
654 {
655 for author_value in authors_array.iter().take(MAX_ITERATION_COUNT) {
656 if let Some(author_str) = author_value.as_str() {
657 parties.push(Party {
658 r#type: None,
659 role: Some("author".to_string()),
660 name: Some(truncate_field(author_str.to_string())),
661 email: None,
662 url: None,
663 organization: None,
664 organization_url: None,
665 timezone: None,
666 });
667 }
668 }
669 }
670
671 parties
672}
673
674fn build_extra_data(
675 yaml_content: &Value,
676) -> Option<std::collections::HashMap<String, serde_json::Value>> {
677 use std::collections::HashMap;
678 let mut extra_data = HashMap::new();
679
680 if let Some(issue_tracker) = extract_string_field(yaml_content, FIELD_ISSUE_TRACKER) {
681 extra_data.insert(
682 FIELD_ISSUE_TRACKER.to_string(),
683 serde_json::Value::String(issue_tracker),
684 );
685 }
686
687 if let Some(documentation) = extract_string_field(yaml_content, FIELD_DOCUMENTATION) {
688 extra_data.insert(
689 FIELD_DOCUMENTATION.to_string(),
690 serde_json::Value::String(documentation),
691 );
692 }
693
694 if let Some(executables) = yaml_content.get(FIELD_EXECUTABLES) {
695 if let Ok(json_value) = serde_json::to_value(executables) {
697 extra_data.insert(FIELD_EXECUTABLES.to_string(), json_value);
698 }
699 }
700
701 if let Some(publish_to) = extract_string_field(yaml_content, FIELD_PUBLISH_TO) {
702 extra_data.insert(
703 FIELD_PUBLISH_TO.to_string(),
704 serde_json::Value::String(publish_to),
705 );
706 }
707
708 for field in [
709 FIELD_PLATFORMS,
710 FIELD_FUNDING,
711 FIELD_FALSE_SECRETS,
712 FIELD_SCREENSHOTS,
713 FIELD_TOPICS,
714 FIELD_IGNORED_ADVISORIES,
715 ] {
716 if let Some(value) = yaml_content.get(field)
717 && let Ok(json_value) = serde_json::to_value(value)
718 {
719 extra_data.insert(field.to_string(), json_value);
720 }
721 }
722
723 if extra_data.is_empty() {
724 None
725 } else {
726 Some(extra_data)
727 }
728}
729
730fn extract_string_list_field(yaml_content: &Value, field: &str) -> Vec<String> {
731 yaml_content
732 .get(field)
733 .and_then(Value::as_sequence)
734 .into_iter()
735 .flatten()
736 .filter_map(Value::as_str)
737 .map(str::trim)
738 .filter(|value| !value.is_empty())
739 .map(|value| truncate_field(value.to_string()))
740 .collect()
741}
742
743fn extract_manifest_dependency_extra_data(
744 requirement_value: &Value,
745) -> Option<HashMap<String, serde_json::Value>> {
746 requirement_value
747 .as_mapping()
748 .and_then(|map| serde_json::to_value(map).ok())
749 .and_then(|json| json.as_object().cloned())
750 .map(|map| map.into_iter().collect())
751}
752
753fn extract_lock_descriptor_extra_data(
754 details: &Mapping,
755) -> Option<HashMap<String, serde_json::Value>> {
756 let mut extra = HashMap::new();
757
758 if let Some(source) = mapping_get(details, "source").and_then(Value::as_str) {
759 extra.insert(
760 "source".to_string(),
761 serde_json::Value::String(source.to_string()),
762 );
763 }
764
765 if let Some(description) = mapping_get(details, FIELD_DESCRIPTION)
766 && let Ok(json_value) = serde_json::to_value(description)
767 {
768 extra.insert("description".to_string(), json_value);
769 }
770
771 if let Some(kind) = mapping_get(details, FIELD_DEPENDENCY).and_then(Value::as_str) {
772 extra.insert(
773 FIELD_DEPENDENCY.to_string(),
774 serde_json::Value::String(kind.to_string()),
775 );
776 }
777
778 (!extra.is_empty()).then_some(extra)
779}
780
781fn reachable_lock_packages(packages: &Mapping, roots: &[&str]) -> HashSet<String> {
782 let mut reachable = HashSet::new();
783 let mut queue = VecDeque::new();
784 let mut iterations = 0usize;
785
786 for (name_value, details_value) in packages.iter().take(MAX_ITERATION_COUNT) {
787 let Some(name) = name_value.as_str() else {
788 continue;
789 };
790 let Some(details) = details_value.as_mapping() else {
791 continue;
792 };
793 let kind = mapping_get(details, FIELD_DEPENDENCY).and_then(Value::as_str);
794 if roots.contains(&kind.unwrap_or_default()) {
795 queue.push_back(truncate_field(name.to_string()));
796 }
797 }
798
799 while let Some(current) = queue.pop_front() {
800 if iterations >= MAX_ITERATION_COUNT {
801 warn!("Iteration count exceeded in reachable_lock_packages BFS");
802 break;
803 }
804 iterations += 1;
805
806 if !reachable.insert(current.clone()) {
807 continue;
808 }
809
810 let Some(details_value) = packages.get(Value::String(current.clone())) else {
811 continue;
812 };
813 let Some(details) = details_value.as_mapping() else {
814 continue;
815 };
816 let Some(dep_map) = mapping_get(details, FIELD_DEPENDENCIES).and_then(Value::as_mapping)
817 else {
818 continue;
819 };
820
821 for dep_name in dep_map
822 .keys()
823 .filter_map(Value::as_str)
824 .take(MAX_ITERATION_COUNT)
825 {
826 queue.push_back(truncate_field(dep_name.to_string()));
827 }
828 }
829
830 reachable
831}
832
833fn classify_lock_dependency(
834 name: &str,
835 dependency_kind: Option<&str>,
836 runtime_reachable: &HashSet<String>,
837 dev_only_reachable: &HashSet<String>,
838) -> (bool, bool, bool) {
839 match dependency_kind {
840 Some("direct main") | Some("direct overridden") => (true, false, true),
841 Some("direct dev") => (false, true, true),
842 Some("transitive") => {
843 if runtime_reachable.contains(name) {
844 (true, false, false)
845 } else if dev_only_reachable.contains(name) {
846 (false, true, false)
847 } else {
848 (true, false, false)
849 }
850 }
851 _ => (true, false, true),
852 }
853}
854
855crate::register_parser!(
856 "Dart pubspec.yaml manifest",
857 &["**/pubspec.yaml", "**/pubspec.lock"],
858 "pub",
859 "Dart",
860 Some("https://dart.dev/tools/pub/pubspec"),
861);