1 /* extract doxygen comments from C++ header files
2  *
3  * Copyright (C) 2016 Robin Gareus <robin@gareus.org>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include <stdlib.h>
20 #include <unistd.h>
21 #include <getopt.h>
22 #include <pthread.h>
23 #include <cstdio>
24 #include <cstring>
25 #include <sstream>
26 #include <iomanip>
27 #include <algorithm>
28 #include <map>
29 #include <vector>
30 #include <clang-c/Index.h>
31 #include <clang-c/Documentation.h>
32 
33 struct Dox2jsConfig {
Dox2jsConfigDox2jsConfig34 	Dox2jsConfig () : clang_argc (3), clang_argv (0), excl_argc (0), excl_argv (0)
35 	{
36 		excl_argv = (char**) calloc (1, sizeof (char*));
37 		clang_argv = (char**) malloc (clang_argc * sizeof (char*));
38 		clang_argv[0] = strdup ("-x");
39 		clang_argv[1] = strdup ("c++");
40 		clang_argv[2] = strdup ("-std=c++11");
41 	}
42 
~Dox2jsConfigDox2jsConfig43 	~Dox2jsConfig () {
44 		for (int i = 0; i < clang_argc; ++i) {
45 			free (clang_argv[i]);
46 		}
47 		for (int i = 0; excl_argv[i]; ++i) {
48 			free (excl_argv[i]);
49 		}
50 		free (clang_argv);
51 		free (excl_argv);
52 	}
53 
add_clang_argDox2jsConfig54 	void add_clang_arg (const char *a) {
55 		clang_argv = (char**) realloc (clang_argv, (clang_argc + 1) * sizeof (char*));
56 		clang_argv[clang_argc++] = strdup (a);
57 	}
58 
add_excludeDox2jsConfig59 	void add_exclude (const char *a) {
60 		excl_argv = (char**) realloc (excl_argv, (excl_argc + 2) * sizeof (char*));
61 		excl_argv[excl_argc++] = strdup (a);
62 		excl_argv[excl_argc] = NULL;
63 	}
64 
65 	int    clang_argc;
66 	char** clang_argv;
67 	int    excl_argc;
68 	char** excl_argv;
69 };
70 
71 typedef std::map <std::string, std::string> ResultMap;
72 
73 struct dox2js {
74 	Dox2jsConfig *cfg;
75 	ResultMap results;
76 };
77 
78 struct ProcessQueue {
ProcessQueueProcessQueue79 	ProcessQueue (std::vector<char*>* files, ResultMap* r, Dox2jsConfig* c, pthread_mutex_t* lck, bool report, bool check)
80 		: fq (files)
81 		, rm (r)
82 		, djc (c)
83 		, lock (lck)
84 		, total (files->size ())
85 		, done (0)
86 		, report_progress (report)
87 		, check_compile (check)
88 	{ }
89 
90 	std::vector<char*>* fq;
91 	ResultMap *rm;
92 	Dox2jsConfig *djc;
93 	pthread_mutex_t* lock;
94 	unsigned int total;
95 	unsigned int done;
96 	bool report_progress;
97 	bool check_compile;
98 };
99 
100 
101 static const char*
kind_to_txt(CXCursor cursor)102 kind_to_txt (CXCursor cursor)
103 {
104 	CXCursorKind kind  = clang_getCursorKind (cursor);
105 	switch (kind) {
106 		case CXCursor_StructDecl   : return "Struct";
107 		case CXCursor_EnumDecl     : return "Enum";
108 		case CXCursor_UnionDecl    : return "Union";
109 		case CXCursor_FunctionDecl : return "C Function";
110 		case CXCursor_VarDecl      : return "Variable";
111 		case CXCursor_ClassDecl    : return "C++ Class";
112 		case CXCursor_CXXMethod    : return "C++ Method";
113 		case CXCursor_Namespace    : return "C++ Namespace";
114 		case CXCursor_Constructor  : return "C++ Constructor";
115 		case CXCursor_Destructor   : return "C++ Destructor";
116 		case CXCursor_FieldDecl    : return "Data Member/Field";
117 		default: break;
118 	}
119 	return "";
120 }
121 
122 static std::string
escape_json(const std::string & s)123 escape_json (const std::string &s)
124 {
125 	std::ostringstream o;
126 	for (auto c = s.cbegin (); c != s.cend (); c++) {
127 		switch (*c) {
128 			case '"':  o << "\\\""; break;
129 			case '\\': o << "\\\\"; break;
130 			case '\n': o << "\\n"; break;
131 			case '\r': o << "\\r"; break;
132 			case '\t': o << "\\t"; break;
133 			default:
134 				if ('\x00' <= *c && *c <= '\x1f') {
135 					o << "\\u" << std::hex << std::setw (4) << std::setfill ('0') << (int)*c;
136 				} else {
137 				  o << *c;
138 				}
139 		}
140 	}
141 	return o.str ();
142 }
143 
144 static std::string
recurse_parents(CXCursor cr)145 recurse_parents (CXCursor cr) {
146 	std::string rv;
147 	CXCursor pc = clang_getCursorSemanticParent (cr);
148 	if (CXCursor_TranslationUnit == clang_getCursorKind (pc)) {
149 		return rv;
150 	}
151 	if (!clang_Cursor_isNull (pc)) {
152 		rv += recurse_parents (pc);
153 		rv += clang_getCString (clang_getCursorDisplayName (pc));
154 		rv += "::";
155 	}
156 	return rv;
157 }
158 
159 static bool
check_excludes(const std::string & decl,CXClientData d)160 check_excludes (const std::string& decl, CXClientData d) {
161 	struct Dox2jsConfig* djc = (struct Dox2jsConfig*) d;
162 	char** excl = djc->excl_argv;
163 	for (int i = 0; excl[i]; ++i) {
164 		if (decl.compare (0, strlen (excl[i]), excl[i]) == 0) {
165 			return true;
166 		}
167 	}
168 	return false;
169 }
170 
171 static enum CXChildVisitResult
traverse(CXCursor cr,CXCursor,CXClientData d)172 traverse (CXCursor cr, CXCursor /*parent*/, CXClientData d)
173 {
174 	struct dox2js* dj = (struct dox2js*) d;
175 	CXComment c = clang_Cursor_getParsedComment (cr);
176 
177 	if (clang_Comment_getKind (c) != CXComment_Null
178 			&& clang_isDeclaration (clang_getCursorKind (cr))
179 			&& 0 != strlen (kind_to_txt (cr))
180 		 ) {
181 
182 		// TODO: resolve typedef enum { .. } name;
183 		// use clang_getCursorDefinition (clang_getCanonicalCursor (cr)) ??
184 		std::string decl = recurse_parents (cr);
185 		decl += clang_getCString (clang_getCursorDisplayName (cr));
186 
187 		if (decl.empty () || check_excludes (decl, dj->cfg)) {
188 			return CXChildVisit_Recurse;
189 		}
190 
191 		std::ostringstream o;
192 		o << "{ \"decl\" : \"" << decl << "\",\n";
193 
194 		if (clang_Cursor_isVariadic (cr)) {
195 			o << "  \"variadic\" : true,\n";
196 		}
197 
198 		CXSourceLocation  loc = clang_getCursorLocation (cr);
199 		CXFile file; unsigned line, col, off;
200 		clang_getFileLocation (loc, &file, &line, &col, &off);
201 
202 		o << "  \"kind\" : \"" << kind_to_txt (cr) << "\",\n"
203 			<< "  \"src\" : \"" << clang_getCString (clang_getFileName (file)) << ":" << line << "\",\n"
204 			<< "  \"doc\" : \"" << escape_json (clang_getCString (clang_FullComment_getAsHTML (c))) << "\"\n"
205 			<< "},\n";
206 
207 		dj->results[decl] = o.str ();
208 	}
209 	return CXChildVisit_Recurse;
210 }
211 
212 static ResultMap
process_file(const char * fn,struct Dox2jsConfig * djc,bool check)213 process_file (const char* fn, struct Dox2jsConfig *djc, bool check)
214 {
215 	dox2js dj;
216 	dj.cfg = djc;
217 
218 	if (check) {
219 		fprintf (stderr, "--- %s ---\n", fn);
220 	}
221 	CXIndex index = clang_createIndex (0, check ? 1 : 0);
222 	CXTranslationUnit tu = clang_createTranslationUnitFromSourceFile (index, fn, djc->clang_argc, djc->clang_argv, 0, 0);
223 
224 	if (tu == NULL) {
225 		fprintf (stderr, "Cannot create translation unit for src: %s\n", fn);
226 		return ResultMap ();
227 	}
228 
229 	clang_visitChildren (clang_getTranslationUnitCursor (tu), traverse, (CXClientData) &dj);
230 
231 	clang_disposeTranslationUnit (tu);
232 	clang_disposeIndex (index);
233 	return dj.results;
234 }
235 
236 static void*
process_thread(void * d)237 process_thread (void *d)
238 {
239 	struct ProcessQueue* proc = (struct ProcessQueue*) d;
240 	pthread_mutex_lock (proc->lock);
241 
242 	while (1) {
243 		if (proc->fq->empty ()) {
244 			break;
245 		}
246 		char* fn = strdup (proc->fq->back ());
247 		proc->fq->pop_back ();
248 		pthread_mutex_unlock (proc->lock);
249 
250 		ResultMap rm = process_file (fn, proc->djc, proc->check_compile);
251 		free (fn);
252 
253 		pthread_mutex_lock (proc->lock);
254 		for (ResultMap::const_iterator i = rm.begin (); i != rm.end (); ++i) {
255 			(*proc->rm)[i->first] = i->second;
256 		}
257 		++proc->done;
258 
259 		if (proc->report_progress) {
260 			fprintf (stderr, "progress: %4.1f%%  [%4d / %4d] decl: %ld         \r",
261 					100.f * proc->done / (float)proc->total, proc->done, proc->total,
262 					proc->rm->size ());
263 		}
264 	}
265 	pthread_mutex_unlock (proc->lock);
266 	pthread_exit (NULL);
267 	return NULL;
268 }
269 
270 
271 static void
usage(int status)272 usage (int status)
273 {
274 	printf ("doxy2json - extract doxygen doc from C++ headers.\n\n");
275 	fprintf (stderr, "Usage: dox2json [-I path]* [-X exclude]* <filename> [filename]*\n");
276 	exit (status);
277 }
278 
main(int argc,char ** argv)279 int main (int argc, char** argv)
280 {
281 	struct Dox2jsConfig djc;
282 
283 #define MAX_THREADS 16
284 	pthread_t threads[MAX_THREADS];
285 
286 	bool report_progress = false;
287 	bool check_compile = false;
288 	size_t num_threads = 1;
289   int c;
290 	while (EOF != (c = getopt (argc, argv, "j:D:I:TX:"))) {
291 		switch (c) {
292 			case 'j':
293 				num_threads = std::max (1, std::min ((int)MAX_THREADS, atoi (optarg)));
294 				break;
295 			case 'I':
296 				djc.add_clang_arg ("-I");
297 				djc.add_clang_arg (optarg);
298 				break;
299 			case 'D':
300 				djc.add_clang_arg ("-D");
301 				djc.add_clang_arg (optarg);
302 				break;
303 			case 'X':
304 				djc.add_exclude (optarg);
305 				break;
306 			case 'T':
307 				check_compile = true;
308 				break;
309 			case 'h':
310 				usage (0);
311 			default:
312 				usage (EXIT_FAILURE);
313 				break;
314 		}
315 	}
316 
317 	if (optind >= argc) {
318 		usage (EXIT_FAILURE);
319 	}
320 
321 	const int total = (argc - optind);
322 	if (total > 6 && !check_compile) {
323 		report_progress = true;
324 	}
325 
326 	ResultMap results;
327 	std::vector<char*> src;
328 	pthread_mutex_t lock;
329 	pthread_mutex_init (&lock, NULL);
330 
331 	for (int i = optind; i < argc; ++i) {
332 		src.push_back (argv[i]);
333 	}
334 
335 	if (check_compile) {
336 		num_threads = 1;
337 	} else {
338 		num_threads = std::min (src.size (), num_threads);
339 	}
340 
341 	struct ProcessQueue proc (&src, &results, &djc, &lock, report_progress, check_compile);
342 
343 	for (unsigned int i = 0; i < num_threads; ++i) {
344 		int rc = pthread_create (&threads[i], NULL, process_thread, (void *)&proc);
345 		if (rc) {
346 			fprintf (stderr, "failed to create thread.\n");
347 			exit(EXIT_FAILURE);
348 		}
349 	}
350 
351 	pthread_yield();
352 
353 	for (unsigned int i = 0; i < num_threads; ++i) {
354 		pthread_join (threads[i], NULL);
355 	}
356 
357 	if (!check_compile) {
358 		printf ("[\n");
359 		for (std::map <std::string, std::string>::const_iterator i = results.begin (); i != results.end (); ++i) {
360 			printf ("%s\n", (*i).second.c_str ());
361 		}
362 		printf ("{} ]\n");
363 	}
364 
365 	pthread_mutex_destroy (&lock);
366 
367   return 0;
368 }
369