prepare-ChangeLog should not list added layout tests in PAL ChangeLog
[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 (unixPath($prefix) =~ 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             if ($1 eq "const" and !$in_parentheses) {
1011                 $potential_name .= " const";
1012                 next;
1013             }
1014
1015             if ($1 eq "volatile" and !$in_parentheses) {
1016                 $potential_name .= " volatile";
1017                 next;
1018             }
1019
1020             # C++ auto function() -> type
1021             if ($1 eq ">") {
1022                 $skip_til_brace_or_semicolon = 1 unless ($in_parentheses || $in_braces);
1023                 next;
1024             }
1025
1026             # C++ constructor initializers
1027             if ($1 eq ":") {
1028                 $skip_til_brace_or_semicolon = 1 unless ($in_parentheses || $in_braces);
1029             }
1030
1031             # Open brace.
1032             if ($1 eq "{") {
1033                 $skip_til_brace_or_semicolon = 0;
1034
1035                 if (!$in_braces) {
1036                     if ($namespace_start >= 0 and $namespace_start < $potential_start) {
1037                         push @ranges, [ $namespace_start . "", $potential_start - 1, $name ];
1038                     }
1039
1040                     if ($potential_namespace) {
1041                         push @namespaces, $potential_namespace;
1042                         push @all_namespaces, $potential_namespace;
1043                         $potential_namespace = "";
1044                         $name = $namespaces[-1];
1045                         $namespace_start = $. + 1;
1046                         next;
1047                     }
1048
1049                     # Promote potential name to real function name at the
1050                     # start of the outer level set of braces (function body?).
1051                     if ($potential_start) {
1052                         $start = $potential_start;
1053                         $name = $potential_name;
1054                         if (@namespaces && $name && (length($name) < 2 || substr($name,1,1) ne "[")) {
1055                             $name = join ('::', @namespaces, $name);
1056                         }
1057                     }
1058                 }
1059
1060                 $in_method_declaration = 0;
1061
1062                 $brace_start = $. if (!$in_braces);
1063                 $in_braces++;
1064                 next;
1065             }
1066
1067             # Close brace.
1068             if ($1 eq "}") {
1069                 if (!$in_braces && @namespaces) {
1070                     if ($namespace_start >= 0 and $namespace_start < $.) {
1071                         push @ranges, [ $namespace_start . "", $. - 1, $name ];
1072                     }
1073
1074                     pop @namespaces;
1075                     if (@namespaces) {
1076                         $name = $namespaces[-1];
1077                         $namespace_start = $. + 1;
1078                     } else {
1079                         $name = "";
1080                         $namespace_start = -1;
1081                     }
1082                     next;
1083                 }
1084
1085                 $in_braces--;
1086                 $brace_end = $. if (!$in_braces);
1087
1088                 # End of an outer level set of braces.
1089                 # This could be a function body.
1090                 if (!$in_braces and $name) {
1091                     # This is the end of an array definition at the top level, not the end of a method.
1092                     if ($in_toplevel_array_brace) {
1093                         $in_toplevel_array_brace = 0;
1094                         next;
1095                     }
1096
1097                     push @ranges, [ $start, $., $name ];
1098                     if (@namespaces) {
1099                         $name = $namespaces[-1];
1100                         $namespace_start = $. + 1;
1101                     } else {
1102                         $name = "";
1103                         $namespace_start = -1;
1104                     }
1105                 }
1106
1107                 $potential_start = 0;
1108                 $potential_name = "";
1109                 next;
1110             }
1111
1112             # Semicolon.
1113             if ($1 eq ";") {
1114                 $skip_til_brace_or_semicolon = 0;
1115                 $potential_start = 0;
1116                 $potential_name = "";
1117                 $in_method_declaration = 0;
1118                 next;
1119             }
1120
1121             # Ignore "const" method qualifier.
1122             if ($1 eq "const") {
1123                 next;
1124             }
1125
1126             if ($1 eq "namespace" || $1 eq "class" || $1 eq "struct") {
1127                 $next_word_could_be_namespace = 1;
1128                 next;
1129             }
1130
1131             # Word.
1132             $word = $1;
1133             if (!$skip_til_brace_or_semicolon) {
1134                 if ($next_word_could_be_namespace) {
1135                     $potential_namespace = $word;
1136                     $next_word_could_be_namespace = 0;
1137                 } elsif ($potential_namespace) {
1138                     $potential_namespace = "";
1139                 }
1140
1141                 if (!$in_parentheses) {
1142                     $potential_start = 0;
1143                     $potential_name = "";
1144                 }
1145                 if (!$potential_start) {
1146                     $potential_start = $.;
1147                     $potential_name = "";
1148                 }
1149             }
1150         }
1151     }
1152
1153     warn "missing close braces in $file_name (probable start at $brace_start)\n" if ($in_braces > 0);
1154     warn "too many close braces in $file_name (probable start at $brace_end)\n" if ($in_braces < 0);
1155
1156     warn "mismatched parentheses in $file_name\n" if $in_parentheses;
1157
1158     return delete_namespaces_from_ranges_for_cpp(@ranges, @all_namespaces);
1159 }
1160
1161
1162 # Take in references to an array of line ranges for C functions in a given file 
1163 # and an array of namespaces declared in that file and return an updated
1164 # list of line ranges with the namespaces removed.
1165
1166 sub delete_namespaces_from_ranges_for_cpp(\@\@)
1167 {
1168     my ($ranges, $namespaces) = @_;
1169     return grep {!is_function_in_namespace($namespaces, $$_[2])} @$ranges;
1170 }
1171
1172
1173 sub is_function_in_namespace($$)
1174 {
1175     my ($namespaces, $function_name) = @_;
1176     return grep {$_ eq $function_name} @$namespaces;
1177 }
1178
1179
1180 # Read a file and get all the line ranges of the things that look like Java
1181 # classes, interfaces and methods.
1182 #
1183 # A class or interface name is the word that immediately follows
1184 # `class' or `interface' when followed by an open curly brace and not
1185 # a semicolon. It can appear at the top level, or inside another class
1186 # or interface block, but not inside a function block
1187 #
1188 # A class or interface starts at the first character after the first close
1189 # brace or after the function name and ends at the close brace.
1190 #
1191 # A function name is the last word before an open parenthesis before
1192 # an open brace rather than a semicolon. It can appear at top level or
1193 # inside a class or interface block, but not inside a function block.
1194 #
1195 # A function starts at the first character after the first close
1196 # brace or after the function name and ends at the close brace.
1197 #
1198 # Comment handling is simple-minded but will work for all but pathological cases.
1199 #
1200 # Result is a list of triples: [ start_line, end_line, function_name ].
1201
1202 sub get_function_line_ranges_for_java($$)
1203 {
1204     my ($file_handle, $file_name) = @_;
1205
1206     my @current_scopes;
1207
1208     my @ranges;
1209
1210     my $in_comment = 0;
1211     my $in_macro = 0;
1212     my $in_parentheses = 0;
1213     my $in_braces = 0;
1214     my $in_non_block_braces = 0;
1215     my $class_or_interface_just_seen = 0;
1216     my $in_class_declaration = 0;
1217
1218     my $word = "";
1219
1220     my $potential_start = 0;
1221     my $potential_name = "";
1222     my $potential_name_is_class_or_interface = 0;
1223
1224     my $start = 0;
1225     my $name = "";
1226     my $current_name_is_class_or_interface = 0;
1227
1228     while (<$file_handle>) {
1229         # Handle continued multi-line comment.
1230         if ($in_comment) {
1231             next unless s-.*\*/--;
1232             $in_comment = 0;
1233         }
1234
1235         # Handle continued macro.
1236         if ($in_macro) {
1237             $in_macro = 0 unless /\\$/;
1238             next;
1239         }
1240
1241         # Handle start of macro (or any preprocessor directive).
1242         if (/^\s*\#/) {
1243             $in_macro = 1 if /^([^\\]|\\.)*\\$/;
1244             next;
1245         }
1246
1247         # Handle comments and quoted text.
1248         while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
1249             my $match = $1;
1250             if ($match eq "/*") {
1251                 if (!s-/\*.*?\*/--) {
1252                     s-/\*.*--;
1253                     $in_comment = 1;
1254                 }
1255             } elsif ($match eq "//") {
1256                 s-//.*--;
1257             } else { # ' or "
1258                 if (!s-$match([^\\]|\\.)*?$match--) {
1259                     warn "mismatched quotes at line $. in $file_name\n";
1260                     s-$match.*--;
1261                 }
1262             }
1263         }
1264
1265         # Find function names.
1266         while (m-(\w+|[(){};])-g) {
1267             # Open parenthesis.
1268             if ($1 eq "(") {
1269                 if (!$in_parentheses) {
1270                     $potential_name = $word;
1271                     $potential_name_is_class_or_interface = 0;
1272                 }
1273                 $in_parentheses++;
1274                 next;
1275             }
1276
1277             # Close parenthesis.
1278             if ($1 eq ")") {
1279                 $in_parentheses--;
1280                 next;
1281             }
1282
1283             # Open brace.
1284             if ($1 eq "{") {
1285                 $in_class_declaration = 0;
1286
1287                 # Promote potential name to real function name at the
1288                 # start of the outer level set of braces (function/class/interface body?).
1289                 if (!$in_non_block_braces
1290                     and (!$in_braces or $current_name_is_class_or_interface)
1291                     and $potential_start) {
1292                     if ($name) {
1293                           push @ranges, [ $start, ($. - 1),
1294                                           join ('.', @current_scopes) ];
1295                     }
1296
1297
1298                     $current_name_is_class_or_interface = $potential_name_is_class_or_interface;
1299
1300                     $start = $potential_start;
1301                     $name = $potential_name;
1302
1303                     push (@current_scopes, $name);
1304                 } else {
1305                     $in_non_block_braces++;
1306                 }
1307
1308                 $potential_name = "";
1309                 $potential_start = 0;
1310
1311                 $in_braces++;
1312                 next;
1313             }
1314
1315             # Close brace.
1316             if ($1 eq "}") {
1317                 $in_braces--;
1318
1319                 # End of an outer level set of braces.
1320                 # This could be a function body.
1321                 if (!$in_non_block_braces) {
1322                     if ($name) {
1323                         push @ranges, [ $start, $.,
1324                                         join ('.', @current_scopes) ];
1325
1326                         pop (@current_scopes);
1327
1328                         if (@current_scopes) {
1329                             $current_name_is_class_or_interface = 1;
1330
1331                             $start = $. + 1;
1332                             $name =  $current_scopes[$#current_scopes-1];
1333                         } else {
1334                             $current_name_is_class_or_interface = 0;
1335                             $start = 0;
1336                             $name =  "";
1337                         }
1338                     }
1339                 } else {
1340                     $in_non_block_braces-- if $in_non_block_braces;
1341                 }
1342
1343                 $potential_start = 0;
1344                 $potential_name = "";
1345                 next;
1346             }
1347
1348             # Semicolon.
1349             if ($1 eq ";") {
1350                 $potential_start = 0;
1351                 $potential_name = "";
1352                 next;
1353             }
1354
1355             if ($1 eq "class") {
1356                 $in_class_declaration = 1;
1357             }
1358             if ($1 eq "class" or (!$in_class_declaration and $1 eq "interface")) {
1359                 $class_or_interface_just_seen = 1;
1360                 next;
1361             }
1362
1363             # Word.
1364             $word = $1;
1365             if (!$in_parentheses) {
1366                 if ($class_or_interface_just_seen) {
1367                     $potential_name = $word;
1368                     $potential_start = $.;
1369                     $class_or_interface_just_seen = 0;
1370                     $potential_name_is_class_or_interface = 1;
1371                     next;
1372                 }
1373             }
1374             if (!$potential_start) {
1375                 $potential_start = $.;
1376                 $potential_name = "";
1377             }
1378             $class_or_interface_just_seen = 0;
1379         }
1380     }
1381
1382     warn "mismatched braces in $file_name\n" if $in_braces;
1383     warn "mismatched parentheses in $file_name\n" if $in_parentheses;
1384
1385     return @ranges;
1386 }
1387
1388
1389
1390 # Read a file and get all the line ranges of the things that look like
1391 # JavaScript functions or methods.
1392 #
1393 # A function name is the word that immediately follows `function' when
1394 # followed by an open curly brace. It can appear at the top level,
1395 # or inside other functions. For example:
1396 #
1397 #    function name() { // (name)
1398 #        function inner() { } // (name.inner)
1399 #    }
1400 #
1401 # An anonymous function name is the identifier on the left hand side of
1402 # an assignment with the equals operator or object notation that has a
1403 # value starting with `function' followed an open curly brace.
1404 # For example:
1405 #
1406 #    namespace = {
1407 #        name: function() {} // (namespace.name)
1408 #    }
1409 #    namespace.Foo = function() {} // (namespace.Foo)
1410 #
1411 # A getter or setter name is the word that immediately follows `get' or
1412 # `set' when followed by params and an open curly brace. For example:
1413 #
1414 #    namespace = {
1415 #      get foo() {} // (namespace.get foo)
1416 #    }
1417 #
1418 # A method name is the word immediately before parenthesis, with an open
1419 # curly brace immediately following closing parenthesis. For a class expression
1420 # we take the assignment identifier instead of the class name for namespacing.
1421 #
1422 #    namespace.Foo = class DoesNotMatter extends Bar {
1423 #        constructor() {} // (namespace.Foo)
1424 #        static staticMethod() {} // (namespace.Foo.staticMethod)
1425 #        instanceMethod() {} // (namespace.Foo.prototype.instanceMethod)
1426 #        get getter() {} // (namespace.Foo.prototype.get getter)
1427 #    }
1428 #    class ClassName {
1429 #        constructor() {} // (ClassName)
1430 #        method() {} // (ClassName.prototype.method)
1431 #    }
1432 #
1433 # Methods may exist in object literals, outside of classes.
1434 #
1435 #   Foo.prototype = {
1436 #       method() {}, // (Foo.prototype.method)
1437 #       otherMethod() {} // (Foo.prototype.otherMethod)
1438 #   }
1439 #
1440 # Comment handling is simple-minded but will work for all but pathological cases.
1441 #
1442 # Result is a list of triples: [ start_line, end_line, function_name ].
1443
1444 sub get_function_line_ranges_for_javascript($$)
1445 {
1446     my ($fileHandle, $fileName) = @_;
1447
1448     my @currentScopes;
1449     my @currentIdentifiers;
1450     my @currentParsingMode = ("global");
1451     my @currentFunctionNames;
1452     my @currentFunctionDepths;
1453     my @currentFunctionStartLines;
1454
1455     my @ranges;
1456
1457     my $inComment = 0;
1458     my $inQuotedText = "";
1459     my $inExtends = 0;
1460     my $inMethod = 0;
1461     my $inAnonymousFunctionParameters = 0;
1462     my $parenthesesDepth = 0;
1463     my $globalParenthesesDepth = 0;
1464     my $bracesDepth = 0;
1465
1466     my $classJustSeen = 0;
1467     my $parenthesisJustSeen = 0;
1468     my $functionJustSeen = 0;
1469     my $getterJustSeen = 0;
1470     my $setterJustSeen = 0;
1471     my $assignmentJustSeen = 0;
1472     my $staticOrContructorSeen = 0;
1473
1474     my $currentToken = "";
1475     my $lastToken = "";
1476     my $possibleMethodName = "";
1477     my $word = "";
1478
1479     while (<$fileHandle>) {
1480         # Handle continued multi-line comment.
1481         if ($inComment) {
1482             next unless s-.*\*/--;
1483             $inComment = 0;
1484         }
1485
1486         # Handle continued quoted text.
1487         if ($inQuotedText ne "") {
1488             next if /\\$/;
1489             s-([^\\]|\\.)*?$inQuotedText--;
1490             $inQuotedText = "";
1491         }
1492
1493         # Handle comments and quoted text.
1494         while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
1495             my $match = $1;
1496             if ($match eq '/*') {
1497                 if (!s-/\*.*?\*/--) {
1498                     s-/\*.*--;
1499                     $inComment = 1;
1500                 }
1501             } elsif ($match eq '//') {
1502                 s-//.*--;
1503             } else { # ' or "
1504                 if (!s-$match([^\\]|\\.)*?$match-string_appeared_here-) {
1505                     $inQuotedText = $match if /\\$/;
1506                     warn "mismatched quotes at line $. in $fileName\n" if $inQuotedText eq "";
1507                     s-$match.*--;
1508                 }
1509             }
1510         }
1511
1512         # Find function names.
1513         while (m-(\w+|[(){}=:;,.])-g) {
1514             # Skip everything until "{" after extends.
1515             if ($inExtends) {
1516                 next if $1 ne '{';
1517                 $inExtends = 0;
1518             }
1519
1520             $lastToken = $currentToken;
1521             $currentToken = $1;
1522
1523             # Open parenthesis.
1524             if ($1 eq '(') {
1525                 $parenthesesDepth++;
1526                 $globalParenthesesDepth++ if $currentParsingMode[$#currentParsingMode] eq "global";
1527                 $possibleMethodName = join('.', @currentIdentifiers);
1528                 $inAnonymousFunctionParameters = 1 if $functionJustSeen;
1529                 $functionJustSeen = 0;
1530                 next;
1531             }
1532
1533             # Close parenthesis.
1534             if ($1 eq ')') {
1535                 $parenthesesDepth--;
1536                 $globalParenthesesDepth-- if $currentParsingMode[$#currentParsingMode] eq "global";
1537                 @currentIdentifiers = () if $inAnonymousFunctionParameters;
1538                 $inAnonymousFunctionParameters = 0;
1539                 $parenthesisJustSeen = 1;
1540                 next;
1541             }
1542
1543             # Open brace.
1544             if ($1 eq '{') {
1545                 my $methodName = "";
1546                 my $mode = $currentParsingMode[$#currentParsingMode];
1547
1548                 # Method.
1549                 if (($mode eq 'class' or $mode eq 'global') and $parenthesisJustSeen and ($staticOrContructorSeen or $possibleMethodName)) {
1550                     if ($mode eq 'class') {
1551                         $methodName = join('.', $staticOrContructorSeen ? "" : "prototype", $possibleMethodName);
1552                     } else {
1553                         $methodName = $possibleMethodName;
1554                     }
1555
1556                     $methodName =~ s/\.{2,}/\./g; # Removes consecutive periods.
1557                     $methodName =~ s/\.$//; # Remove trailing period.
1558
1559                     my $currentMethod = join('.', @currentScopes, $methodName);
1560                     $currentMethod =~ s/\.{2,}/\./g; # Removes consecutive periods.
1561                     $currentMethod =~ s/\.$//; # Remove trailing period.
1562
1563                     push(@currentParsingMode, "method");
1564                     push(@currentFunctionNames, $currentMethod);
1565                     push(@currentFunctionDepths, $bracesDepth);
1566                     push(@currentFunctionStartLines, $.);
1567                 }
1568
1569                 $bracesDepth++;
1570                 $functionJustSeen = 0;
1571
1572                 push(@currentScopes, join('.', $methodName ? $methodName : @currentIdentifiers));
1573                 @currentIdentifiers = ();
1574
1575                 $staticOrContructorSeen = 0;
1576                 next;
1577             }
1578
1579             # Close brace.
1580             if ($1 eq '}') {
1581                 $bracesDepth--;
1582                 $functionJustSeen = 0;
1583
1584                 if (@currentFunctionDepths and $bracesDepth == $currentFunctionDepths[$#currentFunctionDepths]) {
1585                     pop(@currentFunctionDepths);
1586                     pop(@currentParsingMode);
1587
1588                     my $currentName = pop(@currentFunctionNames);
1589                     my $start = pop(@currentFunctionStartLines);
1590
1591                     $currentName =~ s/^\.//g; # Removes leading periods.
1592
1593                     push(@ranges, [$start, $., $currentName]);
1594                 }
1595
1596                 pop(@currentScopes);
1597                 @currentIdentifiers = ();
1598
1599                 next;
1600             }
1601
1602             # Dot.
1603             if ($1 eq '.') {
1604                 next;
1605             }
1606
1607             # Semicolon or comma.
1608             if ($1 eq ';' or $1 eq ',') {
1609                 @currentIdentifiers = ();
1610                 next;
1611             }
1612
1613             # Class.
1614             if ($1 eq 'class') {
1615                 $classJustSeen = 1;
1616                 next;
1617             }
1618
1619             # Extends.
1620             if ($1 eq 'extends') {
1621                 $inExtends = 1;
1622                 next;
1623             }
1624
1625             # Function.
1626             if ($1 eq 'function') {
1627                 $functionJustSeen = 1;
1628
1629                 if ($assignmentJustSeen) {
1630                     my $currentFunction = join('.', (@currentScopes, @currentIdentifiers));
1631                     $currentFunction =~ s/\.{2,}/\./g; # Removes consecutive periods.
1632
1633                     push(@currentParsingMode, "function");
1634                     push(@currentFunctionNames, $currentFunction);
1635                     push(@currentFunctionDepths, $bracesDepth);
1636                     push(@currentFunctionStartLines, $.);
1637                 }
1638
1639                 next;
1640             }
1641
1642             # Getter prefix.
1643             if ($1 eq 'get') {
1644                 next if $lastToken eq '.'; # Avoid map.get(...).
1645                 $getterJustSeen = 1;
1646                 next;
1647             }
1648
1649             # Setter prefix.
1650             if ($1 eq 'set') {
1651                 next if $lastToken eq '.'; # Avoid map.set(...).
1652                 $setterJustSeen = 1;
1653                 next;
1654             }
1655
1656             # Static.
1657             if ($1 eq 'static' or $1 eq 'constructor') {
1658                 $staticOrContructorSeen = 1;
1659                 next;
1660             }
1661
1662             # Assignment operator.
1663             if ($1 eq '=' or $1 eq ':') {
1664                 $assignmentJustSeen = 1;
1665                 next;
1666             }
1667
1668             next if $parenthesesDepth > $globalParenthesesDepth;
1669
1670             # Word.
1671             $word = $1;
1672
1673             if ($classJustSeen) {
1674                 push(@currentIdentifiers, $word) if !$assignmentJustSeen;
1675
1676                 my $currentClass = join('.', (@currentScopes, @currentIdentifiers));
1677                 $currentClass =~ s/\.{2,}/\./g; # Removes consecutive periods.
1678
1679                 push(@currentParsingMode, "class");
1680                 push(@currentFunctionNames, $currentClass);
1681                 push(@currentFunctionDepths, $bracesDepth);
1682                 push(@currentFunctionStartLines, $.);
1683             } elsif ($getterJustSeen or $setterJustSeen) {
1684                 $word = "get $word" if $getterJustSeen;
1685                 $word = "set $word" if $setterJustSeen;
1686
1687                 push(@currentIdentifiers, $word);
1688
1689                 my $mode = $currentParsingMode[$#currentParsingMode];
1690                 my $currentFunction = join('.', (@currentScopes, ($mode eq 'class') ? "prototype" : "", @currentIdentifiers));
1691                 $currentFunction =~ s/\.{2,}/\./g; # Removes consecutive periods.
1692
1693                 push(@currentParsingMode, "function");
1694                 push(@currentFunctionNames, $currentFunction);
1695                 push(@currentFunctionDepths, $bracesDepth);
1696                 push(@currentFunctionStartLines, $.);
1697             } elsif ($functionJustSeen and !$assignmentJustSeen) {
1698                 push(@currentIdentifiers, $word);
1699
1700                 my $currentFunction = join('.', (@currentScopes, @currentIdentifiers));
1701                 $currentFunction =~ s/\.{2,}/\./g; # Removes consecutive periods.
1702
1703                 push(@currentParsingMode, "function");
1704                 push(@currentFunctionNames, $currentFunction);
1705                 push(@currentFunctionDepths, $bracesDepth);
1706                 push(@currentFunctionStartLines, $.);
1707             } elsif ($word ne 'if' and $word ne 'for' and $word ne 'do' and $word ne 'while' and $word ne 'which' and $word ne 'var') {
1708                 push(@currentIdentifiers, $word);
1709             }
1710
1711             $classJustSeen = 0;
1712             $parenthesisJustSeen = 0;
1713             $functionJustSeen = 0;
1714             $getterJustSeen = 0;
1715             $setterJustSeen = 0;
1716             $assignmentJustSeen = 0;
1717         }
1718     }
1719
1720     warn "mismatched braces in $fileName\n" if $bracesDepth;
1721     warn "mismatched parentheses in $fileName\n" if $parenthesesDepth;
1722
1723     return @ranges;
1724 }
1725
1726 # Read a file and get all the line ranges of the things that look like Perl functions. Functions
1727 # start on a line that starts with "sub ", and end on the first line starting with "}" thereafter.
1728 #
1729 # Result is a list of triples: [ start_line, end_line, function ].
1730
1731 sub get_function_line_ranges_for_perl($$)
1732 {
1733     my ($fileHandle, $fileName) = @_;
1734
1735     my @ranges;
1736
1737     my $currentFunction = "";
1738     my $start = 0;
1739     my $hereDocumentIdentifier = "";
1740
1741     while (<$fileHandle>) {
1742         chomp;
1743         if (!$hereDocumentIdentifier) {
1744             if (/^sub\s+([\w_][\w\d_]*)/) {
1745                 # Skip over forward declarations, which don't contain a brace and end with a semicolon.
1746                 next if /;\s*$/;
1747
1748                 if ($currentFunction) {
1749                     warn "nested functions found at top-level at $fileName:$.\n";
1750                     next;
1751                 }
1752                 $currentFunction = $1;
1753                 $start = $.;
1754             }
1755             if (/<<\s*[\"\']?([\w_][\w_\d]*)/) {
1756                 # Enter here-document.
1757                 $hereDocumentIdentifier = $1;
1758             }
1759             if (index($_, "}") == 0) {
1760                 next unless $start;
1761                 push(@ranges, [$start, $., $currentFunction]);
1762                 $currentFunction = "";
1763                 $start = 0;
1764             }
1765         } elsif ($_ eq $hereDocumentIdentifier) {
1766             # Escape from here-document.
1767             $hereDocumentIdentifier = "";
1768         }
1769     }
1770
1771     return @ranges;
1772 }
1773
1774 # Read a file and get all the line ranges of the things that look like Python classes, methods, or functions.
1775 #
1776 # FIXME: Maybe we should use Python's ast module to do the parsing for us?
1777 #
1778 # Result is a list of triples: [ start_line, end_line, function ].
1779
1780 sub get_function_line_ranges_for_python($$)
1781 {
1782     my ($fileHandle, $fileName) = @_;
1783
1784     my @ranges;
1785
1786     my @scopeStack = ({ line => 0, indent => -1, name => undef });
1787     my $lastLine = 0;
1788     until ($lastLine) {
1789         $_ = <$fileHandle>;
1790         unless ($_) {
1791             # To pop out all popped scopes, run the loop once more after
1792             # we encountered the end of the file.
1793             $_ = "pass\n";
1794             $.++;
1795             $lastLine = 1;
1796         }
1797         chomp;
1798         next unless /^(\s*)([^#].*)$/;
1799
1800         my $indent = length $1;
1801         my $rest = $2;
1802         my $scope = $scopeStack[-1];
1803
1804         if ($indent <= $scope->{indent}) {
1805             # Find all the scopes that we have just exited.
1806             my $i = 0;
1807             for (; $i < @scopeStack; ++$i) {
1808                 last if $indent <= $scopeStack[$i]->{indent};
1809             }
1810             my @poppedScopes = splice @scopeStack, $i;
1811
1812             # For each scope that was just exited, add a range that goes from the start of that
1813             # scope to the start of the next nested scope, or to the line just before this one for
1814             # the innermost scope.
1815             for ($i = 0; $i < @poppedScopes; ++$i) {
1816                 my $lineAfterEnd = $i + 1 == @poppedScopes ? $. : $poppedScopes[$i + 1]->{line};
1817                 push @ranges, [$poppedScopes[$i]->{line}, $lineAfterEnd - 1, $poppedScopes[$i]->{name}];
1818             }
1819             @scopeStack or warn "Popped off last scope at $fileName:$.\n";
1820
1821             # Set the now-current scope to start at the current line. Any lines within this scope
1822             # before this point should already have been added to @ranges.
1823             $scope = $scopeStack[-1];
1824             $scope->{line} = $.;
1825         }
1826
1827         next unless $rest =~ /(?:class|def)\s+(\w+)/;
1828         my $name = $1;
1829         my $fullName = $scope->{name} ? join('.', $scope->{name}, $name) : $name;
1830         push @scopeStack, { line => $., indent => $indent, name => $fullName };
1831
1832         if ($scope->{indent} >= 0) {
1833             push @ranges, [$scope->{line}, $. - 1, $scope->{name}];
1834         }
1835     }
1836
1837     return @ranges;
1838 }
1839
1840 # Read a file and get all the line ranges of the things that look like CSS selectors.  A selector is
1841 # anything before an opening brace on a line. A selector starts at the line containing the opening
1842 # brace and ends at the closing brace.
1843 #
1844 # Result is a list of triples: [ start_line, end_line, selector ].
1845
1846 sub get_selector_line_ranges_for_css($$)
1847 {
1848     my ($fileHandle, $fileName) = @_;
1849
1850     my @ranges;
1851
1852     my $inComment = 0;
1853     my $inBrace = 0;
1854     my @stack;
1855     my $context;
1856     my @currentParseMode = ("global");
1857     my $selectorBraces = 0;
1858
1859     while (<$fileHandle>) {
1860         foreach my $token (split m-(\{|\}|/\*|\*/)-, $_) {
1861             if ($token eq "{") {
1862                 if (!$inComment) {
1863                     $inBrace += 1;                    
1864                     $selectorBraces += 1 if $currentParseMode[$#currentParseMode] eq "selector";
1865                     warn "mismatched opening brace found in $fileName:$.\n" if $selectorBraces > 1;
1866                 }
1867             } elsif ($token eq "}") {
1868                 if (!$inComment) {
1869                     if (!$inBrace or $currentParseMode[$#currentParseMode] eq "global") {
1870                         warn "mismatched closing brace found in $fileName:$.\n";
1871                         next;
1872                     }
1873
1874                     $inBrace -= 1;
1875
1876                     pop(@currentParseMode);
1877                     my $name = pop(@stack);
1878                     my $startLine = pop(@stack);
1879                     my $endLine = $.;
1880                     push(@ranges, [$startLine, $endLine, $name]);
1881                     $selectorBraces = 0;
1882                 }
1883             } elsif ($token eq "/*") {
1884                 $inComment = 1;
1885             } elsif ($token eq "*/") {
1886                 warn "mismatched comment found in $fileName:$.\n" if !$inComment;
1887                 $inComment = 0;
1888             } else {
1889                 if (!$inComment and $currentParseMode[$#currentParseMode] ne "selector" and $token !~ /^[\s\t]*$/) {
1890                     $token =~ s/^[\s\t]*|[\s\t]*$//g;
1891                     my $startLine = $.;
1892                     if ($token =~ /^\@media/) {
1893                         push(@currentParseMode, "media");
1894                         push(@stack, ($startLine, $token));
1895                     } else {
1896                         push(@currentParseMode, "selector");
1897                         push(@stack, ($startLine, $token));
1898                     }
1899                 }
1900             }
1901         }
1902     }
1903
1904     # Sort by start line.
1905     return sort {$a->[0] <=> $b->[0]} @ranges;
1906 }
1907
1908 # Read a file and get all the line ranges of the things that look like Swift classes, methods,
1909 # or functions.
1910 #
1911 # Result is a list of triples: [ start_line, end_line, function ].
1912
1913 sub get_function_line_ranges_for_swift($$)
1914 {
1915     my ($fileHandle, $fileName) = @_;
1916
1917     my @ranges;
1918
1919     my $currentFunction = "";
1920     my $currentClass = "";
1921     my $functionStart = 0;
1922     my $classStart = 0;
1923     my $functionScopeDepth = 0;
1924     my $classScopeDepth = 0;
1925     my $scopeDepth = 0;
1926
1927     while (<$fileHandle>) {
1928         chomp;
1929         next if (/^\s*\/\/.*/);
1930         if (/func\s+([\w_][\w\d_]*)\((.*)\)/ || /var\s+([\w_][\w\d_]*):\s+/) {
1931             $functionScopeDepth = $scopeDepth;
1932             $currentFunction = $1;
1933             if ($2) {
1934                 $currentFunction = "$currentFunction(". parseSwiftFunctionArgs($2) . ")";
1935             }
1936             if ($currentClass) {
1937                 $currentFunction = "$currentClass.$currentFunction";
1938             }
1939             $functionStart = $.;
1940         } elsif (/class\s+([\w_][\w\d_]*)/) {
1941             $classScopeDepth = $scopeDepth;
1942             $currentClass = $1;
1943             $classStart = $.;
1944         }
1945         if (index($_, "{") > -1) {
1946             $scopeDepth++;
1947         }
1948         if (index($_, "}") > -1) {
1949             $scopeDepth--;
1950         }
1951         if ($scopeDepth == $functionScopeDepth) {
1952             next unless $functionStart;
1953             push(@ranges, [$functionStart, $., $currentFunction]);
1954             $currentFunction = "";
1955             $functionStart = 0;
1956         } elsif ($scopeDepth == $classScopeDepth) {
1957             next unless $classStart;
1958             $currentClass = "";
1959             $classStart = 0;
1960         }
1961     }
1962
1963     return @ranges;
1964 }
1965
1966 sub parseSwiftFunctionArgs($)
1967 {
1968     my ($functionArgs) = @_;
1969     my @words = split /, /, $functionArgs;
1970     my $argCount = scalar(@words);
1971     if ($argCount == 0) {
1972         return "";
1973     } elsif ($argCount > 0) {
1974         # If the first argument is unnamed, give it the name "_"
1975         $words[0] =~ s/^(\w+: .*)/_ $1/;
1976         return join("", map { $_ =~ s/^(\w+).*/$1/; "$_:" } @words);
1977     } else {
1978         warn "Unknown argument count.\n";
1979     }
1980 }
1981
1982 sub processPaths(\@)
1983 {
1984     my ($paths) = @_;
1985     return ("." => 1) if (!@{$paths});
1986
1987     my %result = ();
1988
1989     for my $file (@{$paths}) {
1990         die "can't handle absolute paths like \"$file\"\n" if File::Spec->file_name_is_absolute($file);
1991         die "can't handle empty string path\n" if $file eq "";
1992         die "can't handle path with single quote in the name like \"$file\"\n" if $file =~ /'/; # ' (keep Xcode syntax highlighting happy)
1993
1994         my $untouchedFile = $file;
1995
1996         $file = canonicalizePath($file);
1997
1998         die "can't handle paths with .. like \"$untouchedFile\"\n" if $file =~ m|/\.\./|;
1999
2000         $result{$file} = 1;
2001     }
2002
2003     return ("." => 1) if ($result{"."});
2004
2005     # Remove any paths that also have a parent listed.
2006     for my $path (keys %result) {
2007         for (my $parent = dirname($path); $parent ne '.'; $parent = dirname($parent)) {
2008             if ($result{$parent}) {
2009                 delete $result{$path};
2010                 last;
2011             }
2012         }
2013     }
2014
2015     return %result;
2016 }
2017
2018 sub diffFromToString($$$)
2019 {
2020     my ($gitCommit, $gitIndex, $mergeBase) = @_;
2021
2022     return "" if isSVN();
2023     return $gitCommit if $gitCommit =~ m/.+\.\..+/;
2024     return "\"$gitCommit^\" \"$gitCommit\"" if $gitCommit;
2025     return "--cached" if $gitIndex;
2026     return $mergeBase if $mergeBase;
2027     return "HEAD" if isGit();
2028 }
2029
2030 sub diffCommand($$$$)
2031 {
2032     my ($paths, $gitCommit, $gitIndex, $mergeBase) = @_;
2033
2034     # The function overlap detection logic in computeModifiedFunctions() assumes that its line
2035     # ranges were from a unified diff without any context lines.
2036     my $command;
2037     if (isSVN()) {
2038         my @escapedPaths = map(escapeSubversionPath($_), @$paths);
2039         my $escapedPathsString = qq(") . join(qq(" "), @escapedPaths) . qq(");
2040         $command = SVN . " diff --diff-cmd diff -x -U0 $escapedPathsString";
2041     } elsif (isGit()) {
2042         my $pathsString = "'" . join("' '", @$paths) . "'"; 
2043         $command = GIT . " diff --no-ext-diff -U0 " . diffFromToString($gitCommit, $gitIndex, $mergeBase);
2044         $command .= " -- $pathsString" unless $gitCommit or $mergeBase;
2045     }
2046
2047     return $command;
2048 }
2049
2050 sub statusCommand($$$$)
2051 {
2052     my ($paths, $gitCommit, $gitIndex, $mergeBase) = @_;
2053
2054     my $command;
2055     if (isSVN()) {
2056         my @escapedFiles = map(escapeSubversionPath($_), keys %$paths);
2057         my $escapedFilesString = qq(") . join(qq(" "), @escapedFiles) . qq(");
2058         $command = SVN . " stat $escapedFilesString";
2059     } elsif (isGit()) {
2060         my $filesString = '"' . join('" "', keys %$paths) . '"';
2061         $command = GIT . " diff -r --name-status -M -C " . diffFromToString($gitCommit, $gitIndex, $mergeBase);
2062         $command .= " -- $filesString" unless $gitCommit;
2063     }
2064
2065     return "$command 2>&1";
2066 }
2067
2068 sub attributeCommand($$)
2069 {
2070     my ($file, $attr) = @_;
2071
2072     my $result;
2073     if (isSVN()) {
2074         my $foundAttribute = 0;
2075         my $subPath = ".";
2076         my (@directoryParts) = File::Spec->splitdir($file);
2077         foreach my $part (@directoryParts) {
2078             if ($part eq ".") {
2079                 next;
2080             }
2081             $subPath = File::Spec->join($subPath, $part);
2082             $subPath =~ s/^\.\///;
2083             if ($foundAttribute || exists $attributeCache{$attr}{$subPath} && $attributeCache{$attr}{$subPath} eq "1") {
2084                 $attributeCache{$attr}{$subPath} = "1";
2085                 $foundAttribute = 1;
2086                 next;
2087             }
2088             my $command = SVN . " propget $attr '$subPath'";
2089             my $attrib = $attributeCache{$attr}{$subPath} || `$command 2> $devNull`;
2090             chomp $attrib;
2091             if ($attrib eq "1") {
2092                 $foundAttribute = 1;
2093             }
2094             $attributeCache{$attr}{$subPath} = $attrib || "0";
2095         }
2096         $result = $attributeCache{$attr}{$file};
2097     } elsif (isGit()) {
2098         my $command = GIT . " check-attr $attr -- $file";
2099         $result = `$command`;
2100         chomp $result;
2101         $result =~ s/.*\W(\w)/$1/;
2102     }
2103
2104     $result =~ s/\D//g;
2105     return int($result || 0);
2106 }
2107
2108 sub createPatchCommand($$$$)
2109 {
2110     my ($changedFilesString, $gitCommit, $gitIndex, $mergeBase) = @_;
2111
2112     my $command;
2113     if (isSVN()) {
2114         $command = "'$FindBin::Bin/svn-create-patch --no-style' $changedFilesString";
2115     } elsif (isGit()) {
2116         $command = GIT . " diff -M -C " . diffFromToString($gitCommit, $gitIndex, $mergeBase);
2117         $command .= " -- $changedFilesString" unless $gitCommit;
2118     }
2119
2120     return $command;
2121 }
2122
2123 sub findOriginalFileFromSvn($)
2124 {
2125     my ($file) = @_;
2126     my $baseUrl;
2127     open INFO, SVN . " info . |" or die;
2128     while (<INFO>) {
2129         if (/^URL: (.+?)[\r\n]*$/) {
2130             $baseUrl = $1;
2131         }
2132     }
2133     close INFO;
2134     my $sourceFile;
2135     my $escapedFile = escapeSubversionPath($file);
2136     open INFO, SVN . " info '$escapedFile' |" or die;
2137     while (<INFO>) {
2138         if (/^Copied From URL: (.+?)[\r\n]*$/) {
2139             $sourceFile = File::Spec->abs2rel($1, $baseUrl);
2140         }
2141     }
2142     close INFO;
2143     return $sourceFile;
2144 }
2145
2146 sub determinePropertyChanges($$$)
2147 {
2148     my ($file, $isAdd, $original) = @_;
2149
2150     my $escapedFile = escapeSubversionPath($file);
2151     my %changes;
2152     if ($isAdd) {
2153         my %addedProperties;
2154         my %removedProperties;
2155         open PROPLIST, SVN . " proplist '$escapedFile' |" or die;
2156         while (<PROPLIST>) {
2157             $addedProperties{$1} = 1 if /^  (.+?)[\r\n]*$/ && $1 ne 'svn:mergeinfo';
2158         }
2159         close PROPLIST;
2160         if ($original) {
2161             my $escapedOriginal = escapeSubversionPath($original);
2162             open PROPLIST, SVN . " proplist '$escapedOriginal' |" or die;
2163             while (<PROPLIST>) {
2164                 next unless /^  (.+?)[\r\n]*$/;
2165                 my $property = $1;
2166                 if (exists $addedProperties{$property}) {
2167                     delete $addedProperties{$1};
2168                 } else {
2169                     $removedProperties{$1} = 1;
2170                 }
2171             }
2172         }
2173         $changes{"A"} = [sort keys %addedProperties] if %addedProperties;
2174         $changes{"D"} = [sort keys %removedProperties] if %removedProperties;
2175     } else {
2176         open DIFF, SVN . " diff '$escapedFile' |" or die;
2177         while (<DIFF>) {
2178             if (/^Property changes on:/) {
2179                 while (<DIFF>) {
2180                     my $operation;
2181                     my $property;
2182                     if (/^Added: (\S*)/) {
2183                         $operation = "A";
2184                         $property = $1;
2185                     } elsif (/^Modified: (\S*)/) {
2186                         $operation = "M";
2187                         $property = $1;
2188                     } elsif (/^Deleted: (\S*)/) {
2189                         $operation = "D";
2190                         $property = $1;
2191                     } elsif (/^Name: (\S*)/) {
2192                         # Older versions of svn just say "Name" instead of the type
2193                         # of property change.
2194                         $operation = "C";
2195                         $property = $1;
2196                     }
2197                     if ($operation) {
2198                         $changes{$operation} = [] unless exists $changes{$operation};
2199                         push @{$changes{$operation}}, $property;
2200                     }
2201                 }
2202             }
2203         }
2204         close DIFF;
2205     }
2206     return \%changes;
2207 }
2208
2209 sub pluralizeAndList($$@)
2210 {
2211     my ($singular, $plural, @items) = @_;
2212
2213     return if @items == 0;
2214     return "$singular $items[0]" if @items == 1;
2215     return "$plural " . join(", ", @items[0 .. $#items - 1]) . " and " . $items[-1];
2216 }
2217
2218 sub generateFileList(\%$$$)
2219 {
2220     my ($paths, $gitCommit, $gitIndex, $mergeBase) = @_;
2221
2222     my @changedFiles;
2223     my @conflictFiles;
2224     my %functionLists;
2225     my @addedRegressionTests;
2226     my @requiresTests;
2227     print STDERR "  Running status to find changed, added, or removed files.\n";
2228     open STAT, "-|", statusCommand($paths, $gitCommit, $gitIndex, $mergeBase) or die "The status failed: $!.\n";
2229     while (<STAT>) {
2230         my $status;
2231         my $propertyStatus;
2232         my $propertyChanges;
2233         my $original;
2234         my $file;
2235
2236         if (isSVN()) {
2237             my $matches;
2238             if (isSVNVersion16OrNewer()) {
2239                 $matches = /^([ ACDMR])([ CM]).{5} (.+?)[\r\n]*$/;
2240                 $status = $1;
2241                 $propertyStatus = $2;
2242                 $file = $3;
2243             } else {
2244                 $matches = /^([ ACDMR])([ CM]).{4} (.+?)[\r\n]*$/;
2245                 $status = $1;
2246                 $propertyStatus = $2;
2247                 $file = $3;
2248             }
2249             if ($matches) {
2250                 $file = normalizePath($file);
2251                 $original = findOriginalFileFromSvn($file) if substr($_, 3, 1) eq "+";
2252                 my $isAdd = isAddedStatus($status);
2253                 $propertyChanges = determinePropertyChanges($file, $isAdd, $original) if isModifiedStatus($propertyStatus) || $isAdd;
2254             } else {
2255                 print;  # error output from svn stat
2256             }
2257         } elsif (isGit()) {
2258             if (/^([ADM])\t(.+)$/) {
2259                 $status = $1;
2260                 $propertyStatus = " ";  # git doesn't have properties
2261                 $file = normalizePath($2);
2262             } elsif (/^([CR])[0-9]{1,3}\t([^\t]+)\t([^\t\n]+)$/) { # for example: R90%    newfile    oldfile
2263                 $status = $1;
2264                 $propertyStatus = " ";
2265                 $original = normalizePath($2);
2266                 $file = normalizePath($3);
2267             } else {
2268                 print;  # error output from git diff
2269             }
2270         }
2271
2272         next if !$status || isUnmodifiedStatus($status) && isUnmodifiedStatus($propertyStatus);
2273
2274         $file = makeFilePathRelative($file);
2275
2276         if (isModifiedStatus($status) || isAddedStatus($status) || isModifiedStatus($propertyStatus)) {
2277             my @components = File::Spec->splitdir($file);
2278             if ($components[0] eq "LayoutTests") {
2279                 push @addedRegressionTests, $file
2280                     if isAddedStatus($status)
2281                        && $file =~ /\.([a-zA-Z]+)$/
2282                        && SupportedTestExtensions->{lc($1)}
2283                        && $file !~ /-expected(-mismatch)?\.html$/
2284                        && !scalar(grep(/^resources$/i, @components))
2285                        && !scalar(grep(/^script-tests$/i, @components));
2286             } elsif (attributeCommand($file, "test")) {
2287                 push @addedRegressionTests, $file;
2288             } elsif (attributeCommand($file, "requiresTests")) {
2289                 push @requiresTests, $file
2290             }
2291             push @changedFiles, $file if $components[$#components] ne "ChangeLog";
2292         } elsif (isConflictStatus($status, $gitCommit, $gitIndex) || isConflictStatus($propertyStatus, $gitCommit, $gitIndex)) {
2293             push @conflictFiles, $file;
2294         }
2295         if (basename($file) ne "ChangeLog") {
2296             my $description = statusDescription($status, $propertyStatus, $original, $propertyChanges);
2297             $functionLists{$file} = $description if defined $description;
2298         }
2299     }
2300     close STAT;
2301     return (\@changedFiles, \@conflictFiles, \%functionLists, \@addedRegressionTests, \@requiresTests);
2302 }
2303
2304 sub isUnmodifiedStatus($)
2305 {
2306     my ($status) = @_;
2307
2308     my %statusCodes = (
2309         " " => 1,
2310     );
2311
2312     return $statusCodes{$status};
2313 }
2314
2315 sub isModifiedStatus($)
2316 {
2317     my ($status) = @_;
2318
2319     my %statusCodes = (
2320         "M" => 1,
2321     );
2322
2323     return $statusCodes{$status};
2324 }
2325
2326 sub isAddedStatus($)
2327 {
2328     my ($status) = @_;
2329
2330     my %statusCodes = (
2331         "A" => 1,
2332         "C" => isGit(),
2333         "R" => 1,
2334     );
2335
2336     return $statusCodes{$status};
2337 }
2338
2339 sub isConflictStatus($$$)
2340 {
2341     my ($status, $gitCommit, $gitIndex) = @_;
2342
2343     my %svn = (
2344         "C" => 1,
2345     );
2346
2347     my %git = (
2348         "U" => 1,
2349     );
2350
2351     return 0 if ($gitCommit || $gitIndex); # an existing commit or staged change cannot have conflicts
2352     return $svn{$status} if isSVN();
2353     return $git{$status} if isGit();
2354 }
2355
2356 sub statusDescription($$$$)
2357 {
2358     my ($status, $propertyStatus, $original, $propertyChanges) = @_;
2359
2360     my $propertyDescription = defined $propertyChanges ? propertyChangeDescription($propertyChanges) : "";
2361
2362     my %svn = (
2363         "A" => defined $original ? sprintf(" Copied from \%s.", $original) : " Added.",
2364         "D" => " Removed.",
2365         "M" => "",
2366         "R" => defined $original ? sprintf(" Replaced with \%s.", $original) : " Replaced.",
2367         " " => "",
2368     );
2369
2370     my %git = %svn;
2371     $git{"A"} = " Added.";
2372     if (defined $original) {
2373         $git{"C"} = sprintf(" Copied from \%s.", $original);
2374         $git{"R"} = sprintf(" Renamed from \%s.", $original);
2375     }
2376
2377     my $description;
2378     $description = $svn{$status} if isSVN() && exists $svn{$status};
2379     $description = $git{$status} if isGit() && exists $git{$status};
2380     return unless defined $description;
2381
2382     $description .= $propertyDescription unless isAddedStatus($status);
2383     return $description;
2384 }
2385
2386 sub propertyChangeDescription($)
2387 {
2388     my ($propertyChanges) = @_;
2389
2390     my %operations = (
2391         "A" => "Added",
2392         "M" => "Modified",
2393         "D" => "Removed",
2394         "C" => "Changed",
2395     );
2396
2397     my $description = "";
2398     while (my ($operation, $properties) = each %$propertyChanges) {
2399         my $word = $operations{$operation};
2400         my $list = pluralizeAndList("property", "properties", @$properties);
2401         $description .= " $word $list.";
2402     }
2403     return $description;
2404 }
2405
2406 sub extractLineRangeAfterChange($)
2407 {
2408     my ($string) = @_;
2409     my $chunkRange = parseChunkRange($string);
2410     if (!$chunkRange) {
2411         return (-1, -1); # Malformed
2412     }
2413     if (!$chunkRange->{newStartingLine} || !$chunkRange->{newLineCount}) {
2414          # Deletion; no lines exist after change.
2415         return ($chunkRange->{newStartingLine}, $chunkRange->{newStartingLine});
2416     }
2417     return ($chunkRange->{newStartingLine}, $chunkRange->{newStartingLine} + $chunkRange->{newLineCount} - 1);
2418 }
2419
2420 sub extractLineRangeBeforeChange($)
2421 {
2422     my ($string) = @_;
2423     my $chunkRange = parseChunkRange($string);
2424     if (!$chunkRange) {
2425         return (-1, -1); # Malformed
2426     }
2427     if (!$chunkRange->{startingLine} || !$chunkRange->{lineCount}) {
2428         # Addition; no lines existed before change.
2429         return ($chunkRange->{startingLine}, $chunkRange->{startingLine});
2430     }
2431     return ($chunkRange->{startingLine}, $chunkRange->{startingLine} + $chunkRange->{lineCount} - 1);
2432 }
2433
2434 sub testListForChangeLog(@)
2435 {
2436     my (@tests) = @_;
2437
2438     return "" unless @tests;
2439
2440     my $leadString = "        Test" . (@tests == 1 ? "" : "s") . ": ";
2441     my $list = $leadString;
2442     foreach my $i (0..$#tests) {
2443         $list .= " " x length($leadString) if $i;
2444         my $test = $tests[$i];
2445         $test =~ s/^LayoutTests\///;
2446         $list .= "$test\n";
2447     }
2448     $list .= "\n";
2449
2450     return $list;
2451 }
2452
2453 sub reviewerAndDescriptionForGitCommit($$)
2454 {
2455     my ($commit, $gitReviewer) = @_;
2456
2457     my $description = '';
2458     my $reviewer;
2459
2460     my @args = qw(rev-list --pretty);
2461     push @args, '-1' if $commit !~ m/.+\.\..+/;
2462     my $gitLog;
2463     {
2464         local $/ = undef;
2465         open(GITLOG, "-|", GIT, @args, $commit) || die;
2466         $gitLog = <GITLOG>;
2467         close(GITLOG);
2468     }
2469
2470     my @commitLogs = split(/^[Cc]ommit [a-f0-9]{40}/m, $gitLog);
2471     shift @commitLogs; # Remove initial blank commit log
2472     my $commitLogCount = 0;
2473     foreach my $commitLog (@commitLogs) {
2474         $description .= "\n" if $commitLogCount;
2475         $commitLogCount++;
2476         my $inHeader = 1;
2477         my $commitLogIndent; 
2478         my @lines = split(/\n/, $commitLog);
2479         shift @lines; # Remove initial blank line
2480         foreach my $line (@lines) {
2481             if ($inHeader) {
2482                 if (!$line) {
2483                     $inHeader = 0;
2484                 }
2485                 next;
2486             } elsif ($line =~ /[Ss]igned-[Oo]ff-[Bb]y: (.+)/) {
2487                 if (!$reviewer) {
2488                     $reviewer = $1;
2489                 } else {
2490                     $reviewer .= ", " . $1;
2491                 }
2492             } elsif ($line =~ /^\s*$/) {
2493                 $description = $description . "\n";
2494             } else {
2495                 if (!defined($commitLogIndent)) {
2496                     # Let the first line with non-white space determine
2497                     # the global indent.
2498                     $line =~ /^(\s*)\S/;
2499                     $commitLogIndent = length($1);
2500                 }
2501                 # Strip at most the indent to preserve relative indents.
2502                 $line =~ s/^\s{0,$commitLogIndent}//;
2503                 $description = $description . (" " x 8) . $line . "\n";
2504             }
2505         }
2506     }
2507     if (!$reviewer) {
2508       $reviewer = $gitReviewer;
2509     }
2510
2511     return ($reviewer, $description);
2512 }
2513
2514 sub normalizeLineEndings($$)
2515 {
2516     my ($string, $endl) = @_;
2517     $string =~ s/\r?\n/$endl/g;
2518     return $string;
2519 }
2520
2521 sub decodeEntities($)
2522 {
2523     my ($text) = @_;
2524     $text =~ s/\&lt;/</g;
2525     $text =~ s/\&gt;/>/g;
2526     $text =~ s/\&quot;/\"/g;
2527     $text =~ s/\&apos;/\'/g;
2528     $text =~ s/\&amp;/\&/g;
2529     return $text;
2530 }