1 //          Copyright Mario Kröplin 2015.
2 // Distributed under the Boost Software License, Version 1.0.
3 //    (See accompanying file LICENSE_1_0.txt or copy at
4 //          http://www.boost.org/LICENSE_1_0.txt)
5 
6 module outliner;
7 
8 import dparse.ast;
9 import dparse.formatter;
10 import dparse.lexer;
11 import std.algorithm;
12 import std.array;
13 import std.conv;
14 import std.stdio;
15 import std.typecons;
16 
17 class Outliner : ASTVisitor
18 {
19     private File output;
20 
21     private string fileName;
22 
23     private Classifier classifier;
24 
25     private string visibility = "+";
26 
27     private string[] modifiers;
28 
29     private Classifier[] classifiers = null;
30 
31     alias visit = ASTVisitor.visit;
32 
33     public this(File output, string fileName)
34     {
35         this.output = output;
36         this.fileName = fileName;
37     }
38 
39     public override void visit(const AttributeDeclaration attributeDeclaration)
40     {
41         const attributes = protectionAttributes(attributeDeclaration.attribute);
42         if (!attributes.empty)
43             visibility = attributes.back.attribute.toVisibility;
44     }
45 
46     public override void visit(const ClassDeclaration classDeclaration)
47     {
48         auto qualifiedName = classifier.qualifiedName;
49         auto fullyQualifiedName = classifier.fullyQualifiedName;
50         auto outliner = scoped!Outliner(output, fileName);
51         with (outliner)
52         {
53             classifier.type = "class";
54             classifier.qualifiedName = qualifiedName ~ classDeclaration.name.text;
55             classifier.fullyQualifiedName = fullyQualifiedName ~ classDeclaration.name.text;
56             classDeclaration.accept(outliner);
57         }
58         classifiers ~= outliner.classifier ~ outliner.classifiers;
59     }
60 
61     public override void visit(const Constructor constructor)
62     {
63         Method method;
64         method.visibility = visibility;
65         method.name = "this";
66         auto app = appender!(char[]);
67         app.format(constructor.parameters);
68         method.parameters = app.data.to!string;
69         classifier.methods ~= method;
70     }
71 
72     public override void visit(const Declaration declaration)
73     {
74         string visibility = this.visibility;
75         const attributes = protectionAttributes(declaration);
76         if (!attributes.empty)
77             this.visibility = attributes.back.attribute.toVisibility;
78         this.modifiers = declaration.modifiers;
79         super.visit(declaration);
80         if (!attributes.empty)
81             this.visibility = visibility;
82     }
83 
84     public override void visit(const Destructor destructor)
85     {
86         Method method;
87         method.visibility = visibility;
88         method.name = "~this";
89         classifier.methods ~= method;
90     }
91 
92     public override void visit(const EnumDeclaration enumDeclaration)
93     {
94         auto qualifiedName = classifier.qualifiedName;
95         auto fullyQualifiedName = classifier.fullyQualifiedName;
96         auto outliner = scoped!Outliner(output, fileName);
97         with (outliner)
98         {
99             classifier.type = "enum";
100             classifier.qualifiedName = qualifiedName ~ enumDeclaration.name.text;
101             classifier.fullyQualifiedName = fullyQualifiedName ~ enumDeclaration.name.text;
102             enumDeclaration.accept(outliner);
103         }
104         classifiers ~= outliner.classifier ~ outliner.classifiers;
105     }
106 
107     public override void visit(const EnumMember enumMember)
108     {
109         Field field;
110         field.name = enumMember.name.text;
111         classifier.fields ~= field;
112     }
113 
114     public override void visit(const FunctionDeclaration functionDeclaration)
115     {
116         Method method;
117         method.visibility = visibility;
118         method.modifiers = modifiers.dup;
119         method.name = functionDeclaration.name.text;
120         if (functionDeclaration.hasAuto)
121             method.modifiers ~= "auto";
122         if (functionDeclaration.hasRef)
123             method.modifiers ~= "ref";
124         if (functionDeclaration.returnType !is null)
125         {
126             auto app = appender!(char[]);
127             app.format(functionDeclaration.returnType);
128             method.type = app.data.to!string;
129         }
130         auto app = appender!(char[]);
131         app.format(functionDeclaration.parameters);
132         method.parameters = app.data.to!string;
133         classifier.methods ~= method;
134     }
135 
136     public override void visit(const InterfaceDeclaration interfaceDeclaration)
137     {
138         auto qualifiedName = classifier.qualifiedName;
139         auto fullyQualifiedName = classifier.fullyQualifiedName;
140         auto outliner = scoped!Outliner(output, fileName);
141         with (outliner)
142         {
143             classifier.type = "interface";
144             classifier.qualifiedName = qualifiedName ~ interfaceDeclaration.name.text;
145             classifier.fullyQualifiedName = fullyQualifiedName ~ interfaceDeclaration.name.text;
146             interfaceDeclaration.accept(outliner);
147         }
148         classifiers ~= outliner.classifier ~ outliner.classifiers;
149     }
150 
151     public override void visit(const Invariant invariant_)
152     {
153         // skip
154     }
155 
156     public override void visit(const Module module_)
157     {
158         import std.string : toLower;
159 
160         classifier.fullyQualifiedName = module_.moduleDeclaration.moduleName.identifiers.map!"a.text".array;
161 
162         super.visit(module_);
163         string name;
164         if (module_.moduleDeclaration is null)
165         {
166             import std.path : baseName, stripExtension;
167             name = fileName.stripExtension.baseName;
168         }
169         else
170         {
171             name = module_.moduleDeclaration.moduleName.identifiers.back.text;
172         }
173         name = name.toLower;  // XXX workaround for module Foo; class Foo;
174         if (!classifier.fields.empty || !classifier.methods.empty)
175         {
176             classifier.type = "class";
177             classifier.qualifiedName = [name];
178             classifier.stereotype = "<<(M,gold)>>";
179             classifier.write(output.lockingTextWriter);
180         }
181         foreach (classifier; classifiers)
182             classifier.write(output.lockingTextWriter);
183     }
184 
185     public override void visit(const SharedStaticConstructor sharedStaticConstructor)
186     {
187         Method method;
188         method.visibility = visibility;
189         method.modifiers = ["{static}", "shared"];
190         method.name = "this";
191         classifier.methods ~= method;
192     }
193 
194     public override void visit(const SharedStaticDestructor sharedStaticDestructor)
195     {
196         Method method;
197         method.visibility = visibility;
198         method.modifiers = ["{static}", "shared"];
199         method.name = "~this";
200         classifier.methods ~= method;
201     }
202 
203     public override void visit(const StaticConstructor staticConstructor)
204     {
205         Method method;
206         method.visibility = visibility;
207         method.modifiers = ["{static}"];
208         method.name = "this";
209         classifier.methods ~= method;
210     }
211 
212     public override void visit(const StaticDestructor staticDestructor)
213     {
214         Method method;
215         method.visibility = visibility;
216         method.modifiers = ["{static}"];
217         method.name = "~this";
218         classifier.methods ~= method;
219     }
220 
221     public override void visit(const StructDeclaration structDeclaration)
222     {
223         auto qualifiedName = classifier.qualifiedName;
224         auto fullyQualifiedName = classifier.fullyQualifiedName;
225         auto outliner = scoped!Outliner(output, fileName);
226         with (outliner)
227         {
228             classifier.type = "class";
229             classifier.qualifiedName = qualifiedName ~ structDeclaration.name.text;
230             classifier.fullyQualifiedName = fullyQualifiedName ~ structDeclaration.name.text;
231             classifier.stereotype = "<<(S,silver)>>";
232             structDeclaration.accept(outliner);
233         }
234         classifiers ~= outliner.classifier ~ outliner.classifiers;
235     }
236 
237     public override void visit(const Unittest unittest_)
238     {
239         // skip
240     }
241 
242     public override void visit(const VariableDeclaration variableDeclaration)
243     {
244         Field field;
245         field.visibility = visibility;
246         field.modifiers = modifiers.dup;
247         if (variableDeclaration.type !is null)
248         {
249             auto app = appender!(char[]);
250             app.format(variableDeclaration.type);
251             field.type = app.data.to!string;
252         }
253         foreach (declarator; variableDeclaration.declarators)
254         {
255             field.name = declarator.name.text;
256             classifier.fields ~= field;
257         }
258     }
259 }
260 
261 struct Classifier
262 {
263     const string indent = "  ";
264 
265     string type;
266 
267     string[] qualifiedName = null;
268 
269     const(string)[] fullyQualifiedName = null;
270 
271     string stereotype;
272 
273     Field[] fields;
274 
275     Method[] methods;
276 
277     void write(Sink)(Sink sink) const
278     {
279         sink.put(type);
280         sink.put(' ');
281         foreach (index, name; qualifiedName)
282         {
283             if (index > 0)
284                 sink.put('.');
285             sink.put(name);
286         }
287         sink.put(' ');
288         if (!stereotype.empty)
289         {
290             sink.put(stereotype);
291             sink.put(' ');
292         }
293         sink.put("$generated");
294         sink.put(' ');
295         sink.put("$");
296         foreach (index, name; fullyQualifiedName)
297         {
298             if (index > 0)
299                 sink.put('.');
300             sink.put(name);
301         }
302         sink.put(' ');
303         sink.put("{");
304         sink.put('\n');
305         foreach (field; fields)
306         {
307             sink.put(indent);
308             field.write(sink);
309             sink.put('\n');
310         }
311         foreach (method; methods)
312         {
313             sink.put(indent);
314             method.write(sink);
315             sink.put('\n');
316         }
317         sink.put("}");
318         sink.put('\n');
319     }
320 }
321 
322 struct Field
323 {
324     string visibility;
325 
326     string[] modifiers;
327 
328     string type;
329 
330     string name;
331 
332     void write(Sink)(Sink sink) const
333     {
334         // override check for parenthesis to choose between methods and fields
335         sink.put("{field} ");
336         sink.put(visibility);
337         foreach (modifier; modifiers)
338         {
339             sink.put(modifier);
340             sink.put(' ');
341         }
342         if (!type.empty)
343         {
344             sink.put(type);
345             sink.put(' ');
346         }
347         sink.put(name);
348     }
349 }
350 
351 struct Method
352 {
353     string visibility;
354 
355     string[] modifiers;
356 
357     string type;
358 
359     string name;
360 
361     string parameters = "()";
362 
363     void write(Sink)(Sink sink) const
364     {
365         sink.put(visibility);
366         foreach (modifier; modifiers)
367         {
368             sink.put(modifier);
369             sink.put(' ');
370         }
371         if (!type.empty)
372         {
373             sink.put(type);
374             sink.put(' ');
375         }
376         sink.put(name);
377         sink.put(escape(parameters));
378     }
379 }
380 
381 private string escape(string source) pure
382 {
383     return source.replace(`\`, `\\`);
384 }
385 
386 private const(Attribute[]) protectionAttributes(const Declaration declaration) pure
387 {
388     const(Attribute)[] attributes = null;
389     foreach (attribute; declaration.attributes)
390         attributes ~= protectionAttributes(attribute);
391     return attributes;
392 }
393 
394 private const(Attribute[]) protectionAttributes(const Attribute attribute) pure
395 {
396     return (attribute.attribute.type.isProtection) ? [attribute] : null;
397 }
398 
399 private string toVisibility(const Token token) pure
400 in
401 {
402     assert(token.type.isProtection);
403 }
404 body
405 {
406     switch (token.type)
407     {
408     case tok!"package":
409         return "~";
410     case tok!"private":
411         return "-";
412     case tok!"protected":
413         return "#";
414     case tok!"public":
415         return "+";
416     default:
417         return "+";
418     }
419 }
420 
421 private string[] modifiers(const Declaration declaration) pure
422 {
423     string[] modifiers = null;
424     if (declaration.attributes.any!(a => a.attribute == tok!"abstract"))
425         modifiers ~= "{abstract}";
426     if (declaration.attributes.any!(a => a.attribute == tok!"static"))
427         modifiers ~= "{static}";
428     return modifiers;
429 }