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