Skip to main content

code_split_sema/
lib.rs

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    // Crate name
215    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    // Module path within the crate (e.g. "handlers::error")
223    let mod_path = build_module_path(f.module(db), db);
224
225    // For methods: impl type name or trait name
226    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                // Strip generics and leading path: "path::Foo<Bar>" → "Foo"
231                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    // Stable ID: no line numbers, no byte offsets
241    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    // Reuse existing syn-created node when available (avoids duplicate nodes
249    // with different ID schemes for the same local workspace function).
250    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
294/// Walk up the module hierarchy to build a `::` separated path,
295/// excluding the crate root (which has no name).
296fn 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}