pecto_typescript/
dependencies.rs1use crate::context::AnalysisContext;
2use crate::extractors::common::*;
3use pecto_core::model::*;
4
5pub fn resolve_dependencies(spec: &mut ProjectSpec, ctx: &AnalysisContext) {
7 let capability_names: Vec<String> = spec.capabilities.iter().map(|c| c.name.clone()).collect();
8
9 for file in &ctx.files {
10 let source = &file.source;
11
12 let from_cap = spec
14 .capabilities
15 .iter()
16 .find(|c| c.source == file.path)
17 .map(|c| c.name.clone());
18
19 let Some(from) = from_cap else {
20 continue;
21 };
22
23 for line in source.lines() {
26 let trimmed = line.trim();
27 if !trimmed.contains("private ")
28 && !trimmed.contains("readonly ")
29 && !trimmed.contains("constructor(")
30 {
31 continue;
32 }
33
34 for type_name in extract_injected_types(trimmed) {
35 if let Some(target) = resolve_type_to_capability(&type_name, &capability_names)
36 && target != from
37 {
38 add_dep(
39 &mut spec.dependencies,
40 &from,
41 &target,
42 infer_kind(&target),
43 format!("{} injects {}", from, type_name),
44 );
45 }
46 }
47 }
48
49 for line in source.lines() {
51 let trimmed = line.trim();
52 if !trimmed.starts_with("import ") {
53 continue;
54 }
55
56 for name in extract_import_names(trimmed) {
57 if let Some(target) = resolve_type_to_capability(&name, &capability_names)
58 && target != from
59 {
60 add_dep(
61 &mut spec.dependencies,
62 &from,
63 &target,
64 infer_kind(&target),
65 format!("{} imports {}", from, name),
66 );
67 }
68 }
69 }
70 }
71}
72
73fn extract_injected_types(line: &str) -> Vec<String> {
74 let mut types = Vec::new();
75
76 for part in line.split(',') {
77 let trimmed = part
78 .trim()
79 .trim_start_matches("constructor(")
80 .trim_start_matches('(')
81 .trim_end_matches(')')
82 .trim_end_matches('{')
83 .trim();
84
85 if trimmed.contains(':') {
86 let type_part = trimmed
87 .split(':')
88 .next_back()
89 .unwrap_or("")
90 .trim()
91 .trim_end_matches(',')
92 .trim_end_matches(')')
93 .trim();
94
95 if !type_part.is_empty()
96 && type_part.chars().next().is_some_and(|c| c.is_uppercase())
97 && !matches!(
98 type_part,
99 "Promise" | "Observable" | "String" | "Number" | "Boolean" | "Record"
100 )
101 && !type_part.starts_with("Promise<")
102 && !type_part.starts_with("Observable<")
103 {
104 types.push(type_part.to_string());
105 }
106 }
107 }
108
109 types
110}
111
112fn extract_import_names(line: &str) -> Vec<String> {
113 let mut names = Vec::new();
114
115 if let Some(start) = line.find('{')
116 && let Some(end) = line.find('}')
117 {
118 let imports = &line[start + 1..end];
119 for name in imports.split(',') {
120 let trimmed = name.trim().split(" as ").next().unwrap_or("").trim();
121 if !trimmed.is_empty() && trimmed.chars().next().is_some_and(|c| c.is_uppercase()) {
122 names.push(trimmed.to_string());
123 }
124 }
125 }
126
127 names
128}
129
130fn resolve_type_to_capability(type_name: &str, capability_names: &[String]) -> Option<String> {
131 let kebab = to_kebab_case(type_name);
132
133 if capability_names.contains(&kebab) {
134 return Some(kebab);
135 }
136
137 let base = type_name
138 .replace("Service", "")
139 .replace("Repository", "")
140 .replace("Controller", "");
141 let kebab_base = to_kebab_case(&base);
142
143 let candidates = [
144 format!("{}-service", kebab_base),
145 format!("{}-entity", kebab_base),
146 kebab_base.clone(),
147 ];
148
149 for candidate in &candidates {
150 if capability_names.contains(candidate) {
151 return Some(candidate.clone());
152 }
153 }
154
155 for cap in capability_names {
156 if cap.starts_with(&kebab_base) && !kebab_base.is_empty() && kebab_base.len() > 2 {
157 return Some(cap.clone());
158 }
159 }
160
161 None
162}
163
164fn infer_kind(target: &str) -> DependencyKind {
165 if target.contains("repository") || target.contains("entity") {
166 DependencyKind::Queries
167 } else {
168 DependencyKind::Calls
169 }
170}
171
172fn add_dep(
173 deps: &mut Vec<DependencyEdge>,
174 from: &str,
175 to: &str,
176 kind: DependencyKind,
177 reference: String,
178) {
179 if let Some(existing) = deps.iter_mut().find(|d| d.from == from && d.to == to) {
180 if !existing.references.contains(&reference) {
181 existing.references.push(reference);
182 }
183 } else {
184 deps.push(DependencyEdge {
185 from: from.to_string(),
186 to: to.to_string(),
187 kind,
188 references: vec![reference],
189 });
190 }
191}