Skip to content

Instantly share code, notes, and snippets.

@kibotu
Last active August 18, 2025 10:07
Show Gist options
  • Save kibotu/b13b56ee9c5c35fbcc36a00f8c0467a3 to your computer and use it in GitHub Desktop.
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.
#!/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()
@kibotu
Copy link
Author

kibotu commented Aug 18, 2025

#!/bin/bash
# Quick script to check DI consistency
python3 check_dagger_consistency.py ${1:-core/src/main/java}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment