execute method
Execute the generate_migration_plan tool.
Input arguments:
projectPath(String, required): Path to the Flutter/Dart project root.upgrades(ListanalysisDepth(String, optional): Depth of impact analysis. One offile_level,line_level, orsymbol_level. Defaults tofile_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();
}