1pub mod cfg;
2mod diags;
3mod graph;
4
5use self::cfg::{ValidBuildConfig, ValidConfig, ValidTreeSkip};
6use crate::{
7 Kid, Krate, Krates, LintLevel,
8 cfg::{PackageSpec, Reason, Span, Spanned},
9 diag::{self, CfgCoord, FileId, KrateCoord},
10};
11use anyhow::Error;
12pub use diags::Code;
13use krates::cm::DependencyKind;
14use semver::VersionReq;
15use std::fmt;
16
17struct ReqMatch<'vr> {
18 specr: &'vr SpecAndReason,
19 index: usize,
20}
21
22pub(crate) struct SpecAndReason {
23 pub(crate) spec: PackageSpec,
24 pub(crate) reason: Option<Reason>,
25 pub(crate) use_instead: Option<Spanned<String>>,
26 pub(crate) file_id: FileId,
27}
28
29#[cfg(test)]
30impl serde::Serialize for SpecAndReason {
31 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
32 where
33 S: serde::Serializer,
34 {
35 use serde::ser::SerializeMap;
36 let mut map = serializer.serialize_map(Some(3))?;
37 map.serialize_entry("spec", &self.spec)?;
38 map.serialize_entry("reason", &self.reason)?;
39 map.serialize_entry("use-instead", &self.use_instead)?;
40 map.end()
41 }
42}
43
44struct SpecsAndReasons(Vec<SpecAndReason>);
45
46impl SpecsAndReasons {
47 #[inline]
49 fn matches<'s>(&'s self, details: &Krate) -> Option<Vec<ReqMatch<'s>>> {
50 let matches: Vec<_> = self
51 .0
52 .iter()
53 .enumerate()
54 .filter_map(|(index, req)| {
55 crate::match_krate(details, &req.spec).then_some(ReqMatch { specr: req, index })
56 })
57 .collect();
58
59 if matches.is_empty() {
60 None
61 } else {
62 Some(matches)
63 }
64 }
65}
66
67struct SkipRoot {
68 specr: SpecAndReason,
69 skip_crates: Vec<Kid>,
70 skip_hits: BitVec,
71}
72
73use bitvec::prelude::*;
74
75struct TreeSkipper {
78 roots: Vec<SkipRoot>,
79}
80
81impl TreeSkipper {
82 fn build(skip_roots: Vec<ValidTreeSkip>, krates: &Krates, cfg_file_id: FileId) -> (Self, Pack) {
83 let mut roots = Vec::with_capacity(skip_roots.len());
84
85 let mut pack = Pack::new(Check::Bans);
86
87 for ts in skip_roots {
88 let num_roots = roots.len();
89
90 for nid in krates.krates_by_name(&ts.spec.name.value).filter_map(|km| {
91 crate::match_req(&km.krate.version, ts.spec.version_req.as_ref())
92 .then_some(km.node_id)
93 }) {
94 roots.push(Self::build_skip_root(ts.clone(), cfg_file_id, nid, krates));
95 }
96
97 if roots.len() == num_roots {
100 pack.push(diags::UnmatchedSkipRoot {
101 skip_root_cfg: CfgCoord {
102 file: cfg_file_id,
103 span: ts.spec.name.span,
104 },
105 });
106 }
107 }
108
109 (Self { roots }, pack)
110 }
111
112 fn build_skip_root(
113 ts: ValidTreeSkip,
114 file_id: FileId,
115 krate_id: krates::NodeId,
116 krates: &Krates,
117 ) -> SkipRoot {
118 let (max_depth, reason) = ts.inner.map_or((usize::MAX, None), |inn| {
119 (inn.depth.unwrap_or(usize::MAX), inn.reason)
120 });
121
122 let mut skip_crates = Vec::with_capacity(10);
123
124 let graph = krates.graph();
125
126 let mut pending = vec![(krate_id, 1)];
127 while let Some((node_id, depth)) = pending.pop() {
128 let pkg_id = if let krates::Node::Krate { id, .. } = &graph[node_id] {
129 id
130 } else {
131 continue;
132 };
133 if let Err(i) = skip_crates.binary_search(pkg_id) {
134 skip_crates.insert(i, pkg_id.clone());
135
136 if depth < max_depth {
137 for dep in krates.direct_dependencies(node_id) {
138 pending.push((dep.node_id, depth + 1));
139 }
140 }
141 }
142 }
143
144 let skip_hits = BitVec::repeat(false, skip_crates.len());
145
146 SkipRoot {
147 specr: SpecAndReason {
148 spec: ts.spec,
149 reason,
150 use_instead: None,
151 file_id,
152 },
153 skip_crates,
154 skip_hits,
155 }
156 }
157
158 fn matches(&mut self, krate: &Krate, pack: &mut Pack) -> bool {
159 let mut skip = false;
160
161 for root in &mut self.roots {
162 if let Ok(i) = root.skip_crates.binary_search(&krate.id) {
163 pack.push(diags::SkippedByRoot {
164 krate,
165 skip_root_cfg: &root.specr,
166 });
167
168 root.skip_hits.as_mut_bitslice().set(i, true);
169 skip = true;
170 }
171 }
172
173 skip
174 }
175}
176
177pub struct DupGraph {
178 pub duplicate: String,
179 pub graph: String,
180}
181
182impl fmt::Debug for DupGraph {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 f.write_str(&self.graph)
185 }
186}
187
188pub type OutputGraph = dyn Fn(DupGraph) -> Result<(), Error> + Send + Sync;
189
190use crate::diag::{Check, Diag, Pack, Severity};
191
192pub fn check(
193 ctx: crate::CheckCtx<'_, ValidConfig>,
194 output_graph: Option<Box<OutputGraph>>,
195 sink: impl Into<diag::ErrorSink>,
196) {
197 let ValidConfig {
198 file_id,
199 denied,
200 denied_multiple_versions,
201 allowed,
202 allow_workspace,
203 features,
204 workspace_default_features,
205 external_default_features,
206 skipped,
207 multiple_versions,
208 multiple_versions_include_dev,
209 workspace_dependencies,
210 highlight,
211 tree_skipped,
212 wildcards,
213 allow_wildcard_paths,
214 build,
215 } = ctx.cfg;
216
217 let mut sink = sink.into();
218 let krate_spans = &ctx.krate_spans;
219 let (mut tree_skipper, build_diags) = TreeSkipper::build(tree_skipped, ctx.krates, file_id);
220
221 if !build_diags.is_empty() {
222 sink.push(build_diags);
223 }
224
225 use std::collections::BTreeMap;
226
227 struct BanWrappers {
228 map: BTreeMap<usize, (usize, Vec<Spanned<String>>)>,
229 hits: BitVec,
230 }
231
232 impl BanWrappers {
233 fn new(mut map: BTreeMap<usize, (usize, Vec<Spanned<String>>)>) -> Self {
234 let hits = BitVec::repeat(
235 false,
236 map.values_mut().fold(0, |sum, v| {
237 v.0 = sum;
238 sum + v.1.len()
239 }),
240 );
241
242 Self { map, hits }
243 }
244
245 #[inline]
246 fn has_wrappers(&self, i: usize) -> bool {
247 self.map.contains_key(&i)
248 }
249
250 #[inline]
251 fn check(&mut self, i: usize, name: &str) -> Option<Span> {
252 let (offset, wrappers) = &self.map[&i];
253 if let Some(pos) = wrappers.iter().position(|wrapper| wrapper.value == name) {
254 self.hits.set(*offset + pos, true);
255 Some(wrappers[pos].span)
256 } else {
257 None
258 }
259 }
260 }
261
262 let (denied_ids, mut ban_wrappers) = {
263 let mut bw = BTreeMap::new();
264
265 (
266 SpecsAndReasons(
267 denied
268 .into_iter()
269 .enumerate()
270 .map(|(i, kb)| {
271 let (reason, use_instead) = if let Some(ext) = kb.inner {
272 if let Some(wrappers) = ext.wrappers.filter(|w| !w.is_empty()) {
273 bw.insert(i, (0, wrappers));
274 }
275
276 (ext.reason, ext.use_instead)
277 } else {
278 (None, None)
279 };
280
281 SpecAndReason {
282 spec: kb.spec,
283 reason,
284 use_instead,
285 file_id,
286 }
287 })
288 .collect(),
289 ),
290 BanWrappers::new(bw),
291 )
292 };
293
294 let (feature_ids, features): (Vec<_>, Vec<_>) = features
295 .into_iter()
296 .map(|cf| {
297 (
298 SpecAndReason {
299 spec: cf.spec,
300 reason: cf.reason,
301 use_instead: None,
302 file_id,
303 },
304 cf.features,
305 )
306 })
307 .unzip();
308
309 let feature_ids = SpecsAndReasons(feature_ids);
310
311 let mut skip_hit: BitVec = BitVec::repeat(false, skipped.len());
315
316 struct MultiDetector<'a> {
317 name: &'a str,
318 dupes: smallvec::SmallVec<[(usize, bool); 4]>,
319 krates_with_dupes: Vec<&'a str>,
322 }
323
324 let mut multi_detector = MultiDetector {
325 name: &ctx.krates.krates().next().unwrap().name,
326 dupes: smallvec::SmallVec::new(),
327 krates_with_dupes: Vec::new(),
328 };
329
330 let filtered_krates = if !multiple_versions_include_dev {
331 ctx.krates.krates_filtered(krates::DepKind::Dev)
332 } else {
333 Vec::new()
334 };
335
336 let should_add_dupe = move |kid| {
339 if multiple_versions_include_dev {
340 true
341 } else {
342 filtered_krates
343 .binary_search_by(|krate| krate.id.cmp(kid))
344 .is_ok()
345 }
346 };
347
348 let dmv = SpecsAndReasons(
349 denied_multiple_versions
350 .into_iter()
351 .map(|spec| SpecAndReason {
352 spec,
353 reason: None,
354 use_instead: None,
355 file_id,
356 })
357 .collect(),
358 );
359
360 let allowed = SpecsAndReasons(
361 allowed
362 .into_iter()
363 .map(|all| SpecAndReason {
364 spec: all.spec,
365 reason: all.inner,
366 use_instead: None,
367 file_id,
368 })
369 .collect(),
370 );
371
372 let skipped = SpecsAndReasons(
373 skipped
374 .into_iter()
375 .map(|skip| SpecAndReason {
376 spec: skip.spec,
377 reason: skip.inner,
378 use_instead: None,
379 file_id,
380 })
381 .collect(),
382 );
383
384 let workspace_members: std::collections::HashSet<Kid> = if allow_workspace {
386 let members: std::collections::HashSet<Kid> = ctx
387 .krates
388 .workspace_members()
389 .filter_map(|node| {
390 if let krates::Node::Krate { id, .. } = node {
391 Some(id.clone())
392 } else {
393 None
394 }
395 })
396 .collect();
397
398 members
399 } else {
400 std::collections::HashSet::new()
401 };
402
403 let report_duplicates = |multi_detector: &mut MultiDetector<'_>, sink: &mut diag::ErrorSink| {
404 if multi_detector.dupes.len() != 1 {
405 multi_detector.dupes.retain(|(index, _)| {
407 let krate = &ctx.krates[*index];
408
409 let direct = ctx
415 .krates
416 .direct_dependents(ctx.krates.nid_for_kid(&krate.id).unwrap());
417
418 let res = !direct.iter().all(|dir| dir.krate.name == krate.name);
419
420 if !res {
421 log::debug!("ignoring duplicate crate '{krate}', its only dependents was another version of itself");
422 }
423
424 res
425 });
426 }
427
428 let skipped = multi_detector
429 .dupes
430 .iter()
431 .filter(|(_, skipped)| *skipped)
432 .count();
433 if multi_detector.dupes.len() > 1 {
434 multi_detector.krates_with_dupes.push(multi_detector.name);
435 }
436
437 if multi_detector.dupes.len() - skipped <= 1 {
438 return;
439 }
440
441 let lint_level = if multi_detector.dupes.iter().any(|(kindex, skipped)| {
442 if *skipped {
443 return false;
444 }
445
446 let krate = &ctx.krates[*kindex];
447 dmv.matches(krate).is_some()
448 }) {
449 LintLevel::Deny
450 } else {
451 multiple_versions
452 };
453
454 let severity = match lint_level {
455 LintLevel::Warn => Severity::Warning,
456 LintLevel::Deny => Severity::Error,
457 LintLevel::Allow => return,
458 };
459
460 let mut all_start = usize::MAX;
461 let mut all_end = 0;
462
463 struct Dupe {
464 id: Kid,
466 version: semver::Version,
468 }
469
470 let mut kids = smallvec::SmallVec::<[Dupe; 2]>::new();
471
472 for dup in multi_detector
473 .dupes
474 .iter()
475 .filter_map(|(ind, skipped)| (!*skipped).then_some(*ind))
476 {
477 let krate = &ctx.krates[dup];
478
479 let span = &ctx.krate_spans.lock_span(&krate.id).total;
480 all_start = all_start.min(span.start);
481 all_end = all_end.max(span.end);
482
483 if let Err(i) = kids.binary_search_by(|other| match other.version.cmp(&krate.version) {
484 std::cmp::Ordering::Equal => other.id.cmp(&krate.id),
485 ord => ord,
486 }) {
487 kids.insert(
488 i,
489 Dupe {
490 id: krate.id.clone(),
491 version: krate.version.clone(),
492 },
493 );
494 }
495 }
496
497 {
498 let mut diag: Diag = diags::Duplicates {
499 krate_name: multi_detector.name,
500 num_dupes: kids.len(),
501 krates_coord: KrateCoord {
502 file: krate_spans.lock_id,
503 span: (all_start..all_end).into(),
504 },
505 severity,
506 }
507 .into();
508
509 diag.graph_nodes = kids
510 .into_iter()
511 .map(|dupe| crate::diag::GraphNode {
512 kid: dupe.id,
513 feature: None,
514 })
515 .collect();
516
517 let mut pack = Pack::new(Check::Bans);
518 pack.push(diag);
519
520 sink.push(pack);
521 }
522
523 if let Some(og) = &output_graph {
524 match graph::create_graph(
525 multi_detector.name,
526 highlight,
527 ctx.krates,
528 &multi_detector.dupes,
529 ) {
530 Ok(graph) => {
531 if let Err(err) = og(DupGraph {
532 duplicate: multi_detector.name.to_owned(),
533 graph,
534 }) {
535 log::error!("{err}");
536 }
537 }
538 Err(err) => {
539 log::error!("unable to create graph for {}: {err}", multi_detector.name);
540 }
541 };
542 }
543 };
544
545 enum Sink<'k> {
546 Build(crossbeam::channel::Sender<(usize, &'k Krate, Pack)>),
547 NoBuild(diag::ErrorSink),
548 }
549
550 impl<'k> Sink<'k> {
551 #[inline]
552 fn push(&mut self, index: usize, krate: &'k Krate, pack: Pack) {
553 match self {
554 Self::Build(tx) => tx.send((index, krate, pack)).unwrap(),
555 Self::NoBuild(sink) => {
556 if !pack.is_empty() {
557 sink.push(pack);
558 }
559 }
560 }
561 }
562 }
563
564 let (mut tx, build_config) = if let Some(bc) = build {
565 let (tx, rx) = crossbeam::channel::unbounded();
566
567 (Sink::Build(tx), Some((bc, rx)))
568 } else {
569 (Sink::NoBuild(sink.clone()), None)
570 };
571
572 struct BuildCheckCtx {
573 bypasses: parking_lot::Mutex<BitVec>,
574 diag_packs: parking_lot::Mutex<std::collections::BTreeMap<usize, Pack>>,
575 cargo_home: Option<crate::PathBuf>,
576 build_config: ValidBuildConfig,
577 }
578
579 let build_check_ctx = build_config.map(|(build_config, rx)| {
580 let cargo_home = home::cargo_home()
582 .map_err(|err| {
583 log::error!("unable to locate $CARGO_HOME: {err}");
584 err
585 })
586 .ok()
587 .and_then(|pb| {
588 crate::PathBuf::from_path_buf(pb)
589 .map_err(|pb| {
590 log::error!("$CARGO_HOME path '{}' is not utf-8", pb.display());
591 })
592 .ok()
593 });
594
595 let bypasses =
598 parking_lot::Mutex::<BitVec>::new(BitVec::repeat(false, build_config.bypass.len()));
599
600 (
601 BuildCheckCtx {
602 cargo_home,
603 bypasses,
604 diag_packs: parking_lot::Mutex::new(std::collections::BTreeMap::new()),
605 build_config,
606 },
607 rx,
608 )
609 });
610
611 let mut ws_duplicate_packs = Vec::new();
612
613 rayon::scope(|scope| {
614 scope.spawn(|_| {
615 let last = ctx.krates.len() - 1;
616
617 for (i, krate) in ctx.krates.krates().enumerate() {
618 let mut pack = Pack::with_kid(Check::Bans, krate.id.clone());
619
620 if let Some(matches) = denied_ids.matches(krate) {
622 let is_workspace_member = workspace_members.contains(&krate.id);
623
624 for rm in matches {
625 let ban_cfg = CfgCoord {
626 file: file_id,
627 span: rm.specr.spec.name.span,
628 };
629
630 if is_workspace_member && allow_workspace {
633 continue;
634 }
635
636 let is_allowed_by_wrapper = if ban_wrappers.has_wrappers(rm.index) {
639 let nid = ctx.krates.nid_for_kid(&krate.id).unwrap();
640
641 let mut all = true;
646 for src in ctx.krates.direct_dependents(nid) {
647 let (diag, is_allowed): (Diag, _) =
648 match ban_wrappers.check(rm.index, &src.krate.name) {
649 Some(span) => (
650 diags::BannedAllowedByWrapper {
651 ban_cfg: ban_cfg.clone(),
652 ban_exception_cfg: CfgCoord {
653 file: file_id,
654 span,
655 },
656 banned_krate: krate,
657 wrapper_krate: src.krate,
658 }
659 .into(),
660 true,
661 ),
662 None => (
663 diags::BannedUnmatchedWrapper {
664 ban_cfg: rm.specr,
665 banned_krate: krate,
666 parent_krate: src.krate,
667 }
668 .into(),
669 false,
670 ),
671 };
672
673 pack.push(diag);
674 all = all && is_allowed;
675 }
676
677 all
678 } else {
679 false
680 };
681
682 if !is_allowed_by_wrapper {
683 pack.push(diags::ExplicitlyBanned {
684 krate,
685 ban_cfg: rm.specr,
686 });
687 }
688 }
689 }
690
691 if !allowed.0.is_empty() {
692 let is_workspace_member = workspace_members.contains(&krate.id);
695
696 match allowed.matches(krate) {
697 Some(matches) => {
698 for rm in matches {
699 pack.push(diags::ExplicitlyAllowed {
700 krate,
701 allow_cfg: rm.specr,
702 });
703 }
704 }
705 None => {
706 if is_workspace_member && allow_workspace {
709 } else {
711 pack.push(diags::NotAllowed { krate });
712 }
713 }
714 }
715 }
716
717 let enabled_features = ctx.krates.get_enabled_features(&krate.id).unwrap();
718
719 let default_lint_level = if enabled_features.contains("default") {
720 if ctx.krates.workspace_members().any(|n| {
721 if let krates::Node::Krate { id, .. } = n {
722 id == &krate.id
723 } else {
724 false
725 }
726 }) {
727 workspace_default_features.as_ref()
728 } else {
729 external_default_features.as_ref()
730 }
731 } else {
732 None
733 };
734
735 if let Some(ll) = default_lint_level
736 && ll.value == LintLevel::Warn
737 {
738 pack.push(diags::DefaultFeatureEnabled {
739 krate,
740 level: ll,
741 file_id,
742 });
743 }
744
745 if let Some(matches) = feature_ids.matches(krate) {
747 for rm in matches {
748 let feature_bans = &features[rm.index];
749
750 let feature_set_allowed = {
751 let not_explicitly_allowed: Vec<_> = enabled_features
753 .iter()
754 .filter_map(|ef| {
755 if !feature_bans.allow.value.iter().any(|af| &af.value == ef) {
756 if ef == "default"
757 && let Some(ll) = default_lint_level
758 && ll.value != LintLevel::Deny
759 {
760 return None;
761 }
762
763 Some(ef.as_str())
764 } else {
765 None
766 }
767 })
768 .collect();
769
770 if feature_bans.exact.value {
771 let missing_allowed: Vec<_> = feature_bans
773 .allow
774 .value
775 .iter()
776 .filter_map(|af| {
777 if !enabled_features.contains(&af.value) {
778 Some(CfgCoord {
779 file: file_id,
780 span: af.span,
781 })
782 } else {
783 None
784 }
785 })
786 .collect();
787
788 if missing_allowed.is_empty() && not_explicitly_allowed.is_empty() {
789 true
790 } else {
791 pack.push(diags::ExactFeaturesMismatch {
792 missing_allowed,
793 not_allowed: ¬_explicitly_allowed,
794 exact_coord: CfgCoord {
795 file: file_id,
796 span: feature_bans.exact.span,
797 },
798 krate,
799 });
800 false
801 }
802 } else {
803 let diag_count = pack.len();
806
807 if !feature_bans.allow.value.is_empty() {
810 for feature in ¬_explicitly_allowed {
811 fn has_feature(
816 map: &std::collections::BTreeMap<String, Vec<String>>,
817 parent: &str,
818 feature: &str,
819 ) -> bool {
820 if let Some(parent) = map.get(parent) {
821 parent.iter().any(|f| {
822 let pf =
823 krates::ParsedFeature::from(f.as_str());
824
825 if let krates::Feature::Simple(feat) = pf.feat()
826 {
827 if feat == feature {
828 true
829 } else {
830 has_feature(map, feat, feature)
831 }
832 } else {
833 false
834 }
835 })
836 } else {
837 false
838 }
839 }
840
841 if !feature_bans.allow.value.iter().any(|allowed| {
842 has_feature(
843 &krate.features,
844 allowed.value.as_str(),
845 feature,
846 )
847 }) {
848 pack.push(diags::FeatureNotExplicitlyAllowed {
849 krate,
850 feature,
851 allowed: CfgCoord {
852 file: file_id,
853 span: feature_bans.allow.span,
854 },
855 });
856 }
857 }
858 }
859
860 if let Some(ll) = default_lint_level
866 && ll.value == LintLevel::Deny
867 && !feature_bans
868 .allow
869 .value
870 .iter()
871 .any(|d| d.value == "default")
872 && !feature_bans.deny.iter().any(|d| d.value == "default")
873 {
874 pack.push(diags::DefaultFeatureEnabled {
875 krate,
876 level: ll,
877 file_id,
878 });
879 }
880
881 for feature in feature_bans
882 .deny
883 .iter()
884 .filter(|feat| enabled_features.contains(&feat.value))
885 {
886 pack.push(diags::FeatureBanned {
887 krate,
888 feature,
889 file_id,
890 });
891 }
892
893 diag_count <= pack.len()
894 }
895 };
896
897 if feature_set_allowed {
904 for feature in feature_bans
905 .allow
906 .value
907 .iter()
908 .chain(feature_bans.deny.iter())
909 {
910 if !krate.features.contains_key(&feature.value) {
911 pack.push(diags::UnknownFeature {
912 krate,
913 feature,
914 file_id,
915 });
916 }
917 }
918 }
919 }
920 } else if let Some(ll) = default_lint_level
921 && ll.value == LintLevel::Deny
922 {
923 pack.push(diags::DefaultFeatureEnabled {
924 krate,
925 level: ll,
926 file_id,
927 });
928 }
929
930 if should_add_dupe(&krate.id) {
931 if let Some(matches) = skipped.matches(krate) {
932 if multi_detector.name != krate.name {
933 report_duplicates(&mut multi_detector, &mut sink);
934
935 multi_detector.name = &krate.name;
936 multi_detector.dupes.clear();
937 }
938
939 multi_detector.dupes.push((i, true));
940
941 for rm in matches {
942 pack.push(diags::Skipped {
943 krate,
944 skip_cfg: rm.specr,
945 });
946
947 skip_hit.as_mut_bitslice().set(rm.index, true);
951 }
952 } else if !tree_skipper.matches(krate, &mut pack) {
953 if multi_detector.name != krate.name {
954 report_duplicates(&mut multi_detector, &mut sink);
955
956 multi_detector.name = &krate.name;
957 multi_detector.dupes.clear();
958 }
959
960 multi_detector.dupes.push((i, false));
961
962 'wildcards: {
963 if wildcards != LintLevel::Allow && !krate.is_git_source() {
964 let severity = match wildcards {
965 LintLevel::Warn => Severity::Warning,
966 LintLevel::Deny => Severity::Error,
967 LintLevel::Allow => unreachable!(),
968 };
969
970 let Some(manifest) = ctx.krate_spans.manifest(&krate.id) else {
971 break 'wildcards;
972 };
973 let is_private = krate.is_private(&[]);
974 let mut labels = Vec::new();
975 let mut pack = Pack::with_kid(Check::Bans, krate.id.clone());
976
977 for mdep in manifest.deps(false) {
978 if mdep.dep.req != VersionReq::STAR {
979 continue;
980 }
981
982 if allow_wildcard_paths
985 && !mdep.krate.is_registry()
986 && (is_private
987 || mdep.dep.kind == DependencyKind::Development)
988 {
989 continue;
990 }
991
992 labels.push(
993 crate::diag::Label::primary(
994 manifest.id,
995 mdep.version_req
996 .as_ref()
997 .map_or(mdep.value_span, |vr| vr.span),
998 )
999 .with_message("wildcard dependency"),
1000 );
1001
1002 if let Some(workspace) = &mdep.workspace {
1005 if !workspace.value {
1006 continue;
1007 }
1008
1009 if let Some(ws_dep) =
1010 ctx.krate_spans.workspace_span(&krate.id)
1011 {
1012 labels.push(
1013 crate::diag::Label::secondary(
1014 ctx.krate_spans.workspace_id.unwrap(),
1015 ws_dep
1016 .version
1017 .as_ref()
1018 .map_or(ws_dep.value, |vr| vr.span),
1019 )
1020 .with_message("workspace dependency"),
1021 );
1022 } else {
1023 pack.push(diags::UnresolveWorkspaceDependency {
1026 manifest,
1027 dep: mdep,
1028 });
1029 }
1030 }
1031 }
1032
1033 sink.push(pack);
1034
1035 if !labels.is_empty() {
1036 sink.push(diags::Wildcards {
1037 krate,
1038 severity,
1039 labels,
1040 allow_wildcard_paths,
1041 });
1042 }
1043 }
1044 }
1045 }
1046 }
1047
1048 if i == last {
1049 report_duplicates(&mut multi_detector, &mut sink);
1050 }
1051
1052 tx.push(i, krate, pack);
1053 }
1054
1055 drop(tx);
1056 });
1057
1058 scope.spawn(|scope| {
1059 let Some((build_ctx, rx)) = &build_check_ctx else {
1060 return;
1061 };
1062 while let Ok((index, krate, mut pack)) = rx.recv() {
1063 scope.spawn(move |_s| {
1064 if let Some(bcc) = check_build(
1065 ctx.cfg.file_id,
1066 &build_ctx.build_config,
1067 build_ctx.cargo_home.as_deref(),
1068 krate,
1069 ctx.krates,
1070 &mut pack,
1071 ) {
1072 build_ctx.bypasses.lock().set(bcc, true);
1073 }
1074
1075 if !pack.is_empty() {
1076 build_ctx.diag_packs.lock().insert(index, pack);
1077 }
1078 });
1079 }
1080 });
1081
1082 if let Some(ws_deps) = &workspace_dependencies
1085 && ws_deps.duplicates != LintLevel::Allow
1086 {
1087 scope.spawn(|_| {
1088 check_workspace_duplicates(
1089 ctx.krates,
1090 ctx.krate_spans,
1091 ws_deps,
1092 &mut ws_duplicate_packs,
1093 );
1094 });
1095 }
1096 });
1097
1098 if let Some((bcc, _)) = build_check_ctx {
1099 for bp in bcc.diag_packs.into_inner().into_values() {
1100 sink.push(bp);
1101 }
1102
1103 let mut pack = Pack::new(Check::Bans);
1104 for ve in bcc
1105 .bypasses
1106 .into_inner()
1107 .into_iter()
1108 .zip(bcc.build_config.bypass.into_iter())
1109 .filter_map(|(hit, ve)| if !hit { Some(ve) } else { None })
1110 {
1111 pack.push(diags::UnmatchedBypass {
1112 unmatched: &ve,
1113 file_id,
1114 });
1115 }
1116
1117 sink.push(pack);
1118 }
1119
1120 for pack in ws_duplicate_packs {
1121 sink.push(pack);
1122 }
1123
1124 if let Some(ws_deps) = workspace_dependencies
1125 && ws_deps.unused != LintLevel::Allow
1126 && let Some(id) = krate_spans
1127 .workspace_id
1128 .filter(|_id| !krate_spans.unused_workspace_deps.is_empty())
1129 {
1130 sink.push(diags::UnusedWorkspaceDependencies {
1131 id,
1132 unused: &krate_spans.unused_workspace_deps,
1133 level: ws_deps.unused,
1134 });
1135 }
1136
1137 let mut pack = Pack::new(Check::Bans);
1138
1139 for (hit, skip) in skip_hit.into_iter().zip(skipped.0.into_iter()) {
1140 if !hit {
1141 pack.push(diags::UnmatchedSkip { skip_cfg: &skip });
1142 } else if multi_detector
1143 .krates_with_dupes
1144 .binary_search(&skip.spec.name.value.as_str())
1145 .is_err()
1146 {
1147 pack.push(diags::UnnecessarySkip { skip_cfg: &skip });
1148 }
1149 }
1150
1151 for wrapper in ban_wrappers
1152 .hits
1153 .into_iter()
1154 .zip(ban_wrappers.map.into_values().flat_map(|(_, w)| w))
1155 .filter_map(|(hit, wrapper)| (!hit).then_some(wrapper))
1156 {
1157 pack.push(diags::UnusedWrapper {
1158 wrapper_cfg: CfgCoord {
1159 file: file_id,
1160 span: wrapper.span,
1161 },
1162 });
1163 }
1164
1165 sink.push(pack);
1166}
1167
1168pub fn check_build(
1169 file_id: FileId,
1170 config: &ValidBuildConfig,
1171 home: Option<&crate::Path>,
1172 krate: &Krate,
1173 krates: &Krates,
1174 pack: &mut Pack,
1175) -> Option<usize> {
1176 use krates::cm::TargetKind;
1177
1178 let build_script_allowed = if let Some(allow_build_scripts) = &config.allow_build_scripts {
1179 let has_build_script = krate
1180 .targets
1181 .iter()
1182 .any(|t| t.kind.contains(&TargetKind::CustomBuild));
1183
1184 !has_build_script
1185 || allow_build_scripts
1186 .iter()
1187 .any(|id| crate::match_krate(krate, id))
1188 } else {
1189 true
1190 };
1191
1192 if build_script_allowed && config.executables == LintLevel::Allow {
1193 return None;
1194 }
1195
1196 #[inline]
1197 fn executes_at_buildtime(krate: &Krate) -> bool {
1198 krate.targets.iter().any(|t| {
1199 t.kind
1200 .iter()
1201 .any(|k| matches!(*k, TargetKind::CustomBuild | TargetKind::ProcMacro))
1202 })
1203 }
1204
1205 fn needs_checking(krate: krates::NodeId, krates: &Krates) -> bool {
1206 if executes_at_buildtime(&krates[krate]) {
1207 return true;
1208 }
1209
1210 for dd in krates.direct_dependents(krate) {
1211 if needs_checking(dd.node_id, krates) {
1212 return true;
1213 }
1214 }
1215
1216 false
1217 }
1218
1219 if !config.include_workspace
1222 && krates.workspace_members().any(|n| {
1223 if let krates::Node::Krate { id, .. } = n {
1224 id == &krate.id
1225 } else {
1226 false
1227 }
1228 })
1229 || (!config.include_dependencies && !executes_at_buildtime(krate))
1230 || (config.include_dependencies
1231 && !needs_checking(krates.nid_for_kid(&krate.id).unwrap(), krates))
1232 {
1233 return None;
1234 }
1235
1236 let (kc_index, krate_config) = config
1237 .bypass
1238 .iter()
1239 .enumerate()
1240 .find_map(|(i, ae)| crate::match_krate(krate, &ae.spec).then_some((i, ae)))
1241 .unzip();
1242
1243 if let Some(kc) = krate_config
1246 && let Some(bsc) = &kc.build_script
1247 && let Some(path) = krate
1248 .targets
1249 .iter()
1250 .find_map(|t| (t.name == "build-script-build").then_some(&t.src_path))
1251 {
1252 let root = &krate.manifest_path.parent().unwrap();
1253 match validate_file_checksum(path, &bsc.value) {
1254 Ok(_) => {
1255 pack.push(diags::ChecksumMatch {
1256 path: diags::HomePath { path, root, home },
1257 checksum: bsc,
1258 severity: None,
1259 file_id,
1260 });
1261
1262 for rfeat in &kc.required_features {
1264 if !krate.features.contains_key(&rfeat.value) {
1265 pack.push(diags::UnknownFeature {
1266 krate,
1267 feature: rfeat,
1268 file_id,
1269 });
1270 }
1271 }
1272
1273 let enabled = krates.get_enabled_features(&krate.id).unwrap();
1274
1275 let enabled_features: Vec<_> = kc
1276 .required_features
1277 .iter()
1278 .filter(|f| enabled.contains(&f.value))
1279 .collect();
1280
1281 if enabled_features.is_empty() {
1284 return kc_index;
1285 }
1286
1287 pack.push(diags::FeaturesEnabled {
1288 enabled_features,
1289 file_id,
1290 });
1291 }
1292 Err(err) => {
1293 pack.push(diags::ChecksumMismatch {
1294 path: diags::HomePath { path, root, home },
1295 checksum: bsc,
1296 severity: Some(Severity::Warning),
1297 error: format!("build script failed checksum: {err:#}"),
1298 file_id,
1299 });
1300 }
1301 }
1302 }
1303
1304 if !build_script_allowed {
1305 pack.push(diags::BuildScriptNotAllowed { krate });
1306 return kc_index;
1307 }
1308
1309 let root = krate.manifest_path.parent().unwrap();
1310
1311 let (tx, rx) = crossbeam::channel::unbounded();
1312
1313 let (_, checksum_diags) = rayon::join(
1314 || {
1315 let mut matches = Vec::new();
1317 let is_git_src = krate.is_git_source();
1318
1319 let mut allow_hit: BitVec =
1320 BitVec::repeat(false, krate_config.map_or(0, |kc| kc.allow.len()));
1321 let mut glob_hit: BitVec = BitVec::repeat(
1322 false,
1323 krate_config.map_or(0, |kc| {
1324 kc.allow_globs.as_ref().map_or(0, |ag| ag.patterns.len())
1325 }),
1326 );
1327
1328 for entry in walkdir::WalkDir::new(root)
1329 .sort_by_file_name()
1330 .into_iter()
1331 .filter_entry(|entry| {
1332 !is_git_src
1336 || (entry.path().file_name() == Some(std::ffi::OsStr::new(".git"))
1337 && entry.path().parent() == Some(root.as_std_path()))
1338 })
1339 {
1340 let Ok(entry) = entry else {
1341 continue;
1342 };
1343
1344 if entry.file_type().is_dir() {
1345 continue;
1346 }
1347
1348 let absolute_path = match crate::PathBuf::from_path_buf(entry.into_path()) {
1349 Ok(p) => p,
1350 Err(path) => {
1351 pack.push(diags::NonUtf8Path { path: &path });
1352 continue;
1353 }
1354 };
1355
1356 let path = &absolute_path;
1357
1358 let Ok(rel_path) = path.strip_prefix(root) else {
1359 pack.push(diags::NonRootPath { path, root });
1360 continue;
1361 };
1362
1363 let candidate = globset::Candidate::new(rel_path);
1364
1365 if let Some(kc) = krate_config {
1366 let ae = kc
1369 .allow
1370 .binary_search_by(|ae| ae.path.value.as_path().cmp(rel_path))
1371 .ok()
1372 .map(|i| {
1373 allow_hit.set(i, true);
1374 &kc.allow[i]
1375 });
1376
1377 if let Some(ae) = ae
1378 && ae.checksum.is_none()
1379 {
1380 pack.push(diags::ExplicitPathAllowance {
1381 allowed: ae,
1382 file_id,
1383 });
1384 continue;
1385 }
1386
1387 if let Some(ag) = &kc.allow_globs
1389 && let Some(globs) = ag.matches(&candidate, &mut matches)
1390 {
1391 for &i in &matches {
1392 glob_hit.set(i, true);
1393 }
1394
1395 pack.push(diags::GlobAllowance {
1396 path: diags::HomePath { path, root, home },
1397 globs,
1398 file_id,
1399 });
1400 continue;
1401 }
1402
1403 if let Some(checksum) = ae.as_ref().and_then(|ae| ae.checksum.as_ref()) {
1406 let _ = tx.send((absolute_path, checksum));
1407 continue;
1408 }
1409 }
1410
1411 if let Some(globs) = config.script_extensions.matches(&candidate, &mut matches) {
1413 pack.push(diags::DeniedByExtension {
1414 path: diags::HomePath { path, root, home },
1415 globs,
1416 file_id,
1417 });
1418 continue;
1419 }
1420
1421 let diag: Diag = match check_is_executable(path, !config.include_archives) {
1424 Ok(None) => continue,
1425 Ok(Some(exe_kind)) => diags::DetectedExecutable {
1426 path: diags::HomePath { path, root, home },
1427 interpreted: config.interpreted,
1428 exe_kind,
1429 }
1430 .into(),
1431 Err(error) => diags::UnableToCheckPath {
1432 path: diags::HomePath { path, root, home },
1433 error,
1434 }
1435 .into(),
1436 };
1437
1438 pack.push(diag);
1439 }
1440
1441 if let Some(ae) = krate_config.map(|kc| &kc.allow) {
1442 for ae in allow_hit
1443 .into_iter()
1444 .zip(ae.iter())
1445 .filter_map(|(hit, ae)| if !hit { Some(ae) } else { None })
1446 {
1447 pack.push(diags::UnmatchedPathBypass {
1448 unmatched: ae,
1449 file_id,
1450 });
1451 }
1452 }
1453
1454 if let Some(vgs) = krate_config.and_then(|kc| kc.allow_globs.as_ref()) {
1455 for gp in glob_hit
1456 .into_iter()
1457 .zip(vgs.patterns.iter())
1458 .filter_map(|(hit, gp)| {
1459 if !hit && let cfg::GlobPattern::User(gp) = gp {
1460 return Some(gp);
1461 }
1462
1463 None
1464 })
1465 {
1466 pack.push(diags::UnmatchedGlob {
1467 unmatched: gp,
1468 file_id,
1469 });
1470 }
1471 }
1472
1473 drop(tx);
1474 },
1475 || {
1476 let checksum_diags = parking_lot::Mutex::new(std::collections::BTreeMap::new());
1480 rayon::scope(|s| {
1481 while let Ok((path, checksum)) = rx.recv() {
1482 s.spawn(|_s| {
1483 let absolute_path = path;
1484 let path = &absolute_path;
1485 if let Err(err) = validate_file_checksum(&absolute_path, &checksum.value) {
1486 let diag: Diag = diags::ChecksumMismatch {
1487 path: diags::HomePath { path, root, home },
1488 checksum,
1489 severity: None,
1490 error: format!("{err:#}"),
1491 file_id,
1492 }
1493 .into();
1494
1495 checksum_diags.lock().insert(absolute_path, diag);
1496 } else {
1497 let diag: Diag = diags::ChecksumMatch {
1498 path: diags::HomePath { path, root, home },
1499 checksum,
1500 severity: None,
1501 file_id,
1502 }
1503 .into();
1504
1505 checksum_diags.lock().insert(absolute_path, diag);
1506 }
1507 });
1508 }
1509 });
1510
1511 checksum_diags.into_inner().into_values()
1512 },
1513 );
1514
1515 for diag in checksum_diags {
1516 pack.push(diag);
1517 }
1518
1519 kc_index
1520}
1521
1522pub(crate) enum ExecutableKind {
1523 Native(goblin::Hint),
1524 Interpreted(String),
1525}
1526
1527fn check_is_executable(
1528 path: &crate::Path,
1529 exclude_archives: bool,
1530) -> anyhow::Result<Option<ExecutableKind>> {
1531 use std::io::Read;
1532
1533 let mut file = std::fs::File::open(path)?;
1534 let mut header = [0u8; 16];
1535 let read = file.read(&mut header)?;
1536 if read != header.len() {
1537 return Ok(None);
1538 }
1539
1540 use goblin::Hint;
1541
1542 match goblin::peek_bytes(&header)
1543 .map_err(|err| anyhow::format_err!("failed to peek bytes: {err}"))?
1544 {
1545 Hint::Archive if exclude_archives => Ok(None),
1548 Hint::Unknown(_) => {
1549 if header[..2] != [0x23, 0x21] {
1551 return Ok(None);
1552 }
1553
1554 let mut hdr = [0u8; 256];
1556 let header = if !header.contains(&b'\n') {
1557 hdr[..16].copy_from_slice(&header);
1558 let read = file.read(&mut hdr[16..])?;
1559 &hdr[..read + 16]
1560 } else {
1561 &header[..]
1562 };
1563
1564 let parse = || {
1565 let line_end = header.iter().position(|b| *b == b'\n')?;
1566 let line = std::str::from_utf8(&header[..line_end]).ok()?;
1567
1568 if path.extension() == Some("rs") && line.starts_with("#![") {
1571 return None;
1572 }
1573
1574 let mut items = line.split(' ');
1577 let maybe_interpreter = items.next()?;
1578 let interpreter = if maybe_interpreter.ends_with("#!") {
1579 items.next()?
1580 } else {
1581 maybe_interpreter
1582 };
1583
1584 if interpreter.ends_with("/env") {
1588 items.next()
1589 } else if let Some((_, bin)) = interpreter.rsplit_once('/') {
1590 Some(bin)
1591 } else {
1592 Some(interpreter)
1593 }
1594 };
1595
1596 Ok(parse().map(|s| ExecutableKind::Interpreted(s.to_owned())))
1597 }
1598 Hint::COFF => Ok(None),
1599 hint => Ok(Some(ExecutableKind::Native(hint))),
1600 }
1601}
1602
1603fn validate_checksum(
1605 mut stream: impl std::io::Read,
1606 expected: &cfg::Checksum,
1607) -> anyhow::Result<()> {
1608 let digest = {
1609 let mut dc = ring::digest::Context::new(&ring::digest::SHA256);
1610 let mut chunk = [0; 8 * 1024];
1611 loop {
1612 let read = stream.read(&mut chunk)?;
1613 if read == 0 {
1614 break;
1615 }
1616 dc.update(&chunk[..read]);
1617 }
1618 dc.finish()
1619 };
1620
1621 let digest = digest.as_ref();
1622 if digest != expected.0 {
1623 let mut hs = [0u8; 64];
1624 const CHARS: &[u8] = b"0123456789abcdef";
1625 for (i, &byte) in digest.iter().enumerate() {
1626 let i = i * 2;
1627 hs[i] = CHARS[(byte >> 4) as usize];
1628 hs[i + 1] = CHARS[(byte & 0xf) as usize];
1629 }
1630
1631 let digest = std::str::from_utf8(&hs).unwrap();
1632 anyhow::bail!("checksum mismatch, calculated {digest}");
1633 }
1634
1635 Ok(())
1636}
1637
1638#[inline]
1639fn validate_file_checksum(path: &crate::Path, expected: &cfg::Checksum) -> anyhow::Result<()> {
1640 let file = std::fs::File::open(path)?;
1641 validate_checksum(std::io::BufReader::new(file), expected)?;
1642 Ok(())
1643}
1644
1645fn check_workspace_duplicates(
1646 krates: &Krates,
1647 krate_spans: &crate::diag::KrateSpans<'_>,
1648 cfg: &cfg::WorkspaceDepsConfig,
1649 diags: &mut Vec<Pack>,
1650) {
1651 use crate::diag::Label;
1652
1653 if krates.workspace_members().count() <= 1 {
1656 return;
1657 }
1658
1659 let mut deps = std::collections::BTreeMap::<
1661 _,
1662 Vec<(
1663 &crate::diag::ManifestDep<'_>,
1664 &crate::Krate,
1665 crate::diag::FileId,
1666 )>,
1667 >::new();
1668
1669 for wsm in krates.workspace_members() {
1670 let krates::Node::Krate { id, krate, .. } = wsm else {
1671 continue; };
1673
1674 let Some(man) = krate_spans.manifest(id) else {
1675 continue;
1676 };
1677
1678 for mdep in man.deps(true) {
1679 if mdep.dep.path.is_some() && !cfg.include_path_dependencies {
1680 continue;
1681 }
1682
1683 deps.entry(&mdep.krate.id)
1684 .or_default()
1685 .push((mdep, krate, man.id));
1686 }
1687 }
1688
1689 deps.retain(|_, parents| parents.len() > 1);
1694
1695 for (kid, parents) in deps {
1696 let total = parents.len();
1697 let mut labels = Vec::new();
1698
1699 if let Some((ws_span, ws_id)) = krate_spans
1700 .workspace_span(kid)
1701 .zip(krate_spans.workspace_id)
1702 {
1703 labels.push(
1704 Label::secondary(ws_id, ws_span.key).with_message(format_args!(
1705 "{}workspace dependency",
1706 if ws_span.patched.is_some() {
1707 "patched "
1708 } else {
1709 ""
1710 }
1711 )),
1712 );
1713
1714 if let Some(patched) = ws_span.patched {
1715 labels.push(
1716 Label::secondary(ws_id, patched)
1717 .with_message("note this is the original dependency that is patched"),
1718 );
1719 }
1720
1721 if let Some(rename) = &ws_span.rename {
1722 labels.push(
1723 Label::secondary(ws_id, rename.span)
1724 .with_message("note the workspace dependency is renamed"),
1725 );
1726 }
1727 }
1728
1729 let llen = labels.len();
1730 let has_workspace_declaration = llen != 0;
1731
1732 for (mdep, _parent, id) in parents {
1733 if mdep.workspace.is_some() {
1739 continue;
1740 }
1741
1742 labels.push(Label::primary(id, mdep.key_span));
1743
1744 if let Some(rename) = &mdep.rename {
1745 labels.push(
1746 Label::secondary(id, rename.span)
1747 .with_message("note the dependency is renamed"),
1748 );
1749 }
1750 }
1751
1752 if llen >= labels.len() {
1753 continue;
1754 }
1755
1756 let Some(duplicate) = krates.node_for_kid(kid) else {
1757 log::error!("failed to find node for {kid}");
1758 continue;
1759 };
1760
1761 let krates::Node::Krate {
1762 krate: duplicate, ..
1763 } = duplicate
1764 else {
1765 log::error!("{kid} pointed a crate feature, this should be impossible");
1766 continue;
1767 };
1768
1769 let mut pack = Pack::with_kid(Check::Bans, duplicate.id.clone());
1770 pack.push(diags::WorkspaceDuplicate {
1771 duplicate,
1772 labels,
1773 severity: cfg.duplicates,
1774 has_workspace_declaration,
1775 total_uses: total,
1776 });
1777 diags.push(pack);
1778 }
1779}