summaryrefslogtreecommitdiff
path: root/scripts/macro_checker.py
blob: ba550982e98f09729d2d611d8768821852c9f5b0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#!/usr/bin/python3
# SPDX-License-Identifier: GPL-2.0
# Author: Julian Sun <sunjunchao2870@gmail.com>

""" Find macro definitions with unused parameters. """

import argparse
import os
import re

parser = argparse.ArgumentParser()

parser.add_argument("path", type=str, help="The file or dir path that needs check")
parser.add_argument("-v", "--verbose", action="store_true",
                    help="Check conditional macros, but may lead to more false positives")
args = parser.parse_args()

macro_pattern = r"#define\s+(\w+)\(([^)]*)\)"
# below vars were used to reduce false positives
fp_patterns = [r"\s*do\s*\{\s*\}\s*while\s*\(\s*0\s*\)",
               r"\(?0\)?", r"\(?1\)?"]
correct_macros = []
cond_compile_mark = "#if"
cond_compile_end = "#endif"

def check_macro(macro_line, report):
    match = re.match(macro_pattern, macro_line)
    if match:
        macro_def = re.sub(macro_pattern, '', macro_line)
        identifier = match.group(1)
        content = match.group(2)
        arguments = [item.strip() for item in content.split(',') if item.strip()]

        macro_def = macro_def.strip()
        if not macro_def:
            return
        # used to reduce false positives, like #define endfor_nexthops(rt) }
        if len(macro_def) == 1:
            return

        for fp_pattern in fp_patterns:
            if (re.match(fp_pattern, macro_def)):
                return

        for arg in arguments:
            # used to reduce false positives
            if "..." in arg:
                return
        for arg in arguments:
            if not arg in macro_def and report == False:
                return
            # if there is a correct macro with the same name, do not report it.
            if not arg in macro_def and identifier not in correct_macros:
                print(f"Argument {arg} is not used in function-line macro {identifier}")
                return

        correct_macros.append(identifier)


# remove comment and whitespace
def macro_strip(macro):
    comment_pattern1 = r"\/\/*"
    comment_pattern2 = r"\/\**\*\/"

    macro = macro.strip()
    macro = re.sub(comment_pattern1, '', macro)
    macro = re.sub(comment_pattern2, '', macro)

    return macro

def file_check_macro(file_path, report):
    # number of conditional compiling
    cond_compile = 0
    # only check .c and .h file
    if not file_path.endswith(".c") and not file_path.endswith(".h"):
        return

    with open(file_path, "r") as f:
        while True:
            line = f.readline()
            if not line:
                break
            line = line.strip()
            if line.startswith(cond_compile_mark):
                cond_compile += 1
                continue
            if line.startswith(cond_compile_end):
                cond_compile -= 1
                continue

            macro = re.match(macro_pattern, line)
            if macro:
                macro = macro_strip(macro.string)
                while macro[-1] == '\\':
                    macro = macro[0:-1]
                    macro = macro.strip()
                    macro += f.readline()
                    macro = macro_strip(macro)
                if not args.verbose:
                    if file_path.endswith(".c")  and cond_compile != 0:
                        continue
                    # 1 is for #ifdef xxx at the beginning of the header file
                    if file_path.endswith(".h") and cond_compile != 1:
                        continue
                check_macro(macro, report)

def get_correct_macros(path):
    file_check_macro(path, False)

def dir_check_macro(dir_path):

    for dentry in os.listdir(dir_path):
        path = os.path.join(dir_path, dentry)
        if os.path.isdir(path):
            dir_check_macro(path)
        elif os.path.isfile(path):
            get_correct_macros(path)
            file_check_macro(path, True)


def main():
    if os.path.isfile(args.path):
        get_correct_macros(args.path)
        file_check_macro(args.path, True)
    elif os.path.isdir(args.path):
        dir_check_macro(args.path)
    else:
        print(f"{args.path} doesn't exit or is neither a file nor a dir")

if __name__ == "__main__":
    main()