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