execute method
Execute the analyze_upgrade_impact tool.
Required parameters:
projectPath: absolute path to the Flutter/Dart project root.packageName: the package to analyze.targetVersion: the version to upgrade to.
Optional parameters:
analysisDepth:"summary","file_level"(default), or"line_level".includeCascading: whether to check for cascading dependency impacts (defaulttrue).
Returns the AnalysisResult serialised as a JSON-compatible map.
Implementation
Future<Map<String, dynamic>> execute({
required String projectPath,
required String packageName,
required String targetVersion,
String analysisDepth = 'file_level',
bool includeCascading = true,
}) async {
Logger.info(
'Analyzing upgrade impact: $packageName -> $targetVersion '
'($analysisDepth)',
);
// -------------------------------------------------------------------------
// Step 1: Resolve the current version from pubspec / lockfile.
// -------------------------------------------------------------------------
final pubspec = await _pubspecParser.parse(projectPath);
final currentDep = pubspec.dependencies[packageName] ??
pubspec.devDependencies[packageName];
final currentVersion = currentDep?.resolvedVersion;
if (currentVersion == null) {
throw ArgumentError(
'Package $packageName not found in project or has no resolved version',
);
}
Logger.info(
'Current version of $packageName: $currentVersion -> $targetVersion',
);
final warnings = <String>[];
final isMajor = _versionResolver.isMajorBump(currentVersion, targetVersion);
if (isMajor) {
warnings.add(
'This is a major version bump ($currentVersion -> $targetVersion). '
'Breaking changes are likely.',
);
}
// -------------------------------------------------------------------------
// Step 2: Fetch breaking-change data from all sources IN PARALLEL.
// -------------------------------------------------------------------------
final isFlutterSdk = packageName == 'flutter' ||
packageName == 'flutter_sdk' ||
currentDep?.source == 'sdk';
// Launch all independent data fetches concurrently.
final changelogEntriesFuture = _fetchChangelogEntries(
packageName,
currentVersion,
targetVersion,
);
final releaseBreakingChangesFuture = _fetchReleaseBreakingChanges(
packageName,
currentVersion,
targetVersion,
);
final issueBreakingChangesFuture = _fetchIssueBreakingChanges(
packageName,
);
final flutterDocsChangesFuture = isFlutterSdk
? _flutterDocs.getBreakingChangesForVersionRange(
currentVersion,
targetVersion,
)
: Future.value(<Map<String, dynamic>>[]);
final versionsFuture = _pubApi.getVersions(packageName);
// Await all results together.
final results = await Future.wait([
changelogEntriesFuture, // [0] List<ChangelogEntry>
releaseBreakingChangesFuture, // [1] List<BreakingChange>
issueBreakingChangesFuture, // [2] List<BreakingChange>
flutterDocsChangesFuture, // [3] List<Map<String, dynamic>>
versionsFuture, // [4] List<String>
]);
final changelogEntries = results[0] as List<ChangelogEntry>;
final releaseBreakingChanges = results[1] as List<BreakingChange>;
final issueBreakingChanges = results[2] as List<BreakingChange>;
final flutterDocsChanges = results[3] as List<Map<String, dynamic>>;
final allVersions = results[4] as List<String>;
// Log version range information.
final versionsInRange = _versionResolver.getVersionsInRange(
allVersions,
currentVersion,
targetVersion,
);
Logger.info(
'Found ${versionsInRange.length} version(s) between '
'$currentVersion and $targetVersion',
);
// -------------------------------------------------------------------------
// Step 2b: Merge and deduplicate breaking changes from all sources.
// -------------------------------------------------------------------------
final allBreakingChanges = <BreakingChange>[];
// From CHANGELOG entries.
for (final entry in changelogEntries) {
allBreakingChanges.addAll(entry.breakingChanges);
}
// From GitHub releases.
allBreakingChanges.addAll(releaseBreakingChanges);
// From GitHub issues.
allBreakingChanges.addAll(issueBreakingChanges);
// From Flutter docs (convert maps to BreakingChange objects).
for (final doc in flutterDocsChanges) {
allBreakingChanges.add(_flutterDocToBreakingChange(doc));
}
// Deduplicate by affectedApi (prefer higher-confidence entries).
final deduplicated = _deduplicateBreakingChanges(allBreakingChanges);
Logger.info(
'Collected ${allBreakingChanges.length} breaking change(s), '
'${deduplicated.length} after deduplication',
);
if (deduplicated.isEmpty && isMajor) {
warnings.add(
'No breaking changes detected for a major version bump. '
'This may indicate incomplete changelog data.',
);
}
// -------------------------------------------------------------------------
// Step 3: AST-based codebase analysis (depth-dependent).
// -------------------------------------------------------------------------
var totalFilesAffected = 0;
var totalLocationsAffected = 0;
var impacts = <BreakingChangeImpact>[];
if (analysisDepth == 'summary') {
// Summary mode: just count files importing the package; skip AST.
totalFilesAffected = await _codebaseAnalyzer.countImportingFiles(
projectPath,
packageName,
);
Logger.info(
'Summary mode: $totalFilesAffected file(s) import $packageName',
);
// Create impacts with empty locations for each breaking change.
impacts = deduplicated
.map((bc) => BreakingChangeImpact(
breakingChange: bc,
affectedLocations: const [],
suggestedFix: _buildSuggestedFix(bc),
))
.toList();
// In summary mode, totalLocationsAffected is estimated as
// totalFilesAffected (at least one usage per importing file).
totalLocationsAffected = totalFilesAffected;
} else {
// file_level or line_level: use CodebaseAnalyzer.searchApiUsages
// to find exact usages of affected APIs.
final resolveTypes = analysisDepth == 'line_level';
// Extract all affected API names from breaking changes.
final affectedApis = _extractAffectedApis(deduplicated);
if (affectedApis.isNotEmpty) {
Logger.info(
'Searching for ${affectedApis.length} affected API(s) '
'in codebase (resolve=$resolveTypes)',
);
final usageResults = await _codebaseAnalyzer.searchApiUsages(
projectPath: projectPath,
apis: affectedApis,
packageFilter: packageName,
resolveTypes: resolveTypes,
);
// Build BreakingChangeImpact list mapping each breaking change to
// the code locations that use its affected API.
impacts = _buildImpacts(deduplicated, usageResults);
// Compute totals.
final allAffectedFiles = <String>{};
for (final impact in impacts) {
for (final loc in impact.affectedLocations) {
allAffectedFiles.add(loc.filePath);
totalLocationsAffected++;
}
}
totalFilesAffected = allAffectedFiles.length;
} else {
// No affected APIs known, but we still know files that import
// the package.
totalFilesAffected = await _codebaseAnalyzer.countImportingFiles(
projectPath,
packageName,
);
impacts = deduplicated
.map((bc) => BreakingChangeImpact(
breakingChange: bc,
affectedLocations: const [],
suggestedFix: _buildSuggestedFix(bc),
))
.toList();
if (deduplicated.isNotEmpty) {
warnings.add(
'Breaking changes were detected but none specify affected API '
'names. Could not perform targeted codebase search. '
'$totalFilesAffected file(s) import this package.',
);
}
}
Logger.info(
'Codebase analysis: $totalFilesAffected file(s) affected, '
'$totalLocationsAffected location(s)',
);
}
// -------------------------------------------------------------------------
// Step 4: Cascade analysis (if requested).
// -------------------------------------------------------------------------
var cascadingImpacts = <CascadingImpact>[];
if (includeCascading) {
try {
cascadingImpacts = await _cascadeResolver.resolve(
packageName: packageName,
targetVersion: targetVersion,
currentPubspec: pubspec,
);
if (cascadingImpacts.isNotEmpty) {
Logger.info(
'Found ${cascadingImpacts.length} cascading impact(s)',
);
warnings.add(
'${cascadingImpacts.length} cascading dependency conflict(s) '
'detected. Other packages may need to be upgraded simultaneously.',
);
}
} catch (e) {
Logger.warn('Cascade analysis failed: $e');
warnings.add('Cascade analysis failed: $e');
}
}
// -------------------------------------------------------------------------
// Step 5: Risk assessment.
// -------------------------------------------------------------------------
final overallConfidence = _calculateOverallConfidence(deduplicated);
final riskScore = _calculateRiskScore(
impacts,
totalFilesAffected,
overallConfidence,
);
// Boost risk for cascading impacts.
final adjustedScore = cascadingImpacts.isNotEmpty
? math.min(10.0, riskScore + cascadingImpacts.length * 0.5)
: riskScore;
final adjustedLevel = _riskLevelFromScore(adjustedScore);
Logger.info(
'Risk assessment: score=$adjustedScore, level=${adjustedLevel.name}, '
'confidence=$overallConfidence',
);
// -------------------------------------------------------------------------
// Build and return the result.
// -------------------------------------------------------------------------
final result = AnalysisResult(
packageName: packageName,
currentVersion: currentVersion,
targetVersion: targetVersion,
riskLevel: adjustedLevel,
riskScore: adjustedScore,
totalFilesAffected: totalFilesAffected,
totalLocationsAffected: totalLocationsAffected,
impacts: impacts,
cascadingImpacts: cascadingImpacts,
warnings: warnings,
overallConfidence: overallConfidence,
);
return result.toJson();
}