import re, os, sys valid_extensions = ['c', 'cpp', 'h', 'hpp', 'mm', 'lua', 'rb', 'proto', 'init', 'init-example', 'rst'] path_blacklist = [ '^library/include/df/', '^plugins/stonesense/allegro', '^plugins/isoworld/allegro', '^plugins/isoworld/agui', '^depends/', '^.git/', '^build', '.pb.h', ] def valid_file(filename): return len(list(filter(lambda ext: filename.endswith('.' + ext), valid_extensions))) and \ not len(list(filter(lambda path: path.replace('\\', '/') in filename.replace('\\', '/'), path_blacklist))) success = True def error(msg): global success success = False sys.stderr.write(msg + '\n') class LinterError(Exception): pass class Linter(object): ignore = False def check(self, lines): failures = [] for i, line in enumerate(lines): if not self.check_line(line): failures.append(i + 1) if len(failures): raise LinterError('%s: %s' % (self.msg, self.display_lines(failures, len(lines)))) def fix(self, lines): for i in range(len(lines)): lines[i] = self.fix_line(lines[i]) def display_lines(self, lines, total): if len(lines) == total - 1: return 'entire file' if not len(lines): # should never happen return 'nowhere' if len(lines) == 1: return 'line %i' % lines[0] s = 'lines ' range_start = range_end = lines[0] for i, line in enumerate(lines): if line > range_end + 1: if range_start == range_end: s += ('%i, ' % range_end) else: s += ('%i-%i, ' % (range_start, range_end)) range_start = range_end = line if i == len(lines) - 1: s += ('%i' % line) else: range_end = line if i == len(lines) - 1: s += ('%i-%i, ' % (range_start, range_end)) return s.rstrip(' ').rstrip(',') class NewlineLinter(Linter): msg = 'Contains DOS-style newlines' # git supports newline conversion. Catch in CI, ignore on Windows. ignore = os.linesep != '\n' and not os.environ.get('TRAVIS') def check_line(self, line): return '\r' not in line def fix_line(self, line): return line.replace('\r', '') class TrailingWhitespaceLinter(Linter): msg = 'Contains trailing whitespace' def check_line(self, line): line = line.replace('\r', '').replace('\n', '') return not line.strip() or line == line.rstrip('\t ') def fix_line(self, line): return line.rstrip('\t ') class TabLinter(Linter): msg = 'Contains tabs' def check_line(self, line): return '\t' not in line def fix_line(self, line): return line.replace('\t', ' ') linters = [cls() for cls in Linter.__subclasses__() if not cls.ignore] def main(): root_path = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else '.') if not os.path.exists(root_path): print('Nonexistent path: %s' % root_path) sys.exit(2) fix = (len(sys.argv) > 2 and sys.argv[2] == '--fix') global path_blacklist path_blacklist = list(map(lambda s: os.path.join(root_path, s.replace('^', '')) if s.startswith('^') else s, path_blacklist)) for cur, dirnames, filenames in os.walk(root_path): for filename in filenames: full_path = os.path.join(cur, filename) rel_path = full_path.replace(root_path, '.') if not valid_file(full_path): continue lines = [] with open(full_path, 'rb') as f: lines = f.read().split(b'\n') for i, line in enumerate(lines): try: lines[i] = line.decode('utf-8') except UnicodeDecodeError: error('%s:%i: Invalid UTF-8 (other errors will be ignored)' % (rel_path, i + 1)) lines[i] = '' for linter in linters: try: linter.check(lines) except LinterError as e: error('%s: %s' % (rel_path, e)) if fix: linter.fix(lines) contents = '\n'.join(lines) with open(full_path, 'wb') as f: f.write(contents) if success: print('All linters completed successfully') sys.exit(0) else: sys.exit(1) if __name__ == '__main__': main()