Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
## 2.3.0

- **External Package Dependencies Auditing Engine (`--packages` / `--audit-packages`):**
- Added full detection of unused runtime (`dependencies:`) and development (`dev_dependencies:`) packages declared in `pubspec.yaml` but never imported or referenced in Dart source code.
- Isolated per-package scoping: each `pubspec.yaml` across monorepos and nested sub-packages is evaluated strictly against its own package code scope, preventing false negatives where an import in a sub-package excuses an unused dependency in the root.
- Zero false-positives guard: protects core SDK frameworks (`flutter`, `flutter_test`, `flutter_localizations`), CLI tools, linters (`lints`, `flutter_lints`, `custom_lint`), code generators and builders (`build_runner`, `json_serializable`, `freezed`, etc.), and `build.yaml` configurations.
- **Duplicate Dependency Detection:**
- Automatically identifies duplicate package declarations within the same `pubspec.yaml` (duplicate entries under `dependencies:` or duplicated across both `dependencies:` and `dev_dependencies:`).
- **Safe Surgical Dependency Pruning (`--clean-packages`):**
- Dedicated dependency cleaner with automatic pre-modification backup snapshot to `.detect_unused/backups/<id>/files/`.
- Surgically removes both single-line (`pkg: ^1.0.0`) and multi-line (`git:`, `path:`, `hosted:`) dependency blocks from targeted `pubspec.yaml` files.
- Preserves surrounding indentation, spacing, and comments.
- Full instant rollback support via `detect_unused --restore <id>`.
- Integrated into `--clean-all` as Step 9.
- **Automated Test Suite:**
- Added comprehensive automated unit and integration tests under `test/dependency_auditor_test.dart` covering multi-pubspec scoping, duplicate detection, and surgical pruning.

## 2.2.0

