Blame view

scripts/checkkconfigsymbols.py 9.27 KB
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
1
  #!/usr/bin/env python
b1a3f2434   Valentin Rothberg   checkkconfigsymbo...
2
  """Find Kconfig symbols that are referenced but not defined."""
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
3

208d51154   Valentin Rothberg   checkkconfigsymbo...
4
  # (c) 2014-2015 Valentin Rothberg <Valentin.Rothberg@lip6.fr>
cc641d552   Valentin Rothberg   checkkconfigsymbo...
5
  # (c) 2014 Stefan Hengelein <stefan.hengelein@fau.de>
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
6
  #
cc641d552   Valentin Rothberg   checkkconfigsymbo...
7
  # Licensed under the terms of the GNU GPL License version 2
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
8
9
10
11
  
  
  import os
  import re
b1a3f2434   Valentin Rothberg   checkkconfigsymbo...
12
  import sys
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
13
  from subprocess import Popen, PIPE, STDOUT
b1a3f2434   Valentin Rothberg   checkkconfigsymbo...
14
  from optparse import OptionParser
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
15

cc641d552   Valentin Rothberg   checkkconfigsymbo...
16
17
  
  # regex expressions
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
18
  OPERATORS = r"&|\(|\)|\||\!"
cc641d552   Valentin Rothberg   checkkconfigsymbo...
19
20
  FEATURE = r"(?:\w*[A-Z0-9]\w*){2,}"
  DEF = r"^\s*(?:menu){,1}config\s+(" + FEATURE + r")\s*"
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
21
22
  EXPR = r"(?:" + OPERATORS + r"|\s|" + FEATURE + r")+"
  STMT = r"^\s*(?:if|select|depends\s+on)\s+" + EXPR
cc641d552   Valentin Rothberg   checkkconfigsymbo...
23
  SOURCE_FEATURE = r"(?:\W|\b)+[D]{,1}CONFIG_(" + FEATURE + r")"
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
24

cc641d552   Valentin Rothberg   checkkconfigsymbo...
25
  # regex objects
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
26
27
  REGEX_FILE_KCONFIG = re.compile(r".*Kconfig[\.\w+\-]*$")
  REGEX_FEATURE = re.compile(r"(" + FEATURE + r")")
cc641d552   Valentin Rothberg   checkkconfigsymbo...
28
29
  REGEX_SOURCE_FEATURE = re.compile(SOURCE_FEATURE)
  REGEX_KCONFIG_DEF = re.compile(DEF)
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
30
31
32
33
  REGEX_KCONFIG_EXPR = re.compile(EXPR)
  REGEX_KCONFIG_STMT = re.compile(STMT)
  REGEX_KCONFIG_HELP = re.compile(r"^\s+(help|---help---)\s*$")
  REGEX_FILTER_FEATURES = re.compile(r"[A-Za-z0-9]$")
b1a3f2434   Valentin Rothberg   checkkconfigsymbo...
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
  def parse_options():
      """The user interface of this module."""
      usage = "%prog [options]
  
  "                                              \
              "Run this tool to detect Kconfig symbols that are referenced but " \
              "not defined in
  Kconfig.  The output of this tool has the "       \
              "format \'Undefined symbol\\tFile list\'
  
  "                      \
              "If no option is specified, %prog will default to check your
  "    \
              "current tree.  Please note that specifying commits will "         \
              "\'git reset --hard\'
  your current tree!  You may save "          \
              "uncommitted changes to avoid losing data."
  
      parser = OptionParser(usage=usage)
  
      parser.add_option('-c', '--commit', dest='commit', action='store',
                        default="",
                        help="Check if the specified commit (hash) introduces "
                             "undefined Kconfig symbols.")
  
      parser.add_option('-d', '--diff', dest='diff', action='store',
                        default="",
                        help="Diff undefined symbols between two commits.  The "
                             "input format bases on Git log's "
                             "\'commmit1..commit2\'.")
  
      parser.add_option('', '--force', dest='force', action='store_true',
                        default=False,
                        help="Reset current Git tree even when it's dirty.")
  
      (opts, _) = parser.parse_args()
  
      if opts.commit and opts.diff:
          sys.exit("Please specify only one option at once.")
  
      if opts.diff and not re.match(r"^[\w\-\.]+\.\.[\w\-\.]+$", opts.diff):
          sys.exit("Please specify valid input in the following format: "
                   "\'commmit1..commit2\'")
  
      if opts.commit or opts.diff:
          if not opts.force and tree_is_dirty():
              sys.exit("The current Git tree is dirty (see 'git status').  "
                       "Running this script may
  delete important data since it "
                       "calls 'git reset --hard' for some performance
  reasons. "
                       " Please run this script in a clean Git tree or pass "
                       "'--force' if you
  want to ignore this warning and "
                       "continue.")
  
      return opts
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
91
92
  def main():
      """Main function of this module."""
