From 7f176093898bb4074ce5fc9879f6a3839ce9f064 Mon Sep 17 00:00:00 2001 From: Iain Buclaw Date: Sat, 7 Oct 2017 18:26:48 +0200 Subject: [PATCH] Add updatecopyright script. --- README.md | 1 + posix.mak | 3 +- updatecopyright.d | 391 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 updatecopyright.d 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)); + } +}