1#!/usr/bin/perl
2use strict;
3use warnings FATAL => 'all';
4use FindBin();
5use lib $FindBin::Bin;
6use FileUtils();
7
8#############################################################################
9# Globals
10#############################################################################
11
12#! Help sections
13my $HELP = {
14    NAME => <<END,
15    $FindBin::Script - Reindent C++ source code
16END
17
18    DESCRIPTION => <<END,
19    $FindBin::Script searches C and C++ files in the specified file arguments
20    and reindents the source code using the code indenter "uncrustify".
21
22    By default the result of file F is dumped to file F.indent unless options
23    --replace or --no-backup are specified.
24
25    NOTE: Only files with extension (c, h, cpp, hpp, cxx, hxx, C, H) are
26    processed.
27END
28};
29
30#! Expected version of uncrustify
31my $UNCRUSTIFY_VERSION = '0.60';
32
33#! Option values
34my %OPTIONS;
35
36#! Debug flag
37my $DEBUG;
38
39#############################################################################
40# Main
41#############################################################################
42
43FileUtils::update_files(
44    id => 'indent',
45    option_values => \%OPTIONS,
46    init => \&init,
47    file_handler => \&process_file,
48    help => $HELP,
49);
50
51#############################################################################
52
53#!
54# \brief Initialization function
55# \details This called just after parsing command line
56#
57sub init {
58    $DEBUG = $OPTIONS{debug};
59
60    # Verify that uncrustify is available and has the right version
61
62    my $version = qx{uncrustify --version};
63    chomp($version);
64
65    if( not $version ) {
66        die <<END;
67Error: $FindBin::Script requires uncrustify version $UNCRUSTIFY_VERSION
68END
69    }
70
71    if( $version !~ /\s+$UNCRUSTIFY_VERSION$/o ) {
72        die <<END;
73Error: $FindBin::Script requires uncrustify version $UNCRUSTIFY_VERSION
74Found $version which is not suitable.
75END
76    }
77
78    return;
79}
80
81#!
82# \brief Run uncrustify on a file
83# \param[in] file path to the input file
84# \return the text indented by uncrustify
85#
86sub process_file {
87    my($file) = @_;
88
89    my @command = (
90        'uncrustify',
91        '-q',
92        '-c', "$FindBin::Bin/uncrustify.cfg",
93        #'-s', '-p', 'uncrustify.log',
94        '-l', 'CPP',
95    );
96
97    if( defined($file) ) {
98        push(@command, '-f', $file);
99    }
100
101    # Run uncrustify
102    # Get the the result from the uncrustify output
103
104    if( $DEBUG ) {
105        print "DEBUG: executing command: @command\n";
106    }
107
108    my $text = qx{@command};
109    if( $? != 0 ) {
110        print "Error: uncrustify failed to process ",
111            defined($file) ? $file : "standard input",
112            " (return code $?)\n";
113        return;
114    }
115
116    $text = fix_indent($text);
117    return $text;
118}
119
120#!
121# \brief Fix indentation in a given text
122# \param[in] input the input text
123# \return the output text
124# \details This functions fixes indentation problems left by uncrustify:
125#
126# 1) Indentation of class headers
127#
128# Uncrustify has been purposely configured to reformat the constructor
129# initializer list by breaking after the first ':' and each ',':
130#
131# \code
132# Class::Class() :
133#     init1_(...),
134#     init2_(...)
135# {
136# }
137# \endcode
138#
139#
140# Unfortunately this also impacts class headers formatting as follows:
141# \code
142# class A :
143#     public B,
144#     public C
145# {
146# \endcode
147#
148# We want to reformat them as follows:
149#
150# \code
151# class A : public B, public C {
152# \endcode
153#
154#
155# 2) Unexpected indentation problems left by uncrustify:
156#
157# - closing parens are not indented properly: closing chars are indented
158# one level too far and the contents of the parens is not satisfactory
159# - extra spaces between a switch case and the case value are not removed
160# - empty for(;;;) statements have a space before the closing paren.
161#
162#
163sub fix_indent {
164    my($input) = @_;
165
166    # Paren nesting level
167    my $paren_level = 0;
168
169    # Pointer to the char after the last newline
170    my $last_newline = '';
171
172    # Indentation of the last opening paren
173    my $paren_indent = '';
174
175    # Last match
176    my $match;
177
178    # Output text
179    my $output = '';
180
181    while( $input =~ /(?:
182        # Skip preprocessor directive
183        \#(?:\\\n|\/\*.*?\*\/|\N)*
184
185        # Skip C++ comment
186        |\/\/\N*(?:\n\s*\/\/\N*)*
187
188        # Skip C comment
189        |\/\*.*?\*\/
190
191        # Skip string constant
192        |"(?:\\.|\N)*?"
193
194        # Skip character constants
195        |'(?:\\.|[^\\'])+'
196
197        # Opening paren
198        |(?<open_paren>\()
199
200        # Closing paren with leading indent
201        |(?<leading_close_paren>\n\h+\))
202
203        # Closing paren
204        |(?<close_paren>\h*\))
205
206        # Leading class header
207        |(?<class_head>\n\h*(?:class|struct)[^;{]+[;{])
208
209        # Left shift with leading indent
210        |(?<left_shift>\n\h+<<)
211
212        # Case labels with extra spaces
213        |(?<switch_case>\bcase\h\h+)
214
215        # C++ keywords immediately followed by ::
216        |(?:\b(?<keyword>typedef|class|struct|union|enum|public|protected|private|static|inline|extern|virtual|explicit)::)
217
218        # Newline
219        |(?<newline>\n)
220        )/sx
221    ) {
222        $match = $&;
223        $output .= $`;
224        $input = $';
225
226        #print "DEBUG: $match\n";
227
228        if( exists $+{newline} ) {
229            # Newline
230            $last_newline = $';
231            if( $paren_level ) {
232                $input =~ s/^\h+/    $paren_indent/;
233            }
234        }
235        elsif( exists $+{open_paren} ) {
236            # Opening paren:
237            # - Remember the indentation of the first non-white space char in
238            # the line
239            ++$paren_level;
240            if( $paren_level == 1 ) {
241                ($paren_indent) = ($last_newline =~ /^(\h*)/);
242            } else {
243                $paren_indent .= '    ';
244            }
245        }
246        elsif( exists $+{leading_close_paren} ) {
247            # Closing paren with leading indent:
248            # - Reindent the closing paren at the same level as the line that
249            # contains the corresponding opening paren
250            # - Decrease indentation
251            --$paren_level;
252            $match = "\n$paren_indent)";
253            $paren_indent =~ s/^    //;
254        }
255        elsif( exists $+{close_paren} ) {
256            # Closing paren
257            # - Decrease indentation
258            --$paren_level;
259            $match = ')';
260            $paren_indent =~ s/^    //;
261        }
262        elsif( exists $+{class_head} ) {
263            # Class header with leading indent
264            # - Reformat the class header
265            $match = reformat_class_header($+{class_head});
266        }
267        elsif( exists $+{left_shift} ) {
268            # Left shift with leading indent
269            # - Fix indentation to 4 characters
270            # (Uncrustify aligns "<<" vertically by default)
271            my($indent) = ($last_newline =~ /^(\h*)/);
272            $match = "\n    $indent<<";
273        }
274        elsif( exists $+{switch_case} ) {
275            # Case labels with extra spaces left by uncrustify
276            $match = 'case ';
277        }
278        elsif( exists $+{keyword} ) {
279            # C++ keyword immediately followed by ::
280            $match = $+{keyword}. ' ::';
281        }
282
283        $output .= $match;
284    }
285
286    $output .= $input;
287    return $output;
288}
289
290#!
291# \brief Reformat a class header
292# \details Removes extra newlines purposely added by the uncrustify
293# formatter in class definition headers "class C : public B, ... {"
294# \param[in] text text to transform
295# \return the transformed text
296# \todo The current implementation flattens the whole class header without
297# checking the length of the resulting text. We should be a bit smarter than
298# that...
299#
300sub reformat_class_header {
301    my($class_head) = @_;
302
303    # Only reformat class definitions (terminated by '{')
304    if( $class_head !~ /{$/ ) {
305        return $class_head;
306    }
307
308    # Preserve leading indentation
309    my $indent = '';
310    if( $class_head =~ /^\s+/s ) {
311        $indent = $&;
312        $class_head = $';
313    }
314
315    # Compact all spaces
316    $class_head =~ s/\s+/ /sg;
317
318    # Fix the missing space before leading :: that has been eaten by uncrustify
319    $class_head =~ s/\b(class|struct|union|enum|public|protected|private|virtual)::/$1 ::/gs;
320
321    return $indent . $class_head;
322}
323
324
325