Skip to main content

portalis_core/assessment/
compatibility_analyzer.rs

1//! Compatibility Analyzer
2//!
3//! Analyzes detected features and calculates translatability scores.
4
5use super::feature_detector::{DetectedFeature, FeatureSet, FeatureSupport, FeatureCategory, FeatureSummary};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Compatibility analysis report
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct CompatibilityReport {
12    pub score: TranslatabilityScore,
13    pub blockers: Vec<Blocker>,
14    pub warnings: Vec<Warning>,
15    pub recommendations: Vec<Recommendation>,
16    pub file_analysis: HashMap<String, FileCompatibility>,
17}
18
19/// Translatability score (0-100%)
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct TranslatabilityScore {
22    pub overall: f64,
23    pub by_category: HashMap<FeatureCategory, f64>,
24    pub confidence: ConfidenceLevel,
25}
26
27/// Confidence in the score
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub enum ConfidenceLevel {
30    High,    // > 90% of features analyzed
31    Medium,  // 70-90% of features analyzed
32    Low,     // < 70% of features analyzed
33}
34
35/// Translation blocker
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Blocker {
38    pub feature: String,
39    pub category: FeatureCategory,
40    pub count: usize,
41    pub impact: BlockerImpact,
42    pub description: String,
43    pub workaround: Option<String>,
44    pub locations: Vec<String>,
45}
46
47/// Impact level of a blocker
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub enum BlockerImpact {
50    Critical,  // Prevents translation entirely
51    High,      // Prevents translation of specific modules
52    Medium,    // Requires significant refactoring
53    Low,       // Minor workaround needed
54}
55
56/// Warning about partial support
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Warning {
59    pub feature: String,
60    pub category: FeatureCategory,
61    pub count: usize,
62    pub description: String,
63    pub limitation: String,
64}
65
66/// Recommendation for migration
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct Recommendation {
69    pub priority: RecommendationPriority,
70    pub title: String,
71    pub description: String,
72    pub action_items: Vec<String>,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76pub enum RecommendationPriority {
77    High,
78    Medium,
79    Low,
80}
81
82/// Per-file compatibility analysis
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct FileCompatibility {
85    pub file_path: String,
86    pub translatability: f64,
87    pub features_total: usize,
88    pub features_supported: usize,
89    pub blockers: usize,
90    pub warnings: usize,
91}
92
93/// Compatibility analyzer
94pub struct CompatibilityAnalyzer {
95    /// Minimum score to consider translatable
96    min_translatable_score: f64,
97}
98
99impl CompatibilityAnalyzer {
100    pub fn new() -> Self {
101        Self {
102            min_translatable_score: 70.0,
103        }
104    }
105
106    /// Analyze feature set for compatibility
107    pub fn analyze(&self, feature_set: &FeatureSet) -> CompatibilityReport {
108        let score = self.calculate_score(feature_set);
109        let blockers = self.identify_blockers(feature_set);
110        let warnings = self.identify_warnings(feature_set);
111        let recommendations = self.generate_recommendations(feature_set, &score, &blockers);
112
113        CompatibilityReport {
114            score,
115            blockers,
116            warnings,
117            recommendations,
118            file_analysis: HashMap::new(), // Will be populated per-file
119        }
120    }
121
122    /// Analyze multiple files
123    pub fn analyze_files(&self, file_features: &HashMap<String, FeatureSet>) -> CompatibilityReport {
124        let mut all_features = Vec::new();
125        let mut file_analysis = HashMap::new();
126
127        // Collect all features and analyze per file
128        for (file_path, features) in file_features {
129            all_features.extend(features.features.clone());
130
131            let file_score = self.calculate_file_score(features);
132            let file_blockers = features.features.iter()
133                .filter(|f| f.support == FeatureSupport::None)
134                .count();
135            let file_warnings = features.features.iter()
136                .filter(|f| f.support == FeatureSupport::Partial)
137                .count();
138
139            file_analysis.insert(file_path.clone(), FileCompatibility {
140                file_path: file_path.clone(),
141                translatability: file_score,
142                features_total: features.features.len(),
143                features_supported: features.summary.fully_supported,
144                blockers: file_blockers,
145                warnings: file_warnings,
146            });
147        }
148
149        // Create combined feature set
150        let combined = FeatureSet {
151            features: all_features.clone(),
152            summary: self.summarize_features(&all_features),
153        };
154
155        let score = self.calculate_score(&combined);
156        let blockers = self.identify_blockers(&combined);
157        let warnings = self.identify_warnings(&combined);
158        let recommendations = self.generate_recommendations(&combined, &score, &blockers);
159
160        CompatibilityReport {
161            score,
162            blockers,
163            warnings,
164            recommendations,
165            file_analysis,
166        }
167    }
168
169    /// Calculate translatability score
170    fn calculate_score(&self, features: &FeatureSet) -> TranslatabilityScore {
171        let total = features.summary.total_features as f64;
172
173        if total == 0.0 {
174            return TranslatabilityScore {
175                overall: 100.0,
176                by_category: HashMap::new(),
177                confidence: ConfidenceLevel::Low,
178            };
179        }
180
181        // Weighted scoring: full support = 1.0, partial = 0.5, none = 0.0
182        let score = (features.summary.fully_supported as f64
183                    + features.summary.partially_supported as f64 * 0.5) / total * 100.0;
184
185        // Calculate per-category scores
186        let mut by_category = HashMap::new();
187        for (category, count) in &features.summary.by_category {
188            let category_features: Vec<_> = features.features.iter()
189                .filter(|f| &f.category == category)
190                .collect();
191
192            let category_total = category_features.len() as f64;
193            let category_supported = category_features.iter()
194                .filter(|f| f.support == FeatureSupport::Full)
195                .count() as f64;
196            let category_partial = category_features.iter()
197                .filter(|f| f.support == FeatureSupport::Partial)
198                .count() as f64;
199
200            let category_score = if category_total > 0.0 {
201                (category_supported + category_partial * 0.5) / category_total * 100.0
202            } else {
203                100.0
204            };
205
206            by_category.insert(category.clone(), category_score);
207        }
208
209        // Determine confidence based on feature coverage
210        let confidence = if total > 100.0 {
211            ConfidenceLevel::High
212        } else if total > 20.0 {
213            ConfidenceLevel::Medium
214        } else {
215            ConfidenceLevel::Low
216        };
217
218        TranslatabilityScore {
219            overall: score,
220            by_category,
221            confidence,
222        }
223    }
224
225    /// Calculate score for a single file
226    fn calculate_file_score(&self, features: &FeatureSet) -> f64 {
227        let total = features.summary.total_features as f64;
228        if total == 0.0 {
229            return 100.0;
230        }
231
232        (features.summary.fully_supported as f64
233         + features.summary.partially_supported as f64 * 0.5) / total * 100.0
234    }
235
236    /// Identify translation blockers
237    fn identify_blockers(&self, features: &FeatureSet) -> Vec<Blocker> {
238        let mut blockers = Vec::new();
239        let mut blocker_map: HashMap<String, (usize, Vec<String>)> = HashMap::new();
240
241        for feature in &features.features {
242            if feature.support == FeatureSupport::None {
243                let entry = blocker_map.entry(feature.name.clone()).or_insert((0, Vec::new()));
244                entry.0 += feature.count;
245                for loc in &feature.locations {
246                    entry.1.push(format!("{}:{}", loc.file, loc.context));
247                }
248            }
249        }
250
251        for (name, (count, locations)) in blocker_map {
252            let (impact, description, workaround) = self.classify_blocker(&name);
253
254            blockers.push(Blocker {
255                feature: name.clone(),
256                category: self.get_category_for_blocker(&name),
257                count,
258                impact,
259                description,
260                workaround,
261                locations,
262            });
263        }
264
265        // Sort by impact (Critical first)
266        blockers.sort_by(|a, b| {
267            let order_a = match a.impact {
268                BlockerImpact::Critical => 0,
269                BlockerImpact::High => 1,
270                BlockerImpact::Medium => 2,
271                BlockerImpact::Low => 3,
272            };
273            let order_b = match b.impact {
274                BlockerImpact::Critical => 0,
275                BlockerImpact::High => 1,
276                BlockerImpact::Medium => 2,
277                BlockerImpact::Low => 3,
278            };
279            order_a.cmp(&order_b).then(b.count.cmp(&a.count))
280        });
281
282        blockers
283    }
284
285    /// Classify blocker impact
286    fn classify_blocker(&self, feature_name: &str) -> (BlockerImpact, String, Option<String>) {
287        if feature_name.contains("metaclass") {
288            (
289                BlockerImpact::Critical,
290                "Metaclasses are not supported in Portalis. They require deep runtime introspection.".to_string(),
291                Some("Refactor to use composition or regular classes with factory functions.".to_string()),
292            )
293        } else if feature_name == "eval" || feature_name == "exec" {
294            (
295                BlockerImpact::Critical,
296                "Dynamic code execution is not supported in WASM environment.".to_string(),
297                Some("Replace with static code or pre-compile all needed functionality.".to_string()),
298            )
299        } else if feature_name.contains("__getattr__") || feature_name.contains("__setattr__") {
300            (
301                BlockerImpact::High,
302                "Dynamic attribute access is not fully supported.".to_string(),
303                Some("Use explicit attributes or dictionary-based storage.".to_string()),
304            )
305        } else if feature_name == "abstractmethod" {
306            (
307                BlockerImpact::Medium,
308                "Abstract methods require interface-like patterns.".to_string(),
309                Some("Use trait-based design in Rust translation.".to_string()),
310            )
311        } else {
312            (
313                BlockerImpact::Low,
314                format!("{} is not currently supported.", feature_name),
315                None,
316            )
317        }
318    }
319
320    /// Get category for a blocker
321    fn get_category_for_blocker(&self, name: &str) -> FeatureCategory {
322        if name.contains("metaclass") {
323            FeatureCategory::Metaclass
324        } else if name == "eval" || name == "exec" {
325            FeatureCategory::DynamicFeature
326        } else if name.starts_with("__") && name.ends_with("__") {
327            FeatureCategory::MagicMethod
328        } else {
329            FeatureCategory::Other
330        }
331    }
332
333    /// Identify warnings for partial support
334    fn identify_warnings(&self, features: &FeatureSet) -> Vec<Warning> {
335        let mut warnings = Vec::new();
336        let mut warning_map: HashMap<String, usize> = HashMap::new();
337
338        for feature in &features.features {
339            if feature.support == FeatureSupport::Partial {
340                *warning_map.entry(feature.name.clone()).or_insert(0) += feature.count;
341            }
342        }
343
344        for (name, count) in warning_map {
345            let (description, limitation) = self.describe_partial_support(&name);
346
347            warnings.push(Warning {
348                feature: name.clone(),
349                category: self.get_category_for_warning(&name),
350                count,
351                description,
352                limitation,
353            });
354        }
355
356        warnings
357    }
358
359    /// Describe partial support limitations
360    fn describe_partial_support(&self, feature_name: &str) -> (String, String) {
361        if feature_name.contains("async") {
362            (
363                "Async/await functionality is partially supported.".to_string(),
364                "Limited to basic async functions. Complex async patterns may not work.".to_string(),
365            )
366        } else if feature_name == "dataclass" {
367            (
368                "Dataclasses have partial support.".to_string(),
369                "Basic fields work, but advanced features (frozen, slots) may not.".to_string(),
370            )
371        } else if feature_name == "lru_cache" {
372            (
373                "LRU cache decorator has partial support.".to_string(),
374                "Caching works but size limits may not be enforced.".to_string(),
375            )
376        } else {
377            (
378                format!("{} has partial support.", feature_name),
379                "Some features may not work as expected.".to_string(),
380            )
381        }
382    }
383
384    /// Get category for a warning
385    fn get_category_for_warning(&self, name: &str) -> FeatureCategory {
386        if name.contains("async") {
387            FeatureCategory::AsyncAwait
388        } else if name == "dataclass" {
389            FeatureCategory::Decorator
390        } else {
391            FeatureCategory::Other
392        }
393    }
394
395    /// Generate recommendations
396    fn generate_recommendations(
397        &self,
398        features: &FeatureSet,
399        score: &TranslatabilityScore,
400        blockers: &[Blocker],
401    ) -> Vec<Recommendation> {
402        let mut recommendations = Vec::new();
403
404        // Overall strategy recommendation
405        if score.overall >= 90.0 {
406            recommendations.push(Recommendation {
407                priority: RecommendationPriority::High,
408                title: "Full Migration Recommended".to_string(),
409                description: "Your codebase is highly compatible with Portalis.".to_string(),
410                action_items: vec![
411                    "Translate all modules at once for maximum benefit.".to_string(),
412                    "Focus on testing to ensure behavioral equivalence.".to_string(),
413                    "Consider parallel development during transition.".to_string(),
414                ],
415            });
416        } else if score.overall >= 70.0 {
417            recommendations.push(Recommendation {
418                priority: RecommendationPriority::High,
419                title: "Incremental Migration Recommended".to_string(),
420                description: "Your codebase is mostly compatible. Migrate in phases.".to_string(),
421                action_items: vec![
422                    "Start with highly compatible modules (90%+ score).".to_string(),
423                    "Address blockers in critical modules first.".to_string(),
424                    "Maintain Python fallbacks during transition.".to_string(),
425                ],
426            });
427        } else if score.overall >= 50.0 {
428            recommendations.push(Recommendation {
429                priority: RecommendationPriority::High,
430                title: "Refactoring Required Before Migration".to_string(),
431                description: "Significant blockers present. Refactor first.".to_string(),
432                action_items: vec![
433                    format!("Address {} critical blockers before migration.",
434                            blockers.iter().filter(|b| b.impact == BlockerImpact::Critical).count()),
435                    "Consider refactoring to eliminate unsupported patterns.".to_string(),
436                    "Start with a small proof-of-concept module.".to_string(),
437                ],
438            });
439        } else {
440            recommendations.push(Recommendation {
441                priority: RecommendationPriority::High,
442                title: "Migration Not Recommended at This Time".to_string(),
443                description: "Too many incompatibilities for successful migration.".to_string(),
444                action_items: vec![
445                    "Review blockers and consider if Portalis is the right solution.".to_string(),
446                    "Alternatively, refactor heavily to remove unsupported features.".to_string(),
447                    "Consider waiting for future Portalis versions with broader support.".to_string(),
448                ],
449            });
450        }
451
452        // Blocker-specific recommendations
453        if !blockers.is_empty() {
454            let critical_count = blockers.iter().filter(|b| b.impact == BlockerImpact::Critical).count();
455
456            if critical_count > 0 {
457                recommendations.push(Recommendation {
458                    priority: RecommendationPriority::High,
459                    title: format!("Address {} Critical Blockers", critical_count),
460                    description: "These features prevent translation and must be resolved.".to_string(),
461                    action_items: blockers.iter()
462                        .filter(|b| b.impact == BlockerImpact::Critical)
463                        .take(5)
464                        .map(|b| format!("{}: {}", b.feature, b.description))
465                        .collect(),
466                });
467            }
468        }
469
470        // Testing recommendations
471        recommendations.push(Recommendation {
472            priority: RecommendationPriority::Medium,
473            title: "Comprehensive Testing Required".to_string(),
474            description: "Ensure behavioral equivalence through testing.".to_string(),
475            action_items: vec![
476                "Create test suite covering all translated functionality.".to_string(),
477                "Use property-based testing for complex behaviors.".to_string(),
478                "Validate WASM output against Python reference implementation.".to_string(),
479            ],
480        });
481
482        recommendations
483    }
484
485    /// Summarize features manually
486    fn summarize_features(&self, features: &[DetectedFeature]) -> FeatureSummary {
487
488        let total_features = features.len();
489        let fully_supported = features.iter().filter(|f| f.support == FeatureSupport::Full).count();
490        let partially_supported = features.iter().filter(|f| f.support == FeatureSupport::Partial).count();
491        let unsupported = features.iter().filter(|f| f.support == FeatureSupport::None).count();
492
493        let mut by_category = HashMap::new();
494        for feature in features {
495            *by_category.entry(feature.category.clone()).or_insert(0) += 1;
496        }
497
498        FeatureSummary {
499            total_features,
500            fully_supported,
501            partially_supported,
502            unsupported,
503            by_category,
504        }
505    }
506}
507
508impl Default for CompatibilityAnalyzer {
509    fn default() -> Self {
510        Self::new()
511    }
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517    use crate::assessment::feature_detector::FeatureLocation;
518
519    #[test]
520    fn test_score_all_supported() {
521        let analyzer = CompatibilityAnalyzer::new();
522        let features = FeatureSet {
523            features: vec![],
524            summary: FeatureSummary {
525                total_features: 100,
526                fully_supported: 100,
527                partially_supported: 0,
528                unsupported: 0,
529                by_category: HashMap::new(),
530            },
531        };
532
533        let report = analyzer.analyze(&features);
534        assert_eq!(report.score.overall, 100.0);
535    }
536
537    #[test]
538    fn test_score_partial_support() {
539        let analyzer = CompatibilityAnalyzer::new();
540        let features = FeatureSet {
541            features: vec![],
542            summary: FeatureSummary {
543                total_features: 100,
544                fully_supported: 50,
545                partially_supported: 50,
546                unsupported: 0,
547                by_category: HashMap::new(),
548            },
549        };
550
551        let report = analyzer.analyze(&features);
552        assert_eq!(report.score.overall, 75.0); // 50 + 25 (50% of 50)
553    }
554
555    #[test]
556    fn test_score_with_blockers() {
557        let analyzer = CompatibilityAnalyzer::new();
558        let features = FeatureSet {
559            features: vec![],
560            summary: FeatureSummary {
561                total_features: 100,
562                fully_supported: 50,
563                partially_supported: 25,
564                unsupported: 25,
565                by_category: HashMap::new(),
566            },
567        };
568
569        let report = analyzer.analyze(&features);
570        assert_eq!(report.score.overall, 62.5); // (50 + 12.5) / 100 * 100
571    }
572
573    #[test]
574    fn test_identify_blockers() {
575        let analyzer = CompatibilityAnalyzer::new();
576        let features = FeatureSet {
577            features: vec![
578                DetectedFeature {
579                    category: FeatureCategory::Metaclass,
580                    name: "metaclass".to_string(),
581                    support: FeatureSupport::None,
582                    count: 1,
583                    locations: vec![FeatureLocation {
584                        file: "test.py".to_string(),
585                        line: Some(10),
586                        context: "class Meta(type)".to_string(),
587                    }],
588                    details: None,
589                },
590            ],
591            summary: FeatureSummary {
592                total_features: 1,
593                fully_supported: 0,
594                partially_supported: 0,
595                unsupported: 1,
596                by_category: HashMap::new(),
597            },
598        };
599
600        let report = analyzer.analyze(&features);
601        assert_eq!(report.blockers.len(), 1);
602        assert_eq!(report.blockers[0].impact, BlockerImpact::Critical);
603    }
604}