File: indent.pl

package info (click to toggle)
geogram 1.9.6-1
  • links: PTS, VCS
  • area: contrib
  • in suites: sid
  • size: 15,436 kB
  • sloc: cpp: 143,890; ansic: 10,098; perl: 1,430; sh: 1,199; yacc: 522; lex: 182; python: 157; javascript: 149; makefile: 17
file content (324 lines) | stat: -rwxr-xr-x 8,221 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
#!/usr/bin/perl
use strict;
use warnings FATAL => 'all';
use FindBin();
use lib $FindBin::Bin;
use FileUtils();

#############################################################################
# Globals
#############################################################################

#! Help sections
my $HELP = {
    NAME => <<END,
    $FindBin::Script - Reindent C++ source code
END

    DESCRIPTION => <<END,
    $FindBin::Script searches C and C++ files in the specified file arguments
    and reindents the source code using the code indenter "uncrustify".

    By default the result of file F is dumped to file F.indent unless options
    --replace or --no-backup are specified.

    NOTE: Only files with extension (c, h, cpp, hpp, cxx, hxx, C, H) are
    processed.
END
};

#! Expected version of uncrustify
my $UNCRUSTIFY_VERSION = '0.60';

#! Option values
my %OPTIONS;

#! Debug flag
my $DEBUG;

#############################################################################
# Main
#############################################################################

FileUtils::update_files(
    id => 'indent',
    option_values => \%OPTIONS,
    init => \&init,
    file_handler => \&process_file,
    help => $HELP,
);

#############################################################################

#!
# \brief Initialization function
# \details This called just after parsing command line
#
sub init {
    $DEBUG = $OPTIONS{debug};

    # Verify that uncrustify is available and has the right version

    my $version = qx{uncrustify --version};
    chomp($version);

    if( not $version ) {
        die <<END;
Error: $FindBin::Script requires uncrustify version $UNCRUSTIFY_VERSION
END
    }

    if( $version !~ /\s+$UNCRUSTIFY_VERSION$/o ) {
        die <<END;
Error: $FindBin::Script requires uncrustify version $UNCRUSTIFY_VERSION
Found $version which is not suitable.
END
    }

    return;
}

#!
# \brief Run uncrustify on a file
# \param[in] file path to the input file
# \return the text indented by uncrustify
#
sub process_file {
    my($file) = @_;

    my @command = (
        'uncrustify',
        '-q',
        '-c', "$FindBin::Bin/uncrustify.cfg",
        #'-s', '-p', 'uncrustify.log',
        '-l', 'CPP',
    );

    if( defined($file) ) {
        push(@command, '-f', $file);
    }

    # Run uncrustify
    # Get the the result from the uncrustify output

    if( $DEBUG ) {
        print "DEBUG: executing command: @command\n";
    }

    my $text = qx{@command};
    if( $? != 0 ) {
        print "Error: uncrustify failed to process ",
            defined($file) ? $file : "standard input",
            " (return code $?)\n";
        return;
    }

    $text = fix_indent($text);
    return $text;
}

