summaryrefslogtreecommitdiff
path: root/check_patch.py (plain)
blob: b3eb451b98868082b27f4743a5f0b05a97666814
1#!/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 = 5
13MAX_MIXED_TABS_MSGS_PER_FILE = 5
14MAX_SPACING_MSGS_PER_FILE = 5
15MAX_INDENT_MSGS_PER_FILE = 1
16MAX_FILES_WITH_MSGS = 10
17
18INDENT_UNKNOWN = 0
19INDENT_SPACES = 1
20INDENT_TABS = 2
21
22reply_msg_extra = ''
23
24class ChangedFile:
25 SOURCE_EXT = ['.c', '.cpp', '.cc', '.h', '.java', '.mk', '.xml']
26 C_JAVA_EXT = ['.c', '.cpp', '.java']
27 TEXT_RESOURCE_EXT = ['.rc', '.prop', '.te', '.kl', '.cfg', '.conf', '.dtd']
28 BINARY_RESOURCE_EXT = ['.txt', '.so', '.ko', '.apk', '.png', '.jpg', '.jpeg', '.gif']
29
30 def __init__(self, filename=None, is_new=False, mode=None):
31 self.filename = filename
32 self.file_ext = None
33 if filename:
34 self.on_update_filename()
35 self.is_new = is_new
36 self.mode = mode
37 self.formattable_carriage_returns = False
38 self.comments = {}
39
40 def on_update_filename(self):
41 if not self.filename:
42 logging.error("couldn't get filename")
43 return
44 self.file_ext = os.path.splitext(self.filename)[1].lower()
45
46 def is_source(self):
47 if self.file_ext in self.SOURCE_EXT:
48 return True
49 if self.filename:
50 b = os.path.basename(self.filename)
51 if (b and (
52 b.startswith("Kconfig") or
53 b == "Makefile") or
54 b.endswith("_defconfig")):
55 return True
56 return False
57
58 def is_binary_resource(self):
59 if self.file_ext in self.BINARY_RESOURCE_EXT:
60 return True
61 return False
62
63 def is_text_resource(self):
64 if self.file_ext in self.TEXT_RESOURCE_EXT:
65 return True
66 return False
67
68 def has_errors(self):
69 if self.comments:
70 return True
71 # same as add_file_comments:
72 if self.mode == 755 and self.should_not_be_executable():
73 return True
74 if self.formattable_carriage_returns and self.should_not_have_carriage_return():
75 return True
76 return False
77
78 def should_check_line_diff(self):
79 if self.is_source() or self.is_text_resource():
80 return True
81 return False
82
83 def should_not_be_executable(self):
84 return self.is_source() or self.is_text_resource() or self.is_binary_resource()
85
86 def should_not_have_carriage_return(self):
87 if self.is_new:
88 if self.is_source() or self.is_text_resource():
89 return True
90 return False
91
92 def should_check_statement_spacing(self):
93 if self.file_ext in self.C_JAVA_EXT:
94 return True
95 return False
96
97 def should_check_indent(self):
98 if self.file_ext in self.C_JAVA_EXT:
99 return True
100 return False
101
102 def add_file_comments(self):
103 if self.mode == 755 and self.should_not_be_executable():
104 self.append_comment(0, "{} file should not be executable".format(self.file_ext))
105
106 def append_comment(self, line, msg):
107 if line in self.comments:
108 self.comments[line] += "\n\n"
109 self.comments[line] += msg
110 else:
111 self.comments[line] = msg
112
113
114 # types of files/checks
115 # source/resource:
116 # should be non-executable (new/changed source + .ko, etc)
117 # source:
118 # should not have carriage return (new source + text resources)
119 # text resource:
120 # should not have trailing spaces (source + text resources)
121 # should not have mixed spaces/tabs (source + text resources)
122 # source + syntax
123 # should have space in if statements (source c/java)
124 # added line indent should match context
125 # *could be imported code - warn only..?
126
127
128def check(filename):
129 """
130 Checks unified diff.
131 :param filename: diff file to check
132 :return: 0 on patch errors, 1 on no patch errors, < 0 on other errors
133 """
134 if not filename:
135 return -1
136
137 try:
138 with open(filename) as fp:
139 return check_fp(fp)
140 except OSError:
141 logging.error(" failed to open? OSError %s", filename)
142 return -2
143 except IOError:
144 logging.error(" failed to open? IOError %s", filename)
145 return -3
146 return -4
147
148
149# TODO split checks into separate functions
150def check_fp(fp):
151 file_sections = []
152 f = None
153 check_lines = False
154 check_statement_spacing = False
155 trailing_sp_msg_count = 0
156 mixed_tabs_msg_count = 0
157 spacing_msg_count = 0
158 in_line_diff = False
159 section_line_start = 0
160 section_line_start_err = False
161 files_with_msgs = 0
162 cur_line = 0
163 error_num = 0
164 for line in fp:
165 if line.startswith("diff"):
166 if f and f.has_errors():
167 f.add_file_comments()
168 error_num += 1
169 file_sections.append(f)
170 if len(file_sections) >= MAX_FILES_WITH_MSGS:
171 global reply_msg_extra
172 reply_msg_extra += '\n\nStopped code style check at {} files.'.format(MAX_FILES_WITH_MSGS)
173 break;
174 # start of new file
175 f = ChangedFile()
176 check_lines = False
177 trailing_sp_msg_count = 0
178 mixed_tabs_msg_count = 0
179 spacing_msg_count = 0
180 indent_msg_count = 0
181 context_indent = INDENT_UNKNOWN
182 in_line_diff = False
183
184 # get filename
185 # might fail on paths like "dir b/file.txt"
186 m = re.match(r"^diff --git a/(.*) b/.*", line)
187 if m:
188 f.filename = m.group(1)
189 f.on_update_filename()
190 check_lines = f.should_check_line_diff()
191 check_statement_spacing = f.should_check_statement_spacing()
192 check_indent = f.should_check_indent()
193 elif line.startswith("new file mode "):
194 f.is_new = True
195 if line.startswith("100755", len("new file mode ")):
196 f.mode = 755
197 elif line.startswith("new mode 100755"):
198 f.mode = 755
199
200 if f and f.has_errors():
201 f.add_file_comments()
202 file_sections.append(f)
203 error_num += 1
204
205 if False:
206 for f in file_sections:
207 assert isinstance(f, ChangedFile)
208 if f.comments:
209 print f.filename
210 pprint.pprint(f.comments)
211 print "---"
212 json_ret = file_comments_to_review(file_sections)
213 if json_ret:
214 print json_ret
215
216 #print error_num
217 if error_num > 0:
218 return 1
219 else:
220 return 0
221
222REPLY_MSG = "This is an automated message.\n\nPlease check it & commit again."
223POSITIVE_REPLY_MSG = "This is an automated message.\n\nNo problems found."
224
225def file_comments_to_array(changed_file):
226 """
227 Return a list of comments for a CommentInput entry from a ChangedFile
228 :param changed_file: a ChangedFile object
229 :return: a list of comments for CommentInput
230 """
231 ret = []
232 assert isinstance(changed_file, ChangedFile)
233 for line, msg in changed_file.comments.iteritems():
234 ret.append({"line": line,
235 "message": msg})
236 return ret
237
238def file_comments_to_review(changed_files):
239 """
240 Create a JSON ReviewInput from a list of ChangedFiles
241 :param changed_files: list of ChangedFiles
242 :return: JSON ReviewInput string
243 """
244 review = {}
245 review['comments'] = {}
246 for f in changed_files:
247 if f.filename and f.comments:
248
249 c = file_comments_to_array(f)
250 if not c:
251 logging.error("no comments for file")
252 review['comments'][f.filename] = c
253 if review['comments']:
254 review['message'] = REPLY_MSG + reply_msg_extra
255 else:
256 del review['comments']
257 review['message'] = POSITIVE_REPLY_MSG
258 #return json.dumps(review, indent=2)
259 return json.dumps(review)
260
261if __name__ == '__main__':
262 if len(sys.argv) == 2:
263 sys.stderr.write("%s <patch filename>....\n" % sys.argv[0])
264 r = check(sys.argv[1])
265 sys.exit(r)
266 else:
267 sys.stderr.write("%s <patch filename>\n" % sys.argv[0])
268 sys.exit(0)
269