1 //          Copyright Brian Schott (Hackerpilot) 2012.
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 ctags;
7 
8 import dparse.parser;
9 import dparse.lexer;
10 import dparse.ast;
11 import std.algorithm;
12 import std.range;
13 import std.stdio;
14 import std.array;
15 import std.conv;
16 import std.typecons;
17 
18 /**
19  * Prints CTAGS information to the given file.
20  * Includes metadata according to exuberant format used by Vim.
21  * Params:
22  *     output = the file that Exuberant TAGS info is written to
23  *     fileNames = tags will be generated from these files
24  */
25 void printCtags(File output, string[] fileNames)
26 {
27 	string[] tags;
28 	LexerConfig config;
29 	StringCache cache = StringCache(StringCache.defaultBucketCount);
30 	foreach (fileName; fileNames)
31 	{
32 		File f = File(fileName);
33 		if (f.size == 0)
34 			continue;
35 		auto bytes = uninitializedArray!(ubyte[])(to!size_t(f.size));
36 		f.rawRead(bytes);
37 		auto tokens = getTokensForParser(bytes, config, &cache);
38 		Module m = parseModule(tokens.array, fileName, null, &doNothing);
39 
40 		auto printer = new CTagsPrinter;
41 		printer.fileName = fileName;
42 		printer.visit(m);
43 		tags ~= printer.tagLines;
44 	}
45 	output.write(
46 		"!_TAG_FILE_FORMAT\t2\n" ~ "!_TAG_FILE_SORTED\t1\n" ~ "!_TAG_FILE_AUTHOR\tBrian Schott\n" ~ "!_TAG_PROGRAM_URL\thttps://github.com/Hackerpilot/Dscanner/\n");
47 	tags.sort().copy(output.lockingTextWriter);
48 }
49 
50 private:
51 
52 /// States determining how an access modifier affects tags when traversing the
53 /// AST.
54 /// The assumption is that there are fewer AST nodes and patterns that affects
55 /// the whole scope.
56 /// Therefor the default was chosen to be Reset.
57 enum AccessState
58 {
59 	Reset, /// when ascending the AST reset back to the previous access.
60 	Keep /// when ascending the AST keep the new access.
61 }
62 
63 alias ContextType = Tuple!(string, "c", string, "access");
64 
65 void doNothing(string, size_t, size_t, string, bool)
66 {
67 }
68 
69 string paramsToString(Dec)(const Dec dec)
70 {
71 	import dparse.formatter : Formatter;
72 
73 	auto app = appender!string();
74 	auto formatter = new Formatter!(typeof(app))(app);
75 
76 	static if (is(Dec == FunctionDeclaration) || is(Dec == Constructor))
77 	{
78 		formatter.format(dec.parameters);
79 	}
80 	else static if (is(Dec == TemplateDeclaration))
81 	{
82 		formatter.format(dec.templateParameters);
83 	}
84 
85 	return app.data;
86 }
87 
88 final class CTagsPrinter
89 	: ASTVisitor
90 {
91 	override void visit(const ClassDeclaration dec)
92 	{
93 		tagLines ~= "%s\t%s\t%d;\"\tc\tline:%d%s%s\n".format(dec.name.text,
94 			fileName, dec.name.line, dec.name.line, context.c, context.access);
95 		auto c = context;
96 		context = ContextType("\tclass:" ~ dec.name.text, "\taccess:public");
97 		dec.accept(this);
98 		context = c;
99 	}
100 
101 	override void visit(const StructDeclaration dec)
102 	{
103 		if (dec.name == tok!"")
104 		{
105 			dec.accept(this);
106 			return;
107 		}
108 		tagLines ~= "%s\t%s\t%d;\"\ts\tline:%d%s%s\n".format(dec.name.text,
109 			fileName, dec.name.line, dec.name.line, context.c, context.access);
110 		auto c = context;
111 		context = ContextType("\tstruct:" ~ dec.name.text, "\taccess:public");
112 		dec.accept(this);
113 		context = c;
114 	}
115 
116 	override void visit(const InterfaceDeclaration dec)
117 	{
118 		tagLines ~= "%s\t%s\t%d;\"\ti\tline:%d%s%s\n".format(dec.name.text,
119 			fileName, dec.name.line, dec.name.line, context.c, context.access);
120 		auto c = context;
121 		context = ContextType("\tclass:" ~ dec.name.text, context.access);
122 		dec.accept(this);
123 		context = c;
124 	}
125 
126 	override void visit(const TemplateDeclaration dec)
127 	{
128 		auto params = paramsToString(dec);
129 
130 		tagLines ~= "%s\t%s\t%d;\"\tT\tline:%d%s%s\tsignature:%s\n".format(
131 			dec.name.text, fileName, dec.name.line, dec.name.line, context.c,
132 			context.access, params);
133 		auto c = context;
134 		context = ContextType("\ttemplate:" ~ dec.name.text, context.access);
135 		dec.accept(this);
136 		context = c;
137 	}
138 
139 	override void visit(const FunctionDeclaration dec)
140 	{
141 		auto params = paramsToString(dec);
142 
143 		tagLines ~= "%s\t%s\t%d;\"\tf\tline:%d%s%s\tsignature:%s\n".format(
144 			dec.name.text, fileName, dec.name.line, dec.name.line, context.c,
145 			context.access, params);
146 	}
147 
148 	override void visit(const Constructor dec)
149 	{
150 		auto params = paramsToString(dec);
151 
152 		tagLines ~= "this\t%s\t%d;\"\tf\tline:%d%s\tsignature:%s%s\n".format(fileName,
153 			dec.line, dec.line, context.c, params, context.access);
154 	}
155 
156 	override void visit(const Destructor dec)
157 	{
158 		tagLines ~= "~this\t%s\t%d;\"\tf\tline:%d%s\n".format(fileName,
159 			dec.line, dec.line, context.c);
160 	}
161 
162 	override void visit(const EnumDeclaration dec)
163 	{
164 		tagLines ~= "%s\t%s\t%d;\"\tg\tline:%d%s%s\n".format(dec.name.text,
165 			fileName, dec.name.line, dec.name.line, context.c, context.access);
166 		auto c = context;
167 		context = ContextType("\tenum:" ~ dec.name.text, context.access);
168 		dec.accept(this);
169 		context = c;
170 	}
171 
172 	override void visit(const UnionDeclaration dec)
173 	{
174 		if (dec.name == tok!"")
175 		{
176 			dec.accept(this);
177 			return;
178 		}
179 		tagLines ~= "%s\t%s\t%d;\"\tu\tline:%d%s\n".format(dec.name.text,
180 			fileName, dec.name.line, dec.name.line, context.c);
181 		auto c = context;
182 		context = ContextType("\tunion:" ~ dec.name.text, context.access);
183 		dec.accept(this);
184 		context = c;
185 	}
186 
187 	override void visit(const AnonymousEnumMember mem)
188 	{
189 		tagLines ~= "%s\t%s\t%d;\"\te\tline:%d%s\n".format(mem.name.text,
190 			fileName, mem.name.line, mem.name.line, context.c);
191 	}
192 
193 	override void visit(const EnumMember mem)
194 	{
195 		tagLines ~= "%s\t%s\t%d;\"\te\tline:%d%s\n".format(mem.name.text,
196 			fileName, mem.name.line, mem.name.line, context.c);
197 	}
198 
199 	override void visit(const VariableDeclaration dec)
200 	{
201 		foreach (d; dec.declarators)
202 		{
203 			tagLines ~= "%s\t%s\t%d;\"\tv\tline:%d%s%s\n".format(d.name.text,
204 				fileName, d.name.line, d.name.line, context.c, context.access);
205 		}
206 		dec.accept(this);
207 	}
208 
209 	override void visit(const AutoDeclaration dec)
210 	{
211 		foreach (i; dec.identifiers)
212 		{
213 			tagLines ~= "%s\t%s\t%d;\"\tv\tline:%d%s%s\n".format(i.text,
214 				fileName, i.line, i.line, context.c, context.access);
215 		}
216 		dec.accept(this);
217 	}
218 
219 	override void visit(const Invariant dec)
220 	{
221 		tagLines ~= "invariant\t%s\t%d;\"\tv\tline:%d%s%s\n".format(fileName,
222 			dec.line, dec.line, context.c, context.access);
223 	}
224 
225 	override void visit(const ModuleDeclaration dec)
226 	{
227 		context = ContextType("", "\taccess:public");
228 		dec.accept(this);
229 	}
230 
231 	override void visit(const Attribute attribute)
232 	{
233 		if (attribute.attribute != tok!"")
234 		{
235 			switch (attribute.attribute.type)
236 			{
237 			case tok!"export":
238 				context.access = "\taccess:public";
239 				break;
240 			case tok!"public":
241 				context.access = "\taccess:public";
242 				break;
243 			case tok!"package":
244 				context.access = "\taccess:protected";
245 				break;
246 			case tok!"protected":
247 				context.access = "\taccess:protected";
248 				break;
249 			case tok!"private":
250 				context.access = "\taccess:private";
251 				break;
252 			default:
253 			}
254 		}
255 
256 		attribute.accept(this);
257 	}
258 
259 	override void visit(const AttributeDeclaration dec)
260 	{
261 		accessSt = AccessState.Keep;
262 		dec.accept(this);
263 	}
264 
265 	override void visit(const Declaration dec)
266 	{
267 		auto c = context;
268 		dec.accept(this);
269 
270 		final switch (accessSt) with (AccessState)
271 		{
272 		case Reset:
273 			context = c;
274 			break;
275 		case Keep:
276 			break;
277 		}
278 		accessSt = AccessState.Reset;
279 	}
280 
281 	override void visit(const Unittest dec)
282 	{
283 		// skipping symbols inside a unit test to not clutter the ctags file
284 		// with "temporary" symbols.
285 		// TODO when phobos have a unittest library investigate how that could
286 		// be used to describe the tests.
287 		// Maybe with UDA's to give the unittest a "name".
288 	}
289 
290 	override void visit(const AliasDeclaration dec)
291 	{
292 		// Old style alias
293 		if (dec.identifierList)
294 		{
295 			foreach (i; dec.identifierList.identifiers)
296 			{
297 				tagLines ~= "%s\t%s\t%d;\"\ta\tline:%d%s%s\n".format(i.text,
298 					fileName, i.line, i.line, context.c, context.access);
299 			}
300 		}
301 		dec.accept(this);
302 	}
303 
304 	override void visit(const AliasInitializer dec)
305 	{
306 		tagLines ~= "%s\t%s\t%d;\"\ta\tline:%d%s%s\n".format(dec.name.text,
307 			fileName, dec.name.line, dec.name.line, context.c, context.access);
308 
309 		dec.accept(this);
310 	}
311 
312 	override void visit(const AliasThisDeclaration dec)
313 	{
314 		auto name = dec.identifier;
315 		tagLines ~= "%s\t%s\t%d;\"\ta\tline:%d%s%s\n".format(name.text,
316 			fileName, name.line, name.line, context.c, context.access);
317 
318 		dec.accept(this);
319 	}
320 
321 	alias visit = ASTVisitor.visit;
322 	string fileName;
323 	string[] tagLines;
324 	ContextType context;
325 	AccessState accessSt;
326 }