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