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 #[serde(skip)]
50 pub text: Option<std::sync::Arc<str>>,
51 #[cfg(feature = "metacall-deploy")]
52 pub call_sites: Vec<crate::deploy::scanner::CallSite>,
53 #[cfg(feature = "dataflow")]
54 pub data_nodes: Vec<DataNode>,
55 #[cfg(feature = "dataflow")]
56 pub flow_edges: Vec<FlowEdge>,
57}
58
59impl FileExtraction {
60 pub fn empty(path: PathBuf, lang: LangId) -> Self {
65 Self {
66 path,
67 lang,
68 symbols: Vec::new(),
69 imports: Vec::new(),
70 references: Vec::new(),
71 diagnostics: Vec::new(),
72 ast_node_count: 0,
73 text: None,
74 #[cfg(feature = "metacall-deploy")]
75 call_sites: Vec::new(),
76 #[cfg(feature = "dataflow")]
77 data_nodes: Vec::new(),
78 #[cfg(feature = "dataflow")]
79 flow_edges: Vec::new(),
80 }
81 }
82
83 pub fn failed(path: PathBuf, lang: LangId, message: String) -> Self {
85 let mut out = Self::empty(path.clone(), lang);
86 out.diagnostics.push(crate::error::Diagnostic {
87 path,
88 severity: crate::error::Severity::Error,
89 message,
90 source_range: None,
91 });
92 out
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97pub struct LineColumn {
98 pub line: usize,
99 pub column: usize,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct SourceRange {
104 pub byte_start: usize,
105 pub byte_end: usize,
106 pub start: LineColumn,
107 pub end: LineColumn,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
111#[non_exhaustive]
112#[serde(rename_all = "snake_case")]
113pub enum SymbolKind {
114 Function,
115 Method,
116 Class,
117 Struct,
118 Interface,
119 Trait,
120 Enum,
121 Object,
122 Constant,
123 Static,
124 Module,
125 Namespace,
126 TypeAlias,
127 Declaration,
129}
130
131impl SymbolKind {
132 pub fn as_str(self) -> &'static str {
135 match self {
136 SymbolKind::Function => "function",
137 SymbolKind::Method => "method",
138 SymbolKind::Class => "class",
139 SymbolKind::Struct => "struct",
140 SymbolKind::Interface => "interface",
141 SymbolKind::Trait => "trait",
142 SymbolKind::Enum => "enum",
143 SymbolKind::Object => "object",
144 SymbolKind::Constant => "constant",
145 SymbolKind::Static => "static",
146 SymbolKind::Module => "module",
147 SymbolKind::Namespace => "namespace",
148 SymbolKind::TypeAlias => "type_alias",
149 SymbolKind::Declaration => "declaration",
150 }
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
155#[non_exhaustive]
156#[serde(rename_all = "snake_case")]
157pub enum Visibility {
158 Public,
159 Private,
160}
161
162impl Visibility {
163 pub fn as_str(self) -> &'static str {
165 match self {
166 Visibility::Public => "public",
167 Visibility::Private => "private",
168 }
169 }
170}
171
172#[derive(Debug, Clone, Serialize)]
173pub struct Symbol {
174 pub id: SymbolId,
175 pub name: String,
176 pub kind: SymbolKind,
177 pub language: LangId,
178 pub file_path: PathBuf,
179 pub source_range: SourceRange,
180 #[serde(skip_serializing_if = "Option::is_none")]
185 pub name_range: Option<SourceRange>,
186 pub visibility: Option<Visibility>,
187 pub signature: Option<String>,
188 pub docstring: Option<String>,
189 pub is_async: bool,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
194#[non_exhaustive]
195#[serde(rename_all = "snake_case")]
196pub enum DataScope {
197 Local,
198 Parameter,
199 Member,
200 Closure,
201 Temporary,
202}
203
204impl DataScope {
205 pub fn as_str(self) -> &'static str {
206 match self {
207 DataScope::Local => "local",
208 DataScope::Parameter => "parameter",
209 DataScope::Member => "member",
210 DataScope::Closure => "closure",
211 DataScope::Temporary => "temporary",
212 }
213 }
214}
215
216#[derive(Debug, Clone, Serialize)]
218pub struct DataNode {
219 pub id: DataNodeId,
220 pub symbol_id: Option<SymbolId>,
221 pub name: Option<String>,
222 pub scope: DataScope,
223 pub type_hint: Option<String>,
224 pub source_range: SourceRange,
225}
226
227#[derive(Debug, Clone, Serialize)]
229pub struct FlowEdge {
230 pub source: DataNodeId,
231 pub target: DataNodeId,
232 pub kind: FlowKind,
233 pub confidence: f32,
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
238#[non_exhaustive]
239#[serde(rename_all = "snake_case")]
240pub enum FlowKind {
241 DefUse,
242 Argument,
243 Return,
244 FieldAccess,
245}
246
247impl FlowKind {
248 pub fn as_str(self) -> &'static str {
249 match self {
250 FlowKind::DefUse => "def_use",
251 FlowKind::Argument => "argument",
252 FlowKind::Return => "return",
253 FlowKind::FieldAccess => "field_access",
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use serde_json;
262
263 fn sample_source_range() -> SourceRange {
264 SourceRange {
265 byte_start: 0,
266 byte_end: 10,
267 start: LineColumn { line: 1, column: 0 },
268 end: LineColumn {
269 line: 1,
270 column: 10,
271 },
272 }
273 }
274
275 #[test]
276 fn symbol_construction_all_fields() {
277 let sym = Symbol {
278 id: SymbolId::new(42).unwrap(),
279 name: "my_func".into(),
280 kind: SymbolKind::Function,
281 language: LangId::Rust,
282 file_path: PathBuf::from("src/main.rs"),
283 source_range: sample_source_range(),
284 name_range: None,
285 visibility: Some(Visibility::Public),
286 signature: Some("fn my_func() -> bool".into()),
287 docstring: Some("does a thing".into()),
288 is_async: true,
289 };
290 assert_eq!(sym.id, SymbolId::new(42).unwrap());
291 assert_eq!(sym.name, "my_func");
292 assert!(matches!(sym.kind, SymbolKind::Function));
293 assert_eq!(sym.language, LangId::Rust);
294 assert_eq!(sym.file_path, PathBuf::from("src/main.rs"));
295 assert_eq!(sym.visibility, Some(Visibility::Public));
296 assert_eq!(sym.signature.as_deref(), Some("fn my_func() -> bool"));
297 assert_eq!(sym.docstring.as_deref(), Some("does a thing"));
298 assert!(sym.is_async);
299 }
300
301 #[test]
302 fn symbol_with_optional_fields_none() {
303 let sym = Symbol {
304 id: SymbolId::new(1).unwrap(),
305 name: "x".into(),
306 kind: SymbolKind::Constant,
307 language: LangId::Python,
308 file_path: PathBuf::from("a.py"),
309 source_range: sample_source_range(),
310 name_range: None,
311 visibility: None,
312 signature: None,
313 docstring: None,
314 is_async: false,
315 };
316 assert!(sym.visibility.is_none());
317 assert!(sym.signature.is_none());
318 assert!(sym.docstring.is_none());
319 assert!(!sym.is_async);
320 }
321
322 #[test]
323 fn source_range_fields() {
324 let sr = sample_source_range();
325 assert_eq!(sr.byte_start, 0);
326 assert_eq!(sr.byte_end, 10);
327 assert_eq!(sr.start, LineColumn { line: 1, column: 0 });
328 assert_eq!(
329 sr.end,
330 LineColumn {
331 line: 1,
332 column: 10
333 }
334 );
335 }
336
337 #[test]
338 fn line_column_zero_indexed() {
339 let lc = LineColumn { line: 0, column: 0 };
340 assert_eq!(lc.line, 0);
341 assert_eq!(lc.column, 0);
342 }
343
344 #[test]
345 fn visibility_serialization() {
346 assert_eq!(
347 serde_json::to_string(&Visibility::Public).unwrap(),
348 "\"public\""
349 );
350 assert_eq!(
351 serde_json::to_string(&Visibility::Private).unwrap(),
352 "\"private\""
353 );
354 }
355
356 #[test]
357 fn symbol_kind_all_variants_serialize() {
358 let variants: Vec<SymbolKind> = vec![
359 SymbolKind::Function,
360 SymbolKind::Method,
361 SymbolKind::Class,
362 SymbolKind::Struct,
363 SymbolKind::Interface,
364 SymbolKind::Trait,
365 SymbolKind::Enum,
366 SymbolKind::Object,
367 SymbolKind::Constant,
368 SymbolKind::Static,
369 SymbolKind::Module,
370 SymbolKind::Namespace,
371 SymbolKind::TypeAlias,
372 SymbolKind::Declaration,
373 ];
374 for v in &variants {
375 let json = serde_json::to_string(v).unwrap();
376 assert!(
377 json.starts_with('"') && json.ends_with('"'),
378 "expected a JSON string, got: {json}"
379 );
380 assert!(
381 json.len() > 2,
382 "expected non-empty variant name, got: {json}"
383 );
384 assert!(
385 json.chars()
386 .all(|c| c.is_ascii_lowercase() || c == '_' || c == '"'),
387 "kind names are lowercase snake_case, got: {json}"
388 );
389 }
390 }
391
392 #[test]
393 fn symbol_serde_roundtrip() {
394 let sym = Symbol {
395 id: SymbolId::new(7).unwrap(),
396 name: "roundtrip_fn".into(),
397 kind: SymbolKind::Method,
398 language: LangId::Go,
399 file_path: PathBuf::from("main.go"),
400 source_range: sample_source_range(),
401 name_range: None,
402 visibility: Some(Visibility::Private),
403 signature: Some("func (t T) roundtripFn()".into()),
404 docstring: Some("doc".into()),
405 is_async: false,
406 };
407 let json = serde_json::to_string(&sym).unwrap();
408 let val: serde_json::Value = serde_json::from_str(&json).unwrap();
409 assert_eq!(val["name"], "roundtrip_fn");
410 assert_eq!(val["kind"], "method");
411 assert_eq!(val["is_async"], false);
412 }
413
414 #[test]
415 fn data_scope_as_str_all_variants() {
416 assert_eq!(DataScope::Local.as_str(), "local");
417 assert_eq!(DataScope::Parameter.as_str(), "parameter");
418 assert_eq!(DataScope::Member.as_str(), "member");
419 assert_eq!(DataScope::Closure.as_str(), "closure");
420 assert_eq!(DataScope::Temporary.as_str(), "temporary");
421 }
422
423 #[test]
424 fn data_scope_serialization() {
425 assert_eq!(
426 serde_json::to_string(&DataScope::Local).unwrap(),
427 "\"local\""
428 );
429 assert_eq!(
430 serde_json::to_string(&DataScope::Parameter).unwrap(),
431 "\"parameter\""
432 );
433 }
434
435 #[test]
436 fn flow_kind_as_str_all_variants() {
437 assert_eq!(FlowKind::DefUse.as_str(), "def_use");
438 assert_eq!(FlowKind::Argument.as_str(), "argument");
439 assert_eq!(FlowKind::Return.as_str(), "return");
440 assert_eq!(FlowKind::FieldAccess.as_str(), "field_access");
441 }
442
443 #[test]
444 fn flow_kind_serialization() {
445 assert_eq!(
446 serde_json::to_string(&FlowKind::DefUse).unwrap(),
447 "\"def_use\""
448 );
449 assert_eq!(
450 serde_json::to_string(&FlowKind::Argument).unwrap(),
451 "\"argument\""
452 );
453 assert_eq!(
454 serde_json::to_string(&FlowKind::Return).unwrap(),
455 "\"return\""
456 );
457 assert_eq!(
458 serde_json::to_string(&FlowKind::FieldAccess).unwrap(),
459 "\"field_access\""
460 );
461 }
462
463 #[test]
464 fn data_node_construction_and_serde() {
465 let data = DataNode {
466 id: DataNodeId::new(10).unwrap(),
467 symbol_id: Some(SymbolId::new(5).unwrap()),
468 name: Some("x".into()),
469 scope: DataScope::Local,
470 type_hint: Some("int".into()),
471 source_range: sample_source_range(),
472 };
473 let json = serde_json::to_string(&data).unwrap();
474 let val: serde_json::Value = serde_json::from_str(&json).unwrap();
475 assert_eq!(val["name"], "x");
476 assert_eq!(val["scope"], "local");
477 assert_eq!(val["type_hint"], "int");
478 }
479
480 #[test]
481 fn data_node_optional_fields_none() {
482 let data = DataNode {
483 id: DataNodeId::new(1).unwrap(),
484 symbol_id: None,
485 name: None,
486 scope: DataScope::Temporary,
487 type_hint: None,
488 source_range: sample_source_range(),
489 };
490 assert!(data.symbol_id.is_none());
491 assert!(data.name.is_none());
492 assert!(data.type_hint.is_none());
493 }
494
495 #[test]
496 fn flow_edge_serde() {
497 let edge = FlowEdge {
498 source: DataNodeId::new(1).unwrap(),
499 target: DataNodeId::new(2).unwrap(),
500 kind: FlowKind::Argument,
501 confidence: 0.85,
502 };
503 let json = serde_json::to_string(&edge).unwrap();
504 let val: serde_json::Value = serde_json::from_str(&json).unwrap();
505 assert_eq!(val["source"], 1);
506 assert_eq!(val["target"], 2);
507 assert_eq!(val["kind"], "argument");
508 assert_eq!(val["confidence"], 0.85);
509 }
510
511 #[test]
512 fn data_node_id_serde_roundtrip() {
513 let original = DataNodeId::new(99).unwrap();
514 let json = serde_json::to_string(&original).unwrap();
515 let roundtrip: DataNodeId = serde_json::from_str(&json).unwrap();
516 assert_eq!(original, roundtrip);
517 }
518}