1/* See LICENSE file for copyright and license details. */
2#include "bltin.h"
3#include "fs.h"
4#include "util.h"
5
6#include <sys/stat.h>
7#include <sys/types.h>
8
9#include <fcntl.h>
10#include <string.h>
11#include <unistd.h>
12
13enum {
14 COMMENT_C, /* line // and block comments */
15 COMMENT_HASH, /* line # only */
16 COMMENT_PERCENT, /* line % only */
17 COMMENT_SEMI, /* line ; only */
18 COMMENT_DASH, /* line -- only */
19 COMMENT_HASKELL, /* line -- and block {- -} */
20 COMMENT_PAREN, /* block (* *) only, no line comment */
21};
22
23enum {
24 LANG_C,
25 LANG_CPP,
26 LANG_SH,
27 LANG_AWK,
28 LANG_MAKE,
29 LANG_M4,
30 LANG_LUA,
31 LANG_PERL,
32 LANG_PYTHON,
33 LANG_RUBY,
34 LANG_PHP,
35 LANG_JS,
36 LANG_TS,
37 LANG_JAVA,
38 LANG_KOTLIN,
39 LANG_SCALA,
40 LANG_CS,
41 LANG_GO,
42 LANG_SWIFT,
43 LANG_ZIG,
44 LANG_DART,
45 LANG_ELIXIR,
46 LANG_ERLANG,
47 LANG_HASKELL,
48 LANG_OCAML,
49 LANG_CLOJURE,
50 LANG_RUST,
51 LANG_ADA,
52 LANG_R,
53 LANG_COUNT
54};
55
56struct CommentSpec {
57 const char *line;
58 const char *bopen;
59 const char *bclose;
60};
61
62static const struct CommentSpec comment_specs[] = {
63 [COMMENT_C] = {"//", "/*", "*/"},
64 [COMMENT_HASH] = {"#", NULL, NULL},
65 [COMMENT_PERCENT] = {"%", NULL, NULL},
66 [COMMENT_SEMI] = {";", NULL, NULL},
67 [COMMENT_DASH] = {"--", NULL, NULL},
68 [COMMENT_HASKELL] = {"--", "{-", "-}"},
69 [COMMENT_PAREN] = {NULL, "(*", "*)"},
70};
71
72/* weight is this languages contribution per percent of the codebase */
73struct LangDef {
74 const char *name;
75 int weight;
76 int cstyle;
77};
78
79static const struct LangDef lang_defs[LANG_COUNT] = {
80 [LANG_C] = {"C", 1, COMMENT_C},
81 [LANG_CPP] = {"C++", 300, COMMENT_C},
82 [LANG_SH] = {"Shell", 6, COMMENT_HASH},
83 [LANG_AWK] = {"Awk", 6, COMMENT_HASH},
84 [LANG_MAKE] = {"Make", 10, COMMENT_HASH},
85 [LANG_M4] = {"M4", 25, COMMENT_HASH},
86 [LANG_LUA] = {"Lua", 25, COMMENT_DASH},
87 [LANG_PERL] = {"Perl", 500, COMMENT_HASH},
88 [LANG_PYTHON] = {"Python", 80, COMMENT_HASH},
89 [LANG_RUBY] = {"Ruby", 90, COMMENT_HASH},
90 [LANG_PHP] = {"PHP", 110, COMMENT_C},
91 [LANG_JS] = {"JS", 100, COMMENT_C},
92 [LANG_TS] = {"TS", 140, COMMENT_C},
93 [LANG_JAVA] = {"Java", 150, COMMENT_C},
94 [LANG_KOTLIN] = {"Kotlin", 160, COMMENT_C},
95 [LANG_SCALA] = {"Scala", 180, COMMENT_C},
96 [LANG_CS] = {"C#", 140, COMMENT_C},
97 [LANG_GO] = {"Go", 3, COMMENT_C},
98 [LANG_SWIFT] = {"Swift", 70, COMMENT_C},
99 [LANG_ZIG] = {"Zig", 65, COMMENT_C},
100 [LANG_DART] = {"Dart", 55, COMMENT_C},
101 [LANG_ELIXIR] = {"Elixir", 50, COMMENT_HASH},
102 [LANG_ERLANG] = {"Erlang", 55, COMMENT_PERCENT},
103 [LANG_HASKELL] = {"Haskell", 200, COMMENT_HASKELL},
104 [LANG_OCAML] = {"OCaml", 45, COMMENT_PAREN},
105 [LANG_CLOJURE] = {"Clojure", 85, COMMENT_SEMI},
106 [LANG_RUST] = {"Rust", 3000, COMMENT_C},
107 [LANG_ADA] = {"Ada", 15, COMMENT_DASH},
108 [LANG_R] = {"R", 40, COMMENT_HASH},
109};
110
111struct ExtMap {
112 const char *ext;
113 int lang;
114};
115
116static const struct ExtMap ext_map[] = {
117 {".c", LANG_C}, {".cc", LANG_CPP}, {".cpp", LANG_CPP}, {".cxx", LANG_CPP},
118 {".hpp", LANG_CPP}, {".sh", LANG_SH}, {".awk", LANG_AWK}, {".mk", LANG_MAKE},
119 {".m4", LANG_M4}, {".lua", LANG_LUA}, {".pl", LANG_PERL}, {".pm", LANG_PERL},
120 {".py", LANG_PYTHON}, {".pyw", LANG_PYTHON}, {".rb", LANG_RUBY}, {".php", LANG_PHP},
121 {".js", LANG_JS}, {".mjs", LANG_JS}, {".cjs", LANG_JS}, {".ts", LANG_TS},
122 {".tsx", LANG_TS}, {".java", LANG_JAVA}, {".kt", LANG_KOTLIN}, {".kts", LANG_KOTLIN},
123 {".scala", LANG_SCALA}, {".cs", LANG_CS}, {".go", LANG_GO}, {".swift", LANG_SWIFT},
124 {".zig", LANG_ZIG}, {".dart", LANG_DART}, {".ex", LANG_ELIXIR}, {".exs", LANG_ELIXIR},
125 {".erl", LANG_ERLANG}, {".hrl", LANG_ERLANG}, {".hs", LANG_HASKELL}, {".lhs", LANG_HASKELL},
126 {".ml", LANG_OCAML}, {".mli", LANG_OCAML}, {".clj", LANG_CLOJURE}, {".cljs", LANG_CLOJURE},
127 {".rs", LANG_RUST}, {".adb", LANG_ADA}, {".ads", LANG_ADA}, {".r", LANG_R},
128 {".R", LANG_R},
129};
130
131struct NameMap {
132 const char *name;
133 int lang;
134};
135
136static const struct NameMap name_map[] = {
137 {"Makefile", LANG_MAKE},
138 {"GNUmakefile", LANG_MAKE},
139 {".profile", LANG_SH},
140 {"profile", LANG_SH},
141 {".bashrc", LANG_SH},
142 {".bash_profile", LANG_SH},
143 {".shrc", LANG_SH},
144 {".kshrc", LANG_SH},
145 {".zshrc", LANG_SH},
146 {".cshrc", LANG_SH},
147 {".login", LANG_SH},
148};
149
150/* not scanned for lines, but still checked against markers below */
151static const char *const skip_dirs[] = {
152 ".git",
153 ".hg",
154 ".svn",
155 "node_modules",
156 "vendor",
157 "__pycache__",
158 ".venv",
159 "venv",
160 "target",
161 "build",
162 "dist",
163 ".cache",
164 ".next",
165 ".idea",
166 ".vscode",
167 NULL,
168};
169
170struct Marker {
171 const char *name;
172 int is_dir;
173 double penalty;
174 const char *remark;
175};
176
177static const struct Marker markers[] = {
178 {"node_modules", 1, 150.0, "skipping bullshit / malware"},
179 {"Cargo.toml", 0, 80.0, "I challenge you to bootstrapping this shitty lang"},
180 {"package.json", 0, 60.0, "js. Self-explanatory"},
181 {"go.mod", 0, 20.0, "Not C. But still good. Minor penalty"},
182 {"Dockerfile", 0, 20.0, "containerization crutch"},
183 {"Gemfile", 0, 55.0, "shitty language baseline"},
184 {"CMakeLists.txt", 0, 55.0, "shitty build system"},
185 {"tsconfig.json", 0, 60.0, "a language with a configuration file. Degenerate."},
186};
187
188struct ComboPenalty {
189 int lang_a;
190 int lang_b;
191 double penalty;
192 const char *remark;
193};
194
195static const struct ComboPenalty combos[] = {
196 {LANG_RUST, LANG_CPP, 4000.0, "MAXIMUM BOGUSNESS, rust and c++ sharing a tree"},
197 {LANG_RUST, LANG_C, 400.0, "comical bogusness, rust apologizing to the c it links against"},
198 {LANG_JS, LANG_TS, 150.0, "half the codebase does not trust the other half"},
199 {LANG_C, LANG_CPP, 120.0, "abi boundary between two compilers of the same language"},
200};
201
202struct LangStat {
203 long files;
204 long blank;
205 long comment;
206 long code;
207};
208
209struct ScanState {
210 struct LangStat stats[LANG_COUNT];
211 double bonus;
212 int marker_seen[LEN(markers)];
213};
214
215#define BABEL_THRESHOLD 3
216#define BABEL_MULT 1.5
217
218static int
219is_classic(int lang)
220{
221 return lang == LANG_C || lang == LANG_SH || lang == LANG_AWK;
222}
223
224static int
225is_skip_dir(const char *name)
226{
227 int i;
228
229 for (i = 0; skip_dirs[i]; i++)
230 if (!strcmp(name, skip_dirs[i]))
231 return 1;
232 return 0;
233}
234
235/* checks a 1 or 2 char marker at p without reading past bufend */
236static int
237marker_at(const char *p, const char *bufend, const char *marker)
238{
239 if (!marker)
240 return 0;
241 if (!marker[1])
242 return *p == marker[0];
243 return p + 1 < bufend && p[0] == marker[0] && p[1] == marker[1];
244}
245
246static int
247detect_lang(const char *name)
248{
249 const char *ext;
250 size_t i, nlen;
251
252 for (i = 0; i < LEN(name_map); i++)
253 if (!strcmp(name, name_map[i].name))
254 return name_map[i].lang;
255
256 ext = strrchr(name, '.');
257 if (ext) {
258 for (i = 0; i < LEN(ext_map); i++)
259 if (!strcmp(ext, ext_map[i].ext))
260 return ext_map[i].lang;
261 }
262
263 /*
264 * anchored to the end of the name, so arch.txt (which contains "rc"
265 * but does not end in it) is never mistaken for a shell rc file
266 */
267 nlen = strlen(name);
268 if (nlen >= 2 && !strcmp(name + nlen - 2, "rc"))
269 return LANG_SH;
270 if (nlen >= 7 && !strcmp(name + nlen - 7, "profile"))
271 return LANG_SH;
272
273 return -1;
274}
275
276static void
277check_marker(const char *name, int is_dir, struct ScanState *st)
278{
279 size_t i;
280
281 for (i = 0; i < LEN(markers); i++) {
282 if (markers[i].is_dir != is_dir || strcmp(name, markers[i].name) != 0)
283 continue;
284 if (st->marker_seen[i])
285 return;
286 st->marker_seen[i] = 1;
287 st->bonus += markers[i].penalty;
288 fprintf(stderr, "note: %s: %s\n", markers[i].name, markers[i].remark);
289 return;
290 }
291}
292
293static void
294scan_file(int dirfd, const char *name, int lang, struct LangStat *lp)
295{
296 const struct CommentSpec *cs;
297 char buf[8192];
298 char *p, *bufend;
299 ssize_t nread;
300 int fd, in_block, leading_ws, checked, line_has_content, line_is_comment;
301
302 fd = openat(dirfd, name, O_RDONLY);
303 if (fd < 0)
304 return;
305
306 lp->files++;
307 cs = &comment_specs[lang_defs[lang].cstyle];
308 in_block = 0;
309 leading_ws = 1;
310 checked = 0;
311 line_has_content = 0;
312 line_is_comment = 0;
313
314 while ((nread = read(fd, buf, sizeof(buf))) > 0) {
315 for (p = buf, bufend = buf + nread; p < bufend; p++) {
316 if (*p == '\n') {
317 if (!line_has_content)
318 lp->blank++;
319 else if (line_is_comment)
320 lp->comment++;
321 else
322 lp->code++;
323 leading_ws = 1;
324 checked = 0;
325 line_has_content = 0;
326 line_is_comment = 0;
327 continue;
328 }
329
330 if (leading_ws) {
331 if (*p == ' ' || *p == '\t' || *p == '\r')
332 continue;
333 leading_ws = 0;
334 line_has_content = 1;
335 }
336
337 /* only the first token of a line decides if the line starts a comment */
338 if (!checked) {
339 checked = 1;
340 if (in_block) {
341 line_is_comment = 1;
342 if (marker_at(p, bufend, cs->bclose))
343 in_block = 0;
344 } else if (marker_at(p, bufend, cs->line)) {
345 line_is_comment = 1;
346 } else if (marker_at(p, bufend, cs->bopen)) {
347 line_is_comment = 1;
348 in_block = 1;
349 }
350 } else if (in_block && marker_at(p, bufend, cs->bclose)) {
351 in_block = 0;
352 }
353 }
354 }
355
356 /* files that dont end in a newline still have one line left to count */
357 if (line_has_content) {
358 if (line_is_comment)
359 lp->comment++;
360 else
361 lp->code++;
362 }
363
364 close(fd);
365}
366
367static void
368scan(int dirfd, const char *name, struct stat *st, void *data, struct recursor *r)
369{
370 struct ScanState *scanst = data;
371 int lang;
372
373 check_marker(name, S_ISDIR(st->st_mode), scanst);
374
375 if (S_ISDIR(st->st_mode)) {
376 if (is_skip_dir(name))
377 return;
378 recurse(dirfd, name, data, r);
379 return;
380 }
381
382 if (!S_ISREG(st->st_mode))
383 return;
384
385 lang = detect_lang(name);
386 if (lang < 0)
387 return;
388
389 scan_file(dirfd, name, lang, &scanst->stats[lang]);
390}
391
392int
393bogometercmd(int argc, char **argv)
394{
395 struct recursor r = {.fn = scan, .follow = 'P', .flags = SILENT};
396 struct ScanState st = {0};
397 const char *target;
398 double pct, score;
399 long total_code;
400 int i, modern_langs;
401
402 target = (argc > 1) ? argv[1] : ".";
403 total_code = 0;
404 score = 0.0;
405
406 recurse(AT_FDCWD, target, &st, &r);
407
408 printf("%-10s %10s %10s %10s %10s\n", "Language", "Files", "Blank", "Comment", "Code");
409 printf("---------------------------------------------------------\n");
410
411 for (i = 0; i < LANG_COUNT; i++) {
412 if (st.stats[i].files == 0)
413 continue;
414 printf(
415 "%-10s %10ld %10ld %10ld %10ld\n",
416 lang_defs[i].name,
417 st.stats[i].files,
418 st.stats[i].blank,
419 st.stats[i].comment,
420 st.stats[i].code
421 );
422 total_code += st.stats[i].code;
423 }
424
425 if (total_code == 0) {
426 printf("\nNo targeted code found. System is pure.\n");
427 return 0;
428 }
429
430 printf("---------------------------------------------------------\n");
431 modern_langs = 0;
432 for (i = 0; i < LANG_COUNT; i++) {
433 if (st.stats[i].code == 0)
434 continue;
435 pct = ((double)st.stats[i].code / total_code) * 100.0;
436 score += pct * lang_defs[i].weight;
437 printf("Distribution: %-6s is %5.2f%% of targeted language lines\n", lang_defs[i].name, pct);
438 if (!is_classic(i))
439 modern_langs++;
440 }
441
442 if (modern_langs >= BABEL_THRESHOLD) {
443 score *= BABEL_MULT;
444 fprintf(
445 stderr,
446 "note: %d languages in one tree: babel tower, no two files agree on what tongue they "
447 "speak\n",
448 modern_langs
449 );
450 }
451
452 fflush(stdout);
453 for (i = 0; i < (int)LEN(combos); i++) {
454 if (st.stats[combos[i].lang_a].code > 0 && st.stats[combos[i].lang_b].code > 0) {
455 score += combos[i].penalty;
456 fprintf(
457 stderr,
458 "note: %s + %s: %s\n",
459 lang_defs[combos[i].lang_a].name,
460 lang_defs[combos[i].lang_b].name,
461 combos[i].remark
462 );
463 fflush(stderr);
464 }
465 }
466
467 if (st.stats[LANG_RUST].code > 0 && st.stats[LANG_CPP].code == 0 && st.stats[LANG_C].code == 0)
468 fprintf(stderr, "note: rust: enormous magnitudes of bogus\n");
469
470 score += st.bonus;
471
472 if (modern_langs == 0 && st.stats[LANG_C].code > 0 && st.stats[LANG_SH].code > 0
473 && st.stats[LANG_AWK].code > 0)
474 printf("note: sh + awk + c only: peak codebase\n");
475
476 printf("---------------------------------------------------------\n");
477 printf("TOTAL CODE LINES: %ld\n", total_code);
478 printf("AGGREGATE BOGOSITY INDEX: %5.2f uL\n", score);
479
480 if (score > 5000.0)
481 fprintf(stderr, "note: bogosity index over 5000: critical\n");
482 else if (st.stats[LANG_C].code > 0 && score < 200.0)
483 printf("note: bogosity index under 200 with c present: clean architecture\n");
484
485 return 0;
486}
487
488int
489boguscmd(int argc __unused, char **argv __unused)
490{
491 printf(
492 "BOGOSITY n. The degree to which something is BOGUS. Measured with a\n"
493 "bogometer, in units of the microLenat (uL).\n"
494 "\n"
495 "BOGUS adj. 1. Non-functional. \"Your patches are bogus.\"\n"
496 "2. Useless. \"OPCON is a bogus program.\"\n"
497 "3. False. \"Your arguments are bogus.\"\n"
498 "4. Incorrect. \"That algorithm is bogus.\"\n"
499 "5. Silly. \"Stop writing those bogus sagas.\"\n"
500 );
501 return 0;
502}