Initial import of files for generating Doxygen documentation.
[ohcount] / src / detector.c
1 // detector.c written by Mitchell Foral. mitchell<att>caladbolg.net.
2 // See COPYING for license information.
3
4 #include <ctype.h>
5 #include <stdio.h>
6 #include <stdlib.h>
7 #include <string.h>
8
9 #include "detector.h"
10 #include "languages.h"
11
12 #include "hash/cppheader_hash.h"
13 #include "hash/disambiguatefunc_hash.h"
14 #include "hash/extension_hash.h"
15 #include "hash/filename_hash.h"
16 #include "hash/language_hash.h"
17
18 #define ISAMBIGUOUS(x) (x[0] == '\1')
19 #define DISAMBIGUATEWHAT(x) &x[1]
20
21 const char *ohcount_detect_language(SourceFile *sourcefile) {
22   const char *language = NULL;
23   char *p, *pe;
24   int length;
25
26   // Attempt to detect based on file extension.
27   length = strlen(sourcefile->ext);
28   struct ExtensionMap *re = ohcount_hash_language_from_ext(sourcefile->ext,
29                                                            length);
30   if (re) language = re->value;
31   if (language == NULL) {
32     // Try the lower-case version of this extension.
33     char lowerext[length];
34     strncpy(lowerext, sourcefile->ext, length);
35     lowerext[length] = '\0';
36     for (p = lowerext; p < lowerext + length; p++) *p = tolower(*p);
37     struct ExtensionMap *re = ohcount_hash_language_from_ext(lowerext, length);
38     if (re) return re->value;
39   }
40   if (language) {
41     if (ISAMBIGUOUS(language)) {
42       // Call the appropriate function for disambiguation.
43       length = strlen(DISAMBIGUATEWHAT(language));
44       struct DisambiguateFuncsMap *rd =
45         ohcount_hash_disambiguate_func_from_id(DISAMBIGUATEWHAT(language),
46                                                length);
47       if (rd) return rd->value(sourcefile);
48     } else return language;
49   }
50
51   // Attempt to detect based on filename.
52   length = strlen(sourcefile->filename);
53   struct FilenameMap *rf =
54     ohcount_hash_language_from_filename(sourcefile->filename, length);
55   if (rf) return rf->value;
56
57   char line[81], buf[81];
58
59   // Attempt to detect using Emacs mode line (/^-\*-\s*mode[\s:]*\w/i).
60   p = ohcount_sourcefile_get_contents(sourcefile);
61   pe = p;
62   char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
63   while (pe < eof) {
64     // Get the contents of the first line.
65     while (pe < eof && *pe != '\r' && *pe != '\n') pe++;
66     length = (pe - p <= sizeof(line)) ? pe - p : sizeof(line);
67     strncpy(line, p, length);
68     line[length] = '\0';
69     if (*line == '#' && *(line + 1) == '!') {
70       // First line was sh-bang; loop to get contents of second line.
71       while (*pe == '\r' || *pe == '\n') pe++;
72       p = pe;
73     } else break;
74   }
75   for (p = line; p < line + strlen(line); p++) *p = tolower(*p);
76   p = strstr(line, "-*-");
77   if (p) {
78     p += 3;
79     while (*p == ' ' || *p == '\t') p++;
80     if (strncmp(p, "mode", 4) == 0) {
81       p += 4;
82       while (*p == ' ' || *p == '\t' || *p == ':') p++;
83     }
84     pe = p;
85     while (isalnum(*pe)) pe++;
86     length = pe - p;
87     strncpy(buf, p, length);
88     buf[length] = '\0';
89     struct LanguageMap *rl = ohcount_hash_language_from_name(buf, length);
90     if (rl) return rl->value;
91   }
92
93   // Attempt to detect based on Unix 'file' command.
94   char *path = sourcefile->filepath;
95   if (sourcefile->diskpath)
96     path = sourcefile->diskpath;
97   char command[strlen(path) + 10];
98   sprintf(command, "file -b '%s'", path);
99   FILE *f = popen(command, "r");
100   if (f) {
101     fgets(line, sizeof(line), f);
102     for (p = line; p < line + strlen(line); p++) *p = tolower(*p);
103     p = strstr(line, "script text");
104     if (p && p == line) { // /^script text(?: executable)? for \w/
105       p = strstr(line, "for ");
106       if (p) {
107         p += 4;
108         pe = p;
109         while (isalnum(*pe)) pe++;
110         length = pe - p;
111         strncpy(buf, p, length);
112         buf[length] = '\0';
113         struct LanguageMap *rl = ohcount_hash_language_from_name(buf, length);
114         if (rl) language = rl->value;
115       }
116     } else if (p) { // /(\w+)(?: -\w+)* script text/
117       do {
118         p--;
119         pe = p;
120         while (*p == ' ') p--;
121         while (p != line && isalnum(*(p - 1))) p--;
122         if (*(p - 1) == '-') p--;
123       } while (*p == '-'); // Skip over any switches.
124       length = pe - p;
125       strncpy(buf, p, length);
126       buf[length] = '\0';
127       struct LanguageMap *rl = ohcount_hash_language_from_name(buf, length);
128       if (rl) language = rl->value;
129     } else if (strstr(line, "xml")) language = LANG_XML;
130     fclose(f);
131     if (language) return language;
132   }
133
134   return NULL;
135 }
136
137 const char *disambiguate_aspx(SourceFile *sourcefile) {
138   char *p, *pe;
139   int length;
140
141   p = ohcount_sourcefile_get_contents(sourcefile);
142   char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
143   while (p < eof) {
144     // /<%@\s*Page[^>]+Language="VB"[^>]+%>/
145     p = strstr(p, "<%@");
146     pe = strstr(p, "%>");
147     if (p && pe) {
148       p += 3;
149       length = pe - p;
150       char buf[length];
151       strncpy(buf, p, length);
152       buf[length] = '\0';
153       for (p = buf; p < buf + strlen(buf); p++)
154         *p = tolower(*p);
155       p = buf;
156       while (*p == ' ' || *p == '\t') p++;
157       if (strncmp(p, "page", 4) == 0) {
158         p += 4;
159         if (strstr(p, "language=\"vb\""))
160           return LANG_VB_ASPX;
161       }
162     }
163     p++;
164   }
165   return LANG_CS_ASPX;
166 }
167
168 const char *disambiguate_b(SourceFile *sourcefile) {
169   char *p = ohcount_sourcefile_get_contents(sourcefile);
170   char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
171   while (p < eof) {
172     // /(implement[ \t])|(include[ \t]+"[^"]*";)|
173     //  ((return|break|continue).*;|(pick|case).*\{)/
174     if (strncmp(p, "implement", 9) == 0 &&
175         (*(p + 9) == ' ' || *(p + 9) == '\t'))
176       return LANG_LIMBO;
177     else if (strncmp(p, "include", 7) == 0 &&
178         (*(p + 7) == ' ' || *(p + 7) == '\t')) {
179       p += 7;
180       while (*p == ' ' || *p == '\t') p++;
181       if (*p == '"') {
182         while (*p != '"' && p < eof) p++;
183         if (*p == '"' && *(p + 1) == ';')
184           return LANG_LIMBO;
185       }
186     } else if (strncmp(p, "return", 6) == 0 ||
187                strncmp(p, "break", 5) == 0 ||
188                strncmp(p, "continue", 8) == 0) {
189       if (strstr(p, ";"))
190         return LANG_LIMBO;
191     } else if (strncmp(p, "pick", 4) == 0 ||
192                strncmp(p, "case", 4) == 0) {
193       if (strstr(p, "{"))
194         return LANG_LIMBO;
195     }
196     p++;
197   }
198   return disambiguate_basic(sourcefile);
199 }
200
201 const char *disambiguate_basic(SourceFile *sourcefile) {
202   char *p, *pe;
203   int length;
204
205   // Attempt to detect based on file contents.
206   char line[81];
207   p = ohcount_sourcefile_get_contents(sourcefile);
208   pe = p;
209   char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
210   while (pe < eof) {
211     // Get a line at a time.
212     while (pe < eof && *pe != '\r' && *pe != '\n') pe++;
213     length = (pe - p <= sizeof(line)) ? pe - p : sizeof(line);
214     strncpy(line, p, length);
215     line[length] = '\0';
216     char *line_end = pe;
217
218     p = line;
219     if (isdigit(*p)) {
220       // /^\d+\s+\w/
221       p++;
222       while (isdigit(*p)) p++;
223       if (*p == ' ' || *p == '\t') {
224         p++;
225         while (*p == ' ' || *p == '\t') p++;
226         if (isalnum(*p))
227           return LANG_CLASSIC_BASIC;
228       }
229     }
230
231     // Next line.
232     pe = line_end;
233     while (*pe == '\r' || *pe == '\n') pe++;
234     p = pe;
235   }
236
237   // Attempt to detect from associated VB files in file context.
238   char **filenames = ohcount_sourcefile_get_filenames(sourcefile);
239   if (filenames) {
240     int i;
241     for (i = 0; filenames[i] != NULL; i++) {
242       pe = filenames[i] + strlen(filenames[i]);
243       p = pe;
244       while (p > filenames[i] && *(p - 1) != '.') p--;
245       length = pe - p;
246       if (length == 3 &&
247           (strncmp(p, "frm", length) == 0 ||
248            strncmp(p, "frx", length) == 0 ||
249            strncmp(p, "vba", length) == 0 ||
250            strncmp(p, "vbp", length) == 0 ||
251            strncmp(p, "vbs", length) == 0)) {
252         return LANG_VISUALBASIC;
253       }
254     }
255   }
256
257   return LANG_STRUCTURED_BASIC;
258 }
259
260 const char *disambiguate_cs(SourceFile *sourcefile) {
261   // Attempt to detect based on file contents.
262   if (strstr(ohcount_sourcefile_get_contents(sourcefile), "<?cs"))
263     return LANG_CLEARSILVER_TEMPLATE;
264   else
265     return LANG_CSHARP;
266 }
267
268 const char *disambiguate_fortran(SourceFile *sourcefile) {
269   char *p, *pe;
270
271   p = ohcount_sourcefile_get_contents(sourcefile);
272   char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
273   while (p < eof) {
274     if (*p == ' ' && p + 5 < eof) {
275       int i;
276       for (i = 1; i <= 5; i++)
277         if (!isdigit(*(p + i)) && *(p + i) != ' ')
278           return LANG_FORTRANFIXED; // definately not f77
279       // Possibly fixed (doesn't match /^\s*\d+\s*$/).
280       pe = p;
281       while (*pe == ' ' || *pe == '\t') pe++;
282       if (pe - p <= 5) {
283         if (!isdigit(*pe))
284           return LANG_FORTRANFIXED;
285         while (isdigit(*pe)) pe++;
286         while (*pe == ' ' || *pe == '\t') pe++;
287         if (*pe != '\r' && *pe != '\n' && pe - p == 5)
288           return LANG_FORTRANFIXED;
289       }
290     }
291     while (*p != '\r' && *p != '\n' && *p != '&' && p < eof) p++;
292     if (*p == '&') {
293       p++;
294       // Look for free-form continuation.
295       while (*p == ' ' || *p == '\t') p++;
296       if (*p == '\r' || *p == '\n') {
297         pe = p;
298         while (*pe == '\r' || *pe == '\n' || *pe == ' ' || *pe == '\t') pe++;
299         if (*pe == '&')
300           return LANG_FORTRANFREE;
301       }
302     }
303     while (*p == '\r' || *p == '\n') p++;
304   }
305   return LANG_FORTRANFREE; // might as well be free-form
306 }
307
308 const char *disambiguate_h(SourceFile *sourcefile) {
309   char *p, *pe;
310   int length;
311
312   // If the directory contains a matching *.m file, likely Objective C.
313   length = strlen(sourcefile->filename);
314   if (strcmp(sourcefile->ext, "h") == 0) {
315     char path[length];
316     strncpy(path, sourcefile->filename, length);
317     path[length] = '\0';
318     *(path + length - 1) = 'm';
319     char **filenames = ohcount_sourcefile_get_filenames(sourcefile);
320     if (filenames) {
321       int i;
322       for (i = 0; filenames[i] != NULL; i++)
323         if (strcmp(path, filenames[i]) == 0)
324           return LANG_OBJECTIVE_C;
325     }
326   }
327
328   // Attempt to detect based on file contents.
329   char line[81], buf[81];
330   p = ohcount_sourcefile_get_contents(sourcefile);
331   pe = p;
332   char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
333   while (pe < eof) {
334     // Get a line at a time.
335     while (pe < eof && *pe != '\r' && *pe != '\n') pe++;
336     length = (pe - p <= sizeof(line)) ? pe - p : sizeof(line);
337     strncpy(line, p, length);
338     line[length] = '\0';
339     char *line_end = pe;
340
341     // Look for C++ headers.
342     if (*line == '#') {
343       p = line + 1;
344       while (*p == ' ' || *p == '\t') p++;
345       if (strncmp(p, "include", 7) == 0 &&
346           (*(p + 7) == ' ' || *(p + 7) == '\t')) {
347         // /^#\s*include\s+[<"][^>"]+[>"]/
348         p += 8;
349         while (*p == ' ' || *p == '\t') p++;
350         if (*p == '<' || *p == '"') {
351           // Is the header file a C++ header file?
352           p++;
353           pe = p;
354           while (pe < p + strlen(line) && *pe != '>' && *pe != '"') pe++;
355           length = pe - p;
356           strncpy(buf, p, length);
357           buf[length] = '\0';
358           if (ohcount_hash_is_cppheader(buf, length))
359             return LANG_CPP;
360           // Is the extension for the header file a C++ file?
361           p = pe;
362           while (p > line && *(p - 1) != '.') p--;
363           length = pe - p;
364           strncpy(buf, p, length);
365           buf[length] = '\0';
366           struct ExtensionMap *re = ohcount_hash_language_from_ext(buf, length);
367           if (re && strcmp(re->value, LANG_CPP) == 0)
368             return LANG_CPP;
369         }
370       }
371     }
372
373     // Look for C++ keywords.
374     p = line;
375     while (p < line + strlen(line) && *p != '\r' && *p != '\n') {
376       if (islower(*p) && !isalnum(*(p - 1)) && *(p - 1) != '_') {
377         pe = p;
378         while (islower(*pe)) pe++;
379         if (!isalnum(*pe) && *pe != '_') {
380           length = pe - p;
381           strncpy(buf, p, length);
382           buf[length] = '\0';
383           if (strcmp(buf, "class") == 0 ||
384               strcmp(buf, "namespace") == 0 ||
385               strcmp(buf, "template") == 0 ||
386               strcmp(buf, "typename") == 0)
387             return LANG_CPP;
388         }
389         p = pe + 1;
390       } else p++;
391     }
392
393     // Next line.
394     pe = line_end;
395     while (*pe == '\r' || *pe == '\n') pe++;
396     p = pe;
397   }
398
399   // Nothing to suggest C++.
400   return LANG_C;
401 }
402
403 const char *disambiguate_in(SourceFile *sourcefile) {
404   char *p, *pe;
405   int length;
406   const char *language = NULL;
407
408   p = sourcefile->filepath;
409   pe = p + strlen(p) - 3;
410   if (strstr(p, ".") <= pe) {
411     // Only if the filename has an extension prior to the .in
412     length = pe - p;
413     char buf[length];
414     strncpy(buf, p, length);
415     buf[length] = '\0';
416     SourceFile *undecorated = ohcount_sourcefile_new(buf);
417     p = ohcount_sourcefile_get_contents(sourcefile);
418     // The filepath without the '.in' extension does not exist on disk. The
419     // sourcefile->diskpath field must be set incase the detector needs to run
420     // 'file -b' on the file.
421     ohcount_sourcefile_set_diskpath(undecorated, sourcefile->filepath);
422     ohcount_sourcefile_set_contents(undecorated, p);
423     char **filenames = ohcount_sourcefile_get_filenames(sourcefile);
424     ohcount_sourcefile_set_filenames(undecorated, filenames);
425     language = ohcount_sourcefile_get_language(undecorated);
426     ohcount_sourcefile_free(undecorated);
427   }
428   return language;
429 }
430
431 const char *disambiguate_inc(SourceFile *sourcefile) {
432   char *p = ohcount_sourcefile_get_contents(sourcefile);
433   char *eof = p + strlen(p);
434   while (p < eof) {
435     if (*p == '\0')
436       return BINARY;
437     else if (*p == '?' && strncmp(p + 1, "php", 3) == 0)
438       return LANG_PHP;
439     p++;
440   }
441   return NULL;
442 }
443
444 const char *disambiguate_m(SourceFile *sourcefile) {
445   char *p, *pe;
446   int length;
447
448   // Attempt to detect based on a weighted heuristic of file contents.
449   int matlab_score = 0;
450   int objective_c_score = 0;
451   int limbo_score = 0;
452   int octave_syntax_detected = 0;
453
454   int i, has_h_headers = 0, has_c_files = 0;
455   char **filenames = ohcount_sourcefile_get_filenames(sourcefile);
456   if (filenames) {
457     for (i = 0; filenames[i] != NULL; i++) {
458       p = filenames[i];
459       pe = p + strlen(p);
460       if (pe - p >= 4) {
461         if (*(pe - 4) == '.' && *(pe - 3) == 'c' &&
462             ((*(pe - 2) == 'p' && *(pe - 1) == 'p') ||
463              (*(pe - 2) == '+' && *(pe - 1) == '+') ||
464              (*(pe - 2) == 'x' && *(pe - 1) == 'x'))) {
465           has_c_files = 1;
466           break; // short circuit
467         }
468       } else if (pe - p >= 3) {
469         if (*(pe - 3) == '.' && *(pe - 2) == 'c' && *(pe - 1) == 'c') {
470           has_c_files = 1;
471           break; // short circuit
472         }
473       } else if (pe - p >= 2) {
474         if (*(pe - 2) == '.') {
475           if (*(pe - 1) == 'h')
476             has_h_headers = 1;
477           else if (*(pe - 1) == 'c' || *(pe - 1) == 'C') {
478             has_c_files = 1;
479             break; // short circuit
480           }
481         }
482       }
483     }
484   }
485   if (has_h_headers && !has_c_files)
486     objective_c_score += 5;
487
488   char line[81], buf[81];
489   p = ohcount_sourcefile_get_contents(sourcefile);
490   pe = p;
491   char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
492   while (pe < eof) {
493     // Get a line at a time.
494     while (pe < eof && *pe != '\r' && *pe != '\n') pe++;
495     length = (pe - p <= sizeof(line)) ? pe - p : sizeof(line);
496     strncpy(line, p, length);
497     line[length] = '\0';
498     char *line_end = pe;
499
500     // Look for tell-tale lines.
501     p = line;
502     while (*p == ' ' || *p == '\t') p++;
503     if (*p == '%') { // Matlab comment
504       matlab_score++;
505     } else if (*p == '#') { // Limbo or Octave comment
506       while (*p == '#') p++;
507       if (*p == ' ' || *p == '\t') {
508         limbo_score++;
509         matlab_score++;
510         octave_syntax_detected = 1;
511       }
512     } else if (*p == '/' && *(p + 1) == '/' || *(p + 1) == '*') {
513       objective_c_score++; // Objective C comment
514     } else if (*p == '+' || *p == '-') { // Objective C method signature
515       objective_c_score++;
516     } else if (*p == '@') { // Objective C method signature
517       if (strncmp(p, "@implementation", 15) == 0 ||
518           strncmp(p, "@interface", 10) == 0)
519         objective_c_score++;
520     } else if (strncmp(p, "function", 8) == 0) { // Matlab or Octave function
521       p += 8;
522       while (*p == ' ' || *p == '\t') p++;
523       if (*p == '(')
524         matlab_score++;
525     } else if (strncmp(p, "include", 7) == 0) { // Limbo include
526       // /^include[ \t]+"[^"]+\.m";/
527       p += 7;
528       if (*p == ' ' || *p == '\t') {
529         while (*p == ' ' || *p == '\t') p++;
530         if (*p == '"') {
531           while (*p != '"' && p < line + strlen(line)) p++;
532           if (*p == '"' && *(p - 2) == '.' && *(p - 1) == 'm')
533             limbo_score++;
534         }
535       }
536     }
537
538     // Look for Octave keywords.
539     p = line;
540     while (p < line + strlen(line)) {
541       if (islower(*p) && !isalnum(*(p - 1))) {
542         pe = p;
543         while (islower(*pe) || *pe == '_') pe++;
544         if (!isalnum(*pe)) {
545           length = pe - p;
546           strncpy(buf, p, length);
547           buf[length] = '\0';
548           if (strcmp(buf, "end_try_catch") == 0 ||
549               strcmp(buf, "end_unwind_protect") == 0 ||
550               strcmp(buf, "endfunction") == 0 ||
551               strcmp(buf, "endwhile") == 0)
552             octave_syntax_detected = 1;
553         }
554         p = pe + 1;
555       } else p++;
556     }
557
558     // Look for Limbo declarations
559     p = line;
560     while (p < line + strlen(line)) {
561       if (*p == ':' && (*(p + 1) == ' ' || *(p + 1) == '\t')) {
562         // /:[ \t]+(module|adt|fn ?\(|con[ \t])/
563         p += 2;
564         if (strncmp(p, "module", 6) == 0 && !isalnum(*(p + 6)) ||
565             strncmp(p, "adt", 3) == 0 && !isalnum(*(p + 3)) ||
566             strncmp(p, "fn", 2) == 0 &&
567               (*(p + 2) == ' ' && *(p + 3) == '(' || *(p + 2) == '(') ||
568             strncmp(p, "con", 3) == 0 &&
569               (*(p + 3) == ' ' || *(p + 3) == '\t'))
570           limbo_score++;
571       } else p++;
572     }
573
574     // Next line.
575     pe = line_end;
576     while (*pe == '\r' || *pe == '\n') pe++;
577     p = pe;
578   }
579
580   if (limbo_score > objective_c_score && limbo_score > matlab_score)
581     return LANG_LIMBO;
582   else if (objective_c_score > matlab_score)
583     return LANG_OBJECTIVE_C;
584   else
585     return octave_syntax_detected ? LANG_OCTAVE : LANG_MATLAB;
586 }
587
588 const char *disambiguate_st(SourceFile *sourcefile) {
589   char *p, *pe;
590   int length;
591
592   // Attempt to detect based on file contents.
593   int found_assignment = 0, found_block_start = 0, found_block_end = 0;
594
595   char line[81];
596   p = ohcount_sourcefile_get_contents(sourcefile);
597   pe = p;
598   char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
599   while (pe < eof) {
600     // Get a line at a time.
601     while (p < eof && *pe != '\r' && *pe != '\n') pe++;
602     length = (pe - p <= sizeof(line)) ? pe - p : sizeof(line);
603     strncpy(line, p, length);
604     line[length] = '\0';
605     char *line_end = pe;
606
607     for (p = line; p < line + strlen(line); p++) {
608       if (*p == ':') {
609         p++;
610         while (p < line + strlen(line) && (*p == ' ' || *p == '\t')) p++;
611         if (*p == '=')
612           found_assignment = 1;
613         else if (*p == '[')
614           found_block_start = 1;
615       } else if (*p == ']' && *(p + 1) == '.') found_block_end = 1;
616       if (found_assignment && found_block_start && found_block_end)
617         return LANG_SMALLTALK;
618     }
619
620     // Next line.
621     pe = line_end;
622     while (*pe == '\r' || *pe == '\n') pe++;
623     p = pe;
624   }
625
626   return NULL;
627 }