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