17c5227afSValentin Rothberg#!/usr/bin/env python3
224fe1f03SValentin Rothberg
3b1a3f243SValentin Rothberg"""Find Kconfig symbols that are referenced but not defined."""
424fe1f03SValentin Rothberg
58e8e3331SValentin Rothberg# (c) 2014-2017 Valentin Rothberg <valentinrothberg@gmail.com>
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
1114390e31SValentin Rothbergimport argparse
121b2c8414SValentin Rothbergimport difflib
1324fe1f03SValentin Rothbergimport os
1424fe1f03SValentin Rothbergimport re
15e2042a8aSValentin Rothbergimport signal
16f175ba17SValentin Rothbergimport subprocess
17b1a3f243SValentin Rothbergimport sys
18e2042a8aSValentin Rothbergfrom multiprocessing import Pool, cpu_count
1924fe1f03SValentin Rothberg
20cc641d55SValentin Rothberg
21cc641d55SValentin Rothberg# regex expressions
2224fe1f03SValentin RothbergOPERATORS = r"&|\(|\)|\||\!"
23ef3f5543SValentin RothbergSYMBOL = r"(?:\w*[A-Z0-9]\w*){2,}"
24ef3f5543SValentin RothbergDEF = r"^\s*(?:menu){,1}config\s+(" + SYMBOL + r")\s*"
25ef3f5543SValentin RothbergEXPR = r"(?:" + OPERATORS + r"|\s|" + SYMBOL + r")+"
260bd38ae3SValentin RothbergDEFAULT = r"default\s+.*?(?:if\s.+){,1}"
273b28f4f2SValentin RothbergSTMT = r"^\s*(?:if|select|imply|depends\s+on|(?:" + DEFAULT + r"))\s+" + EXPR
28ef3f5543SValentin RothbergSOURCE_SYMBOL = r"(?:\W|\b)+[D]{,1}CONFIG_(" + SYMBOL + r")"
2924fe1f03SValentin Rothberg
30cc641d55SValentin Rothberg# regex objects
3124fe1f03SValentin RothbergREGEX_FILE_KCONFIG = re.compile(r".*Kconfig[\.\w+\-]*$")
32ef3f5543SValentin RothbergREGEX_SYMBOL = re.compile(r'(?!\B)' + SYMBOL + r'(?!\B)')
33ef3f5543SValentin RothbergREGEX_SOURCE_SYMBOL = re.compile(SOURCE_SYMBOL)
34cc641d55SValentin RothbergREGEX_KCONFIG_DEF = re.compile(DEF)
3524fe1f03SValentin RothbergREGEX_KCONFIG_EXPR = re.compile(EXPR)
3624fe1f03SValentin RothbergREGEX_KCONFIG_STMT = re.compile(STMT)
3724fe1f03SValentin RothbergREGEX_KCONFIG_HELP = re.compile(r"^\s+(help|---help---)\s*$")
38ef3f5543SValentin RothbergREGEX_FILTER_SYMBOLS = re.compile(r"[A-Za-z0-9]$")
390bd38ae3SValentin RothbergREGEX_NUMERIC = re.compile(r"0[xX][0-9a-fA-F]+|[0-9]+")
40e2042a8aSValentin RothbergREGEX_QUOTES = re.compile("(\"(.*?)\")")
4124fe1f03SValentin Rothberg
4224fe1f03SValentin Rothberg
43b1a3f243SValentin Rothbergdef parse_options():
44b1a3f243SValentin Rothberg    """The user interface of this module."""
4514390e31SValentin Rothberg    usage = "Run this tool to detect Kconfig symbols that are referenced but " \
4614390e31SValentin Rothberg            "not defined in Kconfig.  If no option is specified, "             \
4714390e31SValentin Rothberg            "checkkconfigsymbols defaults to check your current tree.  "       \
4814390e31SValentin Rothberg            "Please note that specifying commits will 'git reset --hard\' "    \
4914390e31SValentin Rothberg            "your current tree!  You may save uncommitted changes to avoid "   \
5014390e31SValentin Rothberg            "losing data."
51b1a3f243SValentin Rothberg
5214390e31SValentin Rothberg    parser = argparse.ArgumentParser(description=usage)
53b1a3f243SValentin Rothberg
5414390e31SValentin Rothberg    parser.add_argument('-c', '--commit', dest='commit', action='store',
55b1a3f243SValentin Rothberg                        default="",
5614390e31SValentin Rothberg                        help="check if the specified commit (hash) introduces "
5714390e31SValentin Rothberg                             "undefined Kconfig symbols")
58b1a3f243SValentin Rothberg
5914390e31SValentin Rothberg    parser.add_argument('-d', '--diff', dest='diff', action='store',
60b1a3f243SValentin Rothberg                        default="",
6114390e31SValentin Rothberg                        help="diff undefined symbols between two commits "
6214390e31SValentin Rothberg                             "(e.g., -d commmit1..commit2)")
63b1a3f243SValentin Rothberg
6414390e31SValentin Rothberg    parser.add_argument('-f', '--find', dest='find', action='store_true',
65a42fa92cSValentin Rothberg                        default=False,
6614390e31SValentin Rothberg                        help="find and show commits that may cause symbols to be "
6714390e31SValentin Rothberg                             "missing (required to run with --diff)")
68a42fa92cSValentin Rothberg
6914390e31SValentin Rothberg    parser.add_argument('-i', '--ignore', dest='ignore', action='store',
70cf132e4aSValentin Rothberg                        default="",
7114390e31SValentin Rothberg                        help="ignore files matching this Python regex "
7214390e31SValentin Rothberg                             "(e.g., -i '.*defconfig')")
73cf132e4aSValentin Rothberg
7414390e31SValentin Rothberg    parser.add_argument('-s', '--sim', dest='sim', action='store', default="",
7514390e31SValentin Rothberg                        help="print a list of max. 10 string-similar symbols")
761b2c8414SValentin Rothberg
7714390e31SValentin Rothberg    parser.add_argument('--force', dest='force', action='store_true',
78b1a3f243SValentin Rothberg                        default=False,
7914390e31SValentin Rothberg                        help="reset current Git tree even when it's dirty")
80b1a3f243SValentin Rothberg
8114390e31SValentin Rothberg    parser.add_argument('--no-color', dest='color', action='store_false',
824c73c088SAndrew Donnellan                        default=True,
8314390e31SValentin Rothberg                        help="don't print colored output (default when not "
8414390e31SValentin Rothberg                             "outputting to a terminal)")
854c73c088SAndrew Donnellan
8614390e31SValentin Rothberg    args = parser.parse_args()
87b1a3f243SValentin Rothberg
8814390e31SValentin Rothberg    if args.commit and args.diff:
89b1a3f243SValentin Rothberg        sys.exit("Please specify only one option at once.")
90b1a3f243SValentin Rothberg
910d18c192SValentin Rothberg    if args.diff and not re.match(r"^[\w\-\.\^]+\.\.[\w\-\.\^]+$", args.diff):
92b1a3f243SValentin Rothberg        sys.exit("Please specify valid input in the following format: "
9338cbfe4fSAndreas Ziegler                 "\'commit1..commit2\'")
94b1a3f243SValentin Rothberg
9514390e31SValentin Rothberg    if args.commit or args.diff:
9614390e31SValentin Rothberg        if not args.force and tree_is_dirty():
97b1a3f243SValentin Rothberg            sys.exit("The current Git tree is dirty (see 'git status').  "
98b1a3f243SValentin Rothberg                     "Running this script may\ndelete important data since it "
99b1a3f243SValentin Rothberg                     "calls 'git reset --hard' for some performance\nreasons. "
100b1a3f243SValentin Rothberg                     " Please run this script in a clean Git tree or pass "
101b1a3f243SValentin Rothberg                     "'--force' if you\nwant to ignore this warning and "
102b1a3f243SValentin Rothberg                     "continue.")
103b1a3f243SValentin Rothberg
10414390e31SValentin Rothberg    if args.commit:
10514390e31SValentin Rothberg        args.find = False
106a42fa92cSValentin Rothberg
10714390e31SValentin Rothberg    if args.ignore:
108cf132e4aSValentin Rothberg        try:
10914390e31SValentin Rothberg            re.match(args.ignore, "this/is/just/a/test.c")
110cf132e4aSValentin Rothberg        except:
111cf132e4aSValentin Rothberg            sys.exit("Please specify a valid Python regex.")
112cf132e4aSValentin Rothberg
11314390e31SValentin Rothberg    return args
114b1a3f243SValentin Rothberg
115b1a3f243SValentin Rothberg
11624fe1f03SValentin Rothbergdef main():
11724fe1f03SValentin Rothberg    """Main function of this module."""
11814390e31SValentin Rothberg    args = parse_options()
119b1a3f243SValentin Rothberg
12036c79c7fSValentin Rothberg    global COLOR
12136c79c7fSValentin Rothberg    COLOR = args.color and sys.stdout.isatty()
1224c73c088SAndrew Donnellan
12314390e31SValentin Rothberg    if args.sim and not args.commit and not args.diff:
12414390e31SValentin Rothberg        sims = find_sims(args.sim, args.ignore)
1251b2c8414SValentin Rothberg        if sims:
1267c5227afSValentin Rothberg            print("%s: %s" % (yel("Similar symbols"), ', '.join(sims)))
1271b2c8414SValentin Rothberg        else:
1287c5227afSValentin Rothberg            print("%s: no similar symbols found" % yel("Similar symbols"))
1291b2c8414SValentin Rothberg        sys.exit(0)
1301b2c8414SValentin Rothberg
1311b2c8414SValentin Rothberg    # dictionary of (un)defined symbols
1321b2c8414SValentin Rothberg    defined = {}
1331b2c8414SValentin Rothberg    undefined = {}
1341b2c8414SValentin Rothberg
13514390e31SValentin Rothberg    if args.commit or args.diff:
136b1a3f243SValentin Rothberg        head = get_head()
137b1a3f243SValentin Rothberg
138b1a3f243SValentin Rothberg        # get commit range
139b1a3f243SValentin Rothberg        commit_a = None
140b1a3f243SValentin Rothberg        commit_b = None
14114390e31SValentin Rothberg        if args.commit:
14214390e31SValentin Rothberg            commit_a = args.commit + "~"
14314390e31SValentin Rothberg            commit_b = args.commit
14414390e31SValentin Rothberg        elif args.diff:
14514390e31SValentin Rothberg            split = args.diff.split("..")
146b1a3f243SValentin Rothberg            commit_a = split[0]
147b1a3f243SValentin Rothberg            commit_b = split[1]
148b1a3f243SValentin Rothberg            undefined_a = {}
149b1a3f243SValentin Rothberg            undefined_b = {}
150b1a3f243SValentin Rothberg
151b1a3f243SValentin Rothberg        # get undefined items before the commit
1522f9cc12bSValentin Rothberg        reset(commit_a)
15314390e31SValentin Rothberg        undefined_a, _ = check_symbols(args.ignore)
154b1a3f243SValentin Rothberg
155b1a3f243SValentin Rothberg        # get undefined items for the commit
1562f9cc12bSValentin Rothberg        reset(commit_b)
15714390e31SValentin Rothberg        undefined_b, defined = check_symbols(args.ignore)
158b1a3f243SValentin Rothberg
159b1a3f243SValentin Rothberg        # report cases that are present for the commit but not before
160ef3f5543SValentin Rothberg        for symbol in sorted(undefined_b):
161ef3f5543SValentin Rothberg            # symbol has not been undefined before
162ef3f5543SValentin Rothberg            if symbol not in undefined_a:
163ef3f5543SValentin Rothberg                files = sorted(undefined_b.get(symbol))
164ef3f5543SValentin Rothberg                undefined[symbol] = files
165ef3f5543SValentin Rothberg            # check if there are new files that reference the undefined symbol
166b1a3f243SValentin Rothberg            else:
167ef3f5543SValentin Rothberg                files = sorted(undefined_b.get(symbol) -
168ef3f5543SValentin Rothberg                               undefined_a.get(symbol))
169b1a3f243SValentin Rothberg                if files:
170ef3f5543SValentin Rothberg                    undefined[symbol] = files
171b1a3f243SValentin Rothberg
172b1a3f243SValentin Rothberg        # reset to head
1732f9cc12bSValentin Rothberg        reset(head)
174b1a3f243SValentin Rothberg
175b1a3f243SValentin Rothberg    # default to check the entire tree
176b1a3f243SValentin Rothberg    else:
17714390e31SValentin Rothberg        undefined, defined = check_symbols(args.ignore)
1781b2c8414SValentin Rothberg
1791b2c8414SValentin Rothberg    # now print the output
180ef3f5543SValentin Rothberg    for symbol in sorted(undefined):
181ef3f5543SValentin Rothberg        print(red(symbol))
1821b2c8414SValentin Rothberg
183ef3f5543SValentin Rothberg        files = sorted(undefined.get(symbol))
1847c5227afSValentin Rothberg        print("%s: %s" % (yel("Referencing files"), ", ".join(files)))
1851b2c8414SValentin Rothberg
186ef3f5543SValentin Rothberg        sims = find_sims(symbol, args.ignore, defined)
1871b2c8414SValentin Rothberg        sims_out = yel("Similar symbols")
1881b2c8414SValentin Rothberg        if sims:
1897c5227afSValentin Rothberg            print("%s: %s" % (sims_out, ', '.join(sims)))
1901b2c8414SValentin Rothberg        else:
1917c5227afSValentin Rothberg            print("%s: %s" % (sims_out, "no similar symbols found"))
1921b2c8414SValentin Rothberg
19314390e31SValentin Rothberg        if args.find:
1947c5227afSValentin Rothberg            print("%s:" % yel("Commits changing symbol"))
195ef3f5543SValentin Rothberg            commits = find_commits(symbol, args.diff)
1961b2c8414SValentin Rothberg            if commits:
1971b2c8414SValentin Rothberg                for commit in commits:
1981b2c8414SValentin Rothberg                    commit = commit.split(" ", 1)
1997c5227afSValentin Rothberg                    print("\t- %s (\"%s\")" % (yel(commit[0]), commit[1]))
2001b2c8414SValentin Rothberg            else:
2017c5227afSValentin Rothberg                print("\t- no commit found")
2027c5227afSValentin Rothberg        print()  # new line
203c7455663SValentin Rothberg
204c7455663SValentin Rothberg
2052f9cc12bSValentin Rothbergdef reset(commit):
2062f9cc12bSValentin Rothberg    """Reset current git tree to %commit."""
2072f9cc12bSValentin Rothberg    execute(["git", "reset", "--hard", commit])
2082f9cc12bSValentin Rothberg
2092f9cc12bSValentin Rothberg
210c7455663SValentin Rothbergdef yel(string):
211c7455663SValentin Rothberg    """
212c7455663SValentin Rothberg    Color %string yellow.
213c7455663SValentin Rothberg    """
21436c79c7fSValentin Rothberg    return "\033[33m%s\033[0m" % string if COLOR else string
215c7455663SValentin Rothberg
216c7455663SValentin Rothberg
217c7455663SValentin Rothbergdef red(string):
218c7455663SValentin Rothberg    """
219c7455663SValentin Rothberg    Color %string red.
220c7455663SValentin Rothberg    """
22136c79c7fSValentin Rothberg    return "\033[31m%s\033[0m" % string if COLOR else string
222b1a3f243SValentin Rothberg
223b1a3f243SValentin Rothberg
224b1a3f243SValentin Rothbergdef execute(cmd):
225b1a3f243SValentin Rothberg    """Execute %cmd and return stdout.  Exit in case of error."""
226f175ba17SValentin Rothberg    try:
2272f9cc12bSValentin Rothberg        stdout = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=False)
2287c5227afSValentin Rothberg        stdout = stdout.decode(errors='replace')
229f175ba17SValentin Rothberg    except subprocess.CalledProcessError as fail:
2302f9cc12bSValentin Rothberg        exit(fail)
231b1a3f243SValentin Rothberg    return stdout
232b1a3f243SValentin Rothberg
233b1a3f243SValentin Rothberg
234a42fa92cSValentin Rothbergdef find_commits(symbol, diff):
235a42fa92cSValentin Rothberg    """Find commits changing %symbol in the given range of %diff."""
2362f9cc12bSValentin Rothberg    commits = execute(["git", "log", "--pretty=oneline",
2372f9cc12bSValentin Rothberg                       "--abbrev-commit", "-G",
2382f9cc12bSValentin Rothberg                       symbol, diff])
2391b2c8414SValentin Rothberg    return [x for x in commits.split("\n") if x]
240a42fa92cSValentin Rothberg
241a42fa92cSValentin Rothberg
242b1a3f243SValentin Rothbergdef tree_is_dirty():
243b1a3f243SValentin Rothberg    """Return true if the current working tree is dirty (i.e., if any file has
244b1a3f243SValentin Rothberg    been added, deleted, modified, renamed or copied but not committed)."""
2452f9cc12bSValentin Rothberg    stdout = execute(["git", "status", "--porcelain"])
246b1a3f243SValentin Rothberg    for line in stdout:
247b1a3f243SValentin Rothberg        if re.findall(r"[URMADC]{1}", line[:2]):
248b1a3f243SValentin Rothberg            return True
249b1a3f243SValentin Rothberg    return False
250b1a3f243SValentin Rothberg
251b1a3f243SValentin Rothberg
252b1a3f243SValentin Rothbergdef get_head():
253b1a3f243SValentin Rothberg    """Return commit hash of current HEAD."""
2542f9cc12bSValentin Rothberg    stdout = execute(["git", "rev-parse", "HEAD"])
255b1a3f243SValentin Rothberg    return stdout.strip('\n')
256b1a3f243SValentin Rothberg
257b1a3f243SValentin Rothberg
258e2042a8aSValentin Rothbergdef partition(lst, size):
259e2042a8aSValentin Rothberg    """Partition list @lst into eveni-sized lists of size @size."""
2607c5227afSValentin Rothberg    return [lst[i::size] for i in range(size)]
261e2042a8aSValentin Rothberg
262e2042a8aSValentin Rothberg
263e2042a8aSValentin Rothbergdef init_worker():
264e2042a8aSValentin Rothberg    """Set signal handler to ignore SIGINT."""
265e2042a8aSValentin Rothberg    signal.signal(signal.SIGINT, signal.SIG_IGN)
266e2042a8aSValentin Rothberg
267e2042a8aSValentin Rothberg
2681b2c8414SValentin Rothbergdef find_sims(symbol, ignore, defined=[]):
2691b2c8414SValentin Rothberg    """Return a list of max. ten Kconfig symbols that are string-similar to
2701b2c8414SValentin Rothberg    @symbol."""
2711b2c8414SValentin Rothberg    if defined:
2728e8e3331SValentin Rothberg        return difflib.get_close_matches(symbol, set(defined), 10)
2731b2c8414SValentin Rothberg
2741b2c8414SValentin Rothberg    pool = Pool(cpu_count(), init_worker)
2751b2c8414SValentin Rothberg    kfiles = []
2761b2c8414SValentin Rothberg    for gitfile in get_files():
2771b2c8414SValentin Rothberg        if REGEX_FILE_KCONFIG.match(gitfile):
2781b2c8414SValentin Rothberg            kfiles.append(gitfile)
2791b2c8414SValentin Rothberg
2801b2c8414SValentin Rothberg    arglist = []
2811b2c8414SValentin Rothberg    for part in partition(kfiles, cpu_count()):
2821b2c8414SValentin Rothberg        arglist.append((part, ignore))
2831b2c8414SValentin Rothberg
2841b2c8414SValentin Rothberg    for res in pool.map(parse_kconfig_files, arglist):
2851b2c8414SValentin Rothberg        defined.extend(res[0])
2861b2c8414SValentin Rothberg
2878e8e3331SValentin Rothberg    return difflib.get_close_matches(symbol, set(defined), 10)
2881b2c8414SValentin Rothberg
2891b2c8414SValentin Rothberg
2901b2c8414SValentin Rothbergdef get_files():
2911b2c8414SValentin Rothberg    """Return a list of all files in the current git directory."""
2921b2c8414SValentin Rothberg    # use 'git ls-files' to get the worklist
2932f9cc12bSValentin Rothberg    stdout = execute(["git", "ls-files"])
2941b2c8414SValentin Rothberg    if len(stdout) > 0 and stdout[-1] == "\n":
2951b2c8414SValentin Rothberg        stdout = stdout[:-1]
2961b2c8414SValentin Rothberg
2971b2c8414SValentin Rothberg    files = []
2981b2c8414SValentin Rothberg    for gitfile in stdout.rsplit("\n"):
2991b2c8414SValentin Rothberg        if ".git" in gitfile or "ChangeLog" in gitfile or      \
3001b2c8414SValentin Rothberg                ".log" in gitfile or os.path.isdir(gitfile) or \
3011b2c8414SValentin Rothberg                gitfile.startswith("tools/"):
3021b2c8414SValentin Rothberg            continue
3031b2c8414SValentin Rothberg        files.append(gitfile)
3041b2c8414SValentin Rothberg    return files
3051b2c8414SValentin Rothberg
3061b2c8414SValentin Rothberg
307cf132e4aSValentin Rothbergdef check_symbols(ignore):
308b1a3f243SValentin Rothberg    """Find undefined Kconfig symbols and return a dict with the symbol as key
309cf132e4aSValentin Rothberg    and a list of referencing files as value.  Files matching %ignore are not
310cf132e4aSValentin Rothberg    checked for undefined symbols."""
311e2042a8aSValentin Rothberg    pool = Pool(cpu_count(), init_worker)
312e2042a8aSValentin Rothberg    try:
313e2042a8aSValentin Rothberg        return check_symbols_helper(pool, ignore)
314e2042a8aSValentin Rothberg    except KeyboardInterrupt:
315e2042a8aSValentin Rothberg        pool.terminate()
316e2042a8aSValentin Rothberg        pool.join()
317e2042a8aSValentin Rothberg        sys.exit(1)
318e2042a8aSValentin Rothberg
319e2042a8aSValentin Rothberg
320e2042a8aSValentin Rothbergdef check_symbols_helper(pool, ignore):
321e2042a8aSValentin Rothberg    """Helper method for check_symbols().  Used to catch keyboard interrupts in
322e2042a8aSValentin Rothberg    check_symbols() in order to properly terminate running worker processes."""
32324fe1f03SValentin Rothberg    source_files = []
32424fe1f03SValentin Rothberg    kconfig_files = []
325ef3f5543SValentin Rothberg    defined_symbols = []
326ef3f5543SValentin Rothberg    referenced_symbols = dict()  # {file: [symbols]}
32724fe1f03SValentin Rothberg
3281b2c8414SValentin Rothberg    for gitfile in get_files():
32924fe1f03SValentin Rothberg        if REGEX_FILE_KCONFIG.match(gitfile):
33024fe1f03SValentin Rothberg            kconfig_files.append(gitfile)
33124fe1f03SValentin Rothberg        else:
332e2042a8aSValentin Rothberg            if ignore and not re.match(ignore, gitfile):
333e2042a8aSValentin Rothberg                continue
334e2042a8aSValentin Rothberg            # add source files that do not match the ignore pattern
33524fe1f03SValentin Rothberg            source_files.append(gitfile)
33624fe1f03SValentin Rothberg
337e2042a8aSValentin Rothberg    # parse source files
338e2042a8aSValentin Rothberg    arglist = partition(source_files, cpu_count())
339e2042a8aSValentin Rothberg    for res in pool.map(parse_source_files, arglist):
340ef3f5543SValentin Rothberg        referenced_symbols.update(res)
34124fe1f03SValentin Rothberg
342e2042a8aSValentin Rothberg    # parse kconfig files
343e2042a8aSValentin Rothberg    arglist = []
344e2042a8aSValentin Rothberg    for part in partition(kconfig_files, cpu_count()):
345e2042a8aSValentin Rothberg        arglist.append((part, ignore))
346e2042a8aSValentin Rothberg    for res in pool.map(parse_kconfig_files, arglist):
347ef3f5543SValentin Rothberg        defined_symbols.extend(res[0])
348ef3f5543SValentin Rothberg        referenced_symbols.update(res[1])
349ef3f5543SValentin Rothberg    defined_symbols = set(defined_symbols)
350e2042a8aSValentin Rothberg
351ef3f5543SValentin Rothberg    # inverse mapping of referenced_symbols to dict(symbol: [files])
352e2042a8aSValentin Rothberg    inv_map = dict()
353ef3f5543SValentin Rothberg    for _file, symbols in referenced_symbols.items():
354ef3f5543SValentin Rothberg        for symbol in symbols:
355ef3f5543SValentin Rothberg            inv_map[symbol] = inv_map.get(symbol, set())
356ef3f5543SValentin Rothberg            inv_map[symbol].add(_file)
357ef3f5543SValentin Rothberg    referenced_symbols = inv_map
35824fe1f03SValentin Rothberg
359ef3f5543SValentin Rothberg    undefined = {}  # {symbol: [files]}
360ef3f5543SValentin Rothberg    for symbol in sorted(referenced_symbols):
361cc641d55SValentin Rothberg        # filter some false positives
362ef3f5543SValentin Rothberg        if symbol == "FOO" or symbol == "BAR" or \
363ef3f5543SValentin Rothberg                symbol == "FOO_BAR" or symbol == "XXX":
364cc641d55SValentin Rothberg            continue
365ef3f5543SValentin Rothberg        if symbol not in defined_symbols:
366ef3f5543SValentin Rothberg            if symbol.endswith("_MODULE"):
367cc641d55SValentin Rothberg                # avoid false positives for kernel modules
368ef3f5543SValentin Rothberg                if symbol[:-len("_MODULE")] in defined_symbols:
36924fe1f03SValentin Rothberg                    continue
370ef3f5543SValentin Rothberg            undefined[symbol] = referenced_symbols.get(symbol)
371ef3f5543SValentin Rothberg    return undefined, defined_symbols
37224fe1f03SValentin Rothberg
37324fe1f03SValentin Rothberg
374e2042a8aSValentin Rothbergdef parse_source_files(source_files):
375e2042a8aSValentin Rothberg    """Parse each source file in @source_files and return dictionary with source
376e2042a8aSValentin Rothberg    files as keys and lists of references Kconfig symbols as values."""
377ef3f5543SValentin Rothberg    referenced_symbols = dict()
378e2042a8aSValentin Rothberg    for sfile in source_files:
379ef3f5543SValentin Rothberg        referenced_symbols[sfile] = parse_source_file(sfile)
380ef3f5543SValentin Rothberg    return referenced_symbols
381e2042a8aSValentin Rothberg
382e2042a8aSValentin Rothberg
383e2042a8aSValentin Rothbergdef parse_source_file(sfile):
384ef3f5543SValentin Rothberg    """Parse @sfile and return a list of referenced Kconfig symbols."""
38524fe1f03SValentin Rothberg    lines = []
386e2042a8aSValentin Rothberg    references = []
387e2042a8aSValentin Rothberg
388e2042a8aSValentin Rothberg    if not os.path.exists(sfile):
389e2042a8aSValentin Rothberg        return references
390e2042a8aSValentin Rothberg
3917c5227afSValentin Rothberg    with open(sfile, "r", encoding='utf-8', errors='replace') as stream:
39224fe1f03SValentin Rothberg        lines = stream.readlines()
39324fe1f03SValentin Rothberg
39424fe1f03SValentin Rothberg    for line in lines:
39536c79c7fSValentin Rothberg        if "CONFIG_" not in line:
39624fe1f03SValentin Rothberg            continue
397ef3f5543SValentin Rothberg        symbols = REGEX_SOURCE_SYMBOL.findall(line)
398ef3f5543SValentin Rothberg        for symbol in symbols:
399ef3f5543SValentin Rothberg            if not REGEX_FILTER_SYMBOLS.search(symbol):
40024fe1f03SValentin Rothberg                continue
401ef3f5543SValentin Rothberg            references.append(symbol)
402e2042a8aSValentin Rothberg
403e2042a8aSValentin Rothberg    return references
40424fe1f03SValentin Rothberg
40524fe1f03SValentin Rothberg
406ef3f5543SValentin Rothbergdef get_symbols_in_line(line):
407ef3f5543SValentin Rothberg    """Return mentioned Kconfig symbols in @line."""
408ef3f5543SValentin Rothberg    return REGEX_SYMBOL.findall(line)
40924fe1f03SValentin Rothberg
41024fe1f03SValentin Rothberg
411e2042a8aSValentin Rothbergdef parse_kconfig_files(args):
412e2042a8aSValentin Rothberg    """Parse kconfig files and return tuple of defined and references Kconfig
413e2042a8aSValentin Rothberg    symbols.  Note, @args is a tuple of a list of files and the @ignore
414e2042a8aSValentin Rothberg    pattern."""
415e2042a8aSValentin Rothberg    kconfig_files = args[0]
416e2042a8aSValentin Rothberg    ignore = args[1]
417ef3f5543SValentin Rothberg    defined_symbols = []
418ef3f5543SValentin Rothberg    referenced_symbols = dict()
419e2042a8aSValentin Rothberg
420e2042a8aSValentin Rothberg    for kfile in kconfig_files:
421e2042a8aSValentin Rothberg        defined, references = parse_kconfig_file(kfile)
422ef3f5543SValentin Rothberg        defined_symbols.extend(defined)
423e2042a8aSValentin Rothberg        if ignore and re.match(ignore, kfile):
424e2042a8aSValentin Rothberg            # do not collect references for files that match the ignore pattern
425e2042a8aSValentin Rothberg            continue
426ef3f5543SValentin Rothberg        referenced_symbols[kfile] = references
427ef3f5543SValentin Rothberg    return (defined_symbols, referenced_symbols)
428e2042a8aSValentin Rothberg
429e2042a8aSValentin Rothberg
430e2042a8aSValentin Rothbergdef parse_kconfig_file(kfile):
431ef3f5543SValentin Rothberg    """Parse @kfile and update symbol definitions and references."""
43224fe1f03SValentin Rothberg    lines = []
433e2042a8aSValentin Rothberg    defined = []
434e2042a8aSValentin Rothberg    references = []
43524fe1f03SValentin Rothberg    skip = False
43624fe1f03SValentin Rothberg
437e2042a8aSValentin Rothberg    if not os.path.exists(kfile):
438e2042a8aSValentin Rothberg        return defined, references
439e2042a8aSValentin Rothberg
4407c5227afSValentin Rothberg    with open(kfile, "r", encoding='utf-8', errors='replace') as stream:
44124fe1f03SValentin Rothberg        lines = stream.readlines()
44224fe1f03SValentin Rothberg
44324fe1f03SValentin Rothberg    for i in range(len(lines)):
44424fe1f03SValentin Rothberg        line = lines[i]
44524fe1f03SValentin Rothberg        line = line.strip('\n')
446cc641d55SValentin Rothberg        line = line.split("#")[0]  # ignore comments
44724fe1f03SValentin Rothberg
44824fe1f03SValentin Rothberg        if REGEX_KCONFIG_DEF.match(line):
449ef3f5543SValentin Rothberg            symbol_def = REGEX_KCONFIG_DEF.findall(line)
450ef3f5543SValentin Rothberg            defined.append(symbol_def[0])
45124fe1f03SValentin Rothberg            skip = False
45224fe1f03SValentin Rothberg        elif REGEX_KCONFIG_HELP.match(line):
45324fe1f03SValentin Rothberg            skip = True
45424fe1f03SValentin Rothberg        elif skip:
455cc641d55SValentin Rothberg            # ignore content of help messages
45624fe1f03SValentin Rothberg            pass
45724fe1f03SValentin Rothberg        elif REGEX_KCONFIG_STMT.match(line):
458e2042a8aSValentin Rothberg            line = REGEX_QUOTES.sub("", line)
459ef3f5543SValentin Rothberg            symbols = get_symbols_in_line(line)
460cc641d55SValentin Rothberg            # multi-line statements
46124fe1f03SValentin Rothberg            while line.endswith("\\"):
46224fe1f03SValentin Rothberg                i += 1
46324fe1f03SValentin Rothberg                line = lines[i]
46424fe1f03SValentin Rothberg                line = line.strip('\n')
465ef3f5543SValentin Rothberg                symbols.extend(get_symbols_in_line(line))
466ef3f5543SValentin Rothberg            for symbol in set(symbols):
467ef3f5543SValentin Rothberg                if REGEX_NUMERIC.match(symbol):
4680bd38ae3SValentin Rothberg                    # ignore numeric values
4690bd38ae3SValentin Rothberg                    continue
470ef3f5543SValentin Rothberg                references.append(symbol)
471e2042a8aSValentin Rothberg
472e2042a8aSValentin Rothberg    return defined, references
47324fe1f03SValentin Rothberg
47424fe1f03SValentin Rothberg
47524fe1f03SValentin Rothbergif __name__ == "__main__":
47624fe1f03SValentin Rothberg    main()
477