#!/usr/bin/env python # fixcc -- indent and space lily's c++ code # This file is part of LilyPond, the GNU music typesetter. # # Copyright (C) 2005--2020 Jan Nieuwenhuizen # # LilyPond is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # LilyPond is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with LilyPond. If not, see . # Performs string substitution on files, then applies astyle # (http://astyle.sourceforge.net) # TODO # Remove prefiltering as the equivalent formatting becomes available in # astyle, or as the prefiltering is deemed un-necessary. # Soon, this script might be replaced by a simple invocation of astyle import __main__ import getopt import os import re import sys import time import subprocess COMMENT = 'COMMENT' STRING = 'STRING' GLOBAL_CXX = 'GC++' CXX = 'C++' verbose_p = 0 indent_p = 1 PREFERRED_ASTYLE_VERSION = "Artistic Style Version 3.1" rules = { GLOBAL_CXX: [ # delete trailing whitespace ('[ \t]*\n', '\n'), ], CXX: [ # space before parenthesis open; astyle -xd does this except for foo(). (r'([\w\)\]])\(', '\\1 ('), # delete inline double spaces (r'(\S) +', '\\1 '), # delete space before parenthesis close (r' *\)', ')'), # delete spaces after prefix (r'(--|\+\+) *([\w\(])', '\\1\\2'), # delete spaces before postfix (r'([\w\)\]]) *(--|\+\+)', '\\1\\2'), # delete space around operator (r'([\w\(\)\]]) *(\.|->) *([\w\(\)])', '\\1\\2\\3'), # delete space after operator (r'(::) *([\w\(\)])', '\\1\\2'), # delete superflous space around operator (r'([\w\(\)\]]) +(&&|\|\||<=|>=|!=|\|=|==|\+=|-=|\*=|/=|\?|<|>|\+|-|=|/|:|&|\||\*) +([\w\(\)])', '\\1 \\2 \\3'), # trailing operator, but don't un-trail close angle-braces > nor pointer *, and not before a preprocessor line (r'(?=|!=|\|=|==|\+=|-=|\*=|/=|\?|<|\+|-|=|/|:|&XXX|\||\*XXX) *\n( *)([^\s#])', '\n\\2\\1 \\3'), # space after `operator' (r'(\Woperator) *([^\w\s])', '\\1 \\2'), # trailing parenthesis open (r'\( *\n *', '('), # dangling parenthesis close: Disabled to leave ADD_TRANSLATOR format in place #('\n *\)', ')'), # dangling comma ('\n( *),', ',\n\\1'), # delete space after case, label (r'(\W(case|label) [\w]+) :', '\\1:'), # delete space before comma (' +,', ','), # delete space before semicolon ('([^;]) +;', '\\1;'), # dangling newline ('\n\n+', '\n\n'), # delete backslash before empty line (emacs' indent region is broken) ('\\\\\n\n', '\n\n'), ], COMMENT: [ # delete empty first lines (r'(/\*\n)\n*', '\\1'), # delete empty last lines (r'\n*(\n\*/)', '\\1'), # delete newline after start? #('/(\*)\n', '\\1'), # delete newline before end? #('\n(\*/)', '\\1'), ], } # Recognize special sequences in the input. # # (?Pregex) -- Assign result of REGEX to NAME. # *? -- Match non-greedily. # (?m) -- Multiline regex: Make ^ and $ match at each line. # (?s) -- Make the dot match all characters including newline. # (?x) -- Ignore whitespace in patterns. no_match = 'a\ba' snippet_res = { CXX: { 'define': r'''(?x) (?P (?P \#[ \t]*define[ \t]+([^\n]*\\\n)*[^\n]*))''', 'multiline_comment': r'''(?sx) (?P (?P [ \t]*/\*.*?\*/))''', 'singleline_comment': r'''(?mx) ^.*? # leave leading spaces for the comment snippet (?P (?P [ \t]*//[^\n]*\n))''', 'string': r'''(?x) " # leave the leading " character visible to CXX rules (?P (?P ([^"\n]|\\")*"))''', 'char': r'''(?x) (?P (?P '([^']+|\')))''', 'include': r'''(?x) (?P (?P \#[ \t]*include[ \t]*<[^>]*>))''', }, } class Chunk: def replacement_text(self): return '' def filter_text(self): return self.replacement_text() class Substring (Chunk): def __init__(self, source, start, end): self.source = source self.start = start self.end = end def replacement_text(self): s = self.source[self.start:self.end] if verbose_p: sys.stderr.write('CXX Rules') for i in rules[CXX]: if verbose_p: sys.stderr.write('.') #sys.stderr.write ('\n\n***********\n') #sys.stderr.write (i[0]) #sys.stderr.write ('\n***********\n') #sys.stderr.write ('\n=========>>\n') #sys.stderr.write (s) #sys.stderr.write ('\n<<=========\n') s = re.sub(i[0], i[1], s) if verbose_p: sys.stderr.write('done\n') return s class Snippet (Chunk): def __init__(self, type, match, format): self.type = type self.match = match self.hash = 0 self.options = [] self.format = format def replacement_text(self): return self.match.group('match') def substring(self, s): return self.match.group(s) def __repr__(self): return repr(self.__class__) + ' type = ' + self.type class Multiline_comment (Snippet): def __init__(self, source, match, format): self.type = type self.match = match self.hash = 0 self.options = [] self.format = format def replacement_text(self): s = self.match.group('match') if verbose_p: sys.stderr.write('COMMENT Rules') for i in rules[COMMENT]: if verbose_p: sys.stderr.write('.') s = re.sub(i[0], i[1], s) return s snippet_type_to_class = { 'multiline_comment': Multiline_comment, # 'string': Multiline_comment, # 'include': Include_snippet, } def find_toplevel_snippets(s, types): if verbose_p: sys.stderr.write('Dissecting') res = {} for i in types: res[i] = re.compile(snippet_res[format][i]) snippets = [] index = 0 # found = dict (map (lambda x: (x, None), # types)) # urg python2.1 found = {} list(map(lambda x, f=found: f.setdefault(x, None), types)) # We want to search for multiple regexes, without searching # the string multiple times for one regex. # Hence, we use earlier results to limit the string portion # where we search. # Since every part of the string is traversed at most once for # every type of snippet, this is linear. while True: if verbose_p: sys.stderr.write('.') first = None endex = 1 << 30 for type in types: if not found[type] or found[type][0] < index: found[type] = None m = res[type].search(s[index:endex]) if not m: continue cl = Snippet if type in snippet_type_to_class: cl = snippet_type_to_class[type] snip = cl(type, m, format) start = index + m.start('match') found[type] = (start, snip) if found[type] \ and (not first or found[type][0] < found[first][0]): first = type # FIXME. # Limiting the search space is a cute # idea, but this *requires* to search # for possible containing blocks # first, at least as long as we do not # search for the start of blocks, but # always/directly for the entire # @block ... @end block. endex = found[first][0] if not first: snippets.append(Substring(s, index, len(s))) break (start, snip) = found[first] snippets.append(Substring(s, index, start)) snippets.append(snip) found[first] = None index = start + len(snip.match.group('match')) return snippets def nitpick_file(outdir, file): s = open(file, encoding='utf8').read() t = s.expandtabs(8) for i in rules[GLOBAL_CXX]: t = re.sub(i[0], i[1], t) # FIXME: Containing blocks must be first, see # find_toplevel_snippets. # We leave simple strings be part of the code snippet_types = ( 'define', 'multiline_comment', 'singleline_comment', 'string', # 'char', 'include', ) chunks = find_toplevel_snippets(t, snippet_types) # code = filter (lambda x: is_derived_class (x.__class__, Substring), # chunks) t = ''.join([x.filter_text() for x in chunks]) fixt = file if s != t: if not outdir: os.system('mv %s %s~' % (file, file)) else: fixt = os.path.join(outdir, os.path.basename(file)) h = open(fixt, "w", encoding="utf8") h.write(t) h.close() if s != t or indent_p: indent_file(fixt) def indent_file(file): # Astyle aborts on unrecognized options, # so wait until everyone has 2.04 before considering: # --attach-namespaces --indent-namespaces \ # --max-code-length=80 --pad-first-paren-out \ astyle = '''astyle\ --options=none --quiet -n \ --style=gnu --indent=spaces=2 \ --max-instatement-indent=60 \ --indent-cases \ --align-pointer=name --pad-oper \ --keep-one-line-blocks \ %(file)s ''' % vars() if verbose_p: sys.stderr.write(astyle) sys.stderr.write('\n') os.system(astyle) def usage(): sys.stdout.write(r''' Usage: fixcc [OPTION]... FILE... Options: --help --lazy skip astyle, if no changes --sloppy accept any astyle version --verbose --test Typical use with LilyPond: scripts/auxiliar/fixcc.py $(git ls-files '*.cc' '*.hh') ''') def do_options(): global indent_p, outdir, verbose_p, PREFERRED_ASTYLE_VERSION (options, files) = getopt.getopt(sys.argv[1:], '', ['help', 'lazy', 'outdir=', 'sloppy', 'test', 'verbose']) for (o, a) in options: if o == '--help': usage() sys.exit(0) elif o == '--lazy': indent_p = 0 elif o == '--outdir': outdir = a elif o == '--sloppy': PREFERRED_ASTYLE_VERSION = "Artistic Style" elif o == '--verbose': verbose_p = 1 elif o == '--test': test() sys.exit(0) else: assert unimplemented if not files: usage() sys.exit(2) return files def check_astyle_version(): cmd = "astyle --version" process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = process.communicate() return (PREFERRED_ASTYLE_VERSION in stderr.decode()) \ or (PREFERRED_ASTYLE_VERSION in stdout.decode()) outdir = 0 format = CXX socketdir = '/tmp/fixcc' socketname = 'fixcc%d' % os.getpid() def main(): files = do_options() if not check_astyle_version(): print("Warning: try to use %s." % PREFERRED_ASTYLE_VERSION) print("Please limit use of this version to files with changed code.") if len(files) > 4: print("Too many files with this version. See `astyle --help`") sys.exit(1) if outdir and not os.path.isdir(outdir): os.makedirs(outdir) for i in files: sys.stderr.write('%s...\n' % i) nitpick_file(outdir, i) # TODO: make this compilable and check with g++ TEST = ''' #include #include class ostream ; class Foo { public: static char* foo (); std::map* bar (char, char) { return 0; } }; typedef struct { Foo **bar; } String; ostream & operator << (ostream & os, String d); typedef struct _t_ligature { char *succ, *lig; struct _t_ligature * next; } AFM_Ligature; typedef std::map < AFM_Ligature const *, int > Bar; /** Copyright (C) 1997--2020 Han-Wen Nienhuys */ /* || * vv * !OK OK */ /* || vv !OK OK */ char * Foo:: foo () { int i ; char* a= &++ i ; a [*++ a] = (char*) foe (*i, &bar) * 2; int operator double (); std::map y =*bar(-*a ,*b); Interval_t & operator*= (T r); Foo*c; int compare (Pqueue_ent < K, T > const& e1, Pqueue_ent < K,T> *e2); delete *p; if (abs (f)*2 > abs (d) *FUDGE) ; while (0); for (; ibar); for (; *p && > y; foo > bar) ; do { ;;; } while (foe); squiggle. extent; 1 && * Moment::unsmob (lf); line_spanner_ = make_spanner ("DynamicLineSpanner", rq ? rq->*self_scm (): SCM_EOL); case foo: k; if (0) {a=b;} else { c=d; } cookie_io_functions_t Memory_out_stream::functions_ = { Memory_out_stream::reader, ... }; int compare (Array < Pitch> *, Array < Pitch> *); original_ = (Grob *) & s; Drul_array< Link_array > o; } header_.char_info_pos = (6 + header_length) * 4; return ly_bool2scm (*ma < * mb); 1 *::sign(2); (shift) *-d; a = 0 ? *x : *y; a = "foo() 2,2,4"; { if (!span_) { span_ = make_spanner ("StaffSymbol", SCM_EOL); } } { if (!span_) { span_ = make_spanner (StaffSymbol, SCM_EOL); } } ''' def test(): test_file = 'fixcc.cc' open(test_file, 'w', encoding='utf8').write(TEST) nitpick_file(outdir, test_file) sys.stdout.write(open(test_file, encoding='utf8').read()) if __name__ == '__main__': main()