1 /++
2  + Authors: Christian Koestlin
3  + Copyright: Copyright © 2018, Christian Köstlin
4  + License: MIT
5  +/
6 
7 module ponies.dlang;
8 
9 import dyaml;
10 import ponies;
11 import std.algorithm;
12 import std.array;
13 import std.conv;
14 import std.experimental.logger;
15 import std.file;
16 import std.regex;
17 import std.stdio;
18 import std..string;
19 import std.traits;
20 
21 enum ProtectionLevel
22 {
23     Private,
24     Protected,
25     Public
26 }
27 
28 abstract class DlangPony : Pony
29 {
30     protected auto dubSdl = "dub.sdl";
31     protected auto travisYml = ".travis.yml";
32 
33     override bool applicable()
34     {
35         return exists(dubSdl);
36     }
37 
38     protected auto getFromDubSdl(string what)
39     {
40         auto pattern = "^%1$s \"(?P<%1$s>.*)\"$".format(what);
41         auto text = readText(dubSdl);
42         auto match = matchFirst(text, regex(pattern, "m"));
43         return match[what];
44     }
45 
46     protected auto sources()
47     {
48         return dirEntries("source", "*.d", SpanMode.depth);
49     }
50 }
51 
52 class DDoxPony : DlangPony
53 {
54     override string name()
55     {
56         return "Setup ddox in %s".format(dubSdl);
57     }
58 
59     override CheckStatus check()
60     {
61         try
62         {
63             auto content = readText(dubSdl);
64             return content.canFind("x:ddoxFilterArgs").to!CheckStatus;
65         }
66         catch (FileException e)
67         {
68             return CheckStatus.todo;
69         }
70     }
71 
72     override void run()
73     {
74         append(dubSdl, "x:ddoxFilterArgs \"--min-protection=%s\"\n".format(askFor!ProtectionLevel));
75     }
76 }
77 
78 class FormatSourcesPony : DlangPony
79 {
80     override string name()
81     {
82         return "Formats sources with dfmt";
83     }
84 
85     override CheckStatus check()
86     {
87         return CheckStatus.dont_know;
88     }
89 
90     override void run()
91     {
92         foreach (string file; sources)
93         {
94             import std.process;
95 
96             auto oldContent = readText(file);
97             ["dfmt", "-i", file].execute;
98             auto newContent = readText(file);
99             if (oldContent != newContent)
100             {
101                 "FormatSources:%s changed by dfmt -i".format(file).warning;
102             }
103         }
104     }
105 }
106 
107 class CopyrightCommentPony : DlangPony
108 {
109     string[] noCopyrightFiles;
110     string copyright;
111     this()
112     {
113         copyright = applicable ? getFromDubSdl("copyright") : null;
114     }
115 
116     override string name()
117     {
118         return "Setup copyright headers in .d files (taken from %s)".format(dubSdl);
119     }
120 
121     override CheckStatus check()
122     {
123         auto res = appender!(string[]);
124         foreach (string file; sources)
125         {
126             auto content = readText(file);
127             auto pattern = "^ \\+ Copyright: %s$".format(copyright);
128             auto found = matchFirst(content, regex(pattern, "m"));
129             if (!found)
130             {
131                 res.put(file);
132             }
133         }
134         noCopyrightFiles = res.data;
135         return (noCopyrightFiles.length == 0).to!CheckStatus;
136     }
137 
138     override void run()
139     {
140         "Fixing copyright for %s".format(noCopyrightFiles).info;
141 
142         foreach (file; noCopyrightFiles)
143         {
144             auto content = readText(file);
145             auto newContent = replaceFirst(content, regex("^ \\+ Copyright: .*?$",
146                     "m"), " + Copyright: %s".format(copyright));
147             if (content == newContent)
148             {
149                 "Adding copyright %s to file %s".format(copyright, file).info;
150                 newContent = "/++\n + Copyright: %s\n +/\n\n".format(copyright.to!string) ~ content;
151             }
152             else
153             {
154                 "Change copyright to %s in file %s".format(copyright, file).info;
155             }
156             std.file.write(file, newContent);
157         }
158     }
159 }
160 
161 class AuthorsPony : DlangPony
162 {
163     override string name()
164     {
165         return "Setup correct authors line in all .d files (taken from git log)";
166     }
167 
168     override CheckStatus check()
169     {
170         return CheckStatus.dont_know;
171     }
172 
173     override void run()
174     {
175         foreach (file; sources)
176         {
177             import std.process;
178 
179             auto content = readText(file);
180             auto authors = ["git", "log", "--pretty=format:%an", file].execute.output.split("\n")
181                 .sort.uniq.join(", ");
182             auto authorsRegex = regex("^ \\+ Authors: (.*)$", "m");
183             auto hasAuthorsLine = !content.matchFirst(authorsRegex).empty;
184             auto newContent = replaceFirst(content, authorsRegex,
185                     " + Authors: %s".format(authors));
186             if (hasAuthorsLine)
187             {
188                 if (content == newContent)
189                 {
190                     "No change of authors in file %s".format(file).info;
191                 }
192                 else
193                 {
194                     "Change authors to %s in file %s".format(authors, file).info;
195                     std.file.write(file, newContent);
196                 }
197             }
198             else
199             {
200                 "Adding authors line %s to file %s".format(authors, file).warning;
201                 newContent = "/++\n + Authors: %s\n +/\n\n".format(authors) ~ content;
202                 std.file.write(file, newContent);
203             }
204         }
205 
206     }
207 }
208 
209 class LicenseCommentPony : DlangPony
210 {
211     string[] noLicenseFiles;
212     string license;
213 
214     this()
215     {
216         license = applicable ? getFromDubSdl("license") : null;
217     }
218 
219     override string name()
220     {
221         return "Setup license headers in .d files (taken from %s)".format(dubSdl);
222     }
223 
224     override CheckStatus check()
225     {
226         auto res = appender!(string[]);
227         foreach (string file; sources)
228         {
229             auto content = readText(file);
230             auto pattern = "^ \\+ License: %s$".format(license);
231             auto found = matchFirst(content, regex(pattern, "m"));
232             if (!found)
233             {
234                 res.put(file);
235             }
236         }
237         noLicenseFiles = res.data;
238         return (noLicenseFiles.length == 0).to!CheckStatus;
239     }
240 
241     override void run()
242     {
243         "Fixing license for %s".format(noLicenseFiles).info;
244 
245         foreach (file; noLicenseFiles)
246         {
247             auto content = readText(file);
248             auto newContent = replaceFirst(content, regex("^ \\+ License: .*?$",
249                     "m"), " + License: %s".format(license));
250             if (content == newContent)
251             {
252                 "Adding license %s to file %s".format(license, file).info;
253                 newContent = "/++\n + License: %s\n +/\n\n".format(license.to!string) ~ content;
254             }
255             else
256             {
257                 "Change license to %s in file %s".format(license, file).info;
258             }
259             std.file.write(file, newContent);
260         }
261 
262     }
263 }
264 
265 class GeneratePackageDependenciesPony : DlangPony
266 {
267     override string name()
268     {
269         return "Generate dependency diagrams.";
270     }
271 
272     override CheckStatus check()
273     {
274         return CheckStatus.dont_know;
275     }
276 
277     override void run()
278     {
279         import std.conv;
280         import std.file;
281         import std.json;
282         import std.stdio;
283         import std..string;
284 
285         class Package
286         {
287             string name;
288             Package[] dependencies;
289             bool visited;
290             this(string name)
291             {
292                 this.name = name;
293             }
294 
295             auto addDependency(Package p)
296             {
297                 dependencies ~= p;
298             }
299 
300             override string toString()
301             {
302                 return toString("");
303             }
304 
305             string toString(string indent)
306             {
307                 string res = indent;
308                 res ~= name ~ "\n";
309                 foreach (p; dependencies)
310                 {
311                     res ~= p.toString(indent ~ "  ");
312                 }
313                 return res;
314             }
315 
316             Package setVisited(bool value)
317             {
318                 visited = value;
319                 foreach (d; dependencies)
320                 {
321                     d.setVisited(value);
322                 }
323                 return this;
324             }
325 
326             string toDot(string indent = "")
327             {
328                 auto res = "";
329                 visited = true;
330                 foreach (d; dependencies)
331                 {
332                     res ~= "\n%s->%s".format(name, d.name);
333                     if (d.visited == false)
334                     {
335                         res ~= d.toDot(indent ~ "  ");
336                     }
337                 }
338                 return res;
339             }
340 
341         }
342 
343         struct Packages
344         {
345             Package[string] packages;
346             Package addOrGet(string name)
347             {
348                 if (name !in packages)
349                 {
350                     auto newPackage = new Package(name);
351                     packages[name] = newPackage;
352                 }
353                 return packages[name];
354             }
355         }
356 
357         import std.process;
358 
359         "dub describe > out/dependencies.json".executeShell;
360 
361         auto json = parseJSON(readText("out/dependencies.json"));
362         auto packages = Packages();
363         auto rootPackage = json["rootPackage"].str;
364         writeln(rootPackage);
365 
366         foreach (size_t index, value; json["packages"])
367         {
368             auto packageName = value["name"];
369             auto newPackage = packages.addOrGet(packageName.str);
370             foreach (size_t index, value; value["dependencies"])
371             {
372                 auto dep = packages.addOrGet(value.str);
373                 newPackage.addDependency(dep);
374             }
375         }
376 
377         stderr.writeln(packages.addOrGet(rootPackage).to!string);
378         auto dot = "digraph G {%s\n}\n".format(packages.addOrGet(rootPackage)
379                 .setVisited(false).toDot);
380         std.file.write("out/dependencies.dot", dot);
381 
382         import std.process;
383 
384         ["dot", "out/dependencies.dot", "-Tpng", "-o", "docs/images/dependencies.png"].execute;
385         ["dot", "out/dependencies.dot", "-Tsvg", "-o", "docs/images/dependencies.svg"].execute;
386 
387     }
388 
389 }
390 
391 class AddPackageVersionPony : DlangPony
392 {
393     string packageName;
394     string preBuildCommands;
395     string sourceFiles;
396     auto sourcePaths = "sourcePaths \"source\" \"out/generated/packageversion\"\n";
397     auto importPaths = "importPaths \"source\" \"out/generated/packageversion\"\n";
398     auto dubFetchPackageVersion = "dub fetch packageversion";
399     auto addPackageVersionDependency = `dependency "packageversion" version="~>0.0.12"
400 subConfiguration "packageversion" "library"
401 `;
402     this()
403     {
404         packageName = getFromDubSdl("name");
405         preBuildCommands = applicable ? "preBuildCommands \"dub run packageversion -- --packageName=%s\"\n".format(
406                 packageName) : null;
407         sourceFiles = applicable ? "sourceFile \"out/generated/packageversion/%s/packageversion.d".format(packageName)
408             : null;
409     }
410 
411     override string name()
412     {
413         return "Add automatic generation of package version to %s".format(dubSdl);
414     }
415 
416     override CheckStatus check()
417     {
418         auto dubSdlContent = readText(dubSdl);
419         auto travisYml = readText(travisYml);
420         return (dubSdlContent.canFind(sourcePaths) && dubSdlContent.canFind(importPaths)
421                 && dubSdlContent.canFind(preBuildCommands) && dubSdlContent.canFind(sourceFiles)
422                 && dubSdlContent.canFind(addPackageVersionDependency)
423                 && travisYml.canFind(dubFetchPackageVersion)).to!CheckStatus;
424     }
425 
426     override void run()
427     {
428         auto oldContent = readText(dubSdl);
429         auto content = oldContent;
430         if (!content.canFind(sourcePaths))
431         {
432             "Adding sourcePaths to %s".format(dubSdl).info;
433             content ~= sourcePaths;
434         }
435 
436         if (!content.canFind(importPaths))
437         {
438             "Adding importPaths to %s".format(dubSdl).info;
439             content ~= importPaths;
440         }
441         if (!content.canFind(preBuildCommands))
442         {
443             "Adding preBuildCommands to %s".format(dubSdl).info;
444             content ~= preBuildCommands;
445         }
446         if (!content.canFind(sourceFiles))
447         {
448             "Adding sourceFiles to %s".format(dubSdl).info;
449             content ~= sourceFiles;
450         }
451 
452         if (!content.canFind(addPackageVersionDependency))
453         {
454             "Adding packageversion dependency to %s".format(dubSdl).info;
455             content ~= addPackageVersionDependency;
456         }
457         if (content != oldContent)
458         {
459             "Writing new %s".format(dubSdl).info;
460             std.file.write(dubSdl, content);
461         }
462 
463         auto root = Loader(travisYml).load;
464         auto beforeInstall = root["before_install"];
465         if (beforeInstall.isScalar)
466         {
467             if (beforeInstall.as!string != dubFetchPackageVersion)
468             {
469                 "adding %s to %s".format(dubFetchPackageVersion, travisYml).info;
470                 root["before_install"] = Node([beforeInstall, Node(dubFetchPackageVersion)]);
471                 Dumper(travisYml).dump(root);
472             }
473         }
474         else if (beforeInstall.isSequence)
475         {
476             if (!beforeInstall.sequence!string.canFind(dubFetchPackageVersion))
477             {
478                 "adding %s to %s".format(dubFetchPackageVersion, travisYml).info;
479                 beforeInstall.add(Node(dubFetchPackageVersion));
480                 root["before_install"] = beforeInstall;
481                 Dumper(travisYml).dump(root);
482             }
483         }
484         else
485         {
486             throw new Exception("cannot process %s".format(travisYml));
487         }
488     }
489 }