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