以下のような構成で複数のサブモジュール構成からなる 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;
}