| Darryl Green | 10d9ce3 | 2018-02-28 10:02:55 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python3 | 
|  | 2 | """ | 
|  | 3 | This file is part of Mbed TLS (https://tls.mbed.org) | 
|  | 4 |  | 
|  | 5 | Copyright (c) 2018, Arm Limited, All Rights Reserved | 
|  | 6 |  | 
|  | 7 | Purpose | 
|  | 8 |  | 
|  | 9 | This script checks the current state of the source code for minor issues, | 
|  | 10 | including incorrect file permissions, presence of tabs, non-Unix line endings, | 
|  | 11 | trailing whitespace, presence of UTF-8 BOM, and TODO comments. | 
|  | 12 | Note: requires python 3, must be run from Mbed TLS root. | 
|  | 13 | """ | 
|  | 14 |  | 
|  | 15 | import os | 
|  | 16 | import argparse | 
|  | 17 | import logging | 
|  | 18 | import codecs | 
|  | 19 | import sys | 
|  | 20 |  | 
|  | 21 |  | 
|  | 22 | class 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 |  | 
|  | 46 | def check_file_line(self, filepath, line, line_number): | 
|  | 47 | if self.issue_with_line(line): | 
|  | 48 | if filepath not in self.files_with_issues.keys(): | 
|  | 49 | self.files_with_issues[filepath] = [] | 
|  | 50 | self.files_with_issues[filepath].append(line_number) | 
|  | 51 |  | 
|  | 52 | def output_file_issues(self, logger): | 
|  | 53 | if self.files_with_issues.values(): | 
|  | 54 | logger.info(self.heading) | 
|  | 55 | for filename, lines in sorted(self.files_with_issues.items()): | 
|  | 56 | if lines: | 
|  | 57 | logger.info("{}: {}".format( | 
|  | 58 | filename, ", ".join(str(x) for x in lines) | 
|  | 59 | )) | 
|  | 60 | else: | 
|  | 61 | logger.info(filename) | 
|  | 62 | logger.info("") | 
|  | 63 |  | 
|  | 64 |  | 
|  | 65 | class PermissionIssueTracker(IssueTracker): | 
|  | 66 |  | 
|  | 67 | def __init__(self): | 
|  | 68 | super().__init__() | 
|  | 69 | self.heading = "Incorrect permissions:" | 
|  | 70 |  | 
|  | 71 | def check_file_for_issue(self, filepath): | 
|  | 72 | if not (os.access(filepath, os.X_OK) == | 
|  | 73 | filepath.endswith((".sh", ".pl", ".py"))): | 
|  | 74 | self.files_with_issues[filepath] = None | 
|  | 75 |  | 
|  | 76 |  | 
|  | 77 | class EndOfFileNewlineIssueTracker(IssueTracker): | 
|  | 78 |  | 
|  | 79 | def __init__(self): | 
|  | 80 | super().__init__() | 
|  | 81 | self.heading = "Missing newline at end of file:" | 
|  | 82 |  | 
|  | 83 | def check_file_for_issue(self, filepath): | 
|  | 84 | with open(filepath, "rb") as f: | 
|  | 85 | if not f.read().endswith(b"\n"): | 
|  | 86 | self.files_with_issues[filepath] = None | 
|  | 87 |  | 
|  | 88 |  | 
|  | 89 | class Utf8BomIssueTracker(IssueTracker): | 
|  | 90 |  | 
|  | 91 | def __init__(self): | 
|  | 92 | super().__init__() | 
|  | 93 | self.heading = "UTF-8 BOM present:" | 
|  | 94 |  | 
|  | 95 | def check_file_for_issue(self, filepath): | 
|  | 96 | with open(filepath, "rb") as f: | 
|  | 97 | if f.read().startswith(codecs.BOM_UTF8): | 
|  | 98 | self.files_with_issues[filepath] = None | 
|  | 99 |  | 
|  | 100 |  | 
|  | 101 | class LineEndingIssueTracker(IssueTracker): | 
|  | 102 |  | 
|  | 103 | def __init__(self): | 
|  | 104 | super().__init__() | 
|  | 105 | self.heading = "Non Unix line endings:" | 
|  | 106 |  | 
|  | 107 | def issue_with_line(self, line): | 
|  | 108 | return b"\r" in line | 
|  | 109 |  | 
|  | 110 |  | 
|  | 111 | class TrailingWhitespaceIssueTracker(IssueTracker): | 
|  | 112 |  | 
|  | 113 | def __init__(self): | 
|  | 114 | super().__init__() | 
|  | 115 | self.heading = "Trailing whitespace:" | 
|  | 116 | self.files_exemptions = [".md"] | 
|  | 117 |  | 
|  | 118 | def issue_with_line(self, line): | 
|  | 119 | return line.rstrip(b"\r\n") != line.rstrip() | 
|  | 120 |  | 
|  | 121 |  | 
|  | 122 | class TabIssueTracker(IssueTracker): | 
|  | 123 |  | 
|  | 124 | def __init__(self): | 
|  | 125 | super().__init__() | 
|  | 126 | self.heading = "Tabs present:" | 
|  | 127 | self.files_exemptions = [ | 
|  | 128 | "Makefile", "generate_visualc_files.pl" | 
|  | 129 | ] | 
|  | 130 |  | 
|  | 131 | def issue_with_line(self, line): | 
|  | 132 | return b"\t" in line | 
|  | 133 |  | 
|  | 134 |  | 
|  | 135 | class TodoIssueTracker(IssueTracker): | 
|  | 136 |  | 
|  | 137 | def __init__(self): | 
|  | 138 | super().__init__() | 
|  | 139 | self.heading = "TODO present:" | 
|  | 140 | self.files_exemptions = [ | 
|  | 141 | __file__, "benchmark.c", "pull_request_template.md" | 
|  | 142 | ] | 
|  | 143 |  | 
|  | 144 | def issue_with_line(self, line): | 
|  | 145 | return b"todo" in line.lower() | 
|  | 146 |  | 
|  | 147 |  | 
|  | 148 | class IntegrityChecker(object): | 
|  | 149 |  | 
|  | 150 | def __init__(self, log_file): | 
|  | 151 | self.check_repo_path() | 
|  | 152 | self.logger = None | 
|  | 153 | self.setup_logger(log_file) | 
|  | 154 | self.files_to_check = ( | 
|  | 155 | ".c", ".h", ".sh", ".pl", ".py", ".md", ".function", ".data", | 
|  | 156 | "Makefile", "CMakeLists.txt", "ChangeLog" | 
|  | 157 | ) | 
|  | 158 | self.issues_to_check = [ | 
|  | 159 | PermissionIssueTracker(), | 
|  | 160 | EndOfFileNewlineIssueTracker(), | 
|  | 161 | Utf8BomIssueTracker(), | 
|  | 162 | LineEndingIssueTracker(), | 
|  | 163 | TrailingWhitespaceIssueTracker(), | 
|  | 164 | TabIssueTracker(), | 
|  | 165 | TodoIssueTracker(), | 
|  | 166 | ] | 
|  | 167 |  | 
|  | 168 | def check_repo_path(self): | 
|  | 169 | if not all(os.path.isdir(d) for d in ["include", "library", "tests"]): | 
|  | 170 | raise Exception("Must be run from Mbed TLS root") | 
|  | 171 |  | 
|  | 172 | def setup_logger(self, log_file, level=logging.INFO): | 
|  | 173 | self.logger = logging.getLogger() | 
|  | 174 | self.logger.setLevel(level) | 
|  | 175 | if log_file: | 
|  | 176 | handler = logging.FileHandler(log_file) | 
|  | 177 | self.logger.addHandler(handler) | 
|  | 178 | else: | 
|  | 179 | console = logging.StreamHandler() | 
|  | 180 | self.logger.addHandler(console) | 
|  | 181 |  | 
|  | 182 | def check_files(self): | 
|  | 183 | for root, dirs, files in sorted(os.walk(".")): | 
|  | 184 | for filename in sorted(files): | 
|  | 185 | filepath = os.path.join(root, filename) | 
|  | 186 | if (os.path.join("yotta", "module") in filepath or | 
|  | 187 | not filepath.endswith(self.files_to_check)): | 
|  | 188 | continue | 
|  | 189 | for issue_to_check in self.issues_to_check: | 
|  | 190 | if issue_to_check.should_check_file(filepath): | 
|  | 191 | issue_to_check.check_file_for_issue(filepath) | 
|  | 192 |  | 
|  | 193 | def output_issues(self): | 
|  | 194 | integrity_return_code = 0 | 
|  | 195 | for issue_to_check in self.issues_to_check: | 
|  | 196 | if issue_to_check.files_with_issues: | 
|  | 197 | integrity_return_code = 1 | 
|  | 198 | issue_to_check.output_file_issues(self.logger) | 
|  | 199 | return integrity_return_code | 
|  | 200 |  | 
|  | 201 |  | 
|  | 202 | def run_main(): | 
|  | 203 | parser = argparse.ArgumentParser( | 
|  | 204 | description=( | 
|  | 205 | "This script checks the current state of the source code for " | 
|  | 206 | "minor issues, including incorrect file permissions, " | 
|  | 207 | "presence of tabs, non-Unix line endings, trailing whitespace, " | 
|  | 208 | "presence of UTF-8 BOM, and TODO comments. " | 
|  | 209 | "Note: requires python 3, must be run from Mbed TLS root." | 
|  | 210 | ) | 
|  | 211 | ) | 
|  | 212 | parser.add_argument( | 
|  | 213 | "-l", "--log_file", type=str, help="path to optional output log", | 
|  | 214 | ) | 
|  | 215 | check_args = parser.parse_args() | 
|  | 216 | integrity_check = IntegrityChecker(check_args.log_file) | 
|  | 217 | integrity_check.check_files() | 
|  | 218 | return_code = integrity_check.output_issues() | 
|  | 219 | sys.exit(return_code) | 
|  | 220 |  | 
|  | 221 |  | 
|  | 222 | if __name__ == "__main__": | 
|  | 223 | run_main() |