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::{FlowRange, FunctionAnalysisSummary, FunctionSummaryData};
22use super::{concurrency, perf, quality, 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            // Run detectors over the shared precomputed context.
71            let taint = super::taint::detect(&ctx);
72            let quality = quality::detect(&ctx);
73            let performance = perf::detect(&ctx);
74            let concurrency = concurrency::detect(&ctx);
75
76            // Append flows to the file-level aggregate, recording ranges.
77            let taint_range = append_flows(&mut all_flows, taint);
78            let quality_range = append_flows(&mut all_flows, quality);
79            let perf_range = append_flows(&mut all_flows, performance);
80            let conc_range = append_flows(&mut all_flows, concurrency);
81
82            // Resolve types from this function's syntax tree.
83            if let Some(dt) = display_target {
84                resolve_types_in_subtree(pf, fn_node.syntax(), dt, &mut resolved_types);
85            }
86
87            let (name, start, end, entry, lock_acquisitions) = ctx.into_entry_data();
88            fn_entries.push((Box::from(&*name), start, end, entry));
89            fn_summaries.insert(
90                name,
91                FunctionSummaryData {
92                    lock_acquisitions,
93                    taint: taint_range,
94                    quality: quality_range,
95                    performance: perf_range,
96                    concurrency: conc_range,
97                },
98            );
99        }
100
101        call_graph_edges.sort();
102        call_graph_edges.dedup();
103        let call_graph = call_graph_edges.into_boxed_slice();
104        let fn_entries = fn_entries.into_boxed_slice();
105        let reachable_names = reachability::compute_reachable_names(&fn_entries, &call_graph);
106
107        // File-level lock ordering analysis from precomputed summaries.
108        let lock_ordering = concurrency::detect_lock_ordering(&fn_summaries);
109        all_flows.extend(lock_ordering.into_vec());
110
111        let data_flows: Arc<[DataFlowFact]> = all_flows.into();
112
113        // Module-level type resolution: skip nodes inside function syntax ranges.
114        if let Some(dt) = display_target {
115            resolve_module_level_types(pf, dt, &fn_ranges, &mut resolved_types);
116        }
117
118        Self {
119            call_graph,
120            fn_entries,
121            reachable_names,
122            data_flows,
123            fn_summaries,
124            resolved_types,
125        }
126    }
127
128    /// Deduplicated `(caller, callee)` pairs for this file's call graph.
129    pub fn call_graph(&self) -> &[(Box<str>, Box<str>)] {
130        &self.call_graph
131    }
132
133    /// All data flow facts detected in this file (taint, quality, perf, concurrency).
134    ///
135    /// Returns a shared `Arc` — callers that need ownership clone the `Arc`,
136    /// not the individual facts.
137    pub fn data_flows(&self) -> &Arc<[DataFlowFact]> {
138        &self.data_flows
139    }
140
141    /// Borrowed view into a named function's precomputed semantic summary.
142    ///
143    /// Returns `None` when the function is not found in the file or has
144    /// no body (e.g., trait method declarations).
145    pub fn function(&self, name: &str) -> Option<FunctionAnalysisSummary<'_>> {
146        self.fn_summaries
147            .get(name)
148            .map(|data| FunctionAnalysisSummary::new(data, &self.data_flows))
149    }
150
151    /// Resolve the type at a `(line, column)` position from the cached table.
152    ///
153    /// Uses 1-based line numbers and 0-based column offsets. Returns `None`
154    /// when the position was not resolvable during file analysis construction.
155    pub fn resolve_type(&self, line: usize, column: usize) -> Option<&str> {
156        self.resolved_types.get(&(line, column)).map(|s| &**s)
157    }
158
159    /// Check whether a line falls within a function reachable from entry points.
160    pub fn is_line_reachable(&self, line: usize) -> bool {
161        reachability::is_line_in_reachable_fn(&self.fn_entries, &self.reachable_names, line)
162    }
163
164    /// Batch reachability check: one `bool` per input line.
165    pub fn check_reachability_batch(&self, lines: &[usize]) -> Box<[bool]> {
166        lines
167            .iter()
168            .map(|&line| self.is_line_reachable(line))
169            .collect::<Vec<_>>()
170            .into_boxed_slice()
171    }
172}
173
174/// Append a batch of facts to the aggregate and return the range they occupy.
175fn append_flows(all: &mut Vec<DataFlowFact>, facts: Box<[DataFlowFact]>) -> FlowRange {
176    let start = all.len();
177    all.extend(facts.into_vec());
178    FlowRange::new(start, all.len())
179}
180
181/// Resolve type-bearing positions within a syntax subtree (function or item).
182fn resolve_types_in_subtree(
183    pf: &ParsedFile<'_>,
184    root: &ra_ap_syntax::SyntaxNode,
185    display_target: DisplayTarget,
186    resolved: &mut BTreeMap<(usize, usize), Box<str>>,
187) {
188    for node in root.descendants() {
189        resolve_type_bearing_node(pf, &node, display_target, resolved);
190    }
191}
192
193/// Resolve module-level type-bearing positions, skipping function syntax ranges.
194///
195/// Function-scoped types are already resolved during the per-function loop.
196fn resolve_module_level_types(
197    pf: &ParsedFile<'_>,
198    display_target: DisplayTarget,
199    fn_ranges: &[ra_ap_syntax::TextRange],
200    resolved: &mut BTreeMap<(usize, usize), Box<str>>,
201) {
202    for node in pf.tree.syntax().descendants() {
203        let range = node.text_range();
204        if fn_ranges.iter().any(|fr| fr.contains_range(range)) {
205            continue;
206        }
207        resolve_type_bearing_node(pf, &node, display_target, resolved);
208    }
209}
210
211/// Classify and resolve a single type-bearing syntax node.
212fn resolve_type_bearing_node(
213    pf: &ParsedFile<'_>,
214    node: &ra_ap_syntax::SyntaxNode,
215    display_target: DisplayTarget,
216    resolved: &mut BTreeMap<(usize, usize), Box<str>>,
217) {
218    match node.kind() {
219        SyntaxKind::LET_STMT => {
220            resolve_let_stmt_type(pf, node, display_target, resolved);
221        }
222        SyntaxKind::PATH_TYPE
223        | SyntaxKind::TUPLE_TYPE
224        | SyntaxKind::ARRAY_TYPE
225        | SyntaxKind::SLICE_TYPE
226        | SyntaxKind::REF_TYPE
227        | SyntaxKind::PTR_TYPE => {
228            resolve_type_node(pf, node, display_target, resolved);
229        }
230        _ => {}
231    }
232}
233
234/// Resolve the type annotation on a let statement and cache at the annotation's position.
235fn resolve_let_stmt_type(
236    pf: &ParsedFile<'_>,
237    node: &ra_ap_syntax::SyntaxNode,
238    display_target: DisplayTarget,
239    resolved: &mut BTreeMap<(usize, usize), Box<str>>,
240) {
241    let Some(let_stmt) = ast::LetStmt::cast(node.clone()) else {
242        return;
243    };
244
245    // Resolve from initializer expression type.
246    let init_resolved = let_stmt.initializer().and_then(|init| {
247        let type_str = resolve_expr_type(&pf.sema, &init, pf.db, display_target)?;
248        let lc = pf.line_index.line_col(init.syntax().text_range().start());
249        Some(((lc.line + 1) as usize, lc.col as usize, type_str))
250    });
251    if let Some((line, col, type_str)) = init_resolved {
252        resolved.insert((line, col), type_str);
253    }
254
255    // Resolve from type annotation if present.
256    let ann_resolved = let_stmt.ty().and_then(|ty| {
257        let type_str = resolve_ast_type(&pf.sema, &ty, pf.db, display_target)?;
258        let lc = pf.line_index.line_col(ty.syntax().text_range().start());
259        Some(((lc.line + 1) as usize, lc.col as usize, type_str))
260    });
261    if let Some((line, col, type_str)) = ann_resolved {
262        resolved.insert((line, col), type_str);
263    }
264}
265
266/// Resolve a standalone type syntax node.
267fn resolve_type_node(
268    pf: &ParsedFile<'_>,
269    node: &ra_ap_syntax::SyntaxNode,
270    display_target: DisplayTarget,
271    resolved: &mut BTreeMap<(usize, usize), Box<str>>,
272) {
273    let Some(ty) = ast::Type::cast(node.clone()) else {
274        return;
275    };
276    let Some(type_str) = resolve_ast_type(&pf.sema, &ty, pf.db, display_target) else {
277        return;
278    };
279    let range = ty.syntax().text_range();
280    let lc = pf.line_index.line_col(range.start());
281    resolved.insert(((lc.line + 1) as usize, lc.col as usize), type_str);
282}
283
284/// Resolve an expression's type to a canonical string.
285fn resolve_expr_type(
286    sema: &Semantics<'_, RootDatabase>,
287    expr: &ast::Expr,
288    db: &RootDatabase,
289    display_target: DisplayTarget,
290) -> Option<Box<str>> {
291    let ty_info = sema.type_of_expr(expr)?;
292    Some(format_type(&ty_info.original, db, display_target))
293}
294
295/// Resolve an AST type node to a canonical string.
296fn resolve_ast_type(
297    sema: &Semantics<'_, RootDatabase>,
298    ty: &ast::Type,
299    db: &RootDatabase,
300    display_target: DisplayTarget,
301) -> Option<Box<str>> {
302    sema.resolve_type(ty)
303        .map(|resolved| format_type(&resolved, db, display_target))
304        .or_else(|| resolve_path_type(sema, ty, db, display_target))
305}
306
307/// Resolve a path type by looking up the path definition.
308fn resolve_path_type(
309    sema: &Semantics<'_, RootDatabase>,
310    ty: &ast::Type,
311    db: &RootDatabase,
312    display_target: DisplayTarget,
313) -> Option<Box<str>> {
314    let ast::Type::PathType(p) = ty else {
315        return None;
316    };
317    let path = p.path()?;
318    let resolution = sema.resolve_path(&path)?;
319    match resolution {
320        ra_ap_hir::PathResolution::Def(module_def) => {
321            resolve_module_def_type(module_def, db, display_target)
322        }
323        _ => None,
324    }
325}
326
327/// Get the type representation for a module-level definition.
328fn resolve_module_def_type(
329    def: ra_ap_hir::ModuleDef,
330    db: &RootDatabase,
331    display_target: DisplayTarget,
332) -> Option<Box<str>> {
333    match def {
334        ra_ap_hir::ModuleDef::TypeAlias(alias) => {
335            Some(format_type(&alias.ty(db), db, display_target))
336        }
337        ra_ap_hir::ModuleDef::Adt(adt) => Some(format_type(&adt.ty(db), db, display_target)),
338        ra_ap_hir::ModuleDef::BuiltinType(builtin) => Some(builtin.name().as_str().into()),
339        _ => None,
340    }
341}