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 classfinder;
9 
10 import godotutil..string;
11 import godotutil.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) { return node.tokens.front.index; }
29 size_t endLocation(in BaseNode node) { return node.tokens.back.index; }
30 
31 /// A named scope such as a class or struct and its location in the file
32 struct ScopeRange
33 {
34 	string name;
35 	enum Type { class_, struct_, template_ }
36 	Type type;
37 	size_t start, end;
38 }
39 
40 /// libdparse visitor to be used with a dsymbol-like simple parser
41 private class GDVisitor : ASTVisitor
42 {
43 	FileInfo file;
44 	string[] moduleName;
45 	string overrideName; // manually set the class; TODO: not implemented yet
46 	size_t[2][] overrideAttributeRanges;
47 
48 	ScopeRange[] scopeRanges;
49 	size_t[] classScopeRange; /// which ScopeRange each class corresponds to
50 
51 	alias visit = ASTVisitor.visit;
52 	override void visit(in ModuleDeclaration m)
53 	{
54 		moduleName = m.moduleName.identifiers.map!(t => cast(string)t.text).array;
55 		super.visit(m);
56 	}
57 
58 	// TODO: not implemented yet
59 	version(none) override void visit(in AtAttribute a)
60 	{
61 		import std.algorithm.searching : canFind;
62 
63 		if(a.argumentList.items.canFind!(e => (cast(PrimaryExpression)e) && (cast(PrimaryExpression)e).primary.text == "MainClass"))
64 		{
65 			overrideAttributeRanges ~= [a.startLocation, a.endLocation];
66 		}
67 	}
68 
69 	override void visit(in MixinTemplateName m)
70 	{
71 		import std.algorithm.searching;
72 		if(m.tokens.canFind!(t => t.text == "GodotNativeLibrary")) file.hasEntryPoint = true;
73 	}
74 
75 	override void visit(in StructDeclaration s)
76 	{
77 		ScopeRange range = ScopeRange(s.name.text, ScopeRange.Type.struct_, s.startLocation, s.endLocation);
78 		scopeRanges ~= range;
79 	}
80 	override void visit(in InterfaceDeclaration i)
81 	{
82 		ScopeRange range = ScopeRange(i.name.text, ScopeRange.Type.struct_, i.startLocation, i.endLocation);
83 		scopeRanges ~= range;
84 	}
85 	override void visit(in ClassDeclaration c)
86 	{
87 		string name = c.name.text.dup;
88 
89 		classScopeRange ~= scopeRanges.length;
90 		ScopeRange range = ScopeRange(name, ScopeRange.Type.class_, c.startLocation, c.endLocation);
91 		scopeRanges ~= range;
92 
93 		file.classes ~= name;
94 		if(c.name.text.toLower == moduleName.back || c.name.text.camelToSnake == moduleName.back)
95 		{
96 			if(!file.mainClass.empty) writefln!"Module %s: found multiple classes matching the module name (%s and %s)"(
97 				moduleName, file.mainClass, name);
98 			else file.mainClass = name;
99 		}
100 
101 		super.visit(c);
102 	}
103 }
104 
105 /// 
106 FileInfo parse(string path)
107 {
108 	RollbackAllocator rba;
109 	StringCache stringCache = StringCache(StringCache.defaultBucketCount);
110 
111 	ubyte[] bytes = cast(ubyte[])std.file.read(path);
112 
113 	LexerConfig lexerConfig;
114 	lexerConfig.fileName = path;
115 	auto tokens = getTokensForParser(bytes, lexerConfig, &stringCache);
116 
117 	Module m;
118 	m = parseModuleSimple(tokens, path, &rba);
119 
120 	auto visitor = new GDVisitor;
121 	visitor.file.name = path;
122 	// for root modules
123 	visitor.moduleName = [path.baseName.stripExtension];
124 
125 	m.accept(visitor);
126 	visitor.file.moduleName = visitor.moduleName.joiner(".").text;
127 	if(visitor.file.mainClass.empty && visitor.file.classes.length == 1) visitor.file.mainClass = visitor.file.classes[0];
128 
129 	foreach(ci, c; visitor.file.classes)
130 	{
131 		import std.algorithm : filter, multiSort, map;
132 
133 		auto cScope = visitor.scopeRanges[visitor.classScopeRange[ci]];
134 		// names of the scopes that enclose cScope, outermost first
135 		auto parentScopeNames = visitor.scopeRanges
136 			.filter!(sr => sr.start <= cScope.start && sr.end >= cScope.end)
137 			.array
138 			.multiSort!((a, b) => a.start < b.start, (a, b) => a.end > b.end)
139 			.map!(sr => sr.name);
140 
141 		visitor.file.classes[ci] = chain(only(visitor.file.moduleName), parentScopeNames).joiner(".").text;
142 	}
143 
144 	return visitor.file;
145 }
146 
147 
148 
149