blob: 15e94abe3190fa1002606483e0beafffa5bfd018 [file] [log] [blame]
Bo Lv72d0e902023-01-02 14:27:34 +00001#!/usr/bin/env python2
2
3import json
4import logging
5import os.path
6import re
7import pprint
8import sys
9
10__author__ = 'lawrence'
11
12MAX_TRAILING_SPACES_MSGS_PER_FILE = 1000
13MAX_MIXED_TABS_MSGS_PER_FILE = 1000
14MAX_SPACING_MSGS_PER_FILE = 1000
15MAX_INDENT_MSGS_PER_FILE = 1000
16
17INDENT_UNKNOWN = 0
18INDENT_SPACES = 1
19INDENT_TABS = 2
20
21class ChangedFile:
22 SOURCE_EXT = ['.c', '.cpp', '.cc', '.h', '.java', '.mk', '.xml']
23 C_JAVA_EXT = ['.c', '.cpp', '.java']
24 TEXT_RESOURCE_EXT = ['.rc', '.prop', '.te', '.kl', '.cfg', '.conf', '.dtd']
25 BINARY_RESOURCE_EXT = ['.txt', '.so', '.ko', '.apk', '.png', '.jpg', '.jpeg', '.gif']
26
27 def __init__(self, filename=None, is_new=False, mode=None):
28 self.filename = filename
29 self.file_ext = None
30 if filename:
31 self.on_update_filename()
32 self.is_new = is_new
33 self.mode = mode
34 self.formattable_carriage_returns = False
35 self.comments = {}
36
37 def on_update_filename(self):
38 if not self.filename:
39 logging.error("couldn't get filename")
40 return
41 self.file_ext = os.path.splitext(self.filename)[1].lower()
42
43 def is_source(self):
44 #if self.file_ext in self.SOURCE_EXT:
45 # return True
46 return True # return true directly, doesn't check file type
47 if self.filename:
48 b = os.path.basename(self.filename)
49 if (b and (
50 b.startswith("Kconfig") or
51 b == "Makefile")):
52 return True
53 return False
54
55 def is_binary_resource(self):
56 if self.file_ext in self.BINARY_RESOURCE_EXT:
57 return True
58 return False
59
60 def is_text_resource(self):
61 if self.file_ext in self.TEXT_RESOURCE_EXT:
62 return True
63 return False
64
65 def has_errors(self):
66 if self.comments:
67 return True
68 # same as add_file_comments:
69 if self.mode == 755 and self.should_not_be_executable():
70 return True
71 if self.formattable_carriage_returns and self.should_not_have_carriage_return():
72 return True
73 return False
74
75 def should_check_line_diff(self):
76 if self.is_source() or self.is_text_resource():
77 return True
78 return False
79
80 def should_not_be_executable(self):
81 return self.is_source() or self.is_text_resource() or self.is_binary_resource()
82
83 def should_not_have_carriage_return(self):
84 if self.is_new:
85 if self.is_source() or self.is_text_resource():
86 return True
87 return False
88
89 def should_check_statement_spacing(self):
90 if self.file_ext in self.C_JAVA_EXT:
91 return True
92 return False
93
94 def should_check_indent(self):
95 if self.file_ext in self.C_JAVA_EXT:
96 return True
97 return False
98
99 def add_file_comments(self):
100 if self.mode == 755 and self.should_not_be_executable():
101 self.append_comment(0, "{} file should not be executable".format(self.file_ext))
102 if self.formattable_carriage_returns and self.should_not_have_carriage_return():
103 self.append_comment(0, "{} file should not have carriage returns (DOS line endings)".format(self.file_ext))
104
105 def append_comment(self, line, msg):
106 if line in self.comments:
107 self.comments[line] += "\n\n"
108 self.comments[line] += msg
109 else:
110 self.comments[line] = msg
111
112
113 # types of files/checks
114 # source/resource:
115 # should be non-executable (new/changed source + .ko, etc)
116 # source:
117 # should not have carriage return (new source + text resources)
118 # text resource:
119 # should not have trailing spaces (source + text resources)
120 # should not have mixed spaces/tabs (source + text resources)
121 # source + syntax
122 # should have space in if statements (source c/java)
123 # added line indent should match context
124 # *could be imported code - warn only..?
125
126
127def check(filename):
128 """
129 Checks unified diff.
130 :param filename: diff file to check
131 :return: 0 on patch errors, 1 on no patch errors, < 0 on other errors
132 """
133 if not filename:
134 return -1
135
136 try:
137 with open(filename) as fp:
138 return check_fp(fp)
139 except OSError:
140 logging.error(" failed to open? OSError %s", filename)
141 return -2
142 except IOError:
143 logging.error(" failed to open? IOError %s", filename)
144 return -3
145 return -4
146
147
148# TODO split checks into separate functions
149def check_fp(fp):
150 file_sections = []
151 f = None
152 check_lines = False
153 check_statement_spacing = False
154 trailing_sp_msg_count = 0
155 mixed_tabs_msg_count = 0
156 spacing_msg_count = 0
157 in_line_diff = False
158 section_line_start = 0
159 section_line_start_err = False
160 cur_line = 0
161 for line in fp:
162 if line.startswith("diff"):
163 if f and f.has_errors():
164 f.add_file_comments()
165 file_sections.append(f)
166 # start of new file
167 f = ChangedFile()
168 check_lines = False
169 trailing_sp_msg_count = 0
170 mixed_tabs_msg_count = 0
171 spacing_msg_count = 0
172 indent_msg_count = 0
173 context_indent = INDENT_UNKNOWN
174 in_line_diff = False
175
176 # get filename
177 # might fail on paths like "dir b/file.txt"
178 m = re.match(r"^diff --git a/(.*) b/.*", line)
179 if m:
180 f.filename = m.group(1)
181 f.on_update_filename()
182 check_lines = f.should_check_line_diff()
183 check_statement_spacing = f.should_check_statement_spacing()
184 check_indent = f.should_check_indent()
185 elif line.startswith("new file mode "):
186 f.is_new = True
187 if line.startswith("100755", len("new file mode ")):
188 f.mode = 755
189 elif line.startswith("new mode 100755"):
190 f.mode = 755
191 elif f and not f.filename and line.startswith("+++ b/"):
192 # get filename if previously failed for some reason
193 f.filename = line[len("+++ b/"):].rstrip('\r\n ')
194 f.on_update_filename()
195 check_lines = f.should_check_line_diff()
196 check_statement_spacing = f.should_check_statement_spacing()
197 check_indent = f.should_check_indent()
198 else:
199 if not check_lines:
200 continue
201 if line.startswith("@@ "):
202 # keep track of line numbers
203 # @@ -584,7 +681,7 @@
204 m = re.match(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)?\ @@", line)
205 try:
206 section_line_start = int(m.group(1))
207 except ValueError:
208 logging.error("failed to parse section line start")
209 section_line_start_err = True
210 in_line_diff = True
211 cur_line = section_line_start - 1 # next line is the start
212 continue
213 if in_line_diff:
214 # keep track of line numbers
215 if line[0] in ' +':
216 cur_line += 1
217 # get last context line's indent
218 if line[0] == " ":
219 if line.startswith(" ", 1):
220 context_indent = INDENT_SPACES
221 elif line.startswith("\t", 1):
222 context_indent = INDENT_TABS
223 if line[0] == '+' and line[1] != '+':
224 if check_lines and not section_line_start_err:
225 if (f.is_new and
226 not f.formattable_carriage_returns and
227 line[-2] == '\r'):
228 f.formattable_carriage_returns = True
229
230 if trailing_sp_msg_count < MAX_TRAILING_SPACES_MSGS_PER_FILE:
231 if (line.endswith(" \n") or
232 line.endswith(" \r\n") or
233 line.endswith("\t\n") or
234 line.endswith("\t\r\n")):
235 f.append_comment(cur_line, "trailing spaces")
236 trailing_sp_msg_count += 1
237
238 if mixed_tabs_msg_count < MAX_MIXED_TABS_MSGS_PER_FILE:
239 if re.match(r" +\t", line[1:]) or re.match(r"\t+ +\t", line[1:]):
240 # tab space can be correct, but not space tab and tab space tab
241 f.append_comment(cur_line, "possibly incorrect mixed spaces then tabs indentation")
242 mixed_tabs_msg_count += 1
243
244 if check_statement_spacing and spacing_msg_count < MAX_SPACING_MSGS_PER_FILE:
245 m = re.match(r"\s*(if|while|for|switch)", line[1:])
246 if (m):
247 # line starts with if|while|for|switch
248 keyword = m.group(1)
249 # check parenthesis/brace spacing. if( -> if ( or ){ -> ) {
250 m = re.match(r"\s*(?:if|while|for|switch)( ?)\(.*\)( ?)(\{?)", line[1:])
251 if (m):
252 keyword_sp, brace_space, brace = m.groups()
253 if keyword_sp != ' ' or (
254 brace == '{' and brace_space != ' '):
255 f.append_comment(cur_line,
256 "%s (...) %s // spacing around parenthesis" % (keyword, brace))
257 spacing_msg_count += 1
258
259 # check binary operator spacing on if|while line
260 # cpplint.py: match = Search(r'[^<>=!\s](==|!=|<=|>=|\|\|)[^<>=!\s,;\)]', line
261 if keyword in ['if', 'while']:
262 m = re.search(r"[^<>=!\s](==|!=|<=|>=|\|\||&&)[^<>=!\s,;\)]", line[1:])
263 if (m):
264 f.append_comment(cur_line, "spacing around %s" % m.group(1))
265 spacing_msg_count += 1
266 continue
267 # do{ -> do {
268 elif re.match(r"\s*do\{", line[1:]):
269 f.append_comment(cur_line, 'do {')
270 spacing_msg_count += 1
271
272 if check_indent and indent_msg_count < MAX_INDENT_MSGS_PER_FILE:
273 if ((context_indent == INDENT_SPACES and line.startswith("\t", 1)) or
274 (context_indent == INDENT_TABS and line.startswith(" ", 1))):
275 f.append_comment(cur_line, "make sure indent style matches rest of file")
276 indent_msg_count += 1
277
278 if f and f.has_errors():
279 f.add_file_comments()
280 file_sections.append(f)
281
282 if False:
283 for f in file_sections:
284 assert isinstance(f, ChangedFile)
285 if f.comments:
286 print f.filename
287 pprint.pprint(f.comments)
288 print "---"
289 json_ret = file_comments_to_review(file_sections)
290 if json_ret:
291 print json_ret
292 return 0
293 else:
294 return 1
295
296REPLY_MSG = "This is an automated message.\n\nIf you think these comments are incorrect, they can be ignored."
297POSITIVE_REPLY_MSG = "This is an automated message.\n\nNo problems found."
298
299def file_comments_to_array(changed_file):
300 """
301 Return a list of comments for a CommentInput entry from a ChangedFile
302 :param changed_file: a ChangedFile object
303 :return: a list of comments for CommentInput
304 """
305 ret = []
306 assert isinstance(changed_file, ChangedFile)
307 for line, msg in changed_file.comments.iteritems():
308 ret.append({"line": line,
309 "message": msg})
310 return ret
311
312def file_comments_to_review(changed_files):
313 """
314 Create a JSON ReviewInput from a list of ChangedFiles
315 :param changed_files: list of ChangedFiles
316 :return: JSON ReviewInput string
317 """
318 review = {}
319 review['comments'] = {}
320 for f in changed_files:
321 if f.filename and f.comments:
322
323 c = file_comments_to_array(f)
324 if not c:
325 logging.error("no comments for file")
326 review['comments'][f.filename] = c
327 if review['comments']:
328 review['message'] = REPLY_MSG
329 review['labels'] = {'Verified': -1}
330 review['notify'] = 'OWNER'
331 else:
332 del review['comments']
333 review['message'] = POSITIVE_REPLY_MSG
334 review['labels'] = {'Verified': +1}
335 review['notify'] = 'OWNER'
336 #return json.dumps(review, indent=2)
337 return json.dumps(review)
338
339if __name__ == '__main__':
340 if len(sys.argv) == 2:
341 r = check(sys.argv[1])
342 sys.exit(r)
343 else:
344 sys.stderr.write("%s <patch filename>\n" % sys.argv[0])
345 sys.exit(0)
346