ccanlint: print coverage amount when -vv
[ccan] / tools / ccanlint / file_analysis.c
1 #include "ccanlint.h"
2 #include <ccan/talloc/talloc.h>
3 #include <ccan/str/str.h>
4 #include <ccan/str_talloc/str_talloc.h>
5 #include <ccan/talloc_link/talloc_link.h>
6 #include <ccan/hash/hash.h>
7 #include <ccan/htable/htable_type.h>
8 #include <ccan/grab_file/grab_file.h>
9 #include <ccan/noerr/noerr.h>
10 #include <ccan/foreach/foreach.h>
11 #include <ccan/asort/asort.h>
12 #include "../tools.h"
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <sys/stat.h>
16 #include <fcntl.h>
17 #include <err.h>
18 #include <errno.h>
19 #include <dirent.h>
20 #include <ctype.h>
21 #include <stdarg.h>
22 #include <assert.h>
23
24 const char *ccan_dir;
25
26 static size_t dir_hash(const char *name)
27 {
28         return hash(name, strlen(name), 0);
29 }
30
31 static const char *manifest_name(const struct manifest *m)
32 {
33         return m->dir;
34 }
35
36 static bool dir_cmp(const struct manifest *m, const char *dir)
37 {
38         return strcmp(m->dir, dir) == 0;
39 }
40
41 HTABLE_DEFINE_TYPE(struct manifest, manifest_name, dir_hash, dir_cmp, manifest);
42 static struct htable_manifest *manifests;
43
44 const char *get_ccan_file_contents(struct ccan_file *f)
45 {
46         if (!f->contents) {
47                 f->contents = grab_file(f, f->fullname, &f->contents_size);
48                 if (!f->contents)
49                         err(1, "Reading file %s", f->fullname);
50         }
51         return f->contents;
52 }
53
54 char **get_ccan_file_lines(struct ccan_file *f)
55 {
56         if (!f->lines)
57                 f->lines = strsplit(f, get_ccan_file_contents(f), "\n");
58
59         /* FIXME: is f->num_lines necessary? */
60         f->num_lines = talloc_array_length(f->lines) - 1;
61         return f->lines;
62 }
63
64 struct list_head *get_ccan_file_docs(struct ccan_file *f)
65 {
66         if (!f->doc_sections) {
67                 get_ccan_file_lines(f);
68                 f->doc_sections = extract_doc_sections(f->lines);
69         }
70         return f->doc_sections;
71 }
72
73 struct ccan_file *new_ccan_file(const void *ctx, const char *dir, char *name)
74 {
75         struct ccan_file *f;
76
77         assert(dir[0] == '/');
78
79         f = talloc(ctx, struct ccan_file);
80         f->lines = NULL;
81         f->line_info = NULL;
82         f->doc_sections = NULL;
83         f->compiled = NULL;
84         f->name = talloc_steal(f, name);
85         f->fullname = talloc_asprintf(f, "%s/%s", dir, f->name);
86         f->contents = NULL;
87         f->cov_compiled = NULL;
88         return f;
89 }
90
91 static void add_files(struct manifest *m, const char *dir)
92 {
93         DIR *d;
94         struct dirent *ent;
95
96         if (dir[0])
97                 d = opendir(dir);
98         else
99                 d = opendir(".");
100         if (!d)
101                 err(1, "Opening directory %s", dir[0] ? dir : ".");
102
103         while ((ent = readdir(d)) != NULL) {
104                 struct stat st;
105                 struct ccan_file *f;
106                 struct list_head *dest;
107                 bool is_c_src;
108
109                 if (ent->d_name[0] == '.')
110                         continue;
111
112                 f = new_ccan_file(m, m->dir,
113                                   talloc_asprintf(m, "%s%s",
114                                                   dir, ent->d_name));
115                 if (lstat(f->name, &st) != 0)
116                         err(1, "lstat %s", f->name);
117
118                 if (S_ISDIR(st.st_mode)) {
119                         f->name = talloc_append_string(f->name, "/");
120                         add_files(m, f->name);
121                         continue;
122                 }
123                 if (!S_ISREG(st.st_mode)) {
124                         talloc_free(f);
125                         continue;
126                 }
127
128                 if (streq(f->name, "_info")) {
129                         m->info_file = f;
130                         continue;
131                 }
132
133                 is_c_src = strends(f->name, ".c");
134                 if (!is_c_src && !strends(f->name, ".h")) {
135                         dest = &m->other_files;
136                         continue;
137                 }
138
139                 if (!strchr(f->name, '/')) {
140                         if (is_c_src)
141                                 dest = &m->c_files;
142                         else
143                                 dest = &m->h_files;
144                 } else if (strstarts(f->name, "test/")) {
145                         if (is_c_src) {
146                                 if (strstarts(f->name, "test/api"))
147                                         dest = &m->api_tests;
148                                 else if (strstarts(f->name, "test/run"))
149                                         dest = &m->run_tests;
150                                 else if (strstarts(f->name, "test/compile_ok"))
151                                         dest = &m->compile_ok_tests;
152                                 else if (strstarts(f->name, "test/compile_fail"))
153                                         dest = &m->compile_fail_tests;
154                                 else
155                                         dest = &m->other_test_c_files;
156                         } else
157                                 dest = &m->other_test_files;
158                 } else
159                         dest = &m->other_files;
160
161                 list_add(dest, &f->list);
162         }
163         closedir(d);
164 }
165
166 static int cmp_names(struct ccan_file *const *a, struct ccan_file *const *b,
167                      void *unused)
168 {
169         return strcmp((*a)->name, (*b)->name);
170 }
171
172 static void sort_files(struct list_head *list)
173 {
174         struct ccan_file **files = NULL, *f;
175         unsigned int i, num;
176
177         num = 0;
178         while ((f = list_top(list, struct ccan_file, list)) != NULL) {
179                 files = talloc_realloc(NULL, files, struct ccan_file *, num+1);
180                 files[num++] = f;
181                 list_del(&f->list);
182         }
183         asort(files, num, cmp_names, NULL);
184
185         for (i = 0; i < num; i++)
186                 list_add_tail(list, &files[i]->list);
187         talloc_free(files);
188 }
189
190 struct manifest *get_manifest(const void *ctx, const char *dir)
191 {
192         struct manifest *m;
193         char *olddir, *canon_dir;
194         unsigned int len;
195         struct list_head *list;
196
197         if (!manifests)
198                 manifests = htable_manifest_new();
199
200         olddir = talloc_getcwd(NULL);
201         if (!olddir)
202                 err(1, "Getting current directory");
203
204         if (chdir(dir) != 0)
205                 err(1, "Failed to chdir to %s", dir);
206
207         canon_dir = talloc_getcwd(olddir);
208         if (!canon_dir)
209                 err(1, "Getting current directory");
210
211         m = htable_manifest_get(manifests, canon_dir);
212         if (m)
213                 goto done;
214
215         m = talloc_linked(ctx, talloc(NULL, struct manifest));
216         m->info_file = NULL;
217         m->compiled = NULL;
218         m->dir = talloc_steal(m, canon_dir);
219         list_head_init(&m->c_files);
220         list_head_init(&m->h_files);
221         list_head_init(&m->api_tests);
222         list_head_init(&m->run_tests);
223         list_head_init(&m->compile_ok_tests);
224         list_head_init(&m->compile_fail_tests);
225         list_head_init(&m->other_test_c_files);
226         list_head_init(&m->other_test_files);
227         list_head_init(&m->other_files);
228         list_head_init(&m->examples);
229         list_head_init(&m->mangled_examples);
230         list_head_init(&m->deps);
231
232         len = strlen(m->dir);
233         while (len && m->dir[len-1] == '/')
234                 m->dir[--len] = '\0';
235
236         m->basename = strrchr(m->dir, '/');
237         if (!m->basename)
238                 errx(1, "I don't expect to be run from the root directory");
239         m->basename++;
240
241         /* We expect the ccan dir to be two levels above module dir. */
242         if (!ccan_dir) {
243                 char *p;
244                 ccan_dir = talloc_strdup(NULL, m->dir);
245                 p = strrchr(ccan_dir, '/');
246                 *p = '\0';
247                 p = strrchr(ccan_dir, '/');
248                 *p = '\0';
249         }
250
251         add_files(m, "");
252
253         /* Nicer to run tests in a predictable order. */
254         foreach_ptr(list, &m->api_tests, &m->run_tests, &m->compile_ok_tests,
255                     &m->compile_fail_tests)
256                 sort_files(list);
257
258         htable_manifest_add(manifests, m);
259
260 done:
261         if (chdir(olddir) != 0)
262                 err(1, "Returning to original directory '%s'", olddir);
263         talloc_free(olddir);
264
265         return m;
266 }
267
268
269 /**
270  * remove_comments - strip comments from a line, return copy.
271  * @line: line to copy
272  * @in_comment: are we already within a comment (from prev line).
273  * @unterminated: are we still in a comment for next line.
274  */
275 static char *remove_comments(const char *line, bool in_comment,
276                              bool *unterminated)
277 {
278         char *p, *ret = talloc_array(line, char, strlen(line) + 1);
279
280         p = ret;
281         for (;;) {
282                 if (!in_comment) {
283                         /* Find first comment. */
284                         const char *old_comment = strstr(line, "/*");
285                         const char *new_comment = strstr(line, "//");
286                         const char *comment;
287
288                         if (new_comment && old_comment)
289                                 comment = new_comment < old_comment
290                                         ? new_comment : old_comment;
291                         else if (old_comment)
292                                 comment = old_comment;
293                         else if (new_comment)
294                                 comment = new_comment;
295                         else {
296                                 /* Nothing more. */
297                                 strcpy(p, line);
298                                 *unterminated = false;
299                                 break;
300                         }
301
302                         /* Copy up to comment. */
303                         memcpy(p, line, comment - line);
304                         p += comment - line;
305                         line += comment - line + 2;
306
307                         if (comment == new_comment) {
308                                 /* We're done: goes to EOL. */
309                                 p[0] = '\0';
310                                 *unterminated = false;
311                                 break;
312                         }
313                         in_comment = true;
314                 }
315
316                 if (in_comment) {
317                         const char *end = strstr(line, "*/");
318                         if (!end) {
319                                 *unterminated = true;
320                                 p[0] = '\0';
321                                 break;
322                         }
323                         line = end+2;
324                         in_comment = false;
325                 }
326         }
327         return ret;
328 }
329
330 static bool is_empty(const char *line)
331 {
332         return strspn(line, " \t") == strlen(line);
333 }
334
335 static bool continues(const char *line)
336 {
337         /* Technically, any odd number of these.  But who cares? */
338         return strends(line, "\\");
339 }
340
341 /* Get token if it's equal to token. */
342 bool get_token(const char **line, const char *token)
343 {
344         unsigned int toklen;
345
346         *line += strspn(*line, " \t");
347         if (isalnum(token[0]) || token[0] == '_')
348                 toklen = strspn(*line, IDENT_CHARS);
349         else {
350                 /* FIXME: real tokenizer handles ++ and other multi-chars.  */
351                 toklen = strlen(token);
352         }
353
354         if (toklen == strlen(token) && !strncmp(*line, token, toklen)) {
355                 *line += toklen;
356                 return true;
357         }
358         return false;
359 }
360
361 char *get_symbol_token(void *ctx, const char **line)
362 {
363         unsigned int toklen;
364         char *ret;
365
366         *line += strspn(*line, " \t");
367         toklen = strspn(*line, IDENT_CHARS);
368         if (!toklen)
369                 return NULL;
370         ret = talloc_strndup(ctx, *line, toklen);
371         *line += toklen;
372         return ret;
373 }
374
375 static bool parse_hash_if(struct pp_conditions *cond, const char **line)
376 {
377         bool brackets, defined;
378
379         cond->inverse = get_token(line, "!");
380         defined = get_token(line, "defined");
381         brackets = get_token(line, "(");
382         cond->symbol = get_symbol_token(cond, line);
383         if (!cond->symbol)
384                 return false;
385         if (brackets && !get_token(line, ")"))
386                 return false;
387         if (!defined)
388                 cond->type = PP_COND_IF;
389         return true;
390 }
391
392 /* FIXME: Get serious! */
393 static struct pp_conditions *analyze_directive(struct ccan_file *f,
394                                                const char *line,
395                                                struct pp_conditions *parent)
396 {
397         struct pp_conditions *cond = talloc(f, struct pp_conditions);
398         bool unused;
399
400         line = remove_comments(line, false, &unused);
401
402         cond->parent = parent;
403         cond->type = PP_COND_IFDEF;
404
405         if (!get_token(&line, "#"))
406                 abort();
407
408         if (get_token(&line, "if")) {
409                 if (!parse_hash_if(cond, &line))
410                         goto unknown;
411         } else if (get_token(&line, "elif")) {
412                 /* Malformed? */
413                 if (!parent)
414                         return NULL;
415                 cond->parent = parent->parent;
416                 /* FIXME: Not quite true.  This implies !parent, but we don't
417                  * do multiple conditionals yet. */
418                 if (!parse_hash_if(cond, &line))
419                         goto unknown;
420         } else if (get_token(&line, "ifdef")) {
421                 bool brackets;
422                 cond->inverse = false;
423                 brackets = get_token(&line, "(");
424                 cond->symbol = get_symbol_token(cond, &line);
425                 if (!cond->symbol)
426                         goto unknown;
427                 if (brackets && !get_token(&line, ")"))
428                         goto unknown;
429         } else if (get_token(&line, "ifndef")) {
430                 bool brackets;
431                 cond->inverse = true;
432                 brackets = get_token(&line, "(");
433                 cond->symbol = get_symbol_token(cond, &line);
434                 if (!cond->symbol)
435                         goto unknown;
436                 if (brackets && !get_token(&line, ")"))
437                         goto unknown;
438         } else if (get_token(&line, "else")) {
439                 /* Malformed? */
440                 if (!parent)
441                         return NULL;
442
443                 *cond = *parent;
444                 cond->inverse = !cond->inverse;
445                 return cond;
446         } else if (get_token(&line, "endif")) {
447                 talloc_free(cond);
448                 /* Malformed? */
449                 if (!parent)
450                         return NULL;
451                 /* Back up one! */
452                 return parent->parent;
453         } else {
454                 /* Not a conditional. */
455                 talloc_free(cond);
456                 return parent;
457         }
458
459         if (!is_empty(line))
460                 goto unknown;
461         return cond;
462
463 unknown:
464         cond->type = PP_COND_UNKNOWN;
465         return cond;
466 }
467
468 /* This parser is rough, but OK if code is reasonably neat. */
469 struct line_info *get_ccan_line_info(struct ccan_file *f)
470 {
471         bool continued = false, in_comment = false;
472         struct pp_conditions *cond = NULL;
473         unsigned int i;
474
475         if (f->line_info)
476                 return f->line_info;
477
478         get_ccan_file_lines(f);
479         f->line_info = talloc_array(f->lines, struct line_info, f->num_lines);
480
481         for (i = 0; i < f->num_lines; continued = continues(f->lines[i++])) {
482                 char *p;
483                 bool still_doc_line;
484
485                 /* Current conditions apply to this line. */
486                 f->line_info[i].cond = cond;
487                 f->line_info[i].continued = continued;
488
489                 if (continued) {
490                         /* Same as last line. */
491                         f->line_info[i].type = f->line_info[i-1].type;
492                         /* Update in_comment. */
493                         remove_comments(f->lines[i], in_comment, &in_comment);
494                         continue;
495                 }
496
497                 /* Preprocessor directive? */
498                 if (!in_comment
499                     && f->lines[i][strspn(f->lines[i], " \t")] == '#') {
500                         f->line_info[i].type = PREPROC_LINE;
501                         cond = analyze_directive(f, f->lines[i], cond);
502                         continue;
503                 }
504
505                 still_doc_line = (in_comment
506                                   && f->line_info[i-1].type == DOC_LINE);
507
508                 p = remove_comments(f->lines[i], in_comment, &in_comment);
509                 if (is_empty(p)) {
510                         if (strstarts(f->lines[i], "/**") || still_doc_line)
511                                 f->line_info[i].type = DOC_LINE;
512                         else
513                                 f->line_info[i].type = COMMENT_LINE;
514                 } else
515                         f->line_info[i].type = CODE_LINE;
516                 talloc_free(p);
517         }
518         return f->line_info;
519 }
520
521 struct symbol {
522         struct list_node list;
523         const char *name;
524         const unsigned int *value;
525 };
526
527 static struct symbol *find_symbol(struct list_head *syms, const char *sym)
528 {
529         struct symbol *i;
530
531         list_for_each(syms, i, list)
532                 if (streq(sym, i->name))
533                         return i;
534         return NULL;
535 }
536
537 static enum line_compiled get_pp(struct pp_conditions *cond,
538                                  struct list_head *syms)
539 {
540         struct symbol *sym;
541         unsigned int val;
542         enum line_compiled parent, ret;
543
544         /* No conditions?  Easy. */
545         if (!cond)
546                 return COMPILED;
547
548         /* Check we get here at all. */
549         parent = get_pp(cond->parent, syms);
550         if (parent == NOT_COMPILED)
551                 return NOT_COMPILED;
552
553         if (cond->type == PP_COND_UNKNOWN)
554                 return MAYBE_COMPILED;
555
556         sym = find_symbol(syms, cond->symbol);
557         if (!sym)
558                 return MAYBE_COMPILED;
559
560         switch (cond->type) {
561         case PP_COND_IF:
562                 /* Undefined is 0. */
563                 val = sym->value ? *sym->value : 0;
564                 if (!val == cond->inverse)
565                         ret = COMPILED;
566                 else
567                         ret = NOT_COMPILED;
568                 break;
569
570         case PP_COND_IFDEF:
571                 if (cond->inverse == !sym->value)
572                         ret = COMPILED;
573                 else
574                         ret = NOT_COMPILED;
575                 break;
576
577         default:
578                 abort();
579         }
580
581         /* If parent didn't know, NO == NO, but YES == MAYBE. */
582         if (parent == MAYBE_COMPILED && ret == COMPILED)
583                 ret = MAYBE_COMPILED;
584         return ret;
585 }
586
587 static void add_symbol(struct list_head *head,
588                        const char *symbol, const unsigned int *value)
589 {
590         struct symbol *sym = talloc(head, struct symbol);
591         sym->name = symbol;
592         sym->value = value;
593         list_add(head, &sym->list);
594 }
595         
596 enum line_compiled get_ccan_line_pp(struct pp_conditions *cond,
597                                     const char *symbol,
598                                     const unsigned int *value,
599                                     ...)
600 {
601         enum line_compiled ret;
602         struct list_head *head;
603         va_list ap;
604
605         head = talloc(NULL, struct list_head);
606         list_head_init(head);
607
608         va_start(ap, value);
609         add_symbol(head, symbol, value);
610
611         while ((symbol = va_arg(ap, const char *)) != NULL) {
612                 value = va_arg(ap, const unsigned int *);
613                 add_symbol(head, symbol, value);
614         }
615         ret = get_pp(cond, head);
616         talloc_free(head);
617         return ret;
618 }
619
620 void score_file_error(struct score *score, struct ccan_file *f, unsigned line,
621                       const char *errorfmt, ...)
622 {
623         va_list ap;
624
625         struct file_error *fe = talloc(score, struct file_error);
626         fe->file = f;
627         fe->line = line;
628         list_add_tail(&score->per_file_errors, &fe->list);
629
630         if (!score->error)
631                 score->error = talloc_strdup(score, "");
632         
633         if (verbose < 2 && strcount(score->error, "\n") > 5)
634                 return;
635
636         if (line)
637                 score->error = talloc_asprintf_append(score->error,
638                                                       "%s:%u:",
639                                                       f->fullname, line);
640         else
641                 score->error = talloc_asprintf_append(score->error,
642                                                       "%s:", f->fullname);
643
644         va_start(ap, errorfmt);
645         score->error = talloc_vasprintf_append(score->error, errorfmt, ap);
646         va_end(ap);
647         score->error = talloc_append_string(score->error, "\n");
648
649         if (verbose < 2 && strcount(score->error, "\n") > 5)
650                 score->error = talloc_append_string(score->error,
651                                     "... more (use -vv to see them all)\n");
652 }