diff --git a/tools/check-style b/tools/check-style new file mode 100755 index 00000000..fc752fce --- /dev/null +++ b/tools/check-style @@ -0,0 +1,161 @@ +#! /usr/bin/env python + +# Copyright (C) 2010 Christian Dywan +# Copyright (C) 2010 Arno Renevier +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# See the file COPYING for the full license text. +# +# check-style: Verify C source code according to coding style. + +import glob, re, string, subprocess, sys, os + +if len (sys.argv) < 2: + name = os.path.basename (sys.argv[0]) + print ('Usage:\n ' + name + ' FILENAMES\n' + ' Pass "-" to read stdin, eg. "cat my-feature.diff | ' + name + ' -"\n' + ' Pass "." to mean "git diff ^HEAD | ' + name + ' -"') + sys.exit (1) + +# Coding style violations +violations = [ + ['.{101}', 'Line longer than 100 columns'], + ['^[ ]{1,3}[^ ]+', 'Indentation is less than 4 spaces'], + # FIXME: Don't match empty strings + # FIXME: Don't match indented function arguments + # ['^(?!(([ ]{4})*[^ ]+))', 'Indentation is not 4 spaces'], + ['.*[ ]+$', 'Trailing whitespace'], + [r"\t+", 'Tabs instead of spaces'], + ["[^0-9],[^ ][^0-9]", 'No space after comma'], + # ['(([A-Z][a-z]+)+)\*?[ ][a-z]{2,}', 'Good variable name'], + # ['(g?char|g(boolean|pointer))\*?[ ][a-z]{2,}', 'Good variable name'], + # ['(g?int|guint)[ ][a-z]+', 'Good iterator name'], + # ['(struct)[ ]+[_]?([A-Z][a-z]+)+', 'Good type name'], + ['^\s*\w+(?|=]{1,2}\w)", 'No space around operators'], + ["\/\*[^ *\n]", 'No space after open comment'], + ['[^ *]\*\/', 'No space before close comment'], + ['\)\{', 'No space between ) and {'], + [';[^ \s]', 'No space or newline after semicolon'], + # ['(if)( \([^ ].*[^ ]\))$', 'Good if style'], + ['^#\s+(if(n?def)?|define|else|elif)[ ].*$', 'Space between # and cpp'], + [r'^\s*\*\w+(\+\+|--);', 'Invalid increment, use (*i)++ or *i += 1'], + ['asctime|ctime|getgrgid|getprgnam|getlogin \ + |getpwnam|getpwuid|gmtime|localtime \ + |rand|readdir|strtok|ttyname', 'Not thread-safe posix, use _r variant'], +] +# No validation for strings, comments, includes +omissions = [ + [r'["]{1}.*["]', 'STRING'], + ["'\\\?.'", 'CHAR'], + ["^\s*\/\*.*\*\/\s*$", 'COMMENT'], + ['#include <.*>', 'INCLUDE'], +] + +# Output format +fmt = '%s - %d: %s' + +# Pre-compile expressions +for violation in violations: + violation[0] = re.compile (violation[0]) +for omission in omissions: + omission[0] = re.compile (omission[0]) + +for filename_or_glob in sys.argv[1:]: + if filename_or_glob == '-': + handles = [sys.stdin] + else: + handles = [] + for filename in glob.glob (filename_or_glob): + if os.path.isdir (filename): + gitdiff = subprocess.Popen (['git', 'diff', '^HEAD', + '--relative', filename], stdout=subprocess.PIPE) + handles.append (gitdiff.stdout) + else: + handles.append (open (filename)) + if not handles: + print (filename_or_glob + ' not found') + sys.exit (1) + + for handle in handles: + previous = '' + i = 0 + mode = '' + filename = handle.name + comment = False + curly = [] + + for line in handle: + line = line[:-1] + i += 1 + + # Parse diff, only validate modified lines + if i == 1 and 'diff' in line: + mode = 'diff' + if mode == 'diff': + if line[:3] == '+++': + filename = line[6:] + comment = False + curly = [] + continue + if line[:2] == '@@': + i = int (line.split (' ')[2].split (',')[0][1:]) - 1 + curly = [] + if line[0] == '-': + i = i -1 + if line[0] != '+': + continue + line = line[1:] + + # Spurious blank lines + if previous == line == '': + print (fmt % (filename, i, 'Spurious blank line')) + previous = line + continue + previous = line + + # Skip multi-line comment blocks + if '/*' in line and not '*/' in line: + comment = True + if comment: + if '*/' in line and not '/*' in line: + comment = False + continue + + cleaned = line + for omission in omissions: + cleaned = omission[0].sub (omission[1], cleaned) + + # Validate curly bracket indentation + if '{' in cleaned and not '}' in cleaned: + curly.append ((cleaned.index ('{'), cleaned)) + if '}' in cleaned and not '{' in cleaned and not '},' in cleaned: + if len (curly) == 0 or curly[-1][0] != cleaned.index ('}'): + print (fmt % (filename, i, 'Misindented curly bracket')) + print (curly[-1][1]) + print (line) + curly.pop() + continue + curly.pop() + + # Validate preprocessor indentation + # FIXME: Don't warn if the *following* line is a curly + cpp = cleaned.find ('#if') + if cpp != -1: + if len (curly) != 0 and cpp != curly[-1][0] + 4: + print (fmt % (filename, i, 'Misindented preprocessor #if')) + print (curly[-1][1]) + print (line) + + violated = False + for violation in violations: + if violation[0].search (cleaned): + violated = True + print (fmt % (filename, i, violation[1])) + if violated: + print (line) +