1use crate::graph::{Edge, EdgeKind, Graph, Node, NodeKind, NodeRole, TerminalKind};
2use crate::semantic::{
3 ArtifactKind, SemanticAnnotation, SemanticArtifact, SemanticDocument, SemanticRelation,
4 SemanticSymbol, SemanticTarget,
5};
6
7#[derive(Debug, Clone, PartialEq, Eq, Default)]
8pub struct SymbolSelector {
9 pub id: Option<String>,
10 pub name: Option<String>,
11 pub kind: Option<NodeKind>,
12 pub module: Option<String>,
13 pub file_suffix: Option<String>,
14 pub annotation: Option<AnnotationSelector>,
15 pub property_key: Option<String>,
16}
17
18impl SymbolSelector {
19 pub fn by_kind(kind: NodeKind) -> Self {
20 Self {
21 kind: Some(kind),
22 ..Self::default()
23 }
24 }
25
26 pub fn with_annotation(mut self, annotation: AnnotationSelector) -> Self {
27 self.annotation = Some(annotation);
28 self
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum AnnotationSelector {
34 EntryPoint,
35 Terminal(TerminalKind),
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Default)]
39pub struct RelationSelector {
40 pub source: Option<String>,
41 pub relation_kind: Option<EdgeKind>,
42 pub target_symbol: Option<String>,
43 pub external_only: bool,
44 pub terminal_kind: Option<TerminalKind>,
45}
46
47impl RelationSelector {
48 pub fn calls() -> Self {
49 Self {
50 relation_kind: Some(EdgeKind::Calls),
51 ..Self::default()
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57pub struct ArtifactSelector {
58 pub kind: Option<ArtifactKind>,
59}
60
61pub fn select_semantic_symbols<'a>(
62 document: &'a SemanticDocument,
63 selector: &SymbolSelector,
64) -> Vec<&'a SemanticSymbol> {
65 document
66 .symbols
67 .iter()
68 .filter(|symbol| symbol_matches(symbol, selector))
69 .collect()
70}
71
72pub fn select_semantic_relations<'a>(
73 document: &'a SemanticDocument,
74 selector: &RelationSelector,
75) -> Vec<&'a SemanticRelation> {
76 document
77 .relations
78 .iter()
79 .filter(|relation| relation_matches(relation, selector))
80 .collect()
81}
82
83pub fn select_semantic_artifacts<'a>(
84 document: &'a SemanticDocument,
85 selector: &ArtifactSelector,
86) -> Vec<&'a SemanticArtifact> {
87 document
88 .artifacts
89 .iter()
90 .filter(|artifact| selector.kind.is_none_or(|kind| artifact.kind() == kind))
91 .collect()
92}
93
94pub fn select_graph_nodes<'a>(graph: &'a Graph, selector: &SymbolSelector) -> Vec<&'a Node> {
95 graph
96 .nodes
97 .iter()
98 .filter(|node| node_matches(node, selector))
99 .collect()
100}
101
102pub fn select_graph_edges<'a>(graph: &'a Graph, selector: &RelationSelector) -> Vec<&'a Edge> {
103 let node_ids: std::collections::HashSet<&str> =
104 graph.nodes.iter().map(|node| node.id.as_str()).collect();
105 graph
106 .edges
107 .iter()
108 .filter(|edge| edge_matches(edge, &node_ids, selector))
109 .collect()
110}
111
112fn symbol_matches(symbol: &SemanticSymbol, selector: &SymbolSelector) -> bool {
113 selector.id.as_ref().is_none_or(|id| symbol.id == *id)
114 && selector
115 .name
116 .as_ref()
117 .is_none_or(|name| symbol.name == *name)
118 && selector.kind.is_none_or(|kind| symbol.kind == kind)
119 && selector
120 .module
121 .as_ref()
122 .is_none_or(|module| symbol.module.as_deref() == Some(module.as_str()))
123 && selector
124 .file_suffix
125 .as_ref()
126 .is_none_or(|suffix| symbol.file.to_string_lossy().ends_with(suffix))
127 && selector
128 .annotation
129 .is_none_or(|annotation| symbol_annotation_matches(symbol, annotation))
130 && selector
131 .property_key
132 .as_ref()
133 .is_none_or(|key| symbol.properties.contains_key(key))
134}
135
136fn symbol_annotation_matches(symbol: &SemanticSymbol, selector: AnnotationSelector) -> bool {
137 symbol
138 .annotations
139 .iter()
140 .any(|annotation| match (annotation, selector) {
141 (SemanticAnnotation::EntryPoint, AnnotationSelector::EntryPoint) => true,
142 (SemanticAnnotation::Terminal { kind }, AnnotationSelector::Terminal(expected)) => {
143 *kind == expected
144 }
145 _ => false,
146 })
147}
148
149fn relation_matches(relation: &SemanticRelation, selector: &RelationSelector) -> bool {
150 selector
151 .source
152 .as_ref()
153 .is_none_or(|source| relation.source == *source)
154 && selector
155 .relation_kind
156 .is_none_or(|kind| relation.kind == kind)
157 && selector.target_symbol.as_ref().is_none_or(|target| {
158 matches!(&relation.target, SemanticTarget::Symbol(symbol_id) if symbol_id == target)
159 })
160 && (!selector.external_only || matches!(relation.target, SemanticTarget::ExternalRef(_)))
161 && selector
162 .terminal_kind
163 .is_none_or(|kind| relation.terminal_kind == Some(kind))
164}
165
166fn node_matches(node: &Node, selector: &SymbolSelector) -> bool {
167 selector.id.as_ref().is_none_or(|id| node.id == *id)
168 && selector.name.as_ref().is_none_or(|name| node.name == *name)
169 && selector.kind.is_none_or(|kind| node.kind == kind)
170 && selector
171 .module
172 .as_ref()
173 .is_none_or(|module| node.module.as_deref() == Some(module.as_str()))
174 && selector
175 .file_suffix
176 .as_ref()
177 .is_none_or(|suffix| node.file.to_string_lossy().ends_with(suffix))
178 && selector
179 .annotation
180 .is_none_or(|annotation| node_role_matches(node.role.as_ref(), annotation))
181 && selector
182 .property_key
183 .as_ref()
184 .is_none_or(|key| node.metadata.contains_key(key))
185}
186
187fn node_role_matches(role: Option<&NodeRole>, selector: AnnotationSelector) -> bool {
188 match (role, selector) {
189 (Some(NodeRole::EntryPoint), AnnotationSelector::EntryPoint) => true,
190 (Some(NodeRole::Terminal { kind }), AnnotationSelector::Terminal(expected)) => {
191 *kind == expected
192 }
193 _ => false,
194 }
195}
196
197fn edge_matches(
198 edge: &Edge,
199 node_ids: &std::collections::HashSet<&str>,
200 selector: &RelationSelector,
201) -> bool {
202 selector
203 .source
204 .as_ref()
205 .is_none_or(|source| edge.source == *source)
206 && selector.relation_kind.is_none_or(|kind| edge.kind == kind)
207 && selector
208 .target_symbol
209 .as_ref()
210 .is_none_or(|target| edge.target == *target)
211 && (!selector.external_only || !node_ids.contains(edge.target.as_str()))
212}
213
214#[cfg(test)]
215mod tests {
216 use std::collections::HashMap;
217 use std::path::PathBuf;
218
219 use crate::graph::{Edge, FlowDirection, Graph, Node, Span, Visibility};
220 use crate::semantic::{SemanticDocument, SemanticRelation, SemanticSymbol, SemanticTarget};
221
222 use super::*;
223
224 fn test_symbol() -> SemanticSymbol {
225 SemanticSymbol {
226 id: "body".to_string(),
227 kind: NodeKind::View,
228 name: "Text".to_string(),
229 file: PathBuf::from("ContentView.swift"),
230 span: Span {
231 start: [1, 0],
232 end: [2, 0],
233 },
234 visibility: Visibility::Public,
235 properties: HashMap::from([(
236 "swiftui.invalidation_source".to_string(),
237 "true".to_string(),
238 )]),
239 annotations: vec![SemanticAnnotation::EntryPoint],
240 signature: None,
241 doc_comment: None,
242 module: Some("Demo".to_string()),
243 snippet: None,
244 synthetic_kind: Some("swiftui_view".to_string()),
245 }
246 }
247
248 #[test]
249 fn selects_semantic_symbols_and_relations() {
250 let mut document = SemanticDocument::new();
251 document.symbols.push(test_symbol());
252 document.relations.push(SemanticRelation {
253 source: "body".to_string(),
254 target: SemanticTarget::ExternalRef("reqwest::get".to_string()),
255 kind: EdgeKind::Calls,
256 confidence: 1.0,
257 direction: Some(FlowDirection::Read),
258 operation: Some("HTTP".to_string()),
259 condition: None,
260 async_boundary: None,
261 provenance: Vec::new(),
262 terminal_kind: Some(TerminalKind::Network),
263 });
264
265 let symbols = select_semantic_symbols(
266 &document,
267 &SymbolSelector::by_kind(NodeKind::View)
268 .with_annotation(AnnotationSelector::EntryPoint),
269 );
270 assert_eq!(symbols.len(), 1);
271
272 let relations = select_semantic_relations(
273 &document,
274 &RelationSelector {
275 external_only: true,
276 terminal_kind: Some(TerminalKind::Network),
277 ..RelationSelector::calls()
278 },
279 );
280 assert_eq!(relations.len(), 1);
281 }
282
283 #[test]
284 fn selects_only_external_graph_edges_when_requested() {
285 let graph = Graph {
286 version: "0.1.0".to_string(),
287 nodes: vec![
288 Node {
289 id: "caller".to_string(),
290 kind: NodeKind::Function,
291 name: "load".to_string(),
292 file: PathBuf::from("main.rs"),
293 span: Span {
294 start: [1, 0],
295 end: [2, 0],
296 },
297 visibility: Visibility::Public,
298 metadata: HashMap::new(),
299 role: None,
300 signature: None,
301 doc_comment: None,
302 module: None,
303 snippet: None,
304 },
305 Node {
306 id: "callee".to_string(),
307 kind: NodeKind::Function,
308 name: "helper".to_string(),
309 file: PathBuf::from("main.rs"),
310 span: Span {
311 start: [3, 0],
312 end: [4, 0],
313 },
314 visibility: Visibility::Private,
315 metadata: HashMap::new(),
316 role: None,
317 signature: None,
318 doc_comment: None,
319 module: None,
320 snippet: None,
321 },
322 ],
323 edges: vec![
324 Edge {
325 source: "caller".to_string(),
326 target: "callee".to_string(),
327 kind: EdgeKind::Calls,
328 confidence: 1.0,
329 direction: None,
330 operation: None,
331 condition: None,
332 async_boundary: None,
333 provenance: Vec::new(),
334 },
335 Edge {
336 source: "caller".to_string(),
337 target: "reqwest::get".to_string(),
338 kind: EdgeKind::Calls,
339 confidence: 1.0,
340 direction: Some(FlowDirection::Read),
341 operation: Some("HTTP".to_string()),
342 condition: None,
343 async_boundary: None,
344 provenance: Vec::new(),
345 },
346 ],
347 };
348
349 let edges = select_graph_edges(
350 &graph,
351 &RelationSelector {
352 external_only: true,
353 ..RelationSelector::calls()
354 },
355 );
356
357 assert_eq!(edges.len(), 1);
358 assert_eq!(edges[0].target, "reqwest::get");
359 }
360}