Created
November 27, 2024 06:04
-
-
Save EhsanSetayesh/9cd12fc0c58153a471fdeb7104f40862 to your computer and use it in GitHub Desktop.
resource-cleaner
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
| // 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