1 // Emacs tags based on ctags.d with this header:
2 //			Copyright Brian Schott (Hackerpilot) 2012.
3 // Distributed under the Boost Software License, Version 1.0.
4 //	  (See accompanying file LICENSE_1_0.txt or copy at
5 //			http://www.boost.org/LICENSE_1_0.txt)
6 
7 module etags;
8 
9 import dparse.parser;
10 import dparse.lexer;
11 import dparse.ast;
12 import dparse.rollback_allocator;
13 import std.algorithm;
14 import std.range;
15 import std.stdio;
16 import std.path;
17 import std.array;
18 import std.conv;
19 import std.string;
20 
21 // Prefix tags with their module name.	Seems like correct behavior, but just
22 // in case, make it an option.
23 version = UseModuleContext;
24 
25 // Could not find "official" definition of protection (public/private/etc).
26 // Behavior modeled here was reversed engineered based on dmd 2.067.
27 // class/interface and non-anonymous struct/union/enum members default to
28 // public, regardless of the enclosing declaration.	 template and anonymous
29 // struct/union/enum members default to the enclosing protection.
30 
31 /**
32  * Prints ETAGS information to the given file.
33  * Params:
34  *	   outpt = the file that ETAGS info is written to
35  *	   tagAll = if set, tag private/package declaration too
36  *	   fileNames = tags will be generated from these files
37  */
38 void printEtags(File output, bool tagAll, string[] fileNames)
39 {
40 	LexerConfig config;
41 	StringCache cache = StringCache(StringCache.defaultBucketCount);
42 	foreach (fileName; fileNames)
43 	{
44 		RollbackAllocator rba;
45 		File f = File(fileName);
46 		if (f.size == 0)
47 			continue;
48 		auto bytes = uninitializedArray!(ubyte[])(to!size_t(f.size));
49 		f.rawRead(bytes);
50 		auto tokens = getTokensForParser(bytes, config, &cache);
51 		Module m = parseModule(tokens.array, fileName, &rba, &doNothing);
52 
53 		auto printer = new EtagsPrinter;
54 		printer.moduleName = m.moduleFullName(fileName);
55 		version (UseModuleContext)
56 			printer.context = printer.moduleName ~ ".";
57 		printer.privateVisibility = tagAll ? Visibility.exposed : Visibility.hidden;
58 		printer.bytes = bytes.sansBOM;
59 		printer.visit(m);
60 
61 		output.writef("\f\n%s,%u\n", fileName, printer.tags.length);
62 		printer.tags.copy(output.lockingTextWriter);
63 	}
64 }
65 
66 private:
67 
68 enum Visibility
69 {
70 	exposed,
71 	hidden
72 }
73 
74 void doNothing(string, size_t, size_t, string, bool)
75 {
76 }
77 
78 ubyte[] sansBOM(ubyte[] bytes)
79 {
80 	// At least handle UTF-8 since there is some in druntime/phobos
81 	return (bytes.length >= 3 && bytes[0] == 0xef && bytes[1] == 0xbb && bytes[2] == 0xbf)
82 		? bytes[3 .. $] : bytes;
83 }
84 
85 string moduleFullName(Module m, string fileName)
86 {
87 	// When no module declaration, just use filename and hope its valid
88 	if (!m.moduleDeclaration)
89 		return fileName.baseName.stripExtension;
90 
91 	// reconstruct module full name
92 	return m.moduleDeclaration.moduleName.identifiers.map!(i => i.text).join(".");
93 }
94 
95 final class EtagsPrinter : ASTVisitor
96 {
97 	override void visit(const ModuleDeclaration dec)
98 	{
99 		auto tok0 = dec.moduleName.identifiers[0];
100 		auto was = context;
101 		context = "";
102 		maketag(moduleName, tok0.index, tok0.line);
103 		context = was;
104 		dec.accept(this);
105 	}
106 
107 	override void visit(const Declaration dec)
108 	{
109 		// Update value of visibility based on this 'dec'.
110 		if (dec.attributeDeclaration)
111 		{
112 			auto attr = dec.attributeDeclaration.attribute;
113 			updateVisibility(attr.attribute.type);
114 		}
115 
116 		// visibility needs to be restored to what it was when changed by
117 		// attribute.
118 		auto was = visibility;
119 		foreach (attr; dec.attributes)
120 		{
121 			updateVisibility(attr.attribute.type);
122 		}
123 
124 		dec.accept(this);
125 		visibility = was;
126 	}
127 
128 	override void visit(const ClassDeclaration dec)
129 	{
130 		maketag(dec.name);
131 		// class members default to public
132 		visibility = Visibility.exposed;
133 		acceptInContext(dec, dec.name.text);
134 	}
135 
136 	override void visit(const StructDeclaration dec)
137 	{
138 		if (dec.name == tok!"")
139 		{
140 			dec.accept(this);
141 			return;
142 		}
143 		maketag(dec.name);
144 		// struct members default to public
145 		visibility = Visibility.exposed;
146 		acceptInContext(dec, dec.name.text);
147 	}
148 
149 	override void visit(const InterfaceDeclaration dec)
150 	{
151 		maketag(dec.name);
152 		// interface members default to public
153 		visibility = Visibility.exposed;
154 		acceptInContext(dec, dec.name.text);
155 	}
156 
157 	override void visit(const TemplateDeclaration dec)
158 	{
159 		maketag(dec.name);
160 		acceptInContext(dec, dec.name.text);
161 	}
162 
163 	override void visit(const FunctionDeclaration dec)
164 	{
165 		maketag(dec.name);
166 		// don't tag declarations in a function like thing
167 		visibility = Visibility.hidden;
168 		acceptInContext(dec, dec.name.text);
169 	}
170 
171 	override void visit(const Constructor dec)
172 	{
173 		maketag("this", dec.location, dec.line);
174 		// don't tag declarations in a function like thing
175 		visibility = Visibility.hidden;
176 		acceptInContext(dec, "this");
177 	}
178 
179 	override void visit(const Destructor dec)
180 	{
181 		maketag("~this", dec.index, dec.line);
182 		// don't tag declarations in a function like thing
183 		visibility = Visibility.hidden;
184 		acceptInContext(dec, "~this");
185 	}
186 
187 	override void visit(const EnumDeclaration dec)
188 	{
189 		maketag(dec.name);
190 		// enum members default to public
191 		visibility = Visibility.exposed;
192 		acceptInContext(dec, dec.name.text);
193 	}
194 
195 	override void visit(const UnionDeclaration dec)
196 	{
197 		if (dec.name == tok!"")
198 		{
199 			dec.accept(this);
200 			return;
201 		}
202 		maketag(dec.name);
203 		// union members default to public
204 		visibility = Visibility.exposed;
205 		acceptInContext(dec, dec.name.text);
206 	}
207 
208 	override void visit(const AnonymousEnumMember mem)
209 	{
210 		maketag(mem.name);
211 	}
212 
213 	override void visit(const EnumMember mem)
214 	{
215 		maketag(mem.name);
216 	}
217 
218 	override void visit(const Unittest dec)
219 	{
220 		bool was = inUnittest;
221 		inUnittest = true;
222 		dec.accept(this);
223 		inUnittest = was;
224 	}
225 
226 	override void visit(const VariableDeclaration dec)
227 	{
228 		foreach (d; dec.declarators)
229 		{
230 			maketag(d.name);
231 		}
232 		dec.accept(this);
233 	}
234 
235 	override void visit(const AutoDeclaration dec)
236 	{
237 		foreach (i; dec.identifiers)
238 		{
239 			maketag(i);
240 		}
241 		dec.accept(this);
242 	}
243 
244 	override void visit(const AliasDeclaration dec)
245 	{
246 		// Old style alias
247 		if (dec.identifierList)
248 		{
249 			foreach (i; dec.identifierList.identifiers)
250 				maketag(i);
251 		}
252 		dec.accept(this);
253 	}
254 
255 	override void visit(const AliasInitializer dec)
256 	{
257 		maketag(dec.name);
258 		dec.accept(this);
259 	}
260 
261 	override void visit(const Invariant dec)
262 	{
263 		maketag("invariant", dec.index, dec.line);
264 	}
265 
266 private:
267 	void updateVisibility(IdType type)
268 	{
269 		// maybe change visibility based on attribute 'type'
270 		switch (type)
271 		{
272 		case tok!"export":
273 		case tok!"public":
274 		case tok!"protected":
275 			visibility = Visibility.exposed;
276 			break;
277 		case tok!"package":
278 		case tok!"private":
279 			visibility = privateVisibility;
280 			break;
281 		default:
282 			// no change
283 			break;
284 		}
285 	}
286 
287 	void acceptInContext(const ASTNode dec, string name)
288 	{
289 		// nest context before journeying on
290 		auto c = context;
291 		context ~= name ~ ".";
292 		dec.accept(this);
293 		context = c;
294 	}
295 
296 	void maketag(Token name)
297 	{
298 		maketag(name.text, name.index, name.line);
299 	}
300 
301 	void maketag(string text, size_t index, ulong line)
302 	{
303 		// skip unittests and hidden declarations
304 		if (inUnittest || visibility == Visibility.hidden)
305 			return;
306 
307 		// tag is a searchable string from beginning of line
308 		size_t b = index;
309 		while (b > 0 && bytes[b - 1] != '\n')
310 			--b;
311 
312 		// tag end is one char beyond tag name
313 		size_t e = index + text.length;
314 		if (e < bytes.length && bytes[e] != '\n')
315 			++e;
316 
317 		auto tag = cast(char[]) bytes[b .. e];
318 		auto tagname = context.empty ? text : context ~ text;
319 
320 		// drum roll...	 the etags tag format
321 		tags ~= format("%s\x7f%s\x01%u,%u\n", tag, tagname, line, b);
322 	}
323 
324 	alias visit = ASTVisitor.visit;
325 
326 	// state
327 	// visibility of declarations (i.e. should we tag)
328 	Visibility visibility = Visibility.exposed;
329 	bool inUnittest;
330 
331 	// inputs
332 	ubyte[] bytes;
333 	string moduleName;
334 	string context;
335 	Visibility privateVisibility = Visibility.hidden;
336 
337 	// ouput
338 	string tags;
339 }