Dart/Flutter の依存関係をリストアップするスクリプト

Dart/Flutter の依存関係をリストアップするスクリプト

以下のような構成で複数のサブモジュール構成からなる Dart/Flutter プロジェクトがあるとします。

my_app
├ my_main/
│ ├ pubspec.yaml
│ └ lib/
│   └ ...
├ my_feature_1/
│ ├ pubspec.yaml
│ └ lib/
│   └ ...
├ my_feature_2/
│ ├ pubspec.yaml
│ └ lib/
│   └ ...
├ my_feature_3/
│ ├ pubspec.yaml
│ └ lib/
│   └ ...
└ my_utils/
  ├ pubspec.yaml
  └ lib/
    ├ ...
    └ util.dart

ここで、my_utils モジュールに依存している他モジュールを特定したいとします。その場合は、各パッケージの pubspec.yaml を見て、my_utils への依存が dependencies に書かれているかを調べればわかります。

では、my_utils/lib/util.dart のコードに依存している他のサブモジュールを特定したい場合はどうでしょうか? my_utils に依存しているサブモジュールの中でも、実際に util.dart のコードを使っているのは一部かもしれません。それは、以下で提示するスクリプトでこれを特定することが可能です。


スクリプト #

使い方

dart run depends.dart . my_sub_module/lib/util.dart

depends.dart

import 'dart:io';

import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:path/path.dart' as path;

/// CLI entry point.
Future<void> main(List<String> args) async {
  if (args.length != 2) {
    stdout.writeln('''
Usage:   dart run depends.dart <repo_root> <target_dart>
Example: dart run depends.dart . my_sub_module/util.dart
''');
    return;
  }
  final rootPath = path.normalize(path.absolute(args[0]));
  final targetPath = path.normalize(path.absolute(args[1]));
  await _main(rootPath, targetPath);
}

// Main logic.
Future<void> _main(String rootPath, String targetPath) async {
  stdout.writeln('Analyzing dependencies in $rootPath for target: $targetPath');

  if (!File(targetPath).existsSync()) {
    throw Exception('Target file does not exist: $targetPath');
  }

  // Build analyzer contexts for the whole repo.
  final collection = AnalysisContextCollection(
    includedPaths: [rootPath],
    resourceProvider: PhysicalResourceProvider.INSTANCE,
  );

  // Resolve the target library (handles part files).
  final context = collection.contextFor(targetPath);
  final targetLibrary = await _libraryFor(context.currentSession, targetPath);
  if (targetLibrary == null) {
    throw Exception('Could not resolve target library: $targetPath');
  }

  // Build reverse dependency graph.
  stdout.writeln('### Building syntactic dependency graph ###');
  final syntacticDependencyGraph =
      await _buildSyntacticDependencyGraph(rootPath, collection);

  // Traverse from target to collect all dependents.
  stdout.writeln('### Collecting syntactic dependents ###');
  final syntacticDependents =
      _collectSyntacticDependents(targetLibrary, syntacticDependencyGraph);

  // Print results (exclude target itself, sorted, root-relative).
  syntacticDependents.map((p) => path.relative(p, from: rootPath)).toList()
    ..sort()
    ..forEach(stdout.writeln);

  stdout.writeln('### Collecting semantic dependents ###');
  final semanticDependents = await _collectSemanticDependents(
    collection,
    targetLibrary,
    syntacticDependents,
  );

  // Print results (exclude target itself, sorted, root-relative).
  semanticDependents.map((p) => path.relative(p, from: rootPath)).toList()
    ..sort()
    ..forEach(stdout.writeln);
}

/// Returns the [LibraryElement] for [filePath].
/// - If [filePath] is a library file, returns that library.
/// - If [filePath] is a part file, returns its parent library.
/// - Otherwise (e.g. not analyzable), returns `null`.
Future<LibraryElement?> _libraryFor(
  AnalysisSession session,
  String filePath,
) async {
  final resolvedLibrary = await session.getResolvedLibrary(filePath);
  if (resolvedLibrary is ResolvedLibraryResult) {
    return resolvedLibrary.element;
  }
  final resolvedUnit = await session.getResolvedUnit(filePath);
  if (resolvedUnit is ResolvedUnitResult) {
    return resolvedUnit.libraryElement;
  }
  return null;
}

