1#
2# Copyright (c) 2006-2022, RT-Thread Development Team
3#
4# SPDX-License-Identifier: Apache-2.0
5#
6# Change Logs:
7# Date           Author       Notes
8# 2021-04-01     LiuKang      the first version
9#
10
11import os
12import re
13import sys
14import click
15import yaml
16import chardet
17import logging
18import datetime
19
20
21def init_logger():
22    log_format = "[%(filename)s %(lineno)d %(levelname)s] %(message)s "
23    date_format = '%Y-%m-%d  %H:%M:%S %a '
24    logging.basicConfig(level=logging.INFO,
25                        format=log_format,
26                        datefmt=date_format,
27                        )
28
29
30class CheckOut:
31    def __init__(self, rtt_repo, rtt_branch):
32        self.root = os.getcwd()
33        self.rtt_repo = rtt_repo
34        self.rtt_branch = rtt_branch
35
36    def __exclude_file(self, file_path):
37        dir_number = file_path.split('/')
38        ignore_path = file_path
39
40        # gets the file path depth.
41        for i in dir_number:
42            # current directory.
43            dir_name = os.path.dirname(ignore_path)
44            ignore_path = dir_name
45            # judge the ignore file exists in the current directory.
46            ignore_file_path = os.path.join(dir_name, ".ignore_format.yml")
47            if not os.path.exists(ignore_file_path):
48                continue
49            try:
50                with open(ignore_file_path) as f:
51                    ignore_config = yaml.safe_load(f.read())
52                file_ignore = ignore_config.get("file_path", [])
53                dir_ignore = ignore_config.get("dir_path", [])
54            except Exception as e:
55                logging.error(e)
56                continue
57            logging.debug("ignore file path: {}".format(ignore_file_path))
58            logging.debug("file_ignore: {}".format(file_ignore))
59            logging.debug("dir_ignore: {}".format(dir_ignore))
60            try:
61                # judge file_path in the ignore file.
62                for file in file_ignore:
63                    if file is not None:
64                        file_real_path = os.path.join(dir_name, file)
65                        if file_real_path == file_path:
66                            logging.info("ignore file path: {}".format(file_real_path))
67                            return 0
68
69                file_dir_path = os.path.dirname(file_path)
70                for _dir in dir_ignore:
71                    if _dir is not None:
72                        dir_real_path = os.path.join(dir_name, _dir)
73                        if file_dir_path.startswith(dir_real_path):
74                            logging.info("ignore dir path: {}".format(dir_real_path))
75                            return 0
76            except Exception as e:
77                logging.error(e)
78                continue
79
80        return 1
81
82    def get_new_file(self):
83        file_list = list()
84        try:
85            os.system('git remote add rtt_repo {}'.format(self.rtt_repo))
86            os.system('git fetch rtt_repo')
87            os.system('git merge rtt_repo/{}'.format(self.rtt_branch))
88            os.system('git reset rtt_repo/{} --soft'.format(self.rtt_branch))
89            os.system('git status > git.txt')
90        except Exception as e:
91            logging.error(e)
92            return None
93        try:
94            with open('git.txt', 'r') as f:
95                file_lines = f.readlines()
96        except Exception as e:
97            logging.error(e)
98            return None
99        file_path = ''
100        for line in file_lines:
101            if 'new file' in line:
102                file_path = line.split('new file:')[1].strip()
103                logging.info('new file -> {}'.format(file_path))
104            elif 'deleted' in line:
105                logging.info('deleted file -> {}'.format(line.split('deleted:')[1].strip()))
106            elif 'modified' in line:
107                file_path = line.split('modified:')[1].strip()
108                logging.info('modified file -> {}'.format(file_path))
109            else:
110                continue
111
112            result = self.__exclude_file(file_path)
113            if result != 0:
114                file_list.append(file_path)
115
116        return file_list
117
118
119class FormatCheck:
120    def __init__(self, file_list):
121        self.file_list = file_list
122
123    def __check_rt_errorcode(self, line):
124        pattern = re.compile(r'return\s+(RT_ERROR|RT_ETIMEOUT|RT_EFULL|RT_EEMPTY|RT_ENOMEM|RT_ENOSYS|RT_EBUSY|RT_EIO|RT_EINTR|RT_EINVAL|RT_ENOENT|RT_ENOSPC|RT_EPERM|RT_ETRAP|RT_EFAULT)')
125        match = pattern.search(line)
126        if match:
127            return False
128        else:
129            return True
130
131    def __check_file(self, file_lines, file_path):
132        line_num = 0
133        check_result = True
134        for line in file_lines:
135            line_num += 1
136            # check line start
137            line_start = line.replace(' ', '')
138            # find tab
139            if line_start.startswith('\t'):
140                logging.error("{} line[{}]: please use space replace tab at the start of this line.".format(file_path, line_num))
141                check_result = False
142            # check line end
143            line_end = line.split('\n')[0]
144            if line_end.endswith(' ') or line_end.endswith('\t'):
145                logging.error("{} line[{}]: please delete extra space at the end of this line.".format(file_path, line_num))
146                check_result = False
147            if self.__check_rt_errorcode(line) == False:
148                logging.error("{} line[{}]: the RT-Thread error code should return negative value. e.g. return -RT_ERROR".format(file_path, line_num))
149                check_result = False
150        return check_result
151
152    def check(self):
153        logging.info("Start to check files format.")
154        if len(self.file_list) == 0:
155            logging.warning("There are no files to check format.")
156            return True
157        encoding_check_result = True
158        format_check_fail_files = 0
159        for file_path in self.file_list:
160            code = ''
161            if file_path.endswith(".c") or file_path.endswith(".h"):
162                try:
163                    with open(file_path, 'rb') as f:
164                        file = f.read()
165                        # get file encoding
166                        chardet_report = chardet.detect(file)
167                        code = chardet_report['encoding']
168                        confidence = chardet_report['confidence']
169                except Exception as e:
170                    logging.error(e)
171            else:
172                continue
173
174            if code != 'utf-8' and code != 'ascii' and confidence > 0.8:
175                logging.error("[{0}]: encoding {1} not utf-8, please format it.".format(file_path, code))
176                encoding_check_result = False
177            else:
178                logging.info('[{0}]: encoding check success.'.format(file_path))
179
180            with open(file_path, 'r', encoding = "utf-8") as f:
181                file_lines = f.readlines()
182            if not self.__check_file(file_lines, file_path):
183                format_check_fail_files += 1
184
185        if (not encoding_check_result) or (format_check_fail_files != 0):
186            logging.error("files format check fail.")
187            return False
188
189        logging.info("files format check success.")
190
191        return True
192
193
194class LicenseCheck:
195    def __init__(self, file_list):
196        self.file_list = file_list
197
198    def check(self):
199        current_year = datetime.date.today().year
200        logging.info("current year: {}".format(current_year))
201        if len(self.file_list) == 0:
202            logging.warning("There are no files to check license.")
203            return 0
204        logging.info("Start to check files license.")
205        check_result = True
206        for file_path in self.file_list:
207            if file_path.endswith(".c") or file_path.endswith(".h"):
208                try:
209                    with open(file_path, 'r') as f:
210                        file = f.readlines()
211                except Exception as e:
212                    logging.error(e)
213            else:
214                continue
215
216            if 'Copyright' in file[1] and 'SPDX-License-Identifier: Apache-2.0' in file[3]:
217                try:
218                    license_year = re.search(r'2006-\d{4}', file[1]).group()
219                    true_year = '2006-{}'.format(current_year)
220                    if license_year != true_year:
221                        logging.warning("[{0}]: license year: {} is not true: {}, please update.".format(file_path,
222                                                                                                         license_year,
223                                                                                                         true_year))
224
225                    else:
226                        logging.info("[{0}]: license check success.".format(file_path))
227                except Exception as e:
228                    logging.error(e)
229
230            else:
231                logging.error("[{0}]: license check fail.".format(file_path))
232                check_result = False
233
234        return check_result
235
236
237@click.group()
238@click.pass_context
239def cli(ctx):
240    pass
241
242
243@cli.command()
244@click.option(
245    '--license',
246    "check_license",
247    required=False,
248    type=click.BOOL,
249    flag_value=True,
250    help="Enable File license check.",
251)
252@click.argument(
253    'repo',
254    nargs=1,
255    type=click.STRING,
256    default='https://github.com/RT-Thread/rt-thread',
257)
258@click.argument(
259    'branch',
260    nargs=1,
261    type=click.STRING,
262    default='master',
263)
264def check(check_license, repo, branch):
265    """
266    check files license and format.
267    """
268    init_logger()
269    # get modified files list
270    checkout = CheckOut(repo, branch)
271    file_list = checkout.get_new_file()
272    if file_list is None:
273        logging.error("checkout files fail")
274        sys.exit(1)
275
276    # check modified files format
277    format_check = FormatCheck(file_list)
278    format_check_result = format_check.check()
279    license_check_result = True
280    if check_license:
281        license_check = LicenseCheck(file_list)
282        license_check_result = license_check.check()
283
284    if not format_check_result or not license_check_result:
285        logging.error("file format check or license check fail.")
286        sys.exit(1)
287    logging.info("check success.")
288    sys.exit(0)
289
290
291if __name__ == '__main__':
292    cli()
293