18c2ecf20Sopenharmony_ci#!/usr/bin/env python3
28c2ecf20Sopenharmony_ci# SPDX-License-Identifier: GPL-2.0-only
38c2ecf20Sopenharmony_ci
48c2ecf20Sopenharmony_ci"""Find Kconfig symbols that are referenced but not defined."""
58c2ecf20Sopenharmony_ci
68c2ecf20Sopenharmony_ci# (c) 2014-2017 Valentin Rothberg <valentinrothberg@gmail.com>
78c2ecf20Sopenharmony_ci# (c) 2014 Stefan Hengelein <stefan.hengelein@fau.de>
88c2ecf20Sopenharmony_ci#
98c2ecf20Sopenharmony_ci
108c2ecf20Sopenharmony_ci
118c2ecf20Sopenharmony_ciimport argparse
128c2ecf20Sopenharmony_ciimport difflib
138c2ecf20Sopenharmony_ciimport os
148c2ecf20Sopenharmony_ciimport re
158c2ecf20Sopenharmony_ciimport signal
168c2ecf20Sopenharmony_ciimport subprocess
178c2ecf20Sopenharmony_ciimport sys
188c2ecf20Sopenharmony_cifrom multiprocessing import Pool, cpu_count
198c2ecf20Sopenharmony_ci
208c2ecf20Sopenharmony_ci
218c2ecf20Sopenharmony_ci# regex expressions
228c2ecf20Sopenharmony_ciOPERATORS = r"&|\(|\)|\||\!"
238c2ecf20Sopenharmony_ciSYMBOL = r"(?:\w*[A-Z0-9]\w*){2,}"
248c2ecf20Sopenharmony_ciDEF = r"^\s*(?:menu){,1}config\s+(" + SYMBOL + r")\s*"
258c2ecf20Sopenharmony_ciEXPR = r"(?:" + OPERATORS + r"|\s|" + SYMBOL + r")+"
268c2ecf20Sopenharmony_ciDEFAULT = r"default\s+.*?(?:if\s.+){,1}"
278c2ecf20Sopenharmony_ciSTMT = r"^\s*(?:if|select|imply|depends\s+on|(?:" + DEFAULT + r"))\s+" + EXPR
288c2ecf20Sopenharmony_ciSOURCE_SYMBOL = r"(?:\W|\b)+[D]{,1}CONFIG_(" + SYMBOL + r")"
298c2ecf20Sopenharmony_ci
308c2ecf20Sopenharmony_ci# regex objects
318c2ecf20Sopenharmony_ciREGEX_FILE_KCONFIG = re.compile(r".*Kconfig[\.\w+\-]*$")
328c2ecf20Sopenharmony_ciREGEX_SYMBOL = re.compile(r'(?!\B)' + SYMBOL + r'(?!\B)')
338c2ecf20Sopenharmony_ciREGEX_SOURCE_SYMBOL = re.compile(SOURCE_SYMBOL)
348c2ecf20Sopenharmony_ciREGEX_KCONFIG_DEF = re.compile(DEF)
358c2ecf20Sopenharmony_ciREGEX_KCONFIG_EXPR = re.compile(EXPR)
368c2ecf20Sopenharmony_ciREGEX_KCONFIG_STMT = re.compile(STMT)
378c2ecf20Sopenharmony_ciREGEX_KCONFIG_HELP = re.compile(r"^\s+help\s*$")
388c2ecf20Sopenharmony_ciREGEX_FILTER_SYMBOLS = re.compile(r"[A-Za-z0-9]$")
398c2ecf20Sopenharmony_ciREGEX_NUMERIC = re.compile(r"0[xX][0-9a-fA-F]+|[0-9]+")
408c2ecf20Sopenharmony_ciREGEX_QUOTES = re.compile("(\"(.*?)\")")
418c2ecf20Sopenharmony_ci
428c2ecf20Sopenharmony_ci
438c2ecf20Sopenharmony_cidef parse_options():
448c2ecf20Sopenharmony_ci    """The user interface of this module."""
458c2ecf20Sopenharmony_ci    usage = "Run this tool to detect Kconfig symbols that are referenced but " \
468c2ecf20Sopenharmony_ci            "not defined in Kconfig.  If no option is specified, "             \
478c2ecf20Sopenharmony_ci            "checkkconfigsymbols defaults to check your current tree.  "       \
488c2ecf20Sopenharmony_ci            "Please note that specifying commits will 'git reset --hard\' "    \
498c2ecf20Sopenharmony_ci            "your current tree!  You may save uncommitted changes to avoid "   \
508c2ecf20Sopenharmony_ci            "losing data."
518c2ecf20Sopenharmony_ci
528c2ecf20Sopenharmony_ci    parser = argparse.ArgumentParser(description=usage)
538c2ecf20Sopenharmony_ci
548c2ecf20Sopenharmony_ci    parser.add_argument('-c', '--commit', dest='commit', action='store',
558c2ecf20Sopenharmony_ci                        default="",
568c2ecf20Sopenharmony_ci                        help="check if the specified commit (hash) introduces "
578c2ecf20Sopenharmony_ci                             "undefined Kconfig symbols")
588c2ecf20Sopenharmony_ci
598c2ecf20Sopenharmony_ci    parser.add_argument('-d', '--diff', dest='diff', action='store',
608c2ecf20Sopenharmony_ci                        default="",
618c2ecf20Sopenharmony_ci                        help="diff undefined symbols between two commits "
628c2ecf20Sopenharmony_ci                             "(e.g., -d commmit1..commit2)")
638c2ecf20Sopenharmony_ci
648c2ecf20Sopenharmony_ci    parser.add_argument('-f', '--find', dest='find', action='store_true',
658c2ecf20Sopenharmony_ci                        default=False,
668c2ecf20Sopenharmony_ci                        help="find and show commits that may cause symbols to be "
678c2ecf20Sopenharmony_ci                             "missing (required to run with --diff)")
688c2ecf20Sopenharmony_ci
698c2ecf20Sopenharmony_ci    parser.add_argument('-i', '--ignore', dest='ignore', action='store',
708c2ecf20Sopenharmony_ci                        default="",
718c2ecf20Sopenharmony_ci                        help="ignore files matching this Python regex "
728c2ecf20Sopenharmony_ci                             "(e.g., -i '.*defconfig')")
738c2ecf20Sopenharmony_ci
748c2ecf20Sopenharmony_ci    parser.add_argument('-s', '--sim', dest='sim', action='store', default="",
758c2ecf20Sopenharmony_ci                        help="print a list of max. 10 string-similar symbols")
768c2ecf20Sopenharmony_ci
778c2ecf20Sopenharmony_ci    parser.add_argument('--force', dest='force', action='store_true',
788c2ecf20Sopenharmony_ci                        default=False,
798c2ecf20Sopenharmony_ci                        help="reset current Git tree even when it's dirty")
808c2ecf20Sopenharmony_ci
818c2ecf20Sopenharmony_ci    parser.add_argument('--no-color', dest='color', action='store_false',
828c2ecf20Sopenharmony_ci                        default=True,
838c2ecf20Sopenharmony_ci                        help="don't print colored output (default when not "
848c2ecf20Sopenharmony_ci                             "outputting to a terminal)")
858c2ecf20Sopenharmony_ci
868c2ecf20Sopenharmony_ci    args = parser.parse_args()
878c2ecf20Sopenharmony_ci
888c2ecf20Sopenharmony_ci    if args.commit and args.diff:
898c2ecf20Sopenharmony_ci        sys.exit("Please specify only one option at once.")
908c2ecf20Sopenharmony_ci
918c2ecf20Sopenharmony_ci    if args.diff and not re.match(r"^[\w\-\.\^]+\.\.[\w\-\.\^]+$", args.diff):
928c2ecf20Sopenharmony_ci        sys.exit("Please specify valid input in the following format: "
938c2ecf20Sopenharmony_ci                 "\'commit1..commit2\'")
948c2ecf20Sopenharmony_ci
958c2ecf20Sopenharmony_ci    if args.commit or args.diff:
968c2ecf20Sopenharmony_ci        if not args.force and tree_is_dirty():
978c2ecf20Sopenharmony_ci            sys.exit("The current Git tree is dirty (see 'git status').  "
988c2ecf20Sopenharmony_ci                     "Running this script may\ndelete important data since it "
998c2ecf20Sopenharmony_ci                     "calls 'git reset --hard' for some performance\nreasons. "
1008c2ecf20Sopenharmony_ci                     " Please run this script in a clean Git tree or pass "
1018c2ecf20Sopenharmony_ci                     "'--force' if you\nwant to ignore this warning and "
1028c2ecf20Sopenharmony_ci                     "continue.")
1038c2ecf20Sopenharmony_ci
1048c2ecf20Sopenharmony_ci    if args.commit:
1058c2ecf20Sopenharmony_ci        args.find = False
1068c2ecf20Sopenharmony_ci
1078c2ecf20Sopenharmony_ci    if args.ignore:
1088c2ecf20Sopenharmony_ci        try:
1098c2ecf20Sopenharmony_ci            re.match(args.ignore, "this/is/just/a/test.c")
1108c2ecf20Sopenharmony_ci        except:
1118c2ecf20Sopenharmony_ci            sys.exit("Please specify a valid Python regex.")
1128c2ecf20Sopenharmony_ci
1138c2ecf20Sopenharmony_ci    return args
1148c2ecf20Sopenharmony_ci
1158c2ecf20Sopenharmony_ci
1168c2ecf20Sopenharmony_cidef print_undefined_symbols():
1178c2ecf20Sopenharmony_ci    """Main function of this module."""
1188c2ecf20Sopenharmony_ci    args = parse_options()
1198c2ecf20Sopenharmony_ci
1208c2ecf20Sopenharmony_ci    global COLOR
1218c2ecf20Sopenharmony_ci    COLOR = args.color and sys.stdout.isatty()
1228c2ecf20Sopenharmony_ci
1238c2ecf20Sopenharmony_ci    if args.sim and not args.commit and not args.diff:
1248c2ecf20Sopenharmony_ci        sims = find_sims(args.sim, args.ignore)
1258c2ecf20Sopenharmony_ci        if sims:
1268c2ecf20Sopenharmony_ci            print("%s: %s" % (yel("Similar symbols"), ', '.join(sims)))
1278c2ecf20Sopenharmony_ci        else:
1288c2ecf20Sopenharmony_ci            print("%s: no similar symbols found" % yel("Similar symbols"))
1298c2ecf20Sopenharmony_ci        sys.exit(0)
1308c2ecf20Sopenharmony_ci
1318c2ecf20Sopenharmony_ci    # dictionary of (un)defined symbols
1328c2ecf20Sopenharmony_ci    defined = {}
1338c2ecf20Sopenharmony_ci    undefined = {}
1348c2ecf20Sopenharmony_ci
1358c2ecf20Sopenharmony_ci    if args.commit or args.diff:
1368c2ecf20Sopenharmony_ci        head = get_head()
1378c2ecf20Sopenharmony_ci
1388c2ecf20Sopenharmony_ci        # get commit range
1398c2ecf20Sopenharmony_ci        commit_a = None
1408c2ecf20Sopenharmony_ci        commit_b = None
1418c2ecf20Sopenharmony_ci        if args.commit:
1428c2ecf20Sopenharmony_ci            commit_a = args.commit + "~"
1438c2ecf20Sopenharmony_ci            commit_b = args.commit
1448c2ecf20Sopenharmony_ci        elif args.diff:
1458c2ecf20Sopenharmony_ci            split = args.diff.split("..")
1468c2ecf20Sopenharmony_ci            commit_a = split[0]
1478c2ecf20Sopenharmony_ci            commit_b = split[1]
1488c2ecf20Sopenharmony_ci            undefined_a = {}
1498c2ecf20Sopenharmony_ci            undefined_b = {}
1508c2ecf20Sopenharmony_ci
1518c2ecf20Sopenharmony_ci        # get undefined items before the commit
1528c2ecf20Sopenharmony_ci        reset(commit_a)
1538c2ecf20Sopenharmony_ci        undefined_a, _ = check_symbols(args.ignore)
1548c2ecf20Sopenharmony_ci
1558c2ecf20Sopenharmony_ci        # get undefined items for the commit
1568c2ecf20Sopenharmony_ci        reset(commit_b)
1578c2ecf20Sopenharmony_ci        undefined_b, defined = check_symbols(args.ignore)
1588c2ecf20Sopenharmony_ci
1598c2ecf20Sopenharmony_ci        # report cases that are present for the commit but not before
1608c2ecf20Sopenharmony_ci        for symbol in sorted(undefined_b):
1618c2ecf20Sopenharmony_ci            # symbol has not been undefined before
1628c2ecf20Sopenharmony_ci            if symbol not in undefined_a:
1638c2ecf20Sopenharmony_ci                files = sorted(undefined_b.get(symbol))
1648c2ecf20Sopenharmony_ci                undefined[symbol] = files
1658c2ecf20Sopenharmony_ci            # check if there are new files that reference the undefined symbol
1668c2ecf20Sopenharmony_ci            else:
1678c2ecf20Sopenharmony_ci                files = sorted(undefined_b.get(symbol) -
1688c2ecf20Sopenharmony_ci                               undefined_a.get(symbol))
1698c2ecf20Sopenharmony_ci                if files:
1708c2ecf20Sopenharmony_ci                    undefined[symbol] = files
1718c2ecf20Sopenharmony_ci
1728c2ecf20Sopenharmony_ci        # reset to head
1738c2ecf20Sopenharmony_ci        reset(head)
1748c2ecf20Sopenharmony_ci
1758c2ecf20Sopenharmony_ci    # default to check the entire tree
1768c2ecf20Sopenharmony_ci    else:
1778c2ecf20Sopenharmony_ci        undefined, defined = check_symbols(args.ignore)
1788c2ecf20Sopenharmony_ci
1798c2ecf20Sopenharmony_ci    # now print the output
1808c2ecf20Sopenharmony_ci    for symbol in sorted(undefined):
1818c2ecf20Sopenharmony_ci        print(red(symbol))
1828c2ecf20Sopenharmony_ci
1838c2ecf20Sopenharmony_ci        files = sorted(undefined.get(symbol))
1848c2ecf20Sopenharmony_ci        print("%s: %s" % (yel("Referencing files"), ", ".join(files)))
1858c2ecf20Sopenharmony_ci
1868c2ecf20Sopenharmony_ci        sims = find_sims(symbol, args.ignore, defined)
1878c2ecf20Sopenharmony_ci        sims_out = yel("Similar symbols")
1888c2ecf20Sopenharmony_ci        if sims:
1898c2ecf20Sopenharmony_ci            print("%s: %s" % (sims_out, ', '.join(sims)))
1908c2ecf20Sopenharmony_ci        else:
1918c2ecf20Sopenharmony_ci            print("%s: %s" % (sims_out, "no similar symbols found"))
1928c2ecf20Sopenharmony_ci
1938c2ecf20Sopenharmony_ci        if args.find:
1948c2ecf20Sopenharmony_ci            print("%s:" % yel("Commits changing symbol"))
1958c2ecf20Sopenharmony_ci            commits = find_commits(symbol, args.diff)
1968c2ecf20Sopenharmony_ci            if commits:
1978c2ecf20Sopenharmony_ci                for commit in commits:
1988c2ecf20Sopenharmony_ci                    commit = commit.split(" ", 1)
1998c2ecf20Sopenharmony_ci                    print("\t- %s (\"%s\")" % (yel(commit[0]), commit[1]))
2008c2ecf20Sopenharmony_ci            else:
2018c2ecf20Sopenharmony_ci                print("\t- no commit found")
2028c2ecf20Sopenharmony_ci        print()  # new line
2038c2ecf20Sopenharmony_ci
2048c2ecf20Sopenharmony_ci
2058c2ecf20Sopenharmony_cidef reset(commit):
2068c2ecf20Sopenharmony_ci    """Reset current git tree to %commit."""
2078c2ecf20Sopenharmony_ci    execute(["git", "reset", "--hard", commit])
2088c2ecf20Sopenharmony_ci
2098c2ecf20Sopenharmony_ci
2108c2ecf20Sopenharmony_cidef yel(string):
2118c2ecf20Sopenharmony_ci    """
2128c2ecf20Sopenharmony_ci    Color %string yellow.
2138c2ecf20Sopenharmony_ci    """
2148c2ecf20Sopenharmony_ci    return "\033[33m%s\033[0m" % string if COLOR else string
2158c2ecf20Sopenharmony_ci
2168c2ecf20Sopenharmony_ci
2178c2ecf20Sopenharmony_cidef red(string):
2188c2ecf20Sopenharmony_ci    """
2198c2ecf20Sopenharmony_ci    Color %string red.
2208c2ecf20Sopenharmony_ci    """
2218c2ecf20Sopenharmony_ci    return "\033[31m%s\033[0m" % string if COLOR else string
2228c2ecf20Sopenharmony_ci
2238c2ecf20Sopenharmony_ci
2248c2ecf20Sopenharmony_cidef execute(cmd):
2258c2ecf20Sopenharmony_ci    """Execute %cmd and return stdout.  Exit in case of error."""
2268c2ecf20Sopenharmony_ci    try:
2278c2ecf20Sopenharmony_ci        stdout = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=False)
2288c2ecf20Sopenharmony_ci        stdout = stdout.decode(errors='replace')
2298c2ecf20Sopenharmony_ci    except subprocess.CalledProcessError as fail:
2308c2ecf20Sopenharmony_ci        exit(fail)
2318c2ecf20Sopenharmony_ci    return stdout
2328c2ecf20Sopenharmony_ci
2338c2ecf20Sopenharmony_ci
2348c2ecf20Sopenharmony_cidef find_commits(symbol, diff):
2358c2ecf20Sopenharmony_ci    """Find commits changing %symbol in the given range of %diff."""
2368c2ecf20Sopenharmony_ci    commits = execute(["git", "log", "--pretty=oneline",
2378c2ecf20Sopenharmony_ci                       "--abbrev-commit", "-G",
2388c2ecf20Sopenharmony_ci                       symbol, diff])
2398c2ecf20Sopenharmony_ci    return [x for x in commits.split("\n") if x]
2408c2ecf20Sopenharmony_ci
2418c2ecf20Sopenharmony_ci
2428c2ecf20Sopenharmony_cidef tree_is_dirty():
2438c2ecf20Sopenharmony_ci    """Return true if the current working tree is dirty (i.e., if any file has
2448c2ecf20Sopenharmony_ci    been added, deleted, modified, renamed or copied but not committed)."""
2458c2ecf20Sopenharmony_ci    stdout = execute(["git", "status", "--porcelain"])
2468c2ecf20Sopenharmony_ci    for line in stdout:
2478c2ecf20Sopenharmony_ci        if re.findall(r"[URMADC]{1}", line[:2]):
2488c2ecf20Sopenharmony_ci            return True
2498c2ecf20Sopenharmony_ci    return False
2508c2ecf20Sopenharmony_ci
2518c2ecf20Sopenharmony_ci
2528c2ecf20Sopenharmony_cidef get_head():
2538c2ecf20Sopenharmony_ci    """Return commit hash of current HEAD."""
2548c2ecf20Sopenharmony_ci    stdout = execute(["git", "rev-parse", "HEAD"])
2558c2ecf20Sopenharmony_ci    return stdout.strip('\n')
2568c2ecf20Sopenharmony_ci
2578c2ecf20Sopenharmony_ci
2588c2ecf20Sopenharmony_cidef partition(lst, size):
2598c2ecf20Sopenharmony_ci    """Partition list @lst into eveni-sized lists of size @size."""
2608c2ecf20Sopenharmony_ci    return [lst[i::size] for i in range(size)]
2618c2ecf20Sopenharmony_ci
2628c2ecf20Sopenharmony_ci
2638c2ecf20Sopenharmony_cidef init_worker():
2648c2ecf20Sopenharmony_ci    """Set signal handler to ignore SIGINT."""
2658c2ecf20Sopenharmony_ci    signal.signal(signal.SIGINT, signal.SIG_IGN)
2668c2ecf20Sopenharmony_ci
2678c2ecf20Sopenharmony_ci
2688c2ecf20Sopenharmony_cidef find_sims(symbol, ignore, defined=[]):
2698c2ecf20Sopenharmony_ci    """Return a list of max. ten Kconfig symbols that are string-similar to
2708c2ecf20Sopenharmony_ci    @symbol."""
2718c2ecf20Sopenharmony_ci    if defined:
2728c2ecf20Sopenharmony_ci        return difflib.get_close_matches(symbol, set(defined), 10)
2738c2ecf20Sopenharmony_ci
2748c2ecf20Sopenharmony_ci    pool = Pool(cpu_count(), init_worker)
2758c2ecf20Sopenharmony_ci    kfiles = []
2768c2ecf20Sopenharmony_ci    for gitfile in get_files():
2778c2ecf20Sopenharmony_ci        if REGEX_FILE_KCONFIG.match(gitfile):
2788c2ecf20Sopenharmony_ci            kfiles.append(gitfile)
2798c2ecf20Sopenharmony_ci
2808c2ecf20Sopenharmony_ci    arglist = []
2818c2ecf20Sopenharmony_ci    for part in partition(kfiles, cpu_count()):
2828c2ecf20Sopenharmony_ci        arglist.append((part, ignore))
2838c2ecf20Sopenharmony_ci
2848c2ecf20Sopenharmony_ci    for res in pool.map(parse_kconfig_files, arglist):
2858c2ecf20Sopenharmony_ci        defined.extend(res[0])
2868c2ecf20Sopenharmony_ci
2878c2ecf20Sopenharmony_ci    return difflib.get_close_matches(symbol, set(defined), 10)
2888c2ecf20Sopenharmony_ci
2898c2ecf20Sopenharmony_ci
2908c2ecf20Sopenharmony_cidef get_files():
2918c2ecf20Sopenharmony_ci    """Return a list of all files in the current git directory."""
2928c2ecf20Sopenharmony_ci    # use 'git ls-files' to get the worklist
2938c2ecf20Sopenharmony_ci    stdout = execute(["git", "ls-files"])
2948c2ecf20Sopenharmony_ci    if len(stdout) > 0 and stdout[-1] == "\n":
2958c2ecf20Sopenharmony_ci        stdout = stdout[:-1]
2968c2ecf20Sopenharmony_ci
2978c2ecf20Sopenharmony_ci    files = []
2988c2ecf20Sopenharmony_ci    for gitfile in stdout.rsplit("\n"):
2998c2ecf20Sopenharmony_ci        if ".git" in gitfile or "ChangeLog" in gitfile or      \
3008c2ecf20Sopenharmony_ci                ".log" in gitfile or os.path.isdir(gitfile) or \
3018c2ecf20Sopenharmony_ci                gitfile.startswith("tools/"):
3028c2ecf20Sopenharmony_ci            continue
3038c2ecf20Sopenharmony_ci        files.append(gitfile)
3048c2ecf20Sopenharmony_ci    return files
3058c2ecf20Sopenharmony_ci
3068c2ecf20Sopenharmony_ci
3078c2ecf20Sopenharmony_cidef check_symbols(ignore):
3088c2ecf20Sopenharmony_ci    """Find undefined Kconfig symbols and return a dict with the symbol as key
3098c2ecf20Sopenharmony_ci    and a list of referencing files as value.  Files matching %ignore are not
3108c2ecf20Sopenharmony_ci    checked for undefined symbols."""
3118c2ecf20Sopenharmony_ci    pool = Pool(cpu_count(), init_worker)
3128c2ecf20Sopenharmony_ci    try:
3138c2ecf20Sopenharmony_ci        return check_symbols_helper(pool, ignore)
3148c2ecf20Sopenharmony_ci    except KeyboardInterrupt:
3158c2ecf20Sopenharmony_ci        pool.terminate()
3168c2ecf20Sopenharmony_ci        pool.join()
3178c2ecf20Sopenharmony_ci        sys.exit(1)
3188c2ecf20Sopenharmony_ci
3198c2ecf20Sopenharmony_ci
3208c2ecf20Sopenharmony_cidef check_symbols_helper(pool, ignore):
3218c2ecf20Sopenharmony_ci    """Helper method for check_symbols().  Used to catch keyboard interrupts in
3228c2ecf20Sopenharmony_ci    check_symbols() in order to properly terminate running worker processes."""
3238c2ecf20Sopenharmony_ci    source_files = []
3248c2ecf20Sopenharmony_ci    kconfig_files = []
3258c2ecf20Sopenharmony_ci    defined_symbols = []
3268c2ecf20Sopenharmony_ci    referenced_symbols = dict()  # {file: [symbols]}
3278c2ecf20Sopenharmony_ci
3288c2ecf20Sopenharmony_ci    for gitfile in get_files():
3298c2ecf20Sopenharmony_ci        if REGEX_FILE_KCONFIG.match(gitfile):
3308c2ecf20Sopenharmony_ci            kconfig_files.append(gitfile)
3318c2ecf20Sopenharmony_ci        else:
3328c2ecf20Sopenharmony_ci            if ignore and not re.match(ignore, gitfile):
3338c2ecf20Sopenharmony_ci                continue
3348c2ecf20Sopenharmony_ci            # add source files that do not match the ignore pattern
3358c2ecf20Sopenharmony_ci            source_files.append(gitfile)
3368c2ecf20Sopenharmony_ci
3378c2ecf20Sopenharmony_ci    # parse source files
3388c2ecf20Sopenharmony_ci    arglist = partition(source_files, cpu_count())
3398c2ecf20Sopenharmony_ci    for res in pool.map(parse_source_files, arglist):
3408c2ecf20Sopenharmony_ci        referenced_symbols.update(res)
3418c2ecf20Sopenharmony_ci
3428c2ecf20Sopenharmony_ci    # parse kconfig files
3438c2ecf20Sopenharmony_ci    arglist = []
3448c2ecf20Sopenharmony_ci    for part in partition(kconfig_files, cpu_count()):
3458c2ecf20Sopenharmony_ci        arglist.append((part, ignore))
3468c2ecf20Sopenharmony_ci    for res in pool.map(parse_kconfig_files, arglist):
3478c2ecf20Sopenharmony_ci        defined_symbols.extend(res[0])
3488c2ecf20Sopenharmony_ci        referenced_symbols.update(res[1])
3498c2ecf20Sopenharmony_ci    defined_symbols = set(defined_symbols)
3508c2ecf20Sopenharmony_ci
3518c2ecf20Sopenharmony_ci    # inverse mapping of referenced_symbols to dict(symbol: [files])
3528c2ecf20Sopenharmony_ci    inv_map = dict()
3538c2ecf20Sopenharmony_ci    for _file, symbols in referenced_symbols.items():
3548c2ecf20Sopenharmony_ci        for symbol in symbols:
3558c2ecf20Sopenharmony_ci            inv_map[symbol] = inv_map.get(symbol, set())
3568c2ecf20Sopenharmony_ci            inv_map[symbol].add(_file)
3578c2ecf20Sopenharmony_ci    referenced_symbols = inv_map
3588c2ecf20Sopenharmony_ci
3598c2ecf20Sopenharmony_ci    undefined = {}  # {symbol: [files]}
3608c2ecf20Sopenharmony_ci    for symbol in sorted(referenced_symbols):
3618c2ecf20Sopenharmony_ci        # filter some false positives
3628c2ecf20Sopenharmony_ci        if symbol == "FOO" or symbol == "BAR" or \
3638c2ecf20Sopenharmony_ci                symbol == "FOO_BAR" or symbol == "XXX":
3648c2ecf20Sopenharmony_ci            continue
3658c2ecf20Sopenharmony_ci        if symbol not in defined_symbols:
3668c2ecf20Sopenharmony_ci            if symbol.endswith("_MODULE"):
3678c2ecf20Sopenharmony_ci                # avoid false positives for kernel modules
3688c2ecf20Sopenharmony_ci                if symbol[:-len("_MODULE")] in defined_symbols:
3698c2ecf20Sopenharmony_ci                    continue
3708c2ecf20Sopenharmony_ci            undefined[symbol] = referenced_symbols.get(symbol)
3718c2ecf20Sopenharmony_ci    return undefined, defined_symbols
3728c2ecf20Sopenharmony_ci
3738c2ecf20Sopenharmony_ci
3748c2ecf20Sopenharmony_cidef parse_source_files(source_files):
3758c2ecf20Sopenharmony_ci    """Parse each source file in @source_files and return dictionary with source
3768c2ecf20Sopenharmony_ci    files as keys and lists of references Kconfig symbols as values."""
3778c2ecf20Sopenharmony_ci    referenced_symbols = dict()
3788c2ecf20Sopenharmony_ci    for sfile in source_files:
3798c2ecf20Sopenharmony_ci        referenced_symbols[sfile] = parse_source_file(sfile)
3808c2ecf20Sopenharmony_ci    return referenced_symbols
3818c2ecf20Sopenharmony_ci
3828c2ecf20Sopenharmony_ci
3838c2ecf20Sopenharmony_cidef parse_source_file(sfile):
3848c2ecf20Sopenharmony_ci    """Parse @sfile and return a list of referenced Kconfig symbols."""
3858c2ecf20Sopenharmony_ci    lines = []
3868c2ecf20Sopenharmony_ci    references = []
3878c2ecf20Sopenharmony_ci
3888c2ecf20Sopenharmony_ci    if not os.path.exists(sfile):
3898c2ecf20Sopenharmony_ci        return references
3908c2ecf20Sopenharmony_ci
3918c2ecf20Sopenharmony_ci    with open(sfile, "r", encoding='utf-8', errors='replace') as stream:
3928c2ecf20Sopenharmony_ci        lines = stream.readlines()
3938c2ecf20Sopenharmony_ci
3948c2ecf20Sopenharmony_ci    for line in lines:
3958c2ecf20Sopenharmony_ci        if "CONFIG_" not in line:
3968c2ecf20Sopenharmony_ci            continue
3978c2ecf20Sopenharmony_ci        symbols = REGEX_SOURCE_SYMBOL.findall(line)
3988c2ecf20Sopenharmony_ci        for symbol in symbols:
3998c2ecf20Sopenharmony_ci            if not REGEX_FILTER_SYMBOLS.search(symbol):
4008c2ecf20Sopenharmony_ci                continue
4018c2ecf20Sopenharmony_ci            references.append(symbol)
4028c2ecf20Sopenharmony_ci
4038c2ecf20Sopenharmony_ci    return references
4048c2ecf20Sopenharmony_ci
4058c2ecf20Sopenharmony_ci
4068c2ecf20Sopenharmony_cidef get_symbols_in_line(line):
4078c2ecf20Sopenharmony_ci    """Return mentioned Kconfig symbols in @line."""
4088c2ecf20Sopenharmony_ci    return REGEX_SYMBOL.findall(line)
4098c2ecf20Sopenharmony_ci
4108c2ecf20Sopenharmony_ci
4118c2ecf20Sopenharmony_cidef parse_kconfig_files(args):
4128c2ecf20Sopenharmony_ci    """Parse kconfig files and return tuple of defined and references Kconfig
4138c2ecf20Sopenharmony_ci    symbols.  Note, @args is a tuple of a list of files and the @ignore
4148c2ecf20Sopenharmony_ci    pattern."""
4158c2ecf20Sopenharmony_ci    kconfig_files = args[0]
4168c2ecf20Sopenharmony_ci    ignore = args[1]
4178c2ecf20Sopenharmony_ci    defined_symbols = []
4188c2ecf20Sopenharmony_ci    referenced_symbols = dict()
4198c2ecf20Sopenharmony_ci
4208c2ecf20Sopenharmony_ci    for kfile in kconfig_files:
4218c2ecf20Sopenharmony_ci        defined, references = parse_kconfig_file(kfile)
4228c2ecf20Sopenharmony_ci        defined_symbols.extend(defined)
4238c2ecf20Sopenharmony_ci        if ignore and re.match(ignore, kfile):
4248c2ecf20Sopenharmony_ci            # do not collect references for files that match the ignore pattern
4258c2ecf20Sopenharmony_ci            continue
4268c2ecf20Sopenharmony_ci        referenced_symbols[kfile] = references
4278c2ecf20Sopenharmony_ci    return (defined_symbols, referenced_symbols)
4288c2ecf20Sopenharmony_ci
4298c2ecf20Sopenharmony_ci
4308c2ecf20Sopenharmony_cidef parse_kconfig_file(kfile):
4318c2ecf20Sopenharmony_ci    """Parse @kfile and update symbol definitions and references."""
4328c2ecf20Sopenharmony_ci    lines = []
4338c2ecf20Sopenharmony_ci    defined = []
4348c2ecf20Sopenharmony_ci    references = []
4358c2ecf20Sopenharmony_ci    skip = False
4368c2ecf20Sopenharmony_ci
4378c2ecf20Sopenharmony_ci    if not os.path.exists(kfile):
4388c2ecf20Sopenharmony_ci        return defined, references
4398c2ecf20Sopenharmony_ci
4408c2ecf20Sopenharmony_ci    with open(kfile, "r", encoding='utf-8', errors='replace') as stream:
4418c2ecf20Sopenharmony_ci        lines = stream.readlines()
4428c2ecf20Sopenharmony_ci
4438c2ecf20Sopenharmony_ci    for i in range(len(lines)):
4448c2ecf20Sopenharmony_ci        line = lines[i]
4458c2ecf20Sopenharmony_ci        line = line.strip('\n')
4468c2ecf20Sopenharmony_ci        line = line.split("#")[0]  # ignore comments
4478c2ecf20Sopenharmony_ci
4488c2ecf20Sopenharmony_ci        if REGEX_KCONFIG_DEF.match(line):
4498c2ecf20Sopenharmony_ci            symbol_def = REGEX_KCONFIG_DEF.findall(line)
4508c2ecf20Sopenharmony_ci            defined.append(symbol_def[0])
4518c2ecf20Sopenharmony_ci            skip = False
4528c2ecf20Sopenharmony_ci        elif REGEX_KCONFIG_HELP.match(line):
4538c2ecf20Sopenharmony_ci            skip = True
4548c2ecf20Sopenharmony_ci        elif skip:
4558c2ecf20Sopenharmony_ci            # ignore content of help messages
4568c2ecf20Sopenharmony_ci            pass
4578c2ecf20Sopenharmony_ci        elif REGEX_KCONFIG_STMT.match(line):
4588c2ecf20Sopenharmony_ci            line = REGEX_QUOTES.sub("", line)
4598c2ecf20Sopenharmony_ci            symbols = get_symbols_in_line(line)
4608c2ecf20Sopenharmony_ci            # multi-line statements
4618c2ecf20Sopenharmony_ci            while line.endswith("\\"):
4628c2ecf20Sopenharmony_ci                i += 1
4638c2ecf20Sopenharmony_ci                line = lines[i]
4648c2ecf20Sopenharmony_ci                line = line.strip('\n')
4658c2ecf20Sopenharmony_ci                symbols.extend(get_symbols_in_line(line))
4668c2ecf20Sopenharmony_ci            for symbol in set(symbols):
4678c2ecf20Sopenharmony_ci                if REGEX_NUMERIC.match(symbol):
4688c2ecf20Sopenharmony_ci                    # ignore numeric values
4698c2ecf20Sopenharmony_ci                    continue
4708c2ecf20Sopenharmony_ci                references.append(symbol)
4718c2ecf20Sopenharmony_ci
4728c2ecf20Sopenharmony_ci    return defined, references
4738c2ecf20Sopenharmony_ci
4748c2ecf20Sopenharmony_ci
4758c2ecf20Sopenharmony_cidef main():
4768c2ecf20Sopenharmony_ci    try:
4778c2ecf20Sopenharmony_ci        print_undefined_symbols()
4788c2ecf20Sopenharmony_ci    except BrokenPipeError:
4798c2ecf20Sopenharmony_ci        # Python flushes standard streams on exit; redirect remaining output
4808c2ecf20Sopenharmony_ci        # to devnull to avoid another BrokenPipeError at shutdown
4818c2ecf20Sopenharmony_ci        devnull = os.open(os.devnull, os.O_WRONLY)
4828c2ecf20Sopenharmony_ci        os.dup2(devnull, sys.stdout.fileno())
4838c2ecf20Sopenharmony_ci        sys.exit(1)  # Python exits with error code 1 on EPIPE
4848c2ecf20Sopenharmony_ci
4858c2ecf20Sopenharmony_ci
4868c2ecf20Sopenharmony_ciif __name__ == "__main__":
4878c2ecf20Sopenharmony_ci    main()
488