Skip to main content

morph_cli/recipes/react_class_to_hooks/
mod.rs

1mod detect;
2mod transform;
3
4use std::path::Path;
5
6use anyhow::Result;
7use indicatif::ProgressBar;
8use walkdir::WalkDir;
9
10use crate::core::ast::parser::parse_file;
11use crate::core::cache::{CachedDetectionOutcome, load_detection, record_miss, save_detection};
12use crate::core::recipe::{
13    DetectionFailure, DetectionReport, Recipe, RecipeMetadata, TransformOptions, TransformReport,
14};
15
16use self::detect::analyze_parsed_module;
17use self::transform::transform_report;
18
19pub struct ReactClassToHooksRecipe;
20
21const METADATA: RecipeMetadata = RecipeMetadata {
22    name: "react-class-to-hooks",
23    description: "Detects React class components before a future hooks migration.",
24    supported_extensions: &["js", "jsx", "ts", "tsx"],
25    required_recipes: &[],
26    incompatible_recipes: &[],
27    should_run_before: &[],
28    should_run_after: &["js-to-ts"],
29    maturity: crate::core::recipe::RecipeMaturity::Experimental,
30    category: crate::core::recipe::RecipeCategory::Experimental,
31    tags: &["risky", "react", "frontend", "hooks", "components", "class-components"],
32};
33
34impl Recipe for ReactClassToHooksRecipe {
35    fn metadata(&self) -> &'static RecipeMetadata {
36        &METADATA
37    }
38
39    fn detect(&self, root: &Path, progress: &ProgressBar) -> Result<DetectionReport> {
40        let mut report = DetectionReport::default();
41
42        for entry in WalkDir::new(root)
43            .into_iter()
44            .filter_entry(|e| {
45                let name = e.file_name().to_string_lossy();
46                name != "node_modules" && name != ".git" && name != "target" && name != "dist" && name != "build"
47            })
48            .filter_map(|entry| entry.ok())
49        {
50            let path = entry.path();
51            progress.set_message(format!("Scanning {}", path.display()));
52
53            if entry.file_type().is_file() && has_supported_extension(path) {
54                report.total_files += 1;
55
56                if let Some(outcome) = load_detection(METADATA.name, path) {
57                    apply_cached_outcome(&mut report, outcome);
58                    continue;
59                }
60                record_miss();
61
62                match parse_file(path) {
63                    Ok(parsed) => {
64                        report.parseable_files += 1;
65
66                        if let Some(analysis) = analyze_parsed_module(&parsed) {
67                            let _ = save_detection(
68                                METADATA.name,
69                                path,
70                                &CachedDetectionOutcome::Analysis(analysis.clone()),
71                            );
72                            report.analyses.push(analysis);
73                        } else {
74                            let skipped = path.to_path_buf();
75                            let _ = save_detection(
76                                METADATA.name,
77                                path,
78                                &CachedDetectionOutcome::Skipped(skipped.clone()),
79                            );
80                            report.skipped_files.push(skipped);
81                        }
82                    }
83                    Err(error) => {
84                        let failure = DetectionFailure {
85                            path: error.path().to_path_buf(),
86                            error: error.to_string(),
87                        };
88                        let _ = save_detection(
89                            METADATA.name,
90                            path,
91                            &CachedDetectionOutcome::Failure(failure.clone()),
92                        );
93                        report.failed_files.push(failure);
94                    }
95                }
96            }
97        }
98
99        Ok(report)
100    }
101
102    fn transform(
103        &self,
104        report: &DetectionReport,
105        options: TransformOptions,
106    ) -> Result<TransformReport> {
107        transform_report(report, options)
108    }
109}
110
111fn apply_cached_outcome(report: &mut DetectionReport, outcome: CachedDetectionOutcome) {
112    match outcome {
113        CachedDetectionOutcome::Analysis(analysis) => {
114            report.parseable_files += 1;
115            report.analyses.push(analysis);
116        }
117        CachedDetectionOutcome::Skipped(path) => {
118            report.parseable_files += 1;
119            report.skipped_files.push(path);
120        }
121        CachedDetectionOutcome::Failure(failure) => report.failed_files.push(failure),
122    }
123}
124
125fn has_supported_extension(path: &Path) -> bool {
126    path.extension()
127        .and_then(|extension| extension.to_str())
128        .is_some_and(|extension| METADATA.supported_extensions.contains(&extension))
129}