1use crate::context::AnalysisContext;
2use crate::extractors::common::*;
3use pecto_core::model::*;
4
5const MAX_DEPTH: usize = 4;
6
7pub fn extract_flows(spec: &mut ProjectSpec, ctx: &AnalysisContext) {
9 let mut flows = Vec::new();
10
11 for cap in &spec.capabilities {
12 for endpoint in &cap.endpoints {
13 let trigger = format!("{:?} {}", endpoint.method, endpoint.path);
14 let entry_point = format!("{}#{}", cap.source, cap.name);
15
16 let Some(file) = ctx.files.iter().find(|f| f.path == cap.source) else {
17 continue;
18 };
19
20 let root = file.tree.root_node();
21 let source = file.source.as_bytes();
22 let mut steps = Vec::new();
23
24 if let Some(sec) = &endpoint.security
26 && sec.authentication.is_some()
27 {
28 steps.push(FlowStep {
29 actor: cap.name.clone(),
30 method: "guard".to_string(),
31 kind: FlowStepKind::SecurityGuard,
32 description: format!(
33 "Auth: {}",
34 sec.roles.first().map(|r| r.as_str()).unwrap_or("required")
35 ),
36 condition: None,
37 children: Vec::new(),
38 });
39 }
40
41 let method_steps =
43 if let Some(method_body) = find_endpoint_method_body(&root, source, endpoint) {
44 trace_method_body(&method_body, source, ctx, 0)
45 } else {
46 let method_source = find_endpoint_source(&file.source, endpoint)
48 .unwrap_or_else(|| file.source.clone());
49 let mut fallback_steps = Vec::new();
50 trace_source_text(&method_source, &mut fallback_steps);
51 fallback_steps
52 };
53 steps.extend(method_steps);
54
55 if let Some(b) = endpoint.behaviors.first() {
57 steps.push(FlowStep {
58 actor: cap.name.clone(),
59 method: "return".to_string(),
60 kind: FlowStepKind::Return,
61 description: format!("Return: {}", b.returns.status),
62 condition: None,
63 children: Vec::new(),
64 });
65 }
66
67 if steps.len() > 1 {
68 flows.push(RequestFlow {
69 trigger,
70 entry_point,
71 steps,
72 });
73 }
74 }
75 }
76
77 spec.flows = flows;
78}
79
80fn find_endpoint_method_body<'a>(
84 root: &'a tree_sitter::Node<'a>,
85 source: &[u8],
86 endpoint: &Endpoint,
87) -> Option<tree_sitter::Node<'a>> {
88 let method_lower = format!("{:?}", endpoint.method).to_lowercase();
89
90 for i in 0..root.named_child_count() {
91 let node = root.named_child(i).unwrap();
92
93 if node.kind() == "class_declaration" || node.kind() == "export_statement" {
95 let class_node = if node.kind() == "export_statement" {
97 let mut found = None;
98 for k in 0..node.named_child_count() {
99 let c = node.named_child(k).unwrap();
100 if c.kind() == "class_declaration" {
101 found = Some(c);
102 break;
103 }
104 }
105 match found {
106 Some(c) => c,
107 None => continue,
108 }
109 } else {
110 node
111 };
112
113 if let Some(body) = class_node.child_by_field_name("body") {
114 let mut pending_decorators: Vec<String> = Vec::new();
116
117 for j in 0..body.child_count() {
119 let member = body.child(j).unwrap();
120
121 if member.kind() == "decorator" {
122 let dec_text = node_text(&member, source).to_lowercase();
123 pending_decorators.push(dec_text);
124 continue;
125 }
126
127 if member.kind() == "method_definition" {
128 let mut method_decorators = Vec::new();
130 for k in 0..member.child_count() {
131 let child = member.child(k).unwrap();
132 if child.kind() == "decorator" {
133 method_decorators.push(node_text(&child, source).to_lowercase());
134 }
135 }
136
137 let all_decorators: Vec<&String> = pending_decorators
138 .iter()
139 .chain(method_decorators.iter())
140 .collect();
141
142 for dec_text in &all_decorators {
143 let has_method = dec_text.contains(&format!("@{}(", method_lower))
144 || dec_text.contains(&format!("@{}", method_lower));
145
146 if !has_method {
147 continue;
148 }
149
150 let dec_is_bare = dec_text.contains("()")
152 || dec_text.trim().ends_with(&format!("@{}", method_lower));
153
154 let has_path_params = endpoint.path.contains('{');
156
157 let path_ok = if has_path_params {
158 endpoint.path.rsplit('/').any(|seg| {
161 if seg.starts_with('{') && seg.ends_with('}') {
162 let colon = format!(":{}", &seg[1..seg.len() - 1]);
163 dec_text.contains(seg) || dec_text.contains(&colon)
164 } else {
165 false
166 }
167 })
168 } else {
169 dec_is_bare
171 };
172
173 if path_ok {
174 return member.child_by_field_name("body");
175 }
176 }
177
178 pending_decorators.clear();
179 } else if member.kind() != "comment" {
180 pending_decorators.clear();
181 }
182 }
183 }
184 }
185
186 if node.kind() == "export_statement" {
188 for j in 0..node.named_child_count() {
189 let child = node.named_child(j).unwrap();
190 if child.kind() == "function_declaration" || child.kind() == "lexical_declaration" {
191 let name = child
192 .child_by_field_name("name")
193 .map(|n| node_text(&n, source))
194 .unwrap_or_default();
195 let method_upper = format!("{:?}", endpoint.method);
196 if name == method_upper {
197 return child.child_by_field_name("body");
198 }
199 }
200 }
201 }
202 }
203 None
204}
205
206fn trace_method_body(
208 body: &tree_sitter::Node,
209 source: &[u8],
210 _ctx: &AnalysisContext,
211 depth: usize,
212) -> Vec<FlowStep> {
213 let class_body = find_enclosing_class_body(body);
214 let mut steps = Vec::new();
215 if depth >= MAX_DEPTH {
216 return steps;
217 }
218 trace_node_recursive(body, source, depth, &mut steps, class_body.as_ref());
219 steps
220}
221
222fn find_enclosing_class_body<'a>(node: &'a tree_sitter::Node<'a>) -> Option<tree_sitter::Node<'a>> {
224 let mut current = node.parent();
225 while let Some(n) = current {
226 if n.kind() == "class_declaration" || n.kind() == "class" {
227 return n.child_by_field_name("body");
228 }
229 current = n.parent();
230 }
231 None
232}
233
234fn find_method_in_class<'a>(
236 class_body: &'a tree_sitter::Node<'a>,
237 method_name: &str,
238 source: &[u8],
239) -> Option<tree_sitter::Node<'a>> {
240 for i in 0..class_body.named_child_count() {
241 let member = class_body.named_child(i).unwrap();
242 if member.kind() == "method_definition"
243 && let Some(name_node) = member.child_by_field_name("name")
244 && node_text(&name_node, source) == method_name
245 {
246 return member.child_by_field_name("body");
247 }
248 }
249 None
250}
251
252fn trace_node_recursive(
254 node: &tree_sitter::Node,
255 source: &[u8],
256 depth: usize,
257 steps: &mut Vec<FlowStep>,
258 class_body: Option<&tree_sitter::Node>,
259) {
260 match node.kind() {
261 "call_expression" => {
262 let text = node_text(node, source);
263
264 if let Some(func) = node.child_by_field_name("function")
266 && func.kind() == "member_expression"
267 && let Some(obj) = func.child_by_field_name("object")
268 {
269 let obj_text = node_text(&obj, source);
270
271 if obj_text == "this"
273 && let Some(prop) = func.child_by_field_name("property")
274 {
275 let method_name = node_text(&prop, source);
276 if let Some(cb) = class_body
277 && depth < MAX_DEPTH
278 && let Some(target_body) = find_method_in_class(cb, &method_name, source)
279 {
280 trace_node_recursive(&target_body, source, depth + 1, steps, Some(cb));
281 return;
282 }
283 }
284
285 if obj.kind() == "member_expression"
287 && let Some(inner_obj) = obj.child_by_field_name("object")
288 && node_text(&inner_obj, source) == "this"
289 && let Some(svc) = obj.child_by_field_name("property")
290 && let Some(method) = func.child_by_field_name("property")
291 {
292 let svc_name = node_text(&svc, source);
293 let method_name = node_text(&method, source);
294 if !is_excluded_ts_method(&method_name) {
295 steps.push(FlowStep {
296 actor: svc_name.clone(),
297 method: method_name.clone(),
298 kind: FlowStepKind::ServiceCall,
299 description: format!("Call: {}.{}()", svc_name, method_name),
300 condition: None,
301 children: Vec::new(),
302 });
303 return;
304 }
305 }
306 }
307
308 if let Some(step) = classify_method_call(&text) {
310 steps.push(step);
311 return;
312 }
313 }
314 "throw_statement" => {
315 let text = node_text(node, source);
316 let exception = text
317 .split("new ")
318 .nth(1)
319 .and_then(|s| s.split('(').next())
320 .unwrap_or("Error")
321 .trim();
322 steps.push(FlowStep {
323 actor: "".to_string(),
324 method: "throw".to_string(),
325 kind: FlowStepKind::ThrowException,
326 description: format!("throw {}", exception),
327 condition: None,
328 children: Vec::new(),
329 });
330 return;
331 }
332 "if_statement" => {
333 let condition_text = node
334 .child_by_field_name("condition")
335 .map(|c| node_text(&c, source))
336 .unwrap_or_default();
337
338 let mut if_children = Vec::new();
339 if let Some(consequence) = node.child_by_field_name("consequence") {
340 trace_node_recursive(
341 &consequence,
342 source,
343 depth + 1,
344 &mut if_children,
345 class_body,
346 );
347 }
348
349 let mut else_children = Vec::new();
350 if let Some(alternative) = node.child_by_field_name("alternative") {
351 trace_node_recursive(
352 &alternative,
353 source,
354 depth + 1,
355 &mut else_children,
356 class_body,
357 );
358 }
359
360 if !if_children.is_empty() || !else_children.is_empty() {
361 steps.push(FlowStep {
362 actor: "".to_string(),
363 method: "if".to_string(),
364 kind: FlowStepKind::Condition,
365 description: format!("if {}", condition_text),
366 condition: Some(condition_text),
367 children: if_children,
368 });
369 if !else_children.is_empty() {
370 steps.push(FlowStep {
371 actor: "".to_string(),
372 method: "else".to_string(),
373 kind: FlowStepKind::Condition,
374 description: "else".to_string(),
375 condition: Some("else".to_string()),
376 children: else_children,
377 });
378 }
379 return;
380 }
381 }
382 _ => {}
383 }
384
385 for i in 0..node.child_count() {
387 let child = node.child(i).unwrap();
388 trace_node_recursive(&child, source, depth, steps, class_body);
389 }
390}
391
392fn classify_method_call(text: &str) -> Option<FlowStep> {
394 if text.contains(".save(")
396 || text.contains(".insert(")
397 || text.contains(".update(")
398 || text.contains(".insertOne(")
399 || text.contains(".updateOne(")
400 {
401 let target = text.split('.').next().unwrap_or("").trim();
402 return Some(FlowStep {
403 actor: target.to_string(),
404 method: "save".to_string(),
405 kind: FlowStepKind::DbWrite,
406 description: format!("DB write: {}", target),
407 condition: None,
408 children: Vec::new(),
409 });
410 }
411
412 if text.contains(".create(") && !text.contains("createElement") {
413 let target = text.split('.').next().unwrap_or("").trim();
414 if !target.is_empty()
416 && target.chars().next().is_some_and(|c| c.is_lowercase())
417 && !matches!(target, "document" | "Object" | "Array")
418 {
419 return Some(FlowStep {
420 actor: target.to_string(),
421 method: "create".to_string(),
422 kind: FlowStepKind::DbWrite,
423 description: format!("DB write: {}.create()", target),
424 condition: None,
425 children: Vec::new(),
426 });
427 }
428 }
429
430 if text.contains(".delete(") || text.contains(".remove(") || text.contains(".deleteOne(") {
431 let target = text.split('.').next().unwrap_or("").trim();
432 return Some(FlowStep {
433 actor: target.to_string(),
434 method: "delete".to_string(),
435 kind: FlowStepKind::DbWrite,
436 description: format!("DB delete: {}", target),
437 condition: None,
438 children: Vec::new(),
439 });
440 }
441
442 if text.contains(".find(")
444 || text.contains(".findOne(")
445 || text.contains(".findById(")
446 || text.contains(".findAll(")
447 || text.contains(".findOneBy(")
448 || text.contains(".query(")
449 {
450 let target = text.split('.').next().unwrap_or("").trim();
451 return Some(FlowStep {
452 actor: target.to_string(),
453 method: "find".to_string(),
454 kind: FlowStepKind::DbRead,
455 description: format!("DB read: {}", target),
456 condition: None,
457 children: Vec::new(),
458 });
459 }
460
461 if text.contains(".emit(") || text.contains(".publish(") {
463 let event = text
464 .split(".emit(")
465 .nth(1)
466 .or_else(|| text.split(".publish(").nth(1))
467 .and_then(|s| s.split(')').next())
468 .unwrap_or("event");
469 return Some(FlowStep {
470 actor: "EventBus".to_string(),
471 method: "emit".to_string(),
472 kind: FlowStepKind::EventPublish,
473 description: format!("Emit: {}", event.chars().take(60).collect::<String>()),
474 condition: None,
475 children: Vec::new(),
476 });
477 }
478
479 if text.contains('.') && text.contains('(') {
481 let parts: Vec<&str> = text.splitn(2, '.').collect();
482 if parts.len() == 2 {
483 let target = parts[0].trim();
484 let method = parts[1].split('(').next().unwrap_or("").trim();
485
486 if target == "this" && parts[1].contains('.') {
488 let inner_parts: Vec<&str> = parts[1].splitn(2, '.').collect();
489 if inner_parts.len() == 2 {
490 let service = inner_parts[0].trim();
491 let svc_method = inner_parts[1].split('(').next().unwrap_or("").trim();
492 if !service.is_empty()
493 && service.chars().next().is_some_and(|c| c.is_lowercase())
494 && !is_excluded_ts_method(svc_method)
495 {
496 return Some(FlowStep {
497 actor: service.to_string(),
498 method: svc_method.to_string(),
499 kind: FlowStepKind::ServiceCall,
500 description: format!("Call: {}.{}()", service, svc_method),
501 condition: None,
502 children: Vec::new(),
503 });
504 }
505 }
506 }
507
508 if !target.is_empty()
509 && target.chars().next().is_some_and(|c| c.is_lowercase())
510 && !is_excluded_ts_target(target)
511 && !is_excluded_ts_method(method)
512 {
513 return Some(FlowStep {
514 actor: target.to_string(),
515 method: method.to_string(),
516 kind: FlowStepKind::ServiceCall,
517 description: format!("Call: {}.{}()", target, method),
518 condition: None,
519 children: Vec::new(),
520 });
521 }
522 }
523 }
524
525 None
526}
527
528fn is_excluded_ts_target(target: &str) -> bool {
529 matches!(
530 target,
531 "this"
532 | "console"
533 | "Math"
534 | "JSON"
535 | "Object"
536 | "Array"
537 | "Promise"
538 | "Buffer"
539 | "Date"
540 | "RegExp"
541 | "Error"
542 | "process"
543 | "window"
544 | "document"
545 | "response"
546 | "res"
547 | "req"
548 | "request"
549 )
550}
551
552fn is_excluded_ts_method(method: &str) -> bool {
553 matches!(
554 method,
555 "toString"
556 | "valueOf"
557 | "map"
558 | "filter"
559 | "reduce"
560 | "forEach"
561 | "then"
562 | "catch"
563 | "finally"
564 | "push"
565 | "pop"
566 | "shift"
567 | "unshift"
568 | "join"
569 | "split"
570 | "slice"
571 | "splice"
572 | "concat"
573 | "includes"
574 | "indexOf"
575 | "keys"
576 | "values"
577 | "entries"
578 | "log"
579 | "warn"
580 | "error"
581 | "info"
582 | "debug"
583 | "status"
584 | "json"
585 | "send"
586 | "pipe"
587 | "subscribe"
588 | "toPromise"
589 | "bind"
590 | "apply"
591 | "call"
592 )
593}
594
595fn find_endpoint_source(source: &str, endpoint: &Endpoint) -> Option<String> {
599 let method_lower = format!("{:?}", endpoint.method).to_lowercase();
600 let method_upper = format!("{:?}", endpoint.method);
601
602 let lines: Vec<&str> = source.lines().collect();
603
604 for (i, line) in lines.iter().enumerate() {
605 let trimmed = line.trim();
606
607 if trimmed.contains(&format!(".{}(", method_lower)) && trimmed.contains(&endpoint.path) {
609 return extract_brace_block(&lines, i);
610 }
611
612 if trimmed.contains(&format!("function {}", method_upper))
614 || trimmed.contains(&format!("const {} ", method_upper))
615 {
616 return extract_brace_block(&lines, i);
617 }
618 }
619 None
620}
621
622fn extract_brace_block(lines: &[&str], start: usize) -> Option<String> {
623 let mut depth = 0;
624 let mut started = false;
625 let mut result = Vec::new();
626
627 for line in &lines[start..] {
628 for ch in line.chars() {
629 if ch == '{' {
630 depth += 1;
631 started = true;
632 } else if ch == '}' {
633 depth -= 1;
634 }
635 }
636 if started {
637 result.push(*line);
638 }
639 if started && depth == 0 {
640 return Some(result.join("\n"));
641 }
642 }
643 None
644}
645
646fn trace_source_text(source: &str, steps: &mut Vec<FlowStep>) {
647 for line in source.lines() {
648 let trimmed = line.trim();
649
650 if trimmed.contains(".save(") || trimmed.contains(".create(") {
651 steps.push(FlowStep {
652 actor: "Repository".to_string(),
653 method: "save".to_string(),
654 kind: FlowStepKind::DbWrite,
655 description: "DB write".to_string(),
656 condition: None,
657 children: Vec::new(),
658 });
659 } else if trimmed.contains(".find(") || trimmed.contains(".findOne(") {
660 steps.push(FlowStep {
661 actor: "Repository".to_string(),
662 method: "find".to_string(),
663 kind: FlowStepKind::DbRead,
664 description: "DB read".to_string(),
665 condition: None,
666 children: Vec::new(),
667 });
668 } else if trimmed.contains(".emit(") || trimmed.contains(".publish(") {
669 steps.push(FlowStep {
670 actor: "EventBus".to_string(),
671 method: "emit".to_string(),
672 kind: FlowStepKind::EventPublish,
673 description: "Emit event".to_string(),
674 condition: None,
675 children: Vec::new(),
676 });
677 }
678 }
679}