b1a3f2434   Valentin Rothberg   checkkconfigsymbo...
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
      opts = parse_options()
  
      if opts.commit or opts.diff:
          head = get_head()
  
          # get commit range
          commit_a = None
          commit_b = None
          if opts.commit:
              commit_a = opts.commit + "~"
              commit_b = opts.commit
          elif opts.diff:
              split = opts.diff.split("..")
              commit_a = split[0]
              commit_b = split[1]
              undefined_a = {}
              undefined_b = {}
  
          # get undefined items before the commit
          execute("git reset --hard %s" % commit_a)
          undefined_a = check_symbols()
  
          # get undefined items for the commit
          execute("git reset --hard %s" % commit_b)
          undefined_b = check_symbols()
  
          # report cases that are present for the commit but not before
e9533ae53   Valentin Rothberg   checkkconfigsymbo...
120
          for feature in sorted(undefined_b):
b1a3f2434   Valentin Rothberg   checkkconfigsymbo...
121
122
              # feature has not been undefined before
              if not feature in undefined_a:
e9533ae53   Valentin Rothberg   checkkconfigsymbo...
123
                  files = sorted(undefined_b.get(feature))
b1a3f2434   Valentin Rothberg   checkkconfigsymbo...
124
125
126
                  print "%s\t%s" % (feature, ", ".join(files))
              # check if there are new files that reference the undefined feature
              else:
e9533ae53   Valentin Rothberg   checkkconfigsymbo...
127
128
                  files = sorted(undefined_b.get(feature) -
                                 undefined_a.get(feature))
b1a3f2434   Valentin Rothberg   checkkconfigsymbo...
129
130
131
132
133
134
135
136
137
                  if files:
                      print "%s\t%s" % (feature, ", ".join(files))
  
          # reset to head
          execute("git reset --hard %s" % head)
  
      # default to check the entire tree
      else:
          undefined = check_symbols()
e9533ae53   Valentin Rothberg   checkkconfigsymbo...
138
139
140
          for feature in sorted(undefined):
              files = sorted(undefined.get(feature))
              print "%s\t%s" % (feature, ", ".join(files))
