blob: b3eb451b98868082b27f4743a5f0b05a97666814
1 | #!/usr/bin/env python2 |
2 | |
3 | import json |
4 | import logging |
5 | import os.path |
6 | import re |
7 | import pprint |
8 | import sys |
9 | |
10 | __author__ = 'lawrence' |
11 | |
12 | MAX_TRAILING_SPACES_MSGS_PER_FILE = 5 |
13 | MAX_MIXED_TABS_MSGS_PER_FILE = 5 |
14 | MAX_SPACING_MSGS_PER_FILE = 5 |
15 | MAX_INDENT_MSGS_PER_FILE = 1 |
16 | MAX_FILES_WITH_MSGS = 10 |
17 | |
18 | INDENT_UNKNOWN = 0 |
19 | INDENT_SPACES = 1 |
20 | INDENT_TABS = 2 |
21 | |
22 | reply_msg_extra = '' |
23 | |
24 | class 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 | |
128 | def 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 |
150 | def 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 | |
222 | REPLY_MSG = "This is an automated message.\n\nPlease check it & commit again." |
223 | POSITIVE_REPLY_MSG = "This is an automated message.\n\nNo problems found." |
224 | |
225 | def 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 | |
238 | def 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 | |
261 | if __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 |