scan method
Executes the scan and returns a ScannerResult.
Implementation
@override
Future<ScannerResult> scan(ProjectContext context) async {
context.logger.startSpinner('Scanning Assets...');
final issues = <Issue>[];
int totalAssets = 0;
double totalSizeMb = 0.0;
final flutterMap = context.pubspec?['flutter'] as Map?;
final assetsList = flutterMap?['assets'];
if (assetsList == null || assetsList is! List) {
context.logger.stopSpinner();
return ScannerResult(issues: issues, metrics: {});
}
final declaredAssets = <String>{};
for (final asset in assetsList) {
declaredAssets.add(asset.toString());
}
// Expand directories to actual files
final actualAssetFiles = <File>[];
for (final assetPath in declaredAssets) {
if (assetPath.endsWith('/')) {
final dir = Directory(p.join(context.directory, assetPath));
if (dir.existsSync()) {
actualAssetFiles.addAll(
dir.listSync(recursive: false).whereType<File>(),
);
}
} else {
final file = File(p.join(context.directory, assetPath));
if (file.existsSync()) {
actualAssetFiles.add(file);
} else {
issues.add(
Issue(
title: 'Missing Asset',
description:
'Asset "$assetPath" is declared in pubspec.yaml but does not exist.',
category: 'Assets',
severity: IssueSeverity.high,
suggestion:
'Remove it from pubspec.yaml or add the missing file.',
),
);
}
}
}
// Find all strings in dart code (simple regex for now, AST is more complex for pure string finding across whole codebase quickly, but let's do a fast regex + AST combined approach)
final dartFiles = FileUtils.getDartFiles(context.directory);
final allDartCode = dartFiles.map((f) => f.readAsStringSync()).join('\n');
final assetHashes = <String, String>{}; // hash -> path
for (final file in actualAssetFiles) {
totalAssets++;
final fileSize = file.lengthSync() / (1024 * 1024);
totalSizeMb += fileSize;
final relPath = p.relative(file.path, from: context.directory);
// Check if it's used in code
// We look for Exact String references, Image.asset(), etc.
// Easiest robust way for asset usages is checking if the exact relative path string exists anywhere in Dart code.
if (!allDartCode.contains(relPath) &&
!allDartCode.contains(p.basename(relPath))) {
issues.add(
Issue(
title: 'Unused Asset',
description: 'Asset "$relPath" is not referenced in any Dart file.',
category: 'Assets',
severity: IssueSeverity.medium,
file: relPath,
suggestion: 'Delete the asset to save space.',
),
);
}
// Check duplicates
final bytes = file.readAsBytesSync();
final hash = md5.convert(bytes).toString();
if (assetHashes.containsKey(hash)) {
issues.add(
Issue(
title: 'Duplicate Asset',
description:
'Asset "$relPath" is identical to "${assetHashes[hash]}".',
category: 'Assets',
severity: IssueSeverity.medium,
file: relPath,
suggestion:
'Use a single asset reference and delete the duplicate.',
),
);
} else {
assetHashes[hash] = relPath;
}
// Check sizes
if (fileSize > context.config.maxImageSizeMb) {
issues.add(
Issue(
title: 'Oversized Asset',
description:
'Asset "$relPath" is ${fileSize.toStringAsFixed(2)}MB (Limit: ${context.config.maxImageSizeMb}MB).',
category: 'Assets',
severity: IssueSeverity.high,
file: relPath,
suggestion: 'Compress the asset or use a vector format (SVG).',
),
);
}
}
context.logger.stopSpinner();
return ScannerResult(
issues: issues,
metrics: {
'total_assets': totalAssets,
'total_assets_size_mb': totalSizeMb,
},
);
}