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