1#!/usr/bin/python3
2# SPDX-License-Identifier: GPL-2.0
3# Author: Julian Sun <sunjunchao2870@gmail.com>
4
5""" Find macro definitions with unused parameters. """
6
7import argparse
8import os
9import re
10
11parser = argparse.ArgumentParser()
12
13parser.add_argument("path", type=str, help="The file or dir path that needs check")
14parser.add_argument("-v", "--verbose", action="store_true",
15                    help="Check conditional macros, but may lead to more false positives")
16args = parser.parse_args()
17
18macro_pattern = r"#define\s+(\w+)\(([^)]*)\)"
19# below vars were used to reduce false positives
20fp_patterns = [r"\s*do\s*\{\s*\}\s*while\s*\(\s*0\s*\)",
21               r"\(?0\)?", r"\(?1\)?"]
22correct_macros = []
23cond_compile_mark = "#if"
24cond_compile_end = "#endif"
25
26def check_macro(macro_line, report):
27    match = re.match(macro_pattern, macro_line)
28    if match:
29        macro_def = re.sub(macro_pattern, '', macro_line)
30        identifier = match.group(1)
31        content = match.group(2)
32        arguments = [item.strip() for item in content.split(',') if item.strip()]
33
34        macro_def = macro_def.strip()
35        if not macro_def:
36            return
37        # used to reduce false positives, like #define endfor_nexthops(rt) }
38        if len(macro_def) == 1:
39            return
40
41        for fp_pattern in fp_patterns:
42            if (re.match(fp_pattern, macro_def)):
43                return
44
45        for arg in arguments:
46            # used to reduce false positives
47            if "..." in arg:
48                return
49        for arg in arguments:
50            if not arg in macro_def and report == False:
51                return
52            # if there is a correct macro with the same name, do not report it.
53            if not arg in macro_def and identifier not in correct_macros:
54                print(f"Argument {arg} is not used in function-line macro {identifier}")
55                return
56
57        correct_macros.append(identifier)
58
59
60# remove comment and whitespace
61def macro_strip(macro):
62    comment_pattern1 = r"\/\/*"
63    comment_pattern2 = r"\/\**\*\/"
64
65    macro = macro.strip()
66    macro = re.sub(comment_pattern1, '', macro)
67    macro = re.sub(comment_pattern2, '', macro)
68
69    return macro
70
71def file_check_macro(file_path, report):
72    # number of conditional compiling
73    cond_compile = 0
74    # only check .c and .h file
75    if not file_path.endswith(".c") and not file_path.endswith(".h"):
76        return
77
78    with open(file_path, "r") as f:
79        while True:
80            line = f.readline()
81            if not line:
82                break
83            line = line.strip()
84            if line.startswith(cond_compile_mark):
85                cond_compile += 1
86                continue
87            if line.startswith(cond_compile_end):
88                cond_compile -= 1
89                continue
90
91            macro = re.match(macro_pattern, line)
92            if macro:
93                macro = macro_strip(macro.string)
94                while macro[-1] == '\\':
95                    macro = macro[0:-1]
96                    macro = macro.strip()
97                    macro += f.readline()
98                    macro = macro_strip(macro)
99                if not args.verbose:
100                    if file_path.endswith(".c")  and cond_compile != 0:
101                        continue
102                    # 1 is for #ifdef xxx at the beginning of the header file
103                    if file_path.endswith(".h") and cond_compile != 1:
104                        continue
105                check_macro(macro, report)
106
107def get_correct_macros(path):
108    file_check_macro(path, False)
109
110def dir_check_macro(dir_path):
111
112    for dentry in os.listdir(dir_path):
113        path = os.path.join(dir_path, dentry)
114        if os.path.isdir(path):
115            dir_check_macro(path)
116        elif os.path.isfile(path):
117            get_correct_macros(path)
118            file_check_macro(path, True)
119
120
121def main():
122    if os.path.isfile(args.path):
123        get_correct_macros(args.path)
124        file_check_macro(args.path, True)
125    elif os.path.isdir(args.path):
126        dir_check_macro(args.path)
127    else:
128        print(f"{args.path} doesn't exit or is neither a file nor a dir")
129
130if __name__ == "__main__":
131    main()