Skip to content

Commit a4c7a87

Browse files
committed
Handle dev-dependency cycles
1 parent 980c75b commit a4c7a87

File tree

3 files changed

+752
-535
lines changed

3 files changed

+752
-535
lines changed

crates/base-db/src/input.rs

+11
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,11 @@ impl CrateGraph {
417417
Ok(())
418418
}
419419

420+
pub fn duplicate(&mut self, id: CrateId) -> CrateId {
421+
let data = self[id].clone();
422+
self.arena.alloc(data)
423+
}
424+
420425
pub fn add_dep(
421426
&mut self,
422427
from: CrateId,
@@ -612,6 +617,12 @@ impl ops::Index<CrateId> for CrateGraph {
612617
}
613618
}
614619

620+
impl ops::IndexMut<CrateId> for CrateGraph {
621+
fn index_mut(&mut self, crate_id: CrateId) -> &mut CrateData {
622+
&mut self.arena[crate_id]
623+
}
624+
}
625+
615626
impl CrateData {
616627
fn add_dep(&mut self, dep: Dependency) {
617628
self.dependencies.push(dep)

crates/project-model/src/workspace.rs

+119-36
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
//! metadata` or `rust-project.json`) into representation stored in the salsa
33
//! database -- `CrateGraph`.
44
5-
use std::{collections::VecDeque, fmt, fs, process::Command, sync::Arc};
5+
use std::{
6+
collections::{hash_map::Entry, VecDeque},
7+
fmt, fs,
8+
process::Command,
9+
sync::Arc,
10+
};
611

712
use anyhow::{format_err, Context, Result};
813
use base_db::{
@@ -844,12 +849,12 @@ fn cargo_to_crate_graph(
844849
None => (SysrootPublicDeps::default(), None),
845850
};
846851

847-
let cfg_options = {
852+
let cfg_options = forced_cfg.clone().unwrap_or_else(|| {
848853
let mut cfg_options = CfgOptions::default();
849854
cfg_options.extend(rustc_cfg);
850855
cfg_options.insert_atom("debug_assertions".into());
851856
cfg_options
852-
};
857+
});
853858

854859
// Mapping of a package to its library target
855860
let mut pkg_to_lib_crate = FxHashMap::default();
@@ -861,32 +866,6 @@ fn cargo_to_crate_graph(
861866
for pkg in cargo.packages() {
862867
has_private |= cargo[pkg].metadata.rustc_private;
863868

864-
let cfg_options = forced_cfg.clone().unwrap_or_else(|| {
865-
let mut cfg_options = cfg_options.clone();
866-
867-
// Add test cfg for local crates
868-
if cargo[pkg].is_local {
869-
cfg_options.insert_atom("test".into());
870-
}
871-
872-
let overrides = match override_cfg {
873-
CfgOverrides::Wildcard(cfg_diff) => Some(cfg_diff),
874-
CfgOverrides::Selective(cfg_overrides) => cfg_overrides.get(&cargo[pkg].name),
875-
};
876-
877-
if let Some(overrides) = overrides {
878-
// FIXME: this is sort of a hack to deal with #![cfg(not(test))] vanishing such as seen
879-
// in ed25519_dalek (#7243), and libcore (#9203) (although you only hit that one while
880-
// working on rust-lang/rust as that's the only time it appears outside sysroot).
881-
//
882-
// A more ideal solution might be to reanalyze crates based on where the cursor is and
883-
// figure out the set of cfgs that would have to apply to make it active.
884-
885-
cfg_options.apply_diff(overrides.clone());
886-
};
887-
cfg_options
888-
});
889-
890869
let mut lib_tgt = None;
891870
for &tgt in cargo[pkg].targets.iter() {
892871
if cargo[tgt].kind != TargetKind::Lib && !cargo[pkg].is_member {
@@ -897,7 +876,7 @@ fn cargo_to_crate_graph(
897876
// https://github.com/rust-lang/rust-analyzer/issues/11300
898877
continue;
899878
}
900-
let Some(file_id) = load(&cargo[tgt].root) else { continue };
879+
let Some(file_id) = load(&cargo[tgt].root) else { continue };
901880

902881
let crate_id = add_target_crate_root(
903882
crate_graph,
@@ -925,15 +904,19 @@ fn cargo_to_crate_graph(
925904
pkg_crates.entry(pkg).or_insert_with(Vec::new).push((crate_id, cargo[tgt].kind));
926905
}
927906

907+
let Some(targets) = pkg_crates.get(&pkg) else { continue };
928908
// Set deps to the core, std and to the lib target of the current package
929-
for &(from, kind) in pkg_crates.get(&pkg).into_iter().flatten() {
909+
for &(from, kind) in targets {
930910
// Add sysroot deps first so that a lib target named `core` etc. can overwrite them.
931911
public_deps.add_to_crate_graph(crate_graph, from);
932912

933913
// Add dep edge of all targets to the package's lib target
934914
if let Some((to, name)) = lib_tgt.clone() {
935-
if to != from && kind != TargetKind::BuildScript {
936-
// (build script can not depend on its library target)
915+
if to != from {
916+
if kind == TargetKind::BuildScript {
917+
// build script can not depend on its library target
918+
continue;
919+
}
937920

938921
// For root projects with dashes in their name,
939922
// cargo metadata does not do any normalization,
@@ -945,6 +928,43 @@ fn cargo_to_crate_graph(
945928
}
946929
}
947930

931+
// We now need to duplicate workspace members that are used as dev-dependencies to prevent
932+
// cycles from forming.
933+
934+
// Map from crate id to it's dev-dependency clone id
935+
let mut test_dupes = FxHashMap::default();
936+
let mut work = vec![];
937+
938+
// Get all dependencies of the workspace members that are used as dev-dependencies
939+
for pkg in cargo.packages() {
940+
for dep in &cargo[pkg].dependencies {
941+
if dep.kind == DepKind::Dev {
942+
work.push(dep.pkg);
943+
}
944+
}
945+
}
946+
while let Some(pkg) = work.pop() {
947+
let Some(&to) = pkg_to_lib_crate.get(&pkg) else { continue };
948+
match test_dupes.entry(to) {
949+
Entry::Occupied(_) => continue,
950+
Entry::Vacant(v) => {
951+
for dep in &cargo[pkg].dependencies {
952+
if dep.kind == DepKind::Normal && cargo[dep.pkg].is_member {
953+
work.push(dep.pkg);
954+
}
955+
}
956+
v.insert({
957+
let duped = crate_graph.duplicate(to);
958+
if let Some(proc_macro) = proc_macros.get(&to).cloned() {
959+
proc_macros.insert(duped, proc_macro);
960+
}
961+
crate_graph[duped].cfg_options.insert_atom("test".into());
962+
duped
963+
});
964+
}
965+
}
966+
}
967+
948968
// Now add a dep edge from all targets of upstream to the lib
949969
// target of downstream.
950970
for pkg in cargo.packages() {
@@ -958,12 +978,66 @@ fn cargo_to_crate_graph(
958978
if (dep.kind == DepKind::Build) != (kind == TargetKind::BuildScript) {
959979
continue;
960980
}
981+
add_dep(
982+
crate_graph,
983+
from,
984+
name.clone(),
985+
if dep.kind == DepKind::Dev {
986+
// point to the test enabled duplicate for dev-dependencies
987+
test_dupes.get(&to).copied().unwrap_or(to)
988+
} else {
989+
to
990+
},
991+
);
961992

962-
add_dep(crate_graph, from, name.clone(), to)
993+
if dep.kind == DepKind::Normal && cargo[dep.pkg].is_member {
994+
// Also apply the dependency as a test enabled dependency to the test duplicate
995+
if let Some(&dupe) = test_dupes.get(&from) {
996+
let to = test_dupes.get(&to).copied().unwrap_or_else(|| {
997+
panic!(
998+
"dependency of a dev dependency did not get duplicated! {:?} {:?}",
999+
crate_graph[to].display_name, crate_graph[from].display_name,
1000+
)
1001+
});
1002+
add_dep(crate_graph, dupe, name.clone(), to);
1003+
}
1004+
}
1005+
}
1006+
}
1007+
}
1008+
1009+
for (&pkg, targets) in &pkg_crates {
1010+
for &(krate, _) in targets {
1011+
if test_dupes.get(&krate).is_some() {
1012+
// if the crate got duped as a dev-dep the dupe already has test set
1013+
continue;
1014+
}
1015+
let cfg_options = &mut crate_graph[krate].cfg_options;
1016+
1017+
// Add test cfg for local crates
1018+
if cargo[pkg].is_local {
1019+
cfg_options.insert_atom("test".into());
9631020
}
1021+
1022+
let overrides = match override_cfg {
1023+
CfgOverrides::Wildcard(cfg_diff) => Some(cfg_diff),
1024+
CfgOverrides::Selective(cfg_overrides) => cfg_overrides.get(&cargo[pkg].name),
1025+
};
1026+
1027+
if let Some(overrides) = overrides {
1028+
// FIXME: this is sort of a hack to deal with #![cfg(not(test))] vanishing such as seen
1029+
// in ed25519_dalek (#7243), and libcore (#9203) (although you only hit that one while
1030+
// working on rust-lang/rust as that's the only time it appears outside sysroot).
1031+
//
1032+
// A more ideal solution might be to reanalyze crates based on where the cursor is and
1033+
// figure out the set of cfgs that would have to apply to make it active.
1034+
1035+
cfg_options.apply_diff(overrides.clone());
1036+
};
9641037
}
9651038
}
9661039

1040+
// FIXME: Handle rustc private crates properly when used as dev-dependencies
9671041
if has_private {
9681042
// If the user provided a path to rustc sources, we add all the rustc_private crates
9691043
// and create dependencies on them for the crates which opt-in to that
@@ -1087,7 +1161,9 @@ fn handle_rustc_crates(
10871161
continue;
10881162
}
10891163
for dep in &rustc_workspace[pkg].dependencies {
1090-
queue.push_back(dep.pkg);
1164+
if dep.kind == DepKind::Normal {
1165+
queue.push_back(dep.pkg);
1166+
}
10911167
}
10921168

10931169
let mut cfg_options = cfg_options.clone();
@@ -1397,10 +1473,12 @@ fn handle_hack_cargo_workspace(
13971473
.collect()
13981474
}
13991475

1476+
#[track_caller]
14001477
fn add_dep(graph: &mut CrateGraph, from: CrateId, name: CrateName, to: CrateId) {
14011478
add_dep_inner(graph, from, Dependency::new(name, to))
14021479
}
14031480

1481+
#[track_caller]
14041482
fn add_dep_with_prelude(
14051483
graph: &mut CrateGraph,
14061484
from: CrateId,
@@ -1411,13 +1489,18 @@ fn add_dep_with_prelude(
14111489
add_dep_inner(graph, from, Dependency::with_prelude(name, to, prelude))
14121490
}
14131491

1492+
#[track_caller]
14141493
fn add_proc_macro_dep(crate_graph: &mut CrateGraph, from: CrateId, to: CrateId, prelude: bool) {
14151494
add_dep_with_prelude(crate_graph, from, CrateName::new("proc_macro").unwrap(), to, prelude);
14161495
}
14171496

1497+
#[track_caller]
14181498
fn add_dep_inner(graph: &mut CrateGraph, from: CrateId, dep: Dependency) {
14191499
if let Err(err) = graph.add_dep(from, dep) {
1420-
tracing::error!("{}", err)
1500+
if cfg!(test) {
1501+
panic!("{}", err);
1502+
}
1503+
tracing::error!("{}", err);
14211504
}
14221505
}
14231506

0 commit comments

Comments
 (0)