b1a3f2434   Valentin Rothberg   checkkconfigsymbo...
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
  
  
  def execute(cmd):
      """Execute %cmd and return stdout.  Exit in case of error."""
      pop = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True)
      (stdout, _) = pop.communicate()  # wait until finished
      if pop.returncode != 0:
          sys.exit(stdout)
      return stdout
  
  
  def tree_is_dirty():
      """Return true if the current working tree is dirty (i.e., if any file has
      been added, deleted, modified, renamed or copied but not committed)."""
      stdout = execute("git status --porcelain")
      for line in stdout:
          if re.findall(r"[URMADC]{1}", line[:2]):
              return True
      return False
  
  
  def get_head():
      """Return commit hash of current HEAD."""
      stdout = execute("git rev-parse HEAD")
      return stdout.strip('
  ')
  
  
  def check_symbols():
      """Find undefined Kconfig symbols and return a dict with the symbol as key
      and a list of referencing files as value."""
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
172
173
174
      source_files = []
      kconfig_files = []
      defined_features = set()
cc641d552   Valentin Rothberg   checkkconfigsymbo...
175
      referenced_features = dict()  # {feature: [files]}
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
176
177
  
      # use 'git ls-files' to get the worklist
b1a3f2434   Valentin Rothberg   checkkconfigsymbo...
178
      stdout = execute("git ls-files")
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
179
180
181
182
183
184
      if len(stdout) > 0 and stdout[-1] == "
  ":
          stdout = stdout[:-1]
  
      for gitfile in stdout.rsplit("
  "):
208d51154   Valentin Rothberg   checkkconfigsymbo...
185
186
187
          if ".git" in gitfile or "ChangeLog" in gitfile or      \
                  ".log" in gitfile or os.path.isdir(gitfile) or \
                  gitfile.startswith("tools/"):
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
188
189
190
191
              continue
          if REGEX_FILE_KCONFIG.match(gitfile):
              kconfig_files.append(gitfile)
          else:
cc641d552   Valentin Rothberg   checkkconfigsymbo...
192
              # all non-Kconfig files are checked for consistency
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
193
194
195
196
197
198
199
              source_files.append(gitfile)
  
      for sfile in source_files:
          parse_source_file(sfile, referenced_features)
  
      for kfile in kconfig_files:
          parse_kconfig_file(kfile, defined_features, referenced_features)
b1a3f2434   Valentin Rothberg   checkkconfigsymbo...
200
      undefined = {}  # {feature: [files]}
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
201
      for feature in sorted(referenced_features):
cc641d552   Valentin Rothberg   checkkconfigsymbo...
202
203
204
205
          # filter some false positives
          if feature == "FOO" or feature == "BAR" or \
                  feature == "FOO_BAR" or feature == "XXX":
              continue
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
206
207
          if feature not in defined_features:
              if feature.endswith("_MODULE"):
cc641d552   Valentin Rothberg   checkkconfigsymbo...
208
                  # avoid false positives for kernel modules
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
209
210
                  if feature[:-len("_MODULE")] in defined_features:
                      continue
b1a3f2434   Valentin Rothberg   checkkconfigsymbo...
211
212
              undefined[feature] = referenced_features.get(feature)
      return undefined
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
  
  
  def parse_source_file(sfile, referenced_features):
      """Parse @sfile for referenced Kconfig features."""
      lines = []
      with open(sfile, "r") as stream:
          lines = stream.readlines()
  
      for line in lines:
          if not "CONFIG_" in line:
              continue
          features = REGEX_SOURCE_FEATURE.findall(line)
          for feature in features:
              if not REGEX_FILTER_FEATURES.search(feature):
                  continue
cc641d552   Valentin Rothberg   checkkconfigsymbo...
228
229
230
              sfiles = referenced_features.get(feature, set())
              sfiles.add(sfile)
              referenced_features[feature] = sfiles
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
  
  
  def get_features_in_line(line):
      """Return mentioned Kconfig features in @line."""
      return REGEX_FEATURE.findall(line)
  
  
  def parse_kconfig_file(kfile, defined_features, referenced_features):
      """Parse @kfile and update feature definitions and references."""
      lines = []
      skip = False
  
      with open(kfile, "r") as stream:
          lines = stream.readlines()
  
      for i in range(len(lines)):
          line = lines[i]
          line = line.strip('
  ')
cc641d552   Valentin Rothberg   checkkconfigsymbo...
250
          line = line.split("#")[0]  # ignore comments
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
251
252
253
254
255
256
257
258
  
          if REGEX_KCONFIG_DEF.match(line):
              feature_def = REGEX_KCONFIG_DEF.findall(line)
              defined_features.add(feature_def[0])
              skip = False
          elif REGEX_KCONFIG_HELP.match(line):
              skip = True
          elif skip:
cc641d552   Valentin Rothberg   checkkconfigsymbo...
259
              # ignore content of help messages
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
260
261
262
              pass
          elif REGEX_KCONFIG_STMT.match(line):
              features = get_features_in_line(line)
cc641d552   Valentin Rothberg   checkkconfigsymbo...
263
              # multi-line statements
24fe1f03e   Valentin Rothberg   checkkconfigsymbo...
264
265
266
267
268
269
270
271
272
273
274
275
276
277
              while line.endswith("\\"):
                  i += 1
                  line = lines[i]
                  line = line.strip('
  ')
                  features.extend(get_features_in_line(line))
              for feature in set(features):
                  paths = referenced_features.get(feature, set())
                  paths.add(kfile)
                  referenced_features[feature] = paths
  
  
  if __name__ == "__main__":
      main()