blob: 38a6ba539181bb40c239d5b51eed63a5cfcad999 [file] [log] [blame]
Darryl Greenda02eb32018-02-28 10:02:55 +00001#!/usr/bin/env python3
2"""
3This file is part of Mbed TLS (https://tls.mbed.org)
4
5Copyright (c) 2018, Arm Limited, All Rights Reserved
6
7Purpose
8
9This script checks the current state of the source code for minor issues,
10including incorrect file permissions, presence of tabs, non-Unix line endings,
11trailing whitespace, presence of UTF-8 BOM, and TODO comments.
12Note: requires python 3, must be run from Mbed TLS root.
13"""
14
15import os
16import argparse
17import logging
18import codecs
19import sys
20
21
Gilles Peskine7194ecb2019-02-25 20:59:05 +010022class FileIssueTracker(object):
23 """Base class for file-wide issue tracking.
24
25 To implement a checker that processes a file as a whole, inherit from
26 this class and implement `check_file_for_issue`.
27 """
Darryl Greenda02eb32018-02-28 10:02:55 +000028
29 def __init__(self):
30 self.heading = ""
31 self.files_exemptions = []
32 self.files_with_issues = {}
33
34 def should_check_file(self, filepath):
35 for files_exemption in self.files_exemptions:
36 if filepath.endswith(files_exemption):
37 return False
38 return True
39
Darryl Greenda02eb32018-02-28 10:02:55 +000040 def check_file_for_issue(self, filepath):
Gilles Peskine7194ecb2019-02-25 20:59:05 +010041 raise NotImplementedError
Darryl Greenda02eb32018-02-28 10:02:55 +000042
Gilles Peskine232fae32018-11-23 21:11:30 +010043 def record_issue(self, filepath, line_number):
44 if filepath not in self.files_with_issues.keys():
45 self.files_with_issues[filepath] = []
46 self.files_with_issues[filepath].append(line_number)
47
Darryl Greenda02eb32018-02-28 10:02:55 +000048 def output_file_issues(self, logger):
49 if self.files_with_issues.values():
50 logger.info(self.heading)
51 for filename, lines in sorted(self.files_with_issues.items()):
52 if lines:
53 logger.info("{}: {}".format(
54 filename, ", ".join(str(x) for x in lines)
55 ))
56 else:
57 logger.info(filename)
58 logger.info("")
59
Gilles Peskine7194ecb2019-02-25 20:59:05 +010060class LineIssueTracker(FileIssueTracker):
61 """Base class for line-by-line issue tracking.
Darryl Greenda02eb32018-02-28 10:02:55 +000062
Gilles Peskine7194ecb2019-02-25 20:59:05 +010063 To implement a checker that processes files line by line, inherit from
64 this class and implement `line_with_issue`.
65 """
66
67 def issue_with_line(self, line, filepath):
68 raise NotImplementedError
69
70 def check_file_line(self, filepath, line, line_number):
71 if self.issue_with_line(line, filepath):
72 self.record_issue(filepath, line_number)
73
74 def check_file_for_issue(self, filepath):
75 with open(filepath, "rb") as f:
76 for i, line in enumerate(iter(f.readline, b"")):
77 self.check_file_line(filepath, line, i + 1)
78
79class PermissionIssueTracker(FileIssueTracker):
Gilles Peskine4fb66782019-02-25 20:35:31 +010080 """Track files with bad permissions.
81
82 Files that are not executable scripts must not be executable."""
Darryl Greenda02eb32018-02-28 10:02:55 +000083
84 def __init__(self):
85 super().__init__()
86 self.heading = "Incorrect permissions:"
87
88 def check_file_for_issue(self, filepath):
89 if not (os.access(filepath, os.X_OK) ==
90 filepath.endswith((".sh", ".pl", ".py"))):
91 self.files_with_issues[filepath] = None
92
93
Gilles Peskine7194ecb2019-02-25 20:59:05 +010094class EndOfFileNewlineIssueTracker(FileIssueTracker):
Gilles Peskine4fb66782019-02-25 20:35:31 +010095 """Track files that end with an incomplete line
96 (no newline character at the end of the last line)."""
Darryl Greenda02eb32018-02-28 10:02:55 +000097
98 def __init__(self):
99 super().__init__()
100 self.heading = "Missing newline at end of file:"
101
102 def check_file_for_issue(self, filepath):
103 with open(filepath, "rb") as f:
104 if not f.read().endswith(b"\n"):
105 self.files_with_issues[filepath] = None
106
107
Gilles Peskine7194ecb2019-02-25 20:59:05 +0100108class Utf8BomIssueTracker(FileIssueTracker):
Gilles Peskine4fb66782019-02-25 20:35:31 +0100109 """Track files that start with a UTF-8 BOM.
110 Files should be ASCII or UTF-8. Valid UTF-8 does not start with a BOM."""
Darryl Greenda02eb32018-02-28 10:02:55 +0000111
112 def __init__(self):
113 super().__init__()
114 self.heading = "UTF-8 BOM present:"
115
116 def check_file_for_issue(self, filepath):
117 with open(filepath, "rb") as f:
118 if f.read().startswith(codecs.BOM_UTF8):
119 self.files_with_issues[filepath] = None
120
121
Gilles Peskine7194ecb2019-02-25 20:59:05 +0100122class LineEndingIssueTracker(LineIssueTracker):
Gilles Peskine4fb66782019-02-25 20:35:31 +0100123 """Track files with non-Unix line endings (i.e. files with CR)."""
Darryl Greenda02eb32018-02-28 10:02:55 +0000124
125 def __init__(self):
126 super().__init__()
127 self.heading = "Non Unix line endings:"
128
Gilles Peskine7194ecb2019-02-25 20:59:05 +0100129 def issue_with_line(self, line, _filepath):
Darryl Greenda02eb32018-02-28 10:02:55 +0000130 return b"\r" in line
131
132
Gilles Peskine7194ecb2019-02-25 20:59:05 +0100133class TrailingWhitespaceIssueTracker(LineIssueTracker):
Gilles Peskine4fb66782019-02-25 20:35:31 +0100134 """Track lines with trailing whitespace."""
Darryl Greenda02eb32018-02-28 10:02:55 +0000135
136 def __init__(self):
137 super().__init__()
138 self.heading = "Trailing whitespace:"
139 self.files_exemptions = [".md"]
140
Gilles Peskine7194ecb2019-02-25 20:59:05 +0100141 def issue_with_line(self, line, _filepath):
Darryl Greenda02eb32018-02-28 10:02:55 +0000142 return line.rstrip(b"\r\n") != line.rstrip()
143
144
Gilles Peskine7194ecb2019-02-25 20:59:05 +0100145class TabIssueTracker(LineIssueTracker):
Gilles Peskine4fb66782019-02-25 20:35:31 +0100146 """Track lines with tabs."""
Darryl Greenda02eb32018-02-28 10:02:55 +0000147
148 def __init__(self):
149 super().__init__()
150 self.heading = "Tabs present:"
151 self.files_exemptions = [
152 "Makefile", "generate_visualc_files.pl"
153 ]
154
Gilles Peskine7194ecb2019-02-25 20:59:05 +0100155 def issue_with_line(self, line, _filepath):
Darryl Greenda02eb32018-02-28 10:02:55 +0000156 return b"\t" in line
157
158
Gilles Peskine7194ecb2019-02-25 20:59:05 +0100159class MergeArtifactIssueTracker(LineIssueTracker):
Gilles Peskine4fb66782019-02-25 20:35:31 +0100160 """Track lines with merge artifacts.
161 These are leftovers from a ``git merge`` that wasn't fully edited."""
Gilles Peskineda6ccfc2018-11-23 21:11:52 +0100162
163 def __init__(self):
164 super().__init__()
165 self.heading = "Merge artifact:"
166
Gilles Peskine7194ecb2019-02-25 20:59:05 +0100167 def issue_with_line(self, line, _filepath):
Gilles Peskineda6ccfc2018-11-23 21:11:52 +0100168 # Detect leftover git conflict markers.
169 if line.startswith(b'<<<<<<< ') or line.startswith(b'>>>>>>> '):
170 return True
171 if line.startswith(b'||||||| '): # from merge.conflictStyle=diff3
172 return True
173 if line.rstrip(b'\r\n') == b'=======' and \
Gilles Peskine7194ecb2019-02-25 20:59:05 +0100174 not _filepath.endswith('.md'):
Gilles Peskineda6ccfc2018-11-23 21:11:52 +0100175 return True
176 return False
177
Gilles Peskine7194ecb2019-02-25 20:59:05 +0100178class TodoIssueTracker(LineIssueTracker):
Gilles Peskine4fb66782019-02-25 20:35:31 +0100179 """Track lines containing ``TODO``."""
Darryl Greenda02eb32018-02-28 10:02:55 +0000180
181 def __init__(self):
182 super().__init__()
183 self.heading = "TODO present:"
184 self.files_exemptions = [
185 __file__, "benchmark.c", "pull_request_template.md"
186 ]
187
Gilles Peskine7194ecb2019-02-25 20:59:05 +0100188 def issue_with_line(self, line, _filepath):
Darryl Greenda02eb32018-02-28 10:02:55 +0000189 return b"todo" in line.lower()
190
191
192class IntegrityChecker(object):
Gilles Peskine4fb66782019-02-25 20:35:31 +0100193 """Sanity-check files under the current directory."""
Darryl Greenda02eb32018-02-28 10:02:55 +0000194
195 def __init__(self, log_file):
Gilles Peskine4fb66782019-02-25 20:35:31 +0100196 """Instantiate the sanity checker.
197 Check files under the current directory.
198 Write a report of issues to log_file."""
Darryl Greenda02eb32018-02-28 10:02:55 +0000199 self.check_repo_path()
200 self.logger = None
201 self.setup_logger(log_file)
202 self.files_to_check = (
203 ".c", ".h", ".sh", ".pl", ".py", ".md", ".function", ".data",
204 "Makefile", "CMakeLists.txt", "ChangeLog"
205 )
Gilles Peskine3400b4d2018-09-28 11:48:10 +0200206 self.excluded_directories = ['.git', 'mbed-os']
207 self.excluded_paths = list(map(os.path.normpath, [
208 'cov-int',
209 'examples',
210 'yotta/module'
211 ]))
Darryl Greenda02eb32018-02-28 10:02:55 +0000212 self.issues_to_check = [
213 PermissionIssueTracker(),
214 EndOfFileNewlineIssueTracker(),
215 Utf8BomIssueTracker(),
216 LineEndingIssueTracker(),
217 TrailingWhitespaceIssueTracker(),
218 TabIssueTracker(),
Gilles Peskineda6ccfc2018-11-23 21:11:52 +0100219 MergeArtifactIssueTracker(),
Darryl Greenda02eb32018-02-28 10:02:55 +0000220 TodoIssueTracker(),
221 ]
222
Gilles Peskine4fb66782019-02-25 20:35:31 +0100223 @staticmethod
224 def check_repo_path():
Darryl Greenda02eb32018-02-28 10:02:55 +0000225 if not all(os.path.isdir(d) for d in ["include", "library", "tests"]):
226 raise Exception("Must be run from Mbed TLS root")
227
228 def setup_logger(self, log_file, level=logging.INFO):
229 self.logger = logging.getLogger()
230 self.logger.setLevel(level)
231 if log_file:
232 handler = logging.FileHandler(log_file)
233 self.logger.addHandler(handler)
234 else:
235 console = logging.StreamHandler()
236 self.logger.addHandler(console)
237
Gilles Peskine3400b4d2018-09-28 11:48:10 +0200238 def prune_branch(self, root, d):
239 if d in self.excluded_directories:
240 return True
241 if os.path.normpath(os.path.join(root, d)) in self.excluded_paths:
242 return True
243 return False
244
Darryl Greenda02eb32018-02-28 10:02:55 +0000245 def check_files(self):
Gilles Peskine3400b4d2018-09-28 11:48:10 +0200246 for root, dirs, files in os.walk("."):
247 dirs[:] = sorted(d for d in dirs if not self.prune_branch(root, d))
Darryl Greenda02eb32018-02-28 10:02:55 +0000248 for filename in sorted(files):
249 filepath = os.path.join(root, filename)
Gilles Peskine3400b4d2018-09-28 11:48:10 +0200250 if not filepath.endswith(self.files_to_check):
Darryl Greenda02eb32018-02-28 10:02:55 +0000251 continue
252 for issue_to_check in self.issues_to_check:
253 if issue_to_check.should_check_file(filepath):
254 issue_to_check.check_file_for_issue(filepath)
255
256 def output_issues(self):
257 integrity_return_code = 0
258 for issue_to_check in self.issues_to_check:
259 if issue_to_check.files_with_issues:
260 integrity_return_code = 1
261 issue_to_check.output_file_issues(self.logger)
262 return integrity_return_code
263
264
265def run_main():
266 parser = argparse.ArgumentParser(
267 description=(
268 "This script checks the current state of the source code for "
269 "minor issues, including incorrect file permissions, "
270 "presence of tabs, non-Unix line endings, trailing whitespace, "
271 "presence of UTF-8 BOM, and TODO comments. "
272 "Note: requires python 3, must be run from Mbed TLS root."
273 )
274 )
275 parser.add_argument(
276 "-l", "--log_file", type=str, help="path to optional output log",
277 )
278 check_args = parser.parse_args()
279 integrity_check = IntegrityChecker(check_args.log_file)
280 integrity_check.check_files()
281 return_code = integrity_check.output_issues()
282 sys.exit(return_code)
283
284
285if __name__ == "__main__":
286 run_main()