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