mirror of
https://github.com/dlang/tools.git
synced 2025-04-25 12:40:21 +03:00
391 lines
11 KiB
D
391 lines
11 KiB
D
#!/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] <dir>...",
|
|
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));
|
|
}
|
|
}
|