-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
31 changed files
with
835 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
cyclic_dependency_checks/lib/cycle_detection/cycle_detector.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import 'dart:io'; | ||
|
||
import 'imported_dependency.dart'; | ||
import 'module_dependency.dart'; | ||
import 'module_dependency_graph.dart'; | ||
|
||
class CycleDetector { | ||
Future<List<List<ModuleDependency>>> detect(String packagePath, {int? maxDepth}) async { | ||
final pubspecFile = File('$packagePath/pubspec.yaml'); | ||
final pubspecContent = await pubspecFile.readAsLines(); | ||
final appPackage = pubspecContent.firstOrNull?.replaceFirst('name: ', ''); | ||
final libPath = '$packagePath/lib'; | ||
|
||
if (appPackage == null) { | ||
throw Exception('Could not read appPackage from file ${pubspecFile.absolute.path}'); | ||
} | ||
|
||
final graph = ModuleDependencyGraph(appPackage: appPackage); | ||
|
||
await for (final entity in Directory(libPath).list(recursive: true)) { | ||
final entityPath = entity.path; | ||
|
||
if (!entityPath.endsWith('.dart')) { | ||
continue; | ||
} | ||
|
||
final content = await File(entityPath).readAsString(); | ||
final relativePath = entityPath.replaceFirst(libPath, ''); | ||
final source = SourceFile(relativePath); | ||
|
||
final importResults = content | ||
.split('\n') | ||
.where((line) => line.startsWith('import ')) | ||
.map((line) => ImportedDependency.tryCreate(appPackage, source, line)); | ||
|
||
for (var res in importResults) { | ||
if (res is DisallowedNestedImport) { | ||
throw Exception('Found a disallowed dependency, from $entityPath to ${res.import}'); | ||
} | ||
} | ||
|
||
final validImports = importResults.expand((res) { | ||
if (res is ValidImport) { | ||
return [res.dependency]; | ||
} else { | ||
return <ImportedDependency>[]; | ||
} | ||
}).toList(); | ||
|
||
graph.addAll(validImports); | ||
} | ||
|
||
return graph.detectCycles(maxDepth: maxDepth); | ||
} | ||
} |
115 changes: 115 additions & 0 deletions
115
cyclic_dependency_checks/lib/cycle_detection/imported_dependency.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import 'module_dependency.dart'; | ||
|
||
class ImportedDependency { | ||
final String appPackage; | ||
final SourceFile source; | ||
final String import; | ||
final Module sourceModule; | ||
final Module importedModule; | ||
|
||
ImportedDependency._( | ||
this.appPackage, | ||
this.source, | ||
this.import, | ||
this.sourceModule, | ||
this.importedModule, | ||
); | ||
|
||
@override | ||
String toString() => 'src: ${source.path}, import: $import'; | ||
|
||
static DependencyCreationResult tryCreate( | ||
String appPackage, SourceFile source, String importLine) { | ||
final import = importLine.split("'")[1]; | ||
final isStandardLibrary = import.startsWith('dart:'); | ||
final isPackageDependency = import.startsWith('package:'); | ||
final isAppPackageDependency = import.startsWith('package:$appPackage'); | ||
|
||
if (isStandardLibrary) { | ||
return IgnoredStandardLibrary(import); | ||
} | ||
|
||
if (isPackageDependency && !isAppPackageDependency) { | ||
return IgnoredExternalPackage(import); | ||
} | ||
|
||
final isRelative = !isAppPackageDependency; | ||
final isNested = import.contains('/'); | ||
|
||
if (isRelative && isNested) { | ||
return DisallowedNestedImport(import); | ||
} | ||
|
||
if (isRelative) { | ||
return IgnoredInnerImport(import); | ||
} | ||
|
||
final sourceModule = _sourceModule(appPackage, source); | ||
final importedModule = _importedModule(appPackage, import, sourceModule); | ||
|
||
if (importedModule == sourceModule) { | ||
return IgnoredInnerImport(import); | ||
} | ||
|
||
return ValidImport(ImportedDependency._( | ||
appPackage, | ||
source, | ||
import, | ||
sourceModule, | ||
importedModule, | ||
)); | ||
} | ||
|
||
static Module _sourceModule(String appPackage, SourceFile source) { | ||
final components = source.path.split('/') | ||
..removeLast() | ||
..removeAt(0) | ||
..insert(0, appPackage); | ||
final path = components.join('/'); | ||
|
||
return Module(path); | ||
} | ||
|
||
static Module _importedModule(String appPackage, String import, Module sourceModule) { | ||
final components = import.replaceFirst('package:', '').split('/')..removeLast(); | ||
return Module(components.join('/')); | ||
} | ||
} | ||
|
||
class SourceFile { | ||
final String path; | ||
|
||
SourceFile(this.path); | ||
} | ||
|
||
sealed class DependencyCreationResult {} | ||
|
||
class ValidImport implements DependencyCreationResult { | ||
final ImportedDependency dependency; | ||
|
||
ValidImport(this.dependency); | ||
} | ||
|
||
class IgnoredStandardLibrary implements DependencyCreationResult { | ||
final String import; | ||
|
||
IgnoredStandardLibrary(this.import); | ||
} | ||
|
||
class IgnoredExternalPackage implements DependencyCreationResult { | ||
final String import; | ||
|
||
IgnoredExternalPackage(this.import); | ||
} | ||
|
||
class DisallowedNestedImport implements DependencyCreationResult { | ||
final String import; | ||
|
||
DisallowedNestedImport(this.import); | ||
} | ||
|
||
class IgnoredInnerImport implements DependencyCreationResult { | ||
final String import; | ||
|
||
IgnoredInnerImport(this.import); | ||
} |
9 changes: 9 additions & 0 deletions
9
cyclic_dependency_checks/lib/cycle_detection/list_extensions.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
extension Flatten<T> on List<List<T>> { | ||
List<T> flatten() { | ||
final result = <T>[]; | ||
for (final list in this) { | ||
result.addAll(list); | ||
} | ||
return result; | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
cyclic_dependency_checks/lib/cycle_detection/module_dependency.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
class Module { | ||
final String path; | ||
|
||
Module(this.path); | ||
|
||
@override | ||
bool operator ==(Object other) => | ||
identical(this, other) || | ||
other is Module && runtimeType == other.runtimeType && path == other.path; | ||
|
||
@override | ||
int get hashCode => path.hashCode; | ||
|
||
@override | ||
String toString() => 'module:$path'; | ||
} | ||
|
||
class ModuleDependency { | ||
final Module from; | ||
final Module to; | ||
|
||
ModuleDependency({required this.from, required this.to}); | ||
|
||
@override | ||
bool operator ==(Object other) => | ||
identical(this, other) || | ||
other is ModuleDependency && | ||
runtimeType == other.runtimeType && | ||
from == other.from && | ||
to == other.to; | ||
|
||
@override | ||
int get hashCode => from.hashCode ^ to.hashCode; | ||
|
||
@override | ||
String toString() => '$from -> $to'; | ||
} |
75 changes: 75 additions & 0 deletions
75
cyclic_dependency_checks/lib/cycle_detection/module_dependency_graph.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import 'dart:isolate'; | ||
|
||
import 'package:cyclic_dependency_checks/cycle_detection/list_extensions.dart'; | ||
|
||
import 'imported_dependency.dart'; | ||
import 'module_dependency.dart'; | ||
|
||
class ModuleDependencyGraph { | ||
final String appPackage; | ||
final List<ImportedDependency> _dependencies = []; | ||
final Set<ModuleDependency> _moduleDependencies = {}; | ||
|
||
ModuleDependencyGraph({required this.appPackage}); | ||
|
||
void addAll(List<ImportedDependency> dependencies) { | ||
_dependencies.addAll(dependencies); | ||
_moduleDependencies.addAll(dependencies.map( | ||
(d) => ModuleDependency(from: d.sourceModule, to: d.importedModule), | ||
)); | ||
} | ||
|
||
Future<List<List<ModuleDependency>>> detectCycles({int? maxDepth}) async { | ||
final moduleCount = _countModules(); | ||
final actualMaxDepth = maxDepth ?? moduleCount; | ||
|
||
final resultsFutures = _moduleDependencies.map((value) => Isolate.run( | ||
() => _detectDependencyCycles([value], maxDepth: actualMaxDepth), | ||
)); | ||
|
||
return (await Future.wait(resultsFutures)).flatten(); | ||
} | ||
|
||
int _countModules() { | ||
final allModules = <Module>{}; | ||
for (var value in _moduleDependencies) { | ||
allModules.addAll([value.from, value.to]); | ||
} | ||
return allModules.length; | ||
} | ||
|
||
Future<List<List<ModuleDependency>>> _detectDependencyCycles( | ||
List<ModuleDependency> currentPath, { | ||
required int maxDepth, | ||
}) async { | ||
if (currentPath.hasCycle()) { | ||
return [currentPath]; | ||
} | ||
|
||
if (currentPath.length - 1 > maxDepth) { | ||
return []; | ||
} | ||
|
||
final last = currentPath.last; | ||
final nextOptions = _moduleDependencies.where((dep) => dep.from == last.to); | ||
|
||
final resultsFutures = nextOptions.map((next) => _detectDependencyCycles( | ||
List.from(currentPath)..add(next), | ||
maxDepth: maxDepth, | ||
)); | ||
|
||
return (await Future.wait(resultsFutures)).flatten(); | ||
} | ||
|
||
@override | ||
String toString() => _moduleDependencies.map((d) => d.toString()).join('\n'); | ||
} | ||
|
||
extension CycleDetection on List<ModuleDependency> { | ||
List<Module> path() => map((dep) => dep.from).toList()..add(last.to); | ||
|
||
bool hasCycle() { | ||
final allModules = path(); | ||
return allModules.length > allModules.toSet().length; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import 'dart:async'; | ||
import 'dart:io'; | ||
|
||
import 'cycle_detection/cycle_detector.dart'; | ||
import 'cycle_detection/module_dependency.dart'; | ||
import 'cycle_detection/module_dependency_graph.dart'; | ||
|
||
void main(List<String> args) async { | ||
switch (args) { | ||
case [final path]: | ||
await _run(path); | ||
default: | ||
throw Exception( | ||
'Expected exactly only one argument, ' | ||
'the path to dart package folder as argument', | ||
); | ||
} | ||
} | ||
|
||
Future _run(String path) async { | ||
final stopwatch = Stopwatch()..start(); | ||
final cycles = await CycleDetector().detect(path); | ||
stopwatch.stop(); | ||
|
||
final formattedTime = stopwatch.elapsed.toString().substring(0, 11); | ||
|
||
if (cycles.isNotEmpty) { | ||
stderr.writeln('Detected cycles after ${formattedTime}'); | ||
for (var cycle in cycles) { | ||
cycle.printError(); | ||
} | ||
exit(1); | ||
} | ||
|
||
stdout.writeln('No import cycles were detected after ${formattedTime}'); | ||
} | ||
|
||
extension ErrorPrinting on List<ModuleDependency> { | ||
void printError() { | ||
stderr.writeln(path().join(' -> ')); | ||
} | ||
} |
Oops, something went wrong.