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)
37f70f74d1SMasahiro YamadaREGEX_KCONFIG_HELP = re.compile(r"^\s+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:
105*d62d5aedSAriel Marcovitch        if args.commit.startswith('HEAD'):
106*d62d5aedSAriel Marcovitch            sys.exit("The --commit option can't use the HEAD ref")
107*d62d5aedSAriel Marcovitch
10814390e31SValentin Rothberg        args.find = False
109a42fa92cSValentin Rothberg
11014390e31SValentin Rothberg    if args.ignore:
111cf132e4aSValentin Rothberg        try:
11214390e31SValentin Rothberg            re.match(args.ignore, "this/is/just/a/test.c")
113cf132e4aSValentin Rothberg        except:
114cf132e4aSValentin Rothberg            sys.exit("Please specify a valid Python regex.")
115cf132e4aSValentin Rothberg
11614390e31SValentin Rothberg    return args
117b1a3f243SValentin Rothberg
118b1a3f243SValentin Rothberg
11924fe1f03SValentin Rothbergdef main():
12024fe1f03SValentin Rothberg    """Main function of this module."""
12114390e31SValentin Rothberg    args = parse_options()
122b1a3f243SValentin Rothberg
12336c79c7fSValentin Rothberg    global COLOR
12436c79c7fSValentin Rothberg    COLOR = args.color and sys.stdout.isatty()
1254c73c088SAndrew Donnellan
12614390e31SValentin Rothberg    if args.sim and not args.commit and not args.diff:
12714390e31SValentin Rothberg        sims = find_sims(args.sim, args.ignore)
1281b2c8414SValentin Rothberg        if sims:
1297c5227afSValentin Rothberg            print("%s: %s" % (yel("Similar symbols"), ', '.join(sims)))
1301b2c8414SValentin Rothberg        else:
1317c5227afSValentin Rothberg            print("%s: no similar symbols found" % yel("Similar symbols"))
1321b2c8414SValentin Rothberg        sys.exit(0)
1331b2c8414SValentin Rothberg
1341b2c8414SValentin Rothberg    # dictionary of (un)defined symbols
1351b2c8414SValentin Rothberg    defined = {}
1361b2c8414SValentin Rothberg    undefined = {}
1371b2c8414SValentin Rothberg
13814390e31SValentin Rothberg    if args.commit or args.diff:
139b1a3f243SValentin Rothberg        head = get_head()
140b1a3f243SValentin Rothberg
141b1a3f243SValentin Rothberg        # get commit range
142b1a3f243SValentin Rothberg        commit_a = None
143b1a3f243SValentin Rothberg        commit_b = None
14414390e31SValentin Rothberg        if args.commit:
14514390e31SValentin Rothberg            commit_a = args.commit + "~"
14614390e31SValentin Rothberg            commit_b = args.commit
14714390e31SValentin Rothberg        elif args.diff:
14814390e31SValentin Rothberg            split = args.diff.split("..")
149b1a3f243SValentin Rothberg            commit_a = split[0]
150b1a3f243SValentin Rothberg            commit_b = split[1]
151b1a3f243SValentin Rothberg            undefined_a = {}
152b1a3f243SValentin Rothberg            undefined_b = {}
153b1a3f243SValentin Rothberg
154b1a3f243SValentin Rothberg        # get undefined items before the commit
1552f9cc12bSValentin Rothberg        reset(commit_a)
15614390e31SValentin Rothberg        undefined_a, _ = check_symbols(args.ignore)
157b1a3f243SValentin Rothberg
158b1a3f243SValentin Rothberg        # get undefined items for the commit
1592f9cc12bSValentin Rothberg        reset(commit_b)
16014390e31SValentin Rothberg        undefined_b, defined = check_symbols(args.ignore)
161b1a3f243SValentin Rothberg
162b1a3f243SValentin Rothberg        # report cases that are present for the commit but not before
163ef3f5543SValentin Rothberg        for symbol in sorted(undefined_b):
164ef3f5543SValentin Rothberg            # symbol has not been undefined before
165ef3f5543SValentin Rothberg            if symbol not in undefined_a:
166ef3f5543SValentin Rothberg                files = sorted(undefined_b.get(symbol))
167ef3f5543SValentin Rothberg                undefined[symbol] = files
168ef3f5543SValentin Rothberg            # check if there are new files that reference the undefined symbol
169b1a3f243SValentin Rothberg            else:
170ef3f5543SValentin Rothberg                files = sorted(undefined_b.get(symbol) -
171ef3f5543SValentin Rothberg                               undefined_a.get(symbol))
172b1a3f243SValentin Rothberg                if files:
173ef3f5543SValentin Rothberg                    undefined[symbol] = files
174b1a3f243SValentin Rothberg
175b1a3f243SValentin Rothberg        # reset to head
1762f9cc12bSValentin Rothberg        reset(head)
177b1a3f243SValentin Rothberg
178b1a3f243SValentin Rothberg    # default to check the entire tree
179b1a3f243SValentin Rothberg    else:
18014390e31SValentin Rothberg        undefined, defined = check_symbols(args.ignore)
1811b2c8414SValentin Rothberg
1821b2c8414SValentin Rothberg    # now print the output
183ef3f5543SValentin Rothberg    for symbol in sorted(undefined):
184ef3f5543SValentin Rothberg        print(red(symbol))
1851b2c8414SValentin Rothberg
186ef3f5543SValentin Rothberg        files = sorted(undefined.get(symbol))
1877c5227afSValentin Rothberg        print("%s: %s" % (yel("Referencing files"), ", ".join(files)))
1881b2c8414SValentin Rothberg
189ef3f5543SValentin Rothberg        sims = find_sims(symbol, args.ignore, defined)
1901b2c8414SValentin Rothberg        sims_out = yel("Similar symbols")
1911b2c8414SValentin Rothberg        if sims:
1927c5227afSValentin Rothberg            print("%s: %s" % (sims_out, ', '.join(sims)))
1931b2c8414SValentin Rothberg        else:
1947c5227afSValentin Rothberg            print("%s: %s" % (sims_out, "no similar symbols found"))
1951b2c8414SValentin Rothberg
19614390e31SValentin Rothberg        if args.find:
1977c5227afSValentin Rothberg            print("%s:" % yel("Commits changing symbol"))
198ef3f5543SValentin Rothberg            commits = find_commits(symbol, args.diff)
1991b2c8414SValentin Rothberg            if commits:
2001b2c8414SValentin Rothberg                for commit in commits:
2011b2c8414SValentin Rothberg                    commit = commit.split(" ", 1)
2027c5227afSValentin Rothberg                    print("\t- %s (\"%s\")" % (yel(commit[0]), commit[1]))
2031b2c8414SValentin Rothberg            else:
2047c5227afSValentin Rothberg                print("\t- no commit found")
2057c5227afSValentin Rothberg        print()  # new line
206c7455663SValentin Rothberg
207c7455663SValentin Rothberg
2082f9cc12bSValentin Rothbergdef reset(commit):
2092f9cc12bSValentin Rothberg    """Reset current git tree to %commit."""
2102f9cc12bSValentin Rothberg    execute(["git", "reset", "--hard", commit])
2112f9cc12bSValentin Rothberg
2122f9cc12bSValentin Rothberg
213c7455663SValentin Rothbergdef yel(string):
214c7455663SValentin Rothberg    """
215c7455663SValentin Rothberg    Color %string yellow.
216c7455663SValentin Rothberg    """
21736c79c7fSValentin Rothberg    return "\033[33m%s\033[0m" % string if COLOR else string
218c7455663SValentin Rothberg
219c7455663SValentin Rothberg
220c7455663SValentin Rothbergdef red(string):
221c7455663SValentin Rothberg    """
222c7455663SValentin Rothberg    Color %string red.
223c7455663SValentin Rothberg    """
22436c79c7fSValentin Rothberg    return "\033[31m%s\033[0m" % string if COLOR else string
225b1a3f243SValentin Rothberg
226b1a3f243SValentin Rothberg
227b1a3f243SValentin Rothbergdef execute(cmd):
228b1a3f243SValentin Rothberg    """Execute %cmd and return stdout.  Exit in case of error."""
229f175ba17SValentin Rothberg    try:
2302f9cc12bSValentin Rothberg        stdout = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=False)
2317c5227afSValentin Rothberg        stdout = stdout.decode(errors='replace')
232f175ba17SValentin Rothberg    except subprocess.CalledProcessError as fail:
2332f9cc12bSValentin Rothberg        exit(fail)
234b1a3f243SValentin Rothberg    return stdout
235b1a3f243SValentin Rothberg
236b1a3f243SValentin Rothberg
237a42fa92cSValentin Rothbergdef find_commits(symbol, diff):
238a42fa92cSValentin Rothberg    """Find commits changing %symbol in the given range of %diff."""
2392f9cc12bSValentin Rothberg    commits = execute(["git", "log", "--pretty=oneline",
2402f9cc12bSValentin Rothberg                       "--abbrev-commit", "-G",
2412f9cc12bSValentin Rothberg                       symbol, diff])
2421b2c8414SValentin Rothberg    return [x for x in commits.split("\n") if x]
243a42fa92cSValentin Rothberg
244a42fa92cSValentin Rothberg
245b1a3f243SValentin Rothbergdef tree_is_dirty():
246b1a3f243SValentin Rothberg    """Return true if the current working tree is dirty (i.e., if any file has
247b1a3f243SValentin Rothberg    been added, deleted, modified, renamed or copied but not committed)."""
2482f9cc12bSValentin Rothberg    stdout = execute(["git", "status", "--porcelain"])
249b1a3f243SValentin Rothberg    for line in stdout:
250b1a3f243SValentin Rothberg        if re.findall(r"[URMADC]{1}", line[:2]):
251b1a3f243SValentin Rothberg            return True
252b1a3f243SValentin Rothberg    return False
253b1a3f243SValentin Rothberg
254b1a3f243SValentin Rothberg
255b1a3f243SValentin Rothbergdef get_head():
256b1a3f243SValentin Rothberg    """Return commit hash of current HEAD."""
2572f9cc12bSValentin Rothberg    stdout = execute(["git", "rev-parse", "HEAD"])
258b1a3f243SValentin Rothberg    return stdout.strip('\n')
259b1a3f243SValentin Rothberg
260b1a3f243SValentin Rothberg
261e2042a8aSValentin Rothbergdef partition(lst, size):
262e2042a8aSValentin Rothberg    """Partition list @lst into eveni-sized lists of size @size."""
2637c5227afSValentin Rothberg    return [lst[i::size] for i in range(size)]
264e2042a8aSValentin Rothberg
265e2042a8aSValentin Rothberg
266e2042a8aSValentin Rothbergdef init_worker():
267e2042a8aSValentin Rothberg    """Set signal handler to ignore SIGINT."""
268e2042a8aSValentin Rothberg    signal.signal(signal.SIGINT, signal.SIG_IGN)
269e2042a8aSValentin Rothberg
270e2042a8aSValentin Rothberg
2711b2c8414SValentin Rothbergdef find_sims(symbol, ignore, defined=[]):
2721b2c8414SValentin Rothberg    """Return a list of max. ten Kconfig symbols that are string-similar to
2731b2c8414SValentin Rothberg    @symbol."""
2741b2c8414SValentin Rothberg    if defined:
2758e8e3331SValentin Rothberg        return difflib.get_close_matches(symbol, set(defined), 10)
2761b2c8414SValentin Rothberg
2771b2c8414SValentin Rothberg    pool = Pool(cpu_count(), init_worker)
2781b2c8414SValentin Rothberg    kfiles = []
2791b2c8414SValentin Rothberg    for gitfile in get_files():
2801b2c8414SValentin Rothberg        if REGEX_FILE_KCONFIG.match(gitfile):
2811b2c8414SValentin Rothberg            kfiles.append(gitfile)
2821b2c8414SValentin Rothberg
2831b2c8414SValentin Rothberg    arglist = []
2841b2c8414SValentin Rothberg    for part in partition(kfiles, cpu_count()):
2851b2c8414SValentin Rothberg        arglist.append((part, ignore))
2861b2c8414SValentin Rothberg
2871b2c8414SValentin Rothberg    for res in pool.map(parse_kconfig_files, arglist):
2881b2c8414SValentin Rothberg        defined.extend(res[0])
2891b2c8414SValentin Rothberg
2908e8e3331SValentin Rothberg    return difflib.get_close_matches(symbol, set(defined), 10)
2911b2c8414SValentin Rothberg
2921b2c8414SValentin Rothberg
2931b2c8414SValentin Rothbergdef get_files():
2941b2c8414SValentin Rothberg    """Return a list of all files in the current git directory."""
2951b2c8414SValentin Rothberg    # use 'git ls-files' to get the worklist
2962f9cc12bSValentin Rothberg    stdout = execute(["git", "ls-files"])
2971b2c8414SValentin Rothberg    if len(stdout) > 0 and stdout[-1] == "\n":
2981b2c8414SValentin Rothberg        stdout = stdout[:-1]
2991b2c8414SValentin Rothberg
3001b2c8414SValentin Rothberg    files = []
3011b2c8414SValentin Rothberg    for gitfile in stdout.rsplit("\n"):
3021b2c8414SValentin Rothberg        if ".git" in gitfile or "ChangeLog" in gitfile or      \
3031b2c8414SValentin Rothberg                ".log" in gitfile or os.path.isdir(gitfile) or \
3041b2c8414SValentin Rothberg                gitfile.startswith("tools/"):
3051b2c8414SValentin Rothberg            continue
3061b2c8414SValentin Rothberg        files.append(gitfile)
3071b2c8414SValentin Rothberg    return files
3081b2c8414SValentin Rothberg
3091b2c8414SValentin Rothberg
310cf132e4aSValentin Rothbergdef check_symbols(ignore):
311b1a3f243SValentin Rothberg    """Find undefined Kconfig symbols and return a dict with the symbol as key
312cf132e4aSValentin Rothberg    and a list of referencing files as value.  Files matching %ignore are not
313cf132e4aSValentin Rothberg    checked for undefined symbols."""
314e2042a8aSValentin Rothberg    pool = Pool(cpu_count(), init_worker)
315e2042a8aSValentin Rothberg    try:
316e2042a8aSValentin Rothberg        return check_symbols_helper(pool, ignore)
317e2042a8aSValentin Rothberg    except KeyboardInterrupt:
318e2042a8aSValentin Rothberg        pool.terminate()
319e2042a8aSValentin Rothberg        pool.join()
320e2042a8aSValentin Rothberg        sys.exit(1)
321e2042a8aSValentin Rothberg
322e2042a8aSValentin Rothberg
323e2042a8aSValentin Rothbergdef check_symbols_helper(pool, ignore):
324e2042a8aSValentin Rothberg    """Helper method for check_symbols().  Used to catch keyboard interrupts in
325e2042a8aSValentin Rothberg    check_symbols() in order to properly terminate running worker processes."""
32624fe1f03SValentin Rothberg    source_files = []
32724fe1f03SValentin Rothberg    kconfig_files = []
328ef3f5543SValentin Rothberg    defined_symbols = []
329ef3f5543SValentin Rothberg    referenced_symbols = dict()  # {file: [symbols]}
33024fe1f03SValentin Rothberg
3311b2c8414SValentin Rothberg    for gitfile in get_files():
33224fe1f03SValentin Rothberg        if REGEX_FILE_KCONFIG.match(gitfile):
33324fe1f03SValentin Rothberg            kconfig_files.append(gitfile)
33424fe1f03SValentin Rothberg        else:
3351439ebd2SAriel Marcovitch            if ignore and re.match(ignore, gitfile):
336e2042a8aSValentin Rothberg                continue
337e2042a8aSValentin Rothberg            # add source files that do not match the ignore pattern
33824fe1f03SValentin Rothberg            source_files.append(gitfile)
33924fe1f03SValentin Rothberg
340e2042a8aSValentin Rothberg    # parse source files
341e2042a8aSValentin Rothberg    arglist = partition(source_files, cpu_count())
342e2042a8aSValentin Rothberg    for res in pool.map(parse_source_files, arglist):
343ef3f5543SValentin Rothberg        referenced_symbols.update(res)
34424fe1f03SValentin Rothberg
345e2042a8aSValentin Rothberg    # parse kconfig files
346e2042a8aSValentin Rothberg    arglist = []
347e2042a8aSValentin Rothberg    for part in partition(kconfig_files, cpu_count()):
348e2042a8aSValentin Rothberg        arglist.append((part, ignore))
349e2042a8aSValentin Rothberg    for res in pool.map(parse_kconfig_files, arglist):
350ef3f5543SValentin Rothberg        defined_symbols.extend(res[0])
351ef3f5543SValentin Rothberg        referenced_symbols.update(res[1])
352ef3f5543SValentin Rothberg    defined_symbols = set(defined_symbols)
353e2042a8aSValentin Rothberg
354ef3f5543SValentin Rothberg    # inverse mapping of referenced_symbols to dict(symbol: [files])
355e2042a8aSValentin Rothberg    inv_map = dict()
356ef3f5543SValentin Rothberg    for _file, symbols in referenced_symbols.items():
357ef3f5543SValentin Rothberg        for symbol in symbols:
358ef3f5543SValentin Rothberg            inv_map[symbol] = inv_map.get(symbol, set())
359ef3f5543SValentin Rothberg            inv_map[symbol].add(_file)
360ef3f5543SValentin Rothberg    referenced_symbols = inv_map
36124fe1f03SValentin Rothberg
362ef3f5543SValentin Rothberg    undefined = {}  # {symbol: [files]}
363ef3f5543SValentin Rothberg    for symbol in sorted(referenced_symbols):
364cc641d55SValentin Rothberg        # filter some false positives
365ef3f5543SValentin Rothberg        if symbol == "FOO" or symbol == "BAR" or \
366ef3f5543SValentin Rothberg                symbol == "FOO_BAR" or symbol == "XXX":
367cc641d55SValentin Rothberg            continue
368ef3f5543SValentin Rothberg        if symbol not in defined_symbols:
369ef3f5543SValentin Rothberg            if symbol.endswith("_MODULE"):
370cc641d55SValentin Rothberg                # avoid false positives for kernel modules
371ef3f5543SValentin Rothberg                if symbol[:-len("_MODULE")] in defined_symbols:
37224fe1f03SValentin Rothberg                    continue
373ef3f5543SValentin Rothberg            undefined[symbol] = referenced_symbols.get(symbol)
374ef3f5543SValentin Rothberg    return undefined, defined_symbols
37524fe1f03SValentin Rothberg
37624fe1f03SValentin Rothberg
377e2042a8aSValentin Rothbergdef parse_source_files(source_files):
378e2042a8aSValentin Rothberg    """Parse each source file in @source_files and return dictionary with source
379e2042a8aSValentin Rothberg    files as keys and lists of references Kconfig symbols as values."""
380ef3f5543SValentin Rothberg    referenced_symbols = dict()
381e2042a8aSValentin Rothberg    for sfile in source_files:
382ef3f5543SValentin Rothberg        referenced_symbols[sfile] = parse_source_file(sfile)
383ef3f5543SValentin Rothberg    return referenced_symbols
384e2042a8aSValentin Rothberg
385e2042a8aSValentin Rothberg
386e2042a8aSValentin Rothbergdef parse_source_file(sfile):
387ef3f5543SValentin Rothberg    """Parse @sfile and return a list of referenced Kconfig symbols."""
38824fe1f03SValentin Rothberg    lines = []
389e2042a8aSValentin Rothberg    references = []
390e2042a8aSValentin Rothberg
391e2042a8aSValentin Rothberg    if not os.path.exists(sfile):
392e2042a8aSValentin Rothberg        return references
393e2042a8aSValentin Rothberg
3947c5227afSValentin Rothberg    with open(sfile, "r", encoding='utf-8', errors='replace') as stream:
39524fe1f03SValentin Rothberg        lines = stream.readlines()
39624fe1f03SValentin Rothberg
39724fe1f03SValentin Rothberg    for line in lines:
39836c79c7fSValentin Rothberg        if "CONFIG_" not in line:
39924fe1f03SValentin Rothberg            continue
400ef3f5543SValentin Rothberg        symbols = REGEX_SOURCE_SYMBOL.findall(line)
401ef3f5543SValentin Rothberg        for symbol in symbols:
402ef3f5543SValentin Rothberg            if not REGEX_FILTER_SYMBOLS.search(symbol):
40324fe1f03SValentin Rothberg                continue
404ef3f5543SValentin Rothberg            references.append(symbol)
405e2042a8aSValentin Rothberg
406e2042a8aSValentin Rothberg    return references
40724fe1f03SValentin Rothberg
40824fe1f03SValentin Rothberg
409ef3f5543SValentin Rothbergdef get_symbols_in_line(line):
410ef3f5543SValentin Rothberg    """Return mentioned Kconfig symbols in @line."""
411ef3f5543SValentin Rothberg    return REGEX_SYMBOL.findall(line)
41224fe1f03SValentin Rothberg
41324fe1f03SValentin Rothberg
414e2042a8aSValentin Rothbergdef parse_kconfig_files(args):
415e2042a8aSValentin Rothberg    """Parse kconfig files and return tuple of defined and references Kconfig
416e2042a8aSValentin Rothberg    symbols.  Note, @args is a tuple of a list of files and the @ignore
417e2042a8aSValentin Rothberg    pattern."""
418e2042a8aSValentin Rothberg    kconfig_files = args[0]
419e2042a8aSValentin Rothberg    ignore = args[1]
420ef3f5543SValentin Rothberg    defined_symbols = []
421ef3f5543SValentin Rothberg    referenced_symbols = dict()
422e2042a8aSValentin Rothberg
423e2042a8aSValentin Rothberg    for kfile in kconfig_files:
424e2042a8aSValentin Rothberg        defined, references = parse_kconfig_file(kfile)
425ef3f5543SValentin Rothberg        defined_symbols.extend(defined)
426e2042a8aSValentin Rothberg        if ignore and re.match(ignore, kfile):
427e2042a8aSValentin Rothberg            # do not collect references for files that match the ignore pattern
428e2042a8aSValentin Rothberg            continue
429ef3f5543SValentin Rothberg        referenced_symbols[kfile] = references
430ef3f5543SValentin Rothberg    return (defined_symbols, referenced_symbols)
431e2042a8aSValentin Rothberg
432e2042a8aSValentin Rothberg
433e2042a8aSValentin Rothbergdef parse_kconfig_file(kfile):
434ef3f5543SValentin Rothberg    """Parse @kfile and update symbol definitions and references."""
43524fe1f03SValentin Rothberg    lines = []
436e2042a8aSValentin Rothberg    defined = []
437e2042a8aSValentin Rothberg    references = []
43824fe1f03SValentin Rothberg    skip = False
43924fe1f03SValentin Rothberg
440e2042a8aSValentin Rothberg    if not os.path.exists(kfile):
441e2042a8aSValentin Rothberg        return defined, references
442e2042a8aSValentin Rothberg
4437c5227afSValentin Rothberg    with open(kfile, "r", encoding='utf-8', errors='replace') as stream:
44424fe1f03SValentin Rothberg        lines = stream.readlines()
44524fe1f03SValentin Rothberg
44624fe1f03SValentin Rothberg    for i in range(len(lines)):
44724fe1f03SValentin Rothberg        line = lines[i]
44824fe1f03SValentin Rothberg        line = line.strip('\n')
449cc641d55SValentin Rothberg        line = line.split("#")[0]  # ignore comments
45024fe1f03SValentin Rothberg
45124fe1f03SValentin Rothberg        if REGEX_KCONFIG_DEF.match(line):
452ef3f5543SValentin Rothberg            symbol_def = REGEX_KCONFIG_DEF.findall(line)
453ef3f5543SValentin Rothberg            defined.append(symbol_def[0])
45424fe1f03SValentin Rothberg            skip = False
45524fe1f03SValentin Rothberg        elif REGEX_KCONFIG_HELP.match(line):
45624fe1f03SValentin Rothberg            skip = True
45724fe1f03SValentin Rothberg        elif skip:
458cc641d55SValentin Rothberg            # ignore content of help messages
45924fe1f03SValentin Rothberg            pass
46024fe1f03SValentin Rothberg        elif REGEX_KCONFIG_STMT.match(line):
461e2042a8aSValentin Rothberg            line = REGEX_QUOTES.sub("", line)
462ef3f5543SValentin Rothberg            symbols = get_symbols_in_line(line)
463cc641d55SValentin Rothberg            # multi-line statements
46424fe1f03SValentin Rothberg            while line.endswith("\\"):
46524fe1f03SValentin Rothberg                i += 1
46624fe1f03SValentin Rothberg                line = lines[i]
46724fe1f03SValentin Rothberg                line = line.strip('\n')
468ef3f5543SValentin Rothberg                symbols.extend(get_symbols_in_line(line))
469ef3f5543SValentin Rothberg            for symbol in set(symbols):
470ef3f5543SValentin Rothberg                if REGEX_NUMERIC.match(symbol):
4710bd38ae3SValentin Rothberg                    # ignore numeric values
4720bd38ae3SValentin Rothberg                    continue
473ef3f5543SValentin Rothberg                references.append(symbol)
474e2042a8aSValentin Rothberg
475e2042a8aSValentin Rothberg    return defined, references
47624fe1f03SValentin Rothberg
47724fe1f03SValentin Rothberg
47824fe1f03SValentin Rothbergif __name__ == "__main__":
47924fe1f03SValentin Rothberg    main()
480