1use crate::level_graph::LevelGraph;
7use code_ranker_plugin_api::{attrs::AttrValue, edge::Edge, graph::Graph, node::Node};
8use std::cmp::Reverse;
9use std::collections::BTreeMap;
10use std::path::Path;
11
12pub fn relativize_level(level: &mut LevelGraph, target: &Path, roots: &BTreeMap<String, String>) {
19 let id_map = relativize_graph_inner(&mut level.nodes, &mut level.edges, target, roots);
20 for cycle in &mut level.cycles {
21 for n in &mut cycle.nodes {
22 if let Some(nn) = id_map.get(n) {
23 *n = nn.clone();
24 }
25 }
26 }
27}
28
29pub fn relativize_graph(graph: &mut Graph, target: &Path, roots: &BTreeMap<String, String>) {
34 relativize_graph_inner(&mut graph.nodes, &mut graph.edges, target, roots);
35}
36
37fn relativize_graph_inner(
38 nodes: &mut [Node],
39 edges: &mut [Edge],
40 target: &Path,
41 roots: &BTreeMap<String, String>,
42) -> BTreeMap<String, String> {
43 let mut id_map: BTreeMap<String, String> = BTreeMap::new();
44 for node in nodes.iter() {
45 if node.kind == "external" {
46 continue; }
48 let new_id = relativize_path(&node.id, target, roots);
49 if new_id != node.id {
50 id_map.insert(node.id.clone(), new_id);
51 }
52 }
53
54 for node in nodes.iter_mut() {
55 if let Some(new_id) = id_map.get(&node.id) {
56 node.id = new_id.clone();
57 }
58 if let Some(parent) = node.parent.as_mut()
59 && let Some(np) = id_map.get(parent)
60 {
61 *parent = np.clone();
62 }
63 if let Some(AttrValue::Str(p)) = node.attrs.get("path") {
64 let rel = relativize_path(p, target, roots);
65 if rel.is_empty() || rel == node.id {
66 node.attrs.remove("path");
67 } else {
68 node.attrs.insert("path".to_string(), AttrValue::Str(rel));
69 }
70 }
71 }
72 for edge in edges.iter_mut() {
73 if let Some(s) = id_map.get(&edge.source) {
74 edge.source = s.clone();
75 }
76 if let Some(t) = id_map.get(&edge.target) {
77 edge.target = t.clone();
78 }
79 }
80 id_map
81}
82
83fn relativize_path(path: &str, target: &Path, roots: &BTreeMap<String, String>) -> String {
84 if path.is_empty() {
85 return path.to_string();
86 }
87 let p = Path::new(path);
88 if let Ok(rel) = p.strip_prefix(target) {
89 return format!("{{target}}/{}", rel.to_string_lossy());
90 }
91 let mut sorted: Vec<_> = roots.iter().collect();
93 sorted.sort_by_key(|(_, root)| Reverse(root.len()));
94 for (name, root) in &sorted {
95 if let Ok(rel) = p.strip_prefix(root.as_str()) {
96 return format!("{{{name}}}/{}", rel.to_string_lossy());
97 }
98 }
99 path.to_string()
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::level_graph::CycleGroup;
106
107 #[test]
108 fn relativize_path_under_target_uses_token() {
109 let got = relativize_path("/p/src/main.rs", Path::new("/p"), &BTreeMap::new());
110 assert_eq!(got, "{target}/src/main.rs");
111 }
112
113 #[test]
114 fn relativize_path_longest_root_wins() {
115 let roots = BTreeMap::from([
116 ("home".to_string(), "/home/u".to_string()),
117 ("registry".to_string(), "/home/u/.cargo".to_string()),
118 ]);
119 let got = relativize_path("/home/u/.cargo/x.rs", Path::new("/p"), &roots);
120 assert_eq!(got, "{registry}/x.rs");
121 }
122
123 #[test]
124 fn relativize_level_rewrites_ids_edges_and_cycles() {
125 use code_ranker_plugin_api::edge::Edge;
126 let mut level = LevelGraph::default();
127 level.nodes.push(Node {
128 id: "/p/src/a.rs".into(),
129 kind: "file".into(),
130 name: "a.rs".into(),
131 parent: None,
132 attrs: Default::default(),
133 });
134 level.nodes.push(Node {
135 id: "ext:serde".into(),
136 kind: "external".into(),
137 name: "serde".into(),
138 parent: None,
139 attrs: Default::default(),
140 });
141 level.edges.push(Edge {
142 source: "/p/src/a.rs".into(),
143 target: "ext:serde".into(),
144 kind: "uses".into(),
145 line: None,
146 attrs: Default::default(),
147 });
148 level.cycles.push(CycleGroup {
149 kind: "mutual".into(),
150 nodes: vec!["/p/src/a.rs".into()],
151 });
152 relativize_level(&mut level, Path::new("/p"), &BTreeMap::new());
153 assert_eq!(level.nodes[0].id, "{target}/src/a.rs");
154 assert_eq!(level.nodes[1].id, "ext:serde");
155 assert_eq!(level.edges[0].source, "{target}/src/a.rs");
156 assert_eq!(level.edges[0].target, "ext:serde");
157 assert_eq!(level.cycles[0].nodes[0], "{target}/src/a.rs");
158 }
159
160 #[test]
161 fn relativize_path_empty_and_unmatched_pass_through() {
162 assert_eq!(relativize_path("", Path::new("/p"), &BTreeMap::new()), "");
165 assert_eq!(
166 relativize_path("/elsewhere/x.rs", Path::new("/p"), &BTreeMap::new()),
167 "/elsewhere/x.rs"
168 );
169 }
170
171 #[test]
172 fn relativize_graph_remaps_parent_and_path_attr() {
173 let roots = BTreeMap::from([("reg".to_string(), "/reg".to_string())]);
174 let mut child_attrs = BTreeMap::new();
175 child_attrs.insert("path".to_string(), AttrValue::Str("/reg/dep/lib.rs".into()));
177 let mut redundant_attrs = BTreeMap::new();
178 redundant_attrs.insert("path".to_string(), AttrValue::Str("/p/b.rs".into()));
180
181 let mut graph = Graph {
182 nodes: vec![
183 Node {
184 id: "/p/a.rs".into(),
185 kind: "file".into(),
186 name: "a.rs".into(),
187 parent: Some("/p/mod.rs".into()),
188 attrs: child_attrs,
189 },
190 Node {
191 id: "/p/mod.rs".into(),
192 kind: "file".into(),
193 name: "mod.rs".into(),
194 parent: None,
195 attrs: Default::default(),
196 },
197 Node {
198 id: "/p/b.rs".into(),
199 kind: "file".into(),
200 name: "b.rs".into(),
201 parent: None,
202 attrs: redundant_attrs,
203 },
204 ],
205 edges: vec![],
206 };
207 relativize_graph(&mut graph, Path::new("/p"), &roots);
208
209 let a = graph.nodes.iter().find(|n| n.name == "a.rs").unwrap();
210 assert_eq!(a.id, "{target}/a.rs");
211 assert_eq!(
212 a.parent.as_deref(),
213 Some("{target}/mod.rs"),
214 "parent remapped"
215 );
216 assert_eq!(
217 a.attrs.get("path"),
218 Some(&AttrValue::Str("{reg}/dep/lib.rs".into())),
219 "path attr relativized against the root"
220 );
221
222 let b = graph.nodes.iter().find(|n| n.name == "b.rs").unwrap();
223 assert!(
224 !b.attrs.contains_key("path"),
225 "a path attr equal to the node's own id is dropped as redundant"
226 );
227 }
228}