1#!/usr/bin/ruby 2# 3# Find unused resources in all the apps found recursively under the current directory 4# Usage: 5# find_unused_resources.rb [-html] 6# 7# If -html is specified, the output will be HTML, otherwise it will be plain text 8# 9# Author: cbeust@google.com 10 11require 'find' 12 13debug = false 14 15@@stringIdPattern = Regexp.new("name=\"([@_a-zA-Z0-9 ]*)\"") 16@@layoutIdPattern = Regexp.new("android:id=\".*id/([_a-zA-Z0-9]*)\"") 17 18@@stringXmlPatterns = [ 19 Regexp.new("@string/([_a-zA-Z0-9]*)"), 20 Regexp.new("@array/([_a-zA-Z0-9]*)"), 21] 22 23@@javaIdPatterns = [ 24 Regexp.new("R.id.([_a-zA-Z0-9]+)"), 25 Regexp.new("R.string.([_a-zA-Z0-9]+)"), 26 Regexp.new("R.array.([_a-zA-Z0-9]+)"), 27 Regexp.new("R.color.([_a-zA-Z0-9]+)"), 28 Regexp.new("R.configVarying.([_a-zA-Z0-9]+)"), 29 Regexp.new("R.dimen.([_a-zA-Z0-9]+)"), 30] 31 32 33@@appDir = "partner/google/apps/Gmail" 34 35def findResDirectories(root) 36 result = Array.new 37 Find.find(root) do |path| 38 if FileTest.directory?(path) 39 if File.basename(path) == "res" 40 result << path 41 else 42 next 43 end 44 end 45 end 46 result 47end 48 49class UnusedResources 50 attr_accessor :appDir, :unusedLayoutIds, :unusedStringIds 51end 52 53class FilePosition 54 attr_accessor :file, :lineNumber 55 56 def initialize(f, ln) 57 @file = f 58 @lineNumber = ln 59 end 60 61 def to_s 62 "#{file}:#{lineNumber}" 63 end 64 65 def <=>(other) 66 if @file == other.file 67 @lineNumber - other.lineNumber 68 else 69 @file <=> other.file 70 end 71 end 72end 73 74 75def findAllOccurrences(re, string) 76 result = Array.new 77 78 s = string 79 matchData = re.match(s) 80 while (matchData) 81 result << matchData[1].to_s 82 s = s[matchData.end(1) .. -1] 83 matchData = re.match(s) 84 end 85 86 result 87end 88 89@@globalJavaIdUses = Hash.new 90 91def recordJavaUses(glob) 92 Dir.glob(glob).each { |filename| 93 File.open(filename) { |file| 94 file.each { |line| 95 @@javaIdPatterns.each { |re| 96 findAllOccurrences(re, line).each { |id| 97 @@globalJavaIdUses[id] = FilePosition.new(filename, file.lineno) 98 } 99 } 100 } 101 } 102 } 103end 104 105def findUnusedResources(dir) 106 javaIdUses = Hash.new 107 layouts = Hash.new 108 strings = Hash.new 109 xmlIdUses = Hash.new 110 111 Dir.glob("#{dir}/res/**/*.xml").each { |filename| 112 if ! (filename =~ /attrs.xml$/) 113 File.open(filename) { |file| 114 file.each { |line| 115 findAllOccurrences(@@stringIdPattern, line).each {|id| 116 strings[id] = FilePosition.new(filename, file.lineno) 117 } 118 findAllOccurrences(@@layoutIdPattern, line).each {|id| 119 layouts[id] = FilePosition.new(filename, file.lineno) 120 } 121 @@stringXmlPatterns.each { |re| 122 findAllOccurrences(re, line).each {|id| 123 xmlIdUses[id] = FilePosition.new(filename, file.lineno) 124 } 125 } 126 } 127 } 128 end 129 } 130 131 Dir.glob("#{dir}/AndroidManifest.xml").each { |filename| 132 File.open(filename) { |file| 133 file.each { |line| 134 @@stringXmlPatterns.each { |re| 135 findAllOccurrences(re, line).each {|id| 136 xmlIdUses[id] = FilePosition.new(filename, file.lineno) 137 } 138 } 139 } 140 } 141 } 142 143 recordJavaUses("#{dir}/src/**/*.java") 144 145 @@globalJavaIdUses.each_pair { |id, file| 146 layouts.delete(id) 147 strings.delete(id) 148 } 149 150 javaIdUses.each_pair { |id, file| 151 layouts.delete(id) 152 strings.delete(id) 153 } 154 155 xmlIdUses.each_pair { |id, file| 156 layouts.delete(id) 157 strings.delete(id) 158 } 159 160 result = UnusedResources.new 161 result.appDir = dir 162 result.unusedLayoutIds = layouts 163 result.unusedStringIds = strings 164 165 result 166end 167 168def findApps(dir) 169 result = Array.new 170 Dir.glob("#{dir}/**/res").each { |filename| 171 a = filename.split("/") 172 result << a.slice(0, a.size-1).join("/") 173 } 174 result 175end 176 177def displayText(result) 178 result.each { |unusedResources| 179 puts "=== #{unusedResources.appDir}" 180 181 puts "----- Unused layout ids" 182 unusedResources.unusedLayoutIds.sort { |id, file| id[1] <=> file[1] }.each {|f| 183 puts " #{f[0]} #{f[1]}" 184 } 185 186 187 puts "----- Unused string ids" 188 unusedResources.unusedStringIds.sort { |id, file| id[1] <=> file[1] }.each {|f| 189 puts " #{f[0]} #{f[1]}" 190 } 191 192 } 193end 194 195def displayHtmlUnused(unusedResourceIds, title) 196 197 puts "<h3>#{title}</h3>" 198 puts "<table border='1'>" 199 unusedResourceIds.sort { |id, file| id[1] <=> file[1] }.each {|f| 200 puts "<tr><td><b>#{f[0]}</b></td> <td>#{f[1]}</td></tr>" 201 } 202 puts "</table>" 203end 204 205def displayHtml(result) 206 title = "Unused resources as of #{Time.now.localtime}" 207 puts "<html><header><title>#{title}</title></header><body>" 208 209 puts "<h1><p align=\"center\">#{title}</p></h1>" 210 result.each { |unusedResources| 211 puts "<h2>#{unusedResources.appDir}</h2>" 212 displayHtmlUnused(unusedResources.unusedLayoutIds, "Unused layout ids") 213 displayHtmlUnused(unusedResources.unusedStringIds, "Unused other ids") 214 } 215 puts "</body>" 216end 217 218result = Array.new 219 220recordJavaUses("java/android/**/*.java") 221 222if debug 223 result << findUnusedResources("apps/Browser") 224else 225 findApps(".").each { |appDir| 226 result << findUnusedResources(appDir) 227 } 228end 229 230if ARGV[0] == "-html" 231 displayHtml result 232else 233 displayText result 234end 235 236