diff --git a/README.md b/README.md
index 8876915203..0637393d53 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ rdmd | Public | [D build tool](http://dlang.org/rdmd.html).
rdmd_test | Internal | rdmd test suite.
tests_extractor | Internal | Extracts public unittests (requires DUB)
tolf | Internal | Line endings converter.
+updatecopyright | Internal | Update the copyright notices in DMD
To report a problem or browse the list of open bugs, please visit the
[bug tracker](http://issues.dlang.org/).
diff --git a/posix.mak b/posix.mak
index 1055e3c766..9664b192e4 100644
--- a/posix.mak
+++ b/posix.mak
@@ -49,7 +49,8 @@ TOOLS = \
$(ROOT)/ddemangle \
$(ROOT)/detab \
$(ROOT)/rdmd \
- $(ROOT)/tolf
+ $(ROOT)/tolf \
+ $(ROOT)/updatecopyright
CURL_TOOLS = \
$(ROOT)/changed \
diff --git a/updatecopyright.d b/updatecopyright.d
new file mode 100644
index 0000000000..777d60f4a8
--- /dev/null
+++ b/updatecopyright.d
@@ -0,0 +1,391 @@
+#!/usr/bin/env rdmd
+
+/**
+Update the copyright notices in source files so that they have the form:
+---
+ Copyright XXXX-YYYY by The D Language Foundation, All Rights Reserved
+---
+It does not change copyright notices of authors that are known to have made
+changes under a proprietary license.
+
+Copyright: Copyright (C) 2017-2018 by The D Language Foundation, All Rights Reserved
+
+License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0)
+
+Authors: Iain Buclaw
+
+Example usage:
+
+---
+updatecopyright.d --update-year src/dmd
+---
+*/
+
+module tools.updatecopyright;
+
+int main(string[] args)
+{
+ import std.getopt;
+
+ bool updateYear;
+ bool verbose;
+ auto opts = getopt(args,
+ "update-year|y", "Update the current year on every notice", &updateYear,
+ "verbose|v", "Be more verbose", &verbose);
+
+ if (args.length == 1 || opts.helpWanted)
+ {
+ defaultGetoptPrinter("usage: updatecopyright [--help|-h] [--update-year|-y]
...",
+ opts.options);
+ return 0;
+ }
+
+ Copyright(updateYear, verbose).run(args[1 .. $]);
+ return 0;
+}
+
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+struct Copyright
+{
+ import std.algorithm : any, canFind, each, filter, joiner, map;
+ import std.array : appender, array;
+ import std.file : DirEntry, SpanMode, dirEntries, remove, rename;
+ import std.stdio : File, stderr, stdout;
+ import std.string : endsWith, strip, stripLeft, stripRight;
+ import std.regex : Regex, matchAll, regex;
+
+ // The author to use in copyright notices.
+ enum author = "The D Language Foundation, All Rights Reserved";
+
+ // The standard (C) form.
+ enum copyright = "(C)";
+
+private:
+ // True if running in verbose mode.
+ bool verbose = false;
+
+ // True if also updating copyright year.
+ bool updateYear = false;
+
+ // An associative array of known copyright holders.
+ // Value set to true if the copyright holder is internal.
+ bool[string] holders;
+
+ // Files and directories to ignore during search.
+ static string[] skipDirs = [
+ "docs",
+ "ini",
+ "test",
+ "samples",
+ "vcbuild",
+ ".git",
+ ];
+
+ static string[] skipFiles = [
+ "Jenkinsfile",
+ "LICENSE.txt",
+ "VERSION",
+ ".a",
+ ".ddoc",
+ ".deps",
+ ".lst",
+ ".map",
+ ".md",
+ ".o",
+ ".obj",
+ ".sdl",
+ ".sh",
+ ".yml",
+ ];
+
+ // Characters in a range of years.
+ // Include '.' for typos, and '?' for unknown years.
+ enum rangesStr = `[0-9?](?:[-0-9.,\s]|\s+and\s+)*[0-9]`;
+
+ // Non-whitespace characters in a copyright holder's name.
+ enum nameStr = `[\w.,-]`;
+
+ // Matches a full copyright notice:
+ // - 'Copyright (C)', etc.
+ // - The years. Includes the whitespace in the year, so that we can
+ // remove any excess.
+ // - 'by ', if used
+ // - The copyright holder.
+ Regex!char copyrightRe;
+
+ // A regexp for notices that might have slipped by.
+ Regex!char otherCopyrightRe;
+
+ // A regexp that matches one year.
+ Regex!char yearRe;
+
+ // Matches part of a year or copyright holder.
+ Regex!char continuationRe;
+
+ Regex!char commentRe;
+
+ // Convenience for passing around file/line number information.
+ struct FileLocation
+ {
+ string filename;
+ size_t linnum;
+
+ string toString()
+ {
+ import std.format : format;
+ return "%s(%d)".format(this.filename, this.linnum);
+ }
+ }
+
+ FileLocation location;
+ char[] previousLine;
+
+ void processFile(string filename)
+ {
+ import std.conv : to;
+
+ // Looks like something we tried to create before.
+ if (filename.endsWith(".tmp"))
+ {
+ remove(filename);
+ return;
+ }
+
+ auto file = File(filename, "rb");
+ auto output = appender!string;
+ int errors = 0;
+ bool changed = false;
+
+ output.reserve(file.size.to!size_t);
+
+ // Reset file location information.
+ this.location = FileLocation(filename, 0);
+ this.previousLine = null;
+
+ foreach (line; file.byLine)
+ {
+ this.location.linnum++;
+ try
+ {
+ changed |= this.processLine(line, output, errors);
+ }
+ catch (Exception)
+ {
+ if (this.verbose)
+ stderr.writeln(filename, ": bad input file");
+ errors++;
+ break;
+ }
+ }
+ file.close();
+
+ // If something changed, write the new file out.
+ if (changed && !errors)
+ {
+ auto tmpfilename = filename ~ ".tmp";
+ auto tmpfile = File(tmpfilename, "w");
+ tmpfile.write(output.data);
+ tmpfile.close();
+ rename(tmpfilename, filename);
+ }
+ }
+
+ bool processLine(String, Array)(String line, ref Array output, ref int errors)
+ {
+ bool changed = false;
+
+ if (this.previousLine)
+ {
+ auto continuation = this.stripContinuation(line);
+
+ // Merge the lines for matching purposes.
+ auto mergedLine = this.previousLine.stripRight() ~ `, ` ~ continuation;
+ auto mergedMatch = mergedLine.matchAll(copyrightRe);
+
+ if (!continuation.matchAll(this.continuationRe) ||
+ !mergedMatch || !this.isComplete(mergedMatch))
+ {
+ // If the next line doesn't look like a proper continuation,
+ // assume that what we've got is complete.
+ auto match = this.previousLine.matchAll(copyrightRe);
+ changed |= this.updateCopyright(line, match, errors);
+ output.put(this.previousLine);
+ output.put('\n');
+ }
+ else
+ {
+ line = mergedLine;
+ }
+ this.previousLine = null;
+ }
+
+ auto match = line.matchAll(copyrightRe);
+ if (match)
+ {
+ // If it looks like the copyright is incomplete, add the next line.
+ if (!this.isComplete(match))
+ {
+ this.previousLine = line.dup;
+ return changed;
+ }
+ changed |= this.updateCopyright(line, match, errors);
+ }
+ else if (line.matchAll(this.otherCopyrightRe))
+ {
+ stderr.writeln(this.location, ": unrecognised copyright: ", line.strip);
+ //errors++; // Only treat this as a warning for now...
+ }
+ output.put(line);
+ output.put('\n');
+
+ return changed;
+ }
+
+ String stripContinuation(String)(String line)
+ {
+ line = line.stripLeft();
+ auto match = line.matchAll(this.commentRe);
+ if (match)
+ {
+ auto captures = match.front;
+ line = captures.post.stripLeft();
+ }
+ return line;
+ }
+
+ bool isComplete(Match)(Match match)
+ {
+ auto captures = match.front;
+ return captures.length >= 5 && captures[4] in this.holders;
+ }
+
+ bool updateCopyright(String, Match)(ref String line, Match match, ref int errors)
+ {
+ auto captures = match.front;
+ if (captures.length < 5)
+ {
+ stderr.writeln(this.location, ": missing copyright holder");
+ errors++;
+ return false;
+ }
+
+ // See if copyright is associated with package author.
+ // Update the author so as to be consistent everywhere.
+ auto holder = captures[4];
+ if (holder !in this.holders)
+ {
+ stderr.writeln(this.location, ": unrecognised copyright holder: ", holder);
+ errors++;
+ return false;
+ }
+ else if (!this.holders[holder])
+ return false;
+
+ // Update the copyright years.
+ auto years = captures[2].strip;
+ if (!this.canonicalizeYears(years))
+ {
+ stderr.writeln(this.location, ": unrecognised year string: ", years);
+ errors++;
+ return false;
+ }
+
+ // Make sure (C) is present.
+ auto intro = captures[1];
+ if (intro.endsWith("right"))
+ intro ~= " " ~ this.copyright;
+ else if (intro.endsWith("(c)"))
+ intro = intro[0 .. $ - 3] ~ this.copyright;
+
+ // Construct the copyright line, removing any 'by '.
+ auto newline = captures.pre ~ intro ~ " " ~ years ~ " by " ~ this.author ~ captures.post;
+ if (line != newline)
+ {
+ line = newline;
+ return true;
+ }
+ return false;
+ }
+
+ bool canonicalizeYears(String)(ref String years)
+ {
+ import std.conv : to;
+ import std.datetime : Clock;
+
+ auto yearList = years.matchAll(this.yearRe).map!(m => m.front).array;
+ if (yearList.length > 0)
+ {
+ auto minYear = yearList[0];
+ auto maxYear = yearList[$ - 1];
+
+ // Update the upper bound, if enabled.
+ if (this.updateYear)
+ maxYear = to!String(Clock.currTime.year);
+
+ // Use a range.
+ if (minYear == maxYear)
+ years = minYear;
+ else
+ years = minYear ~ "-" ~ maxYear;
+ return true;
+ }
+ return false;
+ }
+
+public:
+ this(bool updateYear, bool verbose)
+ {
+ this.updateYear = updateYear;
+ this.verbose = verbose;
+
+ this.copyrightRe = regex(`([Cc]opyright` ~ `|[Cc]opyright\s+\([Cc]\))` ~
+ `(\s*(?:` ~ rangesStr ~ `,?)\s*)` ~
+ `(by\s+)?` ~
+ `(` ~ nameStr ~ `(?:\s?` ~ nameStr ~ `)*)?`);
+ this.otherCopyrightRe = regex(`copyright.*[0-9][0-9]`, `i`);
+ this.yearRe = regex(`[0-9?]+`);
+ this.continuationRe = regex(rangesStr ~ `|` ~ nameStr);
+ this.commentRe = regex(`#+|[*]+|;+|//+`);
+
+ this.holders = [
+ "Digital Mars" : true,
+ "Digital Mars, All Rights Reserved" : true,
+ "The D Language Foundation, All Rights Reserved" : true,
+ "The D Language Foundation" : true,
+
+ // List of external authors.
+ "Northwest Software" : false,
+ "RSA Data Security, Inc. All rights reserved." : false,
+ "Symantec" : false,
+ ];
+ }
+
+ // Main loop.
+ void run(string[] args)
+ {
+ // Returns true if entry should be skipped for processing.
+ bool skipPath(DirEntry entry)
+ {
+ import std.path : baseName, dirName, pathSplitter;
+
+ if (!entry.isFile)
+ return true;
+
+ if (entry.dirName.pathSplitter.filter!(d => this.skipDirs.canFind(d)).any)
+ return true;
+
+ auto basename = entry.baseName;
+ if (this.skipFiles.canFind!(s => basename.endsWith(s)))
+ {
+ if (this.verbose)
+ stderr.writeln(entry, ": skipping file");
+ return true;
+ }
+ return false;
+ }
+
+ args.map!(arg => arg.dirEntries(SpanMode.depth).filter!(a => !skipPath(a)))
+ .joiner.each!(f => this.processFile(f));
+ }
+}