1use anyhow::{Context, Result};
2use code_split_core::{Edge, EdgeKind, GraphBuilder, Node, NodeId, NodeKind, SemanticIndex};
3use ra_ap_hir::{
4 self as hir, AsAssocItem, AssocItem, AssocItemContainer, Crate, HasSource, HirDisplay,
5 ModuleDef, Semantics, attach_db,
6};
7use ra_ap_ide::{AnalysisHost, RootDatabase};
8use ra_ap_ide_db::{base_db::SourceDatabase, line_index};
9use ra_ap_load_cargo::{LoadCargoConfig, ProcMacroServerChoice, load_workspace_at};
10use ra_ap_project_model::{CargoConfig, RustLibSource};
11use ra_ap_syntax::{AstNode, ast};
12use ra_ap_vfs::Vfs;
13use std::collections::{HashMap, HashSet};
14use std::convert::Infallible;
15use std::path::Path;
16
17#[derive(Debug, Default)]
18pub struct NullSemanticIndex;
19
20impl SemanticIndex for NullSemanticIndex {
21 type Error = Infallible;
22
23 fn analyze(&self, _workspace: &Path, _builder: &mut GraphBuilder) -> Result<(), Self::Error> {
24 Ok(())
25 }
26}
27
28#[derive(Debug, Default)]
29pub struct RustAnalyzerSemantic;
30
31#[derive(Debug, thiserror::Error)]
32#[error(transparent)]
33pub struct SemaError(#[from] pub anyhow::Error);
34
35impl SemanticIndex for RustAnalyzerSemantic {
36 type Error = SemaError;
37
38 fn analyze(&self, workspace: &Path, builder: &mut GraphBuilder) -> Result<(), Self::Error> {
39 analyze_inner(workspace, builder).map_err(SemaError)
40 }
41}
42
43fn analyze_inner(workspace: &Path, builder: &mut GraphBuilder) -> Result<()> {
44 let cargo_config = CargoConfig {
45 sysroot: Some(RustLibSource::Discover),
46 all_targets: true,
47 ..Default::default()
48 };
49 let load_config = LoadCargoConfig {
50 load_out_dirs_from_check: true,
51 with_proc_macro_server: ProcMacroServerChoice::Sysroot,
52 prefill_caches: false,
53 num_worker_threads: 1,
54 proc_macro_processes: 1,
55 };
56
57 let (db, vfs, _pm) = load_workspace_at(workspace, &cargo_config, &load_config, &|_| {})
58 .context("load_workspace_at failed")?;
59 let host = AnalysisHost::with_database(db);
60 let db: &RootDatabase = host.raw_database();
61
62 attach_db(db, || analyze_with_db(db, &vfs, builder))
63}
64
65fn analyze_with_db(db: &RootDatabase, vfs: &Vfs, builder: &mut GraphBuilder) -> Result<()> {
66 let sema = Semantics::new(db);
67
68 let mut fn_node_id: HashMap<hir::Function, NodeId> = HashMap::new();
69 let mut emitted_fns: HashSet<NodeId> = HashSet::new();
70 let mut emitted_edges: HashSet<(NodeId, NodeId)> = HashSet::new();
71
72 for krate in Crate::all(db) {
73 let root_file = krate.root_file(db);
74 let sr_id = db.file_source_root(root_file).source_root_id(db);
75 if db.source_root(sr_id).source_root(db).is_library {
76 continue;
77 }
78
79 for module in krate.modules(db) {
80 let mut callers: Vec<hir::Function> = Vec::new();
81 for decl in module.declarations(db) {
82 match decl {
83 ModuleDef::Function(f) => callers.push(f),
84 ModuleDef::Trait(t) => {
85 for item in t.items(db) {
86 if let AssocItem::Function(f) = item {
87 callers.push(f);
88 }
89 }
90 }
91 _ => {}
92 }
93 }
94 for impl_def in module.impl_defs(db) {
95 for item in impl_def.items(db) {
96 if let AssocItem::Function(f) = item {
97 callers.push(f);
98 }
99 }
100 }
101
102 for caller in callers {
103 let Some(caller_id) =
104 ensure_fn_node(caller, db, vfs, &mut fn_node_id, &mut emitted_fns, builder)
105 else {
106 continue;
107 };
108
109 let Some(src) = sema.source(caller) else {
110 continue;
111 };
112 let Some(body) = src.value.body() else {
113 continue;
114 };
115
116 for node in body.syntax().descendants() {
117 if let Some(mc) = ast::MethodCallExpr::cast(node.clone()) {
118 if let Some(callee) = sema.resolve_method_call(&mc) {
119 record_call(
120 caller_id.clone(),
121 callee,
122 db,
123 vfs,
124 &mut fn_node_id,
125 &mut emitted_fns,
126 &mut emitted_edges,
127 builder,
128 );
129 }
130 continue;
131 }
132 if let Some(ce) = ast::CallExpr::cast(node) {
133 let expr: ast::Expr = ce.into();
134 if let Some(callable) = sema.resolve_expr_as_callable(&expr)
135 && let hir::CallableKind::Function(f) = callable.kind()
136 {
137 record_call(
138 caller_id.clone(),
139 f,
140 db,
141 vfs,
142 &mut fn_node_id,
143 &mut emitted_fns,
144 &mut emitted_edges,
145 builder,
146 );
147 }
148 }
149 }
150 }
151 }
152 }
153
154 Ok(())
155}
156
157#[allow(clippy::too_many_arguments)]
158fn record_call(
159 caller_id: NodeId,
160 callee: hir::Function,
161 db: &RootDatabase,
162 vfs: &Vfs,
163 fn_node_id: &mut HashMap<hir::Function, NodeId>,
164 emitted_fns: &mut HashSet<NodeId>,
165 emitted_edges: &mut HashSet<(NodeId, NodeId)>,
166 builder: &mut GraphBuilder,
167) {
168 let Some(callee_id) = ensure_fn_node(callee, db, vfs, fn_node_id, emitted_fns, builder) else {
169 return;
170 };
171 if !emitted_edges.insert((caller_id.clone(), callee_id.clone())) {
172 return;
173 }
174 builder.add_edge(Edge {
175 from: caller_id,
176 to: callee_id,
177 kind: EdgeKind::Calls,
178 unresolved: None,
179 external: None,
180 visibility: None,
181 });
182}
183
184fn ensure_fn_node(
185 f: hir::Function,
186 db: &RootDatabase,
187 vfs: &Vfs,
188 fn_node_id: &mut HashMap<hir::Function, NodeId>,
189 emitted_fns: &mut HashSet<NodeId>,
190 builder: &mut GraphBuilder,
191) -> Option<NodeId> {
192 if let Some(id) = fn_node_id.get(&f) {
193 return Some(id.clone());
194 }
195
196 let src = f.source(db)?;
197
198 if src.file_id.is_macro() {
199 return None;
200 }
201
202 let file_id = src.file_id.original_file(db).file_id(db);
203 let path = vfs.file_path(file_id);
204 let path_str = path.as_path()?.to_string();
205
206 let name = f.name(db).as_str().to_owned();
207 let is_method = f.as_assoc_item(db).is_some();
208 let kind = if is_method {
209 NodeKind::Method
210 } else {
211 NodeKind::Fn
212 };
213
214 let krate = f.module(db).krate(db);
216 let crate_name = krate
217 .display_name(db)
218 .map(|n| n.crate_name().to_string())
219 .unwrap_or_else(|| "unknown".to_string());
220 let display_target = krate.to_display_target(db);
221
222 let mod_path = build_module_path(f.module(db), db);
224
225 let container = if is_method {
227 f.as_assoc_item(db).map(|assoc| match assoc.container(db) {
228 AssocItemContainer::Impl(impl_def) => {
229 let ty = impl_def.self_ty(db).display(db, display_target).to_string();
230 let ty = ty.split('<').next().unwrap_or(&ty);
232 ty.split("::").last().unwrap_or(ty).trim().to_string()
233 }
234 AssocItemContainer::Trait(t) => t.name(db).as_str().to_owned(),
235 })
236 } else {
237 None
238 };
239
240 let id = match (&mod_path[..], container.as_deref()) {
242 ("", Some(c)) => format!("method:{crate_name}::{c}::{name}"),
243 (m, Some(c)) => format!("method:{crate_name}::{m}::{c}::{name}"),
244 ("", None) => format!("fn:{crate_name}::{name}"),
245 (m, None) => format!("fn:{crate_name}::{m}::{name}"),
246 };
247
248 let sema_line = {
251 let range = src.value.syntax().text_range();
252 let li = line_index(db, file_id);
253 li.try_line_col(range.start()).map(|lc| lc.line + 1)
254 };
255 let existing_id = builder
256 .find_fn_node(&path_str, &name)
257 .or_else(|| sema_line.and_then(|l| builder.find_fn_node_by_line(&path_str, &name, l)));
258 if let Some(existing_id) = existing_id {
259 fn_node_id.insert(f, existing_id.clone());
260 emitted_fns.insert(existing_id.clone());
261 return Some(existing_id);
262 }
263
264 if emitted_fns.insert(id.clone()) {
265 let (line, loc) = {
266 let range = src.value.syntax().text_range();
267 let li = line_index(db, file_id);
268 let start = li.try_line_col(range.start());
269 let end = li.try_line_col(range.end());
270 let loc = start.zip(end).map(|(s, e)| e.line - s.line + 1);
271 (sema_line, loc)
272 };
273 builder.add_node(Node {
274 id: id.clone(),
275 kind,
276 name,
277 path: path_str.clone(),
278 parent: Some(format!("file:{path_str}")),
279 external: None,
280 visibility: None,
281 loc,
282 line,
283 item_count: None,
284 method_count: None,
285 complexity: None,
286 cycle_kind: None,
287 });
288 }
289
290 fn_node_id.insert(f, id.clone());
291 Some(id)
292}
293
294fn build_module_path(module: hir::Module, db: &RootDatabase) -> String {
297 let mut parts: Vec<String> = Vec::new();
298 let mut m = module;
299 loop {
300 match m.name(db) {
301 Some(name) => parts.push(name.as_str().to_owned()),
302 None => break,
303 }
304 match m.parent(db) {
305 Some(parent) => m = parent,
306 None => break,
307 }
308 }
309 parts.reverse();
310 parts.join("::")
311}