1#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Without any args, this simply loads the IDs out of a bunch of the Chrome GRD
7files, and then checks the subset of the code that loads the strings to try
8and figure out what isn't in use any more.
9You can give paths to GRD files and source directories to control what is
10check instead.
11"""
12
13import os
14import re
15import sys
16import xml.sax
17
18# Extra messages along the way
19# 1 - Print ids that are found in sources but not in the found id set
20# 2 - Files that aren't processes (don't match the source name regex)
21DEBUG = 0
22
23
24class GrdIDExtractor(xml.sax.handler.ContentHandler):
25  """Extracts the IDs from messages in GRIT files"""
26  def __init__(self):
27    self.id_set_ = set()
28
29  def startElement(self, name, attrs):
30    if name == 'message':
31      self.id_set_.add(attrs['name'])
32
33  def allIDs(self):
34    """Return all the IDs found"""
35    return self.id_set_.copy()
36
37
38def CheckForUnusedGrdIDsInSources(grd_files, src_dirs):
39  """Will collect the message ids out of the given GRD files and then scan
40  the source directories to try and figure out what ids are not currently
41  being used by any source.
42
43  grd_files:
44    A list of GRD files to collect the ids from.
45  src_dirs:
46    A list of directories to walk looking for source files.
47  """
48  # Collect all the ids into a large map
49  all_ids = set()
50  file_id_map = {}
51  for y in grd_files:
52    handler = GrdIDExtractor()
53    xml.sax.parse(y, handler)
54    files_ids = handler.allIDs()
55    file_id_map[y] = files_ids
56    all_ids |= files_ids
57
58
59  # The regex that will be used to check sources
60  id_regex = re.compile('IDS_[A-Z0-9_]+')
61
62  # Make sure the regex matches every id found.
63  got_err = False
64  for x in all_ids:
65    match = id_regex.search(x)
66    if match is None:
67      print 'ERROR: "%s" did not match our regex' % (x)
68      got_err = True
69    if not match.group(0) is x:
70      print 'ERROR: "%s" did not fully match our regex' % (x)
71      got_err = True
72  if got_err:
73    return 1
74
75  # The regex for deciding what is a source file
76  src_regex = re.compile('\.(([chm])|(mm)|(cc)|(cp)|(cpp)|(xib)|(py))$')
77
78  ids_left = all_ids.copy()
79
80  # Scanning time.
81  for src_dir in src_dirs:
82    for root, dirs, files in os.walk(src_dir):
83      # Remove svn directories from recursion
84      if '.svn' in dirs:
85        dirs.remove('.svn')
86      for file in files:
87        if src_regex.search(file.lower()):
88          full_path = os.path.join(root, file)
89          src_file_contents = open(full_path).read()
90          for match in sorted(set(id_regex.findall(src_file_contents))):
91            if match in ids_left:
92              ids_left.remove(match)
93            if DEBUG:
94              if not match in all_ids:
95                print '%s had "%s", which was not in the found IDs' % \
96                  (full_path, match)
97        elif DEBUG > 1:
98          full_path = os.path.join(root, file)
99          print 'Skipping %s.' % (full_path)
100
101  # Anything left?
102  if len(ids_left) > 0:
103    print 'The following ids are in GRD files, but *appear* to be unused:'
104    for file_path, file_ids in file_id_map.iteritems():
105      missing = ids_left.intersection(file_ids)
106      if len(missing) > 0:
107        print '  %s:' % (file_path)
108        print '\n'.join('    %s' % (x) for x in sorted(missing))
109
110  return 0
111
112
113def main():
114  # script lives in src/tools
115  tools_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
116  src_dir = os.path.dirname(tools_dir)
117
118  # Collect the args into the right buckets
119  src_dirs = []
120  grd_files = []
121  for arg in sys.argv[1:]:
122    if arg.lower().endswith('.grd'):
123      grd_files.append(arg)
124    else:
125      src_dirs.append(arg)
126
127  # If no GRD files were given, default them:
128  if len(grd_files) == 0:
129    ash_base_dir = os.path.join(src_dir, 'ash')
130    athena_strings_dir = os.path.join(src_dir, 'athena', 'strings')
131    chrome_dir = os.path.join(src_dir, 'chrome')
132    chrome_app_dir = os.path.join(chrome_dir, 'app')
133    chrome_app_res_dir = os.path.join(chrome_app_dir, 'resources')
134    device_base_dir = os.path.join(src_dir, 'device')
135    ui_dir = os.path.join(src_dir, 'ui')
136    ui_strings_dir = os.path.join(ui_dir, 'strings')
137    ui_chromeos_dir = os.path.join(ui_dir, 'chromeos')
138    grd_files = [
139      os.path.join(ash_base_dir, 'ash_strings.grd'),
140      os.path.join(ash_base_dir, 'resources', 'ash_resources.grd'),
141      os.path.join(athena_strings_dir, 'athena_strings.grd'),
142      os.path.join(chrome_app_dir, 'chromium_strings.grd'),
143      os.path.join(chrome_app_dir, 'generated_resources.grd'),
144      os.path.join(chrome_app_dir, 'google_chrome_strings.grd'),
145      os.path.join(chrome_app_res_dir, 'locale_settings.grd'),
146      os.path.join(chrome_app_res_dir, 'locale_settings_chromiumos.grd'),
147      os.path.join(chrome_app_res_dir, 'locale_settings_google_chromeos.grd'),
148      os.path.join(chrome_app_res_dir, 'locale_settings_linux.grd'),
149      os.path.join(chrome_app_res_dir, 'locale_settings_mac.grd'),
150      os.path.join(chrome_app_res_dir, 'locale_settings_win.grd'),
151      os.path.join(chrome_app_dir, 'theme', 'theme_resources.grd'),
152      os.path.join(chrome_dir, 'browser', 'browser_resources.grd'),
153      os.path.join(chrome_dir, 'common', 'common_resources.grd'),
154      os.path.join(chrome_dir, 'renderer', 'resources',
155                   'renderer_resources.grd'),
156      os.path.join(device_base_dir, 'bluetooth', 'bluetooth_strings.grd'),
157      os.path.join(src_dir, 'extensions', 'extensions_strings.grd'),
158      os.path.join(src_dir, 'ui', 'resources', 'ui_resources.grd'),
159      os.path.join(src_dir, 'ui', 'webui', 'resources', 'webui_resources.grd'),
160      os.path.join(ui_strings_dir, 'app_locale_settings.grd'),
161      os.path.join(ui_strings_dir, 'ui_strings.grd'),
162      os.path.join(ui_chromeos_dir, 'ui_chromeos_strings.grd'),
163    ]
164
165  # If no source directories were given, default them:
166  if len(src_dirs) == 0:
167    src_dirs = [
168      os.path.join(src_dir, 'app'),
169      os.path.join(src_dir, 'ash'),
170      os.path.join(src_dir, 'athena'),
171      os.path.join(src_dir, 'chrome'),
172      os.path.join(src_dir, 'components'),
173      os.path.join(src_dir, 'content'),
174      os.path.join(src_dir, 'device'),
175      os.path.join(src_dir, 'extensions'),
176      os.path.join(src_dir, 'ui'),
177      # nsNSSCertHelper.cpp has a bunch of ids
178      os.path.join(src_dir, 'third_party', 'mozilla_security_manager'),
179      os.path.join(chrome_dir, 'installer'),
180    ]
181
182  return CheckForUnusedGrdIDsInSources(grd_files, src_dirs)
183
184
185if __name__ == '__main__':
186  sys.exit(main())
187