utils/check-style.py
author Josh Pelkey <jpelkey@gatech.edu>
Wed, 11 Aug 2010 11:37:37 -0400
changeset 6553 fb5ad9c7755a
permissions -rwxr-xr-x
update release notes and fix doxygen warnings
     1 #!/usr/bin/env python
     2 
     3 import os
     4 import subprocess
     5 import tempfile
     6 import sys
     7 import filecmp
     8 import optparse
     9 import shutil
    10 import difflib
    11 import re
    12 
    13 def hg_modified_files():
    14     files = os.popen ('hg st -nma')
    15     return [filename.strip() for filename in files]
    16 
    17 def copy_file(filename):
    18     [tmp,pathname] = tempfile.mkstemp()
    19     src = open(filename, 'r')
    20     dst = open(pathname, 'w')
    21     for line in src:
    22         dst.write(line)
    23     dst.close()
    24     src.close()
    25     return pathname
    26 
    27 # generate a temporary configuration file
    28 def uncrustify_config_file(level):
    29     level2 = """
    30 nl_collapse_empty_body=False
    31 nl_if_brace=Add
    32 nl_brace_else=Add
    33 nl_elseif_brace=Add
    34 nl_else_brace=Add
    35 nl_while_brace=Add
    36 nl_do_brace=Add
    37 nl_for_brace=Add
    38 nl_brace_while=Add
    39 nl_switch_brace=Add
    40 nl_after_case=True
    41 nl_namespace_brace=Remove
    42 nl_after_brace_open=True
    43 nl_class_leave_one_liners=False
    44 nl_enum_leave_one_liners=False
    45 nl_func_leave_one_liners=False
    46 nl_if_leave_one_liners=False
    47 nl_class_colon=Ignore
    48 nl_after_access_spec=1
    49 nl_after_semicolon=True
    50 pos_class_colon=Lead
    51 pos_class_comma=Trail
    52 pos_bool=Lead
    53 nl_class_init_args=Add
    54 nl_template_class=Add
    55 nl_class_brace=Add
    56 # does not work very well
    57 nl_func_type_name=Ignore
    58 nl_func_scope_name=Ignore
    59 nl_func_type_name_class=Ignore
    60 nl_func_proto_type_name=Ignore
    61 # function\\n(
    62 nl_func_paren=Remove
    63 nl_fdef_brace=Add
    64 nl_struct_brace=Add
    65 nl_enum_brace=Add
    66 nl_union_brace=Add
    67 mod_full_brace_do=Add
    68 mod_full_brace_for=Add
    69 mod_full_brace_if=Add
    70 mod_full_brace_while=Add
    71 mod_full_brace_for=Add
    72 mod_remove_extra_semicolon=True
    73 # max code width
    74 #code_width=128
    75 #ls_for_split_full=True
    76 #ls_func_split_full=True
    77 """
    78     level1 = """
    79 # extra spaces here and there
    80 sp_func_proto_paren=Add
    81 sp_func_def_paren=Add
    82 sp_func_call_paren=Add
    83 sp_brace_typedef=Add
    84 sp_enum_assign=Add
    85 sp_before_sparen=Add
    86 sp_after_semi_for=Add
    87 sp_arith=Add
    88 sp_assign=Add
    89 sp_compare=Add
    90 sp_cmt_cpp_start=Add
    91 sp_func_class_paren=Add
    92 sp_after_type=Add
    93 sp_type_func=Add
    94 sp_angle_paren=Add
    95 """
    96     level0 = """
    97 sp_after_semi_for=Ignore
    98 sp_before_sparen=Ignore
    99 sp_type_func=Ignore
   100 sp_after_type=Ignore
   101 nl_class_leave_one_liners=True
   102 nl_enum_leave_one_liners=True
   103 nl_func_leave_one_liners=True
   104 nl_assign_leave_one_liners=True
   105 #nl_collapse_empty_body=False
   106 nl_getset_leave_one_liners=True
   107 nl_if_leave_one_liners=True
   108 nl_fdef_brace=Ignore
   109 # finally, indentation configuration
   110 indent_with_tabs=0
   111 indent_namespace=false
   112 indent_columns=2
   113 indent_brace=2
   114 indent_case_brace=2
   115 indent_class=true
   116 indent_class_colon=True
   117 # alignment
   118 indent_align_assign=False
   119 align_left_shift=True
   120 # comment reformating disabled
   121 cmt_reflow_mode=1 # do not touch comments at all
   122 cmt_indent_multi=False # really, do not touch them
   123 """
   124     [tmp,pathname] = tempfile.mkstemp()
   125     dst = open(pathname, 'w')
   126     dst.write(level0)
   127     if level >= 1:
   128         dst.write(level1)
   129     if level >= 2:
   130         dst.write(level2)
   131     dst.close()
   132     return pathname
   133 
   134 class PatchChunkLine:
   135     SRC = 1
   136     DST = 2
   137     BOTH = 3
   138     def __init__(self):
   139         self.__type = 0
   140         self.__line = ''
   141     def set_src(self,line):
   142         self.__type = self.SRC
   143         self.__line = line
   144     def set_dst(self,line):
   145         self.__type = self.DST
   146         self.__line = line
   147     def set_both(self,line):
   148         self.__type = self.BOTH
   149         self.__line = line
   150     def append_to_line(self, s):
   151         self.__line = self.__line + s
   152     def line(self):
   153         return self.__line
   154     def is_src(self):
   155         return self.__type == self.SRC or self.__type == self.BOTH
   156     def is_dst(self):
   157         return self.__type == self.DST or self.__type == self.BOTH
   158     def write(self, f):
   159         if self.__type == self.SRC:
   160             f.write('-%s\n' % self.__line)
   161         elif self.__type == self.DST:
   162             f.write('+%s\n' % self.__line)
   163         elif self.__type == self.BOTH:
   164             f.write(' %s\n' % self.__line)
   165         else:
   166             raise Exception('invalid patch')
   167     
   168 
   169 class PatchChunk:
   170     def __init__(self, src_pos, dst_pos):
   171         self.__lines = []
   172         self.__src_pos = int(src_pos)
   173         self.__dst_pos = int(dst_pos)
   174     def src_start(self):
   175         return self.__src_pos
   176     def add_line(self,line):
   177         self.__lines.append(line)
   178     def src(self):
   179         src = []
   180         for line in self.__lines:
   181             if line.is_src():
   182                 src.append(line)
   183         return src
   184     def dst(self):
   185         dst = []
   186         for line in self.__lines:
   187             if line.is_dst():
   188                 dst.append(line)
   189         return dst
   190     def src_len(self):
   191         return len(self.src())
   192     def dst_len(self):
   193         return len(self.dst())
   194     def write(self,f):
   195         f.write('@@ -%d,%d +%d,%d @@\n' % (self.__src_pos, self.src_len(),
   196                                            self.__dst_pos, self.dst_len()))
   197         for line in self.__lines:
   198             line.write(f)
   199 
   200 class Patch:
   201     def __init__(self):
   202         self.__src = ''
   203         self.__dst = ''
   204         self.__chunks = []
   205     def add_chunk(self, chunk):
   206         self.__chunks.append(chunk)
   207     def chunks(self):
   208         return self.__chunks
   209     def set_src(self,src):
   210         self.__src = src
   211     def set_dst(self,dst):
   212         self.__dst = dst
   213     def apply(self,filename):
   214         # XXX: not implemented
   215         return
   216     def write(self,f):
   217         f.write('--- %s\n' % self.__src )
   218         f.write('+++ %s\n' % self.__dst )
   219         for chunk in self.__chunks:
   220             chunk.write(f)
   221 
   222 def parse_patchset(generator):
   223     src_file = re.compile('^--- (.*)$')
   224     dst_file = re.compile('^\+\+\+ (.*)$')
   225     chunk_start = re.compile('^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@')
   226     src = re.compile('^-(.*)$')
   227     dst = re.compile('^\+(.*)$')
   228     both = re.compile('^ (.*)$')
   229     patchset = []
   230     current_patch = None
   231     for line in generator:
   232         m = src_file.search(line)
   233         if m is not None:
   234             current_patch = Patch()
   235             patchset.append(current_patch)
   236             current_patch.set_src(m.group(1))
   237             continue
   238         m = dst_file.search(line)
   239         if m is not None:
   240             current_patch.set_dst(m.group(1))
   241             continue
   242         m = chunk_start.search(line)
   243         if m is not None:
   244             current_chunk = PatchChunk(m.group(1), m.group(3))
   245             current_patch.add_chunk(current_chunk)
   246             continue
   247         m = src.search(line)
   248         if m is not None:
   249             l = PatchChunkLine()
   250             l.set_src(m.group(1))
   251             current_chunk.add_line(l)
   252             continue
   253         m = dst.search(line)
   254         if m is not None:
   255             l = PatchChunkLine()
   256             l.set_dst(m.group(1))
   257             current_chunk.add_line(l)
   258             continue
   259         m = both.search(line)
   260         if m is not None:
   261             l = PatchChunkLine()
   262             l.set_both(m.group(1))
   263             current_chunk.add_line(l)
   264             continue
   265         raise Exception()
   266     return patchset
   267 
   268 def remove_trailing_whitespace_changes(patch_generator):
   269     whitespace = re.compile('^(.*)([ \t]+)$')
   270     patchset = parse_patchset(patch_generator)
   271     for patch in patchset:
   272         for chunk in patch.chunks():
   273             src = chunk.src()
   274             dst = chunk.dst()
   275             try:
   276                 for i in range(0,len(src)):
   277                     s = src[i]
   278                     d = dst[i]
   279                     m = whitespace.search(s.line())
   280                     if m is not None and m.group(1) == d.line():
   281                         d.append_to_line(m.group(2))
   282             except:
   283                 return patchset
   284     return patchset
   285 
   286 
   287 def indent(source, debug, level):
   288     output = tempfile.mkstemp()[1]
   289     # apply uncrustify
   290     cfg = uncrustify_config_file(level)
   291     if debug:
   292         sys.stderr.write('original file=' + source + '\n')
   293         sys.stderr.write('uncrustify config file=' + cfg + '\n')
   294         sys.stderr.write('temporary file=' + output + '\n')
   295     try:
   296         uncrust = subprocess.Popen(['uncrustify', '-c', cfg, '-f', source, '-o', output],
   297                                    stdin = subprocess.PIPE,
   298                                    stdout = subprocess.PIPE,
   299                                    stderr = subprocess.PIPE)
   300         (out, err) = uncrust.communicate('')
   301         if debug:
   302             sys.stderr.write(out)
   303             sys.stderr.write(err)
   304     except OSError:
   305         raise Exception ('uncrustify not installed')
   306     # generate a diff file
   307     src = open(source, 'r')
   308     dst = open(output, 'r')
   309     diff = difflib.unified_diff(src.readlines(), dst.readlines(), 
   310                                 fromfile=source, tofile=output)
   311     src.close()
   312     dst.close()
   313     if debug:
   314         initial_diff = tempfile.mkstemp()[1]
   315         sys.stderr.write('initial diff file=' + initial_diff + '\n')
   316         tmp = open(initial_diff, 'w')
   317         tmp.writelines(diff)
   318         tmp.close()
   319     final_diff = tempfile.mkstemp()[1]
   320     if level < 3:
   321         patchset = remove_trailing_whitespace_changes(diff);
   322         dst = open(final_diff, 'w')
   323         if len(patchset) != 0:
   324             patchset[0].write(dst)
   325         dst.close()
   326     else:
   327         dst = open(final_diff, 'w')
   328         dst.writelines(diff)
   329         dst.close()
   330             
   331             
   332     # apply diff file
   333     if debug:
   334         sys.stderr.write('final diff file=' + final_diff + '\n')
   335     shutil.copyfile(source,output)
   336     patch = subprocess.Popen(['patch', '-p1', '-i', final_diff, output],
   337                              stdin = subprocess.PIPE,
   338                              stdout = subprocess.PIPE,
   339                              stderr = subprocess.PIPE)
   340     (out, err) = patch.communicate('')
   341     if debug:
   342         sys.stderr.write(out)
   343         sys.stderr.write(err)
   344     return output
   345  
   346 
   347 
   348 def indent_files(files, diff=False, debug=False, level=0, inplace=False):
   349     output = []
   350     for f in files:
   351         dst = indent(f, debug=debug, level=level)
   352         output.append([f,dst])
   353 
   354     # First, copy to inplace
   355     if inplace:
   356         for src,dst in output:
   357             shutil.copyfile(dst,src)
   358         return True
   359 
   360     # now, compare
   361     failed = []
   362     for src,dst in output:
   363         if filecmp.cmp(src,dst) == 0:
   364             failed.append([src, dst])
   365     if len(failed) > 0:
   366         if not diff:
   367             print 'Found %u badly indented files:' % len(failed)
   368             for src,dst in failed:
   369                 print '  ' + src
   370         else:
   371             for src,dst in failed:
   372                 s = open(src, 'r').readlines()
   373                 d = open(dst, 'r').readlines()
   374                 for line in difflib.unified_diff(s, d, fromfile=src, tofile=dst):
   375                     sys.stdout.write(line)
   376         return False
   377     return True
   378 
   379 def run_as_hg_hook(ui, repo, **kwargs):
   380     # hack to work around mercurial < 1.3 bug
   381     from mercurial import lock, error
   382     lock.LockError = error.LockError
   383     # actually do the work
   384     files = hg_modified_files()
   385     if not indent_files(files, inplace=False):
   386         return True
   387     return False
   388 
   389 def run_as_main():
   390     parser = optparse.OptionParser()
   391     parser.add_option('--debug', action='store_true', dest='debug', default=False,
   392                       help='Output some debugging information')
   393     parser.add_option('-l', '--level', type='int', dest='level', default=0,
   394                       help="Level of style conformance: higher levels include all lower levels. "
   395                       "level=0: re-indent only. level=1: add extra spaces. level=2: insert extra newlines and "
   396                       "extra braces around single-line statements. level=3: remove all trailing spaces")
   397     parser.add_option('--check-hg-hook', action='store_true', dest='hg_hook', default=False, 
   398                       help='Get the list of files to check from mercurial\'s list of modified '
   399                       'and added files and assume that the script runs as a pretxncommit mercurial hook')
   400     parser.add_option('--check-hg', action='store_true', dest='hg', default=False,
   401                       help="Get the list of files to check from mercurial\'s list of modified and added files")
   402     parser.add_option('-f', '--check-file', action='store', dest='file', default='',
   403                       help="Check a single file")
   404     parser.add_option('--diff', action='store_true', dest='diff', default=False,
   405                       help="Generate a diff on stdout of the indented files")
   406     parser.add_option('-i', '--in-place', action='store_true', dest='in_place', default=False,
   407                       help="Indent the input files in-place")
   408     (options,args) = parser.parse_args()
   409     debug = options.debug
   410     if options.hg_hook:
   411         files = hg_modified_files()
   412         if not indent_files(files, debug=options.debug,
   413                             level=options.level,
   414                             inplace=False):
   415             sys.exit(1)
   416     elif options.hg:
   417         files = hg_modified_files()
   418         indent_files(files, diff=options.diff, 
   419                      debug=options.debug,
   420                      level=options.level,
   421                      inplace=options.in_place)
   422     elif options.file != '':
   423         file = options.file
   424         if not os.path.exists(file) or \
   425                 not os.path.isfile(file):
   426             print 'file %s does not exist' % file
   427             sys.exit(1)
   428         indent_files([file], diff=options.diff, 
   429                      debug=options.debug,
   430                      level=options.level,
   431                      inplace=options.in_place)
   432     sys.exit(0)
   433 
   434 if __name__ == '__main__':
   435 #    try:
   436         run_as_main()
   437 #    except Exception, e:
   438 #        sys.stderr.write(str(e) + '\n')
   439 #        sys.exit(1)