Skip to content

Instantly share code, notes, and snippets.

@EhsanSetayesh
Created November 27, 2024 06:04
Show Gist options
  • Select an option

  • Save EhsanSetayesh/9cd12fc0c58153a471fdeb7104f40862 to your computer and use it in GitHub Desktop.

Select an option

Save EhsanSetayesh/9cd12fc0c58153a471fdeb7104f40862 to your computer and use it in GitHub Desktop.
resource-cleaner
// resource-cleaner.gradle
import groovy.xml.XmlSlurper
class ResourceCleaner {
Project rootProject
Map<String, Set<String>> usedResources = [:]
Map<String, Set<String>> allResources = [:]
Map<String, File> moduleResDirs = [:]
Set<String> allModules = []
Set<String> coreModules = []
Set<String> featureModules = []
Set<String> sharedModules = []
boolean removeResources
ResourceCleaner(Project rootProject, boolean removeResources = false) {
this.rootProject = rootProject
this.removeResources = removeResources
initializeModules()
}
void initializeModules() {
rootProject.subprojects { module ->
def path = module.path
if (isAndroidModule(module)) {
allModules.add(path)
usedResources[path] = new HashSet<>()
allResources[path] = new HashSet<>()
moduleResDirs[path] = new File("${module.projectDir}/src/main/res")
// Categorize module
if (path.startsWith(":core")) {
coreModules.add(path)
} else if (path.startsWith(":feature")) {
featureModules.add(path)
} else if (path.startsWith(":shared")) {
sharedModules.add(path)
}
}
}
println "\nFound ${allModules.size()} Android modules"
}
boolean isAndroidModule(Project module) {
return module.plugins.hasPlugin('com.android.library') ||
module.plugins.hasPlugin('com.android.application')
}
void execute() {
// Process modules in order
processCoreModules()
processSharedModules()
processFeatureModules()
processRemaining()
// Generate reports
if (removeResources) {
removeUnusedResources()
} else {
generateReport()
}
}
void processCoreModules() {
println "\nProcessing core modules..."
coreModules.each { modulePath ->
analyzeModule(modulePath)
}
}
void processSharedModules() {
println "\nProcessing shared modules..."
sharedModules.each { modulePath ->
analyzeModule(modulePath)
}
}
void processFeatureModules() {
println "\nProcessing feature modules..."
featureModules.each { modulePath ->
analyzeModule(modulePath)
}
}
void processRemaining() {
println "\nProcessing remaining modules..."
def remainingModules = allModules - coreModules - featureModules - sharedModules
remainingModules.each { modulePath ->
analyzeModule(modulePath)
}
}
void analyzeModule(String modulePath) {
print "Analyzing $modulePath... "
scanSourceFiles(modulePath)
scanResourceFiles(modulePath)
println "done"
}
void scanSourceFiles(String modulePath) {
def module = rootProject.project(modulePath)
module.fileTree(dir: "${module.projectDir}/src").matching {
include "**/*.java"
include "**/*.kt"
include "**/*.xml"
}.each { File file ->
scanFile(file, modulePath)
}
}
void scanFile(File file, String modulePath) {
file.eachLine { line ->
// Match resource references
[
~/R\.(id|layout|drawable|string|color|dimen|style)\.[a-zA-Z0-9_]+/,
~/(@\+?id\/[a-zA-Z0-9_]+)/,
~/(@layout\/[a-zA-Z0-9_]+)/,
~/(@drawable\/[a-zA-Z0-9_]+)/,
~/(@string\/[a-zA-Z0-9_]+)/
].each { pattern ->
def matcher = line =~ pattern
while (matcher.find()) {
def resource = matcher.group(0)
if (resource.startsWith("R.")) {
usedResources[modulePath].add(resource.split("\\.")[2])
} else if (resource.startsWith("@")) {
usedResources[modulePath].add(resource.split("/")[1])
}
}
}
}
}
void scanResourceFiles(String modulePath) {
File resDir = moduleResDirs[modulePath]
if (!resDir?.exists()) return
resDir.eachFileRecurse { File file ->
if (file.isFile() && file.name.endsWith('.xml')) {
def resourceType = file.parentFile.name
if (resourceType.startsWith('values')) {
scanValuesFile(file, modulePath)
} else if (isResourceDirectory(resourceType)) {
allResources[modulePath].add(file.name.replaceFirst(/\.[^\.]+$/, ''))
}
}
}
}
void scanValuesFile(File file, String modulePath) {
try {
def xml = new XmlSlurper().parse(file)
xml.children().each { node ->
if (node.@name != '') {
allResources[modulePath].add([email protected]())
}
}
} catch (Exception e) {
println "\nWarning: Failed to parse ${file.name}"
}
}
boolean isResourceDirectory(String dirName) {
return ['layout', 'drawable', 'drawable-v24', 'color', 'menu', 'anim'].any {
dirName.startsWith(it)
}
}
void generateReport() {
println "\n=== Unused Resources Report ==="
allModules.each { modulePath ->
def unused = getUnusedResources(modulePath)
if (!unused.isEmpty()) {
println "\n$modulePath:"
println "Total resources: ${allResources[modulePath]?.size()}"
println "Unused resources: ${unused.size()}"
unused.sort().each { resource ->
println " - $resource"
}
}
}
}
Set<String> getUnusedResources(String modulePath) {
return allResources[modulePath] - usedResources[modulePath]
}
void removeUnusedResources() {
println "\nRemoving unused resources..."
allModules.each { modulePath ->
def unused = getUnusedResources(modulePath)
if (!unused.isEmpty()) {
println "\nCleaning $modulePath"
removeModuleResources(modulePath, unused)
}
}
}
void removeModuleResources(String modulePath, Set<String> unused) {
File resDir = moduleResDirs[modulePath]
if (!resDir?.exists()) return
unused.each { resource ->
resDir.eachFileRecurse { file ->
if (file.isFile() && file.name.endsWith('.xml')) {
if (file.parentFile.name.startsWith('values')) {
removeFromValuesFile(file, resource)
} else if (file.name.replaceFirst(/\.[^\.]+$/, '') == resource) {
file.delete()
println "Removed: ${file.path}"
}
}
}
}
}
void removeFromValuesFile(File file, String resource) {
def tempFile = new File(file.parentFile, "${file.name}.temp")
boolean modified = false
boolean inResource = false
tempFile.withWriter { writer ->
file.eachLine { line ->
if (line.contains("name=\"$resource\"")) {
inResource = true
modified = true
} else if (!inResource) {
writer.println(line)
}
if (inResource && (line.contains('/>') || line.contains('</>'))) {
inResource = false
}
}
}
if (modified) {
tempFile.renameTo(file)
println "Removed resource '$resource' from ${file.name}"
} else {
tempFile.delete()
}
}
}
// Task definitions
task analyzeResources {
doLast {
new ResourceCleaner(project, false).execute()
}
}
task removeUnusedResources {
doLast {
new ResourceCleaner(project, true).execute()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment