1 #!/usr/bin/env rdmd 2 3 /** 4 Update the copyright notices in source files so that they have the form: 5 --- 6 Copyright XXXX-YYYY by The D Language Foundation, All Rights Reserved 7 --- 8 It does not change copyright notices of authors that are known to have made 9 changes under a proprietary license. 10 11 Copyright: Copyright (C) 2017-2018 by The D Language Foundation, All Rights Reserved 12 13 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0) 14 15 Authors: Iain Buclaw 16 17 Example usage: 18 19 --- 20 updatecopyright.d --update-year src/dmd 21 --- 22 */ 23 24 module tools.updatecopyright; 25 26 int main(string[] args) 27 { 28 import std.getopt; 29 30 bool updateYear; 31 bool verbose; 32 auto opts = getopt(args, 33 "update-year|y", "Update the current year on every notice", &updateYear, 34 "verbose|v", "Be more verbose", &verbose); 35 36 if (args.length == 1 || opts.helpWanted) 37 { 38 defaultGetoptPrinter("usage: updatecopyright [--help|-h] [--update-year|-y] <dir>...", 39 opts.options); 40 return 0; 41 } 42 43 Copyright(updateYear, verbose).run(args[1 .. $]); 44 return 0; 45 } 46 47 //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 48 49 struct Copyright 50 { 51 import std.algorithm : any, canFind, each, filter, joiner, map; 52 import std.array : appender, array; 53 import std.file : DirEntry, SpanMode, dirEntries, remove, rename; 54 import std.stdio : File, stderr, stdout; 55 import std.string : endsWith, strip, stripLeft, stripRight; 56 import std.regex : Regex, matchAll, regex; 57 58 // The author to use in copyright notices. 59 enum author = "The D Language Foundation, All Rights Reserved"; 60 61 // The standard (C) form. 62 enum copyright = "(C)"; 63 64 private: 65 // True if running in verbose mode. 66 bool verbose = false; 67 68 // True if also updating copyright year. 69 bool updateYear = false; 70 71 // An associative array of known copyright holders. 72 // Value set to true if the copyright holder is internal. 73 bool[string] holders; 74 75 // Files and directories to ignore during search. 76 static string[] skipDirs = [ 77 "docs", 78 "ini", 79 "test", 80 "samples", 81 "vcbuild", 82 ".git", 83 ]; 84 85 static string[] skipFiles = [ 86 "Jenkinsfile", 87 "LICENSE.txt", 88 "VERSION", 89 ".a", 90 ".ddoc", 91 ".deps", 92 ".lst", 93 ".map", 94 ".md", 95 ".o", 96 ".obj", 97 ".sdl", 98 ".sh", 99 ".yml", 100 ]; 101 102 // Characters in a range of years. 103 // Include '.' for typos, and '?' for unknown years. 104 enum rangesStr = `[0-9?](?:[-0-9.,\s]|\s+and\s+)*[0-9]`; 105 106 // Non-whitespace characters in a copyright holder's name. 107 enum nameStr = `[\w.,-]`; 108 109 // Matches a full copyright notice: 110 // - 'Copyright (C)', etc. 111 // - The years. Includes the whitespace in the year, so that we can 112 // remove any excess. 113 // - 'by ', if used 114 // - The copyright holder. 115 Regex!char copyrightRe; 116 117 // A regexp for notices that might have slipped by. 118 Regex!char otherCopyrightRe; 119 120 // A regexp that matches one year. 121 Regex!char yearRe; 122 123 // Matches part of a year or copyright holder. 124 Regex!char continuationRe; 125 126 Regex!char commentRe; 127 128 // Convenience for passing around file/line number information. 129 struct FileLocation 130 { 131 string filename; 132 size_t linnum; 133 134 string toString() 135 { 136 import std.format : format; 137 return "%s(%d)".format(this.filename, this.linnum); 138 } 139 } 140 141 FileLocation location; 142 char[] previousLine; 143 144 void processFile(string filename) 145 { 146 import std.conv : to; 147 148 // Looks like something we tried to create before. 149 if (filename.endsWith(".tmp")) 150 { 151 remove(filename); 152 return; 153 } 154 155 auto file = File(filename, "rb"); 156 auto output = appender!string; 157 int errors = 0; 158 bool changed = false; 159 160 output.reserve(file.size.to!size_t); 161 162 // Reset file location information. 163 this.location = FileLocation(filename, 0); 164 this.previousLine = null; 165 166 foreach (line; file.byLine) 167 { 168 this.location.linnum++; 169 try 170 { 171 changed |= this.processLine(line, output, errors); 172 } 173 catch (Exception) 174 { 175 if (this.verbose) 176 stderr.writeln(filename, ": bad input file"); 177 errors++; 178 break; 179 } 180 } 181 file.close(); 182 183 // If something changed, write the new file out. 184 if (changed && !errors) 185 { 186 auto tmpfilename = filename ~ ".tmp"; 187 auto tmpfile = File(tmpfilename, "w"); 188 tmpfile.write(output.data); 189 tmpfile.close(); 190 rename(tmpfilename, filename); 191 } 192 } 193 194 bool processLine(String, Array)(String line, ref Array output, ref int errors) 195 { 196 bool changed = false; 197 198 if (this.previousLine) 199 { 200 auto continuation = this.stripContinuation(line); 201 202 // Merge the lines for matching purposes. 203 auto mergedLine = this.previousLine.stripRight() ~ `, ` ~ continuation; 204 auto mergedMatch = mergedLine.matchAll(copyrightRe); 205 206 if (!continuation.matchAll(this.continuationRe) || 207 !mergedMatch || !this.isComplete(mergedMatch)) 208 { 209 // If the next line doesn't look like a proper continuation, 210 // assume that what we've got is complete. 211 auto match = this.previousLine.matchAll(copyrightRe); 212 changed |= this.updateCopyright(line, match, errors); 213 output.put(this.previousLine); 214 output.put('\n'); 215 } 216 else 217 { 218 line = mergedLine; 219 } 220 this.previousLine = null; 221 } 222 223 auto match = line.matchAll(copyrightRe); 224 if (match) 225 { 226 // If it looks like the copyright is incomplete, add the next line. 227 if (!this.isComplete(match)) 228 { 229 this.previousLine = line.dup; 230 return changed; 231 } 232 changed |= this.updateCopyright(line, match, errors); 233 } 234 else if (line.matchAll(this.otherCopyrightRe)) 235 { 236 stderr.writeln(this.location, ": unrecognised copyright: ", line.strip); 237 //errors++; // Only treat this as a warning for now... 238 } 239 output.put(line); 240 output.put('\n'); 241 242 return changed; 243 } 244 245 String stripContinuation(String)(String line) 246 { 247 line = line.stripLeft(); 248 auto match = line.matchAll(this.commentRe); 249 if (match) 250 { 251 auto captures = match.front; 252 line = captures.post.stripLeft(); 253 } 254 return line; 255 } 256 257 bool isComplete(Match)(Match match) 258 { 259 auto captures = match.front; 260 return captures.length >= 5 && captures[4] in this.holders; 261 } 262 263 bool updateCopyright(String, Match)(ref String line, Match match, ref int errors) 264 { 265 auto captures = match.front; 266 if (captures.length < 5) 267 { 268 stderr.writeln(this.location, ": missing copyright holder"); 269 errors++; 270 return false; 271 } 272 273 // See if copyright is associated with package author. 274 // Update the author so as to be consistent everywhere. 275 auto holder = captures[4]; 276 if (holder !in this.holders) 277 { 278 stderr.writeln(this.location, ": unrecognised copyright holder: ", holder); 279 errors++; 280 return false; 281 } 282 else if (!this.holders[holder]) 283 return false; 284 285 // Update the copyright years. 286 auto years = captures[2].strip; 287 if (!this.canonicalizeYears(years)) 288 { 289 stderr.writeln(this.location, ": unrecognised year string: ", years); 290 errors++; 291 return false; 292 } 293 294 // Make sure (C) is present. 295 auto intro = captures[1]; 296 if (intro.endsWith("right")) 297 intro ~= " " ~ this.copyright; 298 else if (intro.endsWith("(c)")) 299 intro = intro[0 .. $ - 3] ~ this.copyright; 300 301 // Construct the copyright line, removing any 'by '. 302 auto newline = captures.pre ~ intro ~ " " ~ years ~ " by " ~ this.author ~ captures.post; 303 if (line != newline) 304 { 305 line = newline; 306 return true; 307 } 308 return false; 309 } 310 311 bool canonicalizeYears(String)(ref String years) 312 { 313 import std.conv : to; 314 import std.datetime : Clock; 315 316 auto yearList = years.matchAll(this.yearRe).map!(m => m.front).array; 317 if (yearList.length > 0) 318 { 319 auto minYear = yearList[0]; 320 auto maxYear = yearList[$ - 1]; 321 322 // Update the upper bound, if enabled. 323 if (this.updateYear) 324 maxYear = to!String(Clock.currTime.year); 325 326 // Use a range. 327 if (minYear == maxYear) 328 years = minYear; 329 else 330 years = minYear ~ "-" ~ maxYear; 331 return true; 332 } 333 return false; 334 } 335 336 public: 337 this(bool updateYear, bool verbose) 338 { 339 this.updateYear = updateYear; 340 this.verbose = verbose; 341 342 this.copyrightRe = regex(`([Cc]opyright` ~ `|[Cc]opyright\s+\([Cc]\))` ~ 343 `(\s*(?:` ~ rangesStr ~ `,?)\s*)` ~ 344 `(by\s+)?` ~ 345 `(` ~ nameStr ~ `(?:\s?` ~ nameStr ~ `)*)?`); 346 this.otherCopyrightRe = regex(`copyright.*[0-9][0-9]`, `i`); 347 this.yearRe = regex(`[0-9?]+`); 348 this.continuationRe = regex(rangesStr ~ `|` ~ nameStr); 349 this.commentRe = regex(`#+|[*]+|;+|//+`); 350 351 this.holders = [ 352 "Digital Mars" : true, 353 "Digital Mars, All Rights Reserved" : true, 354 "The D Language Foundation, All Rights Reserved" : true, 355 "The D Language Foundation" : true, 356 357 // List of external authors. 358 "Northwest Software" : false, 359 "RSA Data Security, Inc. All rights reserved." : false, 360 "Symantec" : false, 361 ]; 362 } 363 364 // Main loop. 365 void run(string[] args) 366 { 367 // Returns true if entry should be skipped for processing. 368 bool skipPath(DirEntry entry) 369 { 370 import std.path : baseName, dirName, pathSplitter; 371 372 if (!entry.isFile) 373 return true; 374 375 if (entry.dirName.pathSplitter.filter!(d => this.skipDirs.canFind(d)).any) 376 return true; 377 378 auto basename = entry.baseName; 379 if (this.skipFiles.canFind!(s => basename.endsWith(s))) 380 { 381 if (this.verbose) 382 stderr.writeln(entry, ": skipping file"); 383 return true; 384 } 385 return false; 386 } 387 388 args.map!(arg => arg.dirEntries(SpanMode.depth).filter!(a => !skipPath(a))) 389 .joiner.each!(f => this.processFile(f)); 390 } 391 }