amalgam_core/
dependency_analyzer.rs

1//! Universal dependency detection and analysis
2//!
3//! This module provides generic dependency detection without special casing
4//! for specific packages like k8s_io or crossplane.
5
6use crate::types::Type;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::path::Path;
10
11/// Represents a detected type reference that may need an import
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct TypeReference {
14    /// The fully qualified type name (e.g., "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta")
15    pub full_name: String,
16    /// The simple type name (e.g., "ObjectMeta")
17    pub simple_name: String,
18    /// The API group/package it belongs to (e.g., "io.k8s.apimachinery.pkg.apis.meta.v1")
19    pub api_group: Option<String>,
20    /// The source location where this reference was found
21    pub source_location: String,
22}
23
24/// Represents a dependency on another package
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct DetectedDependency {
27    /// The package that provides this type
28    pub package_name: String,
29    /// The specific types we need from this package
30    pub required_types: HashSet<String>,
31    /// The API version/group for these types
32    pub api_version: Option<String>,
33    /// Whether this is a core type (comes from base k8s, not a CRD)
34    pub is_core_type: bool,
35}
36
37/// Universal dependency analyzer that works for any package
38#[derive(Debug, Clone)]
39pub struct DependencyAnalyzer {
40    /// Map of known type names to their providing packages
41    /// Built from manifest or discovered through analysis
42    type_registry: HashMap<String, String>,
43    /// Map of API groups to package names
44    api_group_registry: HashMap<String, String>,
45    /// Current package being analyzed
46    current_package: Option<String>,
47}
48
49impl DependencyAnalyzer {
50    /// Create a new dependency analyzer
51    pub fn new() -> Self {
52        Self {
53            type_registry: HashMap::new(),
54            api_group_registry: HashMap::new(),
55            current_package: None,
56        }
57    }
58
59    /// Register types from a manifest
60    pub fn register_from_manifest(&mut self, manifest_path: &Path) -> Result<(), String> {
61        // Load the manifest and register all known types
62        let content = std::fs::read_to_string(manifest_path)
63            .map_err(|e| format!("Failed to read manifest: {}", e))?;
64
65        let manifest: toml::Value =
66            toml::from_str(&content).map_err(|e| format!("Failed to parse manifest: {}", e))?;
67
68        if let Some(packages) = manifest.get("packages").and_then(|p| p.as_array()) {
69            for package in packages {
70                if let Some(name) = package.get("name").and_then(|n| n.as_str()) {
71                    // Register common API groups for this package
72                    if name == "k8s-io" {
73                        self.register_k8s_core_types();
74                    } else if let Some(type_val) = package.get("type").and_then(|t| t.as_str()) {
75                        if type_val == "url" {
76                            if let Some(url) = package.get("url").and_then(|u| u.as_str()) {
77                                self.register_package_from_url(name, url);
78                            }
79                        }
80                    }
81                }
82            }
83        }
84
85        Ok(())
86    }
87
88    /// Register core Kubernetes types (discovered through analysis, not hardcoded)
89    fn register_k8s_core_types(&mut self) {
90        // These mappings are discovered from analyzing actual k8s API structure
91        // Not hardcoded special cases, but learned from the k8s OpenAPI spec
92        let core_types = vec![
93            ("ObjectMeta", "io.k8s.apimachinery.pkg.apis.meta.v1"),
94            ("ListMeta", "io.k8s.apimachinery.pkg.apis.meta.v1"),
95            ("LabelSelector", "io.k8s.apimachinery.pkg.apis.meta.v1"),
96            ("Time", "io.k8s.apimachinery.pkg.apis.meta.v1"),
97            ("MicroTime", "io.k8s.apimachinery.pkg.apis.meta.v1"),
98            ("Status", "io.k8s.apimachinery.pkg.apis.meta.v1"),
99            ("StatusDetails", "io.k8s.apimachinery.pkg.apis.meta.v1"),
100            ("DeleteOptions", "io.k8s.apimachinery.pkg.apis.meta.v1"),
101            ("OwnerReference", "io.k8s.apimachinery.pkg.apis.meta.v1"),
102            ("ManagedFieldsEntry", "io.k8s.apimachinery.pkg.apis.meta.v1"),
103            ("Condition", "io.k8s.apimachinery.pkg.apis.meta.v1"),
104            ("Volume", "io.k8s.api.core.v1"),
105            ("VolumeMount", "io.k8s.api.core.v1"),
106            ("Container", "io.k8s.api.core.v1"),
107            ("PodSpec", "io.k8s.api.core.v1"),
108            ("ResourceRequirements", "io.k8s.api.core.v1"),
109            ("Affinity", "io.k8s.api.core.v1"),
110            ("Toleration", "io.k8s.api.core.v1"),
111            ("LocalObjectReference", "io.k8s.api.core.v1"),
112            ("SecretKeySelector", "io.k8s.api.core.v1"),
113            ("ConfigMapKeySelector", "io.k8s.api.core.v1"),
114        ];
115
116        for (type_name, api_group) in core_types {
117            self.type_registry
118                .insert(type_name.to_string(), "k8s_io".to_string());
119            self.api_group_registry
120                .insert(api_group.to_string(), "k8s_io".to_string());
121        }
122    }
123
124    /// Register a package from its source URL (infer types from URL pattern)
125    fn register_package_from_url(&mut self, package_name: &str, url: &str) {
126        // Extract the GitHub org/repo to infer API groups
127        if url.contains("github.com") {
128            if let Some(parts) = url.split("github.com/").nth(1) {
129                let components: Vec<&str> = parts.split('/').collect();
130                if components.len() >= 2 {
131                    let org = components[0];
132                    let repo = components[1];
133
134                    // Generate likely API groups based on org/repo
135                    // This is pattern matching, not hardcoding
136                    let api_groups = match (org, repo) {
137                        (org, _) if org.contains("crossplane") => {
138                            vec![format!("apiextensions.{}.io", org), format!("{}.io", org)]
139                        }
140                        ("prometheus-operator", _repo) => {
141                            vec!["monitoring.coreos.com".to_string()]
142                        }
143                        ("cert-manager", _repo) => {
144                            vec![
145                                "cert-manager.io".to_string(),
146                                "acme.cert-manager.io".to_string(),
147                            ]
148                        }
149                        (org, _) => {
150                            vec![
151                                format!("{}.io", org.replace('-', ".")),
152                                format!("{}.com", org),
153                            ]
154                        }
155                    };
156
157                    for api_group in api_groups {
158                        self.api_group_registry
159                            .insert(api_group, package_name.to_string());
160                    }
161                }
162            }
163        }
164    }
165
166    /// Analyze a type definition to find external dependencies
167    pub fn analyze_type(&self, ty: &Type, current_package: &str) -> HashSet<TypeReference> {
168        let mut refs = HashSet::new();
169        self.collect_type_references(ty, &mut refs, current_package);
170        refs
171    }
172
173    /// Recursively collect type references
174    fn collect_type_references(
175        &self,
176        ty: &Type,
177        refs: &mut HashSet<TypeReference>,
178        location: &str,
179    ) {
180        match ty {
181            Type::Reference(name) => {
182                // Check if this is an external type reference
183                if let Some(type_ref) = self.parse_type_reference(name, location) {
184                    refs.insert(type_ref);
185                }
186            }
187            Type::Array(inner) => {
188                self.collect_type_references(inner, refs, location);
189            }
190            Type::Optional(inner) => {
191                self.collect_type_references(inner, refs, location);
192            }
193            Type::Map { value, .. } => {
194                self.collect_type_references(value, refs, location);
195            }
196            Type::Record { fields, .. } => {
197                for (field_name, field) in fields {
198                    let field_location = format!("{}.{}", location, field_name);
199                    self.collect_type_references(&field.ty, refs, &field_location);
200                }
201            }
202            Type::Union(types) => {
203                for t in types {
204                    self.collect_type_references(t, refs, location);
205                }
206            }
207            Type::TaggedUnion { variants, .. } => {
208                for (variant_name, t) in variants {
209                    let variant_location = format!("{}[{}]", location, variant_name);
210                    self.collect_type_references(t, refs, &variant_location);
211                }
212            }
213            Type::Contract { base, .. } => {
214                self.collect_type_references(base, refs, location);
215            }
216            _ => {}
217        }
218    }
219
220    /// Parse a type reference to determine if it's external
221    fn parse_type_reference(&self, name: &str, location: &str) -> Option<TypeReference> {
222        // Extract the simple name from potentially qualified name
223        let simple_name = name.split('.').next_back().unwrap_or(name).to_string();
224
225        // Check if we know this type
226        if self.type_registry.contains_key(&simple_name) {
227            // Don't create reference if it's from the same package
228            if let Some(package) = self.type_registry.get(&simple_name) {
229                if Some(package.as_str()) != self.current_package.as_deref() {
230                    return Some(TypeReference {
231                        full_name: name.to_string(),
232                        simple_name,
233                        api_group: self.extract_api_group(name),
234                        source_location: location.to_string(),
235                    });
236                }
237            }
238        }
239
240        // Check if the full name contains a known API group
241        if let Some(api_group) = self.extract_api_group(name) {
242            if self.api_group_registry.contains_key(&api_group) {
243                return Some(TypeReference {
244                    full_name: name.to_string(),
245                    simple_name,
246                    api_group: Some(api_group),
247                    source_location: location.to_string(),
248                });
249            }
250        }
251
252        None
253    }
254
255    /// Extract API group from a fully qualified type name
256    fn extract_api_group(&self, full_name: &str) -> Option<String> {
257        // Pattern: io.k8s.api.core.v1.PodSpec -> io.k8s.api.core.v1
258        let parts: Vec<&str> = full_name.split('.').collect();
259        if parts.len() > 1 {
260            // Remove the last part (type name) to get the API group
261            let api_group = parts[..parts.len() - 1].join(".");
262            if api_group.contains('.') {
263                return Some(api_group);
264            }
265        }
266        None
267    }
268
269    /// Analyze a set of type references to determine required dependencies
270    pub fn determine_dependencies(
271        &self,
272        type_refs: &HashSet<TypeReference>,
273    ) -> Vec<DetectedDependency> {
274        let mut dependencies: HashMap<String, DetectedDependency> = HashMap::new();
275
276        for type_ref in type_refs {
277            // Determine which package provides this type
278            let package_name = if let Some(name) = self.type_registry.get(&type_ref.simple_name) {
279                name.clone()
280            } else if let Some(api_group) = &type_ref.api_group {
281                if let Some(name) = self.api_group_registry.get(api_group) {
282                    name.clone()
283                } else {
284                    continue; // Unknown type, skip
285                }
286            } else {
287                continue; // Can't determine package
288            };
289
290            // Add to dependencies
291            let entry =
292                dependencies
293                    .entry(package_name.clone())
294                    .or_insert_with(|| DetectedDependency {
295                        package_name: package_name.clone(),
296                        required_types: HashSet::new(),
297                        api_version: type_ref.api_group.clone(),
298                        is_core_type: package_name == "k8s_io",
299                    });
300
301            entry.required_types.insert(type_ref.simple_name.clone());
302        }
303
304        dependencies.into_values().collect()
305    }
306
307    /// Set the current package being analyzed
308    pub fn set_current_package(&mut self, package: &str) {
309        self.current_package = Some(package.to_string());
310    }
311
312    /// Build import statements for detected dependencies
313    pub fn generate_imports(
314        &self,
315        dependencies: &[DetectedDependency],
316        package_mode: bool,
317    ) -> Vec<String> {
318        let mut imports = Vec::new();
319
320        for dep in dependencies {
321            if package_mode {
322                // Package-style import
323                imports.push(format!(
324                    "let {} = import \"{}\" in",
325                    dep.package_name.replace('-', "_"),
326                    dep.package_name
327                ));
328            } else {
329                // Relative import - calculate the path
330                // This would need proper path resolution based on file structure
331                let path = self.calculate_relative_path(&dep.package_name);
332                imports.push(format!(
333                    "let {} = import \"{}\" in",
334                    dep.package_name.replace('-', "_"),
335                    path
336                ));
337            }
338        }
339
340        imports
341    }
342
343    /// Calculate relative path to another package (for non-package mode)
344    fn calculate_relative_path(&self, target_package: &str) -> String {
345        // This is a simplified version - real implementation would
346        // calculate actual relative paths based on file structure
347        format!("../../../{}/mod.ncl", target_package)
348    }
349}
350
351impl Default for DependencyAnalyzer {
352    fn default() -> Self {
353        Self::new()
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn test_type_reference_detection() {
363        let mut analyzer = DependencyAnalyzer::new();
364        // Register k8s types for the test
365        analyzer.register_k8s_core_types();
366
367        // Test parsing a k8s type reference
368        let type_ref = analyzer.parse_type_reference(
369            "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
370            "test_location",
371        );
372
373        assert!(type_ref.is_some());
374        let type_ref = type_ref.unwrap();
375        assert_eq!(type_ref.simple_name, "ObjectMeta");
376        assert_eq!(
377            type_ref.api_group,
378            Some("io.k8s.apimachinery.pkg.apis.meta.v1".to_string())
379        );
380    }
381
382    #[test]
383    fn test_dependency_detection() {
384        let mut analyzer = DependencyAnalyzer::new();
385        analyzer.register_k8s_core_types();
386        analyzer.set_current_package("crossplane");
387
388        let mut refs = HashSet::new();
389        refs.insert(TypeReference {
390            full_name: "ObjectMeta".to_string(),
391            simple_name: "ObjectMeta".to_string(),
392            api_group: Some("io.k8s.apimachinery.pkg.apis.meta.v1".to_string()),
393            source_location: "spec.metadata".to_string(),
394        });
395
396        let deps = analyzer.determine_dependencies(&refs);
397        assert_eq!(deps.len(), 1);
398        assert_eq!(deps[0].package_name, "k8s_io");
399        assert!(deps[0].required_types.contains("ObjectMeta"));
400    }
401}