Skip to main content

konveyor_core/
fix.rs

1//! Fix strategy types for the Konveyor migration pipeline.
2//!
3//! These types define the JSON schema for `fix-strategies.json`, which is
4//! written by the semver-analyzer and read by the frontend-analyzer-provider's
5//! fix engine. All types derive both `Serialize` and `Deserialize` to ensure
6//! round-trip compatibility.
7
8use serde::{Deserialize, Serialize};
9use std::collections::{BTreeMap, HashMap};
10use std::path::Path;
11
12use anyhow::{Context, Result};
13
14// ── Fix guidance types ──────────────────────────────────────────────────
15
16/// How to fix a detected issue.
17///
18/// Mirrors the frontend-analyzer-provider's fix engine: each rule is mapped
19/// to a deterministic fix strategy with confidence level.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct FixGuidanceEntry {
22    /// The rule ID this fix corresponds to.
23    #[serde(rename = "ruleID")]
24    pub rule_id: String,
25
26    /// The fix strategy to apply.
27    pub strategy: FixStrategyKind,
28
29    /// How confident we are this fix is correct.
30    pub confidence: FixConfidence,
31
32    /// Where this fix guidance came from.
33    pub source: FixSource,
34
35    /// The affected symbol.
36    pub symbol: String,
37
38    /// Source file where the breaking change originates.
39    pub file: String,
40
41    /// Concrete instructions for fixing the issue.
42    pub fix_description: String,
43
44    /// Example of the old code pattern (when available).
45    #[serde(skip_serializing_if = "Option::is_none", default)]
46    pub before: Option<String>,
47
48    /// Example of the new code pattern (when available).
49    #[serde(skip_serializing_if = "Option::is_none", default)]
50    pub after: Option<String>,
51
52    /// Search pattern to find code that needs fixing.
53    pub search_pattern: String,
54
55    /// Suggested replacement (for mechanical fixes).
56    #[serde(skip_serializing_if = "Option::is_none", default)]
57    pub replacement: Option<String>,
58}
59
60/// What kind of fix to apply (classification label).
61///
62/// This is a classification enum used in fix guidance documents.
63/// It is distinct from the runtime `FixStrategy` in the fix engine,
64/// which carries data payloads for each variant.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(rename_all = "snake_case")]
67pub enum FixStrategyKind {
68    /// Find-and-replace: rename old symbol to new symbol.
69    Rename,
70    /// Update function call sites to match new signature.
71    UpdateSignature,
72    /// Update type annotations to match new types.
73    UpdateType,
74    /// Remove usages of a deleted symbol and find alternatives.
75    FindAlternative,
76    /// Update import paths or module system (require <-> import).
77    UpdateImport,
78    /// Ensure package.json has the correct dependency (add if missing, update if present).
79    EnsureDependency,
80    /// Requires manual review -- behavioral change or complex refactor.
81    ManualReview,
82}
83
84/// How confident the fix guidance is.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub enum FixConfidence {
88    /// Mechanical rename or direct replacement -- safe to auto-apply.
89    Exact,
90    /// Pattern-based fix -- likely correct but may need review.
91    High,
92    /// Inferred fix -- needs human verification.
93    Medium,
94    /// Best-effort suggestion -- may not be applicable.
95    Low,
96}
97
98/// Where the fix guidance originates.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub enum FixSource {
102    /// Deterministic -- derived from structural analysis.
103    Pattern,
104    /// AI-generated -- from LLM behavioral analysis.
105    Llm,
106    /// Flagged for manual intervention.
107    Manual,
108}
109
110/// Top-level fix guidance document written to `fix-guidance.yaml`.
111#[derive(Debug, Serialize, Deserialize)]
112pub struct FixGuidanceDoc {
113    /// Version range this guidance applies to.
114    pub migration: MigrationInfo,
115    /// Summary statistics.
116    pub summary: FixSummary,
117    /// Per-rule fix entries.
118    pub fixes: Vec<FixGuidanceEntry>,
119}
120
121/// Migration metadata.
122#[derive(Debug, Serialize, Deserialize)]
123pub struct MigrationInfo {
124    pub from_ref: String,
125    pub to_ref: String,
126    pub generated_by: String,
127}
128
129/// Summary of fix guidance.
130#[derive(Debug, Serialize, Deserialize)]
131pub struct FixSummary {
132    pub total_fixes: usize,
133    pub auto_fixable: usize,
134    pub needs_review: usize,
135    pub manual_only: usize,
136}
137
138// ── Machine-readable fix strategy types (fix-strategies.json) ───────────
139
140/// A single from/to mapping within a consolidated fix strategy.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct MappingEntry {
143    #[serde(skip_serializing_if = "Option::is_none", default)]
144    pub from: Option<String>,
145    #[serde(skip_serializing_if = "Option::is_none", default)]
146    pub to: Option<String>,
147    #[serde(skip_serializing_if = "Option::is_none", default)]
148    pub component: Option<String>,
149    #[serde(skip_serializing_if = "Option::is_none", default)]
150    pub prop: Option<String>,
151}
152
153/// A member-level mapping entry for structural migration strategies.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct MemberMappingEntry {
156    pub old_name: String,
157    pub new_name: String,
158}
159
160/// A single prop's migration mapping between a deprecated and v6 component.
161///
162/// Extends `MemberMappingEntry` with type information from the SD pipeline's
163/// `old_component_prop_types` and `new_component_prop_types`.
164#[derive(Debug, Clone, Default, Serialize, Deserialize)]
165pub struct PropMigrationEntry {
166    /// Prop name on the deprecated component.
167    pub old_name: String,
168    /// Prop name on the v6 replacement component.
169    pub new_name: String,
170    /// Full TypeScript type on the deprecated component (if available).
171    #[serde(skip_serializing_if = "Option::is_none", default)]
172    pub old_type: Option<String>,
173    /// Full TypeScript type on the v6 replacement component (if available).
174    #[serde(skip_serializing_if = "Option::is_none", default)]
175    pub new_type: Option<String>,
176    /// Whether the type changed between deprecated and v6.
177    #[serde(default)]
178    pub type_changed: bool,
179}
180
181/// Migration context for a deprecated component → v6 replacement.
182///
183/// Stored on family-level `FixStrategyEntry` entries to provide the LLM with
184/// the complete old→new prop mapping, including type signatures for matching
185/// props that changed type, new-only props with their types, and removed-only
186/// props with no v6 equivalent.
187#[derive(Debug, Clone, Default, Serialize, Deserialize)]
188pub struct DeprecatedMigrationContext {
189    /// Package the deprecated component was imported from.
190    pub old_package: String,
191    /// Package the v6 replacement is imported from.
192    pub new_package: String,
193    /// Props that exist on both old and new, with name mappings and types.
194    #[serde(skip_serializing_if = "Vec::is_empty", default)]
195    pub matching_props: Vec<PropMigrationEntry>,
196    /// Props that exist ONLY on the v6 component (not on deprecated).
197    /// Map of prop_name → TypeScript type string.
198    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
199    pub new_props: BTreeMap<String, String>,
200    /// Props that exist ONLY on the deprecated component (no v6 equivalent).
201    #[serde(skip_serializing_if = "Vec::is_empty", default)]
202    pub removed_props: Vec<String>,
203}
204
205/// A machine-readable fix strategy entry.
206///
207/// For non-consolidated rules, `from`/`to` hold the single mapping.
208/// For consolidated rules, `mappings` holds all individual mappings from the
209/// merged rules, allowing the fix engine to apply all renames/removals.
210/// For structural migration rules, `member_mappings` and `removed_members`
211/// describe the member-level overlap between removed and replacement interfaces.
212#[derive(Debug, Clone, Default, Serialize, Deserialize)]
213pub struct FixStrategyEntry {
214    pub strategy: String,
215    #[serde(skip_serializing_if = "Option::is_none", default)]
216    pub from: Option<String>,
217    #[serde(skip_serializing_if = "Option::is_none", default)]
218    pub to: Option<String>,
219    #[serde(skip_serializing_if = "Option::is_none", default)]
220    pub component: Option<String>,
221    #[serde(skip_serializing_if = "Option::is_none", default)]
222    pub prop: Option<String>,
223    /// All individual mappings when this strategy was merged from multiple rules.
224    #[serde(skip_serializing_if = "Vec::is_empty", default)]
225    pub mappings: Vec<MappingEntry>,
226    /// Structural migration: matching member mappings between removed and replacement.
227    #[serde(skip_serializing_if = "Vec::is_empty", default)]
228    pub member_mappings: Vec<MemberMappingEntry>,
229    /// Structural migration: member names only in the removed interface (no match).
230    #[serde(skip_serializing_if = "Vec::is_empty", default)]
231    pub removed_members: Vec<String>,
232    /// Structural migration: the replacement symbol name.
233    #[serde(skip_serializing_if = "Option::is_none", default)]
234    pub replacement: Option<String>,
235    /// Structural migration: overlap ratio between removed and replacement.
236    #[serde(skip_serializing_if = "Option::is_none", default)]
237    pub overlap_ratio: Option<f64>,
238    /// Dependency update: npm package name (e.g., "@patternfly/react-core").
239    #[serde(skip_serializing_if = "Option::is_none", default)]
240    pub package: Option<String>,
241    /// Dependency update: new version range (e.g., "^6.1.0").
242    #[serde(skip_serializing_if = "Option::is_none", default)]
243    pub new_version: Option<String>,
244
245    // ── Family migration fields ────────────────────────────────────────
246    // Used by `FamilyMigration` strategy entries (keyed `family:<Name>`)
247    // to describe the complete target component structure for a family.
248    /// Target JSX structure template showing correct composition.
249    /// Example: `<Modal ...>\n  <ModalHeader .../>\n  <ModalBody>...</ModalBody>\n</Modal>`
250    #[serde(skip_serializing_if = "Option::is_none", default)]
251    pub target_structure: Option<String>,
252    /// Props that remain on the root component in the new version.
253    #[serde(skip_serializing_if = "Vec::is_empty", default)]
254    pub retained_props: Vec<String>,
255    /// Map of removed prop name → child component that now owns it (as a named prop).
256    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
257    pub prop_to_child: BTreeMap<String, String>,
258    /// Props removed from the root that don't have an exact prop-name match
259    /// on any child component. Maps prop name → description of where/how to
260    /// migrate (e.g., "ModalFooter (as children)"). Unlike `prop_to_child`,
261    /// these props typically become *children* of the target component or
262    /// are removed entirely.
263    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
264    pub unmapped_removed_props: BTreeMap<String, String>,
265    /// Child component names removed from the family (flattened into parent).
266    #[serde(skip_serializing_if = "Vec::is_empty", default)]
267    pub removed_children: Vec<String>,
268    /// Map of "Child.prop" → "Parent.prop" for child-to-prop migrations.
269    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
270    pub child_props_to_parent: BTreeMap<String, String>,
271    /// Prop value changes: prop_name → list of old→new value mappings.
272    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
273    pub prop_value_changes: BTreeMap<String, Vec<MappingEntry>>,
274    /// New imports needed after restructuring (child components to add).
275    #[serde(skip_serializing_if = "Vec::is_empty", default)]
276    pub new_imports: Vec<String>,
277    /// Imports to remove after restructuring (removed child components).
278    #[serde(skip_serializing_if = "Vec::is_empty", default)]
279    pub removed_imports: Vec<String>,
280    /// Import source package (e.g., "@patternfly/react-core").
281    #[serde(skip_serializing_if = "Option::is_none", default)]
282    pub import_source: Option<String>,
283
284    // ── Deprecated migration context ──────────────────────────────────
285    // Populated for families where a deprecated component has a v6 replacement.
286    // Provides the complete old→new prop mapping with type signatures.
287    /// Deprecated → v6 migration context with prop mappings, types, and
288    /// new/removed prop lists. Present when the family includes a deprecated
289    /// component that has a detected migration target.
290    #[serde(skip_serializing_if = "Option::is_none", default)]
291    pub deprecated_migration: Option<DeprecatedMigrationContext>,
292
293    /// Patterns to exclude from this fix strategy's automated replacement.
294    ///
295    /// Used with `CssVariablePrefix` to prevent blind prefix swaps on
296    /// CSS classes that were removed in the target version. When the
297    /// matched text contains any of these patterns, the edit is skipped.
298    /// The incident is still reported (as Manual) by a separate dead-class rule.
299    #[serde(skip_serializing_if = "Vec::is_empty", default)]
300    pub exclude_patterns: Vec<String>,
301}
302
303impl FixStrategyEntry {
304    /// Create a new strategy entry with only the strategy type set.
305    pub fn new(strategy: &str) -> Self {
306        Self {
307            strategy: strategy.into(),
308            ..Default::default()
309        }
310    }
311
312    /// Create a Rename strategy with a single from/to pair.
313    pub fn rename(from: impl Into<String>, to: impl Into<String>) -> Self {
314        Self {
315            strategy: "Rename".into(),
316            from: Some(from.into()),
317            to: Some(to.into()),
318            ..Default::default()
319        }
320    }
321
322    /// Create a strategy with from/to and a named strategy type.
323    pub fn with_from_to(strategy: &str, from: impl Into<String>, to: impl Into<String>) -> Self {
324        Self {
325            strategy: strategy.into(),
326            from: Some(from.into()),
327            to: Some(to.into()),
328            ..Default::default()
329        }
330    }
331
332    /// Create a RemoveProp strategy.
333    pub fn remove_prop(component: impl Into<String>, prop: impl Into<String>) -> Self {
334        Self {
335            strategy: "RemoveProp".into(),
336            component: Some(component.into()),
337            prop: Some(prop.into()),
338            ..Default::default()
339        }
340    }
341
342    /// Create an LlmAssisted strategy enriched with structural migration data.
343    pub fn structural_migration(
344        removed_symbol: &str,
345        replacement_symbol: &str,
346        member_mappings: Vec<MemberMappingEntry>,
347        removed_members: Vec<String>,
348        overlap_ratio: f64,
349    ) -> Self {
350        Self {
351            strategy: "LlmAssisted".into(),
352            from: Some(removed_symbol.into()),
353            to: Some(replacement_symbol.into()),
354            member_mappings,
355            removed_members,
356            replacement: Some(replacement_symbol.into()),
357            overlap_ratio: Some(overlap_ratio),
358            ..Default::default()
359        }
360    }
361
362    /// Create an EnsureDependency strategy: add if missing, update version if present.
363    pub fn ensure_dependency(package: impl Into<String>, new_version: impl Into<String>) -> Self {
364        Self {
365            strategy: "EnsureDependency".into(),
366            package: Some(package.into()),
367            new_version: Some(new_version.into()),
368            ..Default::default()
369        }
370    }
371
372    /// Convert to a MappingEntry (extracting the single mapping).
373    pub fn to_mapping(&self) -> MappingEntry {
374        MappingEntry {
375            from: self.from.clone(),
376            to: self.to.clone(),
377            component: self.component.clone(),
378            prop: self.prop.clone(),
379        }
380    }
381}
382
383// ── IO helpers ──────────────────────────────────────────────────────────
384
385/// Extract fix strategies from the final (post-consolidation) rules.
386pub fn extract_fix_strategies(
387    rules: &[crate::rule::KonveyorRule],
388) -> HashMap<String, FixStrategyEntry> {
389    rules
390        .iter()
391        .filter_map(|r| {
392            r.fix_strategy
393                .as_ref()
394                .map(|s| (r.rule_id.clone(), s.clone()))
395        })
396        .collect()
397}
398
399/// Write fix strategies JSON to the fix-guidance directory.
400pub fn write_fix_strategies(
401    fix_dir: &Path,
402    strategies: &HashMap<String, FixStrategyEntry>,
403) -> Result<()> {
404    let path = fix_dir.join("fix-strategies.json");
405    let json =
406        serde_json::to_string_pretty(strategies).context("Failed to serialize fix strategies")?;
407    std::fs::write(&path, &json).with_context(|| format!("Failed to write {}", path.display()))?;
408    Ok(())
409}
410
411/// Priority for fix strategy type. Higher = more actionable.
412pub fn strategy_priority(strategy: &str) -> u8 {
413    match strategy {
414        "Rename" => 5,
415        "RemoveProp" => 4,
416        "CssVariablePrefix" => 4,
417        "ImportPathChange" => 3,
418        "PropValueChange" => 2,
419        "PropTypeChange" => 2,
420        "LlmAssisted" => 1,
421        _ => 0,
422    }
423}