Zelalem | 219df41 | 2020-05-17 19:21:20 -0500 | [diff] [blame^] | 1 | #!/usr/bin/env python3 |
| 2 | # |
| 3 | # Copyright (c) 2019, Arm Limited. All rights reserved. |
| 4 | # |
| 5 | # SPDX-License-Identifier: BSD-3-Clause |
| 6 | # |
| 7 | |
| 8 | import argparse |
| 9 | import os |
| 10 | import re |
| 11 | import sys |
| 12 | import utils |
| 13 | |
| 14 | # File extensions to check |
| 15 | VALID_FILE_EXTENSIONS = ('.c', '.S', '.h') |
| 16 | |
| 17 | # Paths inside the tree to ignore. Hidden folders and files are always ignored. |
| 18 | # They mustn't end in '/'. |
| 19 | IGNORED_FOLDERS = ( |
| 20 | "tools", |
| 21 | "docs" |
| 22 | ) |
| 23 | |
| 24 | # List of ignored files in folders that aren't ignored |
| 25 | IGNORED_FILES = () |
| 26 | |
| 27 | # Regular expression for searching the Banned APIs. This is taken from the |
| 28 | # Coding guideline in TF-A repo |
| 29 | BANNED_APIS = ["strcpy", "wcscpy", "strncpy", "strcat", "wcscat", "strncat", |
| 30 | "sprintf", "vsprintf", "strtok", "atoi", "atol", "atoll", |
| 31 | "itoa", "ltoa", "lltoa"] |
| 32 | BANNED_PATTERN = re.compile('|'.join(BANNED_APIS)) |
| 33 | |
| 34 | COMMENTS_PATTERN = re.compile(r"//|/\*|\*/") |
| 35 | |
| 36 | |
| 37 | def filter_comments(f): |
| 38 | ''' |
| 39 | filter_comments(f) -> iterator for line number, filtered line |
| 40 | |
| 41 | Given an iterable of lines (such as a file), return another iterable of |
| 42 | lines, with the comments filtered out and removed. |
| 43 | ''' |
| 44 | |
| 45 | in_comment = False |
| 46 | for line_num, line in enumerate(f): |
| 47 | line = line.rstrip('\n') |
| 48 | |
| 49 | temp = "" |
| 50 | breaker = len(line) if in_comment else 0 |
| 51 | for match in COMMENTS_PATTERN.finditer(line): |
| 52 | content = match.group(0) |
| 53 | start, end = match.span() |
| 54 | |
| 55 | if in_comment: |
| 56 | if content == "*/": |
| 57 | in_comment = False |
| 58 | breaker = end |
| 59 | else: |
| 60 | if content == "/*": |
| 61 | in_comment = True |
| 62 | temp += line[breaker:start] |
| 63 | breaker = len(line) |
| 64 | elif content == "//": |
| 65 | temp += line[breaker:start] |
| 66 | breaker = len(line) |
| 67 | break |
| 68 | |
| 69 | temp += line[breaker:] |
| 70 | if temp: |
| 71 | yield line_num + 1, temp |
| 72 | |
| 73 | |
| 74 | def file_check_banned_api(path, encoding='utf-8'): |
| 75 | ''' |
| 76 | Reads all lines from a file in path and checks for any banned APIs. |
| 77 | The combined number of errors and uses of banned APIs is returned. If the |
| 78 | result is equal to 0, the file is clean and contains no banned APIs. |
| 79 | ''' |
| 80 | |
| 81 | count = 0 |
| 82 | |
| 83 | try: |
| 84 | f = open(path, encoding=encoding) |
| 85 | except FileNotFoundError: |
| 86 | print("ERROR: could not open " + path) |
| 87 | utils.print_exception_info() |
| 88 | return True |
| 89 | |
| 90 | try: |
| 91 | for line_num, line in filter_comments(f): |
| 92 | match = BANNED_PATTERN.search(line) |
| 93 | if match: |
| 94 | location = "line {} of file {}".format(line_num, path) |
| 95 | print("BANNED API: in " + location) |
| 96 | |
| 97 | # NOTE: this preview of the error is not perfect if comments |
| 98 | # have been removed - however, it does good enough most of the |
| 99 | # time. |
| 100 | start, end = match.span() |
| 101 | print(">>> {}".format(line)) |
| 102 | print(" {}^{}".format(start * " ", (end - start - 1) * "~")) |
| 103 | |
| 104 | count += 1 |
| 105 | except: |
| 106 | print("ERROR: unexpected exception while parsing " + path) |
| 107 | utils.print_exception_info() |
| 108 | count += 1 |
| 109 | |
| 110 | f.close() |
| 111 | |
| 112 | return count |
| 113 | |
| 114 | |
| 115 | def get_tree_files(): |
| 116 | ''' |
| 117 | Get all files in the git repository |
| 118 | ''' |
| 119 | |
| 120 | # Get patches of the affected commits with one line of context. |
| 121 | (rc, stdout, stderr) = utils.shell_command(['git', 'ls-files']) |
| 122 | if rc != 0: |
| 123 | return False |
| 124 | |
| 125 | lines = stdout.splitlines() |
| 126 | return lines |
| 127 | |
| 128 | |
| 129 | def get_patch_files(base_commit, end_commit): |
| 130 | ''' |
| 131 | Get all files that have changed in a given patch |
| 132 | ''' |
| 133 | |
| 134 | # Get patches of the affected commits with one line of context. |
| 135 | (rc, stdout, stderr) = utils.shell_command([ |
| 136 | 'git', 'diff-tree', '--diff-filter=ACMRT', '-r', '--name-only', |
| 137 | base_commit, end_commit]) |
| 138 | |
| 139 | if rc != 0: |
| 140 | return False |
| 141 | |
| 142 | paths = stdout.splitlines() |
| 143 | return paths |
| 144 | |
| 145 | |
| 146 | def parse_cmd_line(): |
| 147 | parser = argparse.ArgumentParser( |
| 148 | description="Check Banned APIs", |
| 149 | epilog=""" |
| 150 | For each source file in the tree, checks whether Banned APIs as |
| 151 | described in the list are used or not. |
| 152 | """ |
| 153 | ) |
| 154 | |
| 155 | parser.add_argument("--tree", "-t", |
| 156 | help=""" |
| 157 | Path to the source tree to check (default: %(default)s) |
| 158 | """, |
| 159 | default=os.curdir) |
| 160 | parser.add_argument("--patch", "-p", |
| 161 | help=""" |
| 162 | Patch mode. Instead of checking all files in |
| 163 | the source tree, the script will consider only files |
| 164 | that are modified by the latest patch(es). |
| 165 | """, |
| 166 | action="store_true") |
| 167 | parser.add_argument("--from-ref", |
| 168 | help=""" |
| 169 | Base commit in patch mode (default: %(default)s) |
| 170 | """, |
| 171 | default="master") |
| 172 | parser.add_argument("--to-ref", |
| 173 | help=""" |
| 174 | Final commit in patch mode (default: %(default)s) |
| 175 | """, |
| 176 | default="HEAD") |
| 177 | parser.add_argument("--verbose", "-v", |
| 178 | help="Print verbose output", |
| 179 | action="store_true") |
| 180 | args = parser.parse_args() |
| 181 | return args |
| 182 | |
| 183 | |
| 184 | if __name__ == "__main__": |
| 185 | args = parse_cmd_line() |
| 186 | |
| 187 | os.chdir(args.tree) |
| 188 | |
| 189 | if args.patch: |
| 190 | print("Checking files modified between patches " + args.from_ref + |
| 191 | " and " + args.to_ref + "...\n") |
| 192 | files = get_patch_files(args.from_ref, args.to_ref) |
| 193 | else: |
| 194 | print("Checking all files git repo " + os.path.abspath(args.tree) + |
| 195 | "...\n") |
| 196 | files = get_tree_files() |
| 197 | |
| 198 | total_errors = 0 |
| 199 | for filename in files: |
| 200 | ignored = utils.file_is_ignored(filename, VALID_FILE_EXTENSIONS, |
| 201 | IGNORED_FILES, IGNORED_FOLDERS) |
| 202 | if ignored: |
| 203 | if args.verbose: |
| 204 | print("INFO: Skipping ignored file " + filename) |
| 205 | continue |
| 206 | |
| 207 | if args.verbose: |
| 208 | print("INFO: Checking " + filename) |
| 209 | |
| 210 | total_errors += file_check_banned_api(filename) |
| 211 | |
| 212 | print(str(total_errors) + " errors found") |
| 213 | |
| 214 | if total_errors == 0: |
| 215 | sys.exit(0) |
| 216 | else: |
| 217 | sys.exit(1) |