1 module workspaced.com.snippets.generator;
2 
3 import dparse.ast;
4 import dparse.lexer;
5 
6 import workspaced.api;
7 import workspaced.com.snippets;
8 import workspaced.dparseext;
9 
10 import std.algorithm;
11 import std.array;
12 import std.conv;
13 import std.meta : AliasSeq;
14 
15 /// Checks if a variable is suitable to be iterated over and finds out information about it
16 /// Params:
17 ///   ret = loop scope to manipulate
18 ///   variable = variable to check
19 /// Returns: true if this variable is suitable for iteration, false if not
20 bool fillLoopScopeInfo(ref SnippetLoopScope ret, const scope VariableUsage variable)
21 {
22 	// try to minimize false positives as much as possible here!
23 	// the first true result will stop the whole loop variable finding process
24 
25 	if (!variable.name.text.length)
26 		return false;
27 
28 	if (variable.type)
29 	{
30 		if (variable.type.typeSuffixes.length)
31 		{
32 			if (variable.type.typeSuffixes[$ - 1].array)
33 			{
34 				const isMap = !!variable.type.typeSuffixes[$ - 1].type;
35 
36 				ret.stringIterator = variable.type.type2
37 					&& variable.type.type2.builtinType.among!(tok!"char", tok!"wchar", tok!"dchar");
38 				ret.type = formatType(variable.type.typeConstructors,
39 						variable.type.typeSuffixes[0 .. $ - 1], variable.type.type2);
40 				ret.iterator = variable.name.text;
41 				ret.numItems = isMap ? 2 : 1;
42 				return true;
43 			}
44 		}
45 		else if (variable.type.type2 && variable.type.type2.typeIdentifierPart)
46 		{
47 			// hardcode string, wstring and dstring
48 			const t = variable.type.type2.typeIdentifierPart;
49 			if (!t.dot && !t.indexer && !t.typeIdentifierPart && t.identifierOrTemplateInstance)
50 			{
51 				const simpleTypeName = t.identifierOrTemplateInstance.identifier.tokenText;
52 				switch (simpleTypeName)
53 				{
54 				case "string":
55 				case "wstring":
56 				case "dstring":
57 					ret.stringIterator = true;
58 					ret.iterator = variable.name.text;
59 					return true;
60 				default:
61 					break;
62 				}
63 			}
64 		}
65 	}
66 
67 	if (variable.value)
68 	{
69 		if (variable.value.arrayInitializer)
70 		{
71 			bool isMap;
72 			auto items = variable.value.arrayInitializer.arrayMemberInitializations;
73 			if (items.length)
74 				isMap = !!items[0].assignExpression; // this is the value before the ':' or null for no key
75 
76 			ret.stringIterator = false;
77 			ret.iterator = variable.name.text;
78 			ret.numItems = isMap ? 2 : 1;
79 			return true;
80 		}
81 		else if (variable.value.assignExpression)
82 		{
83 			// TODO: determine if this is a loop variable based on value
84 		}
85 	}
86 
87 	return false;
88 }
89 
90 string formatType(const IdType[] typeConstructors, const TypeSuffix[] typeSuffixes, const Type2 type2) @trusted
91 {
92 	Type t = new Type();
93 	t.typeConstructors = cast(IdType[]) typeConstructors;
94 	t.typeSuffixes = cast(TypeSuffix[]) typeSuffixes;
95 	t.type2 = cast(Type2) type2;
96 	return astToString(t);
97 }
98 
99 /// Helper struct containing variable definitions and notable assignments which 
100 struct VariableUsage
101 {
102 	const Type type;
103 	const Token name;
104 	const NonVoidInitializer value;
105 }
106 
107 unittest
108 {
109 	import std.experimental.logger : globalLogLevel, LogLevel;
110 
111 	globalLogLevel = LogLevel.trace;
112 
113 	scope backend = new WorkspaceD();
114 	auto workspace = makeTemporaryTestingWorkspace;
115 	auto instance = backend.addInstance(workspace.directory);
116 	backend.register!SnippetsComponent;
117 	SnippetsComponent snippets = backend.get!SnippetsComponent(workspace.directory);
118 
119 	static immutable loopCode = `module something;
120 
121 void foo()
122 {
123 	int[] x = [1, 2, 3];
124 	// trivial loop
125 	
126 }
127 
128 void bar()
129 {
130 	auto houseNumbers = [1, 2, 3];
131 	// non-trivial (auto) loop
132 	
133 }
134 
135 void existing()
136 {
137 	int[3] items = [1, 2, 3];
138 	int item;
139 	// clashing name
140 	
141 }
142 
143 void strings()
144 {
145 	string x;
146 	int y;
147 	// characters of x
148 	
149 }
150 
151 void noValue()
152 {
153 	int hello;
154 	// no lists
155 	
156 }
157 
158 void map()
159 {
160 	auto map = ["hello": "world", "key": "value"];
161 	// key, value
162 
163 }`;
164 
165 	SnippetInfo i;
166 	SnippetLoopScope s;
167 
168 	i = snippets.determineSnippetInfo(null, loopCode, 18);
169 	shouldEqual(i.level, SnippetLevel.global);
170 	s = i.loopScope; // empty
171 	shouldEqual(s, SnippetLoopScope.init);
172 
173 	i = snippets.determineSnippetInfo(null, loopCode, 30);
174 	assert(i.level == SnippetLevel.other, i.stack.to!string);
175 	s = i.loopScope; // empty
176 	shouldEqual(s, SnippetLoopScope.init);
177 
178 	i = snippets.determineSnippetInfo(null, loopCode, 31);
179 	assert(i.level == SnippetLevel.method, i.stack.to!string);
180 	i = snippets.determineSnippetInfo(null, loopCode, 73);
181 	assert(i.level == SnippetLevel.method, i.stack.to!string);
182 	i = snippets.determineSnippetInfo(null, loopCode, 74);
183 	assert(i.level == SnippetLevel.global, i.stack.to!string);
184 
185 	i = snippets.determineSnippetInfo(null, loopCode, 43);
186 	shouldEqual(i.level, SnippetLevel.value);
187 	s = i.loopScope; // in value
188 	shouldEqual(s, SnippetLoopScope.init);
189 
190 	i = snippets.determineSnippetInfo(null, loopCode, 72);
191 	shouldEqual(i.level, SnippetLevel.method);
192 	s = i.loopScope; // trivial of x
193 	assert(s.supported);
194 	assert(!s.stringIterator);
195 	shouldEqual(s.type, "int");
196 	shouldEqual(s.iterator, "x");
197 
198 	i = snippets.determineSnippetInfo(null, loopCode, 150);
199 	shouldEqual(i.level, SnippetLevel.method);
200 	s = i.loopScope; // non-trivial of houseNumbers (should be named houseNumber)
201 	assert(s.supported);
202 	assert(!s.stringIterator);
203 	shouldBeNull(s.type);
204 	shouldEqual(s.iterator, "houseNumbers");
205 
206 	i = snippets.determineSnippetInfo(null, loopCode, 229);
207 	shouldEqual(i.level, SnippetLevel.method);
208 	s = i.loopScope; // non-trivial of items with existing variable name
209 	assert(s.supported);
210 	assert(!s.stringIterator);
211 	shouldEqual(s.type, "int");
212 	shouldEqual(s.iterator, "items");
213 
214 	i = snippets.determineSnippetInfo(null, loopCode, 290);
215 	shouldEqual(i.level, SnippetLevel.method);
216 	s = i.loopScope; // string iteration 
217 	assert(s.supported);
218 	assert(s.stringIterator);
219 	shouldBeNull(s.type);
220 	shouldEqual(s.iterator, "x");
221 
222 	i = snippets.determineSnippetInfo(null, loopCode, 337);
223 	shouldEqual(i.level, SnippetLevel.method);
224 	s = i.loopScope; // no predefined variable 
225 	assert(s.supported);
226 	assert(!s.stringIterator);
227 	shouldBeNull(s.type);
228 	shouldBeNull(s.iterator);
229 
230 	i = snippets.determineSnippetInfo(null, loopCode, 418);
231 	shouldEqual(i.level, SnippetLevel.method);
232 	s = i.loopScope; // hash map
233 	assert(s.supported);
234 	assert(!s.stringIterator);
235 	shouldBeNull(s.type);
236 	shouldEqual(s.iterator, "map");
237 	shouldEqual(s.numItems, 2);
238 }
239 
240 enum StackStorageScope(string val) = "if (done) return; auto __" ~ val
241 	~ "_scope = " ~ val ~ "; scope (exit) if (!done) " ~ val ~ " = __" ~ val ~ "_scope;";
242 enum SnippetLevelWrapper(SnippetLevel level) = "if (done) return; pushLevel("
243 	~ level.stringof ~ ", dec); scope (exit) popLevel(dec);";
244 enum FullSnippetLevelWrapper(SnippetLevel level) = SnippetLevelWrapper!level ~ " super.visit(dec);";
245 
246 class SnippetInfoGenerator : ASTVisitor
247 {
248 	alias visit = ASTVisitor.visit;
249 
250 	this(size_t position)
251 	{
252 		this.position = position;
253 	}
254 
255 	static foreach (T; AliasSeq!(Declaration, ImportBindings, ImportBind, ModuleDeclaration))
256 		override void visit(const T dec)
257 		{
258 			mixin(FullSnippetLevelWrapper!(SnippetLevel.other));
259 		}
260 
261 	override void visit(const StructBody dec)
262 	{
263 		mixin(FullSnippetLevelWrapper!(SnippetLevel.type));
264 	}
265 
266 	override void visit(const SpecifiedFunctionBody dec)
267 	{
268 		mixin(StackStorageScope!"variableStack");
269 		mixin(FullSnippetLevelWrapper!(SnippetLevel.method));
270 	}
271 
272 	override void visit(const StatementNoCaseNoDefault dec)
273 	{
274 		mixin(StackStorageScope!"variableStack");
275 		super.visit(dec);
276 	}
277 
278 	static foreach (T; AliasSeq!(Arguments, ExpressionNode))
279 		override void visit(const T dec)
280 		{
281 			mixin(FullSnippetLevelWrapper!(SnippetLevel.value));
282 		}
283 
284 	override void visit(const VariableDeclaration dec)
285 	{
286 		// not quite accurate for VariableDeclaration, should only be value after = sign
287 		mixin(SnippetLevelWrapper!(SnippetLevel.value));
288 		super.visit(dec);
289 
290 		foreach (t; dec.declarators)
291 		{
292 			trace("push variable ", variableStack.length, " ", t.name.text, " of type ",
293 					astToString(dec.type), " and value ", astToString(t.initializer));
294 			variableStack.assumeSafeAppend ~= VariableUsage(dec.type, t.name,
295 					t.initializer ? t.initializer.nonVoidInitializer : null);
296 		}
297 
298 		if (dec.autoDeclaration)
299 			foreach (t; dec.autoDeclaration.parts)
300 			{
301 				trace("push variable ", variableStack.length, " ", t.identifier.text,
302 						" of type auto and value ", astToString(t.initializer));
303 				variableStack.assumeSafeAppend ~= VariableUsage(dec.type, t.identifier,
304 						t.initializer ? t.initializer.nonVoidInitializer : null);
305 			}
306 	}
307 
308 	ref inout(SnippetInfo) value() inout
309 	{
310 		return ret;
311 	}
312 
313 	void pushLevel(SnippetLevel level, const BaseNode node)
314 	{
315 		if (done)
316 			return;
317 		trace("push ", level, " on ", typeid(node).name, " ", current, " -> ", node.tokens[0].index);
318 
319 		if (node.tokens.length)
320 		{
321 			current = node.tokens[0].index;
322 			if (current >= position)
323 			{
324 				done = true;
325 				trace("done");
326 				return;
327 			}
328 		}
329 		ret.stack.assumeSafeAppend ~= level;
330 	}
331 
332 	void popLevel(const BaseNode node)
333 	{
334 		if (done)
335 			return;
336 		trace("pop from ", typeid(node).name, " ", current, " -> ",
337 				node.tokens[$ - 1].index + node.tokens[$ - 1].tokenText.length);
338 
339 		if (node.tokens.length)
340 		{
341 			current = node.tokens[$ - 1].index + node.tokens[$ - 1].tokenText.length;
342 			if (current > position)
343 			{
344 				done = true;
345 				trace("done");
346 				return;
347 			}
348 		}
349 
350 		ret.stack.length--;
351 	}
352 
353 	bool done;
354 	VariableUsage[] variableStack;
355 	size_t position, current;
356 	SnippetInfo ret;
357 }