findUnusedPackages function

Future<List<String>> findUnusedPackages(
  1. String projectPath
)

Finds unused packages in the 'dependencies' section of pubspec.yaml.

Analyzes Dart files in the project to determine which packages are actually imported and used.

Implementation

Future<List<String>> findUnusedPackages(String projectPath) async {
  final pubspecPath = p.join(projectPath, 'pubspec.yaml');
  final pubspecFile = File(pubspecPath);
  if (!pubspecFile.existsSync()) {
    throw FileSystemException('pubspec.yaml not found', pubspecPath);
  }

  final pubspecContent = await pubspecFile.readAsString();
  final pubspecYaml = loadYaml(pubspecContent);

  // Get the list of declared dependencies
  final declaredDependencies = <String>{};
  final dependencies = pubspecYaml['dependencies'];
  if (dependencies is YamlMap) {
    declaredDependencies.addAll(dependencies.keys.cast<String>());
  }

  if (declaredDependencies.isEmpty) {
    print('No dependencies found in pubspec.yaml.');
    return [];
  }

  // Find all Dart files in the project (lib, bin, test, etc.)
  final dartFilePaths = <String>[];
  final directoriesToScan = ['lib', 'bin', 'test', 'example', 'web']
      .map((dir) => p.join(projectPath, dir))
      .where((dir) => Directory(dir).existsSync());

  for (final dirPath in directoriesToScan) {
    final directory = Directory(dirPath);
    await for (final entity in directory.list(recursive: true)) {
      if (entity is File && entity.path.endsWith('.dart')) {
        dartFilePaths.add(entity.path);
      }
    }
  }

  if (dartFilePaths.isEmpty) {
    print('No Dart files found in the project directories to analyze.');
    return declaredDependencies.toList(); // Assume all are unused if no code
  }

  // Convert relative paths to absolute and normalized paths for the analyzer
  final absoluteDartFilePaths = dartFilePaths.map((path) => p.normalize(p.absolute(path))).toList();


  // Analyze Dart files to find used imports
  // Use the absolute and normalized paths for the AnalysisContextCollection
  final collection = AnalysisContextCollection(includedPaths: absoluteDartFilePaths);

  // Initialize usedPackages here within the function scope
  final usedPackages = <String>{};

  for (final context in collection.contexts) {
    for (final filePath in context.contextRoot.includedPaths) {
      final result = await context.currentSession.getResolvedUnit(filePath);
      if (result is ResolvedUnitResult && result.errors.isEmpty) {
        // Traverse the AST to find import directives
        result.unit.directives.forEach((directive) {
          if (directive is ImportDirective) {
            final uri = directive.uri.stringValue;
            if (uri != null && uri.startsWith('package:')) {
              // Extract package name from 'package:package_name/...'
              final parts = uri.substring('package:'.length).split('/');
              if (parts.isNotEmpty) {
                usedPackages.add(parts.first);
              }
            }
          }
        });
      }
    }
  }

  // Find which declared dependencies are NOT in the used imports
  final unusedPackages = declaredDependencies.difference(usedPackages).toList();

  // Filter out 'flutter' if it's a Flutter project, as it's implicitly used
  // This is a heuristic, a more robust check might be needed for edge cases.
  if (pubspecYaml['dependencies'] is YamlMap && pubspecYaml['dependencies']['flutter'] != null) {
      unusedPackages.remove('flutter');
  }


  return unusedPackages;
}