blob: 005a077c7c798fd1bc19697ebea1ea3f7289f782 [file] [log] [blame]
Darryl Green10d9ce32018-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
22class IssueTracker(object):
23 """Base class for issue tracking. Issues should inherit from this and
24 overwrite either issue_with_line if they check the file line by line, or
25 overwrite check_file_for_issue if they check the file as a whole."""
26
27 def __init__(self):
28 self.heading = ""
29 self.files_exemptions = []
30 self.files_with_issues = {}
31
32 def should_check_file(self, filepath):
33 for files_exemption in self.files_exemptions:
34 if filepath.endswith(files_exemption):
35 return False
36 return True
37
38 def issue_with_line(self, line):
39 raise NotImplementedError
40
41 def check_file_for_issue(self, filepath):
42 with open(filepath, "rb") as f:
43 for i, line in enumerate(iter(f.readline, b"")):
44 self.check_file_line(filepath, line, i + 1)
45
Gilles Peskine04398052018-11-23 21:11:30 +010046 def record_issue(self, filepath, line_number):
47 if filepath not in self.files_with_issues.keys():
48 self.files_with_issues[filepath] = []
49 self.files_with_issues[filepath].append(line_number)
50
Darryl Green10d9ce32018-02-28 10:02:55 +000051 def check_file_line(self, filepath, line, line_number):
52 if self.issue_with_line(line):
Gilles Peskine04398052018-11-23 21:11:30 +010053 self.record_issue(filepath, line_number)
Darryl Green10d9ce32018-02-28 10:02:55 +000054
55 def output_file_issues(self, logger):
56 if self.files_with_issues.values():
57 logger.info(self.heading)
58 for filename, lines in sorted(self.files_with_issues.items()):
59 if lines:
60 logger.info("{}: {}".format(
61 filename, ", ".join(str(x) for x in lines)
62 ))
63 else:
64 logger.info(filename)
65 logger.info("")
66
67
68class PermissionIssueTracker(IssueTracker):
69
70 def __init__(self):
71 super().__init__()
72 self.heading = "Incorrect permissions:"
73
74 def check_file_for_issue(self, filepath):
75 if not (os.access(filepath, os.X_OK) ==
76 filepath.endswith((".sh", ".pl", ".py"))):
77 self.files_with_issues[filepath] = None
78
79
80class EndOfFileNewlineIssueTracker(IssueTracker):
81
82 def __init__(self):
83 super().__init__()
84 self.heading = "Missing newline at end of file:"
85
86 def check_file_for_issue(self, filepath):
87 with open(filepath, "rb") as f:
88 if not f.read().endswith(b"\n"):
89 self.files_with_issues[filepath] = None
90
91
92class Utf8BomIssueTracker(IssueTracker):
93
94 def __init__(self):
95 super().__init__()
96 self.heading = "UTF-8 BOM present:"
97
98 def check_file_for_issue(self, filepath):
99 with open(filepath, "rb") as f:
100 if f.read().startswith(codecs.BOM_UTF8):
101 self.files_with_issues[filepath] = None
102
103
104class LineEndingIssueTracker(IssueTracker):
105
106 def __init__(self):
107 super().__init__()
108 self.heading = "Non Unix line endings:"
109
110 def issue_with_line(self, line):
111 return b"\r" in line
112
113
114class TrailingWhitespaceIssueTracker(IssueTracker):
115
116 def __init__(self):
117 super().__init__()
118 self.heading = "Trailing whitespace:"
119 self.files_exemptions = [".md"]
120
121 def issue_with_line(self, line):
122 return line.rstrip(b"\r\n") != line.rstrip()
123
124
125class TabIssueTracker(IssueTracker):
126
127 def __init__(self):
128 super().__init__()
129 self.heading = "Tabs present:"
130 self.files_exemptions = [
131 "Makefile", "generate_visualc_files.pl"
132 ]
133
134 def issue_with_line(self, line):
135 return b"\t" in line
136
137
Gilles Peskinec117d592018-11-23 21:11:52 +0100138class MergeArtifactIssueTracker(IssueTracker):
139
140 def __init__(self):
141 super().__init__()
142 self.heading = "Merge artifact:"
143
144 def issue_with_line(self, filepath, line):
145 # Detect leftover git conflict markers.
146 if line.startswith(b'<<<<<<< ') or line.startswith(b'>>>>>>> '):
147 return True
148 if line.startswith(b'||||||| '): # from merge.conflictStyle=diff3
149 return True
150 if line.rstrip(b'\r\n') == b'=======' and \
151 not filepath.endswith('.md'):
152 return True
153 return False
154
155 def check_file_line(self, filepath, line, line_number):
156 if self.issue_with_line(filepath, line):
157 self.record_issue(filepath, line_number)
158
Darryl Green10d9ce32018-02-28 10:02:55 +0000159class TodoIssueTracker(IssueTracker):
160
161 def __init__(self):
162 super().__init__()
163 self.heading = "TODO present:"
164 self.files_exemptions = [
Jaeden Amero80a23a52018-11-23 10:33:20 +0000165 os.path.basename(__file__),
166 "benchmark.c",
167 "pull_request_template.md",
Darryl Green10d9ce32018-02-28 10:02:55 +0000168 ]
169
170 def issue_with_line(self, line):
171 return b"todo" in line.lower()
172
173
174class IntegrityChecker(object):
175
176 def __init__(self, log_file):
177 self.check_repo_path()
178 self.logger = None
179 self.setup_logger(log_file)
180 self.files_to_check = (
181 ".c", ".h", ".sh", ".pl", ".py", ".md", ".function", ".data",
182 "Makefile", "CMakeLists.txt", "ChangeLog"
183 )
Gilles Peskine95c55752018-09-28 11:48:10 +0200184 self.excluded_directories = ['.git', 'mbed-os']
185 self.excluded_paths = list(map(os.path.normpath, [
186 'cov-int',
187 'examples',
Gilles Peskine95c55752018-09-28 11:48:10 +0200188 ]))
Darryl Green10d9ce32018-02-28 10:02:55 +0000189 self.issues_to_check = [
190 PermissionIssueTracker(),
191 EndOfFileNewlineIssueTracker(),
192 Utf8BomIssueTracker(),
193 LineEndingIssueTracker(),
194 TrailingWhitespaceIssueTracker(),
195 TabIssueTracker(),
Gilles Peskinec117d592018-11-23 21:11:52 +0100196 MergeArtifactIssueTracker(),
Darryl Green10d9ce32018-02-28 10:02:55 +0000197 TodoIssueTracker(),
198 ]
199
200 def check_repo_path(self):
201 if not all(os.path.isdir(d) for d in ["include", "library", "tests"]):
202 raise Exception("Must be run from Mbed TLS root")
203
204 def setup_logger(self, log_file, level=logging.INFO):
205 self.logger = logging.getLogger()
206 self.logger.setLevel(level)
207 if log_file:
208 handler = logging.FileHandler(log_file)
209 self.logger.addHandler(handler)
210 else:
211 console = logging.StreamHandler()
212 self.logger.addHandler(console)
213
Gilles Peskine95c55752018-09-28 11:48:10 +0200214 def prune_branch(self, root, d):
215 if d in self.excluded_directories:
216 return True
217 if os.path.normpath(os.path.join(root, d)) in self.excluded_paths:
218 return True
219 return False
220
Darryl Green10d9ce32018-02-28 10:02:55 +0000221 def check_files(self):
Gilles Peskine95c55752018-09-28 11:48:10 +0200222 for root, dirs, files in os.walk("."):
223 dirs[:] = sorted(d for d in dirs if not self.prune_branch(root, d))
Darryl Green10d9ce32018-02-28 10:02:55 +0000224 for filename in sorted(files):
225 filepath = os.path.join(root, filename)
Gilles Peskine95c55752018-09-28 11:48:10 +0200226 if not filepath.endswith(self.files_to_check):
Darryl Green10d9ce32018-02-28 10:02:55 +0000227 continue
228 for issue_to_check in self.issues_to_check:
229 if issue_to_check.should_check_file(filepath):
230 issue_to_check.check_file_for_issue(filepath)
231
232 def output_issues(self):
233 integrity_return_code = 0
234 for issue_to_check in self.issues_to_check:
235 if issue_to_check.files_with_issues:
236 integrity_return_code = 1
237 issue_to_check.output_file_issues(self.logger)
238 return integrity_return_code
239
240
241def run_main():
242 parser = argparse.ArgumentParser(
243 description=(
244 "This script checks the current state of the source code for "
245 "minor issues, including incorrect file permissions, "
246 "presence of tabs, non-Unix line endings, trailing whitespace, "
247 "presence of UTF-8 BOM, and TODO comments. "
248 "Note: requires python 3, must be run from Mbed TLS root."
249 )
250 )
251 parser.add_argument(
252 "-l", "--log_file", type=str, help="path to optional output log",
253 )
254 check_args = parser.parse_args()
255 integrity_check = IntegrityChecker(check_args.log_file)
256 integrity_check.check_files()
257 return_code = integrity_check.output_issues()
258 sys.exit(return_code)
259
260
261if __name__ == "__main__":
262 run_main()