1pub mod ids;
9pub mod output;
10
11use std::path::PathBuf;
12
13pub use ids::{DataNodeId, FileId, IdGenerator, SnapshotId, SymbolId};
14pub use output::{ClassEntry, FuncEntry, InspectOutput, ObjectEntry};
15
16use crate::language::LangId;
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct UnresolvedImport {
21 pub import_specifier: String,
22 pub alias: Option<String>,
23 pub symbol: Option<String>,
24 pub star: bool,
25 pub range: SourceRange,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct UnresolvedReference {
30 pub name: String,
31 pub range: SourceRange,
32}
33
34#[derive(Debug, Clone, Serialize)]
35pub struct FileExtraction {
36 pub path: PathBuf,
37 pub lang: LangId,
38 pub symbols: Vec<Symbol>,
39 pub imports: Vec<UnresolvedImport>,
40 pub references: Vec<UnresolvedReference>,
41 pub diagnostics: Vec<crate::error::Diagnostic>,
42 pub ast_node_count: usize,
44 #[cfg(feature = "metacall-deploy")]
45 pub call_sites: Vec<crate::deploy::scanner::CallSite>,
46 #[cfg(feature = "dataflow")]
47 pub data_nodes: Vec<DataNode>,
48 #[cfg(feature = "dataflow")]
49 pub flow_edges: Vec<FlowEdge>,
50}
51
52impl FileExtraction {
53 pub fn empty(path: PathBuf, lang: LangId) -> Self {
58 Self {
59 path,
60 lang,
61 symbols: Vec::new(),
62 imports: Vec::new(),
63 references: Vec::new(),
64 diagnostics: Vec::new(),
65 ast_node_count: 0,
66 #[cfg(feature = "metacall-deploy")]
67 call_sites: Vec::new(),
68 #[cfg(feature = "dataflow")]
69 data_nodes: Vec::new(),
70 #[cfg(feature = "dataflow")]
71 flow_edges: Vec::new(),
72 }
73 }
74
75 pub fn failed(path: PathBuf, lang: LangId, message: String) -> Self {
77 let mut out = Self::empty(path.clone(), lang);
78 out.diagnostics.push(crate::error::Diagnostic {
79 path,
80 severity: crate::error::Severity::Error,
81 message,
82 source_range: None,
83 });
84 out
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89pub struct LineColumn {
90 pub line: usize,
91 pub column: usize,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct SourceRange {
96 pub byte_start: usize,
97 pub byte_end: usize,
98 pub start: LineColumn,
99 pub end: LineColumn,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
103#[non_exhaustive]
104pub enum SymbolKind {
105 Function,
106 Method,
107 Class,
108 Struct,
109 Interface,
110 Trait,
111 Enum,
112 Object,
113 Constant,
114 Static,
115 Module,
116 Namespace,
117 TypeAlias,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121#[non_exhaustive]
122pub enum Visibility {
123 Public,
124 Private,
125}
126
127#[derive(Debug, Clone, Serialize)]
128pub struct Symbol {
129 pub id: SymbolId,
130 pub name: String,
131 pub kind: SymbolKind,
132 pub language: LangId,
133 pub file_path: PathBuf,
134 pub source_range: SourceRange,
135 pub visibility: Option<Visibility>,
136 pub signature: Option<String>,
137 pub docstring: Option<String>,
138 pub is_async: bool,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
143#[non_exhaustive]
144pub enum DataScope {
145 Local,
146 Parameter,
147 Member,
148 Closure,
149 Temporary,
150}
151
152impl DataScope {
153 pub fn as_str(self) -> &'static str {
154 match self {
155 DataScope::Local => "local",
156 DataScope::Parameter => "parameter",
157 DataScope::Member => "member",
158 DataScope::Closure => "closure",
159 DataScope::Temporary => "temporary",
160 }
161 }
162}
163
164#[derive(Debug, Clone, Serialize)]
166pub struct DataNode {
167 pub id: DataNodeId,
168 pub symbol_id: Option<SymbolId>,
169 pub name: Option<String>,
170 pub scope: DataScope,
171 pub type_hint: Option<String>,
172 pub source_range: SourceRange,
173}
174
175#[derive(Debug, Clone, Serialize)]
177pub struct FlowEdge {
178 pub source: DataNodeId,
179 pub target: DataNodeId,
180 pub kind: FlowKind,
181 pub confidence: f32,
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
186#[non_exhaustive]
187pub enum FlowKind {
188 DefUse,
189 Argument,
190 Return,
191 FieldAccess,
192}
193
194impl FlowKind {
195 pub fn as_str(self) -> &'static str {
196 match self {
197 FlowKind::DefUse => "def_use",
198 FlowKind::Argument => "argument",
199 FlowKind::Return => "return",
200 FlowKind::FieldAccess => "field_access",
201 }
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use serde_json;
209
210 fn sample_source_range() -> SourceRange {
211 SourceRange {
212 byte_start: 0,
213 byte_end: 10,
214 start: LineColumn { line: 1, column: 0 },
215 end: LineColumn {
216 line: 1,
217 column: 10,
218 },
219 }
220 }
221
222 #[test]
223 fn symbol_construction_all_fields() {
224 let sym = Symbol {
225 id: SymbolId::new(42).unwrap(),
226 name: "my_func".into(),
227 kind: SymbolKind::Function,
228 language: LangId::Rust,
229 file_path: PathBuf::from("src/main.rs"),
230 source_range: sample_source_range(),
231 visibility: Some(Visibility::Public),
232 signature: Some("fn my_func() -> bool".into()),
233 docstring: Some("does a thing".into()),
234 is_async: true,
235 };
236 assert_eq!(sym.id, SymbolId::new(42).unwrap());
237 assert_eq!(sym.name, "my_func");
238 assert!(matches!(sym.kind, SymbolKind::Function));
239 assert_eq!(sym.language, LangId::Rust);
240 assert_eq!(sym.file_path, PathBuf::from("src/main.rs"));
241 assert_eq!(sym.visibility, Some(Visibility::Public));
242 assert_eq!(sym.signature.as_deref(), Some("fn my_func() -> bool"));
243 assert_eq!(sym.docstring.as_deref(), Some("does a thing"));
244 assert!(sym.is_async);
245 }
246
247 #[test]
248 fn symbol_with_optional_fields_none() {
249 let sym = Symbol {
250 id: SymbolId::new(1).unwrap(),
251 name: "x".into(),
252 kind: SymbolKind::Constant,
253 language: LangId::Python,
254 file_path: PathBuf::from("a.py"),
255 source_range: sample_source_range(),
256 visibility: None,
257 signature: None,
258 docstring: None,
259 is_async: false,
260 };
261 assert!(sym.visibility.is_none());
262 assert!(sym.signature.is_none());
263 assert!(sym.docstring.is_none());
264 assert!(!sym.is_async);
265 }
266
267 #[test]
268 fn source_range_fields() {
269 let sr = sample_source_range();
270 assert_eq!(sr.byte_start, 0);
271 assert_eq!(sr.byte_end, 10);
272 assert_eq!(sr.start, LineColumn { line: 1, column: 0 });
273 assert_eq!(
274 sr.end,
275 LineColumn {
276 line: 1,
277 column: 10
278 }
279 );
280 }
281
282 #[test]
283 fn line_column_zero_indexed() {
284 let lc = LineColumn { line: 0, column: 0 };
285 assert_eq!(lc.line, 0);
286 assert_eq!(lc.column, 0);
287 }
288
289 #[test]
290 fn visibility_serialization() {
291 assert_eq!(
292 serde_json::to_string(&Visibility::Public).unwrap(),
293 "\"Public\""
294 );
295 assert_eq!(
296 serde_json::to_string(&Visibility::Private).unwrap(),
297 "\"Private\""
298 );
299 }
300
301 #[test]
302 fn symbol_kind_all_variants_serialize() {
303 let variants: Vec<SymbolKind> = vec![
304 SymbolKind::Function,
305 SymbolKind::Method,
306 SymbolKind::Class,
307 SymbolKind::Struct,
308 SymbolKind::Interface,
309 SymbolKind::Trait,
310 SymbolKind::Enum,
311 SymbolKind::Object,
312 SymbolKind::Constant,
313 SymbolKind::Static,
314 SymbolKind::Module,
315 SymbolKind::Namespace,
316 SymbolKind::TypeAlias,
317 ];
318 for v in &variants {
319 let json = serde_json::to_string(v).unwrap();
320 assert!(
321 json.starts_with('"') && json.ends_with('"'),
322 "expected a JSON string, got: {json}"
323 );
324 assert!(
325 json.len() > 2,
326 "expected non-empty variant name, got: {json}"
327 );
328 }
329 }
330
331 #[test]
332 fn symbol_serde_roundtrip() {
333 let sym = Symbol {
334 id: SymbolId::new(7).unwrap(),
335 name: "roundtrip_fn".into(),
336 kind: SymbolKind::Method,
337 language: LangId::Go,
338 file_path: PathBuf::from("main.go"),
339 source_range: sample_source_range(),
340 visibility: Some(Visibility::Private),
341 signature: Some("func (t T) roundtripFn()".into()),
342 docstring: Some("doc".into()),
343 is_async: false,
344 };
345 let json = serde_json::to_string(&sym).unwrap();
346 let val: serde_json::Value = serde_json::from_str(&json).unwrap();
347 assert_eq!(val["name"], "roundtrip_fn");
348 assert_eq!(val["kind"], "Method");
349 assert_eq!(val["is_async"], false);
350 }
351
352 #[test]
353 fn data_scope_as_str_all_variants() {
354 assert_eq!(DataScope::Local.as_str(), "local");
355 assert_eq!(DataScope::Parameter.as_str(), "parameter");
356 assert_eq!(DataScope::Member.as_str(), "member");
357 assert_eq!(DataScope::Closure.as_str(), "closure");
358 assert_eq!(DataScope::Temporary.as_str(), "temporary");
359 }
360
361 #[test]
362 fn data_scope_serialization() {
363 assert_eq!(
364 serde_json::to_string(&DataScope::Local).unwrap(),
365 "\"Local\""
366 );
367 assert_eq!(
368 serde_json::to_string(&DataScope::Parameter).unwrap(),
369 "\"Parameter\""
370 );
371 }
372
373 #[test]
374 fn flow_kind_as_str_all_variants() {
375 assert_eq!(FlowKind::DefUse.as_str(), "def_use");
376 assert_eq!(FlowKind::Argument.as_str(), "argument");
377 assert_eq!(FlowKind::Return.as_str(), "return");
378 assert_eq!(FlowKind::FieldAccess.as_str(), "field_access");
379 }
380
381 #[test]
382 fn flow_kind_serialization() {
383 assert_eq!(
384 serde_json::to_string(&FlowKind::DefUse).unwrap(),
385 "\"DefUse\""
386 );
387 assert_eq!(
388 serde_json::to_string(&FlowKind::Argument).unwrap(),
389 "\"Argument\""
390 );
391 assert_eq!(
392 serde_json::to_string(&FlowKind::Return).unwrap(),
393 "\"Return\""
394 );
395 assert_eq!(
396 serde_json::to_string(&FlowKind::FieldAccess).unwrap(),
397 "\"FieldAccess\""
398 );
399 }
400
401 #[test]
402 fn data_node_construction_and_serde() {
403 let data = DataNode {
404 id: DataNodeId::new(10).unwrap(),
405 symbol_id: Some(SymbolId::new(5).unwrap()),
406 name: Some("x".into()),
407 scope: DataScope::Local,
408 type_hint: Some("int".into()),
409 source_range: sample_source_range(),
410 };
411 let json = serde_json::to_string(&data).unwrap();
412 let val: serde_json::Value = serde_json::from_str(&json).unwrap();
413 assert_eq!(val["name"], "x");
414 assert_eq!(val["scope"], "Local");
415 assert_eq!(val["type_hint"], "int");
416 }
417
418 #[test]
419 fn data_node_optional_fields_none() {
420 let data = DataNode {
421 id: DataNodeId::new(1).unwrap(),
422 symbol_id: None,
423 name: None,
424 scope: DataScope::Temporary,
425 type_hint: None,
426 source_range: sample_source_range(),
427 };
428 assert!(data.symbol_id.is_none());
429 assert!(data.name.is_none());
430 assert!(data.type_hint.is_none());
431 }
432
433 #[test]
434 fn flow_edge_serde() {
435 let edge = FlowEdge {
436 source: DataNodeId::new(1).unwrap(),
437 target: DataNodeId::new(2).unwrap(),
438 kind: FlowKind::Argument,
439 confidence: 0.85,
440 };
441 let json = serde_json::to_string(&edge).unwrap();
442 let val: serde_json::Value = serde_json::from_str(&json).unwrap();
443 assert_eq!(val["source"], 1);
444 assert_eq!(val["target"], 2);
445 assert_eq!(val["kind"], "Argument");
446 assert_eq!(val["confidence"], 0.85);
447 }
448
449 #[test]
450 fn data_node_id_serde_roundtrip() {
451 let original = DataNodeId::new(99).unwrap();
452 let json = serde_json::to_string(&original).unwrap();
453 let roundtrip: DataNodeId = serde_json::from_str(&json).unwrap();
454 assert_eq!(original, roundtrip);
455 }
456}