Skip to main content

pedant_core/ir/semantic/
file_analysis.rs

1//! Cached file-level semantic analysis.
2//!
3//! `SemanticFileAnalysis` is the primary semantic boundary. For one file, it
4//! owns every derived fact that the per-query API previously rebuilt on each
5//! call: function entries, the reachability set, per-function data flow facts,
6//! resolved types, a flat aggregate of all flows, and the definition edges
7//! resolution promotes through. Constructed once by
8//! `SemanticContext::analyze_file`, then cached and shared via `Arc`.
9//!
10//! Name-only `(caller, callee)` pairs are not among them. They are derived
11//! while the file is walked, consumed once to compute reachability, and
12//! dropped: a name pair cannot tell one Cargo target's definition from
13//! another's, so it stays detector input and is never a graph surface.
14
15use std::collections::BTreeMap;
16use std::collections::BTreeSet;
17use std::sync::Arc;
18
19use ra_ap_hir::{DisplayTarget, Semantics};
20use ra_ap_ide::RootDatabase;
21use ra_ap_syntax::{AstNode, SyntaxKind, ast};
22
23use super::super::facts::DataFlowFact;
24use super::common::{FnContext, ParsedFile, format_type};
25use super::function_summary::{FunctionAnalysisSummary, FunctionSummaryData, run_detectors};
26use super::targets::{self, SemanticEdge};
27use super::{concurrency, reachability};
28
29/// Function entry: (name, start_line, end_line, is_entry_point).
30pub(super) type FnEntry = (Box<str>, usize, usize, bool);
31
32/// Cached file-level semantic analysis.
33///
34/// Immutable after construction. Collections use `Box<[T]>` or `Arc<[T]>`
35/// depending on whether downstream consumers share ownership. Per-function
36/// summaries are stored in a sorted `BTreeMap` keyed by function name.
37pub struct SemanticFileAnalysis {
38    semantic_edges: Box<[SemanticEdge]>,
39    fn_entries: Box<[FnEntry]>,
40    reachable_names: BTreeSet<Box<str>>,
41    data_flows: Arc<[DataFlowFact]>,
42    fn_summaries: BTreeMap<Box<str>, FunctionSummaryData>,
43    /// Eagerly resolved types keyed by `(line, column)` (1-based line, 0-based col).
44    resolved_types: BTreeMap<(usize, usize), Box<str>>,
45}
46
47impl SemanticFileAnalysis {
48    /// Build a complete file analysis from a parsed file context.
49    ///
50    /// One traversal of the file's functions builds `FnContext` per function,
51    /// deriving the reachability call pairs, function entries, data flow facts,
52    /// and detector outputs from the same precomputed state. Type resolution is
53    /// eagerly cached. No subsequent parse is needed.
54    pub(super) fn build(pf: &ParsedFile<'_>) -> Self {
55        let mut call_graph_edges: Vec<(Box<str>, Box<str>)> = Vec::new();
56        let mut fn_entries: Vec<FnEntry> = Vec::new();
57        let mut fn_summaries: BTreeMap<Box<str>, FunctionSummaryData> = BTreeMap::new();
58        let mut all_flows: Vec<DataFlowFact> = Vec::new();
59        let mut resolved_types: BTreeMap<(usize, usize), Box<str>> = BTreeMap::new();
60
61        // Compute display target once for the file (all functions share it).
62        let display_target = Some(pf.display_target);
63
64        // Track function syntax ranges to skip during module-level type resolution.
65        let mut fn_ranges: Vec<ra_ap_syntax::TextRange> = Vec::new();
66
67        for fn_node in pf.tree.syntax().descendants().filter_map(ast::Fn::cast) {
68            let fn_range = fn_node.syntax().text_range();
69            fn_ranges.push(fn_range);
70
71            let Some(ctx) = FnContext::build(pf, &fn_node) else {
72                continue;
73            };
74
75            // Derive call graph edges from precomputed call sites.
76            ctx.extend_call_graph(&mut call_graph_edges);
77
78            let ranges = run_detectors(&ctx, &mut all_flows);
79
80            // Resolve types from this function's syntax tree.
81            if let Some(dt) = display_target {
82                resolve_types_in_subtree(pf, fn_node.syntax(), dt, &mut resolved_types);
83            }
84
85            let (name, start, end, entry, lock_acquisitions) = ctx.into_entry_data();
86            fn_entries.push((Box::from(&*name), start, end, entry));
87            fn_summaries.insert(name, ranges.into_summary(lock_acquisitions));
88        }
89
90        call_graph_edges.sort();
91        call_graph_edges.dedup();
92        let call_graph = call_graph_edges.into_boxed_slice();
93        let fn_entries = fn_entries.into_boxed_slice();
94        let reachable_names = reachability::compute_reachable_names(&fn_entries, &call_graph);
95
96        // File-level lock ordering analysis from precomputed summaries.
97        let lock_ordering = concurrency::detect_lock_ordering(&fn_summaries);
98        all_flows.extend(lock_ordering.into_vec());
99
100        let data_flows: Arc<[DataFlowFact]> = all_flows.into();
101
102        // Module-level type resolution: skip nodes inside function syntax ranges.
103        if let Some(dt) = display_target {
104            resolve_module_level_types(pf, dt, &fn_ranges, &mut resolved_types);
105        }
106
107        Self {
108            semantic_edges: targets::collect(pf),
109            fn_entries,
110            reachable_names,
111            data_flows,
112            fn_summaries,
113            resolved_types,
114        }
115    }
116
117    /// The definition edges this file states, taken while it was analyzed.
118    pub(super) fn semantic_edges(&self) -> &[SemanticEdge] {
119        &self.semantic_edges
120    }
121
122    /// All data flow facts detected in this file (taint, quality, perf, concurrency).
123    ///
124    /// Returns a shared `Arc` — callers that need ownership clone the `Arc`,
125    /// not the individual facts.
126    pub fn data_flows(&self) -> &Arc<[DataFlowFact]> {
127        &self.data_flows
128    }
129
130    /// Borrowed view into a named function's precomputed semantic summary.
131    ///
132    /// Returns `None` when the function is not found in the file or has
133    /// no body (e.g., trait method declarations).
134    pub fn function(&self, name: &str) -> Option<FunctionAnalysisSummary<'_>> {
135        self.fn_summaries
136            .get(name)
137            .map(|data| FunctionAnalysisSummary::new(data, &self.data_flows))
138    }
139
140    /// Resolve the type at a `(line, column)` position from the cached table.
141    ///
142    /// Uses 1-based line numbers and 0-based column offsets. Returns `None`
143    /// when the position was not resolvable during file analysis construction.
144    pub fn resolve_type(&self, line: usize, column: usize) -> Option<&str> {
145        self.resolved_types.get(&(line, column)).map(|s| &**s)
146    }
147
148    /// Check whether a line falls within a function reachable from entry points.
149    pub fn is_line_reachable(&self, line: usize) -> bool {
150        reachability::is_line_in_reachable_fn(&self.fn_entries, &self.reachable_names, line)
151    }
152
153    /// Batch reachability check: one `bool` per input line.
154    pub fn check_reachability_batch(&self, lines: &[usize]) -> Box<[bool]> {
155        lines
156            .iter()
157            .map(|&line| self.is_line_reachable(line))
158            .collect::<Vec<_>>()
159            .into_boxed_slice()
160    }
161}
162
163/// Resolve type-bearing positions within a syntax subtree (function or item).
164fn resolve_types_in_subtree(
165    pf: &ParsedFile<'_>,
166    root: &ra_ap_syntax::SyntaxNode,
167    display_target: DisplayTarget,
168    resolved: &mut BTreeMap<(usize, usize), Box<str>>,
169) {
170    for node in root.descendants() {
171        resolve_type_bearing_node(pf, &node, display_target, resolved);
172    }
173}
174
175/// Resolve module-level type-bearing positions, skipping function syntax ranges.
176///
177/// Function-scoped types are already resolved during the per-function loop.
178fn resolve_module_level_types(
179    pf: &ParsedFile<'_>,
180    display_target: DisplayTarget,
181    fn_ranges: &[ra_ap_syntax::TextRange],
182    resolved: &mut BTreeMap<(usize, usize), Box<str>>,
183) {
184    for node in pf.tree.syntax().descendants() {
185        let range = node.text_range();
186        if fn_ranges.iter().any(|fr| fr.contains_range(range)) {
187            continue;
188        }
189        resolve_type_bearing_node(pf, &node, display_target, resolved);
190    }
191}
192
193/// Classify and resolve a single type-bearing syntax node.
194fn resolve_type_bearing_node(
195    pf: &ParsedFile<'_>,
196    node: &ra_ap_syntax::SyntaxNode,
197    display_target: DisplayTarget,
198    resolved: &mut BTreeMap<(usize, usize), Box<str>>,
199) {
200    match node.kind() {
201        SyntaxKind::LET_STMT => {
202            resolve_let_stmt_type(pf, node, display_target, resolved);
203        }
204        SyntaxKind::PATH_TYPE
205        | SyntaxKind::TUPLE_TYPE
206        | SyntaxKind::ARRAY_TYPE
207        | SyntaxKind::SLICE_TYPE
208        | SyntaxKind::REF_TYPE
209        | SyntaxKind::PTR_TYPE => {
210            resolve_type_node(pf, node, display_target, resolved);
211        }
212        _ => {}
213    }
214}
215
216/// Resolve the type annotation on a let statement and cache at the annotation's position.
217fn resolve_let_stmt_type(
218    pf: &ParsedFile<'_>,
219    node: &ra_ap_syntax::SyntaxNode,
220    display_target: DisplayTarget,
221    resolved: &mut BTreeMap<(usize, usize), Box<str>>,
222) {
223    let Some(let_stmt) = ast::LetStmt::cast(node.clone()) else {
224        return;
225    };
226
227    // Resolve from initializer expression type.
228    let init_resolved = let_stmt.initializer().and_then(|init| {
229        let type_str = resolve_expr_type(&pf.sema, &init, pf.db, display_target)?;
230        let lc = pf.line_index.line_col(init.syntax().text_range().start());
231        Some(((lc.line + 1) as usize, lc.col as usize, type_str))
232    });
233    if let Some((line, col, type_str)) = init_resolved {
234        resolved.insert((line, col), type_str);
235    }
236
237    // Resolve from type annotation if present.
238    let ann_resolved = let_stmt.ty().and_then(|ty| {
239        let type_str = resolve_ast_type(&pf.sema, &ty, pf.db, display_target)?;
240        let lc = pf.line_index.line_col(ty.syntax().text_range().start());
241        Some(((lc.line + 1) as usize, lc.col as usize, type_str))
242    });
243    if let Some((line, col, type_str)) = ann_resolved {
244        resolved.insert((line, col), type_str);
245    }
246}
247
248/// Resolve a standalone type syntax node.
249fn resolve_type_node(
250    pf: &ParsedFile<'_>,
251    node: &ra_ap_syntax::SyntaxNode,
252    display_target: DisplayTarget,
253    resolved: &mut BTreeMap<(usize, usize), Box<str>>,
254) {
255    let Some(ty) = ast::Type::cast(node.clone()) else {
256        return;
257    };
258    let Some(type_str) = resolve_ast_type(&pf.sema, &ty, pf.db, display_target) else {
259        return;
260    };
261    let range = ty.syntax().text_range();
262    let lc = pf.line_index.line_col(range.start());
263    resolved.insert(((lc.line + 1) as usize, lc.col as usize), type_str);
264}
265
266/// Resolve an expression's type to a canonical string.
267fn resolve_expr_type(
268    sema: &Semantics<'_, RootDatabase>,
269    expr: &ast::Expr,
270    db: &RootDatabase,
271    display_target: DisplayTarget,
272) -> Option<Box<str>> {
273    let ty_info = sema.type_of_expr(expr)?;
274    Some(format_type(&ty_info.original, db, display_target))
275}
276
277/// Resolve an AST type node to a canonical string.
278fn resolve_ast_type(
279    sema: &Semantics<'_, RootDatabase>,
280    ty: &ast::Type,
281    db: &RootDatabase,
282    display_target: DisplayTarget,
283) -> Option<Box<str>> {
284    sema.resolve_type(ty)
285        .map(|resolved| format_type(&resolved, db, display_target))
286        .or_else(|| resolve_path_type(sema, ty, db, display_target))
287}
288
289/// Resolve a path type by looking up the path definition.
290fn resolve_path_type(
291    sema: &Semantics<'_, RootDatabase>,
292    ty: &ast::Type,
293    db: &RootDatabase,
294    display_target: DisplayTarget,
295) -> Option<Box<str>> {
296    let ast::Type::PathType(p) = ty else {
297        return None;
298    };
299    let path = p.path()?;
300    let resolution = sema.resolve_path(&path)?;
301    match resolution {
302        ra_ap_hir::PathResolution::Def(module_def) => {
303            resolve_module_def_type(module_def, db, display_target)
304        }
305        _ => None,
306    }
307}
308
309/// Get the type representation for a module-level definition.
310fn resolve_module_def_type(
311    def: ra_ap_hir::ModuleDef,
312    db: &RootDatabase,
313    display_target: DisplayTarget,
314) -> Option<Box<str>> {
315    match def {
316        ra_ap_hir::ModuleDef::TypeAlias(alias) => {
317            Some(format_type(&alias.ty(db), db, display_target))
318        }
319        ra_ap_hir::ModuleDef::Adt(adt) => Some(format_type(&adt.ty(db), db, display_target)),
320        ra_ap_hir::ModuleDef::BuiltinType(builtin) => Some(builtin.name().as_str().into()),
321        _ => None,
322    }
323}