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