blob: 931b1bdf21d7400a76973756c15df0da0d292c59 [file] [log] [blame]
Leonardo Sandoval314eed82020-08-05 13:32:04 -05001#!/usr/bin/env python3
2#
3# Copyright (c) 2019, Arm Limited. All rights reserved.
4#
5# SPDX-License-Identifier: BSD-3-Clause
6#
7
8import argparse
9import os
10import re
11import sys
12import utils
13
14# File extensions to check
15VALID_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 '/'.
19IGNORED_FOLDERS = (
20 "tools",
21 "docs"
22)
23
24# List of ignored files in folders that aren't ignored
25IGNORED_FILES = ()
26
27# Regular expression for searching the Banned APIs. This is taken from the
28# Coding guideline in TF-A repo
29BANNED_APIS = ["strcpy", "wcscpy", "strncpy", "strcat", "wcscat", "strncat",
30 "sprintf", "vsprintf", "strtok", "atoi", "atol", "atoll",
31 "itoa", "ltoa", "lltoa"]
32BANNED_PATTERN = re.compile('|'.join(BANNED_APIS))
33
34COMMENTS_PATTERN = re.compile(r"//|/\*|\*/")
35
36
37def 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
74def 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
115def 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
129def 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
146def 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 """,
Leonardo Sandoval900de582020-09-07 18:34:57 -0500171 default="origin/master")
Leonardo Sandoval314eed82020-08-05 13:32:04 -0500172 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
184if __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)