#!/usr/bin/env python2 import json import logging import os.path import re import pprint import sys __author__ = 'lawrence' MAX_TRAILING_SPACES_MSGS_PER_FILE = 1000 MAX_MIXED_TABS_MSGS_PER_FILE = 1000 MAX_SPACING_MSGS_PER_FILE = 1000 MAX_INDENT_MSGS_PER_FILE = 1000 INDENT_UNKNOWN = 0 INDENT_SPACES = 1 INDENT_TABS = 2 class ChangedFile: SOURCE_EXT = ['.c', '.cpp', '.cc', '.h', '.java', '.mk', '.xml'] C_JAVA_EXT = ['.c', '.cpp', '.java'] TEXT_RESOURCE_EXT = ['.rc', '.prop', '.te', '.kl', '.cfg', '.conf', '.dtd'] BINARY_RESOURCE_EXT = ['.txt', '.so', '.ko', '.apk', '.png', '.jpg', '.jpeg', '.gif'] def __init__(self, filename=None, is_new=False, mode=None): self.filename = filename self.file_ext = None if filename: self.on_update_filename() self.is_new = is_new self.mode = mode self.formattable_carriage_returns = False self.comments = {} def on_update_filename(self): if not self.filename: logging.error("couldn't get filename") return self.file_ext = os.path.splitext(self.filename)[1].lower() def is_source(self): #if self.file_ext in self.SOURCE_EXT: # return True return True # return true directly, doesn't check file type if self.filename: b = os.path.basename(self.filename) if (b and ( b.startswith("Kconfig") or b == "Makefile")): return True return False def is_binary_resource(self): if self.file_ext in self.BINARY_RESOURCE_EXT: return True return False def is_text_resource(self): if self.file_ext in self.TEXT_RESOURCE_EXT: return True return False def has_errors(self): if self.comments: return True # same as add_file_comments: if self.mode == 755 and self.should_not_be_executable(): return True if self.formattable_carriage_returns and self.should_not_have_carriage_return(): return True return False def should_check_line_diff(self): if self.is_source() or self.is_text_resource(): return True return False def should_not_be_executable(self): return self.is_source() or self.is_text_resource() or self.is_binary_resource() def should_not_have_carriage_return(self): if self.is_new: if self.is_source() or self.is_text_resource(): return True return False def should_check_statement_spacing(self): if self.file_ext in self.C_JAVA_EXT: return True return False def should_check_indent(self): if self.file_ext in self.C_JAVA_EXT: return True return False def add_file_comments(self): if self.mode == 755 and self.should_not_be_executable(): self.append_comment(0, "{} file should not be executable".format(self.file_ext)) if self.formattable_carriage_returns and self.should_not_have_carriage_return(): self.append_comment(0, "{} file should not have carriage returns (DOS line endings)".format(self.file_ext)) def append_comment(self, line, msg): if line in self.comments: self.comments[line] += "\n\n" self.comments[line] += msg else: self.comments[line] = msg # types of files/checks # source/resource: # should be non-executable (new/changed source + .ko, etc) # source: # should not have carriage return (new source + text resources) # text resource: # should not have trailing spaces (source + text resources) # should not have mixed spaces/tabs (source + text resources) # source + syntax # should have space in if statements (source c/java) # added line indent should match context # *could be imported code - warn only..? def check(filename): """ Checks unified diff. :param filename: diff file to check :return: 0 on patch errors, 1 on no patch errors, < 0 on other errors """ if not filename: return -1 try: with open(filename) as fp: return check_fp(fp) except OSError: logging.error(" failed to open? OSError %s", filename) return -2 except IOError: logging.error(" failed to open? IOError %s", filename) return -3 return -4 # TODO split checks into separate functions def check_fp(fp): file_sections = [] f = None check_lines = False check_statement_spacing = False trailing_sp_msg_count = 0 mixed_tabs_msg_count = 0 spacing_msg_count = 0 in_line_diff = False section_line_start = 0 section_line_start_err = False cur_line = 0 for line in fp: if line.startswith("diff"): if f and f.has_errors(): f.add_file_comments() file_sections.append(f) # start of new file f = ChangedFile() check_lines = False trailing_sp_msg_count = 0 mixed_tabs_msg_count = 0 spacing_msg_count = 0 indent_msg_count = 0 context_indent = INDENT_UNKNOWN in_line_diff = False # get filename # might fail on paths like "dir b/file.txt" m = re.match(r"^diff --git a/(.*) b/.*", line) if m: f.filename = m.group(1) f.on_update_filename() check_lines = f.should_check_line_diff() check_statement_spacing = f.should_check_statement_spacing() check_indent = f.should_check_indent() elif line.startswith("new file mode "): f.is_new = True if line.startswith("100755", len("new file mode ")): f.mode = 755 elif line.startswith("new mode 100755"): f.mode = 755 elif f and not f.filename and line.startswith("+++ b/"): # get filename if previously failed for some reason f.filename = line[len("+++ b/"):].rstrip('\r\n ') f.on_update_filename() check_lines = f.should_check_line_diff() check_statement_spacing = f.should_check_statement_spacing() check_indent = f.should_check_indent() else: if not check_lines: continue if line.startswith("@@ "): # keep track of line numbers # @@ -584,7 +681,7 @@ m = re.match(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)?\ @@", line) try: section_line_start = int(m.group(1)) except ValueError: logging.error("failed to parse section line start") section_line_start_err = True in_line_diff = True cur_line = section_line_start - 1 # next line is the start continue if in_line_diff: # keep track of line numbers if line[0] in ' +': cur_line += 1 # get last context line's indent if line[0] == " ": if line.startswith(" ", 1): context_indent = INDENT_SPACES elif line.startswith("\t", 1): context_indent = INDENT_TABS if line[0] == '+' and line[1] != '+': if check_lines and not section_line_start_err: if (f.is_new and not f.formattable_carriage_returns and line[-2] == '\r'): f.formattable_carriage_returns = True if trailing_sp_msg_count < MAX_TRAILING_SPACES_MSGS_PER_FILE: if (line.endswith(" \n") or line.endswith(" \r\n") or line.endswith("\t\n") or line.endswith("\t\r\n")): f.append_comment(cur_line, "trailing spaces") trailing_sp_msg_count += 1 if mixed_tabs_msg_count < MAX_MIXED_TABS_MSGS_PER_FILE: if re.match(r" +\t", line[1:]) or re.match(r"\t+ +\t", line[1:]): # tab space can be correct, but not space tab and tab space tab f.append_comment(cur_line, "possibly incorrect mixed spaces then tabs indentation") mixed_tabs_msg_count += 1 if check_statement_spacing and spacing_msg_count < MAX_SPACING_MSGS_PER_FILE: m = re.match(r"\s*(if|while|for|switch)", line[1:]) if (m): # line starts with if|while|for|switch keyword = m.group(1) # check parenthesis/brace spacing. if( -> if ( or ){ -> ) { m = re.match(r"\s*(?:if|while|for|switch)( ?)\(.*\)( ?)(\{?)", line[1:]) if (m): keyword_sp, brace_space, brace = m.groups() if keyword_sp != ' ' or ( brace == '{' and brace_space != ' '): f.append_comment(cur_line, "%s (...) %s // spacing around parenthesis" % (keyword, brace)) spacing_msg_count += 1 # check binary operator spacing on if|while line # cpplint.py: match = Search(r'[^<>=!\s](==|!=|<=|>=|\|\|)[^<>=!\s,;\)]', line if keyword in ['if', 'while']: m = re.search(r"[^<>=!\s](==|!=|<=|>=|\|\||&&)[^<>=!\s,;\)]", line[1:]) if (m): f.append_comment(cur_line, "spacing around %s" % m.group(1)) spacing_msg_count += 1 continue # do{ -> do { elif re.match(r"\s*do\{", line[1:]): f.append_comment(cur_line, 'do {') spacing_msg_count += 1 if check_indent and indent_msg_count < MAX_INDENT_MSGS_PER_FILE: if ((context_indent == INDENT_SPACES and line.startswith("\t", 1)) or (context_indent == INDENT_TABS and line.startswith(" ", 1))): f.append_comment(cur_line, "make sure indent style matches rest of file") indent_msg_count += 1 if f and f.has_errors(): f.add_file_comments() file_sections.append(f) if False: for f in file_sections: assert isinstance(f, ChangedFile) if f.comments: print f.filename pprint.pprint(f.comments) print "---" json_ret = file_comments_to_review(file_sections) if json_ret: print json_ret return 0 else: return 1 REPLY_MSG = "This is an automated message.\n\nIf you think these comments are incorrect, they can be ignored." POSITIVE_REPLY_MSG = "This is an automated message.\n\nNo problems found." def file_comments_to_array(changed_file): """ Return a list of comments for a CommentInput entry from a ChangedFile :param changed_file: a ChangedFile object :return: a list of comments for CommentInput """ ret = [] assert isinstance(changed_file, ChangedFile) for line, msg in changed_file.comments.iteritems(): ret.append({"line": line, "message": msg}) return ret def file_comments_to_review(changed_files): """ Create a JSON ReviewInput from a list of ChangedFiles :param changed_files: list of ChangedFiles :return: JSON ReviewInput string """ review = {} review['comments'] = {} for f in changed_files: if f.filename and f.comments: c = file_comments_to_array(f) if not c: logging.error("no comments for file") review['comments'][f.filename] = c if review['comments']: review['message'] = REPLY_MSG review['labels'] = {'Verified': -1} review['notify'] = 'OWNER' else: del review['comments'] review['message'] = POSITIVE_REPLY_MSG review['labels'] = {'Verified': +1} review['notify'] = 'OWNER' #return json.dumps(review, indent=2) return json.dumps(review) if __name__ == '__main__': if len(sys.argv) == 2: r = check(sys.argv[1]) sys.exit(r) else: sys.stderr.write("%s \n" % sys.argv[0]) sys.exit(0)