#!
# \brief Fix indentation in a given text
# \param[in] input the input text
# \return the output text
# \details This functions fixes indentation problems left by uncrustify:
#
# 1) Indentation of class headers
#
# Uncrustify has been purposely configured to reformat the constructor
# initializer list by breaking after the first ':' and each ',':
#
# \code
# Class::Class() :
#     init1_(...),
#     init2_(...)
# {
# }
# \endcode
#
#
# Unfortunately this also impacts class headers formatting as follows:
# \code
# class A :
#     public B,
#     public C
# {
# \endcode
#
# We want to reformat them as follows:
#
# \code
# class A : public B, public C {
# \endcode
#
#
# 2) Unexpected indentation problems left by uncrustify:
#
# - closing parens are not indented properly: closing chars are indented
# one level too far and the contents of the parens is not satisfactory
# - extra spaces between a switch case and the case value are not removed
# - empty for(;;;) statements have a space before the closing paren.
#
#
sub fix_indent {
    my($input) = @_;

    # Paren nesting level
    my $paren_level = 0;

    # Pointer to the char after the last newline
    my $last_newline = '';

    # Indentation of the last opening paren
    my $paren_indent = '';

    # Last match
    my $match;

    # Output text
    my $output = '';

    while( $input =~ /(?:
        # Skip preprocessor directive
        \#(?:\\\n|\/\*.*?\*\/|\N)*

        # Skip C++ comment
        |\/\/\N*(?:\n\s*\/\/\N*)*

        # Skip C comment
        |\/\*.*?\*\/

        # Skip string constant
        |"(?:\\.|\N)*?"

        # Skip character constants
        |'(?:\\.|[^\\'])+'

        # Opening paren
        |(?<open_paren>\()

        # Closing paren with leading indent
        |(?<leading_close_paren>\n\h+\))

        # Closing paren
        |(?<close_paren>\h*\))

        # Leading class header
        |(?<class_head>\n\h*(?:class|struct)[^;{]+[;{])

        # Left shift with leading indent
        |(?<left_shift>\n\h+<<)

        # Case labels with extra spaces
        |(?<switch_case>\bcase\h\h+)

        # C++ keywords immediately followed by ::
        |(?:\b(?<keyword>typedef|class|struct|union|enum|public|protected|private|static|inline|extern|virtual|explicit)::)

        # Newline
        |(?<newline>\n)
        )/sx
    ) {
        $match = $&;
        $output .= $`;
        $input = $';

        #print "DEBUG: $match\n";

        if( exists $+{newline} ) {
            # Newline
            $last_newline = $';
            if( $paren_level ) {
                $input =~ s/^\h+/    $paren_indent/;
            }
        }
        elsif( exists $+{open_paren} ) {
            # Opening paren:
            # - Remember the indentation of the first non-white space char in
            # the line
            ++$paren_level;
            if( $paren_level == 1 ) {
                ($paren_indent) = ($last_newline =~ /^(\h*)/);
            } else {
                $paren_indent .= '    ';
            }
        }
        elsif( exists $+{leading_close_paren} ) {
            # Closing paren with leading indent:
            # - Reindent the closing paren at the same level as the line that
            # contains the corresponding opening paren
            # - Decrease indentation
            --$paren_level;
            $match = "\n$paren_indent)";
            $paren_indent =~ s/^    //;
        }
        elsif( exists $+{close_paren} ) {
            # Closing paren
            # - Decrease indentation
            --$paren_level;
            $match = ')';
            $paren_indent =~ s/^    //;
        }
        elsif( exists $+{class_head} ) {
            # Class header with leading indent
            # - Reformat the class header
            $match = reformat_class_header($+{class_head});
        }
        elsif( exists $+{left_shift} ) {
            # Left shift with leading indent
            # - Fix indentation to 4 characters
            # (Uncrustify aligns "<<" vertically by default)
            my($indent) = ($last_newline =~ /^(\h*)/);
            $match = "\n    $indent<<";
        }
        elsif( exists $+{switch_case} ) {
            # Case labels with extra spaces left by uncrustify
            $match = 'case ';
        }
        elsif( exists $+{keyword} ) {
            # C++ keyword immediately followed by ::
            $match = $+{keyword}. ' ::';
        }

        $output .= $match;
    }

    $output .= $input;
    return $output;
}

#!
# \brief Reformat a class header
# \details Removes extra newlines purposely added by the uncrustify
# formatter in class definition headers "class C : public B, ... {"
# \param[in] text text to transform
# \return the transformed text
# \todo The current implementation flattens the whole class header without
# checking the length of the resulting text. We should be a bit smarter than
# that...
#
sub reformat_class_header {
    my($class_head) = @_;

    # Only reformat class definitions (terminated by '{')
    if( $class_head !~ /{$/ ) {
        return $class_head;
    }

    # Preserve leading indentation
    my $indent = '';
    if( $class_head =~ /^\s+/s ) {
        $indent = $&;
        $class_head = $';
    }

    # Compact all spaces
    $class_head =~ s/\s+/ /sg;

    # Fix the missing space before leading :: that has been eaten by uncrustify
    $class_head =~ s/\b(class|struct|union|enum|public|protected|private|virtual)::/$1 ::/gs;

    return $indent . $class_head;
}