From: Michael Tremer Date: Sat, 12 Jul 2008 20:50:06 +0000 (+0000) Subject: Introduced a new script that checks the code (-quality). X-Git-Tag: v3.0-alpha1~871 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=602bc6faec973cc2caf8c7eaa0785acd96aa67f9;p=ipfire-3.x.git Introduced a new script that checks the code (-quality). Will need some testing. --- diff --git a/tools/code-beautify b/tools/code-beautify new file mode 100644 index 000000000..a9c3a9284 --- /dev/null +++ b/tools/code-beautify @@ -0,0 +1,338 @@ +#! /usr/bin/python + +"""code-saner [-d][-r][-v] [ path ... ] + +-d (--dryrun) Dry run. Analyze, but don't make any changes to, files. +-r (--recurse) Recurse. Search for all .py files in subdirectories too. +-b (--backup) Keep backups of files. +-v (--verbose) Verbose. Print informative msgs; else no output. +-h (--help) Help. Print this usage information and exit. +""" + +__version__ = "1" + +import tokenize +import os +import sys + +verbose = 0 +recurse = 0 +dryrun = 0 +backup = 0 + +def usage(msg=None): + if msg is not None: + print >> sys.stderr, msg + print >> sys.stderr, __doc__ + +def errprint(*args): + sep = "" + for arg in args: + sys.stderr.write(sep + str(arg)) + sep = " " + sys.stderr.write("\n") + +def main(): + import getopt + global verbose, recurse, dryrun, backup + try: + opts, args = getopt.getopt(sys.argv[1:], "drbvh", + ["dryrun", "recurse", "backup" "verbose", "help"]) + except getopt.error, msg: + usage(msg) + return + for o, a in opts: + if o in ('-d', '--dryrun'): + dryrun += 1 + elif o in ('-r', '--recurse'): + recurse += 1 + elif o in ('-b', '--backup'): + backup += 1 + elif o in ('-v', '--verbose'): + verbose += 1 + elif o in ('-h', '--help'): + usage() + return + if not args: + r = Reindenter(sys.stdin) + r.run() + r.write(sys.stdout) + return + for arg in args: + check(arg) + +endings = { + 'images' : (".png", ".gif", ".jpg", ".jpeg",), + 'styles' : (".css",), + 'config' : (".conf", ".cnf", ".cf", ".cfg", ".config",), + 'python' : (".py",), + 'script' : (".pl", ".c", ".h", ".sh", ".txt",), +} + +def FileObject(file): + object = None + try: + f = open(file) + except IOError, msg: + errprint("%s: I/O Error: %s" % (file, str(msg))) + raise + + # Python + for ending in endings['python']: + if file.endswith(ending): + object = PythonFile(f=f, file=file) + + # Configs + for ending in endings['config']: + if file.endswith(ending): + object = ConfigFile(f=f, file=file) + + # Perl + for ending in endings['script']: + if file.endswith(ending): + object = ScriptFile(f=f, file=file) + + # Styles + for ending in endings['styles']: + if file.endswith(ending): + object = StyleFile(f=f, file=file) + + # Images + for ending in endings['images']: + if file.endswith(ending): + object = ImageFile(f=f, file=file) + + f.close() + return object or UnknownFile(f=f, file=file) + +def check(file): + if os.path.isdir(file) and not os.path.islink(file): + if verbose: + print "listing directory", file + names = os.listdir(file) + for name in names: + fullname = os.path.join(file, name) + if ((recurse and not os.path.islink(fullname))): + check(fullname) + return + + if verbose: + print "checking", file, "...", + try: + r = FileObject(file) + except IOError: + return + + if r.run(): + if verbose: + print "changed." + if dryrun: + print "But this is a dry run, so leaving it alone." + if not dryrun: + if backup: + bak = file + ".bak" + if os.path.exists(bak): + os.remove(bak) + os.rename(file, bak) + if verbose: + print "renamed", file, "to", bak + f = open(file, "w") + r.write(f) + f.close() + if verbose: + print "wrote new", file + else: + if verbose: + print "unchanged." + +def _rstrip(line, junk='\n \t'): + """Return line stripped of trailing spaces, tabs, newlines. + + Note that line.rstrip() instead also strips sundry control characters, + but at least one known Emacs user expects to keep junk like that, not + mentioning Barry by name or anything . + """ + + i = len(line) + while i > 0 and line[i-1] in junk: + i -= 1 + return line[:i] + +# Count number of leading blanks. +def getlspace(line): + i, n = 0, len(line) + while i < n and line[i] == " ": + i += 1 + return i + +class DefaultFile: + def __init__(self, f, file=None): + self.file = file + + def run(self): + pass + #if verbose: + # errprint("Can't guess filetype of given file %s" % self.file) + + def rm_trailing_lines(self, lines): + # Remove trailing empty lines. + while lines and lines[-1] == "\n": + lines.pop() + return lines + + def expand_tabs(self, lines): + return [_rstrip(line).expandtabs() + "\n" for line in lines] + +class TextFile(DefaultFile): + def __init__(self, f, file=None): + self.file = file # Save filename. + self.raw = f.readlines() # Raw file lines. + + # File lines, rstripped. Dummy at start is so + # that we can use tokenize's 1-based line numbering easily. + # Note that a line is all-blank iff it's "\n". + self.lines = [_rstrip(line) + "\n" for line in self.raw] + + def run(self): + self.after = self.lines + return self.raw != self.after + + def write(self, f): + f.writelines(self.after) + +class PythonFile(TextFile): + def __init__(self, f, file=None): + TextFile.__init__(self, f, file) + self.find_stmt = 1 # next token begins a fresh stmt? + self.level = 0 # current indent level + + self.lines = self.expand_tabs(self.lines) + self.lines.insert(0, None) + self.index = 1 # index into self.lines of next line + + # List of (lineno, indentlevel) pairs, one for each stmt and + # comment line. indentlevel is -1 for comment lines, as a + # signal that tokenize doesn't know what to do about them; + # indeed, they're our headache! + self.stats = [] + + def run(self): + tokenize.tokenize(self.getline, self.tokeneater) + # Remove trailing empty lines. + lines = self.rm_trailing_lines(self.lines) + # Sentinel. + stats = self.stats + stats.append((len(lines), 0)) + # Map count of leading spaces to # we want. + have2want = {} + # Program after transformation. + after = self.after = [] + # Copy over initial empty lines -- there's nothing to do until + # we see a line with *something* on it. + i = stats[0][0] + after.extend(lines[1:i]) + for i in range(len(stats)-1): + thisstmt, thislevel = stats[i] + nextstmt = stats[i+1][0] + have = getlspace(lines[thisstmt]) + want = thislevel * 4 + if want < 0: + # A comment line. + if have: + # An indented comment line. If we saw the same + # indentation before, reuse what it most recently + # mapped to. + want = have2want.get(have, -1) + if want < 0: + # Then it probably belongs to the next real stmt. + for j in xrange(i+1, len(stats)-1): + jline, jlevel = stats[j] + if jlevel >= 0: + if have == getlspace(lines[jline]): + want = jlevel * 4 + break + if want < 0: # Maybe it's a hanging + # comment like this one, + # in which case we should shift it like its base + # line got shifted. + for j in xrange(i-1, -1, -1): + jline, jlevel = stats[j] + if jlevel >= 0: + want = have + getlspace(after[jline-1]) - \ + getlspace(lines[jline]) + break + if want < 0: + # Still no luck -- leave it alone. + want = have + else: + want = 0 + assert want >= 0 + have2want[have] = want + diff = want - have + if diff == 0 or have == 0: + after.extend(lines[thisstmt:nextstmt]) + else: + for line in lines[thisstmt:nextstmt]: + if diff > 0: + if line == "\n": + after.append(line) + else: + after.append(" " * diff + line) + else: + remove = min(getlspace(line), -diff) + after.append(line[remove:]) + return self.raw != self.after + + # Line-getter for tokenize. + def getline(self): + if self.index >= len(self.lines): + line = "" + else: + line = self.lines[self.index] + self.index += 1 + return line + + # Line-eater for tokenize. + def tokeneater(self, type, token, (sline, scol), end, line, + INDENT=tokenize.INDENT, + DEDENT=tokenize.DEDENT, + NEWLINE=tokenize.NEWLINE, + COMMENT=tokenize.COMMENT, + NL=tokenize.NL): + + if type == NEWLINE: + # A program statement, or ENDMARKER, will eventually follow, + # after some (possibly empty) run of tokens of the form + # (NL | COMMENT)* (INDENT | DEDENT+)? + self.find_stmt = 1 + + elif type == INDENT: + self.find_stmt = 1 + self.level += 1 + + elif type == DEDENT: + self.find_stmt = 1 + self.level -= 1 + + elif type == COMMENT: + if self.find_stmt: + self.stats.append((sline, -1)) + # but we're still looking for a new stmt, so leave + # find_stmt alone + + elif type == NL: + pass + + elif self.find_stmt: + # This is the first "real token" following a NEWLINE, so it + # must be the first token of the next program statement, or an + # ENDMARKER. + self.find_stmt = 0 + if line: # not endmarker + self.stats.append((sline, self.level)) + +UnknownFile = ImageFile = DefaultFile # just the empty file object, do nothing +ConfigFile = ScriptFile = StyleFile = TextFile # simple textfiles + +if __name__ == '__main__': + main() diff --git a/tools/make-check b/tools/make-check index 8d9ed2195..d85f0b69f 100644 --- a/tools/make-check +++ b/tools/make-check @@ -140,11 +140,41 @@ check_rootfiles() { } +check_code() { + ACTION=$1 + ARGS="--recurse" + + echo -n " Running code checks" + + case "$ACTION" in + --fix) + echo -n " and fix" + ;; + "") + ARGS+=" --dryrun" + ;; + *) + exiterror "This is not a valid option: $ACTION" + ;; + esac + + echo -n "..." # End the line + + python $BASEDIR/tools/code-beautify $ARGS $BASEDIR/{config,doc,lfs,src,tools} + if [ "$?" -eq "0" ]; then + beautify message DONE + else + beautify message FAIL + exit 1 + fi +} + check_sanity() { echo "Checking sanity of working directory..." check_rootfiles $* + check_code $* } diff --git a/tools/make-git b/tools/make-git index 443d37643..aa67fb7eb 100644 --- a/tools/make-git +++ b/tools/make-git @@ -109,7 +109,7 @@ git_commit() { } git_diff() { - check_sanity + check_sanity --fix echo -ne "Make a local diff to last revision" git diff HEAD > ipfire-diff-$(date +'%Y-%m-%d-%H:%M').diff diff --git a/tools/make-interactive b/tools/make-interactive index be3d624de..8bd16aa09 100644 --- a/tools/make-interactive +++ b/tools/make-interactive @@ -27,6 +27,8 @@ case "$1" in build) # check for prerequisites check_build + # check for sanity of code + check_sanity # compile the distro right now build # beeps when finished