Skip to main content

pecto_typescript/
dependencies.rs

1use crate::context::AnalysisContext;
2use crate::extractors::common::*;
3use pecto_core::model::*;
4
5/// Resolve dependencies between TypeScript capabilities via constructor injection and imports.
6pub 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        // Find which capability this file belongs to
13        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        // Strategy 1: Constructor injection
24        // constructor(private usersService: UsersService)
25        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        // Strategy 2: Import-based dependencies
50        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}