14b6fda0bSValentin Rothberg#!/usr/bin/env python2
224fe1f03SValentin Rothberg
3b1a3f243SValentin Rothberg"""Find Kconfig symbols that are referenced but not defined."""
424fe1f03SValentin Rothberg
5208d5115SValentin Rothberg# (c) 2014-2015 Valentin Rothberg <Valentin.Rothberg@lip6.fr>
6cc641d55SValentin Rothberg# (c) 2014 Stefan Hengelein <stefan.hengelein@fau.de>
724fe1f03SValentin Rothberg#
8cc641d55SValentin Rothberg# Licensed under the terms of the GNU GPL License version 2
924fe1f03SValentin Rothberg
1024fe1f03SValentin Rothberg
1124fe1f03SValentin Rothbergimport os
1224fe1f03SValentin Rothbergimport re
13b1a3f243SValentin Rothbergimport sys
1424fe1f03SValentin Rothbergfrom subprocess import Popen, PIPE, STDOUT
15b1a3f243SValentin Rothbergfrom optparse import OptionParser
1624fe1f03SValentin Rothberg
17cc641d55SValentin Rothberg
18cc641d55SValentin Rothberg# regex expressions
1924fe1f03SValentin RothbergOPERATORS = r"&|\(|\)|\||\!"
20cc641d55SValentin RothbergFEATURE = r"(?:\w*[A-Z0-9]\w*){2,}"
21cc641d55SValentin RothbergDEF = r"^\s*(?:menu){,1}config\s+(" + FEATURE + r")\s*"
2224fe1f03SValentin RothbergEXPR = r"(?:" + OPERATORS + r"|\s|" + FEATURE + r")+"
2324fe1f03SValentin RothbergSTMT = r"^\s*(?:if|select|depends\s+on)\s+" + EXPR
24cc641d55SValentin RothbergSOURCE_FEATURE = r"(?:\W|\b)+[D]{,1}CONFIG_(" + FEATURE + r")"
2524fe1f03SValentin Rothberg
26cc641d55SValentin Rothberg# regex objects
2724fe1f03SValentin RothbergREGEX_FILE_KCONFIG = re.compile(r".*Kconfig[\.\w+\-]*$")
2824fe1f03SValentin RothbergREGEX_FEATURE = re.compile(r"(" + FEATURE + r")")
29cc641d55SValentin RothbergREGEX_SOURCE_FEATURE = re.compile(SOURCE_FEATURE)
30cc641d55SValentin RothbergREGEX_KCONFIG_DEF = re.compile(DEF)
3124fe1f03SValentin RothbergREGEX_KCONFIG_EXPR = re.compile(EXPR)
3224fe1f03SValentin RothbergREGEX_KCONFIG_STMT = re.compile(STMT)
3324fe1f03SValentin RothbergREGEX_KCONFIG_HELP = re.compile(r"^\s+(help|---help---)\s*$")
3424fe1f03SValentin RothbergREGEX_FILTER_FEATURES = re.compile(r"[A-Za-z0-9]$")
3524fe1f03SValentin Rothberg
3624fe1f03SValentin Rothberg
37b1a3f243SValentin Rothbergdef parse_options():
38b1a3f243SValentin Rothberg    """The user interface of this module."""
39b1a3f243SValentin Rothberg    usage = "%prog [options]\n\n"                                              \
40b1a3f243SValentin Rothberg            "Run this tool to detect Kconfig symbols that are referenced but " \
41b1a3f243SValentin Rothberg            "not defined in\nKconfig.  The output of this tool has the "       \
42b1a3f243SValentin Rothberg            "format \'Undefined symbol\\tFile list\'\n\n"                      \
43b1a3f243SValentin Rothberg            "If no option is specified, %prog will default to check your\n"    \
44b1a3f243SValentin Rothberg            "current tree.  Please note that specifying commits will "         \
45b1a3f243SValentin Rothberg            "\'git reset --hard\'\nyour current tree!  You may save "          \
46b1a3f243SValentin Rothberg            "uncommitted changes to avoid losing data."
47b1a3f243SValentin Rothberg
48b1a3f243SValentin Rothberg    parser = OptionParser(usage=usage)
49b1a3f243SValentin Rothberg
50b1a3f243SValentin Rothberg    parser.add_option('-c', '--commit', dest='commit', action='store',
51b1a3f243SValentin Rothberg                      default="",
52b1a3f243SValentin Rothberg                      help="Check if the specified commit (hash) introduces "
53b1a3f243SValentin Rothberg                           "undefined Kconfig symbols.")
54b1a3f243SValentin Rothberg
55b1a3f243SValentin Rothberg    parser.add_option('-d', '--diff', dest='diff', action='store',
56b1a3f243SValentin Rothberg                      default="",
57b1a3f243SValentin Rothberg                      help="Diff undefined symbols between two commits.  The "
58b1a3f243SValentin Rothberg                           "input format bases on Git log's "
59b1a3f243SValentin Rothberg                           "\'commmit1..commit2\'.")
60b1a3f243SValentin Rothberg
61a42fa92cSValentin Rothberg    parser.add_option('-f', '--find', dest='find', action='store_true',
62a42fa92cSValentin Rothberg                      default=False,
63a42fa92cSValentin Rothberg                      help="Find and show commits that may cause symbols to be "
64a42fa92cSValentin Rothberg                           "missing.  Required to run with --diff.")
65a42fa92cSValentin Rothberg
66cf132e4aSValentin Rothberg    parser.add_option('-i', '--ignore', dest='ignore', action='store',
67cf132e4aSValentin Rothberg                      default="",
68cf132e4aSValentin Rothberg                      help="Ignore files matching this pattern.  Note that "
69cf132e4aSValentin Rothberg                           "the pattern needs to be a Python regex.  To "
70cf132e4aSValentin Rothberg                           "ignore defconfigs, specify -i '.*defconfig'.")
71cf132e4aSValentin Rothberg
72b1a3f243SValentin Rothberg    parser.add_option('', '--force', dest='force', action='store_true',
73b1a3f243SValentin Rothberg                      default=False,
74b1a3f243SValentin Rothberg                      help="Reset current Git tree even when it's dirty.")
75b1a3f243SValentin Rothberg
76b1a3f243SValentin Rothberg    (opts, _) = parser.parse_args()
77b1a3f243SValentin Rothberg
78b1a3f243SValentin Rothberg    if opts.commit and opts.diff:
79b1a3f243SValentin Rothberg        sys.exit("Please specify only one option at once.")
80b1a3f243SValentin Rothberg
81b1a3f243SValentin Rothberg    if opts.diff and not re.match(r"^[\w\-\.]+\.\.[\w\-\.]+$", opts.diff):
82b1a3f243SValentin Rothberg        sys.exit("Please specify valid input in the following format: "
83b1a3f243SValentin Rothberg                 "\'commmit1..commit2\'")
84b1a3f243SValentin Rothberg
85b1a3f243SValentin Rothberg    if opts.commit or opts.diff:
86b1a3f243SValentin Rothberg        if not opts.force and tree_is_dirty():
87b1a3f243SValentin Rothberg            sys.exit("The current Git tree is dirty (see 'git status').  "
88b1a3f243SValentin Rothberg                     "Running this script may\ndelete important data since it "
89b1a3f243SValentin Rothberg                     "calls 'git reset --hard' for some performance\nreasons. "
90b1a3f243SValentin Rothberg                     " Please run this script in a clean Git tree or pass "
91b1a3f243SValentin Rothberg                     "'--force' if you\nwant to ignore this warning and "
92b1a3f243SValentin Rothberg                     "continue.")
93b1a3f243SValentin Rothberg
94a42fa92cSValentin Rothberg    if opts.commit:
95a42fa92cSValentin Rothberg        opts.find = False
96a42fa92cSValentin Rothberg
97cf132e4aSValentin Rothberg    if opts.ignore:
98cf132e4aSValentin Rothberg        try:
99cf132e4aSValentin Rothberg            re.match(opts.ignore, "this/is/just/a/test.c")
100cf132e4aSValentin Rothberg        except:
101cf132e4aSValentin Rothberg            sys.exit("Please specify a valid Python regex.")
102cf132e4aSValentin Rothberg
103b1a3f243SValentin Rothberg    return opts
104b1a3f243SValentin Rothberg
105b1a3f243SValentin Rothberg
10624fe1f03SValentin Rothbergdef main():
10724fe1f03SValentin Rothberg    """Main function of this module."""
108b1a3f243SValentin Rothberg    opts = parse_options()
109b1a3f243SValentin Rothberg
110b1a3f243SValentin Rothberg    if opts.commit or opts.diff:
111b1a3f243SValentin Rothberg        head = get_head()
112b1a3f243SValentin Rothberg
113b1a3f243SValentin Rothberg        # get commit range
114b1a3f243SValentin Rothberg        commit_a = None
115b1a3f243SValentin Rothberg        commit_b = None
116b1a3f243SValentin Rothberg        if opts.commit:
117b1a3f243SValentin Rothberg            commit_a = opts.commit + "~"
118b1a3f243SValentin Rothberg            commit_b = opts.commit
119b1a3f243SValentin Rothberg        elif opts.diff:
120b1a3f243SValentin Rothberg            split = opts.diff.split("..")
121b1a3f243SValentin Rothberg            commit_a = split[0]
122b1a3f243SValentin Rothberg            commit_b = split[1]
123b1a3f243SValentin Rothberg            undefined_a = {}
124b1a3f243SValentin Rothberg            undefined_b = {}
125b1a3f243SValentin Rothberg
126b1a3f243SValentin Rothberg        # get undefined items before the commit
127b1a3f243SValentin Rothberg        execute("git reset --hard %s" % commit_a)
128cf132e4aSValentin Rothberg        undefined_a = check_symbols(opts.ignore)
129b1a3f243SValentin Rothberg
130b1a3f243SValentin Rothberg        # get undefined items for the commit
131b1a3f243SValentin Rothberg        execute("git reset --hard %s" % commit_b)
132cf132e4aSValentin Rothberg        undefined_b = check_symbols(opts.ignore)
133b1a3f243SValentin Rothberg
134b1a3f243SValentin Rothberg        # report cases that are present for the commit but not before
135e9533ae5SValentin Rothberg        for feature in sorted(undefined_b):
136b1a3f243SValentin Rothberg            # feature has not been undefined before
137b1a3f243SValentin Rothberg            if not feature in undefined_a:
138e9533ae5SValentin Rothberg                files = sorted(undefined_b.get(feature))
139b1a3f243SValentin Rothberg                print "%s\t%s" % (feature, ", ".join(files))
140a42fa92cSValentin Rothberg                if opts.find:
141a42fa92cSValentin Rothberg                    commits = find_commits(feature, opts.diff)
142a42fa92cSValentin Rothberg                    print commits
143b1a3f243SValentin Rothberg            # check if there are new files that reference the undefined feature
144b1a3f243SValentin Rothberg            else:
145e9533ae5SValentin Rothberg                files = sorted(undefined_b.get(feature) -
146e9533ae5SValentin Rothberg                               undefined_a.get(feature))
147b1a3f243SValentin Rothberg                if files:
148b1a3f243SValentin Rothberg                    print "%s\t%s" % (feature, ", ".join(files))
149a42fa92cSValentin Rothberg                    if opts.find:
150a42fa92cSValentin Rothberg                        commits = find_commits(feature, opts.diff)
151a42fa92cSValentin Rothberg                        print commits
152b1a3f243SValentin Rothberg
153b1a3f243SValentin Rothberg        # reset to head
154b1a3f243SValentin Rothberg        execute("git reset --hard %s" % head)
155b1a3f243SValentin Rothberg
156b1a3f243SValentin Rothberg    # default to check the entire tree
157b1a3f243SValentin Rothberg    else:
158cf132e4aSValentin Rothberg        undefined = check_symbols(opts.ignore)
159e9533ae5SValentin Rothberg        for feature in sorted(undefined):
160e9533ae5SValentin Rothberg            files = sorted(undefined.get(feature))
161e9533ae5SValentin Rothberg            print "%s\t%s" % (feature, ", ".join(files))
162b1a3f243SValentin Rothberg
163b1a3f243SValentin Rothberg
164b1a3f243SValentin Rothbergdef execute(cmd):
165b1a3f243SValentin Rothberg    """Execute %cmd and return stdout.  Exit in case of error."""
166b1a3f243SValentin Rothberg    pop = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True)
167b1a3f243SValentin Rothberg    (stdout, _) = pop.communicate()  # wait until finished
168b1a3f243SValentin Rothberg    if pop.returncode != 0:
169b1a3f243SValentin Rothberg        sys.exit(stdout)
170b1a3f243SValentin Rothberg    return stdout
171b1a3f243SValentin Rothberg
172b1a3f243SValentin Rothberg
173a42fa92cSValentin Rothbergdef find_commits(symbol, diff):
174a42fa92cSValentin Rothberg    """Find commits changing %symbol in the given range of %diff."""
175a42fa92cSValentin Rothberg    commits = execute("git log --pretty=oneline --abbrev-commit -G %s %s"
176a42fa92cSValentin Rothberg                      % (symbol, diff))
177a42fa92cSValentin Rothberg    return commits
178a42fa92cSValentin Rothberg
179a42fa92cSValentin Rothberg
180b1a3f243SValentin Rothbergdef tree_is_dirty():
181b1a3f243SValentin Rothberg    """Return true if the current working tree is dirty (i.e., if any file has
182b1a3f243SValentin Rothberg    been added, deleted, modified, renamed or copied but not committed)."""
183b1a3f243SValentin Rothberg    stdout = execute("git status --porcelain")
184b1a3f243SValentin Rothberg    for line in stdout:
185b1a3f243SValentin Rothberg        if re.findall(r"[URMADC]{1}", line[:2]):
186b1a3f243SValentin Rothberg            return True
187b1a3f243SValentin Rothberg    return False
188b1a3f243SValentin Rothberg
189b1a3f243SValentin Rothberg
190b1a3f243SValentin Rothbergdef get_head():
191b1a3f243SValentin Rothberg    """Return commit hash of current HEAD."""
192b1a3f243SValentin Rothberg    stdout = execute("git rev-parse HEAD")
193b1a3f243SValentin Rothberg    return stdout.strip('\n')
194b1a3f243SValentin Rothberg
195b1a3f243SValentin Rothberg
196cf132e4aSValentin Rothbergdef check_symbols(ignore):
197b1a3f243SValentin Rothberg    """Find undefined Kconfig symbols and return a dict with the symbol as key
198cf132e4aSValentin Rothberg    and a list of referencing files as value.  Files matching %ignore are not
199cf132e4aSValentin Rothberg    checked for undefined symbols."""
20024fe1f03SValentin Rothberg    source_files = []
20124fe1f03SValentin Rothberg    kconfig_files = []
20224fe1f03SValentin Rothberg    defined_features = set()
203cc641d55SValentin Rothberg    referenced_features = dict()  # {feature: [files]}
20424fe1f03SValentin Rothberg
20524fe1f03SValentin Rothberg    # use 'git ls-files' to get the worklist
206b1a3f243SValentin Rothberg    stdout = execute("git ls-files")
20724fe1f03SValentin Rothberg    if len(stdout) > 0 and stdout[-1] == "\n":
20824fe1f03SValentin Rothberg        stdout = stdout[:-1]
20924fe1f03SValentin Rothberg
21024fe1f03SValentin Rothberg    for gitfile in stdout.rsplit("\n"):
21124fe1f03SValentin Rothberg        if ".git" in gitfile or "ChangeLog" in gitfile or      \
212208d5115SValentin Rothberg                ".log" in gitfile or os.path.isdir(gitfile) or \
213208d5115SValentin Rothberg                gitfile.startswith("tools/"):
21424fe1f03SValentin Rothberg            continue
21524fe1f03SValentin Rothberg        if REGEX_FILE_KCONFIG.match(gitfile):
21624fe1f03SValentin Rothberg            kconfig_files.append(gitfile)
21724fe1f03SValentin Rothberg        else:
218cc641d55SValentin Rothberg            # all non-Kconfig files are checked for consistency
21924fe1f03SValentin Rothberg            source_files.append(gitfile)
22024fe1f03SValentin Rothberg
22124fe1f03SValentin Rothberg    for sfile in source_files:
222cf132e4aSValentin Rothberg        if ignore and re.match(ignore, sfile):
223cf132e4aSValentin Rothberg            # do not check files matching %ignore
224cf132e4aSValentin Rothberg            continue
22524fe1f03SValentin Rothberg        parse_source_file(sfile, referenced_features)
22624fe1f03SValentin Rothberg
22724fe1f03SValentin Rothberg    for kfile in kconfig_files:
228cf132e4aSValentin Rothberg        if ignore and re.match(ignore, kfile):
229cf132e4aSValentin Rothberg            # do not collect references for files matching %ignore
230cf132e4aSValentin Rothberg            parse_kconfig_file(kfile, defined_features, dict())
231cf132e4aSValentin Rothberg        else:
23224fe1f03SValentin Rothberg            parse_kconfig_file(kfile, defined_features, referenced_features)
23324fe1f03SValentin Rothberg
234b1a3f243SValentin Rothberg    undefined = {}  # {feature: [files]}
23524fe1f03SValentin Rothberg    for feature in sorted(referenced_features):
236cc641d55SValentin Rothberg        # filter some false positives
237cc641d55SValentin Rothberg        if feature == "FOO" or feature == "BAR" or \
238cc641d55SValentin Rothberg                feature == "FOO_BAR" or feature == "XXX":
239cc641d55SValentin Rothberg            continue
24024fe1f03SValentin Rothberg        if feature not in defined_features:
24124fe1f03SValentin Rothberg            if feature.endswith("_MODULE"):
242cc641d55SValentin Rothberg                # avoid false positives for kernel modules
24324fe1f03SValentin Rothberg                if feature[:-len("_MODULE")] in defined_features:
24424fe1f03SValentin Rothberg                    continue
245b1a3f243SValentin Rothberg            undefined[feature] = referenced_features.get(feature)
246b1a3f243SValentin Rothberg    return undefined
24724fe1f03SValentin Rothberg
24824fe1f03SValentin Rothberg
24924fe1f03SValentin Rothbergdef parse_source_file(sfile, referenced_features):
25024fe1f03SValentin Rothberg    """Parse @sfile for referenced Kconfig features."""
25124fe1f03SValentin Rothberg    lines = []
25224fe1f03SValentin Rothberg    with open(sfile, "r") as stream:
25324fe1f03SValentin Rothberg        lines = stream.readlines()
25424fe1f03SValentin Rothberg
25524fe1f03SValentin Rothberg    for line in lines:
25624fe1f03SValentin Rothberg        if not "CONFIG_" in line:
25724fe1f03SValentin Rothberg            continue
25824fe1f03SValentin Rothberg        features = REGEX_SOURCE_FEATURE.findall(line)
25924fe1f03SValentin Rothberg        for feature in features:
26024fe1f03SValentin Rothberg            if not REGEX_FILTER_FEATURES.search(feature):
26124fe1f03SValentin Rothberg                continue
262cc641d55SValentin Rothberg            sfiles = referenced_features.get(feature, set())
263cc641d55SValentin Rothberg            sfiles.add(sfile)
264cc641d55SValentin Rothberg            referenced_features[feature] = sfiles
26524fe1f03SValentin Rothberg
26624fe1f03SValentin Rothberg
26724fe1f03SValentin Rothbergdef get_features_in_line(line):
26824fe1f03SValentin Rothberg    """Return mentioned Kconfig features in @line."""
26924fe1f03SValentin Rothberg    return REGEX_FEATURE.findall(line)
27024fe1f03SValentin Rothberg
27124fe1f03SValentin Rothberg
27224fe1f03SValentin Rothbergdef parse_kconfig_file(kfile, defined_features, referenced_features):
27324fe1f03SValentin Rothberg    """Parse @kfile and update feature definitions and references."""
27424fe1f03SValentin Rothberg    lines = []
27524fe1f03SValentin Rothberg    skip = False
27624fe1f03SValentin Rothberg
27724fe1f03SValentin Rothberg    with open(kfile, "r") as stream:
27824fe1f03SValentin Rothberg        lines = stream.readlines()
27924fe1f03SValentin Rothberg
28024fe1f03SValentin Rothberg    for i in range(len(lines)):
28124fe1f03SValentin Rothberg        line = lines[i]
28224fe1f03SValentin Rothberg        line = line.strip('\n')
283cc641d55SValentin Rothberg        line = line.split("#")[0]  # ignore comments
28424fe1f03SValentin Rothberg
28524fe1f03SValentin Rothberg        if REGEX_KCONFIG_DEF.match(line):
28624fe1f03SValentin Rothberg            feature_def = REGEX_KCONFIG_DEF.findall(line)
28724fe1f03SValentin Rothberg            defined_features.add(feature_def[0])
28824fe1f03SValentin Rothberg            skip = False
28924fe1f03SValentin Rothberg        elif REGEX_KCONFIG_HELP.match(line):
29024fe1f03SValentin Rothberg            skip = True
29124fe1f03SValentin Rothberg        elif skip:
292cc641d55SValentin Rothberg            # ignore content of help messages
29324fe1f03SValentin Rothberg            pass
29424fe1f03SValentin Rothberg        elif REGEX_KCONFIG_STMT.match(line):
29524fe1f03SValentin Rothberg            features = get_features_in_line(line)
296cc641d55SValentin Rothberg            # multi-line statements
29724fe1f03SValentin Rothberg            while line.endswith("\\"):
29824fe1f03SValentin Rothberg                i += 1
29924fe1f03SValentin Rothberg                line = lines[i]
30024fe1f03SValentin Rothberg                line = line.strip('\n')
30124fe1f03SValentin Rothberg                features.extend(get_features_in_line(line))
30224fe1f03SValentin Rothberg            for feature in set(features):
30324fe1f03SValentin Rothberg                paths = referenced_features.get(feature, set())
30424fe1f03SValentin Rothberg                paths.add(kfile)
30524fe1f03SValentin Rothberg                referenced_features[feature] = paths
30624fe1f03SValentin Rothberg
30724fe1f03SValentin Rothberg
30824fe1f03SValentin Rothbergif __name__ == "__main__":
30924fe1f03SValentin Rothberg    main()
310