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