execute method

Future<Map<String, dynamic>> execute(
  1. Map<String, dynamic> args
)

Execute the generate_migration_plan tool.

Input arguments:

  • projectPath (String, required): Path to the Flutter/Dart project root.
  • upgrades (List
  • analysisDepth (String, optional): Depth of impact analysis. One of file_level, line_level, or symbol_level. Defaults to file_level.

Returns a JSON-serialisable map representing the MigrationPlan.

Implementation

Future<Map<String, dynamic>> execute(Map<String, dynamic> args) async {
  final projectPath = args['projectPath'] as String;
  final upgrades = (args['upgrades'] as List).cast<Map<String, dynamic>>();
  final analysisDepth = args['analysisDepth'] as String? ?? 'file_level';

  Logger.info('Generating migration plan for ${upgrades.length} upgrade(s)');

  if (upgrades.isEmpty) {
    return const MigrationPlan(
      steps: [],
      estimatedEffort: EffortLevel.trivial,
      effortDescription: 'No upgrades requested.',
      prerequisites: [],
      warnings: ['No upgrades were provided.'],
    ).toJson();
  }

  // Parse the project's pubspec to understand current dependency state.
  final pubspec = await _pubspecParser.parse(projectPath);

  // -----------------------------------------------------------------------
  // Step 1: Run impact analysis for each requested upgrade.
  // -----------------------------------------------------------------------
  final analysisByPackage = <String, Map<String, dynamic>>{};
  for (final upgrade in upgrades) {
    final packageName = upgrade['packageName'] as String;
    final targetVersion = upgrade['targetVersion'] as String;

    try {
      final result = await _analyzeImpact.execute(
        projectPath: projectPath,
        packageName: packageName,
        targetVersion: targetVersion,
        analysisDepth: analysisDepth,
        includeCascading: true,
      );
      analysisByPackage[packageName] = result;
    } catch (e) {
      Logger.warn('Failed to analyze impact for $packageName: $e');
      analysisByPackage[packageName] = {'error': e.toString()};
    }
  }

  // -----------------------------------------------------------------------
  // Step 2: Determine upgrade order (dependencies-first topological sort).
  // -----------------------------------------------------------------------
  final orderedUpgrades = _orderUpgrades(upgrades, analysisByPackage);

  // -----------------------------------------------------------------------
  // Step 3: Build the list of migration steps.
  // -----------------------------------------------------------------------
  final steps = <MigrationStep>[];
  var stepOrder = 1;
  final allWarnings = <String>[];
  final prerequisites = <String>[
    'Ensure all tests pass before starting migration',
    'Create a backup branch: git checkout -b pre-migration-backup',
    'Ensure you have a clean working tree (no uncommitted changes)',
  ];

  for (final upgrade in orderedUpgrades) {
    final packageName = upgrade['packageName'] as String;
    final targetVersion = upgrade['targetVersion'] as String;
    final analysis = analysisByPackage[packageName];

    // Look up the current dependency entry.
    final currentDep = pubspec.dependencies[packageName] ??
        pubspec.devDependencies[packageName];

    final currentVersion = currentDep?.resolvedVersion;
    final currentConstraint = currentDep?.versionConstraint;

    // -- Warn if the package is not found in pubspec --
    if (currentDep == null) {
      allWarnings.add(
        '$packageName is not listed in pubspec.yaml; '
        'it will be added as a new dependency.',
      );
    }

    // -- Warn if analysis produced an error --
    if (analysis != null && analysis.containsKey('error')) {
      allWarnings.add(
        'Impact analysis for $packageName failed: ${analysis['error']}. '
        'Manual review is recommended.',
      );
    }

    // -- Warn about non-hosted sources --
    if (currentDep != null && currentDep.source != 'hosted') {
      allWarnings.add(
        '$packageName uses source "${currentDep.source}"; '
        'automatic version constraint update may not apply.',
      );
    }

    // -----------------------------------------------------------------
    // Sub-step A: Update pubspec.yaml
    // -----------------------------------------------------------------
    final suggestedConstraint =
        _versionResolver.suggestConstraint(targetVersion);

    steps.add(MigrationStep(
      order: stepOrder++,
      type: StepType.pubspecChange,
      description: 'Update $packageName to $targetVersion in pubspec.yaml',
      packageName: packageName,
      targetVersion: targetVersion,
      codeChanges: [
        CodeChange(
          filePath: 'pubspec.yaml',
          line: 0,
          before: '$packageName: ${currentConstraint ?? "any"}',
          after: '$packageName: $suggestedConstraint',
          explanation:
              'Update version constraint for $packageName to allow $targetVersion',
        ),
      ],
    ));

    // -----------------------------------------------------------------
    // Sub-step B: Run dependency resolution
    // -----------------------------------------------------------------
    steps.add(MigrationStep(
      order: stepOrder++,
      type: StepType.runCommand,
      description: 'Resolve updated dependencies for $packageName',
      packageName: packageName,
      command: 'dart pub get',
    ));

    // -----------------------------------------------------------------
    // Sub-step C: Handle cascading dependency conflicts
    // -----------------------------------------------------------------
    if (analysis != null && analysis['cascadingImpacts'] is List) {
      final cascading = analysis['cascadingImpacts'] as List;
      for (final impact in cascading) {
        if (impact is Map<String, dynamic>) {
          final depName = impact['dependencyName'] as String? ?? 'unknown';
          final requiredBy = impact['requiredBy'] as String? ?? packageName;
          final currentConstraintStr =
              impact['currentConstraint'] as String? ?? 'any';
          final conflictReason = impact['conflictReason'] as String?;

          steps.add(MigrationStep(
            order: stepOrder++,
            type: StepType.manual,
            description:
                'Resolve cascading dependency conflict: $depName '
                '(required by $requiredBy, current constraint: '
                '$currentConstraintStr)',
            packageName: depName,
            codeChanges: [
              CodeChange(
                filePath: 'pubspec.yaml',
                line: 0,
                before: '$depName: $currentConstraintStr',
                after: '$depName: // Update to a compatible version',
                explanation: conflictReason ??
                    'This dependency may need a version bump to be '
                        'compatible with $packageName $targetVersion',
              ),
            ],
          ));

          allWarnings.add(
            'Cascading impact: $depName may need updating due to '
            '$packageName upgrade (required by $requiredBy).',
          );
        }
      }
    }

    // -----------------------------------------------------------------
    // Sub-step D: Apply code changes based on breaking change impacts
    // -----------------------------------------------------------------
    if (analysis != null && analysis['impacts'] is List) {
      final impacts = analysis['impacts'] as List;
      for (final impact in impacts) {
        if (impact is Map<String, dynamic>) {
          final breakingChange =
              impact['breakingChange'] as Map<String, dynamic>?;
          final locations = impact['affectedLocations'] as List?;
          final suggestedFix = impact['suggestedFix'] as String?;

          if (locations != null && locations.isNotEmpty) {
            final codeChanges = <CodeChange>[];
            for (final loc in locations) {
              if (loc is Map<String, dynamic>) {
                codeChanges.add(CodeChange(
                  filePath: loc['filePath'] as String? ?? '',
                  line: loc['line'] as int? ?? 0,
                  before: loc['lineContent'] as String? ?? '',
                  after: suggestedFix ?? '// TODO: Update this usage',
                  explanation: breakingChange?['description'] as String?,
                ));
              }
            }

            if (codeChanges.isNotEmpty) {
              final changeDescription =
                  breakingChange?['description'] as String? ??
                      'Update code for $packageName breaking change';
              final changeSeverity =
                  breakingChange?['severity'] as String? ?? 'major';
              final affectedApi =
                  breakingChange?['affectedApi'] as String?;

              final descParts = <String>[changeDescription];
              if (affectedApi != null) {
                descParts.add('(API: $affectedApi)');
              }

              steps.add(MigrationStep(
                order: stepOrder++,
                type: StepType.codeChange,
                description: descParts.join(' '),
                packageName: packageName,
                targetVersion: targetVersion,
                codeChanges: codeChanges,
              ));

              // Add warnings for critical breaking changes.
              if (changeSeverity == 'critical') {
                allWarnings.add(
                  'Critical breaking change in $packageName: '
                  '$changeDescription '
                  '(affects ${codeChanges.length} location(s))',
                );
              }
            }
          } else if (breakingChange != null) {
            // Breaking change with no detected locations — add as manual step
            // so it is not silently ignored.
            final description =
                breakingChange['description'] as String? ??
                    'Undetected breaking change in $packageName';
            final migrationGuide =
                breakingChange['migrationGuide'] as String?;

            steps.add(MigrationStep(
              order: stepOrder++,
              type: StepType.manual,
              description:
                  'Review breaking change: $description'
                  '${migrationGuide != null ? " (see: $migrationGuide)" : ""}',
              packageName: packageName,
              targetVersion: targetVersion,
            ));
          }
        }
      }
    }

    // -----------------------------------------------------------------
    // Sub-step E: Run dart fix for major version bumps
    // -----------------------------------------------------------------
    final resolvedCurrentVersion = currentVersion ?? '0.0.0';
    if (_isMajorBumpSafe(resolvedCurrentVersion, targetVersion)) {
      steps.add(MigrationStep(
        order: stepOrder++,
        type: StepType.runCommand,
        description:
            'Apply automated dart fix suggestions for $packageName',
        packageName: packageName,
        command: 'dart fix --apply',
      ));
    }

    // -----------------------------------------------------------------
    // Sub-step F: Run static analysis
    // -----------------------------------------------------------------
    steps.add(MigrationStep(
      order: stepOrder++,
      type: StepType.runCommand,
      description: 'Run static analysis after $packageName migration',
      packageName: packageName,
      command: 'dart analyze',
    ));

    // -- Collect warnings from analysis result --
    if (analysis != null && analysis['warnings'] is List) {
      final analysisWarnings = analysis['warnings'] as List;
      for (final w in analysisWarnings) {
        if (w is String && !allWarnings.contains(w)) {
          allWarnings.add(w);
        }
      }
    }
  }

  // -----------------------------------------------------------------------
  // Step 4: Final verification steps
  // -----------------------------------------------------------------------
  steps.add(MigrationStep(
    order: stepOrder++,
    type: StepType.runCommand,
    description: 'Run full static analysis to verify migration',
    command: 'dart analyze',
  ));

  steps.add(MigrationStep(
    order: stepOrder++,
    type: StepType.runCommand,
    description: 'Run all tests to verify migration',
    command: 'dart test',
  ));

  steps.add(MigrationStep(
    order: stepOrder++,
    type: StepType.manual,
    description: 'Review all changes and run integration/manual tests',
  ));

  // -----------------------------------------------------------------------
  // Step 5: Estimate overall effort
  // -----------------------------------------------------------------------
  final effort = _estimateEffort(analysisByPackage);

  final plan = MigrationPlan(
    steps: steps,
    estimatedEffort: effort,
    effortDescription: _effortDescription(effort, upgrades.length),
    prerequisites: prerequisites,
    warnings: allWarnings,
  );

  Logger.info(
    'Migration plan generated: ${steps.length} step(s), '
    'effort: ${effort.name}',
  );

  return plan.toJson();
}