| #!/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() |