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