1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Copyright (c) 2023-2024 Huawei Device Co., Ltd. 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16import argparse 17import os 18import re 19import sys 20 21 22def get_args(): 23 parser = argparse.ArgumentParser( 24 description="Doxygen style checker for panda project.") 25 parser.add_argument( 26 'panda_dir', help='panda sources directory.', type=str) 27 28 return parser.parse_args() 29 30 31def get_file_list(panda_dir) -> list: 32 src_exts = (".c", '.cc', ".cp", ".cxx", ".cpp", ".CPP", ".c++", ".C", ".h", 33 ".hh", ".H", ".hp", ".hxx", ".hpp", ".HPP", ".h++", ".tcc") 34 skip_dirs = ["third_party", "artifacts", "\..*", "build.*"] 35 file_list = [] 36 for dirpath, dirnames, filenames in os.walk(panda_dir): 37 dirnames[:] = [d for d in dirnames if not re.match(f"({')|('.join(skip_dirs)})", d)] 38 for fname in filenames: 39 if (fname.endswith(src_exts)): 40 full_path = os.path.join(panda_dir, dirpath, fname) 41 full_path = str(os.path.realpath(full_path)) 42 file_list.append(full_path) 43 44 return file_list 45 46 47# Additional check because regexps sometimes find correct comments 48def is_doxygen_comment(s: str) -> bool: 49 # Helps not to raise warnings for fine comments 50 # Examples of fine comments: /************/, /* ///////TEXT */ 51 fine_comments = [re.compile(r'///[^\n]*\*/[^\n]*'), re.compile(r'/\*\*[^\n]*\*/')] 52 for comm in fine_comments: 53 if comm.search(s): 54 return False 55 return True 56 57 58def print_correct_style() -> None: 59 lines = ["\nPlease, for single-line doxygen comments use the following formats:",\ 60 "'/// TEXT' - used for commenting on an empty line", "or",\ 61 "'///< TEXT' - used for commenting after declared/defined variables in the same line",\ 62 "\nand for multi-line doxygen comments use the following Javadoc format:" 63 "\n/**\n * TEXT\n * TEXT\n * TEXT\n */"] 64 for line in lines: 65 print(line) 66 67 68def check_keywords(src_path: str, splitted_lines: list, line_num: int) -> bool: 69 is_right_style = True 70 keywords_to_check = ["brief", "param", "tparam", "return", "class", "see", "code", "endcode"] 71 for line in splitted_lines: 72 for keyword in keywords_to_check: 73 ind = line.find(keyword) 74 if ind != -1 and line[ind - 1] == '\\': 75 err_msg = "%s:%s" % (src_path, line_num) 76 print(err_msg) 77 print("Please, use '@' instead of '\\' before '%s':\n%s\n" % (keyword, line)) 78 is_right_style = False 79 line_num += 1 80 return is_right_style 81 82 83def check_javadoc(src_path: str, strings: list) -> bool: 84 text = [] 85 with open(src_path, 'r') as f: 86 text = f.read() 87 found_wrong_comment = False 88 found_wrong_keyword_sign = False 89 for string in strings: 90 line_num = text[:text.find(string)].count('\n') + 1 91 if string.find("Copyright") != -1 or string.count('\n') <= 1: 92 continue 93 pattern_to_check = re.search(r' */\*\* *\n( *\* *[^\n]*\n)+ *\*/', string) 94 if not pattern_to_check or pattern_to_check.group(0) != string: 95 err_msg = "%s:%s" % (src_path, line_num) 96 print(err_msg) 97 print("Found doxygen comment with a wrong Javadoc style:\n%s\n" % string) 98 found_wrong_comment = True 99 continue 100 if string.count('\n') == 2: 101 err_msg = "%s:%s" % (src_path, line_num) 102 print("%s\n%s\n" % (err_msg, string)) 103 found_wrong_comment = True 104 continue 105 found_wrong_keyword_sign |= not check_keywords(src_path, string.splitlines(), line_num) 106 if found_wrong_comment: 107 print_correct_style() 108 return not (found_wrong_comment or found_wrong_keyword_sign) 109 110 111def check_additional_slashes(src_path: str, strings: list) -> bool: 112 text = [] 113 with open(src_path, 'r') as f: 114 text = f.read() 115 lines = text.splitlines() 116 found_wrong_comment = False 117 found_wrong_keyword_sign = False 118 fine_comments_lines = [] 119 strings = list(set(strings)) # Only unique strings left 120 for string in strings: 121 # Next line is used to find all occurencies of a given string 122 str_indexes = [s.start() for s in re.finditer(re.escape(string), text)] 123 for str_index in str_indexes: 124 line_num = text[:str_index].count('\n') + 1 125 pattern_to_check = re.search(r' */// [^ ]+?[^\n]*', lines[line_num - 1]) 126 if not pattern_to_check or pattern_to_check.group(0) != lines[line_num - 1]: 127 err_msg = "%s:%s" % (src_path, line_num) 128 print(err_msg) 129 print("Found doxygen comment with a wrong style:\n%s\n" % string) 130 found_wrong_comment = True 131 continue 132 fine_comments_lines.append(line_num) 133 found_wrong_keyword_sign |= not check_keywords(src_path, string.splitlines(), line_num) 134 135 fine_comments_lines.sort() 136 for i in range(0, len(fine_comments_lines) - 1): 137 if fine_comments_lines[i] + 1 == fine_comments_lines[i + 1]: 138 err_msg = "%s:%s" % (src_path, fine_comments_lines[i]) 139 print(err_msg) 140 print("Please, use '///' only for single-line comments:\n%s\n%s\n" % ( 141 lines[fine_comments_lines[i] - 1], 142 lines[fine_comments_lines[i + 1] - 1])) 143 found_wrong_comment = True 144 break 145 if found_wrong_comment: 146 print_correct_style() 147 return not (found_wrong_comment or found_wrong_keyword_sign) 148 149 150def check_less_than_slashes(src_path: str, strings: list) -> bool: 151 text = [] 152 with open(src_path, 'r') as f: 153 text = f.read() 154 lines = text.splitlines() 155 found_wrong_comment = False 156 found_wrong_keyword_sign = False 157 for string in strings: 158 line_num = text[:text.find(string)].count('\n') + 1 159 pattern_to_check = re.search(r' *[^ \n]+[^\n]* +///< [^\n]+', lines[line_num - 1]) 160 if not pattern_to_check or pattern_to_check.group(0) != lines[line_num - 1]: 161 err_msg = "%s:%s" % (src_path, line_num) 162 print(err_msg) 163 print("Found doxygen comment with a wrong style:\n%s\n" % string) 164 found_wrong_comment = True 165 continue 166 found_wrong_keyword_sign |= not check_keywords(src_path, string.splitlines(), line_num) 167 if found_wrong_comment: 168 print_correct_style() 169 return not (found_wrong_comment or found_wrong_keyword_sign) 170 171 172def check_all(src_path: str, fine_patterns_found: list, wrong_patterns_number: int) -> bool: 173 passed = wrong_patterns_number == 0 174 passed &= check_javadoc(src_path, fine_patterns_found[0]) 175 passed &= check_additional_slashes(src_path, fine_patterns_found[1]) 176 passed &= check_less_than_slashes(src_path, fine_patterns_found[2]) 177 return passed 178 179 180def run_doxygen_check(src_path: str, msg: str) -> bool: 181 print(msg) 182 # Forbidden styles 183 qt_style = re.compile(r'/\*![^\n]*') 184 slashes_with_exclamation_style = re.compile(r'//![^\n]*') 185 # Allowed styles 186 # Allowed if number of lines in a comment is >= 2 187 javadoc_style = re.compile(r' */\*\*[\w\W]*?\*/') 188 # Allowed to comment only one line. Otherwise javadoc style should be used 189 additional_slashes_style = re.compile(r'/// *[^< ][^\n]*') 190 # Allowed to comment declared/defined variables in the same line 191 less_than_slashes_style = re.compile(r'/// *< *[^\n]*') 192 193 regexps_for_fine_styles = [javadoc_style, additional_slashes_style, less_than_slashes_style] 194 regexps_for_wrong_styles = [qt_style, slashes_with_exclamation_style] 195 fine_patterns_found = [[] for i in range(len(regexps_for_fine_styles))] 196 wrong_patterns_found = [] 197 # Looking for comments with wrong style 198 for regexp in regexps_for_wrong_styles: 199 strings = [] 200 with open(src_path, 'r') as f: 201 strings = regexp.findall(f.read()) 202 for s in strings: 203 if is_doxygen_comment(s): 204 wrong_patterns_found.append(s) 205 lines = [] 206 with open(src_path, 'r') as f: 207 lines = f.readlines() 208 line_num = 1 209 for line in lines: 210 for pattern in wrong_patterns_found: 211 if pattern in line: 212 err_msg = "%s:%s" % (src_path, line_num) 213 print(err_msg) 214 print("Found wrong doxygen style:\n", line) 215 break 216 line_num += 1 217 218 # Getting comments with possibly allowed styles 219 ind = 0 220 for regexp in regexps_for_fine_styles: 221 strings = [] 222 with open(src_path, 'r') as f: 223 strings = regexp.findall(f.read()) 224 for s in strings: 225 if is_doxygen_comment(s): 226 fine_patterns_found[ind].append(s) 227 ind += 1 228 229 return check_all(src_path, fine_patterns_found, len(wrong_patterns_found)) 230 231 232def check_file_list(file_list: list) -> bool: 233 jobs = [] 234 main_ret_val = True 235 total_count = str(len(file_list)) 236 idx = 0 237 for src in file_list: 238 idx += 1 239 msg = "[%s/%s] Running doxygen style checker: %s" % (str(idx), total_count, src) 240 proc = run_doxygen_check(src, msg) 241 jobs.append(proc) 242 243 for job in jobs: 244 if not job: 245 main_ret_val = False 246 break 247 248 return main_ret_val 249 250if __name__ == "__main__": 251 args = get_args() 252 files_list = get_file_list(args.panda_dir) 253 if not files_list: 254 sys.exit( 255 "Source list can't be prepared. Please check panda_dir variable: " + args.panda_dir) 256 257 if not check_file_list(files_list): 258 sys.exit("Failed: doxygen style checker got errors") 259 print("Doxygen style checker was passed successfully!") 260