osmo-ci/scripts/verify_log_statements.py

141 lines
4.1 KiB
Python
Executable File

#!/usr/bin/env python3
__doc__ = '''
With regex magic, try to pinpoint all LOG* macro calls that lack a final newline.
Also find those that have non-printable characters or extra newlines.
Usage:
./verify_log_statements.py [-d|--debug] [dir] [file] [...]
Without args, default to '.'
'''
import re
import sys
import codecs
import os.path
# This regex matches the entire LOGxx(...) statement over multiple lines.
# It pinpoints the format string by looking for the first arg that contains quotes.
# It then matches any number of separate quoted strings, and accepts 0 or more args after that.
log_statement_re = re.compile(r'^[ \t]*LOG[_A-Z]+\(([^";,]*,)*[ \t\r\n]*(("[^"]*"[^";,]*)*)(,[^;]*|)\);',
re.MULTILINE | re.DOTALL)
fmt_re = re.compile(r'("[^"]*".*)*fmt')
osmo_stringify_re = re.compile("OSMO_STRINGIFY[_A-Z]*\([^)]*\)")
debug = ('-d' in sys.argv) or ('--debug' in sys.argv)
args = [x for x in sys.argv[1:] if not (x == '-d' or x == '--debug')]
if not args:
args = ['.']
class error_found:
def __init__(self, f, charpos, msg, text):
self.f = f
self.charpos = charpos
self.msg = msg
self.text = text
self.line = None
def make_line_idx(file_content):
line_idx = []
pos = 0
line_nr = 1
line_idx.append((pos, line_nr))
for line in file_content.split('\n'):
pos += len(line)
line_nr += 1
line_idx.append((pos, line_nr))
pos += 1 # newline char
return line_idx
def char_pos_2_line(line_idx, sorted_char_positions):
r = []
line_i = 0
next_line_i = 1
for char_pos in sorted_char_positions:
while (line_i+1) < len(line_idx) and char_pos > line_idx[line_i+1][0]:
line_i += 1
r.append(line_idx[line_i][1])
return r
def check_file(f):
if not (f.endswith('.h') or f.endswith('.c') or f.endswith('.cpp')):
return []
try:
errors_found = []
file_content = codecs.open(f, "r", "utf-8", errors='ignore').read()
for log in log_statement_re.finditer(file_content):
quoted = log.group(2)
# Skip 'LOG("bla" fmt )' strings that typically appear as #defines.
if fmt_re.match(quoted):
if debug:
errors_found.append(error_found(f, log.start(), 'Skipping define', log.group(0)))
continue
# Drop PRI* parts of 'LOG("bla %"PRIu64" foo")'
for n in (16,32,64):
quoted = quoted.replace('PRIu' + str(n), '')
quoted = quoted.replace('PRId' + str(n), '')
quoted = ''.join(osmo_stringify_re.split(quoted))
# Use py eval to join separate string constants: drop any tabs/newlines
# that are not in quotes, between separate string constants.
try:
quoted = eval('(' + quoted + '\n)' )
except:
# hopefully eval broke because of some '## args' macro def
if debug:
ignored.append(error_found(f, log.start(), 'Ignoring', log.group(0)))
continue
# check for errors...
# final newline
if not quoted.endswith('\n'):
errors_found.append(error_found(f, log.start(), 'Missing final newline', log.group(0)))
# disallowed chars and extra newlines
for c in quoted[:-1]:
if not c.isprintable() and not c == '\t':
if c == '\n':
msg = 'Extraneous newline'
else:
msg = 'Illegal char'
errors_found.append(error_found(f, log.start(), msg + ' %r' % c, log.group(0)))
if not error_found:
return []
line_idx = make_line_idx(file_content)
for r, line in zip(errors_found, char_pos_2_line(line_idx, [rr.charpos for rr in errors_found])):
r.line = line
return errors_found
except:
print("ERROR WHILE PROCESSING %r" % f, file=sys.stderr)
raise
all_errors_found = []
for f in args:
if os.path.isdir(f):
for parent_path, subdirs, files in os.walk(f, None, None):
for ff in files:
all_errors_found.extend(check_file(os.path.join(parent_path, ff)))
else:
all_errors_found.extend(check_file(f))
def print_errors(errs):
for err in errs:
print('%s: %s:%d\n%s\n' % (err.msg, err.f, err.line or 0, err.text))
print_errors(all_errors_found)
sys.exit(len(all_errors_found))
# vim: tabstop=2 shiftwidth=2 expandtab