/// Build a reverse dependency graph for all libraries under [rootPath]
/// by analyzing import/export directives.
/// - Key: dependency library path
/// - Value: set of library paths that import/export it.
/// - e.g., {dependency -> {dependent1, dependent2, ...}}
Future<Map<String, Set<String>>> _buildSyntacticDependencyGraph(
  String rootPath,
  AnalysisContextCollection collection,
) async {
  final graph = <String, Set<String>>{};

  for (final context in collection.contexts) {
    stdout.writeln('Analyzing Context of: ${context.contextRoot.root.path}');

    final session = context.currentSession;
    final dartFiles =
        context.contextRoot.analyzedFiles().where((f) => f.endsWith('.dart'));

    for (final filePath in dartFiles) {
      final parsedUnit = await session.getParsedUnit(filePath);
      if (parsedUnit is! ParsedUnitResult) {
        continue; // Skip unparsed files.
      }
      if (parsedUnit.isPart) {
        continue; // Skip part files.
      }

      for (final directive
          in parsedUnit.unit.directives.whereType<UriBasedDirective>()) {
        final directiveString = directive.uri.stringValue;
        if (directiveString == null) {
          continue;
        }

        Uri depUri = Uri.parse(directiveString);

        // Resolve relative URIs against the base URI.
        if (depUri.scheme.isEmpty) {
          final baseUri = Uri.file(parsedUnit.path);
          depUri = baseUri.resolveUri(depUri);
        }

        final depPath = session.uriConverter.uriToPath(depUri);
        if (depPath == null) {
          continue; // Skip unresolved URIs.
        }
        if (!path.isWithin(rootPath, depPath)) {
          continue; // Skip dependencies outside the root path.
        }

        (graph[depPath] ??= {}).add(parsedUnit.path);
      }
    }
  }

  return graph;
}

/// Collect syntactic dependents of [targetLibrary] from the graph.
Set<String> _collectSyntacticDependents(
  LibraryElement targetLibrary,
  Map<String, Set<String>> graph,
) {
  final targetLibraryPath = targetLibrary.source.fullName;

  final syntacticDependents = <String>{};
  final stack = <String>[targetLibraryPath];
  final visited = <String>{targetLibraryPath};

  while (stack.isNotEmpty) {
    final dependency = stack.removeLast();
    final dependents = graph[dependency] ?? {};
    for (final d in dependents) {
      if (visited.add(d)) {
        syntacticDependents.add(d);
        stack.add(d);
      }
    }
  }

  // Exclude the target library itself from the result.
  syntacticDependents.remove(targetLibraryPath);
  return syntacticDependents;
}

/// Collect semantic dependents of [targetLibrary] from the syntactic ones.
Future<Set<String>> _collectSemanticDependents(
  AnalysisContextCollection collection,
  LibraryElement targetLibrary,
  Set<String> syntacticDependents,
) async {
  final semanticDependents = <String>{};

  for (final depPath in syntacticDependents) {
    stdout.writeln('Analyzing symbols in: $depPath');

    final context = collection.contextFor(depPath);
    final depLibrary = await context.currentSession.getResolvedLibrary(depPath);
    if (depLibrary is! ResolvedLibraryResult) {
      continue;
    }

    bool found = false;

    for (final unit in depLibrary.units) {
      final stack = <AstNode>[unit.unit];

      while (stack.isNotEmpty && !found) {
        final node = stack.removeLast();

        if (node is SimpleIdentifier) {
          if (node.staticElement?.library == targetLibrary) {
            semanticDependents.add(depPath);
            found = true;
            break;
          }
        }

        for (final child in node.childEntities) {
          if (child is AstNode) {
            stack.add(child);
          }
        }
      }

      if (found) {
        break;
      }
    }
  }

  return semanticDependents;
}