1 /++
2 The class finder parses D files and lists any classes defined inside.
3 
4 It does not attempt to limit the search to Godot-related classes or expand
5 mixins; a completely accurate class list would require its own compiler or a
6 compiler plugin system for D to list the classes as they're compiled.
7 +/
8 module godot.tools.classfinder;
9 
10 import godot.util.string;
11 import godot.util.classes;
12 
13 import dparse.parser, dparse.lexer;
14 import dparse.ast;
15 import dparse.rollback_allocator;
16 
17 import dsymbol.conversion : parseModuleSimple;
18 
19 import std.file, std.path;
20 import std.string;
21 import std.range;
22 import std.meta;
23 import std.typecons : scoped;
24 import std.algorithm.iteration : joiner, map;
25 import std.conv : text;
26 import std.stdio : writeln, writefln;
27 
28 size_t startLocation(in BaseNode node) {
29     return node.tokens.front.index;
30 }
31 
32 size_t endLocation(in BaseNode node) {
33     return node.tokens.back.index;
34 }
35 
36 /// A named scope such as a class or struct and its location in the file
37 struct ScopeRange {
38     string name;
39     enum Type {
40         class_,
41         struct_,
42         template_
43     }
44 
45     Type type;
46     size_t start, end;
47 }
48 
49 /// libdparse visitor to be used with a dsymbol-like simple parser
50 private class GDVisitor : ASTVisitor {
51     FileInfo file;
52     string[] moduleName;
53     string overrideName; // manually set the class; TODO: not implemented yet
54     size_t[2][] overrideAttributeRanges;
55 
56     ScopeRange[] scopeRanges;
57     size_t[] classScopeRange; /// which ScopeRange each class corresponds to
58 
59     alias visit = ASTVisitor.visit;
60     override void visit(in ModuleDeclaration m) {
61         moduleName = m.moduleName.identifiers.map!(t => cast(string) t.text).array;
62         super.visit(m);
63     }
64 
65     // TODO: not implemented yet
66     version (none) override void visit(in AtAttribute a) {
67         import std.algorithm.searching : canFind;
68 
69         if (a.argumentList.items.canFind!(e => (cast(PrimaryExpression) e) && (
70                 cast(PrimaryExpression) e).primary.text == "MainClass")) {
71             overrideAttributeRanges ~= [a.startLocation, a.endLocation];
72         }
73     }
74 
75     override void visit(in MixinTemplateName m) {
76         import std.algorithm.searching;
77 
78         if (m.tokens.canFind!(t => t.text == "GodotNativeLibrary"))
79             file.hasEntryPoint = true;
80     }
81 
82     override void visit(in StructDeclaration s) {
83         ScopeRange range = ScopeRange(s.name.text, ScopeRange.Type.struct_, s.startLocation, s
84                 .endLocation);
85         scopeRanges ~= range;
86     }
87 
88     override void visit(in InterfaceDeclaration i) {
89         ScopeRange range = ScopeRange(i.name.text, ScopeRange.Type.struct_, i.startLocation, i
90                 .endLocation);
91         scopeRanges ~= range;
92     }
93 
94     override void visit(in ClassDeclaration c) {
95         string name = c.name.text.dup;
96 
97         classScopeRange ~= scopeRanges.length;
98         ScopeRange range = ScopeRange(name, ScopeRange.Type.class_, c.startLocation, c.endLocation);
99         scopeRanges ~= range;
100 
101         file.classes ~= name;
102         if (c.name.text.toLower == moduleName.back || c.name.text.camelToSnake == moduleName.back) {
103             if (!file.mainClass.empty)
104                 writefln!"Module %s: found multiple classes matching the module name (%s and %s)"(
105                     moduleName, file.mainClass, name);
106             else
107                 file.mainClass = name;
108         }
109 
110         super.visit(c);
111     }
112 }
113 
114 /// 
115 FileInfo parse(string path) {
116     RollbackAllocator rba;
117     StringCache stringCache = StringCache(StringCache.defaultBucketCount);
118 
119     ubyte[] bytes = cast(ubyte[]) std.file.read(path);
120 
121     LexerConfig lexerConfig;
122     lexerConfig.fileName = path;
123     auto tokens = getTokensForParser(bytes, lexerConfig, &stringCache);
124 
125     Module m;
126     m = parseModuleSimple(tokens, path, &rba);
127 
128     auto visitor = new GDVisitor;
129     visitor.file.name = path;
130     // for root modules
131     visitor.moduleName = [path.baseName.stripExtension];
132 
133     m.accept(visitor);
134     visitor.file.moduleName = visitor.moduleName.joiner(".").text;
135     if (visitor.file.mainClass.empty && visitor.file.classes.length == 1)
136         visitor.file.mainClass = visitor.file.classes[0];
137 
138     foreach (ci, c; visitor.file.classes) {
139         import std.algorithm : filter, multiSort, map;
140 
141         auto cScope = visitor.scopeRanges[visitor.classScopeRange[ci]];
142         // names of the scopes that enclose cScope, outermost first
143         auto parentScopeNames = visitor.scopeRanges
144             .filter!(sr => sr.start <= cScope.start && sr.end >= cScope.end)
145             .array
146             .multiSort!((a, b) => a.start < b.start, (a, b) => a.end > b.end)
147             .map!(sr => sr.name);
148 
149         visitor.file.classes[ci] = chain(only(visitor.file.moduleName), parentScopeNames).joiner(".")
150             .text;
151     }
152 
153     return visitor.file;
154 }