import repository from arizona
[raven.git] / tools / stylecheck / stylecheck.py
1 #!/usr/bin/env python
2
3 """
4 <Program Name>
5    stylecheck.py
6
7 <Started>
8    December 8, 2007
9
10 <Author>
11    Ivan Vazquez
12
13 <Purpose>
14    Provides basic Stork python style checking.
15    
16    Currently handles:
17       - Indentation checking (3 spaces)
18       - Unix (\n) not Windows style text files (\r\n)
19       - Header block (Program Name, Started date, Author, and Purpose)
20       - Per Function block (Purpose, Arguments, Exceptions, Side Effects,
21         Returns)
22       - >= 4 blank lines (\n's) between functions
23       - warning on lambda functions
24       - warning on generic  try: except: blocks      
25
26 """
27
28 import sys
29 import re
30 import getopt
31 import os.path
32
33
34
35
36
37 def usage():
38    """
39    <Purpose>
40       Print usage message
41
42    <Arguments>
43       None
44
45    <Exceptions>
46       None
47
48    <Side Effects>
49       Prints usage message
50
51    <Returns>
52       Nothing
53    """
54    print """Usage:  stylecheck <Options>
55                    [-c Collect/Collate output and print at end]
56                    [-v Verbose:  use with -c for per-line info at end]
57                    [-e Errors only, warnings not printed]
58          """
59    
60
61
62
63
64
65 def main(argv):
66    """
67    <Purpose>
68       Checks the style of the files specfied.
69
70    <Arguments>
71       argv:  array of file names.
72
73    <Exceptions>
74       None
75
76    <Side Effects>
77       Opens, reads files and prints results of style checking.
78
79    <Returns>
80       None.
81    """
82    options = {'collect': False,
83                'verbose': False,
84                'errors_only': False,
85                'message_keywords': ('warning', 'error'),
86                'message_separator': ' ',
87                'debug': False,
88                'module_doc': None,
89                }
90
91    
92    if len(argv) < 1:
93       usage()
94       sys.exit(1)
95       
96    (opts, argv) = getopt.getopt(argv, "cveD")
97
98    for (opt, optval) in opts:
99       if opt == "-c":
100          options['collect'] = True
101          
102       if opt == "-v":
103          options['verbose'] = True
104          
105       if opt == "-e":
106          options['errors_only'] = True
107
108       if opt == "-D":
109          options['debug'] = True
110          
111    for file in argv:
112       checkfile(options, file)
113
114
115
116
117
118 def checkfile(options, filename):
119    """
120    <Purpose>
121       Check the style of the file specified.
122
123    <Arguments>
124        options:  dict of options to use in checking (see usage fn.)
125       filename:  filename of file to check.
126
127    <Exceptions>
128       OSError
129
130    <Side Effects>
131       File (if exists and readable) is opened, read, and results of style check
132       are printed to stdout.
133
134    <Returns>
135       True if file exists, is readable and passes style check, otherwise False.
136    """
137    
138    if os.path.isfile(filename) != True:
139       print >> sys.stderr, filename + " is not a file"
140       return False
141    
142    
143    try:
144       file  = open(filename)
145       lines = file.readlines()
146       
147    except IOError:
148       error(filename, -1, "IOError reading from " + filename)
149       return False
150
151    return checkfile_lines(options, filename, lines)
152
153
154
155
156
157 def checkfile_lines(options, filename, lines):
158    """
159    <Purpose>
160       Check the style of the file in the lines specified.
161
162    <Arguments>
163        options:  dict of options to use in checking (see usage fn.)
164       filename:  filename of file from which lines were read
165          lines:  list of lines of text from file
166
167    <Exceptions>
168       None
169
170    <Side Effects>
171       Results of style check are printed to stdout.
172
173    <Returns>
174       True if  passes style check, otherwise False.
175    """
176    ### Constants needed
177    min_blank_lines_twixt_fns = 4
178    stork_indentation_count = 3
179    windows_eol = '\r\n'
180
181    #### Variables we need to keep track of
182    
183    # lists of [<fName>, <fIndentation>, <startLineNo>, <docString>]
184    function_info   = []
185
186    num_blank_lines = 0
187    line_number     = 0
188    found_string    = None
189    in_string       = False
190    just_started_fn = False
191    
192    for line in lines:
193
194       line_number += 1
195
196          
197       # How many times have we iterated on this line?
198       line_iterations = 0
199       line_done       = False;
200
201       # Check for windows-style \r\n line-ending
202       if line.endswith( windows_eol ):
203          warn(options, filename, line_number,
204                "line terminated with windows EOL")
205
206       # Remove trailing newlines/crs
207       line = line.rstrip(windows_eol)
208       
209       # save how many blank lines we had seen up until now
210       last_num_blank_lines = num_blank_lines
211
212       if len(line) == 0 or re.search("^\s*$", line) != None:
213          num_blank_lines += 1
214       else:
215          num_blank_lines = 0
216
217       while not line_done:
218          line_iterations += 1
219
220          if options['debug'] == True:
221             print "%d: Line = \"%s\"" % (line_number, line)
222
223          if options['debug'] == True:
224             print "in_string = %s" % in_string
225
226          # Check for empty, blank, or comment line
227          if line == None or re.search("^\s*(#.*)?$", line) != None:
228             line_done = True
229             continue
230
231
232          # check for location of triple-quotes on line
233          quotes_match = re.search("(\"\"\"|\"|'''|')(.*)", line)
234
235             
236          if not in_string:
237
238             # Is this the start of this line?
239             if line_iterations == 1:
240
241                # Check the indentation
242                space_count = len(line) - len(line.lstrip())
243                if (space_count % stork_indentation_count) != 0:
244                   error(options, filename, line_number,
245                         "Incorrect leading spaces: %d spaces" % space_count)
246
247                # if function_info is non-empty we're in a function
248                # keep iterating through function_info until we're
249                # dedented out of possible nested functions
250                while (len(function_info) != 0 
251                         and space_count < function_info[0][1]):
252                   # We've DEDENTED out of the current (top) function
253                   if len(function_info[0]) < 4:
254                      warn( options, filename, function_info[0][2],
255                            "Function not documented at all:  " +
256                            function_info[0][0]  )
257                   else:
258                      check_function_doc(options, filename, function_info[0])
259
260                   function_info.pop()
261
262             # Check for function definition
263             fn_match = re.match(" *def (\w*)\(.*\):(.*)$", line )
264
265             if fn_match != None:
266                #### we're starting a new function,
267                # Check num blank_lines previous to this definition
268                if last_num_blank_lines < min_blank_lines_twixt_fns:
269                   warn(options, filename, line_number,
270                         """Insufficient blank lines between definitions:
271                            %d < %d at definition of %s
272                            """ % (last_num_blank_lines,
273                                     min_blank_lines_twixt_fns,
274                                     fn_match.group(1)))
275
276                function_info.insert( 0,
277                                        [ fn_match.group(1),
278                                        (space_count 
279                                           + stork_indentation_count),
280                                        line_number ])
281
282                # in case more stuff on this line, probably should be a
283                # warning as well
284                line = fn_match.group(2)
285                just_started_fn = True
286                
287             else:
288                ## Not a fn_match
289
290                # Check for naughty lambdas
291                lambda_match = \
292                      re.search("[^\w]lambda (?:\w+ *)(?:, *\w+ ?)* *:",
293                                  line)
294                if lambda_match != None:
295                   warn(options, filename, line_number, "lambda in use: %s" %
296                         lambda_match.group(0)[:-1])
297
298                # Check for overzealous generic try/excepts
299                except_match = re.search("[^\w]except\s*:", line)
300                if except_match != None:
301                   warn(options, filename, line_number,
302                         "generic try/except in use" )
303
304
305                # Check to see if we are starting a string
306                if quotes_match != None:
307                   # We started a string on this line
308                   # leave just string contents (poss. with ending 3xquotes)
309
310                   # If there is non-whitespace before the starting quote
311                   # then this is not the doc string
312                   if re.search("\w", line[0:quotes_match.start(1)]) != None:
313                      just_started_fn = False
314                      
315                   in_string = True
316                   found_string = ''
317                   open_quote = quotes_match.group(1)
318                   if options['debug'] == True:
319                      print "Start of string:  on line %d [%s]: [%s]" % \
320                            ( line_number, line, quotes_match.group(1))
321                                                                       
322                   line = quotes_match.group(2) or ''
323                else:
324                   just_started_fn = False
325                      
326                   # Move on to the next line
327                   line_done = True
328                   continue
329          else:
330             # we're in_string
331             
332             if quotes_match != None:
333                # Count how many backquotes precede quote (if any)
334                backquotes=0
335                index_before_quote = quotes_match.start(1)-1
336                while index_before_quote >= 0 \
337                            and line[index_before_quote] == '\\':
338                   backquotes += 1
339                   index_before_quote -= 1
340
341                # If the number of backquotes preceeding the quote is
342                # even, then the quote is NOT escaped, and so it signifies the
343                # end of this glorious string
344                if quotes_match.group(1) == open_quote \
345                      and (backquotes % 2) == 0:
346                   # we got a quote and we're inside a string
347                   # End of the current string
348                   found_string += line[0:quotes_match.start(1)]
349                   in_string = False
350                   open_quote = None
351                else:
352                   # A quote, but not matching the opening quote, add matched
353                   # part to found_string, and continue on rest of line
354                   found_string += line[0:quotes_match.end(1)]
355                   
356                line = quotes_match.group(2)
357                   
358             else:
359                # Just a continuation of multi-line string
360                found_string += line
361                line = ''
362                continue  # next line
363
364          if found_string != None and not in_string:
365             if options['debug'] == True:
366                print "End of string on line %d:  [%s]" % \
367                      (line_number, found_string)
368
369             if len(function_info) > 0:
370                if just_started_fn == True:
371                   # We're inside a function, add doc string
372                   function_info[0].insert(3, found_string)
373                else:
374                   pass
375             else:
376                # top level string
377                if options['module_doc'] == None:
378                   options['module_doc'] = found_string
379                   check_top_level_doc(options,filename,line_number,found_string)
380
381             found_string=None
382
383
384    # End of lines
385    print_warnings_and_errors(filename, options)
386    clear_warnings_and_errors(options)   
387    return True
388
389
390
391
392
393
394 def check_function_doc(options, filename, function_info):
395    """
396    <Purpose>
397       Checks the contents of the function_doc string for reqd. fields
398
399    <Arguments>
400        options:  dict of options to use in checking (see usage fn.)
401       filename:  filename in which function is defined
402       function_info: list of [<fName>, <fIndentation>, <startLineNo>,
403                               <docString>]
404    <Exceptions>
405       None
406
407    <Side Effects>
408       Logs warnings if style defects found
409
410    <Returns>
411       None.
412    """
413    FN_DOC_FIELDS = ( '<Purpose>', '<Arguments>', '<Exceptions>',
414                      '<Side Effects>', '<Returns>' )
415    for field in FN_DOC_FIELDS:
416       if field not in function_info[3]:
417          warn(options, filename, function_info[2],
418                "Function doc string missing field:  %s missing %s\n"
419                % ( function_info[0], field ))
420          
421
422
423
424
425
426 def check_top_level_doc(options, filename, line_number, top_level_doc ):
427    """
428    <Purpose>
429       Checks the contents of the top_level_doc string for reqd. fields
430
431    <Arguments>
432             options:  dict of options to use in checking (see usage fn.)
433            filename:  filename containing this top level doc string
434         line_number:  end line of doc string
435       top_level_doc: the module documentation string
436
437    <Exceptions>
438       None
439
440    <Side Effects>
441       Prints warnings if style defects found
442
443    <Returns>
444       None.
445    """
446    TOP_LEVEL_DOC_FIELDS = ('<Program Name>', '<Started>', '<Author>',
447                            '<Purpose>' )
448    for field in TOP_LEVEL_DOC_FIELDS:
449       if field not in top_level_doc:
450          warn(options, filename,line_number,
451                "Module doc string missing field:  %s\n" % field )
452
453
454
455
456
457 def warn(options, filename, line_number, warning):
458    """
459    <Purpose>
460       Print a warning message.
461
462    <Arguments>
463       filename: filename in which warning occurred
464       line_number:  line number at which warning occured
465       warning:  string containing warning message, note warnings
466                 should be in form
467                 "<collectable warning message>:  <specific part>"
468                 so that the warnings can be collected
469
470    <Exceptions>
471       None
472
473    <Side Effects>
474       Prints warnings to stdout
475
476    <Returns>
477       None.
478    """
479    if options['errors_only'] == False:
480       warn_or_error('warning', options, filename, line_number, warning)
481
482       
483
484
485
486 def error(options, filename, line_number, error):
487    """
488    <Purpose>
489       Print or collect an error message.
490
491    <Arguments>
492           options:  dict of options to use in checking (see usage fn.)
493          filename: filename in which warning occurred
494       line_number:  line number at which warning occured
495             error:  string containing error message
496
497    <Exceptions>
498       None
499
500    <Side Effects>
501       Prints error to stdout, or collects
502
503    <Returns>
504       None.
505    """
506    warn_or_error('error', options, filename, line_number, error)
507
508
509
510
511
512 def warn_or_error( event_type, options, filename, line_number,
513                      message ):
514    """
515    <Purpose>
516       Print a warning or error message depending on warn_or_error_string
517
518    <Arguments>
519        event_type:  either 'warning' or 'error'
520           options:  dict of options to use in checking (see usage fn.)
521          filename:  filename in which event occurred
522       line_number:  line number at which event occured
523           warning:  string containing event message, note event msgs
524                     should be in form
525                     "<collectable event message>:  <specific part>"
526                    so that the events can be collected
527
528    <Exceptions>
529       None
530
531    <Side Effects>
532       Prints events to stdout, or collects.
533
534    <Returns>
535       None.
536    """
537
538    if options['collect']:
539       # We collect a list of the messages in options
540       if message.find( ':' ) != -1:
541          (message_type,message_specific) = message.split(':', 1)
542       else:
543          message_type = message
544          message_specific = ''
545          
546       key   = event_type + options['message_separator'] + message_type
547       tuple = (filename, line_number, message_specific)
548
549       if options.has_key(key) == False:
550          options[key] = [tuple]
551       else:
552          options[key].append(tuple)
553    else:
554       print "%s:%d: %s: %s" % (filename, line_number, event_type, message)
555
556
557
558
559
560 def clear_warnings_and_errors(options):
561    """
562    <Purpose>
563       Clear all warnings and errors from options
564
565    <Arguments>
566           options:  dict of options to use in checking (see usage fn.)
567
568    <Exceptions>
569       None
570
571    <Side Effects>
572       Clears all warnings and errors from options.
573
574    <Returns>
575       None.
576    """
577    for option in options.keys():
578       for keyword in options['message_keywords']:
579          if option.find( keyword + options['message_separator'] ) != -1:
580             del options[option]     # clear that option
581    
582
583
584
585
586 def print_warnings_and_errors(filename, options):
587    """
588    <Purpose>
589       Print all warnings and errors from options
590
591    <Arguments>
592          filename:  filename of file being checked
593           options:  dict of options to use in checking (see usage fn.)
594
595    <Exceptions>
596       None
597
598    <Side Effects>
599       Prints all warnings and errors from options.
600
601    <Returns>
602       None.
603    """
604    for option in options.keys():
605       for keyword in options['message_keywords']:
606          if option.find( keyword + options['message_separator']) != -1:
607             line_numbers_seen = []
608             print "\n\n%s\n" % (option)
609             if options['verbose'] == False:
610                print "  On line numbers:  ",
611             for (filename, line_number, message_specific) in options[option]:
612                if options['verbose'] == True:
613                   print "  line %d: %s" % (line_number, message_specific)
614                else:
615                   if not line_number in line_numbers_seen:
616                      print "%d " % line_number,
617                      line_numbers_seen.append(line_number)
618
619
620
621
622
623 # run on file specified
624 if __name__ == "__main__":
625    try:
626       main(sys.argv[1:])
627    except KeyboardInterrupt:
628       print "Exiting via keyboard interrupt"
629       sys.exit(0)