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