- **Assets, Images & Fonts Auditing Engine (`--assets` / `--audit-assets`):**
Expand Down
47 changes: 33 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![License: MIT](https://img.shields.io/badge/license-MIT-purple.svg)](LICENSE)
[![GitHub Stars](https://img.shields.io/github/stars/mdalhagg/detect_unused_code?style=flat&color=1877F2)](https://github.com/mdalhagg/detect_unused_code/stargazers)

A fast, zero-dependency Dart and Flutter dead code detector and automated remediation engine. Safely detects and cleans dead classes, unused widgets, commented-out files, dead code blocks, unused imports, unused variables, and pending TODO tasks across Dart and Flutter projects.
A fast, zero-dependency Dart and Flutter dead code detector and automated remediation engine. Safely detects and cleans dead classes, unused widgets, commented-out files, dead code blocks, unused imports, unused variables, pending TODO tasks, unreferenced media assets, and unused/duplicate external packages across Dart and Flutter projects and monorepos.

Works with zero external dependencies, runs on any Dart SDK from `2.17.0` up to Dart `3.x` / `4.x`, and supports both CLI usage and programmatic Dart API.

Expand Down Expand Up @@ -129,7 +129,7 @@ dart pub global activate detect_unused_code
### As a Dev Dependency (in `pubspec.yaml`)
```yaml
dev_dependencies:
detect_unused_code: ^2.1.2
detect_unused_code: ^2.3.0
```

---
Expand Down Expand Up @@ -197,9 +197,9 @@ detect_unused -v

---

## 🧹 Automated Cleanup & Remediation Engine (v2.0+)
## 🧹 Automated Cleanup & Remediation Engine (v2.0+ / v2.2+ / v2.3+)

Versions `2.0+` and `2.1+` introduce a defensive code remediation engine. It automatically resolves detected issues across a phased 7-stage pipeline while maintaining snapshot backups and verified compilation health:
Versions `2.0+`, `2.2+`, and `2.3+` introduce a defensive code remediation engine. It automatically resolves detected issues across a phased 9-stage pipeline while maintaining snapshot backups and verified compilation health:

```bash
# 1. Remediate ALL detected safe issues with automatic snapshot backup & confirmation
Expand All @@ -216,6 +216,8 @@ detect_unused --privatize-internal # Converts file-internal public classes
detect_unused --clean-todos # Strips and cleans pending // TODO lines
detect_unused --clean-unused-imports # Removes unused and unnecessary imports
detect_unused --clean-dead-code # Replaces dead null-aware ?. with . and cleans dead code
detect_unused --clean-assets # Deletes unused media assets and unregisters from pubspec.yaml
detect_unused --clean-packages # Removes unused and duplicate external dependencies from pubspec.yaml

# 4. Skip confirmation prompt for CI/CD or automated scripts
detect_unused --clean-all -y
Expand All @@ -240,14 +242,15 @@ detect_unused --version
```

### 🛡️ Defensive Safety Architecture
1. **Multi-Stage Phased Pipeline**: `--clean-all` runs in 7 discrete stages (Commented Files → Dead Classes → Dead Blocks → Internal Classes Privatization → TODOs → Unused Imports → Dead Code), re-auditing AST and line offsets between stages to maintain surgical precision.
2. **Automatic Snapshot Backups**: Before any files are touched, an isolated copy is saved to `.detect_unused/backups/`.
3. **Instant Rollback**: If you ever want to revert, run `detect_unused --restore <id>` or `detect_unused --restore-latest` to instantly recover files.
4. **Auto-Clean Snapshot on Restore**: The restored snapshot folder is automatically removed upon successful recovery to reclaim disk space (override with `--keep-backup`).
5. **Smart Dart Extension Awareness**: Scans declared getters, setters, methods, and static fields in extensions to prevent false-positive flags when extension members are used across files.
6. **Constructor Key Cleanup**: Strips redundant `super.key` and `Key? key` parameters when privatizing internal widget classes to prevent `unused_element_parameter` compiler warnings.
7. **Automatic `.gitignore` Protection**: `.detect_unused/` is automatically appended to your `.gitignore`.
8. **Post-Cleanup Health Verification**: The tool automatically verifies `dart analyze` after cleaning. If compiler errors are detected, it prompts to auto-rollback immediately.
1. **Multi-Stage Phased Pipeline**: `--clean-all` runs in 9 discrete stages (Commented Files → Dead Classes → Dead Blocks → Internal Classes Privatization → TODOs → Unused Imports → Dead Code → Unused Assets → Unused/Duplicate Packages), re-auditing AST and line offsets between stages to maintain surgical precision.
2. **Multi-Pubspec & Monorepo Scoping**: Automatically identifies all `pubspec.yaml` files across root, submodules, and nested packages, scoping imports and assets strictly to each package without leaking dependencies.
3. **Automatic Snapshot Backups**: Before any files or `pubspec.yaml` declarations are touched, an isolated copy is saved to `.detect_unused/backups/`.
4. **Instant Rollback**: If you ever want to revert, run `detect_unused --restore <id>` or `detect_unused --restore-latest` to instantly recover files.
5. **Auto-Clean Snapshot on Restore**: The restored snapshot folder is automatically removed upon successful recovery to reclaim disk space (override with `--keep-backup`).
6. **Smart Dart Extension Awareness**: Scans declared getters, setters, methods, and static fields in extensions to prevent false-positive flags when extension members are used across files.
7. **Constructor Key Cleanup**: Strips redundant `super.key` and `Key? key` parameters when privatizing internal widget classes to prevent `unused_element_parameter` compiler warnings.
8. **Automatic `.gitignore` Protection**: `.detect_unused/` is automatically appended to your `.gitignore`.
9. **Post-Cleanup Health Verification**: The tool automatically verifies `dart analyze` after cleaning. If compiler errors are detected, it prompts to auto-rollback immediately.

---

Expand All @@ -256,9 +259,11 @@ detect_unused --version
| Flag | Short | Default | Description |
| :--- | :--- | :--- | :--- |
| `--path=<dir>` / `--url=<dir>` | - | `.` | Target directory or file to audit |
| `--packages` | `--audit-packages` | `false` | Audit unused & duplicate packages across all pubspec.yaml files |
| `--clean-packages` | - | `false` | Safely delete unused & duplicate dependencies from pubspec.yaml |
| `--assets` | `--audit-assets` | `false` | Audit unused assets, images, and fonts across all packages |
| `--clean-assets` | - | `false` | Safely delete unused media assets and prune explicit pubspec lines |
| `--clean-all` | `--clean` | `false` | Remediate all safe issues (including assets) with auto-backup & prompt |
| `--clean-all` | `--clean` | `false` | Remediate all safe issues (including assets & packages) with auto-backup & prompt |
| `--clean-commented-files` | - | `false` | Delete 100% commented-out files |
| `--clean-dead-blocks` | - | `false` | Remove dead commented code blocks |
| `--clean-dead-classes` | - | `false` | Remove zero-usage dead classes and widgets |
Expand Down Expand Up @@ -386,16 +391,24 @@ The CLI returns exit code `1` when dead classes, fully commented files, or unuse
# أو تحديد اسم مخصص للملف:
detect_unused --json=report.json
```
- **فحص وتدقيق المكاتب والتبعيات الخارجية المهملة والمكررة في ملفات pubspec.yaml:**
```bash
detect_unused --packages
```
- **فحص وتدقيق الموارد والوسائط والخطوط المهملة (Assets & Fonts):**
```bash
detect_unused --assets
```

### 🧹 أوامر التنظيف التلقائي والنسخ الاحتياطي (إصدارات 2.0+ و 2.1+ و 2.2+):
### 🧹 أوامر التنظيف التلقائي والنسخ الاحتياطي (إصدارات 2.0+ و 2.2+ و 2.3+):
- **تنظيف آمن وشامل لجميع المشاكل المكتشفة مع إنشاء نسخة احتياطية تلقائية:**
```bash
detect_unused --clean-all
```
- **تنظيف التبعيات والمكاتب المهملة والمكررة من ملفات pubspec.yaml:**
```bash
detect_unused --clean-packages
```
- **تنظيف الموارد والصور والخطوط المهملة واستعادة مساحة التخزين:**
```bash
detect_unused --clean-assets
Expand Down Expand Up @@ -426,6 +439,12 @@ The CLI returns exit code `1` when dead classes, fully commented files, or unuse

# تنظيف الأكواد الميتة والتعبيرات غير المجدية (مثل ?. الزائدة):
detect_unused --clean-dead-code

# تنظيف وحذف الموارد والصور المهملة من ملفات pubspec.yaml والقرص:
detect_unused --clean-assets

# تنظيف وحذف التبعيات والمكاتب المهملة والمكررة من ملفات pubspec.yaml:
detect_unused --clean-packages
```
- **إدارة النسخ الاحتياطية والاسترجاع الفوري:**
```bash
Expand Down
11 changes: 11 additions & 0 deletions lib/detect_unused_code.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,15 @@ export 'src/assets/asset_models.dart'
export 'src/assets/pubspec_scanner.dart' show PubspecScanner;
export 'src/assets/asset_auditor.dart' show AssetAuditor, AssetProgressCallback;
export 'src/cleaners/asset_cleaner.dart' show AssetCleaner, CleanAssetsResult;
export 'src/packages/package_dep_models.dart'
show
DependencyScope,
DependencyIssueType,
DeclaredDependency,
UnusedDependencyItem,
PackageDependenciesReport;
export 'src/packages/dep_auditor.dart'
show DependencyAuditor, DepProgressCallback;
export 'src/cleaners/dep_cleaner.dart'
show DependencyCleaner, CleanDependenciesResult;
export 'src/version.dart' show packageVersion;
151 changes: 151 additions & 0 deletions lib/src/cleaners/dep_cleaner.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import 'dart:io';

import '../backup/backup_manager.dart';
import '../backup/backup_manifest.dart';
import '../packages/package_dep_models.dart';
import '../utils.dart';

/// Result summary of a dependency cleaning operation.
class CleanDependenciesResult {
/// Total count of unused and duplicate dependency declarations removed.
final int removedCount;

/// Number of `pubspec.yaml` files updated.
final int updatedPubspecsCount;

/// List of package names removed.
final List<String> removedPackageNames;

/// The backup manifest created before cleaning, or null if dry-run.
final BackupManifest? backupManifest;

/// Creates a new [CleanDependenciesResult].
CleanDependenciesResult({
required this.removedCount,
required this.updatedPubspecsCount,
required this.removedPackageNames,
this.backupManifest,
});
}

/// Surgical cleaner that removes unused and duplicate dependencies from targeted `pubspec.yaml` files.
class DependencyCleaner {
/// The project or workspace root directory.
final Directory workspaceRoot;

/// The backup manager used for snapshotting files before modification.
final BackupManager backupManager;

/// Creates a new [DependencyCleaner].
DependencyCleaner({
Directory? workspaceRoot,
BackupManager? backupManager,
}) : workspaceRoot = workspaceRoot ?? findProjectOrWorkspaceRoot(),
backupManager = backupManager ??
BackupManager(workspaceRoot ?? findProjectOrWorkspaceRoot());

/// Performs surgical dependency removal on targeted `pubspec.yaml` files.
CleanDependenciesResult clean({
required List<UnusedDependencyItem> issues,
bool dryRun = false,
}) {
if (issues.isEmpty) {
return CleanDependenciesResult(
removedCount: 0,
updatedPubspecsCount: 0,
removedPackageNames: [],
);
}

final removedNames = issues.map((i) => i.packageName).toSet().toList();

// Group issues by target pubspec file
final pubspecGroups = <String, List<UnusedDependencyItem>>{};
final fileMap = <String, File>{};

for (final item in issues) {
final canon = canonicalizePath(item.pubspecFile.path);
pubspecGroups.putIfAbsent(canon, () => []).add(item);
fileMap[canon] = item.pubspecFile;
}

if (dryRun) {
return CleanDependenciesResult(
removedCount: issues.length,
updatedPubspecsCount: pubspecGroups.length,
removedPackageNames: removedNames,
);
}

// 1. Create backup snapshot of all pubspec.yaml files targeted for modification
final filesToBackup = fileMap.values.where((f) => f.existsSync()).toList();
final manifest = backupManager.createBackup(
action: 'clean-packages',
filesToBackup: filesToBackup,
);

int updatedPubspecs = 0;
int totalRemoved = 0;

// 2. Surgically prune each pubspec.yaml
for (final entry in pubspecGroups.entries) {
final pubFile = fileMap[entry.key]!;
final fileIssues = entry.value;

if (!pubFile.existsSync()) continue;

if (_prunePubspecDependencies(pubFile, fileIssues)) {
updatedPubspecs++;
totalRemoved += fileIssues.length;
}
}

return CleanDependenciesResult(
removedCount: totalRemoved,
updatedPubspecsCount: updatedPubspecs,
removedPackageNames: removedNames,
backupManifest: manifest,
);
}

/// Surgically removes targeted dependency entries from [pubFile].
bool _prunePubspecDependencies(
File pubFile, List<UnusedDependencyItem> issuesToRemove) {
try {
final lines = pubFile.readAsLinesSync();

// Sort items by startLine descending so removing lines doesn't shift earlier line numbers
final sortedItems = List<UnusedDependencyItem>.from(issuesToRemove)
..sort((a, b) => b.startLine.compareTo(a.startLine));

final linesList = List<String>.from(lines);

for (final item in sortedItems) {
final startIdx = item.startLine - 1; // 0-based
final endIdx = item.endLine - 1; // 0-based

if (startIdx >= 0 && endIdx < linesList.length && startIdx <= endIdx) {
// Verify that the line at startIdx indeed references the target package
final targetLine = linesList[startIdx];
final pkgRegex = RegExp('^\\s*${RegExp.escape(item.packageName)}:');

if (pkgRegex.hasMatch(targetLine)) {
// Remove the block of lines from startIdx to endIdx
final countToRemove = endIdx - startIdx + 1;
for (int r = 0; r < countToRemove; r++) {
if (startIdx < linesList.length) {
linesList.removeAt(startIdx);
}
}
}
}
}

// Write updated content back to the file
pubFile.writeAsStringSync('${linesList.join('\n')}\n');
return true;
} catch (_) {
return false;
}
}
}
Loading
Loading