• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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