Make prepare-ChangeLog -g <commit> generate a more standard ChangeLog entry.
[WebKit.git] / Tools / Scripts / prepare-ChangeLog
1 #!/usr/bin/env perl
2 # -*- Mode: perl; indent-tabs-mode: nil; c-basic-offset: 2  -*-
3
4 #
5 #  Copyright (C) 2000, 2001 Eazel, Inc.
6 #  Copyright (C) 2002-2007, 2015 Apple Inc.  All rights reserved.
7 #  Copyright (C) 2009 Torch Mobile, Inc.
8 #  Copyright (C) 2009 Cameron McCormack <cam@mcc.id.au>
9 #
10 #  prepare-ChangeLog is free software; you can redistribute it and/or
11 #  modify it under the terms of the GNU General Public
12 #  License as published by the Free Software Foundation; either
13 #  version 2 of the License, or (at your option) any later version.
14 #
15 #  prepare-ChangeLog is distributed in the hope that it will be useful,
16 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
17 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18 #  General Public License for more details.
19 #
20 #  You should have received a copy of the GNU General Public
21 #  License along with this program; if not, write to the Free
22 #  Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
23 #
24
25
26 # Perl script to create a ChangeLog entry with names of files
27 # and functions from a diff.
28 #
29 # Darin Adler <darin@bentspoon.com>, started 20 April 2000
30 # Java support added by Maciej Stachowiak <mjs@eazel.com>
31 # Objective-C, C++ and Objective-C++ support added by Maciej Stachowiak <mjs@apple.com>
32 # Git support added by Adam Roben <aroben@apple.com>
33 # --git-index flag added by Joe Mason <joe.mason@torchmobile.com>
34
35
36 #
37 # TODO:
38 #   Decide what a good logical order is for the changed files
39 #     other than a normal text "sort" (top level first?)
40 #     (group directories?) (.h before .c?)
41 #   Handle yacc source files too (other languages?).
42 #   Help merge when there are ChangeLog conflicts or if there's
43 #     already a partly written ChangeLog entry.
44 #   Add command line option to put the ChangeLog into a separate file.
45 #   Add SVN version numbers for commit (can't do that until
46 #     the changes are checked in, though).
47 #   Work around diff stupidity where deleting a function that starts
48 #     with a comment makes diff think that the following function
49 #     has been changed (if the following function starts with a comment
50 #     with the same first line, such as /**)
51 #   Work around diff stupidity where deleting an entire function and
52 #     the blank lines before it makes diff think you've changed the
53 #     previous function.
54
55 use strict;
56 use warnings;
57
58 use File::Basename;
59 use File::Spec;
60 use FindBin;
61 use Getopt::Long;
62 use lib $FindBin::Bin;
63 use List::Util qw/max/;
64 use POSIX qw(strftime);
65 use VCSUtils;
66
67 sub actuallyGenerateFunctionLists($$$$$$);
68 sub attributeCommand($$);
69 sub changeLogDate($);
70 sub changeLogEmailAddressFromArgs($$);
71 sub changeLogNameFromArgs($$);
72 sub computeModifiedFunctions($$$);
73 sub createPatchCommand($$$$);
74 sub decodeEntities($);
75 sub determinePropertyChanges($$$);
76 sub diffCommand($$$$);
77 sub diffFromToString($$$);
78 sub extractLineRangeAfterChange($);
79 sub extractLineRangeBeforeChange($);
80 sub fetchBugXMLData($$);
81 sub fetchBugDescriptionFromBugXMLData($$$);
82 sub fetchRadarURLFromBugXMLData($$);
83 sub findChangeLogs($$);
84 sub findOriginalFileFromSvn($);
85 sub generateFileList(\%$$$);
86 sub generateFunctionLists($$$$$);
87 sub generateNewChangeLogs($$$$$$$$$$$$$$);
88 sub getLatestChangeLogs($);
89 sub get_function_line_ranges($$);
90 sub get_function_line_ranges_for_cpp($$);
91 sub delete_namespaces_from_ranges_for_cpp(\@\@);
92 sub is_function_in_namespace($$);
93 sub get_function_line_ranges_for_java($$);
94 sub get_function_line_ranges_for_javascript($$);
95 sub get_function_line_ranges_for_perl($$);
96 sub get_selector_line_ranges_for_css($$);
97 sub get_function_line_ranges_for_swift($$);
98 sub parseSwiftFunctionArgs($);
99 sub isAddedStatus($);
100 sub isConflictStatus($$$);
101 sub isModifiedStatus($);
102 sub isUnmodifiedStatus($);
103 sub main();
104 sub method_decl_to_selector($);
105 sub normalizeLineEndings($$);
106 sub openChangeLogs($);
107 sub originalFile($$$$);
108 sub pluralizeAndList($$@);
109 sub printDiff($$$$);
110 sub processPaths(\@);
111 sub propertyChangeDescription($);
112 sub resolveChangeLogsPath($@);
113 sub resolveConflictedChangeLogs($);
114 sub reviewerAndDescriptionForGitCommit($$);
115 sub statusCommand($$$$);
116 sub statusDescription($$$$);
117 sub svnUpdateCommand(@);
118 sub testListForChangeLog(@);
119
120 ### Constant variables.
121 # Project time zone for Cupertino, CA, US
122 use constant ChangeLogTimeZone => "PST8PDT";
123 use constant SVN => "svn";
124 use constant GIT => "git";
125 use constant SupportedTestExtensions => {map { $_ => 1 } qw(html shtml svg xml xhtml pl php)};
126
127 my $devNull = File::Spec->devnull();
128 my %attributeCache;
129
130 exit(main());
131
132 sub main()
133 {
134     my $bugDescription;
135     my $bugRadarURL;
136     my $bugNumber;
137     my $name;
138     my $emailAddress;
139     my $mergeBase = 0;
140     my $gitCommit = 0;
141     my $gitIndex = "";
142     my $gitReviewer = "";
143     my $checkWebKitStyle = 0;
144     my $openChangeLogs = 0;
145     my $writeChangeLogs = 1;
146     my $delimiters = 0;
147     my $showHelp = 0;
148     my $spewDiff = $ENV{"PREPARE_CHANGELOG_DIFF"};
149     my $updateChangeLogs = 1;
150     my $parseOptionsResult =
151         GetOptions("diff|d!" => \$spewDiff,
152                    "bug|b:i" => \$bugNumber,
153                    "delimiters" => \$delimiters,
154                    "description:s" => \$bugDescription,
155                    "name:s" => \$name,
156                    "email:s" => \$emailAddress,
157                    "merge-base:s" => \$mergeBase,
158                    "git-commit|g:s" => \$gitCommit,
159                    "git-index" => \$gitIndex,
160                    "git-reviewer:s" => \$gitReviewer,
161                    "help|h!" => \$showHelp,
162                    "style!" => \$checkWebKitStyle,
163                    "open|o!" => \$openChangeLogs,
164                    "write!" => \$writeChangeLogs,
165                    "update!" => \$updateChangeLogs);
166     if (!$parseOptionsResult || $showHelp) {
167         print STDERR basename($0) . " [-b|--bug=<bugid>] [-d|--diff] [-h|--help] [-o|--open] [-g|--git-commit=<committish>] [--git-reviewer=<name>] [svndir1 [svndir2 ...]]\n";
168         print STDERR "  -b|--bug        Fill in the ChangeLog bug information from the given bug.\n";
169         print STDERR "  --description   One-line description that matches the bug title.\n";
170         print STDERR "  -d|--diff       Spew diff to stdout when running\n";
171         print STDERR "  --merge-base    Populate the ChangeLogs with the diff to this branch\n";
172         print STDERR "  -g|--git-commit Populate the ChangeLogs from the specified git commit\n";
173         print STDERR "  --git-index     Populate the ChangeLogs from the git index only\n";
174         print STDERR "  --git-reviewer  When populating the ChangeLogs from a git commit claim that the spcified name reviewed the change.\n";
175         print STDERR "                  This option is useful when the git commit lacks a Signed-Off-By: line\n";
176         print STDERR "  -h|--help       Show this help message\n";
177         print STDERR "  --[no-]style    Run check-webkit-style script when done (default: no-style)\n";
178         print STDERR "  -o|--open       Open ChangeLogs in an editor when done\n";
179         print STDERR "  --[no-]update   Update ChangeLogs from svn before adding entry (default: update)\n";
180         print STDERR "  --[no-]write    Write ChangeLogs to disk (otherwise send new entries to stdout) (default: write)\n";
181         print STDERR "  --delimiters    When writing to stdout, label and print a \"~\" after each entry\n";
182         print STDERR "  --email=        Specify the email address to be used in the patch\n";
183         return 1;
184     }
185
186     if ($checkWebKitStyle) {
187         print STDERR "  Running check-webkit-style.\n  ";
188         system "$FindBin::Bin/check-webkit-style";
189     }
190
191     die "--git-commit and --git-index are incompatible." if ($gitIndex && $gitCommit);
192
193     isSVN() || isGit() || die "Couldn't determine your version control system.";
194
195     my %paths = processPaths(@ARGV);
196
197     # Find the list of modified files
198     my ($changedFiles, $conflictFiles, $functionLists, $addedRegressionTests, $requiresTests) = generateFileList(%paths, $gitCommit, $gitIndex, $mergeBase);
199
200     if (!@$changedFiles && !@$conflictFiles && !keys %$functionLists) {
201         print STDERR "  No changes found.\n";
202         return 1;
203     }
204
205     if (@$conflictFiles) {
206         print STDERR "  The following files have conflicts. Run prepare-ChangeLog again after fixing the conflicts:\n";
207         print STDERR join("\n", @$conflictFiles), "\n";
208         return 1;
209     }
210
211     generateFunctionLists($changedFiles, $functionLists, $gitCommit, $gitIndex, $mergeBase);
212
213     # Get some parameters for the ChangeLog we are about to write.
214     $name = changeLogNameFromArgs($name, $gitCommit);
215     $emailAddress = changeLogEmailAddressFromArgs($emailAddress, $gitCommit);
216
217     print STDERR "  Change author: $name <$emailAddress>.\n";
218
219     # Remove trailing parenthesized notes from user name (bit of hack).
220     $name =~ s/\(.*?\)\s*$//g;
221
222     my $bugURL;
223     if ($bugNumber) {
224         $bugURL = "https://bugs.webkit.org/show_bug.cgi?id=$bugNumber";
225     }
226
227     if ($bugNumber && !$bugDescription) {
228         my $bugXMLData = fetchBugXMLData($bugURL, $bugNumber);
229         $bugDescription = fetchBugDescriptionFromBugXMLData($bugURL, $bugNumber, $bugXMLData);
230         $bugRadarURL = fetchRadarURLFromBugXMLData($bugNumber, $bugXMLData);
231     }
232
233     my ($filesInChangeLog, $prefixes) = findChangeLogs($functionLists, $writeChangeLogs);
234
235     # Get the latest ChangeLog files from svn.
236     my $changeLogs = getLatestChangeLogs($prefixes);
237
238     if (@$changeLogs && $updateChangeLogs && isSVN()) {
239         resolveConflictedChangeLogs($changeLogs);
240     }
241
242     generateNewChangeLogs($prefixes, $filesInChangeLog, $addedRegressionTests, $requiresTests, $functionLists, $bugURL, $bugDescription, $bugRadarURL, $name, $emailAddress, $gitReviewer, $gitCommit, $writeChangeLogs, $delimiters);
243
244     if ($writeChangeLogs) {
245         print STDERR "-- Please remember to include a detailed description in your ChangeLog entry. --\n-- See <http://webkit.org/coding/contributing.html> for more info --\n";
246     }
247
248     # Write out another diff.
249     if ($spewDiff && @$changedFiles) {
250         printDiff($changedFiles, $gitCommit, $gitIndex, $mergeBase);
251     }
252
253     # Open ChangeLogs.
254     if ($openChangeLogs && @$changeLogs) {
255         openChangeLogs($changeLogs);
256     }
257     return 0;
258 }
259
260 sub originalFile($$$$)
261 {
262     my ($file, $gitCommit, $gitIndex, $mergeBase) = @_;
263
264     my $command;
265     if (isSVN()) {
266         my $escapedPathsString = escapeSubversionPath($file);
267         $command = SVN . " cat $escapedPathsString";
268     } elsif (isGit()) {
269         $command = GIT . " show ";
270         if ($mergeBase) {
271             $command .= "$mergeBase";
272         } else {
273             $command .= "HEAD";
274         }
275         $command .= ":$file";
276     }
277
278     return $command;
279 }
280
281 sub generateFunctionLists($$$$$)
282 {
283     my ($changedFiles, $functionLists, $gitCommit, $gitIndex, $mergeBase) = @_;
284     my %delegateHash = (
285         openDiff => sub ($$$$) {
286             my ($changedFiles, $gitCommit, $gitIndex, $mergeBase) = @_;
287             return unless open(DIFF, "-|", diffCommand($changedFiles, $gitCommit, $gitIndex, $mergeBase));
288             return \*DIFF;
289         },
290         openFile => sub ($) {
291             my ($file) = @_;
292             return unless open(SOURCE, "<", $file);
293             return \*SOURCE;
294         },
295         openOriginalFile => sub ($) {
296             my ($file, $gitCommit, $gitIndex, $mergeBase) = @_;
297             return unless open(SOURCE, "-|", originalFile($file, $gitCommit, $gitIndex, $mergeBase));
298             return \*SOURCE;
299         },
300         normalizePath => sub ($) {
301             my ($path) = @_;
302             return normalizePath(makeFilePathRelative($path));
303         },
304     );
305     actuallyGenerateFunctionLists($changedFiles, $functionLists, $gitCommit, $gitIndex, $mergeBase, \%delegateHash);
306 }
307
308 sub actuallyGenerateFunctionLists($$$$$$)
309 {
310     my ($changedFiles, $functionLists, $gitCommit, $gitIndex, $mergeBase, $delegateHashRef) = @_;
311
312     my %line_ranges_after_changed;
313     my %line_ranges_before_changed;
314     if (@$changedFiles) {
315         # For each file, build a list of modified lines.
316         # Use line numbers from the "after" side of each diff.
317         print STDERR "  Reviewing diff to determine which lines changed.\n";
318         my $file;
319         my $diffFileHandle = $delegateHashRef->{openDiff}($changedFiles, $gitCommit, $gitIndex, $mergeBase);
320         if (!$diffFileHandle) {
321             die "The diff failed: $!.\n";
322         }
323         while (<$diffFileHandle>) {
324             my $filePath = parseDiffStartLine($_);
325             $file = $delegateHashRef->{normalizePath}($filePath) if $filePath;
326             if (defined $file) {
327                 my ($before_start, $before_end) = extractLineRangeBeforeChange($_);
328                 if ($before_start >= 1 && $before_end >= 1) {
329                     push @{$line_ranges_before_changed{$file}}, [ $before_start, $before_end ];
330                 } elsif (/DO_NOT_COMMIT/) {
331                     print STDERR "WARNING: file $file contains the string DO_NOT_COMMIT, line $.\n";
332                 }
333                 my ($after_start, $after_end) = extractLineRangeAfterChange($_);
334                 if ($after_start >= 1 && $after_end >= 1) {
335                     push @{$line_ranges_after_changed{$file}}, [ $after_start, $after_end ];
336                 } elsif (/DO_NOT_COMMIT/) {
337                     print STDERR "WARNING: file $file contains the string DO_NOT_COMMIT, line $.\n";
338                 }
339             }
340         }
341         close($diffFileHandle);
342     }
343
344     # For each source file, convert line range to function list.
345     print STDERR "  Extracting affected function names from source files.\n";
346     my %filesToExamine = map { $_ => 1 } (keys(%line_ranges_before_changed), keys(%line_ranges_after_changed));
347     foreach my $file (keys %filesToExamine) {
348         # Escape whitespace in filenames rather than adding quotes, since many functions can open files
349         # and other code doesn't expect to see a trailing " character when sniffing a file extension.
350         chomp $file;
351         $file =~ s/ /\\ /g;
352
353         my %saw_function;
354
355         # Find all the functions in the file.
356         my $sourceFileHandle = $delegateHashRef->{openFile}($file);
357         next unless $sourceFileHandle;
358         my @afterChangeFunctionRanges = get_function_line_ranges($sourceFileHandle, $file);
359         close($sourceFileHandle);
360
361         # Find modified functions in the file.
362         if ($line_ranges_after_changed{$file}) {
363             my @change_ranges = (@{$line_ranges_after_changed{$file}}, []);
364             my @functions = computeModifiedFunctions($file, \@change_ranges, \@afterChangeFunctionRanges);
365
366             # Format the list of functions.
367             if (@functions) {
368                 $functionLists->{$file} = "" if !defined $functionLists->{$file};
369                 $functionLists->{$file} .= "\n        (" . join("):\n        (", @functions) . "):";
370             }
371         }
372         # Find the deleted functions in the original file.
373         if ($line_ranges_before_changed{$file}) {
374             my $originalFileHandle = $delegateHashRef->{openOriginalFile}($file, $gitCommit, $gitIndex, $mergeBase);
375             next unless $originalFileHandle;
376             my @beforeChangeFunctionRanges = get_function_line_ranges($originalFileHandle, $file);
377             close($originalFileHandle);
378
379             my %existsAfterChange = map { $_->[2] => 1 } @afterChangeFunctionRanges;
380
381             my @functions;
382             my %sawFunctions;
383             for my $functionRange (@beforeChangeFunctionRanges) {
384                 my $functionName = $functionRange->[2];
385                 if (!$existsAfterChange{$functionName} && !$sawFunctions{$functionName}) {
386                     push @functions, $functionName;
387                     $sawFunctions{$functionName} = 1;
388                 }
389             }
390
391             # Format the list of deleted functions.
392             if (@functions) {
393                 $functionLists->{$file} = "" if !defined $functionLists->{$file};
394                 $functionLists->{$file} .= "\n        (" . join("): Deleted.\n        (", @functions) . "): Deleted.";
395             }
396         }
397     }
398 }
399
400 sub computeModifiedFunctions($$$)
401 {
402     my ($file, $changedLineRanges, $functionRanges) = @_;
403
404     my %sawFunction;
405
406     # Find all the modified functions.
407     my @functions;
408     my @change_ranges = @{$changedLineRanges};
409     my @change_range = (0, 0);
410     FUNCTION: foreach my $function_range_ref (@{$functionRanges}) {
411         my @function_range = @{$function_range_ref};
412
413         # FIXME: This is a hack. If the function name is empty, skip it.
414         # The cpp, python, javascript, perl, css and java parsers
415         # are not perfectly implemented and sometimes function names cannot be retrieved
416         # correctly. As you can see in get_function_line_ranges_XXXX(), those parsers
417         # are not intended to implement real parsers but intended to just retrieve function names
418         # for most practical syntaxes.
419         next unless $function_range[2];
420
421         # Advance to successive change ranges.
422         for (;; @change_range = @{shift @change_ranges}) {
423             last FUNCTION unless @change_range;
424
425             # If past this function, move on to the next one.
426             next FUNCTION if $change_range[0] > $function_range[1];
427
428             # If an overlap with this function range, record the function name.
429             if ($change_range[1] >= $function_range[0]
430                 and $change_range[0] <= $function_range[1]) {
431                 if (!$sawFunction{$function_range[2]}) {
432                     $sawFunction{$function_range[2]} = 1;
433                     push @functions, $function_range[2];
434                 }
435                 next FUNCTION;
436             }
437         }
438     }
439
440     return @functions;
441 }
442
443 sub changeLogDate($)
444 {
445     my ($timeZone) = @_;
446     my $savedTimeZone = $ENV{'TZ'};
447     # Set TZ temporarily so that localtime() is in that time zone
448     $ENV{'TZ'} = $timeZone;
449     my $date = strftime("%Y-%m-%d", localtime());
450     if (defined $savedTimeZone) {
451          $ENV{'TZ'} = $savedTimeZone;
452     } else {
453          delete $ENV{'TZ'};
454     }
455     return $date;
456 }
457
458 sub changeLogNameFromArgs($$)
459 {
460     my ($nameFromArgs, $gitCommit) = @_;
461     # Silently allow --git-commit to win, we could warn if $nameFromArgs is defined.
462     my $command = GIT . ' log --max-count=1 --pretty="format:%an" "' . $gitCommit . '"';
463     return `$command` if $gitCommit;
464
465     return $nameFromArgs || changeLogName();
466 }
467
468 sub changeLogEmailAddressFromArgs($$)
469 {
470     my ($emailAddressFromArgs, $gitCommit) = @_;
471     # Silently allow --git-commit to win, we could warn if $emailAddressFromArgs is defined.
472     my $command = GIT . ' log --max-count=1 --pretty="format:%ae" "' . $gitCommit . '"';
473     return `$command` if $gitCommit;
474
475     return $emailAddressFromArgs || changeLogEmailAddress();
476 }
477
478 sub fetchBugXMLData($$)
479 {
480     my ($bugURL, $bugNumber) = @_;
481
482     my $bugXMLURL = "$bugURL&ctype=xml&excludefield=attachmentdata";
483     # Perl has no built in XML processing, so we'll fetch and parse with curl and grep
484     # Pass --insecure because some cygwin installs have no certs we don't
485     # care about validating that bugs.webkit.org is who it says it is here.
486     my $xmlData = `curl --insecure --silent "$bugXMLURL"`;
487     if ($xmlData !~ /<\?xml/) {
488         # Maybe the reason the above did not work is because the curl that is installed doesn't
489         # support ssl at all.
490         if (`curl --version | grep ^Protocols` !~ /\bhttps\b/) {
491             print STDERR "  Could not get description for bug $bugNumber.\n";
492             print STDERR "  It looks like your version of curl does not support ssl.\n";
493             print STDERR "  If you are using macports, this can be fixed with sudo port install curl +ssl.\n";
494         }
495         exit 1;
496     }
497     return $xmlData;
498 }
499
500 sub fetchBugDescriptionFromBugXMLData($$$)
501 {
502     my ($bugURL, $bugNumber, $bugXMLData) = @_;
503
504     if ($bugXMLData !~ /<short_desc>(.*)<\/short_desc>/) {
505         print STDERR "  Bug $bugNumber has no bug description. Maybe you set wrong bug ID?\n";
506         print STDERR "  The bug URL: $bugURL\n";
507         exit 1;
508     }
509
510     my $bugDescription = decodeEntities($1);
511     print STDERR "  Description from bug $bugNumber:\n    \"$bugDescription\".\n";
512     return $bugDescription;
513 }
514
515 sub fetchRadarURLFromBugXMLData($$)
516 {
517     my ($bugNumber, $bugXMLData) = @_;
518
519     return "" if $bugXMLData !~ m|<thetext>\s*(&lt;rdar://problem/\d+&gt;)|;
520
521     my $bugRadarURL = decodeEntities($1);
522     print STDERR "  Radar URL from bug $bugNumber:\n    \"$bugRadarURL\".\n";
523     return $bugRadarURL;
524 }
525
526 sub findChangeLogs($$)
527 {
528     my ($functionLists, $requireChangeLogToExist) = @_;
529
530     # Find the change logs.
531     my %has_log;
532     my %filesInChangeLog;
533     foreach my $file (sort keys %$functionLists) {
534         my $prefix = $file;
535         my $has_log = 0;
536         while ($prefix) {
537             if ($^O eq "MSWin32") {
538                 $prefix =~ s-\\[^\\]+\\?$-\\- or $prefix = "";
539             } else {
540                 $prefix =~ s-/[^/]+/?$-/- or $prefix = "";
541             }
542             $has_log = $has_log{$prefix};
543             if (!defined $has_log) {
544                 $has_log = -f "${prefix}ChangeLog";
545                 $has_log{$prefix} = $has_log;
546             }
547             last if $has_log;
548         }
549         if (!$has_log && $requireChangeLogToExist) {
550             print STDERR "No ChangeLog found for $file.\n";
551         } else {
552             push @{$filesInChangeLog{$prefix}}, $file;
553         }
554     }
555
556     # Build the list of ChangeLog prefixes in the correct project order
557     my @prefixes;
558     my %prefixesSort;
559     foreach my $prefix (keys %filesInChangeLog) {
560         my $prefixDir = substr($prefix, 0, length($prefix) - 1); # strip trailing /
561         my $sortKey = lc $prefix;
562         $sortKey = "top level" unless length $sortKey;
563
564         if ($prefixDir eq "top level") {
565             $sortKey = "";
566         } elsif ($prefixDir eq "Tools") {
567             $sortKey = "-, just after top level";
568         } elsif ($prefixDir eq "WebBrowser") {
569             $sortKey = lc "WebKit, WebBrowser after";
570         } elsif ($prefixDir eq "Source/WebCore") {
571             $sortKey = lc "WebFoundation, WebCore after";
572         } elsif ($prefixDir eq "LayoutTests") {
573             $sortKey = lc "~, LayoutTests last";
574         }
575
576         $prefixesSort{$sortKey} = $prefix;
577     }
578     foreach my $prefixSort (sort keys %prefixesSort) {
579         push @prefixes, $prefixesSort{$prefixSort};
580     }
581     return (\%filesInChangeLog, \@prefixes);
582 }
583
584 sub getLatestChangeLogs($)
585 {
586     my ($prefixes) = @_;
587
588     my @changeLogs = ();
589     foreach my $prefix (@$prefixes) {
590         push @changeLogs, File::Spec->catfile($prefix || ".", "ChangeLog");
591     }
592     return \@changeLogs;
593 }
594
595 sub svnUpdateCommand(@)
596 {
597     my @changeLogs = shift;
598
599     my @escapedChangeLogPaths = map(escapeSubversionPath($_), @changeLogs);
600     my $escapedChangeLogPathsString = qq(") . join(qq(" "), @escapedChangeLogPaths) . qq(");
601     my $command = SVN . " update $escapedChangeLogPathsString";
602
603     return $command;
604 }
605
606 sub resolveChangeLogsPath($@)
607 {
608     my ($resolveChangeLogsPath, @conflictedChangeLogs) = @_;
609
610     my @escapedConflictedChangeLogs = map(escapeSubversionPath($_), @conflictedChangeLogs);
611     my $escapedConflictedChangeLogsString = qq(") . join(qq(" "), @escapedConflictedChangeLogs) . qq(");
612     my $command = "$resolveChangeLogsPath --no-warnings $escapedConflictedChangeLogsString";
613
614     return $command;
615 }
616
617 sub resolveConflictedChangeLogs($)
618 {
619     my ($changeLogs) = @_;
620
621     print STDERR "  Running 'svn update' to update ChangeLog files.\n";
622     open ERRORS, "-|", svnUpdateCommand(@$changeLogs)
623         or die "The svn update of ChangeLog files failed: $!.\n";
624     my @conflictedChangeLogs;
625     while (my $line = <ERRORS>) {
626         print STDERR "    ", $line;
627         push @conflictedChangeLogs, $1 if $line =~ m/^C\s+(.+?)[\r\n]*$/;
628     }
629     close ERRORS;
630
631     return if !@conflictedChangeLogs;
632
633     print STDERR "  Attempting to merge conflicted ChangeLogs.\n";
634     my $resolveChangeLogsPath = File::Spec->catfile(dirname($0), "resolve-ChangeLogs");
635     open RESOLVE, "-|", resolveChangeLogsPath($resolveChangeLogsPath, @conflictedChangeLogs)
636         or die "Could not open resolve-ChangeLogs script: $!.\n";
637     print STDERR "    $_" while <RESOLVE>;
638     close RESOLVE;
639 }
640
641 sub generateNewChangeLogs($$$$$$$$$$$$$$)
642 {
643     my ($prefixes, $filesInChangeLog, $addedRegressionTests, $requiresTests, $functionLists, $bugURL, $bugDescription, $bugRadarURL, $name, $emailAddress, $gitReviewer, $gitCommit, $writeChangeLogs, $delimiters) = @_;
644
645     # Generate new ChangeLog entries and (optionally) write out new ChangeLog files.
646     foreach my $prefix (@$prefixes) {
647         my $endl = "\n";
648         my @old_change_log;
649
650         if ($writeChangeLogs) {
651             my $changeLogPath = File::Spec->catfile($prefix || ".", "ChangeLog");
652             print STDERR "  Editing the ${changeLogPath} file.\n";
653             open OLD_CHANGE_LOG, ${changeLogPath} or die "Could not open ${changeLogPath} file: $!.\n";
654             # It's less efficient to read the whole thing into memory than it would be
655             # to read it while we prepend to it later, but I like doing this part first.
656             @old_change_log = <OLD_CHANGE_LOG>;
657             close OLD_CHANGE_LOG;
658             # We want to match the ChangeLog's line endings in case it doesn't match
659             # the native line endings for this version of perl.
660             if ($old_change_log[0] =~ /(\r?\n)$/g) {
661                 $endl = "$1";
662             }
663             open CHANGE_LOG, "> ${changeLogPath}" or die "Could not write ${changeLogPath}\n.";
664             binmode(CHANGE_LOG);
665         } else {
666             open CHANGE_LOG, ">-" or die "Could not write to STDOUT\n.";
667             print substr($prefix, 0, length($prefix) - 1) . ":\n\n" unless (scalar @$prefixes) == 1 && !$delimiters;
668         }
669
670         my $date = changeLogDate(ChangeLogTimeZone);
671         print CHANGE_LOG normalizeLineEndings("$date  $name  <$emailAddress>\n\n", $endl);
672
673         my ($reviewer, $description) = reviewerAndDescriptionForGitCommit($gitCommit, $gitReviewer) if $gitCommit;
674         $reviewer = "NOBODY (OO" . "PS!)" if !$reviewer;
675
676         ($bugDescription, $description) =
677             ($description =~ /^(?:\s*(.*)\n)?(?:\s*\n)*((?:\n|.)*)/)
678             if !$bugDescription && $description;
679
680         $bugDescription = "Need a short description (OOPS!)." unless $bugDescription;
681         $bugURL = "Need the bug URL (OOPS!)." unless $bugURL;
682
683         print CHANGE_LOG normalizeLineEndings("        $bugDescription\n", $endl) if $bugDescription;
684         print CHANGE_LOG normalizeLineEndings("        $bugURL\n", $endl) if $bugURL;
685         print CHANGE_LOG normalizeLineEndings("        $bugRadarURL\n", $endl) if $bugRadarURL;
686         print CHANGE_LOG normalizeLineEndings("\n", $endl);
687
688         print CHANGE_LOG normalizeLineEndings("        Reviewed by $reviewer.\n\n", $endl);
689         print CHANGE_LOG normalizeLineEndings($description . "\n", $endl) if $description;
690
691         if ($prefix =~ m/WebCore/ || `pwd` =~ m/WebCore/ || @$requiresTests) {
692             if (@$addedRegressionTests) {
693                 print CHANGE_LOG normalizeLineEndings(testListForChangeLog(sort @$addedRegressionTests), $endl);
694             } else {
695                 print CHANGE_LOG normalizeLineEndings("        No new tests (OOPS!).\n\n", $endl);
696             }
697         }
698
699         foreach my $file (sort @{$filesInChangeLog->{$prefix}}) {
700             my $file_stem = substr $file, length $prefix;
701             $file_stem = unixPath($file_stem);
702             print CHANGE_LOG normalizeLineEndings("        * $file_stem:$functionLists->{$file}\n", $endl);
703         }
704
705         if ($writeChangeLogs) {
706             print CHANGE_LOG normalizeLineEndings("\n", $endl), @old_change_log;
707         } else {
708             print CHANGE_LOG "\n";
709             print "~\n"  if $delimiters;
710         }
711
712         close CHANGE_LOG;
713     }
714 }
715
716 sub printDiff($$$$)
717 {
718     my ($changedFiles, $gitCommit, $gitIndex, $mergeBase) = @_;
719
720     print STDERR "  Running diff to help you write the ChangeLog entries.\n";
721     local $/ = undef; # local slurp mode
722     my $changedFilesString = "'" . join("' '", @$changedFiles) . "'";
723     open DIFF, "-|", createPatchCommand($changedFilesString, $gitCommit, $gitIndex, $mergeBase) or die "The diff failed: $!.\n";
724     print <DIFF>;
725     close DIFF;
726 }
727
728 sub openChangeLogs($)
729 {
730     my ($changeLogs) = @_;
731
732     print STDERR "  Opening the edited ChangeLog files.\n";
733     my $editor = $ENV{CHANGE_LOG_EDITOR} || $ENV{VISUAL} || $ENV{EDITOR};
734     if ($editor) {
735         system ((split ' ', $editor), @$changeLogs);
736     } else {
737         $editor = $ENV{CHANGE_LOG_EDIT_APPLICATION};
738         if ($editor) {
739             system "open", "-a", $editor, @$changeLogs;
740         } else {
741             system "open", "-e", @$changeLogs;
742         }
743     }
744 }
745
746 sub get_function_line_ranges($$)
747 {
748     my ($file_handle, $file_name) = @_;
749
750     # Try to determine the source language based on the file extension.
751
752     return get_function_line_ranges_for_cpp($file_handle, $file_name) if $file_name =~ /\.(c|cpp|m|mm|h)$/;
753     return get_function_line_ranges_for_java($file_handle, $file_name) if $file_name =~ /\.java$/;
754     return get_function_line_ranges_for_javascript($file_handle, $file_name) if $file_name =~ /\.js$/;
755     return get_selector_line_ranges_for_css($file_handle, $file_name) if $file_name =~ /\.css$/;
756     return get_function_line_ranges_for_perl($file_handle, $file_name) if $file_name =~ /\.p[lm]$/;
757     return get_function_line_ranges_for_python($file_handle, $file_name) if $file_name =~ /\.py$/ or $file_name =~ /master\.cfg$/;
758     return get_function_line_ranges_for_swift($file_handle, $file_name) if $file_name =~ /\.swift$/;
759
760     # Try to determine the source language based on the script interpreter.
761
762     my $first_line = <$file_handle>;
763     seek($file_handle, 0, 0);
764
765     return () unless $first_line =~ m|^#!(?:/usr/bin/env\s+)?(\S+)|;
766     my $interpreter = $1;
767
768     return get_function_line_ranges_for_perl($file_handle, $file_name) if $interpreter =~ /perl$/;
769     return get_function_line_ranges_for_python($file_handle, $file_name) if $interpreter =~ /python$/;
770
771     return ();
772 }
773
774
775 sub method_decl_to_selector($)
776 {
777     (my $method_decl) = @_;
778
779     $_ = $method_decl;
780
781     if ((my $comment_stripped) = m-([^/]*)(//|/*).*-) {
782         $_ = $comment_stripped;
783     }
784
785     s/,\s*...//;
786
787     # Strip out the return type and parameter types. The extra )? takes care of most block parameter types.
788     s/\([^\)]*\)\)?//g;
789
790     if (/:/) {
791         my @components = split /:/;
792         pop @components if (scalar @components > 1);
793         $_ = (join ':', map {s/.*[^[:word:]]//; scalar $_;} @components) . ':';
794     } else {
795         s/\s*$//;
796         s/.*[^[:word:]]//;
797     }
798
799     return $_;
800 }
801
802
803
804 # Read a file and get all the line ranges of the things that look like C functions.
805 # A function name is the last word before an open parenthesis before the outer
806 # level open brace. A function starts at the first character after the last close
807 # brace or semicolon before the function name and ends at the close brace.
808 # Comment handling is simple-minded but will work for all but pathological cases.
809 #
810 # Result is a list of triples: [ start_line, end_line, function_name ].
811
812 sub get_function_line_ranges_for_cpp($$)
813 {
814     my ($file_handle, $file_name) = @_;
815
816     my @ranges;
817
818     my $in_comment = 0;
819     my $in_macro = 0;
820     my $in_method_declaration = 0;
821     my $in_parentheses = 0;
822     my $quotation_mark;
823     my $in_braces = 0;
824     my $in_toplevel_array_brace = 0;
825     my $brace_start = 0;
826     my $brace_end = 0;
827     my $namespace_start = -1;
828     my $skip_til_brace_or_semicolon = 0;
829     my $equal_observed = 0;
830
831     my $word = "";
832     my $interface_name = "";
833
834     my $potential_method_char = "";
835     my $potential_method_spec = "";
836
837     my $potential_start = 0;
838     my $potential_name = "";
839
840     my $start = 0;
841     my $name = "";
842
843     my $next_word_could_be_namespace = 0;
844     my $potential_namespace = "";
845     my @namespaces;
846     my @all_namespaces;
847
848     while (<$file_handle>) {
849         # Handle continued quoted string.
850         if ($quotation_mark) {
851             if (!s-([^\\]|\\.)*$quotation_mark--) {
852                 if (!m-\\$-) {
853                     warn "mismatched quotes at line $. in $file_name\n";
854                     undef $quotation_mark;
855                 }
856                 next;
857             }
858             undef $quotation_mark;
859         }
860
861         # Handle continued multi-line comment.
862         if ($in_comment) {
863             next unless s-.*\*/--;
864             $in_comment = 0;
865         }
866
867         # Handle continued macro.
868         if ($in_macro) {
869             $in_macro = 0 unless /\\$/;
870             next;
871         }
872
873         # Handle start of macro (or any preprocessor directive).
874         if (/^\s*\#/) {
875             $in_macro = 1 if /^([^\\]|\\.)*\\$/;
876             next;
877         }
878
879         # Handle comments and quoted text.
880         while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
881             my $match = $1;
882             if ($match eq "/*") {
883                 if (!s-/\*.*?\*/--) {
884                     s-/\*.*--;
885                     $in_comment = 1;
886                 }
887             } elsif ($match eq "//") {
888                 s-//.*--;
889             } else { # ' or "
890                 if (!s-$match([^\\]|\\.)*?$match--) {
891                     if (!s-$match.*\\$--) {
892                         warn "mismatched quotes at line $. in $file_name\n";
893                         s-$match.*--;
894                     } else {
895                         $quotation_mark = $match;
896                     }
897                 }
898             }
899         }
900
901
902         # continued method declaration
903         if ($in_method_declaration) {
904               my $original = $_;
905               my $method_cont = $_;
906
907               chomp $method_cont;
908               $method_cont =~ s/[;\{].*//;
909               $potential_method_spec = "${potential_method_spec} ${method_cont}";
910
911               $_ = $original;
912               if (/;/) {
913                   $potential_start = 0;
914                   $potential_method_spec = "";
915                   $potential_method_char = "";
916                   $in_method_declaration = 0;
917                   s/^[^;\{]*//;
918               } elsif (/{/) {
919                   my $selector = method_decl_to_selector ($potential_method_spec);
920                   $potential_name = "${potential_method_char}\[${interface_name} ${selector}\]";
921
922                   $potential_method_spec = "";
923                   $potential_method_char = "";
924                   $in_method_declaration = 0;
925
926                   $_ = $original;
927                   s/^[^;{]*//;
928               } elsif (/\@end/) {
929                   $in_method_declaration = 0;
930                   $interface_name = "";
931                   $_ = $original;
932               } else {
933                   next;
934               }
935         }
936
937
938         # start of method declaration
939         if ((my $method_char, my $method_spec) = m&^([-+])([^0-9;][^;]*);?$&) {
940             my $original = $_;
941
942             if ($interface_name) {
943                 chomp $method_spec;
944                 $method_spec =~ s/\{.*//;
945
946                 $potential_method_char = $method_char;
947                 $potential_method_spec = $method_spec;
948                 $potential_start = $.;
949                 $in_method_declaration = 1;
950             } else { 
951                 warn "declaring a method but don't have interface on line $. in $file_name\n";
952             }
953             $_ = $original;
954             if (/\{/) {
955               my $selector = method_decl_to_selector ($potential_method_spec);
956               $potential_name = "${potential_method_char}\[${interface_name} ${selector}\]";
957
958               $potential_method_spec = "";
959               $potential_method_char = "";
960               $in_method_declaration = 0;
961               $_ = $original;
962               s/^[^{]*//;
963             } elsif (/\@end/) {
964               $in_method_declaration = 0;
965               $interface_name = "";
966               $_ = $original;
967             } else {
968               next;
969             }
970         }
971
972
973         # Find function, interface and method names.
974         while (m&((?:[[:word:]]+::)*operator(?:[ \t]*\(\)|[^()]*)|[[:word:]<>:~]+|[(){}:;=])|\@(?:implementation|interface|protocol)\s+(\w+)[^{]*&g) {
975             # Skip an array definition at the top level.
976             # e.g. static int arr[] = { 1, 2, 3 };
977             if ($1) {
978                 if ($1 eq "=" and !$in_parentheses and !$in_braces) {
979                     $equal_observed = 1;
980                 } elsif ($1 eq "{" and $equal_observed) {
981                     # This '{' is the beginning of an array definition, not the beginning of a method.
982                     $in_toplevel_array_brace = 1;
983                     $in_braces++;
984                     $equal_observed = 0;
985                     next;
986                 } elsif ($1 !~ /[ \t]/) {
987                     $equal_observed = 0;
988                 }
989             }
990
991             # interface name
992             if ($2) {
993                 $interface_name = $2;
994                 next;
995             }
996
997             # Open parenthesis.
998             if ($1 eq "(") {
999                 $potential_name = $word unless $in_parentheses || $skip_til_brace_or_semicolon || grep { $word eq $_ } ("CF_ENUM", "CF_OPTIONS", "NS_ENUM", "NS_OPTIONS");
1000                 $in_parentheses++;
1001                 next;
1002             }
1003
1004             # Close parenthesis.
1005             if ($1 eq ")") {
1006                 $in_parentheses--;
1007                 next;
1008             }
1009
1010             # C++ constructor initializers
1011             if ($1 eq ":") {
1012                   $skip_til_brace_or_semicolon = 1 unless ($in_parentheses || $in_braces);
1013             }
1014
1015             # Open brace.
1016             if ($1 eq "{") {
1017                 $skip_til_brace_or_semicolon = 0;
1018
1019                 if (!$in_braces) {
1020                     if ($namespace_start >= 0 and $namespace_start < $potential_start) {
1021                         push @ranges, [ $namespace_start . "", $potential_start - 1, $name ];
1022                     }
1023
1024                     if ($potential_namespace) {
1025                         push @namespaces, $potential_namespace;
1026                         push @all_namespaces, $potential_namespace;
1027                         $potential_namespace = "";
1028                         $name = $namespaces[-1];
1029                         $namespace_start = $. + 1;
1030                         next;
1031                     }
1032
1033                     # Promote potential name to real function name at the
1034                     # start of the outer level set of braces (function body?).
1035                     if ($potential_start) {
1036                         $start = $potential_start;
1037                         $name = $potential_name;
1038                         if (@namespaces && $name && (length($name) < 2 || substr($name,1,1) ne "[")) {
1039                             $name = join ('::', @namespaces, $name);
1040                         }
1041                     }
1042                 }
1043
1044                 $in_method_declaration = 0;
1045
1046                 $brace_start = $. if (!$in_braces);
1047                 $in_braces++;
1048                 next;
1049             }
1050
1051             # Close brace.
1052             if ($1 eq "}") {
1053                 if (!$in_braces && @namespaces) {
1054                     if ($namespace_start >= 0 and $namespace_start < $.) {
1055                         push @ranges, [ $namespace_start . "", $. - 1, $name ];
1056                     }
1057
1058                     pop @namespaces;
1059                     if (@namespaces) {
1060                         $name = $namespaces[-1];
1061                         $namespace_start = $. + 1;
1062                     } else {
1063                         $name = "";
1064                         $namespace_start = -1;
1065                     }
1066                     next;
1067                 }
1068
1069                 $in_braces--;
1070                 $brace_end = $. if (!$in_braces);
1071
1072                 # End of an outer level set of braces.
1073                 # This could be a function body.
1074                 if (!$in_braces and $name) {
1075                     # This is the end of an array definition at the top level, not the end of a method.
1076                     if ($in_toplevel_array_brace) {
1077                         $in_toplevel_array_brace = 0;
1078                         next;
1079                     }
1080
1081                     push @ranges, [ $start, $., $name ];
1082                     if (@namespaces) {
1083                         $name = $namespaces[-1];
1084                         $namespace_start = $. + 1;
1085                     } else {
1086                         $name = "";
1087                         $namespace_start = -1;
1088                     }
1089                 }
1090
1091                 $potential_start = 0;
1092                 $potential_name = "";
1093                 next;
1094             }
1095
1096             # Semicolon.
1097             if ($1 eq ";") {
1098                 $skip_til_brace_or_semicolon = 0;
1099                 $potential_start = 0;
1100                 $potential_name = "";
1101                 $in_method_declaration = 0;
1102                 next;
1103             }
1104
1105             # Ignore "const" method qualifier.
1106             if ($1 eq "const") {
1107                 next;
1108             }
1109
1110             if ($1 eq "namespace" || $1 eq "class" || $1 eq "struct") {
1111                 $next_word_could_be_namespace = 1;
1112                 next;
1113             }
1114
1115             # Word.
1116             $word = $1;
1117             if (!$skip_til_brace_or_semicolon) {
1118                 if ($next_word_could_be_namespace) {
1119                     $potential_namespace = $word;
1120                     $next_word_could_be_namespace = 0;
1121                 } elsif ($potential_namespace) {
1122                     $potential_namespace = "";
1123                 }
1124
1125                 if (!$in_parentheses) {
1126                     $potential_start = 0;
1127                     $potential_name = "";
1128                 }
1129                 if (!$potential_start) {
1130                     $potential_start = $.;
1131                     $potential_name = "";
1132                 }
1133             }
1134         }
1135     }
1136
1137     warn "missing close braces in $file_name (probable start at $brace_start)\n" if ($in_braces > 0);
1138     warn "too many close braces in $file_name (probable start at $brace_end)\n" if ($in_braces < 0);
1139
1140     warn "mismatched parentheses in $file_name\n" if $in_parentheses;
1141
1142     return delete_namespaces_from_ranges_for_cpp(@ranges, @all_namespaces);
1143 }
1144
1145
1146 # Take in references to an array of line ranges for C functions in a given file 
1147 # and an array of namespaces declared in that file and return an updated
1148 # list of line ranges with the namespaces removed.
1149
1150 sub delete_namespaces_from_ranges_for_cpp(\@\@)
1151 {
1152     my ($ranges, $namespaces) = @_;
1153     return grep {!is_function_in_namespace($namespaces, $$_[2])} @$ranges;
1154 }
1155
1156
1157 sub is_function_in_namespace($$)
1158 {
1159     my ($namespaces, $function_name) = @_;
1160     return grep {$_ eq $function_name} @$namespaces;
1161 }
1162
1163
1164 # Read a file and get all the line ranges of the things that look like Java
1165 # classes, interfaces and methods.
1166 #
1167 # A class or interface name is the word that immediately follows
1168 # `class' or `interface' when followed by an open curly brace and not
1169 # a semicolon. It can appear at the top level, or inside another class
1170 # or interface block, but not inside a function block
1171 #
1172 # A class or interface starts at the first character after the first close
1173 # brace or after the function name and ends at the close brace.
1174 #
1175 # A function name is the last word before an open parenthesis before
1176 # an open brace rather than a semicolon. It can appear at top level or
1177 # inside a class or interface block, but not inside a function block.
1178 #
1179 # A function starts at the first character after the first close
1180 # brace or after the function name and ends at the close brace.
1181 #
1182 # Comment handling is simple-minded but will work for all but pathological cases.
1183 #
1184 # Result is a list of triples: [ start_line, end_line, function_name ].
1185
1186 sub get_function_line_ranges_for_java($$)
1187 {
1188     my ($file_handle, $file_name) = @_;
1189
1190     my @current_scopes;
1191
1192     my @ranges;
1193
1194     my $in_comment = 0;
1195     my $in_macro = 0;
1196     my $in_parentheses = 0;
1197     my $in_braces = 0;
1198     my $in_non_block_braces = 0;
1199     my $class_or_interface_just_seen = 0;
1200     my $in_class_declaration = 0;
1201
1202     my $word = "";
1203
1204     my $potential_start = 0;
1205     my $potential_name = "";
1206     my $potential_name_is_class_or_interface = 0;
1207
1208     my $start = 0;
1209     my $name = "";
1210     my $current_name_is_class_or_interface = 0;
1211
1212     while (<$file_handle>) {
1213         # Handle continued multi-line comment.
1214         if ($in_comment) {
1215             next unless s-.*\*/--;
1216             $in_comment = 0;
1217         }
1218
1219         # Handle continued macro.
1220         if ($in_macro) {
1221             $in_macro = 0 unless /\\$/;
1222             next;
1223         }
1224
1225         # Handle start of macro (or any preprocessor directive).
1226         if (/^\s*\#/) {
1227             $in_macro = 1 if /^([^\\]|\\.)*\\$/;
1228             next;
1229         }
1230
1231         # Handle comments and quoted text.
1232         while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
1233             my $match = $1;
1234             if ($match eq "/*") {
1235                 if (!s-/\*.*?\*/--) {
1236                     s-/\*.*--;
1237                     $in_comment = 1;
1238                 }
1239             } elsif ($match eq "//") {
1240                 s-//.*--;
1241             } else { # ' or "
1242                 if (!s-$match([^\\]|\\.)*?$match--) {
1243                     warn "mismatched quotes at line $. in $file_name\n";
1244                     s-$match.*--;
1245                 }
1246             }
1247         }
1248
1249         # Find function names.
1250         while (m-(\w+|[(){};])-g) {
1251             # Open parenthesis.
1252             if ($1 eq "(") {
1253                 if (!$in_parentheses) {
1254                     $potential_name = $word;
1255                     $potential_name_is_class_or_interface = 0;
1256                 }
1257                 $in_parentheses++;
1258                 next;
1259             }
1260
1261             # Close parenthesis.
1262             if ($1 eq ")") {
1263                 $in_parentheses--;
1264                 next;
1265             }
1266
1267             # Open brace.
1268             if ($1 eq "{") {
1269                 $in_class_declaration = 0;
1270
1271                 # Promote potential name to real function name at the
1272                 # start of the outer level set of braces (function/class/interface body?).
1273                 if (!$in_non_block_braces
1274                     and (!$in_braces or $current_name_is_class_or_interface)
1275                     and $potential_start) {
1276                     if ($name) {
1277                           push @ranges, [ $start, ($. - 1),
1278                                           join ('.', @current_scopes) ];
1279                     }
1280
1281
1282                     $current_name_is_class_or_interface = $potential_name_is_class_or_interface;
1283
1284                     $start = $potential_start;
1285                     $name = $potential_name;
1286
1287                     push (@current_scopes, $name);
1288                 } else {
1289                     $in_non_block_braces++;
1290                 }
1291
1292                 $potential_name = "";
1293                 $potential_start = 0;
1294
1295                 $in_braces++;
1296                 next;
1297             }
1298
1299             # Close brace.
1300             if ($1 eq "}") {
1301                 $in_braces--;
1302
1303                 # End of an outer level set of braces.
1304                 # This could be a function body.
1305                 if (!$in_non_block_braces) {
1306                     if ($name) {
1307                         push @ranges, [ $start, $.,
1308                                         join ('.', @current_scopes) ];
1309
1310                         pop (@current_scopes);
1311
1312                         if (@current_scopes) {
1313                             $current_name_is_class_or_interface = 1;
1314
1315                             $start = $. + 1;
1316                             $name =  $current_scopes[$#current_scopes-1];
1317                         } else {
1318                             $current_name_is_class_or_interface = 0;
1319                             $start = 0;
1320                             $name =  "";
1321                         }
1322                     }
1323                 } else {
1324                     $in_non_block_braces-- if $in_non_block_braces;
1325                 }
1326
1327                 $potential_start = 0;
1328                 $potential_name = "";
1329                 next;
1330             }
1331
1332             # Semicolon.
1333             if ($1 eq ";") {
1334                 $potential_start = 0;
1335                 $potential_name = "";
1336                 next;
1337             }
1338
1339             if ($1 eq "class") {
1340                 $in_class_declaration = 1;
1341             }
1342             if ($1 eq "class" or (!$in_class_declaration and $1 eq "interface")) {
1343                 $class_or_interface_just_seen = 1;
1344                 next;
1345             }
1346
1347             # Word.
1348             $word = $1;
1349             if (!$in_parentheses) {
1350                 if ($class_or_interface_just_seen) {
1351                     $potential_name = $word;
1352                     $potential_start = $.;
1353                     $class_or_interface_just_seen = 0;
1354                     $potential_name_is_class_or_interface = 1;
1355                     next;
1356                 }
1357             }
1358             if (!$potential_start) {
1359                 $potential_start = $.;
1360                 $potential_name = "";
1361             }
1362             $class_or_interface_just_seen = 0;
1363         }
1364     }
1365
1366     warn "mismatched braces in $file_name\n" if $in_braces;
1367     warn "mismatched parentheses in $file_name\n" if $in_parentheses;
1368
1369     return @ranges;
1370 }
1371
1372
1373
1374 # Read a file and get all the line ranges of the things that look like
1375 # JavaScript functions or methods.
1376 #
1377 # A function name is the word that immediately follows `function' when
1378 # followed by an open curly brace. It can appear at the top level,
1379 # or inside other functions. For example:
1380 #
1381 #    function name() { // (name)
1382 #        function inner() { } // (name.inner)
1383 #    }
1384 #
1385 # An anonymous function name is the identifier on the left hand side of
1386 # an assignment with the equals operator or object notation that has a
1387 # value starting with `function' followed an open curly brace.
1388 # For example:
1389 #
1390 #    namespace = {
1391 #        name: function() {} // (namespace.name)
1392 #    }
1393 #    namespace.Foo = function() {} // (namespace.Foo)
1394 #
1395 # A getter or setter name is the word that immediately follows `get' or
1396 # `set' when followed by params and an open curly brace. For example:
1397 #
1398 #    namespace = {
1399 #      get foo() {} // (namespace.get foo)
1400 #    }
1401 #
1402 # A method name is the word immediately before parenthesis, with an open
1403 # curly brace immediately following closing parenthesis. For a class expression
1404 # we take the assignment identifier instead of the class name for namespacing.
1405 #
1406 #    namespace.Foo = class DoesNotMatter extends Bar {
1407 #        constructor() {} // (namespace.Foo)
1408 #        static staticMethod() {} // (namespace.Foo.staticMethod)
1409 #        instanceMethod() {} // (namespace.Foo.prototype.instanceMethod)
1410 #        get getter() {} // (namespace.Foo.prototype.get getter)
1411 #    }
1412 #    class ClassName {
1413 #        constructor() {} // (ClassName)
1414 #        method() {} // (ClassName.prototype.method)
1415 #    }
1416 #
1417 # Methods may exist in object literals, outside of classes.
1418 #
1419 #   Foo.prototype = {
1420 #       method() {}, // (Foo.prototype.method)
1421 #       otherMethod() {} // (Foo.prototype.otherMethod)
1422 #   }
1423 #
1424 # Comment handling is simple-minded but will work for all but pathological cases.
1425 #
1426 # Result is a list of triples: [ start_line, end_line, function_name ].
1427
1428 sub get_function_line_ranges_for_javascript($$)
1429 {
1430     my ($fileHandle, $fileName) = @_;
1431
1432     my @currentScopes;
1433     my @currentIdentifiers;
1434     my @currentParsingMode = ("global");
1435     my @currentFunctionNames;
1436     my @currentFunctionDepths;
1437     my @currentFunctionStartLines;
1438
1439     my @ranges;
1440
1441     my $inComment = 0;
1442     my $inQuotedText = "";
1443     my $inExtends = 0;
1444     my $inMethod = 0;
1445     my $inAnonymousFunctionParameters = 0;
1446     my $parenthesesDepth = 0;
1447     my $globalParenthesesDepth = 0;
1448     my $bracesDepth = 0;
1449
1450     my $classJustSeen = 0;
1451     my $parenthesisJustSeen = 0;
1452     my $functionJustSeen = 0;
1453     my $getterJustSeen = 0;
1454     my $setterJustSeen = 0;
1455     my $assignmentJustSeen = 0;
1456     my $staticOrContructorSeen = 0;
1457
1458     my $currentToken = "";
1459     my $lastToken = "";
1460     my $possibleMethodName = "";
1461     my $word = "";
1462
1463     while (<$fileHandle>) {
1464         # Handle continued multi-line comment.
1465         if ($inComment) {
1466             next unless s-.*\*/--;
1467             $inComment = 0;
1468         }
1469
1470         # Handle continued quoted text.
1471         if ($inQuotedText ne "") {
1472             next if /\\$/;
1473             s-([^\\]|\\.)*?$inQuotedText--;
1474             $inQuotedText = "";
1475         }
1476
1477         # Handle comments and quoted text.
1478         while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
1479             my $match = $1;
1480             if ($match eq '/*') {
1481                 if (!s-/\*.*?\*/--) {
1482                     s-/\*.*--;
1483                     $inComment = 1;
1484                 }
1485             } elsif ($match eq '//') {
1486                 s-//.*--;
1487             } else { # ' or "
1488                 if (!s-$match([^\\]|\\.)*?$match-string_appeared_here-) {
1489                     $inQuotedText = $match if /\\$/;
1490                     warn "mismatched quotes at line $. in $fileName\n" if $inQuotedText eq "";
1491                     s-$match.*--;
1492                 }
1493             }
1494         }
1495
1496         # Find function names.
1497         while (m-(\w+|[(){}=:;,.])-g) {
1498             # Skip everything until "{" after extends.
1499             if ($inExtends) {
1500                 next if $1 ne '{';
1501                 $inExtends = 0;
1502             }
1503
1504             $lastToken = $currentToken;
1505             $currentToken = $1;
1506
1507             # Open parenthesis.
1508             if ($1 eq '(') {
1509                 $parenthesesDepth++;
1510                 $globalParenthesesDepth++ if $currentParsingMode[$#currentParsingMode] eq "global";
1511                 $possibleMethodName = join('.', @currentIdentifiers);
1512                 $inAnonymousFunctionParameters = 1 if $functionJustSeen;
1513                 $functionJustSeen = 0;
1514                 next;
1515             }
1516
1517             # Close parenthesis.
1518             if ($1 eq ')') {
1519                 $parenthesesDepth--;
1520                 $globalParenthesesDepth-- if $currentParsingMode[$#currentParsingMode] eq "global";
1521                 @currentIdentifiers = () if $inAnonymousFunctionParameters;
1522                 $inAnonymousFunctionParameters = 0;
1523                 $parenthesisJustSeen = 1;
1524                 next;
1525             }
1526
1527             # Open brace.
1528             if ($1 eq '{') {
1529                 my $methodName = "";
1530                 my $mode = $currentParsingMode[$#currentParsingMode];
1531
1532                 # Method.
1533                 if (($mode eq 'class' or $mode eq 'global') and $parenthesisJustSeen and ($staticOrContructorSeen or $possibleMethodName)) {
1534                     if ($mode eq 'class') {
1535                         $methodName = join('.', $staticOrContructorSeen ? "" : "prototype", $possibleMethodName);
1536                     } else {
1537                         $methodName = $possibleMethodName;
1538                     }
1539
1540                     $methodName =~ s/\.{2,}/\./g; # Removes consecutive periods.
1541                     $methodName =~ s/\.$//; # Remove trailing period.
1542
1543                     my $currentMethod = join('.', @currentScopes, $methodName);
1544                     $currentMethod =~ s/\.{2,}/\./g; # Removes consecutive periods.
1545                     $currentMethod =~ s/\.$//; # Remove trailing period.
1546
1547                     push(@currentParsingMode, "method");
1548                     push(@currentFunctionNames, $currentMethod);
1549                     push(@currentFunctionDepths, $bracesDepth);
1550                     push(@currentFunctionStartLines, $.);
1551                 }
1552
1553                 $bracesDepth++;
1554                 $functionJustSeen = 0;
1555
1556                 push(@currentScopes, join('.', $methodName ? $methodName : @currentIdentifiers));
1557                 @currentIdentifiers = ();
1558
1559                 $staticOrContructorSeen = 0;
1560                 next;
1561             }
1562
1563             # Close brace.
1564             if ($1 eq '}') {
1565                 $bracesDepth--;
1566                 $functionJustSeen = 0;
1567
1568                 if (@currentFunctionDepths and $bracesDepth == $currentFunctionDepths[$#currentFunctionDepths]) {
1569                     pop(@currentFunctionDepths);
1570                     pop(@currentParsingMode);
1571
1572                     my $currentName = pop(@currentFunctionNames);
1573                     my $start = pop(@currentFunctionStartLines);
1574
1575                     $currentName =~ s/^\.//g; # Removes leading periods.
1576
1577                     push(@ranges, [$start, $., $currentName]);
1578                 }
1579
1580                 pop(@currentScopes);
1581                 @currentIdentifiers = ();
1582
1583                 next;
1584             }
1585
1586             # Dot.
1587             if ($1 eq '.') {
1588                 next;
1589             }
1590
1591             # Semicolon or comma.
1592             if ($1 eq ';' or $1 eq ',') {
1593                 @currentIdentifiers = ();
1594                 next;
1595             }
1596
1597             # Class.
1598             if ($1 eq 'class') {
1599                 $classJustSeen = 1;
1600                 next;
1601             }
1602
1603             # Extends.
1604             if ($1 eq 'extends') {
1605                 $inExtends = 1;
1606                 next;
1607             }
1608
1609             # Function.
1610             if ($1 eq 'function') {
1611                 $functionJustSeen = 1;
1612
1613                 if ($assignmentJustSeen) {
1614                     my $currentFunction = join('.', (@currentScopes, @currentIdentifiers));
1615                     $currentFunction =~ s/\.{2,}/\./g; # Removes consecutive periods.
1616
1617                     push(@currentParsingMode, "function");
1618                     push(@currentFunctionNames, $currentFunction);
1619                     push(@currentFunctionDepths, $bracesDepth);
1620                     push(@currentFunctionStartLines, $.);
1621                 }
1622
1623                 next;
1624             }
1625
1626             # Getter prefix.
1627             if ($1 eq 'get') {
1628                 next if $lastToken eq '.'; # Avoid map.get(...).
1629                 $getterJustSeen = 1;
1630                 next;
1631             }
1632
1633             # Setter prefix.
1634             if ($1 eq 'set') {
1635                 next if $lastToken eq '.'; # Avoid map.set(...).
1636                 $setterJustSeen = 1;
1637                 next;
1638             }
1639
1640             # Static.
1641             if ($1 eq 'static' or $1 eq 'constructor') {
1642                 $staticOrContructorSeen = 1;
1643                 next;
1644             }
1645
1646             # Assignment operator.
1647             if ($1 eq '=' or $1 eq ':') {
1648                 $assignmentJustSeen = 1;
1649                 next;
1650             }
1651
1652             next if $parenthesesDepth > $globalParenthesesDepth;
1653
1654             # Word.
1655             $word = $1;
1656
1657             if ($classJustSeen) {
1658                 push(@currentIdentifiers, $word) if !$assignmentJustSeen;
1659
1660                 my $currentClass = join('.', (@currentScopes, @currentIdentifiers));
1661                 $currentClass =~ s/\.{2,}/\./g; # Removes consecutive periods.
1662
1663                 push(@currentParsingMode, "class");
1664                 push(@currentFunctionNames, $currentClass);
1665                 push(@currentFunctionDepths, $bracesDepth);
1666                 push(@currentFunctionStartLines, $.);
1667             } elsif ($getterJustSeen or $setterJustSeen) {
1668                 $word = "get $word" if $getterJustSeen;
1669                 $word = "set $word" if $setterJustSeen;
1670
1671                 push(@currentIdentifiers, $word);
1672
1673                 my $mode = $currentParsingMode[$#currentParsingMode];
1674                 my $currentFunction = join('.', (@currentScopes, ($mode eq 'class') ? "prototype" : "", @currentIdentifiers));
1675                 $currentFunction =~ s/\.{2,}/\./g; # Removes consecutive periods.
1676
1677                 push(@currentParsingMode, "function");
1678                 push(@currentFunctionNames, $currentFunction);
1679                 push(@currentFunctionDepths, $bracesDepth);
1680                 push(@currentFunctionStartLines, $.);
1681             } elsif ($functionJustSeen and !$assignmentJustSeen) {
1682                 push(@currentIdentifiers, $word);
1683
1684                 my $currentFunction = join('.', (@currentScopes, @currentIdentifiers));
1685                 $currentFunction =~ s/\.{2,}/\./g; # Removes consecutive periods.
1686
1687                 push(@currentParsingMode, "function");
1688                 push(@currentFunctionNames, $currentFunction);
1689                 push(@currentFunctionDepths, $bracesDepth);
1690                 push(@currentFunctionStartLines, $.);
1691             } elsif ($word ne 'if' and $word ne 'for' and $word ne 'do' and $word ne 'while' and $word ne 'which' and $word ne 'var') {
1692                 push(@currentIdentifiers, $word);
1693             }
1694
1695             $classJustSeen = 0;
1696             $parenthesisJustSeen = 0;
1697             $functionJustSeen = 0;
1698             $getterJustSeen = 0;
1699             $setterJustSeen = 0;
1700             $assignmentJustSeen = 0;
1701         }
1702     }
1703
1704     warn "mismatched braces in $fileName\n" if $bracesDepth;
1705     warn "mismatched parentheses in $fileName\n" if $parenthesesDepth;
1706
1707     return @ranges;
1708 }
1709
1710 # Read a file and get all the line ranges of the things that look like Perl functions. Functions
1711 # start on a line that starts with "sub ", and end on the first line starting with "}" thereafter.
1712 #
1713 # Result is a list of triples: [ start_line, end_line, function ].
1714
1715 sub get_function_line_ranges_for_perl($$)
1716 {
1717     my ($fileHandle, $fileName) = @_;
1718
1719     my @ranges;
1720
1721     my $currentFunction = "";
1722     my $start = 0;
1723     my $hereDocumentIdentifier = "";
1724
1725     while (<$fileHandle>) {
1726         chomp;
1727         if (!$hereDocumentIdentifier) {
1728             if (/^sub\s+([\w_][\w\d_]*)/) {
1729                 # Skip over forward declarations, which don't contain a brace and end with a semicolon.
1730                 next if /;\s*$/;
1731
1732                 if ($currentFunction) {
1733                     warn "nested functions found at top-level at $fileName:$.\n";
1734                     next;
1735                 }
1736                 $currentFunction = $1;
1737                 $start = $.;
1738             }
1739             if (/<<\s*[\"\']?([\w_][\w_\d]*)/) {
1740                 # Enter here-document.
1741                 $hereDocumentIdentifier = $1;
1742             }
1743             if (index($_, "}") == 0) {
1744                 next unless $start;
1745                 push(@ranges, [$start, $., $currentFunction]);
1746                 $currentFunction = "";
1747                 $start = 0;
1748             }
1749         } elsif ($_ eq $hereDocumentIdentifier) {
1750             # Escape from here-document.
1751             $hereDocumentIdentifier = "";
1752         }
1753     }
1754
1755     return @ranges;
1756 }
1757
1758 # Read a file and get all the line ranges of the things that look like Python classes, methods, or functions.
1759 #
1760 # FIXME: Maybe we should use Python's ast module to do the parsing for us?
1761 #
1762 # Result is a list of triples: [ start_line, end_line, function ].
1763
1764 sub get_function_line_ranges_for_python($$)
1765 {
1766     my ($fileHandle, $fileName) = @_;
1767
1768     my @ranges;
1769
1770     my @scopeStack = ({ line => 0, indent => -1, name => undef });
1771     my $lastLine = 0;
1772     until ($lastLine) {
1773         $_ = <$fileHandle>;
1774         unless ($_) {
1775             # To pop out all popped scopes, run the loop once more after
1776             # we encountered the end of the file.
1777             $_ = "pass\n";
1778             $.++;
1779             $lastLine = 1;
1780         }
1781         chomp;
1782         next unless /^(\s*)([^#].*)$/;
1783
1784         my $indent = length $1;
1785         my $rest = $2;
1786         my $scope = $scopeStack[-1];
1787
1788         if ($indent <= $scope->{indent}) {
1789             # Find all the scopes that we have just exited.
1790             my $i = 0;
1791             for (; $i < @scopeStack; ++$i) {
1792                 last if $indent <= $scopeStack[$i]->{indent};
1793             }
1794             my @poppedScopes = splice @scopeStack, $i;
1795
1796             # For each scope that was just exited, add a range that goes from the start of that
1797             # scope to the start of the next nested scope, or to the line just before this one for
1798             # the innermost scope.
1799             for ($i = 0; $i < @poppedScopes; ++$i) {
1800                 my $lineAfterEnd = $i + 1 == @poppedScopes ? $. : $poppedScopes[$i + 1]->{line};
1801                 push @ranges, [$poppedScopes[$i]->{line}, $lineAfterEnd - 1, $poppedScopes[$i]->{name}];
1802             }
1803             @scopeStack or warn "Popped off last scope at $fileName:$.\n";
1804
1805             # Set the now-current scope to start at the current line. Any lines within this scope
1806             # before this point should already have been added to @ranges.
1807             $scope = $scopeStack[-1];
1808             $scope->{line} = $.;
1809         }
1810
1811         next unless $rest =~ /(?:class|def)\s+(\w+)/;
1812         my $name = $1;
1813         my $fullName = $scope->{name} ? join('.', $scope->{name}, $name) : $name;
1814         push @scopeStack, { line => $., indent => $indent, name => $fullName };
1815
1816         if ($scope->{indent} >= 0) {
1817             push @ranges, [$scope->{line}, $. - 1, $scope->{name}];
1818         }
1819     }
1820
1821     return @ranges;
1822 }
1823
1824 # Read a file and get all the line ranges of the things that look like CSS selectors.  A selector is
1825 # anything before an opening brace on a line. A selector starts at the line containing the opening
1826 # brace and ends at the closing brace.
1827 #
1828 # Result is a list of triples: [ start_line, end_line, selector ].
1829
1830 sub get_selector_line_ranges_for_css($$)
1831 {
1832     my ($fileHandle, $fileName) = @_;
1833
1834     my @ranges;
1835
1836     my $inComment = 0;
1837     my $inBrace = 0;
1838     my @stack;
1839     my $context;
1840     my @currentParseMode = ("global");
1841     my $selectorBraces = 0;
1842
1843     while (<$fileHandle>) {
1844         foreach my $token (split m-(\{|\}|/\*|\*/)-, $_) {
1845             if ($token eq "{") {
1846                 if (!$inComment) {
1847                     $inBrace += 1;                    
1848                     $selectorBraces += 1 if $currentParseMode[$#currentParseMode] eq "selector";
1849                     warn "mismatched opening brace found in $fileName:$.\n" if $selectorBraces > 1;
1850                 }
1851             } elsif ($token eq "}") {
1852                 if (!$inComment) {
1853                     if (!$inBrace or $currentParseMode[$#currentParseMode] eq "global") {
1854                         warn "mismatched closing brace found in $fileName:$.\n";
1855                         next;
1856                     }
1857
1858                     $inBrace -= 1;
1859
1860                     pop(@currentParseMode);
1861                     my $name = pop(@stack);
1862                     my $startLine = pop(@stack);
1863                     my $endLine = $.;
1864                     push(@ranges, [$startLine, $endLine, $name]);
1865                     $selectorBraces = 0;
1866                 }
1867             } elsif ($token eq "/*") {
1868                 $inComment = 1;
1869             } elsif ($token eq "*/") {
1870                 warn "mismatched comment found in $fileName:$.\n" if !$inComment;
1871                 $inComment = 0;
1872             } else {
1873                 if (!$inComment and $currentParseMode[$#currentParseMode] ne "selector" and $token !~ /^[\s\t]*$/) {
1874                     $token =~ s/^[\s\t]*|[\s\t]*$//g;
1875                     my $startLine = $.;
1876                     if ($token =~ /^\@media/) {
1877                         push(@currentParseMode, "media");
1878                         push(@stack, ($startLine, $token));
1879                     } else {
1880                         push(@currentParseMode, "selector");
1881                         push(@stack, ($startLine, $token));
1882                     }
1883                 }
1884             }
1885         }
1886     }
1887
1888     # Sort by start line.
1889     return sort {$a->[0] <=> $b->[0]} @ranges;
1890 }
1891
1892 # Read a file and get all the line ranges of the things that look like Swift classes, methods,
1893 # or functions.
1894 #
1895 # Result is a list of triples: [ start_line, end_line, function ].
1896
1897 sub get_function_line_ranges_for_swift($$)
1898 {
1899     my ($fileHandle, $fileName) = @_;
1900
1901     my @ranges;
1902
1903     my $currentFunction = "";
1904     my $currentClass = "";
1905     my $functionStart = 0;
1906     my $classStart = 0;
1907     my $functionScopeDepth = 0;
1908     my $classScopeDepth = 0;
1909     my $scopeDepth = 0;
1910
1911     while (<$fileHandle>) {
1912         chomp;
1913         next if (/^\s*\/\/.*/);
1914         if (/func\s+([\w_][\w\d_]*)\((.*)\)/ || /var\s+([\w_][\w\d_]*):\s+/) {
1915             $functionScopeDepth = $scopeDepth;
1916             $currentFunction = $1;
1917             if ($2) {
1918                 $currentFunction = "$currentFunction(". parseSwiftFunctionArgs($2) . ")";
1919             }
1920             if ($currentClass) {
1921                 $currentFunction = "$currentClass.$currentFunction";
1922             }
1923             $functionStart = $.;
1924         } elsif (/class\s+([\w_][\w\d_]*)/) {
1925             $classScopeDepth = $scopeDepth;
1926             $currentClass = $1;
1927             $classStart = $.;
1928         }
1929         if (index($_, "{") > -1) {
1930             $scopeDepth++;
1931         }
1932         if (index($_, "}") > -1) {
1933             $scopeDepth--;
1934         }
1935         if ($scopeDepth == $functionScopeDepth) {
1936             next unless $functionStart;
1937             push(@ranges, [$functionStart, $., $currentFunction]);
1938             $currentFunction = "";
1939             $functionStart = 0;
1940         } elsif ($scopeDepth == $classScopeDepth) {
1941             next unless $classStart;
1942             $currentClass = "";
1943             $classStart = 0;
1944         }
1945     }
1946
1947     return @ranges;
1948 }
1949
1950 sub parseSwiftFunctionArgs($)
1951 {
1952     my ($functionArgs) = @_;
1953     my @words = split /, /, $functionArgs;
1954     my $argCount = scalar(@words);
1955     if ($argCount == 0) {
1956         return "";
1957     } elsif ($argCount > 0) {
1958         # If the first argument is unnamed, give it the name "_"
1959         $words[0] =~ s/^(\w+: .*)/_ $1/;
1960         return join("", map { $_ =~ s/^(\w+).*/$1/; "$_:" } @words);
1961     } else {
1962         warn "Unknown argument count.\n";
1963     }
1964 }
1965
1966 sub processPaths(\@)
1967 {
1968     my ($paths) = @_;
1969     return ("." => 1) if (!@{$paths});
1970
1971     my %result = ();
1972
1973     for my $file (@{$paths}) {
1974         die "can't handle absolute paths like \"$file\"\n" if File::Spec->file_name_is_absolute($file);
1975         die "can't handle empty string path\n" if $file eq "";
1976         die "can't handle path with single quote in the name like \"$file\"\n" if $file =~ /'/; # ' (keep Xcode syntax highlighting happy)
1977
1978         my $untouchedFile = $file;
1979
1980         $file = canonicalizePath($file);
1981
1982         die "can't handle paths with .. like \"$untouchedFile\"\n" if $file =~ m|/\.\./|;
1983
1984         $result{$file} = 1;
1985     }
1986
1987     return ("." => 1) if ($result{"."});
1988
1989     # Remove any paths that also have a parent listed.
1990     for my $path (keys %result) {
1991         for (my $parent = dirname($path); $parent ne '.'; $parent = dirname($parent)) {
1992             if ($result{$parent}) {
1993                 delete $result{$path};
1994                 last;
1995             }
1996         }
1997     }
1998
1999     return %result;
2000 }
2001
2002 sub diffFromToString($$$)
2003 {
2004     my ($gitCommit, $gitIndex, $mergeBase) = @_;
2005
2006     return "" if isSVN();
2007     return $gitCommit if $gitCommit =~ m/.+\.\..+/;
2008     return "\"$gitCommit^\" \"$gitCommit\"" if $gitCommit;
2009     return "--cached" if $gitIndex;
2010     return $mergeBase if $mergeBase;
2011     return "HEAD" if isGit();
2012 }
2013
2014 sub diffCommand($$$$)
2015 {
2016     my ($paths, $gitCommit, $gitIndex, $mergeBase) = @_;
2017
2018     # The function overlap detection logic in computeModifiedFunctions() assumes that its line
2019     # ranges were from a unified diff without any context lines.
2020     my $command;
2021     if (isSVN()) {
2022         my @escapedPaths = map(escapeSubversionPath($_), @$paths);
2023         my $escapedPathsString = qq(") . join(qq(" "), @escapedPaths) . qq(");
2024         $command = SVN . " diff --diff-cmd diff -x -U0 $escapedPathsString";
2025     } elsif (isGit()) {
2026         my $pathsString = "'" . join("' '", @$paths) . "'"; 
2027         $command = GIT . " diff --no-ext-diff -U0 " . diffFromToString($gitCommit, $gitIndex, $mergeBase);
2028         $command .= " -- $pathsString" unless $gitCommit or $mergeBase;
2029     }
2030
2031     return $command;
2032 }
2033
2034 sub statusCommand($$$$)
2035 {
2036     my ($paths, $gitCommit, $gitIndex, $mergeBase) = @_;
2037
2038     my $command;
2039     if (isSVN()) {
2040         my @escapedFiles = map(escapeSubversionPath($_), keys %$paths);
2041         my $escapedFilesString = qq(") . join(qq(" "), @escapedFiles) . qq(");
2042         $command = SVN . " stat $escapedFilesString";
2043     } elsif (isGit()) {
2044         my $filesString = '"' . join('" "', keys %$paths) . '"';
2045         $command = GIT . " diff -r --name-status -M -C " . diffFromToString($gitCommit, $gitIndex, $mergeBase);
2046         $command .= " -- $filesString" unless $gitCommit;
2047     }
2048
2049     return "$command 2>&1";
2050 }
2051
2052 sub attributeCommand($$)
2053 {
2054     my ($file, $attr) = @_;
2055
2056     my $result;
2057     if (isSVN()) {
2058         my $foundAttribute = 0;
2059         my $subPath = ".";
2060         my (@directoryParts) = File::Spec->splitdir($file);
2061         foreach my $part (@directoryParts) {
2062             if ($part eq ".") {
2063                 next;
2064             }
2065             $subPath = File::Spec->join($subPath, $part);
2066             $subPath =~ s/^\.\///;
2067             if ($foundAttribute || exists $attributeCache{$attr}{$subPath} && $attributeCache{$attr}{$subPath} eq "1") {
2068                 $attributeCache{$attr}{$subPath} = "1";
2069                 $foundAttribute = 1;
2070                 next;
2071             }
2072             my $command = SVN . " propget $attr '$subPath'";
2073             my $attrib = $attributeCache{$attr}{$subPath} || `$command 2> $devNull`;
2074             chomp $attrib;
2075             if ($attrib eq "1") {
2076                 $foundAttribute = 1;
2077             }
2078             $attributeCache{$attr}{$subPath} = $attrib || "0";
2079         }
2080         $result = $attributeCache{$attr}{$file};
2081     } elsif (isGit()) {
2082         my $command = GIT . " check-attr $attr -- $file";
2083         $result = `$command`;
2084         chomp $result;
2085         $result =~ s/.*\W(\w)/$1/;
2086     }
2087
2088     $result =~ s/\D//g;
2089     return int($result || 0);
2090 }
2091
2092 sub createPatchCommand($$$$)
2093 {
2094     my ($changedFilesString, $gitCommit, $gitIndex, $mergeBase) = @_;
2095
2096     my $command;
2097     if (isSVN()) {
2098         $command = "'$FindBin::Bin/svn-create-patch --no-style' $changedFilesString";
2099     } elsif (isGit()) {
2100         $command = GIT . " diff -M -C " . diffFromToString($gitCommit, $gitIndex, $mergeBase);
2101         $command .= " -- $changedFilesString" unless $gitCommit;
2102     }
2103
2104     return $command;
2105 }
2106
2107 sub findOriginalFileFromSvn($)
2108 {
2109     my ($file) = @_;
2110     my $baseUrl;
2111     open INFO, SVN . " info . |" or die;
2112     while (<INFO>) {
2113         if (/^URL: (.+?)[\r\n]*$/) {
2114             $baseUrl = $1;
2115         }
2116     }
2117     close INFO;
2118     my $sourceFile;
2119     my $escapedFile = escapeSubversionPath($file);
2120     open INFO, SVN . " info '$escapedFile' |" or die;
2121     while (<INFO>) {
2122         if (/^Copied From URL: (.+?)[\r\n]*$/) {
2123             $sourceFile = File::Spec->abs2rel($1, $baseUrl);
2124         }
2125     }
2126     close INFO;
2127     return $sourceFile;
2128 }
2129
2130 sub determinePropertyChanges($$$)
2131 {
2132     my ($file, $isAdd, $original) = @_;
2133
2134     my $escapedFile = escapeSubversionPath($file);
2135     my %changes;
2136     if ($isAdd) {
2137         my %addedProperties;
2138         my %removedProperties;
2139         open PROPLIST, SVN . " proplist '$escapedFile' |" or die;
2140         while (<PROPLIST>) {
2141             $addedProperties{$1} = 1 if /^  (.+?)[\r\n]*$/ && $1 ne 'svn:mergeinfo';
2142         }
2143         close PROPLIST;
2144         if ($original) {
2145             my $escapedOriginal = escapeSubversionPath($original);
2146             open PROPLIST, SVN . " proplist '$escapedOriginal' |" or die;
2147             while (<PROPLIST>) {
2148                 next unless /^  (.+?)[\r\n]*$/;
2149                 my $property = $1;
2150                 if (exists $addedProperties{$property}) {
2151                     delete $addedProperties{$1};
2152                 } else {
2153                     $removedProperties{$1} = 1;
2154                 }
2155             }
2156         }
2157         $changes{"A"} = [sort keys %addedProperties] if %addedProperties;
2158         $changes{"D"} = [sort keys %removedProperties] if %removedProperties;
2159     } else {
2160         open DIFF, SVN . " diff '$escapedFile' |" or die;
2161         while (<DIFF>) {
2162             if (/^Property changes on:/) {
2163                 while (<DIFF>) {
2164                     my $operation;
2165                     my $property;
2166                     if (/^Added: (\S*)/) {
2167                         $operation = "A";
2168                         $property = $1;
2169                     } elsif (/^Modified: (\S*)/) {
2170                         $operation = "M";
2171                         $property = $1;
2172                     } elsif (/^Deleted: (\S*)/) {
2173                         $operation = "D";
2174                         $property = $1;
2175                     } elsif (/^Name: (\S*)/) {
2176                         # Older versions of svn just say "Name" instead of the type
2177                         # of property change.
2178                         $operation = "C";
2179                         $property = $1;
2180                     }
2181                     if ($operation) {
2182                         $changes{$operation} = [] unless exists $changes{$operation};
2183                         push @{$changes{$operation}}, $property;
2184                     }
2185                 }
2186             }
2187         }
2188         close DIFF;
2189     }
2190     return \%changes;
2191 }
2192
2193 sub pluralizeAndList($$@)
2194 {
2195     my ($singular, $plural, @items) = @_;
2196
2197     return if @items == 0;
2198     return "$singular $items[0]" if @items == 1;
2199     return "$plural " . join(", ", @items[0 .. $#items - 1]) . " and " . $items[-1];
2200 }
2201
2202 sub generateFileList(\%$$$)
2203 {
2204     my ($paths, $gitCommit, $gitIndex, $mergeBase) = @_;
2205
2206     my @changedFiles;
2207     my @conflictFiles;
2208     my %functionLists;
2209     my @addedRegressionTests;
2210     my @requiresTests;
2211     print STDERR "  Running status to find changed, added, or removed files.\n";
2212     open STAT, "-|", statusCommand($paths, $gitCommit, $gitIndex, $mergeBase) or die "The status failed: $!.\n";
2213     while (<STAT>) {
2214         my $status;
2215         my $propertyStatus;
2216         my $propertyChanges;
2217         my $original;
2218         my $file;
2219
2220         if (isSVN()) {
2221             my $matches;
2222             if (isSVNVersion16OrNewer()) {
2223                 $matches = /^([ ACDMR])([ CM]).{5} (.+?)[\r\n]*$/;
2224                 $status = $1;
2225                 $propertyStatus = $2;
2226                 $file = $3;
2227             } else {
2228                 $matches = /^([ ACDMR])([ CM]).{4} (.+?)[\r\n]*$/;
2229                 $status = $1;
2230                 $propertyStatus = $2;
2231                 $file = $3;
2232             }
2233             if ($matches) {
2234                 $file = normalizePath($file);
2235                 $original = findOriginalFileFromSvn($file) if substr($_, 3, 1) eq "+";
2236                 my $isAdd = isAddedStatus($status);
2237                 $propertyChanges = determinePropertyChanges($file, $isAdd, $original) if isModifiedStatus($propertyStatus) || $isAdd;
2238             } else {
2239                 print;  # error output from svn stat
2240             }
2241         } elsif (isGit()) {
2242             if (/^([ADM])\t(.+)$/) {
2243                 $status = $1;
2244                 $propertyStatus = " ";  # git doesn't have properties
2245                 $file = normalizePath($2);
2246             } elsif (/^([CR])[0-9]{1,3}\t([^\t]+)\t([^\t\n]+)$/) { # for example: R90%    newfile    oldfile
2247                 $status = $1;
2248                 $propertyStatus = " ";
2249                 $original = normalizePath($2);
2250                 $file = normalizePath($3);
2251             } else {
2252                 print;  # error output from git diff
2253             }
2254         }
2255
2256         next if !$status || isUnmodifiedStatus($status) && isUnmodifiedStatus($propertyStatus);
2257
2258         $file = makeFilePathRelative($file);
2259
2260         if (isModifiedStatus($status) || isAddedStatus($status) || isModifiedStatus($propertyStatus)) {
2261             my @components = File::Spec->splitdir($file);
2262             if ($components[0] eq "LayoutTests") {
2263                 push @addedRegressionTests, $file
2264                     if isAddedStatus($status)
2265                        && $file =~ /\.([a-zA-Z]+)$/
2266                        && SupportedTestExtensions->{lc($1)}
2267                        && $file !~ /-expected(-mismatch)?\.html$/
2268                        && !scalar(grep(/^resources$/i, @components))
2269                        && !scalar(grep(/^script-tests$/i, @components));
2270             } elsif (attributeCommand($file, "test")) {
2271                 push @addedRegressionTests, $file;
2272             } elsif (attributeCommand($file, "requiresTests")) {
2273                 push @requiresTests, $file
2274             }
2275             push @changedFiles, $file if $components[$#components] ne "ChangeLog";
2276         } elsif (isConflictStatus($status, $gitCommit, $gitIndex) || isConflictStatus($propertyStatus, $gitCommit, $gitIndex)) {
2277             push @conflictFiles, $file;
2278         }
2279         if (basename($file) ne "ChangeLog") {
2280             my $description = statusDescription($status, $propertyStatus, $original, $propertyChanges);
2281             $functionLists{$file} = $description if defined $description;
2282         }
2283     }
2284     close STAT;
2285     return (\@changedFiles, \@conflictFiles, \%functionLists, \@addedRegressionTests, \@requiresTests);
2286 }
2287
2288 sub isUnmodifiedStatus($)
2289 {
2290     my ($status) = @_;
2291
2292     my %statusCodes = (
2293         " " => 1,
2294     );
2295
2296     return $statusCodes{$status};
2297 }
2298
2299 sub isModifiedStatus($)
2300 {
2301     my ($status) = @_;
2302
2303     my %statusCodes = (
2304         "M" => 1,
2305     );
2306
2307     return $statusCodes{$status};
2308 }
2309
2310 sub isAddedStatus($)
2311 {
2312     my ($status) = @_;
2313
2314     my %statusCodes = (
2315         "A" => 1,
2316         "C" => isGit(),
2317         "R" => 1,
2318     );
2319
2320     return $statusCodes{$status};
2321 }
2322
2323 sub isConflictStatus($$$)
2324 {
2325     my ($status, $gitCommit, $gitIndex) = @_;
2326
2327     my %svn = (
2328         "C" => 1,
2329     );
2330
2331     my %git = (
2332         "U" => 1,
2333     );
2334
2335     return 0 if ($gitCommit || $gitIndex); # an existing commit or staged change cannot have conflicts
2336     return $svn{$status} if isSVN();
2337     return $git{$status} if isGit();
2338 }
2339
2340 sub statusDescription($$$$)
2341 {
2342     my ($status, $propertyStatus, $original, $propertyChanges) = @_;
2343
2344     my $propertyDescription = defined $propertyChanges ? propertyChangeDescription($propertyChanges) : "";
2345
2346     my %svn = (
2347         "A" => defined $original ? sprintf(" Copied from \%s.", $original) : " Added.",
2348         "D" => " Removed.",
2349         "M" => "",
2350         "R" => defined $original ? sprintf(" Replaced with \%s.", $original) : " Replaced.",
2351         " " => "",
2352     );
2353
2354     my %git = %svn;
2355     $git{"A"} = " Added.";
2356     if (defined $original) {
2357         $git{"C"} = sprintf(" Copied from \%s.", $original);
2358         $git{"R"} = sprintf(" Renamed from \%s.", $original);
2359     }
2360
2361     my $description;
2362     $description = $svn{$status} if isSVN() && exists $svn{$status};
2363     $description = $git{$status} if isGit() && exists $git{$status};
2364     return unless defined $description;
2365
2366     $description .= $propertyDescription unless isAddedStatus($status);
2367     return $description;
2368 }
2369
2370 sub propertyChangeDescription($)
2371 {
2372     my ($propertyChanges) = @_;
2373
2374     my %operations = (
2375         "A" => "Added",
2376         "M" => "Modified",
2377         "D" => "Removed",
2378         "C" => "Changed",
2379     );
2380
2381     my $description = "";
2382     while (my ($operation, $properties) = each %$propertyChanges) {
2383         my $word = $operations{$operation};
2384         my $list = pluralizeAndList("property", "properties", @$properties);
2385         $description .= " $word $list.";
2386     }
2387     return $description;
2388 }
2389
2390 sub extractLineRangeAfterChange($)
2391 {
2392     my ($string) = @_;
2393     my $chunkRange = parseChunkRange($string);
2394     if (!$chunkRange) {
2395         return (-1, -1); # Malformed
2396     }
2397     if (!$chunkRange->{newStartingLine} || !$chunkRange->{newLineCount}) {
2398          # Deletion; no lines exist after change.
2399         return ($chunkRange->{newStartingLine}, $chunkRange->{newStartingLine});
2400     }
2401     return ($chunkRange->{newStartingLine}, $chunkRange->{newStartingLine} + $chunkRange->{newLineCount} - 1);
2402 }
2403
2404 sub extractLineRangeBeforeChange($)
2405 {
2406     my ($string) = @_;
2407     my $chunkRange = parseChunkRange($string);
2408     if (!$chunkRange) {
2409         return (-1, -1); # Malformed
2410     }
2411     if (!$chunkRange->{startingLine} || !$chunkRange->{lineCount}) {
2412         # Addition; no lines existed before change.
2413         return ($chunkRange->{startingLine}, $chunkRange->{startingLine});
2414     }
2415     return ($chunkRange->{startingLine}, $chunkRange->{startingLine} + $chunkRange->{lineCount} - 1);
2416 }
2417
2418 sub testListForChangeLog(@)
2419 {
2420     my (@tests) = @_;
2421
2422     return "" unless @tests;
2423
2424     my $leadString = "        Test" . (@tests == 1 ? "" : "s") . ": ";
2425     my $list = $leadString;
2426     foreach my $i (0..$#tests) {
2427         $list .= " " x length($leadString) if $i;
2428         my $test = $tests[$i];
2429         $test =~ s/^LayoutTests\///;
2430         $list .= "$test\n";
2431     }
2432     $list .= "\n";
2433
2434     return $list;
2435 }
2436
2437 sub reviewerAndDescriptionForGitCommit($$)
2438 {
2439     my ($commit, $gitReviewer) = @_;
2440
2441     my $description = '';
2442     my $reviewer;
2443
2444     my @args = qw(rev-list --pretty);
2445     push @args, '-1' if $commit !~ m/.+\.\..+/;
2446     my $gitLog;
2447     {
2448         local $/ = undef;
2449         open(GITLOG, "-|", GIT, @args, $commit) || die;
2450         $gitLog = <GITLOG>;
2451         close(GITLOG);
2452     }
2453
2454     my @commitLogs = split(/^[Cc]ommit [a-f0-9]{40}/m, $gitLog);
2455     shift @commitLogs; # Remove initial blank commit log
2456     my $commitLogCount = 0;
2457     foreach my $commitLog (@commitLogs) {
2458         $description .= "\n" if $commitLogCount;
2459         $commitLogCount++;
2460         my $inHeader = 1;
2461         my $commitLogIndent; 
2462         my @lines = split(/\n/, $commitLog);
2463         shift @lines; # Remove initial blank line
2464         foreach my $line (@lines) {
2465             if ($inHeader) {
2466                 if (!$line) {
2467                     $inHeader = 0;
2468                 }
2469                 next;
2470             } elsif ($line =~ /[Ss]igned-[Oo]ff-[Bb]y: (.+)/) {
2471                 if (!$reviewer) {
2472                     $reviewer = $1;
2473                 } else {
2474                     $reviewer .= ", " . $1;
2475                 }
2476             } elsif ($line =~ /^\s*$/) {
2477                 $description = $description . "\n";
2478             } else {
2479                 if (!defined($commitLogIndent)) {
2480                     # Let the first line with non-white space determine
2481                     # the global indent.
2482                     $line =~ /^(\s*)\S/;
2483                     $commitLogIndent = length($1);
2484                 }
2485                 # Strip at most the indent to preserve relative indents.
2486                 $line =~ s/^\s{0,$commitLogIndent}//;
2487                 $description = $description . (" " x 8) . $line . "\n";
2488             }
2489         }
2490     }
2491     if (!$reviewer) {
2492       $reviewer = $gitReviewer;
2493     }
2494
2495     return ($reviewer, $description);
2496 }
2497
2498 sub normalizeLineEndings($$)
2499 {
2500     my ($string, $endl) = @_;
2501     $string =~ s/\r?\n/$endl/g;
2502     return $string;
2503 }
2504
2505 sub decodeEntities($)
2506 {
2507     my ($text) = @_;
2508     $text =~ s/\&lt;/</g;
2509     $text =~ s/\&gt;/>/g;
2510     $text =~ s/\&quot;/\"/g;
2511     $text =~ s/\&apos;/\'/g;
2512     $text =~ s/\&amp;/\&/g;
2513     return $text;
2514 }