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