1use super::feature_detector::{DetectedFeature, FeatureSet, FeatureSupport, FeatureCategory, FeatureSummary};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub enum ConfidenceLevel {
30 High, Medium, Low, }
34
35#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub enum BlockerImpact {
50 Critical, High, Medium, Low, }
55
56#[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#[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#[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
93pub struct CompatibilityAnalyzer {
95 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 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(), }
120 }
121
122 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 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 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 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 let score = (features.summary.fully_supported as f64
183 + features.summary.partially_supported as f64 * 0.5) / total * 100.0;
184
185 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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); }
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); }
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}