1mod node;
8mod rewrite;
9
10use crate::code_cache::CodeCache;
11use crate::error::AqlError;
12use crate::matcher;
13use crate::matcher::Matchable;
14use crate::resolver::ResolverRegistry;
15use crate::selector::parse_selector;
16use crate::store::AnnotationStore;
17use crate::types::{AttrName, Binding, CodeElementName, RelativePath, Scope, TagName};
18use node::{build_unified_nodes, flatten_annotations_recursive};
19use rustc_hash::FxHashMap;
20use serde::Serialize;
21use serde_json::Value as JsonValue;
22
23use crate::resolver::SourceLocation;
24
25const ANN_TAG_ATTR: &str = "ann_tag";
27
28#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
30#[cfg_attr(feature = "flow", derive(flowjs_rs::Flow))]
31#[cfg_attr(feature = "ts", ts(export))]
32#[cfg_attr(feature = "flow", flow(export))]
33#[derive(Debug, Clone, Serialize)]
34pub struct QueryResult {
35 pub code_element: CodeElementSummary,
37 pub annotations: Vec<AnnotationSummary>,
39}
40
41#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
43#[cfg_attr(feature = "flow", derive(flowjs_rs::Flow))]
44#[cfg_attr(feature = "ts", ts(export))]
45#[cfg_attr(feature = "flow", flow(export))]
46#[derive(Debug, Clone, Serialize)]
47pub struct CodeElementSummary {
48 pub tag: TagName,
50 pub name: CodeElementName,
52 pub file: RelativePath,
54 pub source: SourceLocation,
56 #[cfg_attr(
58 feature = "ts",
59 ts(as = "std::collections::HashMap<AttrName, serde_json::Value>")
60 )]
61 #[cfg_attr(
62 feature = "flow",
63 flow(as = "std::collections::HashMap<AttrName, serde_json::Value>")
64 )]
65 pub attrs: FxHashMap<AttrName, JsonValue>,
66}
67
68#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
70#[cfg_attr(feature = "flow", derive(flowjs_rs::Flow))]
71#[cfg_attr(feature = "ts", ts(export))]
72#[cfg_attr(feature = "flow", flow(export))]
73#[derive(Debug, Clone, Serialize)]
74pub struct AnnotationSummary {
75 pub tag: TagName,
77 #[cfg_attr(
79 feature = "ts",
80 ts(as = "std::collections::HashMap<AttrName, serde_json::Value>")
81 )]
82 #[cfg_attr(
83 feature = "flow",
84 flow(as = "std::collections::HashMap<AttrName, serde_json::Value>")
85 )]
86 pub attrs: FxHashMap<AttrName, JsonValue>,
87 pub binding: Binding,
89}
90
91#[must_use = "running a query is useless without inspecting the results"]
101#[cfg_attr(dylint_lib = "amql_lints", allow(no_mut_params))] pub fn unified_query(
103 selector_str: &str,
104 scope: &Scope,
105 code_cache: &mut CodeCache,
106 store: &mut AnnotationStore,
107 resolvers: &ResolverRegistry,
108 opts: Option<&crate::query_options::QueryOptions>,
109) -> Result<Vec<QueryResult>, AqlError> {
110 let code_tags = resolvers.all_code_tags();
112 let rewritten = rewrite::rewrite_selector(selector_str, &code_tags);
113 let selector = parse_selector(&rewritten)?;
114
115 code_cache.ensure_scope(scope, resolvers);
117
118 let _ = store.refresh_scope(scope);
120
121 let elements = code_cache.elements_in_scope(scope);
123 let ann_by_file = store.annotations_in_scope(scope);
124 let ann_map: FxHashMap<&RelativePath, Vec<&crate::store::Annotation>> =
125 ann_by_file.into_iter().collect();
126
127 let mut unified_nodes = Vec::new();
128
129 for (rel_path, root) in &elements {
130 let file_annotations = ann_map.get(*rel_path).cloned().unwrap_or_default();
131 let flat_anns = flatten_annotations_recursive(&file_annotations);
133 build_unified_nodes(root, &flat_anns, &mut unified_nodes);
134 }
135
136 let matchable_refs: Vec<&dyn Matchable> =
138 unified_nodes.iter().map(|n| n as &dyn Matchable).collect();
139 let parent_indices: Vec<Option<usize>> = unified_nodes.iter().map(|n| n.parent_idx).collect();
140 let matched_indices =
141 matcher::filter_by_selector_indexed(&matchable_refs, &parent_indices, &selector);
142
143 let mut seen: FxHashMap<String, usize> = FxHashMap::default();
145 let mut results: Vec<QueryResult> = Vec::new();
146
147 for idx in matched_indices {
148 let node = &unified_nodes[idx];
149
150 let dedup_key = format!(
151 "{}:{}:{}",
152 node.source.file, node.source.line, node.source.column
153 );
154
155 if let Some(&idx) = seen.get(&dedup_key) {
156 if let Some(ann) = node.annotation {
158 results[idx].annotations.push(AnnotationSummary {
159 tag: ann.tag.clone(),
160 attrs: ann.attrs.clone(),
161 binding: ann.binding.clone(),
162 });
163 }
164 } else {
165 let name = node
166 .merged_attrs
167 .get("name")
168 .and_then(|v| v.as_str())
169 .unwrap_or("");
170
171 let mut code_attrs = node.merged_attrs.clone();
173 code_attrs.remove(ANN_TAG_ATTR);
174
175 let annotations = match node.annotation {
176 Some(ann) => vec![AnnotationSummary {
177 tag: ann.tag.clone(),
178 attrs: ann.attrs.clone(),
179 binding: ann.binding.clone(),
180 }],
181 None => vec![],
182 };
183
184 let idx = results.len();
185 seen.insert(dedup_key, idx);
186 results.push(QueryResult {
187 code_element: CodeElementSummary {
188 tag: node.code_tag.clone(),
189 name: CodeElementName::from(name),
190 file: node.source.file.clone(),
191 source: node.source.clone(),
192 attrs: code_attrs,
193 },
194 annotations,
195 });
196 }
197 }
198
199 let results = match opts {
200 Some(o) => crate::query_options::apply_to_query_results(results, o),
201 None => results,
202 };
203
204 Ok(results)
205}
206
207#[must_use = "batch query results should be inspected"]
212pub fn batch_query(
213 selectors: &[&str],
214 scope: &Scope,
215 code_cache: &mut CodeCache,
216 store: &mut AnnotationStore,
217 resolvers: &ResolverRegistry,
218 opts: Option<&crate::query_options::QueryOptions>,
219) -> Result<Vec<Vec<QueryResult>>, AqlError> {
220 if selectors.is_empty() {
221 return Ok(vec![]);
222 }
223
224 let code_tags = resolvers.all_code_tags();
226 code_cache.ensure_scope(scope, resolvers);
227 let _ = store.refresh_scope(scope);
228
229 let elements = code_cache.elements_in_scope(scope);
231 let ann_by_file = store.annotations_in_scope(scope);
232 let ann_map: FxHashMap<&RelativePath, Vec<&crate::store::Annotation>> =
233 ann_by_file.into_iter().collect();
234
235 let mut unified_nodes = Vec::new();
236
237 for (rel_path, root) in &elements {
238 let file_annotations = ann_map.get(*rel_path).cloned().unwrap_or_default();
239 let flat_anns = flatten_annotations_recursive(&file_annotations);
240 build_unified_nodes(root, &flat_anns, &mut unified_nodes);
241 }
242
243 let matchable_refs: Vec<&dyn Matchable> =
244 unified_nodes.iter().map(|n| n as &dyn Matchable).collect();
245 let parent_indices: Vec<Option<usize>> = unified_nodes.iter().map(|n| n.parent_idx).collect();
246
247 let parsed: Vec<_> = selectors
249 .iter()
250 .map(|s| {
251 let rewritten = rewrite::rewrite_selector(s, &code_tags);
252 parse_selector(&rewritten)
253 })
254 .collect::<Result<Vec<_>, _>>()?;
255
256 let all_matched: Vec<Vec<usize>> = parsed
258 .iter()
259 .map(|sel| matcher::filter_by_selector_indexed(&matchable_refs, &parent_indices, sel))
260 .collect();
261
262 let results: Vec<Vec<QueryResult>> = all_matched
264 .into_iter()
265 .map(|matched_indices| build_results(&unified_nodes, matched_indices))
266 .collect();
267
268 let results = match opts {
269 Some(o) => results
270 .into_iter()
271 .map(|set| crate::query_options::apply_to_query_results(set, o))
272 .collect(),
273 None => results,
274 };
275
276 Ok(results)
277}
278
279fn build_results(
281 unified_nodes: &[node::UnifiedNode<'_>],
282 matched_indices: Vec<usize>,
283) -> Vec<QueryResult> {
284 let mut seen: FxHashMap<String, usize> = FxHashMap::default();
285 let mut results: Vec<QueryResult> = Vec::new();
286
287 for idx in matched_indices {
288 let node = &unified_nodes[idx];
289 let dedup_key = format!(
290 "{}:{}:{}",
291 node.source.file, node.source.line, node.source.column
292 );
293
294 if let Some(&result_idx) = seen.get(&dedup_key) {
295 if let Some(ann) = node.annotation {
296 results[result_idx].annotations.push(AnnotationSummary {
297 tag: ann.tag.clone(),
298 attrs: ann.attrs.clone(),
299 binding: ann.binding.clone(),
300 });
301 }
302 } else {
303 let name = node
304 .merged_attrs
305 .get("name")
306 .and_then(|v| v.as_str())
307 .unwrap_or("");
308
309 let mut code_attrs = node.merged_attrs.clone();
310 code_attrs.remove(ANN_TAG_ATTR);
311
312 let annotations = match node.annotation {
313 Some(ann) => vec![AnnotationSummary {
314 tag: ann.tag.clone(),
315 attrs: ann.attrs.clone(),
316 binding: ann.binding.clone(),
317 }],
318 None => vec![],
319 };
320
321 let result_idx = results.len();
322 seen.insert(dedup_key, result_idx);
323 results.push(QueryResult {
324 code_element: CodeElementSummary {
325 tag: node.code_tag.clone(),
326 name: CodeElementName::from(name),
327 file: node.source.file.clone(),
328 source: node.source.clone(),
329 attrs: code_attrs,
330 },
331 annotations,
332 });
333 }
334 }
335
336 results
337}