Last active
August 18, 2025 10:07
-
-
Save kibotu/b13b56ee9c5c35fbcc36a00f8c0467a3 to your computer and use it in GitHub Desktop.
Check if there is a @Inject constructor without a provide function being used.
This file contains hidden or 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
#!/usr/bin/env python3 | |
import os | |
import re | |
import sys | |
from pathlib import Path | |
from typing import Set, List, Tuple | |
from dataclasses import dataclass | |
@dataclass | |
class InjectClass: | |
name: str | |
full_path: str | |
package: str | |
file_path: str | |
is_viewmodel: bool = False | |
@dataclass | |
class ProvideMethod: | |
return_type: str | |
method_name: str | |
file_path: str | |
is_binds: bool = False | |
class DaggerConsistencyChecker: | |
def __init__(self, module_path: str): | |
self.module_path = Path(module_path) | |
self.inject_classes: List[InjectClass] = [] | |
self.provide_methods: List[ProvideMethod] = [] | |
self.viewmodel_binds: Set[str] = set() | |
# ---------- Scanners ---------- | |
def find_inject_constructors(self) -> None: | |
"""Find all classes with @Inject primary constructors in the module.""" | |
kotlin_files = self.module_path.rglob("*.kt") | |
# Covers: class, data class, abstract class, etc., and modifiers; matches "@Inject constructor" | |
inject_pat = re.compile( | |
r'^\s*(?:public|private|protected|internal)?\s*' | |
r'(?:(?:data|sealed|abstract|open|final)\s+)?' | |
r'class\s+(\w+)[^{\n]*?@Inject\s+constructor', | |
re.MULTILINE | re.DOTALL | |
) | |
for file_path in kotlin_files: | |
try: | |
with open(file_path, encoding="utf-8") as f: | |
content = f.read() | |
pkg_match = re.search(r'^\s*package\s+([\w.]+)', content, re.MULTILINE) | |
package = pkg_match.group(1) if pkg_match else "" | |
for m in inject_pat.finditer(content): | |
cls = m.group(1) | |
full = f"{package}.{cls}" if package else cls | |
# Heuristic ViewModel detection: | |
# - Extends/implements something ending with ViewModel | |
# - class header contains ": ...ViewModel(" or ": ...ViewModel" | |
is_vm = bool(re.search( | |
rf'class\s+{re.escape(cls)}[^\n{{:]*:\s*[A-Za-z0-9_\.]*ViewModel\b', | |
content | |
)) | |
self.inject_classes.append( | |
InjectClass( | |
name=cls, | |
full_path=full, | |
package=package, | |
file_path=str(file_path), | |
is_viewmodel=is_vm | |
) | |
) | |
except Exception as e: | |
print(f"Warning: Could not read {file_path}: {e}") | |
def find_provide_methods(self) -> None: | |
"""Scan the whole module for @Provides/@Binds methods (no hard-coded di/ path).""" | |
kotlin_files = self.module_path.rglob("*.kt") | |
# @Provides with explicit return type | |
provides_explicit = re.compile( | |
r'@Provides[\s\S]*?fun\s+(\w+)\s*\([^)]*\)\s*:\s*([<>\w\.\?\s,]+)', | |
re.MULTILINE | |
) | |
# @Provides with inferred return via "= TypeName(" | |
provides_inferred = re.compile( | |
r'@Provides[\s\S]*?fun\s+(\w+)\s*\([^)]*\)\s*=\s*([A-Za-z_][\w\.]*)\s*\(', | |
re.MULTILINE | |
) | |
# @Binds ViewModel with @ViewModelKey(X::class) | |
binds_vm = re.compile( | |
r'@Binds[\s\S]*?@ViewModelKey\(\s*([A-Za-z_][\w\.]*)::class\s*\)[\s\S]*?' | |
r'fun\s+(\w+)\s*\([^:]*:\s*([A-Za-z_][\w\.]*)\s*\)\s*:\s*ViewModel', | |
re.MULTILINE | |
) | |
# Generic @Binds: fun bind(impl: Impl): Api | |
binds_generic = re.compile( | |
r'@Binds[\s\S]*?fun\s+(\w+)\s*\([^:]*:\s*([A-Za-z_][\w\.]*)\s*\)\s*:\s*([A-Za-z_][\w\.\<\>,\s]*)', | |
re.MULTILINE | |
) | |
for file_path in kotlin_files: | |
try: | |
with open(file_path, encoding="utf-8") as f: | |
content = f.read() | |
# Quick skip if file doesn't look like a module/provider/binds file | |
if not any(tok in content for tok in ("@Provides", "@Binds", "@Module")): | |
continue | |
for m in provides_explicit.finditer(content): | |
method_name, return_type = m.groups() | |
self.provide_methods.append( | |
ProvideMethod(return_type=return_type.strip(), | |
method_name=method_name, | |
file_path=str(file_path), | |
is_binds=False) | |
) | |
for m in provides_inferred.finditer(content): | |
method_name, ctor_type = m.groups() | |
self.provide_methods.append( | |
ProvideMethod(return_type=ctor_type.split(".")[-1], | |
method_name=method_name, | |
file_path=str(file_path), | |
is_binds=False) | |
) | |
for m in binds_vm.finditer(content): | |
vm_key, method_name, param_type = m.groups() | |
# Prefer the key (usually the VM class); fall back to param type | |
vm = (vm_key.split(".")[-1]) if vm_key else (param_type.split(".")[-1]) | |
self.viewmodel_binds.add(vm) | |
self.provide_methods.append( | |
ProvideMethod(return_type=vm, | |
method_name=method_name, | |
file_path=str(file_path), | |
is_binds=True) | |
) | |
for m in binds_generic.finditer(content): | |
method_name, impl_type, _api_type = m.groups() | |
self.provide_methods.append( | |
ProvideMethod(return_type=impl_type.split(".")[-1], | |
method_name=method_name, | |
file_path=str(file_path), | |
is_binds=True) | |
) | |
except Exception as e: | |
print(f"Warning: Could not read {file_path}: {e}") | |
# ---------- Logic ---------- | |
@staticmethod | |
def _simple_name(t: str) -> str: | |
"""Extract simple class name from qualified/generic type.""" | |
# Strip generics: Foo<Bar.Baz, Qux> | |
t = re.sub(r'<[^>]*>', '', t) | |
# Last segment of qualified name | |
return t.split('.')[-1].strip() | |
def check_consistency(self) -> Tuple[List[InjectClass], List[InjectClass]]: | |
missing: List[InjectClass] = [] | |
covered: List[InjectClass] = [] | |
provided_names = set() | |
for pm in self.provide_methods: | |
provided_names.add(self._simple_name(pm.return_type)) | |
provided_names.update(self.viewmodel_binds) | |
for ic in self.inject_classes: | |
if ic.name in provided_names: | |
covered.append(ic) | |
else: | |
missing.append(ic) | |
return missing, covered | |
def generate_missing_provides(self, missing: List[InjectClass]) -> str: | |
out = [] | |
for ic in missing: | |
if ic.is_viewmodel: | |
out.append( | |
f"// Missing provider for {ic.full_path}\n" | |
f"@Binds\n@IntoMap\n@ViewModelKey({ic.name}::class)\n" | |
f"abstract fun bind{ic.name}(vm: {ic.name}): ViewModel" | |
) | |
else: | |
out.append( | |
f"// Missing provider for {ic.full_path}\n" | |
f"@Provides\nfun provide{ic.name}(): {ic.name} = {ic.name}()" | |
) | |
return "\n\n".join(out) | |
def print_report(self) -> None: | |
missing, covered = self.check_consistency() | |
print("=" * 80) | |
print("ANDROID DAGGER DI CONSISTENCY REPORT") | |
print("=" * 80) | |
print(f"Module Path: {self.module_path}") | |
print(f"Total @Inject constructors found: {len(self.inject_classes)}") | |
print(f"Total @Provides/@Binds methods found: {len(self.provide_methods)}\n") | |
if missing: | |
print("β MISSING @PROVIDES METHODS:") | |
print("=" * 40) | |
for ic in missing: | |
status = "ViewModel" if ic.is_viewmodel else "Class" | |
print(f" [{status}] {ic.name}") | |
print(f" Path: {ic.full_path}") | |
print(f" File: {os.path.relpath(ic.file_path)}\n") | |
print("π‘ SUGGESTED FIXES:") | |
print("=" * 40) | |
print(self.generate_missing_provides(missing)) | |
print() | |
else: | |
print("β All @Inject constructors have corresponding @Provides/@Binds methods!\n") | |
print("β PROPERLY PROVIDED:") | |
print("=" * 40) | |
for ic in covered: | |
status = "ViewModel" if ic.is_viewmodel else "Class" | |
print(f" [{status}] {ic.name}") | |
if not covered: | |
print(" (none found)") | |
print() | |
print("π SUMMARY:") | |
print("=" * 40) | |
print(f" Missing providers: {len(missing)}") | |
print(f" Properly provided: {len(covered)}") | |
total = len(self.inject_classes) | |
if total == 0: | |
print(" Success rate: n/a (no @Inject constructors found)") | |
else: | |
pct = (len(covered) / total) * 100 | |
print(f" Success rate: {len(covered)}/{total} ({pct:.1f}%)") | |
if not self.inject_classes: | |
print("\nβΉοΈ No @Inject constructors found. " | |
"Verify the module path or whether the module uses constructor injection.") | |
elif missing: | |
print("\nπ§ ACTION REQUIRED:") | |
print(" Add the suggested @Provides or @Binds methods to appropriate DI modules.") | |
def main(): | |
if len(sys.argv) > 1: | |
module_path = sys.argv[1] | |
else: | |
module_path = "profis_events/src/main/java" | |
if not os.path.exists(module_path): | |
print(f"Error: Module path '{module_path}' does not exist") | |
print(f"Usage: {sys.argv[0]} [module_path]") | |
print(f"Example: {sys.argv[0]} profis_events/src/main/java") | |
sys.exit(1) | |
print(f"π Checking Dagger consistency for module: {module_path}\n") | |
checker = DaggerConsistencyChecker(module_path) | |
checker.find_inject_constructors() | |
checker.find_provide_methods() | |
checker.print_report() | |
if __name__ == "__main__": | |
main() |
Author
kibotu
commented
Aug 18, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment