mir_analyzer/db/reference_locations.rs
1use std::sync::Arc;
2
3use mir_issues::Issue;
4
5use crate::pass2::Pass2Driver;
6use crate::PhpVersion;
7
8use super::*;
9
10// S4 Step 1: analyze_file accumulators + tracked-query skeleton
11//
12// First step toward S4 (issues + reference locations as Salsa accumulators,
13// `analyze_file` as a tracked query). This step is purely additive:
14//
15// 1. Defines `IssueAccumulator` and `RefLocAccumulator` salsa accumulator
16// types — push targets for analyzer-emitted issues and reference-index
17// entries during tracked-query evaluation.
18// 2. Defines `analyze_file` as a tracked-query stub keyed on a
19// `(SourceFile, AnalyzeFileInput)` pair. The stub does NOT perform
20// analysis — it accumulates only the parse errors (a strict subset of
21// what `collect_file_definitions` already produces, so semantics are
22// unchanged). The full analyzer wiring follows in subsequent S4 PRs.
23//
24// Nothing in this module is wired into the batch (`analyze`) or LSP
25// (`re_analyze_file`) paths yet. Behavior change: zero.
26
27/// Salsa accumulator carrying analyzer-emitted issues. In the eventual
28/// S4 design, every site that today calls `IssueBuffer::add` / `Vec::push`
29/// from inside a tracked query will instead call
30/// `IssueAccumulator(issue).accumulate(db)`, and `re_analyze_file` will read
31/// the accumulated issues for the file with
32/// `analyze_file::accumulated::<IssueAccumulator>(db, file, ...)`.
33#[salsa::accumulator]
34#[derive(Clone, Debug)]
35pub struct IssueAccumulator(pub Issue);
36
37/// Reference-index entry as produced during analysis. Mirrors the tuple
38/// shape that `Codebase::record_ref` accepts:
39///
40/// - `symbol_key`: interner-bound string (`"fn:foo"`, `"cls:Foo"`,
41/// `"prop:Foo::$bar"`, `"cnst:Foo::BAR"`, `"meth:Foo::bar"` — same keys
42/// `Codebase::mark_*_referenced_at` use).
43/// - `file`: the file in which the reference appears.
44/// - `(line, col_start, col_end)`: span within the file.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct RefLoc {
47 pub symbol_key: Arc<str>,
48 pub file: Arc<str>,
49 pub line: u32,
50 pub col_start: u16,
51 pub col_end: u16,
52}
53
54/// Salsa accumulator carrying reference-index entries. In the eventual
55/// S4 design this replaces the `Codebase::mark_*_referenced_at` side
56/// effects: instead of mutating the codebase's reference index inside a
57/// tracked query (which Salsa cannot observe), the analyzer pushes
58/// `RefLocAccumulator(loc)` and the consumer (LSP / dead-code) reads via
59/// `analyze_file::accumulated::<RefLocAccumulator>(db, …)`.
60#[salsa::accumulator]
61#[derive(Clone, Debug)]
62pub struct RefLocAccumulator(pub RefLoc);
63
64/// Salsa tracked-query input for `analyze_file`. Carries the analysis
65/// parameters that aren't already captured by `SourceFile` itself. Kept
66/// minimal in this PR; subsequent PRs in the S4 chain will extend it as
67/// the query body grows to call the full analyzer pipeline.
68#[salsa::input]
69pub struct AnalyzeFileInput {
70 /// Resolved PHP version (`"8.1"`, `"8.2"`, …) used by the analyzer.
71 /// Mirrors `ProjectAnalyzer::resolved_php_version`.
72 pub php_version: Arc<str>,
73}
74
75// S4 Step 3: Lazy inferred-type queries
76//
77// These tracked queries compute inferred return types on-demand during Pass 2.
78// When `Pass2Driver` encounters a function/method call, it reads the inferred
79// type via these queries instead of from a pre-computed buffer.
80//
81// This enables two key optimizations:
82// 1. Single-pass execution: inferred types are computed as needed, not upfront
83// 2. Incremental caching: if a dependent file doesn't call a function, its
84// inferred type is never computed (Salsa skips the query)
85
86// Helper: collect analysis results via tracked query accumulators
87
88/// Collects all accumulated issues from a set of files analyzed via the
89/// `analyze_file` tracked query. Used during batch analysis to read issues
90/// that were emitted during tracked-query evaluation.
91#[allow(dead_code)]
92pub(crate) fn collect_accumulated_issues(
93 db: &dyn MirDatabase,
94 files: &[(Arc<str>, SourceFile)],
95 php_version: &str,
96) -> Vec<Issue> {
97 let mut all_issues = Vec::new();
98 let input = AnalyzeFileInput::new(db, Arc::from(php_version));
99
100 for (_path, file) in files {
101 // Call the tracked query to trigger analysis + accumulation
102 analyze_file(db, *file, input);
103
104 // Read back the accumulated issues for this file
105 let accumulated: Vec<&IssueAccumulator> = analyze_file::accumulated(db, *file, input);
106 for acc in accumulated {
107 all_issues.push(acc.0.clone());
108 }
109 }
110
111 all_issues
112}
113
114/// Tracked-query skeleton for `analyze_file`.
115///
116/// **Current behavior (S4 step 2):** parses the file, emits parse-error issues,
117/// and calls Pass 2 to analyze function/method bodies. Issues and reference
118/// locations are emitted via `IssueAccumulator` and `RefLocAccumulator`.
119///
120/// This is still a hybrid: inferred types come from the prior
121/// `run_inference_sweep` → `commit_inferred_return_types` in the double-pass
122/// orchestration. Future S4 PRs will replace that with lazy
123/// `inferred_return_type(node)` tracked queries.
124#[salsa::tracked]
125pub fn analyze_file(db: &dyn MirDatabase, file: SourceFile, input: AnalyzeFileInput) {
126 use salsa::Accumulator as _;
127 let path = file.path(db);
128 let text = file.text(db);
129
130 let arena = crate::arena::create_parse_arena(text.len());
131 let parsed = php_rs_parser::parse(&arena, &text);
132
133 // Emit parse errors
134 for err in &parsed.errors {
135 let issue = Issue::new(
136 mir_issues::IssueKind::ParseError {
137 message: err.to_string(),
138 },
139 mir_issues::Location {
140 file: path.clone(),
141 line: 1,
142 line_end: 1,
143 col_start: 0,
144 col_end: 0,
145 },
146 );
147 IssueAccumulator(issue).accumulate(db);
148 }
149
150 // If no parse errors, run full analysis via Pass2Driver
151 if parsed.errors.is_empty() {
152 use std::str::FromStr as _;
153 let php_version =
154 PhpVersion::from_str(input.php_version(db).as_ref()).unwrap_or(PhpVersion::LATEST);
155 let driver = Pass2Driver::new(db, php_version);
156 let (issues, _symbols) = driver.analyze_bodies(
157 &parsed.program,
158 path.clone(),
159 text.as_ref(),
160 &parsed.source_map,
161 );
162
163 // Emit issues via accumulator
164 for issue in issues {
165 IssueAccumulator(issue).accumulate(db);
166 }
167
168 // Emit reference locations via accumulator
169 let ref_locs = db.extract_file_reference_locations(&path);
170 for (symbol_key, line, col_start, col_end) in ref_locs {
171 let ref_loc = RefLoc {
172 symbol_key,
173 file: path.clone(),
174 line,
175 col_start,
176 col_end,
177 };
178 RefLocAccumulator(ref_loc).accumulate(db);
179 }
180 }
181}