17c5227afSValentin Rothberg#!/usr/bin/env python3
24f19048fSThomas Gleixner# SPDX-License-Identifier: GPL-2.0-only
324fe1f03SValentin Rothberg
4b1a3f243SValentin Rothberg"""Find Kconfig symbols that are referenced but not defined."""
524fe1f03SValentin Rothberg
68e8e3331SValentin Rothberg# (c) 2014-2017 Valentin Rothberg <valentinrothberg@gmail.com>
7cc641d55SValentin Rothberg# (c) 2014 Stefan Hengelein <stefan.hengelein@fau.de>
824fe1f03SValentin Rothberg#
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)
37ef3f5543SValentin RothbergREGEX_FILTER_SYMBOLS = re.compile(r"[A-Za-z0-9]$")
380bd38ae3SValentin RothbergREGEX_NUMERIC = re.compile(r"0[xX][0-9a-fA-F]+|[0-9]+")
39e2042a8aSValentin RothbergREGEX_QUOTES = re.compile("(\"(.*?)\")")
4024fe1f03SValentin Rothberg
4124fe1f03SValentin Rothberg
42b1a3f243SValentin Rothbergdef parse_options():
43b1a3f243SValentin Rothberg    """The user interface of this module."""
4414390e31SValentin Rothberg    usage = "Run this tool to detect Kconfig symbols that are referenced but " \
4514390e31SValentin Rothberg            "not defined in Kconfig.  If no option is specified, "             \
4614390e31SValentin Rothberg            "checkkconfigsymbols defaults to check your current tree.  "       \
4714390e31SValentin Rothberg            "Please note that specifying commits will 'git reset --hard\' "    \
4814390e31SValentin Rothberg            "your current tree!  You may save uncommitted changes to avoid "   \
4914390e31SValentin Rothberg            "losing data."
50b1a3f243SValentin Rothberg
5114390e31SValentin Rothberg    parser = argparse.ArgumentParser(description=usage)
52b1a3f243SValentin Rothberg
5314390e31SValentin Rothberg    parser.add_argument('-c', '--commit', dest='commit', action='store',
54b1a3f243SValentin Rothberg                        default="",
5514390e31SValentin Rothberg                        help="check if the specified commit (hash) introduces "
5614390e31SValentin Rothberg                             "undefined Kconfig symbols")
57b1a3f243SValentin Rothberg
5814390e31SValentin Rothberg    parser.add_argument('-d', '--diff', dest='diff', action='store',
59b1a3f243SValentin Rothberg                        default="",
6014390e31SValentin Rothberg                        help="diff undefined symbols between two commits "
6114390e31SValentin Rothberg                             "(e.g., -d commmit1..commit2)")
62b1a3f243SValentin Rothberg
6314390e31SValentin Rothberg    parser.add_argument('-f', '--find', dest='find', action='store_true',
64a42fa92cSValentin Rothberg                        default=False,
6514390e31SValentin Rothberg                        help="find and show commits that may cause symbols to be "
6614390e31SValentin Rothberg                             "missing (required to run with --diff)")
67a42fa92cSValentin Rothberg
6814390e31SValentin Rothberg    parser.add_argument('-i', '--ignore', dest='ignore', action='store',
69cf132e4aSValentin Rothberg                        default="",
7014390e31SValentin Rothberg                        help="ignore files matching this Python regex "
7114390e31SValentin Rothberg                             "(e.g., -i '.*defconfig')")
72cf132e4aSValentin Rothberg
7314390e31SValentin Rothberg    parser.add_argument('-s', '--sim', dest='sim', action='store', default="",
7414390e31SValentin Rothberg                        help="print a list of max. 10 string-similar symbols")
751b2c8414SValentin Rothberg
7614390e31SValentin Rothberg    parser.add_argument('--force', dest='force', action='store_true',
77b1a3f243SValentin Rothberg                        default=False,
7814390e31SValentin Rothberg                        help="reset current Git tree even when it's dirty")
79b1a3f243SValentin Rothberg
8014390e31SValentin Rothberg    parser.add_argument('--no-color', dest='color', action='store_false',
814c73c088SAndrew Donnellan                        default=True,
8214390e31SValentin Rothberg                        help="don't print colored output (default when not "
8314390e31SValentin Rothberg                             "outputting to a terminal)")
844c73c088SAndrew Donnellan
8514390e31SValentin Rothberg    args = parser.parse_args()
86b1a3f243SValentin Rothberg
8714390e31SValentin Rothberg    if args.commit and args.diff:
88b1a3f243SValentin Rothberg        sys.exit("Please specify only one option at once.")
89b1a3f243SValentin Rothberg
900d18c192SValentin Rothberg    if args.diff and not re.match(r"^[\w\-\.\^]+\.\.[\w\-\.\^]+$", args.diff):
91b1a3f243SValentin Rothberg        sys.exit("Please specify valid input in the following format: "
9238cbfe4fSAndreas Ziegler                 "\'commit1..commit2\'")
93b1a3f243SValentin Rothberg
9414390e31SValentin Rothberg    if args.commit or args.diff:
9514390e31SValentin Rothberg        if not args.force and tree_is_dirty():
96b1a3f243SValentin Rothberg            sys.exit("The current Git tree is dirty (see 'git status').  "
97b1a3f243SValentin Rothberg                     "Running this script may\ndelete important data since it "
98b1a3f243SValentin Rothberg                     "calls 'git reset --hard' for some performance\nreasons. "
99b1a3f243SValentin Rothberg                     " Please run this script in a clean Git tree or pass "
100b1a3f243SValentin Rothberg                     "'--force' if you\nwant to ignore this warning and "
101b1a3f243SValentin Rothberg                     "continue.")
102b1a3f243SValentin Rothberg
10314390e31SValentin Rothberg    if args.commit:
104d62d5aedSAriel Marcovitch        if args.commit.startswith('HEAD'):
105d62d5aedSAriel Marcovitch            sys.exit("The --commit option can't use the HEAD ref")
106d62d5aedSAriel Marcovitch
10714390e31SValentin Rothberg        args.find = False
108a42fa92cSValentin Rothberg
10914390e31SValentin Rothberg    if args.ignore:
110cf132e4aSValentin Rothberg        try:
11114390e31SValentin Rothberg            re.match(args.ignore, "this/is/just/a/test.c")
112cf132e4aSValentin Rothberg        except:
113cf132e4aSValentin Rothberg            sys.exit("Please specify a valid Python regex.")
114cf132e4aSValentin Rothberg
11514390e31SValentin Rothberg    return args
116b1a3f243SValentin Rothberg
117b1a3f243SValentin Rothberg
118*87c7ee67SMasahiro Yamadadef print_undefined_symbols():
11924fe1f03SValentin Rothberg    """Main function of this module."""
12014390e31SValentin Rothberg    args = parse_options()
121b1a3f243SValentin Rothberg
12236c79c7fSValentin Rothberg    global COLOR
12336c79c7fSValentin Rothberg    COLOR = args.color and sys.stdout.isatty()
1244c73c088SAndrew Donnellan
12514390e31SValentin Rothberg    if args.sim and not args.commit and not args.diff:
12614390e31SValentin Rothberg        sims = find_sims(args.sim, args.ignore)
1271b2c8414SValentin Rothberg        if sims:
1287c5227afSValentin Rothberg            print("%s: %s" % (yel("Similar symbols"), ', '.join(sims)))
1291b2c8414SValentin Rothberg        else:
1307c5227afSValentin Rothberg            print("%s: no similar symbols found" % yel("Similar symbols"))
1311b2c8414SValentin Rothberg        sys.exit(0)
1321b2c8414SValentin Rothberg
1331b2c8414SValentin Rothberg    # dictionary of (un)defined symbols
1341b2c8414SValentin Rothberg    defined = {}
1351b2c8414SValentin Rothberg    undefined = {}
1361b2c8414SValentin Rothberg
13714390e31SValentin Rothberg    if args.commit or args.diff:
138b1a3f243SValentin Rothberg        head = get_head()
139b1a3f243SValentin Rothberg
140b1a3f243SValentin Rothberg        # get commit range
141b1a3f243SValentin Rothberg        commit_a = None
142b1a3f243SValentin Rothberg        commit_b = None
14314390e31SValentin Rothberg        if args.commit:
14414390e31SValentin Rothberg            commit_a = args.commit + "~"
14514390e31SValentin Rothberg            commit_b = args.commit
14614390e31SValentin Rothberg        elif args.diff:
14714390e31SValentin Rothberg            split = args.diff.split("..")
148b1a3f243SValentin Rothberg            commit_a = split[0]
149b1a3f243SValentin Rothberg            commit_b = split[1]
150b1a3f243SValentin Rothberg            undefined_a = {}
151b1a3f243SValentin Rothberg            undefined_b = {}
152b1a3f243SValentin Rothberg
153b1a3f243SValentin Rothberg        # get undefined items before the commit
1542f9cc12bSValentin Rothberg        reset(commit_a)
15514390e31SValentin Rothberg        undefined_a, _ = check_symbols(args.ignore)
156b1a3f243SValentin Rothberg
157b1a3f243SValentin Rothberg        # get undefined items for the commit
1582f9cc12bSValentin Rothberg        reset(commit_b)
15914390e31SValentin Rothberg        undefined_b, defined = check_symbols(args.ignore)
160b1a3f243SValentin Rothberg
161b1a3f243SValentin Rothberg        # report cases that are present for the commit but not before
162ef3f5543SValentin Rothberg        for symbol in sorted(undefined_b):
163ef3f5543SValentin Rothberg            # symbol has not been undefined before
164ef3f5543SValentin Rothberg            if symbol not in undefined_a:
165ef3f5543SValentin Rothberg                files = sorted(undefined_b.get(symbol))
166ef3f5543SValentin Rothberg                undefined[symbol] = files
167ef3f5543SValentin Rothberg            # check if there are new files that reference the undefined symbol
168b1a3f243SValentin Rothberg            else:
169ef3f5543SValentin Rothberg                files = sorted(undefined_b.get(symbol) -
170ef3f5543SValentin Rothberg                               undefined_a.get(symbol))
171b1a3f243SValentin Rothberg                if files:
172ef3f5543SValentin Rothberg                    undefined[symbol] = files
173b1a3f243SValentin Rothberg
174b1a3f243SValentin Rothberg        # reset to head
1752f9cc12bSValentin Rothberg        reset(head)
176b1a3f243SValentin Rothberg
177b1a3f243SValentin Rothberg    # default to check the entire tree
178b1a3f243SValentin Rothberg    else:
17914390e31SValentin Rothberg        undefined, defined = check_symbols(args.ignore)
1801b2c8414SValentin Rothberg
1811b2c8414SValentin Rothberg    # now print the output
182ef3f5543SValentin Rothberg    for symbol in sorted(undefined):
183ef3f5543SValentin Rothberg        print(red(symbol))
1841b2c8414SValentin Rothberg
185ef3f5543SValentin Rothberg        files = sorted(undefined.get(symbol))
1867c5227afSValentin Rothberg        print("%s: %s" % (yel("Referencing files"), ", ".join(files)))
1871b2c8414SValentin Rothberg
188ef3f5543SValentin Rothberg        sims = find_sims(symbol, args.ignore, defined)
1891b2c8414SValentin Rothberg        sims_out = yel("Similar symbols")
1901b2c8414SValentin Rothberg        if sims:
1917c5227afSValentin Rothberg            print("%s: %s" % (sims_out, ', '.join(sims)))
1921b2c8414SValentin Rothberg        else:
1937c5227afSValentin Rothberg            print("%s: %s" % (sims_out, "no similar symbols found"))
1941b2c8414SValentin Rothberg
19514390e31SValentin Rothberg        if args.find:
1967c5227afSValentin Rothberg            print("%s:" % yel("Commits changing symbol"))
197ef3f5543SValentin Rothberg            commits = find_commits(symbol, args.diff)
1981b2c8414SValentin Rothberg            if commits:
1991b2c8414SValentin Rothberg                for commit in commits:
2001b2c8414SValentin Rothberg                    commit = commit.split(" ", 1)
2017c5227afSValentin Rothberg                    print("\t- %s (\"%s\")" % (yel(commit[0]), commit[1]))
2021b2c8414SValentin Rothberg            else:
2037c5227afSValentin Rothberg                print("\t- no commit found")
2047c5227afSValentin Rothberg        print()  # new line
205c7455663SValentin Rothberg
206c7455663SValentin Rothberg
2072f9cc12bSValentin Rothbergdef reset(commit):
2082f9cc12bSValentin Rothberg    """Reset current git tree to %commit."""
2092f9cc12bSValentin Rothberg    execute(["git", "reset", "--hard", commit])
2102f9cc12bSValentin Rothberg
2112f9cc12bSValentin Rothberg
212c7455663SValentin Rothbergdef yel(string):
213c7455663SValentin Rothberg    """
214c7455663SValentin Rothberg    Color %string yellow.
215c7455663SValentin Rothberg    """
21636c79c7fSValentin Rothberg    return "\033[33m%s\033[0m" % string if COLOR else string
217c7455663SValentin Rothberg
218c7455663SValentin Rothberg
219c7455663SValentin Rothbergdef red(string):
220c7455663SValentin Rothberg    """
221c7455663SValentin Rothberg    Color %string red.
222c7455663SValentin Rothberg    """
22336c79c7fSValentin Rothberg    return "\033[31m%s\033[0m" % string if COLOR else string
224b1a3f243SValentin Rothberg
225b1a3f243SValentin Rothberg
226b1a3f243SValentin Rothbergdef execute(cmd):
227b1a3f243SValentin Rothberg    """Execute %cmd and return stdout.  Exit in case of error."""
228f175ba17SValentin Rothberg    try:
2292f9cc12bSValentin Rothberg        stdout = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=False)
2307c5227afSValentin Rothberg        stdout = stdout.decode(errors='replace')
231f175ba17SValentin Rothberg    except subprocess.CalledProcessError as fail:
2322f9cc12bSValentin Rothberg        exit(fail)
233b1a3f243SValentin Rothberg    return stdout
234b1a3f243SValentin Rothberg
235b1a3f243SValentin Rothberg
236a42fa92cSValentin Rothbergdef find_commits(symbol, diff):
237a42fa92cSValentin Rothberg    """Find commits changing %symbol in the given range of %diff."""
2382f9cc12bSValentin Rothberg    commits = execute(["git", "log", "--pretty=oneline",
2392f9cc12bSValentin Rothberg                       "--abbrev-commit", "-G",
2402f9cc12bSValentin Rothberg                       symbol, diff])
2411b2c8414SValentin Rothberg    return [x for x in commits.split("\n") if x]
242a42fa92cSValentin Rothberg
243a42fa92cSValentin Rothberg
244b1a3f243SValentin Rothbergdef tree_is_dirty():
245b1a3f243SValentin Rothberg    """Return true if the current working tree is dirty (i.e., if any file has
246b1a3f243SValentin Rothberg    been added, deleted, modified, renamed or copied but not committed)."""
2472f9cc12bSValentin Rothberg    stdout = execute(["git", "status", "--porcelain"])
248b1a3f243SValentin Rothberg    for line in stdout:
249b1a3f243SValentin Rothberg        if re.findall(r"[URMADC]{1}", line[:2]):
250b1a3f243SValentin Rothberg            return True
251b1a3f243SValentin Rothberg    return False
252b1a3f243SValentin Rothberg
253b1a3f243SValentin Rothberg
254b1a3f243SValentin Rothbergdef get_head():
255b1a3f243SValentin Rothberg    """Return commit hash of current HEAD."""
2562f9cc12bSValentin Rothberg    stdout = execute(["git", "rev-parse", "HEAD"])
257b1a3f243SValentin Rothberg    return stdout.strip('\n')
258b1a3f243SValentin Rothberg
259b1a3f243SValentin Rothberg
260e2042a8aSValentin Rothbergdef partition(lst, size):
261e2042a8aSValentin Rothberg    """Partition list @lst into eveni-sized lists of size @size."""
2627c5227afSValentin Rothberg    return [lst[i::size] for i in range(size)]
263e2042a8aSValentin Rothberg
264e2042a8aSValentin Rothberg
265e2042a8aSValentin Rothbergdef init_worker():
266e2042a8aSValentin Rothberg    """Set signal handler to ignore SIGINT."""
267e2042a8aSValentin Rothberg    signal.signal(signal.SIGINT, signal.SIG_IGN)
268e2042a8aSValentin Rothberg
269e2042a8aSValentin Rothberg
2701b2c8414SValentin Rothbergdef find_sims(symbol, ignore, defined=[]):
2711b2c8414SValentin Rothberg    """Return a list of max. ten Kconfig symbols that are string-similar to
2721b2c8414SValentin Rothberg    @symbol."""
2731b2c8414SValentin Rothberg    if defined:
2748e8e3331SValentin Rothberg        return difflib.get_close_matches(symbol, set(defined), 10)
2751b2c8414SValentin Rothberg
2761b2c8414SValentin Rothberg    pool = Pool(cpu_count(), init_worker)
2771b2c8414SValentin Rothberg    kfiles = []
2781b2c8414SValentin Rothberg    for gitfile in get_files():
2791b2c8414SValentin Rothberg        if REGEX_FILE_KCONFIG.match(gitfile):
2801b2c8414SValentin Rothberg            kfiles.append(gitfile)
2811b2c8414SValentin Rothberg
2821b2c8414SValentin Rothberg    arglist = []
2831b2c8414SValentin Rothberg    for part in partition(kfiles, cpu_count()):
2841b2c8414SValentin Rothberg        arglist.append((part, ignore))
2851b2c8414SValentin Rothberg
2861b2c8414SValentin Rothberg    for res in pool.map(parse_kconfig_files, arglist):
2871b2c8414SValentin Rothberg        defined.extend(res[0])
2881b2c8414SValentin Rothberg
2898e8e3331SValentin Rothberg    return difflib.get_close_matches(symbol, set(defined), 10)
2901b2c8414SValentin Rothberg
2911b2c8414SValentin Rothberg
2921b2c8414SValentin Rothbergdef get_files():
2931b2c8414SValentin Rothberg    """Return a list of all files in the current git directory."""
2941b2c8414SValentin Rothberg    # use 'git ls-files' to get the worklist
2952f9cc12bSValentin Rothberg    stdout = execute(["git", "ls-files"])
2961b2c8414SValentin Rothberg    if len(stdout) > 0 and stdout[-1] == "\n":
2971b2c8414SValentin Rothberg        stdout = stdout[:-1]
2981b2c8414SValentin Rothberg
2991b2c8414SValentin Rothberg    files = []
3001b2c8414SValentin Rothberg    for gitfile in stdout.rsplit("\n"):
3011b2c8414SValentin Rothberg        if ".git" in gitfile or "ChangeLog" in gitfile or      \
3021b2c8414SValentin Rothberg                ".log" in gitfile or os.path.isdir(gitfile) or \
3031b2c8414SValentin Rothberg                gitfile.startswith("tools/"):
3041b2c8414SValentin Rothberg            continue
3051b2c8414SValentin Rothberg        files.append(gitfile)
3061b2c8414SValentin Rothberg    return files
3071b2c8414SValentin Rothberg
3081b2c8414SValentin Rothberg
309cf132e4aSValentin Rothbergdef check_symbols(ignore):
310b1a3f243SValentin Rothberg    """Find undefined Kconfig symbols and return a dict with the symbol as key
311cf132e4aSValentin Rothberg    and a list of referencing files as value.  Files matching %ignore are not
312cf132e4aSValentin Rothberg    checked for undefined symbols."""
313e2042a8aSValentin Rothberg    pool = Pool(cpu_count(), init_worker)
314e2042a8aSValentin Rothberg    try:
315e2042a8aSValentin Rothberg        return check_symbols_helper(pool, ignore)
316e2042a8aSValentin Rothberg    except KeyboardInterrupt:
317e2042a8aSValentin Rothberg        pool.terminate()
318e2042a8aSValentin Rothberg        pool.join()
319e2042a8aSValentin Rothberg        sys.exit(1)
320e2042a8aSValentin Rothberg
321e2042a8aSValentin Rothberg
322e2042a8aSValentin Rothbergdef check_symbols_helper(pool, ignore):
323e2042a8aSValentin Rothberg    """Helper method for check_symbols().  Used to catch keyboard interrupts in
324e2042a8aSValentin Rothberg    check_symbols() in order to properly terminate running worker processes."""
32524fe1f03SValentin Rothberg    source_files = []
32624fe1f03SValentin Rothberg    kconfig_files = []
327ef3f5543SValentin Rothberg    defined_symbols = []
328ef3f5543SValentin Rothberg    referenced_symbols = dict()  # {file: [symbols]}
32924fe1f03SValentin Rothberg
3301b2c8414SValentin Rothberg    for gitfile in get_files():
33124fe1f03SValentin Rothberg        if REGEX_FILE_KCONFIG.match(gitfile):
33224fe1f03SValentin Rothberg            kconfig_files.append(gitfile)
33324fe1f03SValentin Rothberg        else:
3341439ebd2SAriel Marcovitch            if ignore and re.match(ignore, gitfile):
335e2042a8aSValentin Rothberg                continue
336e2042a8aSValentin Rothberg            # add source files that do not match the ignore pattern
33724fe1f03SValentin Rothberg            source_files.append(gitfile)
33824fe1f03SValentin Rothberg
339e2042a8aSValentin Rothberg    # parse source files
340e2042a8aSValentin Rothberg    arglist = partition(source_files, cpu_count())
341e2042a8aSValentin Rothberg    for res in pool.map(parse_source_files, arglist):
342ef3f5543SValentin Rothberg        referenced_symbols.update(res)
34324fe1f03SValentin Rothberg
344e2042a8aSValentin Rothberg    # parse kconfig files
345e2042a8aSValentin Rothberg    arglist = []
346e2042a8aSValentin Rothberg    for part in partition(kconfig_files, cpu_count()):
347e2042a8aSValentin Rothberg        arglist.append((part, ignore))
348e2042a8aSValentin Rothberg    for res in pool.map(parse_kconfig_files, arglist):
349ef3f5543SValentin Rothberg        defined_symbols.extend(res[0])
350ef3f5543SValentin Rothberg        referenced_symbols.update(res[1])
351ef3f5543SValentin Rothberg    defined_symbols = set(defined_symbols)
352e2042a8aSValentin Rothberg
353ef3f5543SValentin Rothberg    # inverse mapping of referenced_symbols to dict(symbol: [files])
354e2042a8aSValentin Rothberg    inv_map = dict()
355ef3f5543SValentin Rothberg    for _file, symbols in referenced_symbols.items():
356ef3f5543SValentin Rothberg        for symbol in symbols:
357ef3f5543SValentin Rothberg            inv_map[symbol] = inv_map.get(symbol, set())
358ef3f5543SValentin Rothberg            inv_map[symbol].add(_file)
359ef3f5543SValentin Rothberg    referenced_symbols = inv_map
36024fe1f03SValentin Rothberg
361ef3f5543SValentin Rothberg    undefined = {}  # {symbol: [files]}
362ef3f5543SValentin Rothberg    for symbol in sorted(referenced_symbols):
363cc641d55SValentin Rothberg        # filter some false positives
364ef3f5543SValentin Rothberg        if symbol == "FOO" or symbol == "BAR" or \
365ef3f5543SValentin Rothberg                symbol == "FOO_BAR" or symbol == "XXX":
366cc641d55SValentin Rothberg            continue
367ef3f5543SValentin Rothberg        if symbol not in defined_symbols:
368ef3f5543SValentin Rothberg            if symbol.endswith("_MODULE"):
369cc641d55SValentin Rothberg                # avoid false positives for kernel modules
370ef3f5543SValentin Rothberg                if symbol[:-len("_MODULE")] in defined_symbols:
37124fe1f03SValentin Rothberg                    continue
372ef3f5543SValentin Rothberg            undefined[symbol] = referenced_symbols.get(symbol)
373ef3f5543SValentin Rothberg    return undefined, defined_symbols
37424fe1f03SValentin Rothberg
37524fe1f03SValentin Rothberg
376e2042a8aSValentin Rothbergdef parse_source_files(source_files):
377e2042a8aSValentin Rothberg    """Parse each source file in @source_files and return dictionary with source
378e2042a8aSValentin Rothberg    files as keys and lists of references Kconfig symbols as values."""
379ef3f5543SValentin Rothberg    referenced_symbols = dict()
380e2042a8aSValentin Rothberg    for sfile in source_files:
381ef3f5543SValentin Rothberg        referenced_symbols[sfile] = parse_source_file(sfile)
382ef3f5543SValentin Rothberg    return referenced_symbols
383e2042a8aSValentin Rothberg
384e2042a8aSValentin Rothberg
385e2042a8aSValentin Rothbergdef parse_source_file(sfile):
386ef3f5543SValentin Rothberg    """Parse @sfile and return a list of referenced Kconfig symbols."""
38724fe1f03SValentin Rothberg    lines = []
388e2042a8aSValentin Rothberg    references = []
389e2042a8aSValentin Rothberg
390e2042a8aSValentin Rothberg    if not os.path.exists(sfile):
391e2042a8aSValentin Rothberg        return references
392e2042a8aSValentin Rothberg
3937c5227afSValentin Rothberg    with open(sfile, "r", encoding='utf-8', errors='replace') as stream:
39424fe1f03SValentin Rothberg        lines = stream.readlines()
39524fe1f03SValentin Rothberg
39624fe1f03SValentin Rothberg    for line in lines:
39736c79c7fSValentin Rothberg        if "CONFIG_" not in line:
39824fe1f03SValentin Rothberg            continue
399ef3f5543SValentin Rothberg        symbols = REGEX_SOURCE_SYMBOL.findall(line)
400ef3f5543SValentin Rothberg        for symbol in symbols:
401ef3f5543SValentin Rothberg            if not REGEX_FILTER_SYMBOLS.search(symbol):
40224fe1f03SValentin Rothberg                continue
403ef3f5543SValentin Rothberg            references.append(symbol)
404e2042a8aSValentin Rothberg
405e2042a8aSValentin Rothberg    return references
40624fe1f03SValentin Rothberg
40724fe1f03SValentin Rothberg
408ef3f5543SValentin Rothbergdef get_symbols_in_line(line):
409ef3f5543SValentin Rothberg    """Return mentioned Kconfig symbols in @line."""
410ef3f5543SValentin Rothberg    return REGEX_SYMBOL.findall(line)
41124fe1f03SValentin Rothberg
41224fe1f03SValentin Rothberg
413e2042a8aSValentin Rothbergdef parse_kconfig_files(args):
414e2042a8aSValentin Rothberg    """Parse kconfig files and return tuple of defined and references Kconfig
415e2042a8aSValentin Rothberg    symbols.  Note, @args is a tuple of a list of files and the @ignore
416e2042a8aSValentin Rothberg    pattern."""
417e2042a8aSValentin Rothberg    kconfig_files = args[0]
418e2042a8aSValentin Rothberg    ignore = args[1]
419ef3f5543SValentin Rothberg    defined_symbols = []
420ef3f5543SValentin Rothberg    referenced_symbols = dict()
421e2042a8aSValentin Rothberg
422e2042a8aSValentin Rothberg    for kfile in kconfig_files:
423e2042a8aSValentin Rothberg        defined, references = parse_kconfig_file(kfile)
424ef3f5543SValentin Rothberg        defined_symbols.extend(defined)
425e2042a8aSValentin Rothberg        if ignore and re.match(ignore, kfile):
426e2042a8aSValentin Rothberg            # do not collect references for files that match the ignore pattern
427e2042a8aSValentin Rothberg            continue
428ef3f5543SValentin Rothberg        referenced_symbols[kfile] = references
429ef3f5543SValentin Rothberg    return (defined_symbols, referenced_symbols)
430e2042a8aSValentin Rothberg
431e2042a8aSValentin Rothberg
432e2042a8aSValentin Rothbergdef parse_kconfig_file(kfile):
433ef3f5543SValentin Rothberg    """Parse @kfile and update symbol definitions and references."""
43424fe1f03SValentin Rothberg    lines = []
435e2042a8aSValentin Rothberg    defined = []
436e2042a8aSValentin Rothberg    references = []
43724fe1f03SValentin Rothberg
438e2042a8aSValentin Rothberg    if not os.path.exists(kfile):
439e2042a8aSValentin Rothberg        return defined, references
440e2042a8aSValentin Rothberg
4417c5227afSValentin Rothberg    with open(kfile, "r", encoding='utf-8', errors='replace') as stream:
44224fe1f03SValentin Rothberg        lines = stream.readlines()
44324fe1f03SValentin Rothberg
44424fe1f03SValentin Rothberg    for i in range(len(lines)):
44524fe1f03SValentin Rothberg        line = lines[i]
44624fe1f03SValentin Rothberg        line = line.strip('\n')
447cc641d55SValentin Rothberg        line = line.split("#")[0]  # ignore comments
44824fe1f03SValentin Rothberg
44924fe1f03SValentin Rothberg        if REGEX_KCONFIG_DEF.match(line):
450ef3f5543SValentin Rothberg            symbol_def = REGEX_KCONFIG_DEF.findall(line)
451ef3f5543SValentin Rothberg            defined.append(symbol_def[0])
45224fe1f03SValentin Rothberg        elif REGEX_KCONFIG_STMT.match(line):
453e2042a8aSValentin Rothberg            line = REGEX_QUOTES.sub("", line)
454ef3f5543SValentin Rothberg            symbols = get_symbols_in_line(line)
455cc641d55SValentin Rothberg            # multi-line statements
45624fe1f03SValentin Rothberg            while line.endswith("\\"):
45724fe1f03SValentin Rothberg                i += 1
45824fe1f03SValentin Rothberg                line = lines[i]
45924fe1f03SValentin Rothberg                line = line.strip('\n')
460ef3f5543SValentin Rothberg                symbols.extend(get_symbols_in_line(line))
461ef3f5543SValentin Rothberg            for symbol in set(symbols):
462ef3f5543SValentin Rothberg                if REGEX_NUMERIC.match(symbol):
4630bd38ae3SValentin Rothberg                    # ignore numeric values
4640bd38ae3SValentin Rothberg                    continue
465ef3f5543SValentin Rothberg                references.append(symbol)
466e2042a8aSValentin Rothberg
467e2042a8aSValentin Rothberg    return defined, references
46824fe1f03SValentin Rothberg
46924fe1f03SValentin Rothberg
470*87c7ee67SMasahiro Yamadadef main():
471*87c7ee67SMasahiro Yamada    try:
472*87c7ee67SMasahiro Yamada        print_undefined_symbols()
473*87c7ee67SMasahiro Yamada    except BrokenPipeError:
474*87c7ee67SMasahiro Yamada        # Python flushes standard streams on exit; redirect remaining output
475*87c7ee67SMasahiro Yamada        # to devnull to avoid another BrokenPipeError at shutdown
476*87c7ee67SMasahiro Yamada        devnull = os.open(os.devnull, os.O_WRONLY)
477*87c7ee67SMasahiro Yamada        os.dup2(devnull, sys.stdout.fileno())
478*87c7ee67SMasahiro Yamada        sys.exit(1)  # Python exits with error code 1 on EPIPE
479*87c7ee67SMasahiro Yamada
480*87c7ee67SMasahiro Yamada
48124fe1f03SValentin Rothbergif __name__ == "__main__":
48224fe1f03SValentin Rothberg    main()
483