Created
August 20, 2026 13:24
-
-
Save qpwo/b971a1c901bd398764b9b463e6fd6c45 to your computer and use it in GitHub Desktop.
how-to-split-ts-tsx-mjs.c
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //bin/sh -c 'sum=$(cksum < "$0" | tr " " -); o=${TMPDIR:-/tmp}/how-to-split-ts-tsx-mjs-$sum; [ -x "$o" ] || ${CC:-cc} -O3 -Wall -Wextra $(pkg-config --cflags libgit2) "$0" -o "$o" $(pkg-config --libs libgit2) || exit; exec "$o" "$@"' "$0" "$@"; exit | |
| #include <git2.h> | |
| #include <ctype.h> | |
| #include <errno.h> | |
| #include <stdarg.h> | |
| #include <stdint.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <time.h> | |
| #include <unistd.h> | |
| typedef struct { | |
| const char *text; | |
| size_t len; | |
| size_t start; | |
| size_t end; | |
| unsigned line; | |
| unsigned kind; | |
| int mate; | |
| } Token; | |
| typedef struct { | |
| char *key; | |
| char *ident; | |
| uint64_t hash; | |
| size_t start; | |
| size_t end; | |
| unsigned line; | |
| unsigned end_line; | |
| char **refs; | |
| size_t nrefs; | |
| } Symbol; | |
| typedef struct { | |
| char *path; | |
| char *source; | |
| size_t len; | |
| Symbol *symbols; | |
| size_t count; | |
| } File; | |
| typedef struct { | |
| char *name; | |
| char *ident; | |
| unsigned edits; | |
| int module; | |
| int current; | |
| size_t symbols; | |
| size_t bytes; | |
| } Entity; | |
| typedef struct { | |
| uint32_t left; | |
| uint32_t right; | |
| unsigned shared; | |
| double weight; | |
| unsigned used; | |
| } PairSlot; | |
| typedef struct { | |
| char *key; | |
| uint32_t entity; | |
| unsigned used; | |
| } NameSlot; | |
| typedef struct { | |
| uint32_t other; | |
| double weight; | |
| unsigned shared; | |
| } Edge; | |
| typedef struct { | |
| Edge *items; | |
| size_t count; | |
| size_t cap; | |
| } Edges; | |
| typedef struct { | |
| git_repository *repo; | |
| File *files; | |
| size_t nfiles; | |
| size_t filecap; | |
| Entity *entities; | |
| size_t nentities; | |
| size_t entitycap; | |
| NameSlot *names; | |
| size_t namecap; | |
| PairSlot *pairs; | |
| size_t paircap; | |
| size_t npairs; | |
| size_t commits; | |
| size_t mapped; | |
| size_t multi; | |
| size_t merges; | |
| size_t parsed_blobs; | |
| size_t changed_files; | |
| size_t changed_symbols; | |
| size_t broadest; | |
| size_t broad_skipped; | |
| char head[65]; | |
| char *subject; | |
| double repulsion; | |
| size_t median_bytes; | |
| } Analysis; | |
| static const char *source_path_context = "<self-test>"; | |
| static char source_blob_context[1024]; | |
| static _Noreturn void fail(const char *format, ...) { | |
| va_list args; | |
| va_start(args, format); | |
| vfprintf(stderr, format, args); | |
| va_end(args); | |
| fputc('\n', stderr); | |
| exit(1); | |
| } | |
| static void git_fail(const char *operation, int code) { | |
| const git_error *error = git_error_last(); | |
| fail("%s failed (%d): %s", operation, code, error ? error->message : "unknown libgit2 error"); | |
| } | |
| static void *resize(void *old, size_t size) { | |
| void *out = realloc(old, size); | |
| if (!out && size) fail("out of memory allocating %zu bytes", size); | |
| return out; | |
| } | |
| static char *copy_text(const char *text, size_t len) { | |
| char *out = resize(NULL, len + 1); | |
| memcpy(out, text, len); | |
| out[len] = 0; | |
| return out; | |
| } | |
| static int text_is(const char *text, size_t len, const char *wanted) { | |
| return len == strlen(wanted) && !memcmp(text, wanted, len); | |
| } | |
| static int token_is(Token token, const char *wanted) { | |
| return text_is(token.text, token.len, wanted); | |
| } | |
| static int ident_start(unsigned char c) { | |
| return isalpha(c) || c == '_' || c == '$' || c >= 128; | |
| } | |
| static int ident_part(unsigned char c) { | |
| return ident_start(c) || isdigit(c); | |
| } | |
| static size_t quote_end(const char *source, size_t len, size_t i, char quote) { | |
| i++; | |
| while (i < len && source[i] != '\n') { | |
| if (source[i] == '\\' && i + 1 < len) i += 2; | |
| else if (source[i++] == quote) return i; | |
| } | |
| return 0; | |
| } | |
| static size_t regex_end(const char *source, size_t len, size_t i); | |
| static size_t skip_template(const char *source, size_t len, size_t i); | |
| static size_t skip_template_expression(const char *source, size_t len, size_t i) { | |
| size_t depth = 1; | |
| int prefix = 1; | |
| while (i < len) { | |
| if (isspace((unsigned char)source[i])) { | |
| i++; | |
| continue; | |
| } | |
| if (i + 1 < len && source[i] == '/' && source[i + 1] == '/') { | |
| while (i < len && source[i] != '\n') i++; | |
| continue; | |
| } | |
| if (i + 1 < len && source[i] == '/' && source[i + 1] == '*') { | |
| i += 2; | |
| while (i + 1 < len && !(source[i] == '*' && source[i + 1] == '/')) i++; | |
| if (i + 1 == len) return 0; | |
| i += 2; | |
| continue; | |
| } | |
| if (source[i] == '\'' || source[i] == '"') { | |
| size_t end = quote_end(source, len, i, source[i]); | |
| i = end ? end : i + 1; | |
| prefix = 0; | |
| continue; | |
| } | |
| if (source[i] == '`') { | |
| i = skip_template(source, len, i); | |
| prefix = 0; | |
| continue; | |
| } | |
| if (source[i] == '/' && prefix) { | |
| size_t end = regex_end(source, len, i); | |
| if (end) { | |
| i = end; | |
| prefix = 0; | |
| continue; | |
| } | |
| } | |
| if (ident_start((unsigned char)source[i])) { | |
| size_t start = i++; | |
| while (i < len && ident_part((unsigned char)source[i])) i++; | |
| prefix = text_is(source + start, i - start, "return") || | |
| text_is(source + start, i - start, "throw") || | |
| text_is(source + start, i - start, "case") || | |
| text_is(source + start, i - start, "yield") || | |
| text_is(source + start, i - start, "await"); | |
| continue; | |
| } | |
| if (source[i] == '{') { | |
| depth++; | |
| prefix = 1; | |
| } else if (source[i] == '}' && !--depth) { | |
| return i + 1; | |
| } else { | |
| prefix = strchr("([,;:?=!!&|+-*%<>", source[i]) != NULL; | |
| } | |
| i++; | |
| } | |
| return 0; | |
| } | |
| static size_t skip_template(const char *source, size_t len, size_t i) { | |
| size_t start = i; | |
| i++; | |
| while (i < len) { | |
| if (source[i] == '\\' && i + 1 < len) { | |
| i += 2; | |
| continue; | |
| } | |
| if (source[i] == '`') return i + 1; | |
| if (i + 1 < len && source[i] == '$' && source[i + 1] == '{') { | |
| i = skip_template_expression(source, len, i + 2); | |
| if (!i) break; | |
| continue; | |
| } | |
| i++; | |
| } | |
| unsigned line = 1; | |
| for (size_t j = 0; j < start; j++) line += source[j] == '\n'; | |
| fail( | |
| "unclosed template literal in %s at line %u: %.120s", | |
| source_path_context, | |
| line, | |
| source + start | |
| ); | |
| return 0; | |
| } | |
| static int regex_follows(Token *tokens, size_t count) { | |
| if (!count) return 1; | |
| Token token = tokens[count - 1]; | |
| if (!token.kind) return !token_is(token, ")") && !token_is(token, "]") && | |
| !token_is(token, "}") && !token_is(token, "++") && | |
| !token_is(token, "--") && !token_is(token, "<"); | |
| if (token.kind != 1) return 0; | |
| return token_is(token, "return") || token_is(token, "throw") || | |
| token_is(token, "case") || token_is(token, "delete") || | |
| token_is(token, "typeof") || token_is(token, "void") || | |
| token_is(token, "new") || token_is(token, "in") || | |
| token_is(token, "of") || token_is(token, "yield") || | |
| token_is(token, "await"); | |
| } | |
| static size_t regex_end(const char *source, size_t len, size_t i) { | |
| int class = 0; | |
| i++; | |
| while (i < len && source[i] != '\n') { | |
| if (source[i] == '\\' && i + 1 < len) { | |
| i += 2; | |
| continue; | |
| } | |
| if (source[i] == '[') class = 1; | |
| if (source[i] == ']') class = 0; | |
| if (source[i++] == '/' && !class) { | |
| while (i < len && isalpha((unsigned char)source[i])) i++; | |
| return i; | |
| } | |
| } | |
| return 0; | |
| } | |
| static void add_token(Token **tokens, size_t *count, size_t *cap, Token token) { | |
| if (*count == *cap) { | |
| *cap = *cap ? *cap * 2 : 4096; | |
| *tokens = resize(*tokens, *cap * sizeof **tokens); | |
| } | |
| (*tokens)[(*count)++] = token; | |
| } | |
| static Token *lex_ts(const char *source, size_t len, size_t *count) { | |
| Token *tokens = NULL; | |
| size_t used = 0; | |
| size_t cap = 0; | |
| unsigned line = 1; | |
| for (size_t i = 0; i < len;) { | |
| if (isspace((unsigned char)source[i])) { | |
| line += source[i++] == '\n'; | |
| continue; | |
| } | |
| if (i + 1 < len && source[i] == '/' && source[i + 1] == '/') { | |
| i += 2; | |
| while (i < len && source[i] != '\n') i++; | |
| continue; | |
| } | |
| if (i + 1 < len && source[i] == '/' && source[i + 1] == '*') { | |
| i += 2; | |
| while (i + 1 < len && !(source[i] == '*' && source[i + 1] == '/')) { | |
| line += source[i++] == '\n'; | |
| } | |
| if (i + 1 == len) fail("unclosed block comment"); | |
| i += 2; | |
| continue; | |
| } | |
| size_t start = i; | |
| unsigned start_line = line; | |
| unsigned kind = 0; | |
| size_t regex = source[i] == '/' && regex_follows(tokens, used) ? | |
| regex_end(source, len, i) : 0; | |
| size_t quote = source[i] == '\'' || source[i] == '"' ? | |
| quote_end(source, len, i, source[i]) : 0; | |
| if (regex) { | |
| i = regex; | |
| kind = 2; | |
| } else if (quote) { | |
| i = quote; | |
| kind = 2; | |
| } else if (source[i] == '`') { | |
| i = skip_template(source, len, i); | |
| kind = 2; | |
| } else if (ident_start((unsigned char)source[i])) { | |
| i++; | |
| while (i < len && ident_part((unsigned char)source[i])) i++; | |
| kind = 1; | |
| } else if (isdigit((unsigned char)source[i])) { | |
| i++; | |
| while (i < len && (isalnum((unsigned char)source[i]) || strchr("._", source[i]))) i++; | |
| kind = 2; | |
| } else { | |
| static const char *const punct[] = { | |
| ">>>=", "===", "!==", "=>", "?.", "??", "**", "++", "--", "&&", "||", | |
| "==", "!=", "<=", ">=", "<<", ">>", "+=", "-=", "*=", "/=", "%=", "&=", | |
| "|=", "^=", "::", "...", NULL | |
| }; | |
| size_t width = 1; | |
| for (size_t p = 0; punct[p]; p++) { | |
| size_t n = strlen(punct[p]); | |
| if (i + n <= len && !memcmp(source + i, punct[p], n)) { | |
| width = n; | |
| break; | |
| } | |
| } | |
| i += width; | |
| } | |
| for (size_t p = start; p < i; p++) line += source[p] == '\n'; | |
| add_token(&tokens, &used, &cap, (Token){ | |
| source + start, i - start, start, i, start_line, kind, -1 | |
| }); | |
| } | |
| int *stack = resize(NULL, used * sizeof *stack); | |
| size_t depth = 0; | |
| for (size_t i = 0; i < used; i++) { | |
| if (token_is(tokens[i], "(") || token_is(tokens[i], "[") || token_is(tokens[i], "{")) { | |
| stack[depth++] = (int)i; | |
| continue; | |
| } | |
| if (!token_is(tokens[i], ")") && !token_is(tokens[i], "]") && !token_is(tokens[i], "}")) continue; | |
| if (!depth) fail("unmatched closing token on line %u", tokens[i].line); | |
| size_t left = (size_t)stack[--depth]; | |
| int valid = (token_is(tokens[left], "(") && token_is(tokens[i], ")")) || | |
| (token_is(tokens[left], "[") && token_is(tokens[i], "]")) || | |
| (token_is(tokens[left], "{") && token_is(tokens[i], "}")); | |
| if (!valid) { | |
| fprintf(stderr, "%s: tokens before mismatch:", source_path_context); | |
| for (size_t j = i > 30 ? i - 30 : 0; j <= i; j++) | |
| fprintf(stderr, " %.*s@%u", (int)tokens[j].len, tokens[j].text, tokens[j].line); | |
| fprintf(stderr, "\n%s: open stack:", source_path_context); | |
| for (size_t j = 0; j < depth; j++) { | |
| Token open = tokens[stack[j]]; | |
| fprintf(stderr, " %.*s@%u", (int)open.len, open.text, open.line); | |
| } | |
| fputc('\n', stderr); | |
| fail( | |
| "%s: mismatched %.*s on line %u with %.*s on line %u", | |
| source_path_context, | |
| (int)tokens[left].len, | |
| tokens[left].text, | |
| tokens[left].line, | |
| (int)tokens[i].len, | |
| tokens[i].text, | |
| tokens[i].line | |
| ); | |
| } | |
| tokens[left].mate = (int)i; | |
| tokens[i].mate = (int)left; | |
| } | |
| if (depth) fail("unclosed token on line %u", tokens[stack[depth - 1]].line); | |
| free(stack); | |
| *count = used; | |
| return tokens; | |
| } | |
| static uint64_t hash_tokens(Token *tokens, size_t start, size_t end) { | |
| uint64_t hash = 1469598103934665603ULL; | |
| for (size_t i = start; i < end; i++) { | |
| hash ^= tokens[i].len; | |
| hash *= 1099511628211ULL; | |
| for (size_t j = 0; j < tokens[i].len; j++) { | |
| hash ^= (unsigned char)tokens[i].text[j]; | |
| hash *= 1099511628211ULL; | |
| } | |
| } | |
| return hash; | |
| } | |
| static size_t statement_end(Token *tokens, size_t count, size_t start) { | |
| for (size_t i = start; i < count; i++) { | |
| if (tokens[i].mate > (int)i) { | |
| i = (size_t)tokens[i].mate; | |
| continue; | |
| } | |
| if (token_is(tokens[i], ";")) return i + 1; | |
| } | |
| return count; | |
| } | |
| static size_t declaration_end(Token *tokens, size_t count, size_t start, int function) { | |
| for (size_t i = start; i < count; i++) { | |
| if (token_is(tokens[i], "{") && tokens[i].mate > (int)i) { | |
| int signature_type = | |
| function && i && | |
| (token_is(tokens[i - 1], ":") || token_is(tokens[i - 1], "<") || | |
| token_is(tokens[i - 1], "|") || token_is(tokens[i - 1], "&") || | |
| token_is(tokens[i - 1], "=>") || token_is(tokens[i - 1], "extends")); | |
| if (!signature_type) return (size_t)tokens[i].mate + 1; | |
| i = (size_t)tokens[i].mate; | |
| continue; | |
| } | |
| if (token_is(tokens[i], ";")) return i + 1; | |
| if (tokens[i].mate > (int)i) i = (size_t)tokens[i].mate; | |
| } | |
| return count; | |
| } | |
| static int has_name(char **names, size_t count, Token token) { | |
| for (size_t i = 0; i < count; i++) | |
| if (text_is(token.text, token.len, names[i])) return 1; | |
| return 0; | |
| } | |
| static void add_name(char ***names, size_t *count, Token token) { | |
| if (has_name(*names, *count, token)) return; | |
| *names = resize(*names, (*count + 1) * sizeof **names); | |
| (*names)[(*count)++] = copy_text(token.text, token.len); | |
| } | |
| static void add_ref(Symbol *symbol, Token token) { | |
| if (has_name(symbol->refs, symbol->nrefs, token)) return; | |
| symbol->refs = resize(symbol->refs, (symbol->nrefs + 1) * sizeof *symbol->refs); | |
| symbol->refs[symbol->nrefs++] = copy_text(token.text, token.len); | |
| } | |
| static void collect_parameter_names( | |
| char ***bound, | |
| size_t *nbound, | |
| Token *tokens, | |
| size_t open, | |
| size_t close | |
| ) { | |
| size_t segment = open + 1; | |
| for (size_t i = segment; i <= close; i++) { | |
| if (i < close && tokens[i].mate > (int)i) { | |
| i = (size_t)tokens[i].mate; | |
| continue; | |
| } | |
| if (i < close && !token_is(tokens[i], ",")) continue; | |
| for (size_t j = segment; j < i; j++) | |
| if (tokens[j].kind == 1) { | |
| add_name(bound, nbound, tokens[j]); | |
| break; | |
| } | |
| segment = i + 1; | |
| } | |
| } | |
| static void add_template_refs(Symbol *symbol, char **bound, size_t nbound, Token token) { | |
| for (size_t i = 0; i + 2 < token.len; i++) { | |
| if (token.text[i] != '$' || token.text[i + 1] != '{') continue; | |
| size_t end = skip_template_expression(token.text, token.len, i + 2); | |
| if (!end || end <= i + 3) continue; | |
| size_t count = 0; | |
| Token *part = lex_ts(token.text + i + 2, end - i - 3, &count); | |
| for (size_t j = 0; j < count; j++) { | |
| if (part[j].kind == 2 && part[j].len && part[j].text[0] == '`') { | |
| add_template_refs(symbol, bound, nbound, part[j]); | |
| continue; | |
| } | |
| if (part[j].kind != 1 || has_name(bound, nbound, part[j])) continue; | |
| if (j && (token_is(part[j - 1], ".") || token_is(part[j - 1], "?."))) continue; | |
| if (j + 1 < count && token_is(part[j + 1], ":")) continue; | |
| add_ref(symbol, part[j]); | |
| } | |
| free(part); | |
| i = end - 1; | |
| } | |
| } | |
| static void add_symbol( | |
| Symbol **symbols, | |
| size_t *count, | |
| size_t *cap, | |
| const char *kind, | |
| Token name, | |
| Token *tokens, | |
| size_t start, | |
| size_t end, | |
| int references | |
| ) { | |
| if (*count == *cap) { | |
| *cap = *cap ? *cap * 2 : 128; | |
| *symbols = resize(*symbols, *cap * sizeof **symbols); | |
| } | |
| Symbol *symbol = &(*symbols)[(*count)++]; | |
| memset(symbol, 0, sizeof *symbol); | |
| symbol->ident = copy_text(name.text, name.len); | |
| symbol->key = resize(NULL, strlen(kind) + name.len + 2); | |
| snprintf(symbol->key, strlen(kind) + name.len + 2, "%s %.*s", kind, (int)name.len, name.text); | |
| symbol->hash = hash_tokens(tokens, start, end); | |
| symbol->start = tokens[start].start; | |
| symbol->end = tokens[end - 1].end; | |
| symbol->line = tokens[start].line; | |
| symbol->end_line = tokens[end - 1].line; | |
| if (!references) return; | |
| char **bound = NULL; | |
| size_t nbound = 0; | |
| add_name(&bound, &nbound, name); | |
| for (size_t i = start; i < end; i++) { | |
| if ((token_is(tokens[i], "const") || token_is(tokens[i], "let") || | |
| token_is(tokens[i], "var") || token_is(tokens[i], "catch")) && | |
| i + 1 < end) { | |
| size_t next = i + 1; | |
| if (token_is(tokens[next], "(")) next++; | |
| if (next < end && tokens[next].kind == 1) add_name(&bound, &nbound, tokens[next]); | |
| } | |
| if (token_is(tokens[i], "(") && tokens[i].mate > (int)i) { | |
| size_t close = (size_t)tokens[i].mate; | |
| int function_params = | |
| i > start && (token_is(tokens[i - 1], symbol->ident) || | |
| token_is(tokens[i - 1], "function")); | |
| int arrow_params = close + 1 < end && token_is(tokens[close + 1], "=>"); | |
| if (function_params || arrow_params) | |
| collect_parameter_names(&bound, &nbound, tokens, i, close); | |
| } | |
| if (tokens[i].kind == 1 && i + 1 < end && token_is(tokens[i + 1], "=>")) | |
| add_name(&bound, &nbound, tokens[i]); | |
| } | |
| for (size_t i = start; i < end; i++) { | |
| if (tokens[i].kind == 2 && tokens[i].len && tokens[i].text[0] == '`') { | |
| add_template_refs(symbol, bound, nbound, tokens[i]); | |
| continue; | |
| } | |
| if (tokens[i].kind != 1 || has_name(bound, nbound, tokens[i])) continue; | |
| if (i > start && (token_is(tokens[i - 1], ".") || token_is(tokens[i - 1], "?."))) | |
| continue; | |
| if (i + 1 < end && token_is(tokens[i + 1], ":")) continue; | |
| add_ref(symbol, tokens[i]); | |
| } | |
| for (size_t i = 0; i < nbound; i++) free(bound[i]); | |
| free(bound); | |
| } | |
| static int declaration_keyword(Token token) { | |
| static const char *const words[] = { | |
| "function", "class", "interface", "type", "enum", "namespace", "module", NULL | |
| }; | |
| for (size_t i = 0; words[i]; i++) | |
| if (token_is(token, words[i])) return (int)i + 1; | |
| return 0; | |
| } | |
| static Token default_name(Token keyword) { | |
| return (Token){"default", 7, keyword.start, keyword.end, keyword.line, 1, -1}; | |
| } | |
| static size_t extract_declaration( | |
| Symbol **symbols, | |
| size_t *used, | |
| size_t *cap, | |
| Token *tokens, | |
| size_t count, | |
| size_t keyword, | |
| int references | |
| ) { | |
| static const char *const kinds[] = { | |
| "function", "class", "interface", "type", "enum", "namespace", "module" | |
| }; | |
| int declaration = declaration_keyword(tokens[keyword]); | |
| size_t name = keyword + 1; | |
| while (name < count && (token_is(tokens[name], "*") || token_is(tokens[name], "const"))) name++; | |
| Token symbol_name = name < count && tokens[name].kind == 1 ? tokens[name] : default_name(tokens[keyword]); | |
| size_t start = keyword; | |
| while (start && (token_is(tokens[start - 1], "export") || | |
| token_is(tokens[start - 1], "default") || | |
| token_is(tokens[start - 1], "declare") || | |
| token_is(tokens[start - 1], "abstract") || | |
| token_is(tokens[start - 1], "async"))) | |
| start--; | |
| size_t end = declaration == 4 | |
| ? statement_end(tokens, count, keyword) | |
| : declaration_end(tokens, count, keyword, declaration == 1); | |
| add_symbol(symbols, used, cap, kinds[declaration - 1], symbol_name, | |
| tokens, start, end, references); | |
| return end; | |
| } | |
| static size_t extract_variables( | |
| Symbol **symbols, | |
| size_t *used, | |
| size_t *cap, | |
| Token *tokens, | |
| size_t count, | |
| size_t keyword, | |
| int references | |
| ) { | |
| size_t end = statement_end(tokens, count, keyword); | |
| size_t start = keyword; | |
| while (start && (token_is(tokens[start - 1], "export") || | |
| token_is(tokens[start - 1], "declare"))) | |
| start--; | |
| size_t segment = keyword + 1; | |
| size_t type_angles = 0; | |
| int type_annotation = 0; | |
| for (size_t i = segment; i <= end; i++) { | |
| if (i < end && tokens[i].mate > (int)i) { | |
| i = (size_t)tokens[i].mate; | |
| continue; | |
| } | |
| if (i < end && (token_is(tokens[i], ":") || | |
| token_is(tokens[i], "as") || | |
| token_is(tokens[i], "satisfies"))) | |
| type_annotation = 1; | |
| if (i < end && type_annotation && token_is(tokens[i], "<")) type_angles++; | |
| if (i < end && type_annotation && token_is(tokens[i], ">") && type_angles) type_angles--; | |
| if (i < end && type_annotation && token_is(tokens[i], ">>")) { | |
| type_angles = type_angles > 1 ? type_angles - 2 : 0; | |
| } | |
| if (i < end && token_is(tokens[i], "=") && !type_angles) type_annotation = 0; | |
| if (i < end && (!token_is(tokens[i], ",") || type_angles)) continue; | |
| size_t name = segment; | |
| while (name < i && tokens[name].kind != 1) name++; | |
| if (name < i) | |
| add_symbol(symbols, used, cap, | |
| token_is(tokens[keyword], "const") ? "const" : | |
| token_is(tokens[keyword], "let") ? "let" : "var", | |
| tokens[name], tokens, start, end, references); | |
| segment = i + 1; | |
| type_annotation = 0; | |
| type_angles = 0; | |
| } | |
| return end; | |
| } | |
| static Symbol *extract_symbols(const char *source, size_t len, size_t *count, int references) { | |
| size_t ntokens = 0; | |
| Token *tokens = lex_ts(source, len, &ntokens); | |
| Symbol *symbols = NULL; | |
| size_t used = 0; | |
| size_t cap = 0; | |
| for (size_t i = 0; i < ntokens;) { | |
| if (token_is(tokens[i], "import")) { | |
| i = statement_end(tokens, ntokens, i); | |
| continue; | |
| } | |
| if (declaration_keyword(tokens[i])) { | |
| i = extract_declaration(&symbols, &used, &cap, tokens, ntokens, i, references); | |
| continue; | |
| } | |
| if (token_is(tokens[i], "const") || token_is(tokens[i], "let") || token_is(tokens[i], "var")) { | |
| i = extract_variables(&symbols, &used, &cap, tokens, ntokens, i, references); | |
| continue; | |
| } | |
| if (token_is(tokens[i], "{") && tokens[i].mate > (int)i) { | |
| i = (size_t)tokens[i].mate + 1; | |
| continue; | |
| } | |
| i++; | |
| } | |
| free(tokens); | |
| *count = used; | |
| return symbols; | |
| } | |
| static void free_symbol(Symbol *symbol) { | |
| free(symbol->key); | |
| free(symbol->ident); | |
| for (size_t i = 0; i < symbol->nrefs; i++) free(symbol->refs[i]); | |
| free(symbol->refs); | |
| } | |
| static int symbol_order(const void *left, const void *right) { | |
| const Symbol *a = left; | |
| const Symbol *b = right; | |
| int order = strcmp(a->key, b->key); | |
| if (order) return order; | |
| return a->hash < b->hash ? -1 : a->hash != b->hash; | |
| } | |
| static int symbol_has_ref(Symbol *symbol, const char *name) { | |
| for (size_t i = 0; i < symbol->nrefs; i++) | |
| if (!strcmp(symbol->refs[i], name)) return 1; | |
| return 0; | |
| } | |
| static void test_extract(void) { | |
| const char source[] = | |
| "const alpha = { brace: /[{}]/g }, beta = (x) => x + 1;\n" | |
| "const nested = (flag) => `x ${flag ? `yes ${alpha}` : `no`}`;\n" | |
| "const slug = 'top';\n" | |
| "const codes: Record<string, string> = { curp: 'invalid' };\n" | |
| "export const mapped = codes as Record<string, unknown>;\n" | |
| "export function Panel(id: string) { const slug = id; return <span data={{ slug: true }}>{document.getElementById(`output-${id}`)?.textContent}</span>; }\n" | |
| "export async function run(x: number) { return alpha.brace.test(`${x}`); }\n" | |
| "export interface Shape { x: number }\n" | |
| "export default function () { return beta(2) }\n"; | |
| size_t count = 0; | |
| Symbol *symbols = extract_symbols(source, sizeof source - 1, &count, 1); | |
| qsort(symbols, count, sizeof *symbols, symbol_order); | |
| const char *expected[] = { | |
| "const alpha", "const beta", "const codes", "const mapped", "const nested", "const slug", | |
| "function Panel", "function default", "function run", "interface Shape" | |
| }; | |
| if (count != sizeof expected / sizeof *expected) | |
| fail("self-test symbol count: got %zu, expected %zu", count, sizeof expected / sizeof *expected); | |
| for (size_t i = 0; i < count; i++) | |
| if (strcmp(symbols[i].key, expected[i])) | |
| fail("self-test symbol %zu: got %s, expected %s", i, symbols[i].key, expected[i]); | |
| if (!symbol_has_ref(&symbols[4], "alpha") || symbol_has_ref(&symbols[4], "flag")) | |
| fail("self-test nested template references"); | |
| if (!symbol_has_ref(&symbols[6], "document") || symbol_has_ref(&symbols[6], "slug") || | |
| symbol_has_ref(&symbols[6], "getElementById")) | |
| fail("self-test scoped TSX references"); | |
| for (size_t i = 0; i < count; i++) free_symbol(&symbols[i]); | |
| free(symbols); | |
| puts("self-test: extraction ok"); | |
| } | |
| static void test_symbol_bags(void); | |
| static void test_split_helpers(void); | |
| static void run_repository(const char *path, int split); | |
| static double build_graph(Analysis *analysis, Edges **graph_out, double **degree_out); | |
| static size_t cluster_graph(Analysis *analysis, Edges *graph, double *degree, double graph_weight); | |
| static void report_modules(Analysis *analysis, size_t modules); | |
| static void report_split_candidates(Analysis *analysis, size_t modules); | |
| static void report_extractable_communities(Analysis *analysis, size_t modules, int apply); | |
| static void split_repository(Analysis *analysis, size_t modules); | |
| static size_t module_prefix_end(const char *source, size_t len); | |
| int main(int argc, char **argv) { | |
| if (argc == 2 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help"))) { | |
| printf("usage: %s [--report | --split] [REPOSITORY]\n", argv[0]); | |
| puts("Report or apply a path-independent TS/TSX/MJS split from Git co-edit history."); | |
| puts("--split replaces all repository TS/TSX/MJS sources with 50 named root TSX files."); | |
| return 0; | |
| } | |
| if (argc == 2 && !strcmp(argv[1], "--self-test")) { | |
| test_extract(); | |
| test_symbol_bags(); | |
| test_split_helpers(); | |
| return 0; | |
| } | |
| int split = argc > 1 && !strcmp(argv[1], "--split"); | |
| int report = argc > 1 && !strcmp(argv[1], "--report"); | |
| int path_arg = split || report ? 2 : 1; | |
| if (argc > path_arg + 1 || (argc > 1 && argv[1][0] == '-' && !split && !report)) { | |
| fprintf(stderr, "usage: %s [--report | --split] [REPOSITORY]\n", argv[0]); | |
| return 2; | |
| } | |
| run_repository(argc > path_arg ? argv[path_arg] : ".", split); | |
| return 0; | |
| } | |
| static int source_path(const char *path) { | |
| size_t len = strlen(path); | |
| return (len > 3 && !strcmp(path + len - 3, ".ts")) || | |
| (len > 4 && !strcmp(path + len - 4, ".tsx")) || | |
| (len > 4 && !strcmp(path + len - 4, ".mjs")); | |
| } | |
| static size_t module_root_stem_len(const char *path, size_t stem_len) { | |
| if (stem_len < 11) return stem_len; | |
| const char *suffix = path + stem_len - 11; | |
| if (memcmp(suffix, ".module-", 8) || | |
| !isdigit((unsigned char)suffix[8]) || | |
| !isdigit((unsigned char)suffix[9]) || | |
| !isdigit((unsigned char)suffix[10])) | |
| return stem_len; | |
| return stem_len - 11; | |
| } | |
| static uint64_t hash_string(const char *text) { | |
| uint64_t hash = 1469598103934665603ULL; | |
| while (*text) { | |
| hash ^= (unsigned char)*text++; | |
| hash *= 1099511628211ULL; | |
| } | |
| return hash; | |
| } | |
| static void rebuild_names(Analysis *analysis, size_t capacity) { | |
| NameSlot *old = analysis->names; | |
| size_t oldcap = analysis->namecap; | |
| analysis->names = calloc(capacity, sizeof *analysis->names); | |
| if (!analysis->names) fail("out of memory building symbol index"); | |
| analysis->namecap = capacity; | |
| for (size_t i = 0; i < oldcap; i++) { | |
| if (!old[i].used) continue; | |
| size_t slot = hash_string(old[i].key) & (capacity - 1); | |
| while (analysis->names[slot].used) slot = (slot + 1) & (capacity - 1); | |
| analysis->names[slot] = old[i]; | |
| } | |
| free(old); | |
| } | |
| static uint32_t entity_index(Analysis *analysis, const char *key) { | |
| if (!analysis->namecap) rebuild_names(analysis, 1024); | |
| if ((analysis->nentities + 1) * 10 > analysis->namecap * 7) | |
| rebuild_names(analysis, analysis->namecap * 2); | |
| size_t slot = hash_string(key) & (analysis->namecap - 1); | |
| while (analysis->names[slot].used) { | |
| if (!strcmp(analysis->names[slot].key, key)) return analysis->names[slot].entity; | |
| slot = (slot + 1) & (analysis->namecap - 1); | |
| } | |
| if (analysis->nentities == analysis->entitycap) { | |
| analysis->entitycap = analysis->entitycap ? analysis->entitycap * 2 : 1024; | |
| analysis->entities = resize(analysis->entities, analysis->entitycap * sizeof *analysis->entities); | |
| } | |
| uint32_t entity = (uint32_t)analysis->nentities++; | |
| if ((size_t)entity != analysis->nentities - 1) fail("too many symbols"); | |
| char *name = copy_text(key, strlen(key)); | |
| char *space = strchr(name, ' '); | |
| analysis->entities[entity] = (Entity){ | |
| name, copy_text(space ? space + 1 : name, strlen(space ? space + 1 : name)), 0, -1, 0, 0, 0 | |
| }; | |
| analysis->names[slot] = (NameSlot){name, entity, 1}; | |
| return entity; | |
| } | |
| static uint64_t hash_pair(uint32_t left, uint32_t right) { | |
| uint64_t value = ((uint64_t)left << 32) | right; | |
| value ^= value >> 33; | |
| value *= 0xff51afd7ed558ccdULL; | |
| value ^= value >> 33; | |
| value *= 0xc4ceb9fe1a85ec53ULL; | |
| return value ^ (value >> 33); | |
| } | |
| static void rebuild_pairs(Analysis *analysis, size_t capacity) { | |
| PairSlot *old = analysis->pairs; | |
| size_t oldcap = analysis->paircap; | |
| analysis->pairs = calloc(capacity, sizeof *analysis->pairs); | |
| if (!analysis->pairs) fail("out of memory building co-edit graph"); | |
| analysis->paircap = capacity; | |
| for (size_t i = 0; i < oldcap; i++) { | |
| if (!old[i].used) continue; | |
| size_t slot = hash_pair(old[i].left, old[i].right) & (capacity - 1); | |
| while (analysis->pairs[slot].used) slot = (slot + 1) & (capacity - 1); | |
| analysis->pairs[slot] = old[i]; | |
| } | |
| free(old); | |
| } | |
| static PairSlot *pair_slot(Analysis *analysis, uint32_t left, uint32_t right) { | |
| if (left > right) { | |
| uint32_t swap = left; | |
| left = right; | |
| right = swap; | |
| } | |
| if (!analysis->paircap) rebuild_pairs(analysis, 2048); | |
| if ((analysis->npairs + 1) * 10 > analysis->paircap * 7) | |
| rebuild_pairs(analysis, analysis->paircap * 2); | |
| size_t slot = hash_pair(left, right) & (analysis->paircap - 1); | |
| while (analysis->pairs[slot].used) { | |
| if (analysis->pairs[slot].left == left && analysis->pairs[slot].right == right) | |
| return &analysis->pairs[slot]; | |
| slot = (slot + 1) & (analysis->paircap - 1); | |
| } | |
| analysis->npairs++; | |
| analysis->pairs[slot] = (PairSlot){left, right, 0, 0, 1}; | |
| return &analysis->pairs[slot]; | |
| } | |
| typedef struct { | |
| Symbol *items; | |
| size_t count; | |
| size_t cap; | |
| } Symbols; | |
| static void append_blob_symbols( | |
| Analysis *analysis, | |
| Symbols *out, | |
| const git_oid *oid, | |
| const char *path | |
| ) { | |
| if (!source_path(path) || git_oid_is_zero(oid)) return; | |
| git_blob *blob = NULL; | |
| int code = git_blob_lookup(&blob, analysis->repo, oid); | |
| if (code) git_fail("git_blob_lookup", code); | |
| size_t count = 0; | |
| char oid_text[GIT_OID_MAX_HEXSIZE + 1]; | |
| git_oid_tostr(oid_text, sizeof oid_text, oid); | |
| snprintf(source_blob_context, sizeof source_blob_context, "%s@%s", path, oid_text); | |
| source_path_context = source_blob_context; | |
| Symbol *symbols = extract_symbols(git_blob_rawcontent(blob), git_blob_rawsize(blob), &count, 0); | |
| git_blob_free(blob); | |
| if (out->count + count > out->cap) { | |
| out->cap = out->cap ? out->cap * 2 : 128; | |
| while (out->cap < out->count + count) out->cap *= 2; | |
| out->items = resize(out->items, out->cap * sizeof *out->items); | |
| } | |
| if (count) { | |
| memcpy(out->items + out->count, symbols, count * sizeof *symbols); | |
| out->count += count; | |
| } | |
| analysis->parsed_blobs++; | |
| free(symbols); | |
| } | |
| static void free_symbols(Symbols *symbols) { | |
| for (size_t i = 0; i < symbols->count; i++) free_symbol(&symbols->items[i]); | |
| free(symbols->items); | |
| } | |
| static void record_changed(Analysis *analysis, uint32_t *changed, size_t count) { | |
| if (!count) return; | |
| analysis->mapped++; | |
| analysis->multi += count > 1; | |
| analysis->changed_symbols += count; | |
| if (count > analysis->broadest) analysis->broadest = count; | |
| for (size_t i = 0; i < count; i++) analysis->entities[changed[i]].edits++; | |
| if (count < 2) return; | |
| if (count > 512) { | |
| analysis->broad_skipped++; | |
| return; | |
| } | |
| double weight = 1.0 / (double)(count - 1); | |
| for (size_t i = 0; i < count; i++) | |
| for (size_t j = i + 1; j < count; j++) { | |
| PairSlot *pair = pair_slot(analysis, changed[i], changed[j]); | |
| pair->shared++; | |
| pair->weight += weight; | |
| } | |
| } | |
| static void compare_symbol_bags(Analysis *analysis, Symbols *old, Symbols *newer) { | |
| qsort(old->items, old->count, sizeof *old->items, symbol_order); | |
| qsort(newer->items, newer->count, sizeof *newer->items, symbol_order); | |
| uint32_t *changed = resize(NULL, (old->count + newer->count) * sizeof *changed); | |
| size_t used = 0; | |
| size_t left = 0; | |
| size_t right = 0; | |
| while (left < old->count || right < newer->count) { | |
| const char *key = left < old->count ? old->items[left].key : newer->items[right].key; | |
| if (right < newer->count && | |
| (left == old->count || strcmp(newer->items[right].key, key) < 0)) | |
| key = newer->items[right].key; | |
| size_t left_end = left; | |
| size_t right_end = right; | |
| while (left_end < old->count && !strcmp(old->items[left_end].key, key)) left_end++; | |
| while (right_end < newer->count && !strcmp(newer->items[right_end].key, key)) right_end++; | |
| int differs = left_end - left != right_end - right; | |
| for (size_t i = 0; !differs && i < left_end - left; i++) | |
| differs = old->items[left + i].hash != newer->items[right + i].hash; | |
| if (differs) changed[used++] = entity_index(analysis, key); | |
| left = left_end; | |
| right = right_end; | |
| } | |
| record_changed(analysis, changed, used); | |
| free(changed); | |
| } | |
| static void append_test_source(Symbols *bag, const char *source) { | |
| size_t count = 0; | |
| Symbol *symbols = extract_symbols(source, strlen(source), &count, 0); | |
| bag->items = resize(bag->items, (bag->count + count) * sizeof *bag->items); | |
| if (count) memcpy(bag->items + bag->count, symbols, count * sizeof *symbols); | |
| bag->count += count; | |
| bag->cap = bag->count; | |
| free(symbols); | |
| } | |
| static void test_symbol_bags(void) { | |
| Symbols old = {0}; | |
| Symbols moved = {0}; | |
| Symbols edited = {0}; | |
| append_test_source(&old, "export const alpha = () => 1;"); | |
| append_test_source(&old, "export const beta = () => 2;"); | |
| append_test_source(&moved, "export const beta = () => 2;"); | |
| append_test_source(&moved, "export const alpha = () => 1;"); | |
| append_test_source(&edited, "export const beta = () => 3;"); | |
| append_test_source(&edited, "export const alpha = () => 1;"); | |
| Analysis analysis = {0}; | |
| compare_symbol_bags(&analysis, &old, &moved); | |
| if (analysis.mapped || analysis.changed_symbols) | |
| fail("self-test path-independent symbol move"); | |
| compare_symbol_bags(&analysis, &old, &edited); | |
| if (analysis.mapped != 1 || analysis.changed_symbols != 1 || | |
| analysis.nentities != 1 || strcmp(analysis.entities[0].name, "const beta")) | |
| fail("self-test path-independent symbol edit"); | |
| free_symbols(&old); | |
| free_symbols(&moved); | |
| free_symbols(&edited); | |
| for (size_t i = 0; i < analysis.nentities; i++) { | |
| free(analysis.entities[i].name); | |
| free(analysis.entities[i].ident); | |
| } | |
| free(analysis.entities); | |
| free(analysis.names); | |
| free(analysis.pairs); | |
| puts("self-test: path-independent bags ok"); | |
| } | |
| static void analyze_commit(Analysis *analysis, git_commit *commit) { | |
| if (git_commit_parentcount(commit) != 1) { | |
| analysis->merges += git_commit_parentcount(commit) > 1; | |
| return; | |
| } | |
| git_commit *parent = NULL; | |
| git_tree *old_tree = NULL; | |
| git_tree *new_tree = NULL; | |
| git_diff *diff = NULL; | |
| int code = git_commit_parent(&parent, commit, 0); | |
| if (code) git_fail("git_commit_parent", code); | |
| if ((code = git_commit_tree(&old_tree, parent))) git_fail("git_commit_tree(parent)", code); | |
| if ((code = git_commit_tree(&new_tree, commit))) git_fail("git_commit_tree(commit)", code); | |
| if ((code = git_diff_tree_to_tree(&diff, analysis->repo, old_tree, new_tree, NULL))) | |
| git_fail("git_diff_tree_to_tree", code); | |
| Symbols old = {0}; | |
| Symbols newer = {0}; | |
| size_t deltas = git_diff_num_deltas(diff); | |
| for (size_t i = 0; i < deltas; i++) { | |
| const git_diff_delta *delta = git_diff_get_delta(diff, i); | |
| int relevant = source_path(delta->old_file.path) || source_path(delta->new_file.path); | |
| if (!relevant) continue; | |
| analysis->changed_files++; | |
| append_blob_symbols(analysis, &old, &delta->old_file.id, delta->old_file.path); | |
| append_blob_symbols(analysis, &newer, &delta->new_file.id, delta->new_file.path); | |
| } | |
| compare_symbol_bags(analysis, &old, &newer); | |
| free_symbols(&old); | |
| free_symbols(&newer); | |
| git_diff_free(diff); | |
| git_tree_free(old_tree); | |
| git_tree_free(new_tree); | |
| git_commit_free(parent); | |
| } | |
| static int load_current_file( | |
| const char *root, | |
| const git_tree_entry *entry, | |
| void *payload | |
| ) { | |
| Analysis *analysis = payload; | |
| if (git_tree_entry_type(entry) != GIT_OBJECT_BLOB) return 0; | |
| size_t len = strlen(root) + strlen(git_tree_entry_name(entry)); | |
| char *path = resize(NULL, len + 1); | |
| snprintf(path, len + 1, "%s%s", root, git_tree_entry_name(entry)); | |
| if (!source_path(path)) { | |
| free(path); | |
| return 0; | |
| } | |
| git_blob *blob = NULL; | |
| int code = git_blob_lookup(&blob, analysis->repo, git_tree_entry_id(entry)); | |
| if (code) git_fail("git_blob_lookup(HEAD)", code); | |
| size_t count = 0; | |
| size_t source_len = git_blob_rawsize(blob); | |
| char *source = copy_text(git_blob_rawcontent(blob), source_len); | |
| source_path_context = path; | |
| Symbol *symbols = extract_symbols(source, source_len, &count, 1); | |
| git_blob_free(blob); | |
| if (analysis->nfiles == analysis->filecap) { | |
| analysis->filecap = analysis->filecap ? analysis->filecap * 2 : 1024; | |
| analysis->files = resize(analysis->files, analysis->filecap * sizeof *analysis->files); | |
| } | |
| analysis->files[analysis->nfiles++] = (File){path, source, source_len, symbols, count}; | |
| for (size_t i = 0; i < count; i++) { | |
| uint32_t entity = entity_index(analysis, symbols[i].key); | |
| analysis->entities[entity].current = 1; | |
| analysis->entities[entity].symbols++; | |
| analysis->entities[entity].bytes += symbols[i].end - symbols[i].start; | |
| } | |
| return 0; | |
| } | |
| static void load_current(Analysis *analysis, git_commit *head) { | |
| git_tree *tree = NULL; | |
| int code = git_commit_tree(&tree, head); | |
| if (code) git_fail("git_commit_tree(HEAD)", code); | |
| code = git_tree_walk(tree, GIT_TREEWALK_PRE, load_current_file, analysis); | |
| if (code) git_fail("git_tree_walk", code); | |
| git_tree_free(tree); | |
| } | |
| static void scan_history(Analysis *analysis) { | |
| git_revwalk *walk = NULL; | |
| int code = git_revwalk_new(&walk, analysis->repo); | |
| if (code) git_fail("git_revwalk_new", code); | |
| git_revwalk_sorting(walk, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME | GIT_SORT_REVERSE); | |
| if ((code = git_revwalk_push_head(walk))) git_fail("git_revwalk_push_head", code); | |
| git_oid oid; | |
| time_t last_status = time(NULL); | |
| while ((code = git_revwalk_next(&oid, walk)) == 0) { | |
| git_commit *commit = NULL; | |
| if ((code = git_commit_lookup(&commit, analysis->repo, &oid))) | |
| git_fail("git_commit_lookup", code); | |
| analysis->commits++; | |
| analyze_commit(analysis, commit); | |
| git_commit_free(commit); | |
| if (time(NULL) - last_status >= 10) { | |
| fprintf( | |
| stderr, | |
| "scan: commits=%zu mapped=%zu blobs=%zu pairs=%zu\n", | |
| analysis->commits, | |
| analysis->mapped, | |
| analysis->parsed_blobs, | |
| analysis->npairs | |
| ); | |
| last_status = time(NULL); | |
| } | |
| } | |
| if (code != GIT_ITEROVER) git_fail("git_revwalk_next", code); | |
| git_revwalk_free(walk); | |
| } | |
| static void free_analysis(Analysis *analysis, Edges *graph, double *degree) { | |
| for (size_t i = 0; i < analysis->nentities; i++) free(graph[i].items); | |
| free(graph); | |
| free(degree); | |
| for (size_t i = 0; i < analysis->nfiles; i++) { | |
| File *file = &analysis->files[i]; | |
| free(file->path); | |
| free(file->source); | |
| for (size_t j = 0; j < file->count; j++) free_symbol(&file->symbols[j]); | |
| free(file->symbols); | |
| } | |
| free(analysis->files); | |
| for (size_t i = 0; i < analysis->nentities; i++) { | |
| free(analysis->entities[i].name); | |
| free(analysis->entities[i].ident); | |
| } | |
| free(analysis->entities); | |
| free(analysis->names); | |
| free(analysis->pairs); | |
| free(analysis->subject); | |
| } | |
| static void run_repository(const char *path, int split) { | |
| git_libgit2_init(); | |
| Analysis analysis = {0}; | |
| int code = git_repository_open_ext(&analysis.repo, path, 0, NULL); | |
| if (code) git_fail("git_repository_open_ext", code); | |
| git_oid head_id; | |
| if ((code = git_reference_name_to_id(&head_id, analysis.repo, "HEAD"))) | |
| git_fail("git_reference_name_to_id(HEAD)", code); | |
| git_commit *head = NULL; | |
| if ((code = git_commit_lookup(&head, analysis.repo, &head_id))) | |
| git_fail("git_commit_lookup(HEAD)", code); | |
| git_oid_tostr(analysis.head, sizeof analysis.head, &head_id); | |
| analysis.subject = copy_text(git_commit_summary(head), strlen(git_commit_summary(head))); | |
| scan_history(&analysis); | |
| load_current(&analysis, head); | |
| Edges *graph = NULL; | |
| double *degree = NULL; | |
| double graph_weight = build_graph(&analysis, &graph, °ree); | |
| size_t modules = cluster_graph(&analysis, graph, degree, graph_weight); | |
| printf("TS/TSX/MJS HISTORY SPLIT REPORT\n"); | |
| printf("repository=%s endpoint=%.12s subject=%s\n", path, analysis.head, analysis.subject); | |
| printf( | |
| "commits=%zu merges_skipped=%zu mapped=%zu multi_symbol=%zu broad_skipped=%zu\n", | |
| analysis.commits, | |
| analysis.merges, | |
| analysis.mapped, | |
| analysis.multi, | |
| analysis.broad_skipped | |
| ); | |
| printf( | |
| "current_files=%zu historical_symbols=%zu changed_files=%zu parsed_blobs=%zu coedit_pairs=%zu broadest=%zu\n", | |
| analysis.nfiles, | |
| analysis.nentities, | |
| analysis.changed_files, | |
| analysis.parsed_blobs, | |
| analysis.npairs, | |
| analysis.broadest | |
| ); | |
| printf( | |
| "graph_weight=%.2f repulsion=%.9g modules=%zu median_module_bytes=%zu target_bytes=%u\n", | |
| graph_weight, | |
| analysis.repulsion, | |
| modules, | |
| analysis.median_bytes, | |
| 20 * 1024 | |
| ); | |
| if (!split) report_modules(&analysis, modules); | |
| if (split) split_repository(&analysis, modules); | |
| if (getenv("HOW_SPLIT_VERBOSE")) { | |
| report_split_candidates(&analysis, modules); | |
| report_extractable_communities(&analysis, modules, 0); | |
| } | |
| free_analysis(&analysis, graph, degree); | |
| git_commit_free(head); | |
| git_repository_free(analysis.repo); | |
| git_libgit2_shutdown(); | |
| } | |
| static double build_graph(Analysis *analysis, Edges **graph_out, double **degree_out) { | |
| Edges *graph = calloc(analysis->nentities, sizeof *graph); | |
| double *degree = calloc(analysis->nentities, sizeof *degree); | |
| if (!graph || !degree) fail("out of memory building sparse graph"); | |
| double total = 0; | |
| for (size_t i = 0; i < analysis->paircap; i++) { | |
| PairSlot pair = analysis->pairs[i]; | |
| if (!pair.used) continue; | |
| Entity *left = &analysis->entities[pair.left]; | |
| Entity *right = &analysis->entities[pair.right]; | |
| if (!left->current || !right->current || left->symbols != 1 || right->symbols != 1) | |
| continue; | |
| Edges *a = &graph[pair.left]; | |
| Edges *b = &graph[pair.right]; | |
| if (a->count == a->cap) { | |
| a->cap = a->cap ? a->cap * 2 : 8; | |
| a->items = resize(a->items, a->cap * sizeof *a->items); | |
| } | |
| if (b->count == b->cap) { | |
| b->cap = b->cap ? b->cap * 2 : 8; | |
| b->items = resize(b->items, b->cap * sizeof *b->items); | |
| } | |
| a->items[a->count++] = (Edge){pair.right, pair.weight, pair.shared}; | |
| b->items[b->count++] = (Edge){pair.left, pair.weight, pair.shared}; | |
| degree[pair.left] += pair.weight; | |
| degree[pair.right] += pair.weight; | |
| total += pair.weight; | |
| } | |
| if (total == 0.0) fail("not enough current-symbol co-edits to propose a refactor"); | |
| *graph_out = graph; | |
| *degree_out = degree; | |
| return total; | |
| } | |
| static PairSlot *merge_cross_slot( | |
| PairSlot *cross, | |
| size_t capacity, | |
| uint32_t left, | |
| uint32_t right | |
| ) { | |
| if (left > right) { | |
| uint32_t swap = left; | |
| left = right; | |
| right = swap; | |
| } | |
| size_t slot = hash_pair(left, right) & (capacity - 1); | |
| while (cross[slot].used) { | |
| if (cross[slot].left == left && cross[slot].right == right) | |
| return &cross[slot]; | |
| slot = (slot + 1) & (capacity - 1); | |
| } | |
| cross[slot] = (PairSlot){left, right, 0, 0, 1}; | |
| return &cross[slot]; | |
| } | |
| static void build_merge_cross( | |
| Analysis *analysis, | |
| Edges *graph, | |
| PairSlot *cross, | |
| size_t capacity | |
| ) { | |
| memset(cross, 0, capacity * sizeof *cross); | |
| for (size_t i = 0; i < analysis->nentities; i++) | |
| for (size_t e = 0; e < graph[i].count; e++) { | |
| uint32_t other = graph[i].items[e].other; | |
| if (i >= other) continue; | |
| int left = analysis->entities[i].module; | |
| int right = analysis->entities[other].module; | |
| if (left < 0 || right < 0 || left == right) continue; | |
| PairSlot *pair = merge_cross_slot( | |
| cross, | |
| capacity, | |
| (uint32_t)left, | |
| (uint32_t)right | |
| ); | |
| pair->weight += graph[i].items[e].weight; | |
| pair->shared++; | |
| } | |
| } | |
| static size_t merge_modules( | |
| Analysis *analysis, | |
| Edges *graph, | |
| size_t *members, | |
| size_t *bytes, | |
| double repulsion | |
| ) { | |
| size_t capacity = analysis->paircap ? analysis->paircap : 16; | |
| PairSlot *cross = calloc(capacity, sizeof *cross); | |
| if (!cross) fail("out of memory merging modules"); | |
| size_t merged = 0; | |
| for (;;) { | |
| build_merge_cross(analysis, graph, cross, capacity); | |
| int best_left = -1; | |
| int best_right = -1; | |
| double best_gain = 1e-12; | |
| for (size_t i = 0; i < capacity; i++) { | |
| if (!cross[i].used || !members[cross[i].left] || !members[cross[i].right]) | |
| continue; | |
| double pairs = | |
| (double)members[cross[i].left] * members[cross[i].right]; | |
| double gain = cross[i].weight - repulsion * pairs; | |
| if (gain <= best_gain) continue; | |
| best_gain = gain; | |
| best_left = (int)cross[i].left; | |
| best_right = (int)cross[i].right; | |
| } | |
| if (best_left < 0) break; | |
| for (size_t i = 0; i < analysis->nentities; i++) | |
| if (analysis->entities[i].module == best_right) | |
| analysis->entities[i].module = best_left; | |
| members[best_left] += members[best_right]; | |
| members[best_right] = 0; | |
| bytes[best_left] += bytes[best_right]; | |
| bytes[best_right] = 0; | |
| merged++; | |
| } | |
| free(cross); | |
| return merged; | |
| } | |
| static int size_order(const void *left, const void *right) { | |
| size_t a = *(const size_t *)left; | |
| size_t b = *(const size_t *)right; | |
| return a < b ? -1 : a != b; | |
| } | |
| static size_t cluster_once( | |
| Analysis *analysis, | |
| Edges *graph, | |
| double repulsion, | |
| size_t *median_bytes | |
| ) { | |
| size_t n = analysis->nentities; | |
| size_t *members = calloc(n, sizeof *members); | |
| size_t *bytes = calloc(n, sizeof *bytes); | |
| size_t *linked = calloc(n, sizeof *linked); | |
| double *toward = calloc(n, sizeof *toward); | |
| uint32_t *touched = resize(NULL, n * sizeof *touched); | |
| if (!members || !bytes || !linked || !toward) | |
| fail("out of memory clustering graph"); | |
| for (size_t i = 0; i < n; i++) { | |
| analysis->entities[i].module = -1; | |
| if (!graph[i].count) continue; | |
| analysis->entities[i].module = (int)i; | |
| members[i] = 1; | |
| bytes[i] = analysis->entities[i].bytes; | |
| } | |
| for (size_t pass = 0; pass < 100; pass++) { | |
| size_t moved = 0; | |
| for (size_t i = 0; i < n; i++) { | |
| if (analysis->entities[i].module < 0) continue; | |
| int old = analysis->entities[i].module; | |
| members[old]--; | |
| bytes[old] -= analysis->entities[i].bytes; | |
| size_t used = 0; | |
| for (size_t e = 0; e < graph[i].count; e++) { | |
| int module = analysis->entities[graph[i].items[e].other].module; | |
| if (module < 0) continue; | |
| if (!linked[module]) touched[used++] = (uint32_t)module; | |
| linked[module]++; | |
| toward[module] += graph[i].items[e].weight; | |
| } | |
| int best = old; | |
| double best_score = toward[old] - repulsion * members[old]; | |
| for (size_t j = 0; j < used; j++) { | |
| int module = (int)touched[j]; | |
| double score = toward[module] - repulsion * members[module]; | |
| if (score > best_score + 1e-12 || | |
| (score > best_score - 1e-12 && module < best)) { | |
| best = module; | |
| best_score = score; | |
| } | |
| } | |
| for (size_t j = 0; j < used; j++) { | |
| toward[touched[j]] = 0; | |
| linked[touched[j]] = 0; | |
| } | |
| analysis->entities[i].module = best; | |
| members[best]++; | |
| bytes[best] += analysis->entities[i].bytes; | |
| moved += best != old; | |
| } | |
| if (!moved) break; | |
| } | |
| merge_modules(analysis, graph, members, bytes, repulsion); | |
| int *dense = resize(NULL, n * sizeof *dense); | |
| size_t *sizes = resize(NULL, n * sizeof *sizes); | |
| size_t modules = 0; | |
| for (size_t i = 0; i < n; i++) { | |
| dense[i] = members[i] ? (int)modules++ : -1; | |
| if (members[i]) sizes[dense[i]] = bytes[i]; | |
| } | |
| for (size_t i = 0; i < n; i++) | |
| if (analysis->entities[i].module >= 0) | |
| analysis->entities[i].module = dense[analysis->entities[i].module]; | |
| qsort(sizes, modules, sizeof *sizes, size_order); | |
| size_t total_bytes = 0; | |
| for (size_t i = 0; i < modules; i++) total_bytes += sizes[i]; | |
| size_t cumulative_bytes = 0; | |
| *median_bytes = 0; | |
| for (size_t i = 0; i < modules; i++) { | |
| cumulative_bytes += sizes[i]; | |
| if (cumulative_bytes * 2 < total_bytes) continue; | |
| *median_bytes = sizes[i]; | |
| break; | |
| } | |
| free(sizes); | |
| free(dense); | |
| free(members); | |
| free(bytes); | |
| free(linked); | |
| free(toward); | |
| free(touched); | |
| return modules; | |
| } | |
| static size_t cluster_graph( | |
| Analysis *analysis, | |
| Edges *graph, | |
| double *degree, | |
| double graph_weight | |
| ) { | |
| (void)degree; | |
| (void)graph_weight; | |
| const size_t target = 20 * 1024; | |
| int *best = resize(NULL, analysis->nentities * sizeof *best); | |
| size_t best_median = 0; | |
| size_t best_modules = cluster_once(analysis, graph, 0, &best_median); | |
| double best_repulsion = 0; | |
| for (size_t i = 0; i < analysis->nentities; i++) | |
| best[i] = analysis->entities[i].module; | |
| if (best_median > target) { | |
| double low = 0; | |
| double high = 0.000001; | |
| size_t median = best_median; | |
| for (size_t i = 0; i < 60 && median > target; i++) { | |
| low = high; | |
| high *= 2; | |
| size_t modules = cluster_once(analysis, graph, high, &median); | |
| if (getenv("HOW_SPLIT_TRACE")) | |
| fprintf(stderr, "repulsion=%.9g modules=%zu weighted_median_bytes=%zu\n", high, modules, median); | |
| size_t distance = median > target ? median - target : target - median; | |
| size_t best_distance = | |
| best_median > target ? best_median - target : target - best_median; | |
| if (distance < best_distance) { | |
| best_median = median; | |
| best_modules = modules; | |
| best_repulsion = high; | |
| for (size_t j = 0; j < analysis->nentities; j++) | |
| best[j] = analysis->entities[j].module; | |
| } | |
| } | |
| for (size_t i = 0; i < 18; i++) { | |
| double middle = (low + high) / 2; | |
| size_t median; | |
| size_t modules = cluster_once(analysis, graph, middle, &median); | |
| if (getenv("HOW_SPLIT_TRACE")) | |
| fprintf(stderr, "repulsion=%.9g modules=%zu weighted_median_bytes=%zu\n", middle, modules, median); | |
| size_t distance = median > target ? median - target : target - median; | |
| size_t best_distance = | |
| best_median > target ? best_median - target : target - best_median; | |
| if (distance < best_distance) { | |
| best_median = median; | |
| best_modules = modules; | |
| best_repulsion = middle; | |
| for (size_t j = 0; j < analysis->nentities; j++) | |
| best[j] = analysis->entities[j].module; | |
| } | |
| if (median > target) low = middle; | |
| else high = middle; | |
| } | |
| } | |
| for (size_t i = 0; i < analysis->nentities; i++) | |
| analysis->entities[i].module = best[i]; | |
| analysis->repulsion = best_repulsion; | |
| analysis->median_bytes = best_median; | |
| free(best); | |
| return best_modules; | |
| } | |
| typedef struct { | |
| size_t module; | |
| size_t symbols; | |
| unsigned edits; | |
| } ModuleRow; | |
| static int module_row_order(const void *left, const void *right) { | |
| const ModuleRow *a = left; | |
| const ModuleRow *b = right; | |
| if (a->edits != b->edits) return a->edits < b->edits ? 1 : -1; | |
| if (a->symbols != b->symbols) return a->symbols < b->symbols ? 1 : -1; | |
| return a->module < b->module ? -1 : a->module != b->module; | |
| } | |
| static void print_module_chunk( | |
| Analysis *analysis, | |
| ModuleRow row, | |
| size_t *files, | |
| size_t part, | |
| size_t first, | |
| size_t count, | |
| size_t bytes | |
| ) { | |
| printf( | |
| "\nfile_%03zu community=%zu part=%zu symbols=%zu bytes=%zu edits=%u%s\n", | |
| ++*files, row.module + 1, part, count, bytes, row.edits, | |
| bytes > 20 * 1024 ? " oversized_symbol=1" : "" | |
| ); | |
| size_t ordinal = 0; | |
| for (size_t i = 0; i < analysis->nentities; i++) { | |
| Entity *entity = &analysis->entities[i]; | |
| if (entity->module != (int)row.module || | |
| !entity->current || entity->symbols != 1) | |
| continue; | |
| if (ordinal >= first && ordinal < first + count) | |
| printf(" %s\n", entity->name); | |
| ordinal++; | |
| } | |
| } | |
| static void report_modules(Analysis *analysis, size_t modules) { | |
| if (!modules) return; | |
| ModuleRow *rows = calloc(modules, sizeof *rows); | |
| if (!rows) fail("out of memory reporting modules"); | |
| for (size_t i = 0; i < modules; i++) rows[i].module = i; | |
| for (size_t i = 0; i < analysis->nentities; i++) { | |
| Entity *entity = &analysis->entities[i]; | |
| if (entity->module < 0 || !entity->current || entity->symbols != 1) continue; | |
| rows[entity->module].symbols++; | |
| rows[entity->module].edits += entity->edits; | |
| } | |
| qsort(rows, modules, sizeof *rows, module_row_order); | |
| printf("\nPROPOSED SYMBOL FILES\n"); | |
| size_t files = 0; | |
| size_t unclustered = 0; | |
| for (size_t row = 0; row < modules; row++) { | |
| if (rows[row].symbols < 2) { | |
| unclustered += rows[row].symbols; | |
| continue; | |
| } | |
| size_t first = 0; | |
| size_t count = 0; | |
| size_t bytes = 0; | |
| size_t part = 1; | |
| for (size_t i = 0; i < analysis->nentities; i++) { | |
| Entity *entity = &analysis->entities[i]; | |
| if (entity->module != (int)rows[row].module || | |
| !entity->current || entity->symbols != 1) | |
| continue; | |
| if (count && bytes + entity->bytes > 20 * 1024) { | |
| print_module_chunk(&analysis[0], rows[row], &files, part++, first, count, bytes); | |
| first += count; | |
| count = 0; | |
| bytes = 0; | |
| } | |
| count++; | |
| bytes += entity->bytes; | |
| } | |
| if (count) | |
| print_module_chunk(&analysis[0], rows[row], &files, part, first, count, bytes); | |
| } | |
| printf("\nproposed_files=%zu unclustered_symbols=%zu\n", files, unclustered); | |
| free(rows); | |
| } | |
| typedef struct { | |
| size_t file; | |
| size_t placed; | |
| size_t distinct; | |
| size_t first; | |
| size_t second; | |
| unsigned first_edits; | |
| unsigned second_edits; | |
| uint64_t score; | |
| } FileRow; | |
| static int file_row_order(const void *left, const void *right) { | |
| const FileRow *a = left; | |
| const FileRow *b = right; | |
| if (a->score != b->score) return a->score < b->score ? 1 : -1; | |
| if (a->placed != b->placed) return a->placed < b->placed ? 1 : -1; | |
| return a->file < b->file ? -1 : a->file != b->file; | |
| } | |
| static void report_split_candidates(Analysis *analysis, size_t modules) { | |
| if (!modules) return; | |
| FileRow *rows = resize(NULL, analysis->nfiles * sizeof *rows); | |
| size_t used = 0; | |
| size_t *counts = calloc(modules, sizeof *counts); | |
| unsigned *edits = calloc(modules, sizeof *edits); | |
| if (!counts || !edits) fail("out of memory ranking split candidates"); | |
| for (size_t f = 0; f < analysis->nfiles; f++) { | |
| memset(counts, 0, modules * sizeof *counts); | |
| memset(edits, 0, modules * sizeof *edits); | |
| size_t placed = 0; | |
| for (size_t s = 0; s < analysis->files[f].count; s++) { | |
| uint32_t entity = entity_index(analysis, analysis->files[f].symbols[s].key); | |
| int module = analysis->entities[entity].module; | |
| if (module < 0 || analysis->entities[entity].symbols != 1) continue; | |
| counts[module]++; | |
| edits[module] += analysis->entities[entity].edits; | |
| placed++; | |
| } | |
| size_t first = SIZE_MAX; | |
| size_t second = SIZE_MAX; | |
| size_t distinct = 0; | |
| for (size_t module = 0; module < modules; module++) { | |
| if (!counts[module]) continue; | |
| distinct++; | |
| if (first == SIZE_MAX || edits[module] > edits[first]) { | |
| second = first; | |
| first = module; | |
| } else if (second == SIZE_MAX || edits[module] > edits[second]) { | |
| second = module; | |
| } | |
| } | |
| if (second == SIZE_MAX || counts[first] < 2 || counts[second] < 2 || | |
| edits[first] < 4 || edits[second] < 4) continue; | |
| rows[used++] = (FileRow){ | |
| f, | |
| placed, | |
| distinct, | |
| first, | |
| second, | |
| edits[first], | |
| edits[second], | |
| (uint64_t)edits[second] * counts[second] * placed | |
| }; | |
| } | |
| qsort(rows, used, sizeof *rows, file_row_order); | |
| printf("\nPROPOSED FILE SPLITS\n"); | |
| for (size_t r = 0; r < used && r < 20; r++) { | |
| FileRow row = rows[r]; | |
| File *file = &analysis->files[row.file]; | |
| printf( | |
| "\n%s symbols=%zu placed=%zu communities=%zu split_score=%llu\n", | |
| file->path, | |
| file->count, | |
| row.placed, | |
| row.distinct, | |
| (unsigned long long)row.score | |
| ); | |
| for (size_t module = 0; module < modules; module++) { | |
| size_t count = 0; | |
| unsigned body_edits = 0; | |
| for (size_t s = 0; s < file->count; s++) { | |
| uint32_t entity = entity_index(analysis, file->symbols[s].key); | |
| if (analysis->entities[entity].module != (int)module) continue; | |
| count++; | |
| body_edits += analysis->entities[entity].edits; | |
| } | |
| if (!count) continue; | |
| printf(" module_%03zu symbols=%zu edits=%u\n", module + 1, count, body_edits); | |
| for (size_t s = 0; s < file->count; s++) { | |
| Symbol *symbol = &file->symbols[s]; | |
| uint32_t entity = entity_index(analysis, symbol->key); | |
| if (analysis->entities[entity].module != (int)module) continue; | |
| printf( | |
| " %3u line=%u %s\n", | |
| analysis->entities[entity].edits, | |
| symbol->line, | |
| symbol->key | |
| ); | |
| } | |
| } | |
| } | |
| printf("\nshowing %zu of %zu files with two substantial history communities\n", used < 20 ? used : 20, used); | |
| free(counts); | |
| free(edits); | |
| free(rows); | |
| } | |
| typedef struct { | |
| size_t file; | |
| size_t module; | |
| size_t members; | |
| size_t span_symbols; | |
| unsigned edits; | |
| size_t internal_refs; | |
| size_t cut_refs; | |
| size_t first; | |
| size_t last; | |
| uint64_t score; | |
| } ExtractRow; | |
| static int extract_row_order(const void *left, const void *right) { | |
| const ExtractRow *a = left; | |
| const ExtractRow *b = right; | |
| if (a->score != b->score) return a->score < b->score ? 1 : -1; | |
| if (a->edits != b->edits) return a->edits < b->edits ? 1 : -1; | |
| return a->file < b->file ? -1 : a->file != b->file; | |
| } | |
| static int symbol_has_ident(Symbol *symbol, const char *ident) { | |
| return !strcmp(symbol->ident, ident); | |
| } | |
| static void apply_extract(Analysis *analysis, ExtractRow row); | |
| static void report_extractable_communities(Analysis *analysis, size_t modules, int apply) { | |
| ExtractRow *rows = NULL; | |
| size_t used = 0; | |
| size_t cap = 0; | |
| for (size_t f = 0; f < analysis->nfiles; f++) { | |
| File *file = &analysis->files[f]; | |
| for (size_t module = 0; module < modules; module++) { | |
| size_t members = 0; | |
| size_t first = SIZE_MAX; | |
| size_t last = 0; | |
| unsigned edits = 0; | |
| for (size_t s = 0; s < file->count; s++) { | |
| uint32_t entity = entity_index(analysis, file->symbols[s].key); | |
| if (analysis->entities[entity].module != (int)module) continue; | |
| members++; | |
| edits += analysis->entities[entity].edits; | |
| if (first == SIZE_MAX || file->symbols[s].start < file->symbols[first].start) first = s; | |
| if (members == 1 || file->symbols[s].end > file->symbols[last].end) last = s; | |
| } | |
| if (members < 3 || edits < 8) continue; | |
| int expanded; | |
| do { | |
| expanded = 0; | |
| size_t begin = file->symbols[first].start; | |
| size_t end = file->symbols[last].end; | |
| for (size_t s = 0; s < file->count; s++) { | |
| Symbol *from = &file->symbols[s]; | |
| if (from->start < begin || from->end > end) continue; | |
| for (size_t r = 0; r < from->nrefs; r++) | |
| for (size_t t = 0; t < file->count; t++) { | |
| if (!symbol_has_ident(&file->symbols[t], from->refs[r])) continue; | |
| if (file->symbols[t].start < file->symbols[first].start) { | |
| first = t; | |
| expanded = 1; | |
| } | |
| if (file->symbols[t].end > file->symbols[last].end) { | |
| last = t; | |
| expanded = 1; | |
| } | |
| break; | |
| } | |
| } | |
| } while (expanded); | |
| size_t span_symbols = 0; | |
| size_t internal_refs = 0; | |
| size_t cut_refs = 0; | |
| size_t begin = file->symbols[first].start; | |
| size_t end = file->symbols[last].end; | |
| size_t move_first = SIZE_MAX; | |
| for (size_t s = 0; s < file->count; s++) { | |
| Symbol *from = &file->symbols[s]; | |
| int from_span = from->start >= begin && from->end <= end; | |
| if (from_span) { | |
| span_symbols++; | |
| if (move_first == SIZE_MAX) move_first = s; | |
| } | |
| for (size_t r = 0; r < from->nrefs; r++) { | |
| int target_local = 0; | |
| int target_span = 0; | |
| for (size_t t = 0; t < file->count; t++) { | |
| if (!symbol_has_ident(&file->symbols[t], from->refs[r])) continue; | |
| target_local = 1; | |
| target_span = | |
| file->symbols[t].start >= begin && | |
| file->symbols[t].end <= end; | |
| break; | |
| } | |
| if (!target_local) continue; | |
| if (from_span && target_span) internal_refs++; | |
| else if (from_span != target_span) cut_refs++; | |
| } | |
| } | |
| if (move_first == SIZE_MAX || | |
| file->count - span_symbols < 3 || | |
| members * 2 < span_symbols) | |
| continue; | |
| size_t span_lines = | |
| file->symbols[last].end_line - | |
| file->symbols[move_first].line + 1; | |
| uint64_t score = | |
| (uint64_t)edits * members * members * | |
| (internal_refs + 1) * (internal_refs + 1) * 1000000 / | |
| (span_symbols * span_symbols * | |
| (cut_refs + 1) * (cut_refs + 1) * span_lines); | |
| if (used == cap) { | |
| cap = cap ? cap * 2 : 128; | |
| rows = resize(rows, cap * sizeof *rows); | |
| } | |
| rows[used++] = (ExtractRow){ | |
| f, module, members, span_symbols, edits, internal_refs, cut_refs, | |
| move_first, last, score | |
| }; | |
| } | |
| } | |
| qsort(rows, used, sizeof *rows, extract_row_order); | |
| printf("\nEXTRACTABLE HISTORY COMMUNITIES\n"); | |
| for (size_t i = 0; i < used && i < 10; i++) { | |
| ExtractRow row = rows[i]; | |
| File *file = &analysis->files[row.file]; | |
| printf( | |
| "\n%s module_%03zu members=%zu span_symbols=%zu edits=%u internal_refs=%zu cut_refs=%zu score=%llu lines=%u-%u\n", | |
| file->path, | |
| row.module + 1, | |
| row.members, | |
| row.span_symbols, | |
| row.edits, | |
| row.internal_refs, | |
| row.cut_refs, | |
| (unsigned long long)row.score, | |
| file->symbols[row.first].line, | |
| file->symbols[row.last].end_line | |
| ); | |
| size_t begin = file->symbols[row.first].start; | |
| size_t end = file->symbols[row.last].end; | |
| for (size_t s = 0; s < file->count; s++) { | |
| Symbol *symbol = &file->symbols[s]; | |
| if (symbol->start < begin || symbol->end > end) continue; | |
| uint32_t entity = entity_index(analysis, symbol->key); | |
| printf( | |
| " %c %3u line=%u %s\n", | |
| analysis->entities[entity].module == (int)row.module ? '*' : '-', | |
| analysis->entities[entity].edits, | |
| symbol->line, | |
| symbol->key | |
| ); | |
| } | |
| } | |
| printf("\nshowing %zu of %zu cohesive file/community spans\n", used < 10 ? used : 10, used); | |
| if (apply && used) { | |
| unsigned char *done = calloc(analysis->nfiles, 1); | |
| if (!done) fail("out of memory tracking applied files"); | |
| for (size_t i = 0; i < used; i++) { | |
| if (done[rows[i].file]) continue; | |
| done[rows[i].file] = 1; | |
| apply_extract(analysis, rows[i]); | |
| } | |
| free(done); | |
| } | |
| free(rows); | |
| } | |
| static void write_file(const char *path, const char *data, size_t len) { | |
| FILE *out = fopen(path, "wb"); | |
| if (!out) fail("fopen %s failed: %s", path, strerror(errno)); | |
| if (fwrite(data, 1, len, out) != len) { | |
| int code = errno; | |
| fclose(out); | |
| fail("fwrite %s failed: %s", path, strerror(code)); | |
| } | |
| if (fclose(out)) fail("fclose %s failed: %s", path, strerror(errno)); | |
| } | |
| typedef struct { | |
| size_t file; | |
| size_t start; | |
| size_t end; | |
| size_t symbols; | |
| size_t chunk; | |
| int module; | |
| uint32_t leader; | |
| } SplitUnit; | |
| typedef struct { | |
| char *data; | |
| char *path; | |
| size_t len; | |
| size_t cap; | |
| size_t symbols; | |
| size_t source_bytes; | |
| uint32_t leaders[8]; | |
| size_t nleaders; | |
| } SplitChunk; | |
| typedef struct { | |
| int module; | |
| size_t unit; | |
| size_t bytes; | |
| uint32_t leader; | |
| } SplitGroup; | |
| static int find_entity(Analysis *analysis, const char *key) { | |
| if (!analysis->namecap) return -1; | |
| size_t slot = hash_string(key) & (analysis->namecap - 1); | |
| while (analysis->names[slot].used) { | |
| if (!strcmp(analysis->names[slot].key, key)) | |
| return (int)analysis->names[slot].entity; | |
| slot = (slot + 1) & (analysis->namecap - 1); | |
| } | |
| return -1; | |
| } | |
| static char *read_file(const char *path, size_t *len) { | |
| FILE *in = fopen(path, "rb"); | |
| if (!in) fail("fopen %s failed: %s", path, strerror(errno)); | |
| if (fseek(in, 0, SEEK_END)) fail("fseek %s failed: %s", path, strerror(errno)); | |
| long size = ftell(in); | |
| if (size < 0) fail("ftell %s failed: %s", path, strerror(errno)); | |
| rewind(in); | |
| char *data = resize(NULL, (size_t)size + 1); | |
| if (fread(data, 1, (size_t)size, in) != (size_t)size) | |
| fail("fread %s failed: %s", path, strerror(errno)); | |
| if (fclose(in)) fail("fclose %s failed: %s", path, strerror(errno)); | |
| data[size] = 0; | |
| *len = (size_t)size; | |
| return data; | |
| } | |
| static void append_chunk(SplitChunk *chunk, const char *data, size_t len) { | |
| if (chunk->len + len + 1 > chunk->cap) { | |
| chunk->cap = chunk->cap ? chunk->cap * 2 : 32768; | |
| while (chunk->cap < chunk->len + len + 1) chunk->cap *= 2; | |
| chunk->data = resize(chunk->data, chunk->cap); | |
| } | |
| memcpy(chunk->data + chunk->len, data, len); | |
| chunk->len += len; | |
| chunk->data[chunk->len] = 0; | |
| } | |
| static int split_group_order(const void *left, const void *right) { | |
| const SplitGroup *a = left; | |
| const SplitGroup *b = right; | |
| if (a->bytes != b->bytes) return a->bytes < b->bytes ? 1 : -1; | |
| if (a->module != b->module) return a->module < b->module ? -1 : 1; | |
| return a->unit < b->unit ? -1 : a->unit != b->unit; | |
| } | |
| static void add_chunk_leader(SplitChunk *chunk, Analysis *analysis, uint32_t leader) { | |
| if (leader == UINT32_MAX) return; | |
| for (size_t i = 0; i < chunk->nleaders; i++) | |
| if (chunk->leaders[i] == leader) return; | |
| size_t at = chunk->nleaders; | |
| if (at < 8) chunk->nleaders++; | |
| else at = 7; | |
| while (at && analysis->entities[chunk->leaders[at - 1]].edits < | |
| analysis->entities[leader].edits) { | |
| if (at < 8) chunk->leaders[at] = chunk->leaders[at - 1]; | |
| at--; | |
| } | |
| chunk->leaders[at] = leader; | |
| } | |
| static void validate_split_inputs(Analysis *analysis) { | |
| const char *workdir = git_repository_workdir(analysis->repo); | |
| if (!workdir) fail("cannot split a bare repository"); | |
| for (size_t i = 0; i < analysis->nfiles; i++) { | |
| File *file = &analysis->files[i]; | |
| char *path = resize(NULL, strlen(workdir) + strlen(file->path) + 1); | |
| sprintf(path, "%s%s", workdir, file->path); | |
| size_t len = 0; | |
| char *data = read_file(path, &len); | |
| if (len != file->len || memcmp(data, file->source, len)) | |
| fail("refusing to replace modified source: %s", file->path); | |
| free(data); | |
| free(path); | |
| } | |
| git_status_options options; | |
| int code = git_status_options_init(&options, GIT_STATUS_OPTIONS_VERSION); | |
| if (code) git_fail("git_status_options_init", code); | |
| options.flags = | |
| GIT_STATUS_OPT_INCLUDE_UNTRACKED | | |
| GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; | |
| git_status_list *statuses = NULL; | |
| code = git_status_list_new(&statuses, analysis->repo, &options); | |
| if (code) git_fail("git_status_list_new", code); | |
| size_t count = git_status_list_entrycount(statuses); | |
| for (size_t i = 0; i < count; i++) { | |
| const git_status_entry *entry = git_status_byindex(statuses, i); | |
| const char *path = entry->index_to_workdir | |
| ? entry->index_to_workdir->new_file.path | |
| : entry->head_to_index ? entry->head_to_index->new_file.path : NULL; | |
| if (path && source_path(path)) | |
| fail("refusing to replace changed or untracked source: %s status=%u", path, entry->status); | |
| } | |
| git_status_list_free(statuses); | |
| } | |
| static void add_split_unit( | |
| SplitUnit **units, | |
| size_t *count, | |
| size_t *capacity, | |
| SplitUnit unit | |
| ) { | |
| if (*count == *capacity) { | |
| *capacity = *capacity ? *capacity * 2 : 4096; | |
| *units = resize(*units, *capacity * sizeof **units); | |
| } | |
| (*units)[(*count)++] = unit; | |
| } | |
| static SplitUnit *collect_split_units( | |
| Analysis *analysis, | |
| size_t *count, | |
| size_t *source_bytes, | |
| size_t *source_symbols | |
| ) { | |
| SplitUnit *units = NULL; | |
| size_t capacity = 0; | |
| for (size_t f = 0; f < analysis->nfiles; f++) { | |
| File *file = &analysis->files[f]; | |
| *source_bytes += file->len; | |
| *source_symbols += file->count; | |
| size_t cursor = 0; | |
| for (size_t i = 0; i < file->count;) { | |
| size_t next = i + 1; | |
| while (next < file->count && | |
| file->symbols[next].start == file->symbols[i].start && | |
| file->symbols[next].end == file->symbols[i].end) | |
| next++; | |
| if (file->symbols[i].start < cursor) | |
| fail("overlapping symbol spans at byte %zu in %s", file->symbols[i].start, file->path); | |
| int module = -2; | |
| uint32_t leader = UINT32_MAX; | |
| for (size_t j = i; j < next; j++) { | |
| int entity = find_entity(analysis, file->symbols[j].key); | |
| int candidate = | |
| entity >= 0 && | |
| analysis->entities[entity].current && | |
| analysis->entities[entity].symbols == 1 | |
| ? analysis->entities[entity].module | |
| : -1; | |
| if (module == -2) module = candidate; | |
| else if (module != candidate) module = -1; | |
| if (entity >= 0 && | |
| analysis->entities[entity].symbols == 1 && | |
| (leader == UINT32_MAX || | |
| analysis->entities[entity].edits > analysis->entities[leader].edits)) | |
| leader = (uint32_t)entity; | |
| } | |
| add_split_unit( | |
| &units, | |
| count, | |
| &capacity, | |
| (SplitUnit){ | |
| f, cursor, file->symbols[i].end, next - i, | |
| SIZE_MAX, module < 0 ? -1 : module, leader | |
| } | |
| ); | |
| cursor = file->symbols[i].end; | |
| i = next; | |
| } | |
| if (cursor < file->len) | |
| add_split_unit( | |
| &units, | |
| count, | |
| &capacity, | |
| (SplitUnit){f, cursor, file->len, 0, SIZE_MAX, -1, UINT32_MAX} | |
| ); | |
| } | |
| return units; | |
| } | |
| static SplitGroup *build_split_groups( | |
| SplitUnit *units, | |
| size_t nunits, | |
| size_t modules, | |
| size_t max_bytes, | |
| size_t *count | |
| ) { | |
| size_t *module_bytes = calloc(modules, sizeof *module_bytes); | |
| if (!module_bytes) fail("out of memory sizing split communities"); | |
| for (size_t i = 0; i < nunits; i++) | |
| if (units[i].module >= 0) | |
| module_bytes[units[i].module] += units[i].end - units[i].start + 1; | |
| SplitGroup *groups = resize(NULL, (modules + nunits) * sizeof *groups); | |
| for (size_t i = 0; i < modules; i++) | |
| if (module_bytes[i] && module_bytes[i] <= max_bytes) | |
| groups[(*count)++] = (SplitGroup){(int)i, SIZE_MAX, module_bytes[i], UINT32_MAX}; | |
| for (size_t i = 0; i < nunits; i++) { | |
| if (units[i].module >= 0 && module_bytes[units[i].module] <= max_bytes) | |
| continue; | |
| size_t bytes = units[i].end - units[i].start + 1; | |
| if (bytes > max_bytes) | |
| fail("natural declaration unit exceeds one-tenth limit: bytes=%zu max=%zu", bytes, max_bytes); | |
| groups[(*count)++] = (SplitGroup){-1, i, bytes, units[i].leader}; | |
| } | |
| free(module_bytes); | |
| qsort(groups, *count, sizeof *groups, split_group_order); | |
| return groups; | |
| } | |
| static void pack_split_groups( | |
| SplitUnit *units, | |
| size_t nunits, | |
| SplitGroup *groups, | |
| size_t ngroups, | |
| size_t modules, | |
| SplitChunk *chunks, | |
| size_t nchunks, | |
| size_t max_bytes | |
| ) { | |
| size_t *module_chunk = resize(NULL, modules * sizeof *module_chunk); | |
| for (size_t i = 0; i < modules; i++) module_chunk[i] = SIZE_MAX; | |
| for (size_t i = 0; i < ngroups; i++) { | |
| size_t chunk = SIZE_MAX; | |
| for (size_t j = 0; j < nchunks; j++) { | |
| if (chunks[j].len + groups[i].bytes > max_bytes) continue; | |
| if (chunk == SIZE_MAX || chunks[j].len < chunks[chunk].len) chunk = j; | |
| } | |
| if (chunk == SIZE_MAX) | |
| fail("cannot fit split group: bytes=%zu max=%zu", groups[i].bytes, max_bytes); | |
| chunks[chunk].len += groups[i].bytes; | |
| if (groups[i].module >= 0) module_chunk[groups[i].module] = chunk; | |
| else units[groups[i].unit].chunk = chunk; | |
| } | |
| for (size_t i = 0; i < nunits; i++) | |
| if (units[i].chunk == SIZE_MAX && units[i].module >= 0) | |
| units[i].chunk = module_chunk[units[i].module]; | |
| for (size_t i = 0; i < nunits; i++) | |
| if (units[i].chunk == SIZE_MAX) | |
| fail("split unit was not assigned: unit=%zu module=%d", i, units[i].module); | |
| for (size_t i = 0; i < nchunks; i++) { | |
| if (!chunks[i].len) fail("split produced empty output: chunk=%zu", i); | |
| chunks[i].len = 0; | |
| } | |
| free(module_chunk); | |
| } | |
| static void build_split_chunks( | |
| Analysis *analysis, | |
| SplitUnit *units, | |
| size_t nunits, | |
| SplitChunk *chunks | |
| ) { | |
| for (size_t i = 0; i < nunits; i++) { | |
| SplitUnit *unit = &units[i]; | |
| File *file = &analysis->files[unit->file]; | |
| SplitChunk *chunk = &chunks[unit->chunk]; | |
| append_chunk(chunk, file->source + unit->start, unit->end - unit->start); | |
| append_chunk(chunk, "\n", 1); | |
| chunk->source_bytes += unit->end - unit->start; | |
| chunk->symbols += unit->symbols; | |
| add_chunk_leader(chunk, analysis, unit->leader); | |
| } | |
| } | |
| static size_t title_words(const char *ident, char words[][32], size_t count) { | |
| for (size_t i = 0; ident[i] && count < 32;) { | |
| while (ident[i] && !isalnum((unsigned char)ident[i])) i++; | |
| if (!ident[i]) break; | |
| size_t start = i++; | |
| while (ident[i] && isalnum((unsigned char)ident[i])) { | |
| int lower_upper = | |
| islower((unsigned char)ident[i - 1]) && | |
| isupper((unsigned char)ident[i]); | |
| int acronym_word = | |
| i > start + 1 && | |
| isupper((unsigned char)ident[i - 1]) && | |
| isupper((unsigned char)ident[i]) && | |
| islower((unsigned char)ident[i + 1]); | |
| if (lower_upper || acronym_word) break; | |
| i++; | |
| } | |
| size_t len = i - start; | |
| if (len > 31) len = 31; | |
| words[count][0] = (char)toupper((unsigned char)ident[start]); | |
| for (size_t j = 1; j < len; j++) | |
| words[count][j] = (char)tolower((unsigned char)ident[start + j]); | |
| words[count][len] = 0; | |
| int duplicate = 0; | |
| for (size_t j = 0; j < count; j++) | |
| duplicate |= !strcmp(words[j], words[count]); | |
| if (!duplicate) count++; | |
| } | |
| return count; | |
| } | |
| static int split_path_available( | |
| SplitChunk *chunks, | |
| size_t current, | |
| const char *path | |
| ) { | |
| for (size_t i = 0; i < current; i++) | |
| if (!strcmp(chunks[i].path, path)) return 0; | |
| FILE *existing = fopen(path, "rb"); | |
| if (!existing) { | |
| if (errno == ENOENT) return 1; | |
| fail("fopen %s failed: %s", path, strerror(errno)); | |
| } | |
| fclose(existing); | |
| return 0; | |
| } | |
| static void split_repository(Analysis *analysis, size_t modules) { | |
| validate_split_inputs(analysis); | |
| size_t nunits = 0; | |
| size_t source_bytes = 0; | |
| size_t source_symbols = 0; | |
| SplitUnit *units = collect_split_units( | |
| analysis, | |
| &nunits, | |
| &source_bytes, | |
| &source_symbols | |
| ); | |
| if (!nunits || !source_bytes) fail("repository has no source bytes to split"); | |
| const size_t nchunks = 50; | |
| size_t output_bytes = source_bytes + nunits; | |
| size_t max_bytes = output_bytes / 10; | |
| SplitChunk *chunks = calloc(nchunks, sizeof *chunks); | |
| if (!chunks) fail("out of memory allocating split files"); | |
| size_t ngroups = 0; | |
| SplitGroup *groups = build_split_groups( | |
| units, | |
| nunits, | |
| modules, | |
| max_bytes, | |
| &ngroups | |
| ); | |
| pack_split_groups( | |
| units, | |
| nunits, | |
| groups, | |
| ngroups, | |
| modules, | |
| chunks, | |
| nchunks, | |
| max_bytes | |
| ); | |
| build_split_chunks(analysis, units, nunits, chunks); | |
| size_t covered_bytes = 0; | |
| size_t covered_symbols = 0; | |
| size_t written_bytes = 0; | |
| for (size_t i = 0; i < nchunks; i++) { | |
| covered_bytes += chunks[i].source_bytes; | |
| covered_symbols += chunks[i].symbols; | |
| written_bytes += chunks[i].len; | |
| if (chunks[i].len * 10 > output_bytes) | |
| fail( | |
| "split size limit exceeded: chunk=%zu bytes=%zu total=%zu", | |
| i + 1, | |
| chunks[i].len, | |
| output_bytes | |
| ); | |
| } | |
| if (covered_bytes != source_bytes || | |
| covered_symbols != source_symbols || | |
| written_bytes != output_bytes) | |
| fail( | |
| "split coverage mismatch: bytes=%zu/%zu symbols=%zu/%zu output=%zu/%zu", | |
| covered_bytes, | |
| source_bytes, | |
| covered_symbols, | |
| source_symbols, | |
| written_bytes, | |
| output_bytes | |
| ); | |
| static const char *const fallback[] = { | |
| "Core", "Runtime", "Service", "System", "Engine", "Model", "Store", "Flow", | |
| "Tools", "Views", "Rules", "Types", "Tasks", "Events", "Inputs", "Outputs", | |
| "Records", "Queries", "Actions", "Routes", "Workers", "Clients", "Servers", | |
| "Reports", "Reviews", "Search", "Graph", "Auth", "Billing", "Pilots", | |
| "Cases", "Subjects", "Evidence", "Sources", "Documents", "Sessions", | |
| "Metrics", "Registry", "Pipeline", "Studio", "Console", "Network", | |
| "Storage", "Images", "Emails", "Teams", "Access", "Config", "Schema", "Batch" | |
| }; | |
| const char *workdir = git_repository_workdir(analysis->repo); | |
| for (size_t i = 0; i < nchunks; i++) { | |
| char words[32][32] = {{0}}; | |
| size_t nwords = 0; | |
| for (size_t j = 0; j < chunks[i].nleaders; j++) | |
| nwords = title_words( | |
| analysis->entities[chunks[i].leaders[j]].ident, | |
| words, | |
| nwords | |
| ); | |
| char path[4096]; | |
| for (size_t a = 0; a < nwords && !chunks[i].path; a++) | |
| for (size_t b = a + 1; b < nwords && !chunks[i].path; b++) { | |
| snprintf(path, sizeof path, "%s%s%s.tsx", workdir, words[a], words[b]); | |
| if (split_path_available(chunks, i, path)) | |
| chunks[i].path = copy_text(path, strlen(path)); | |
| } | |
| for (size_t j = 0; j < nchunks && !chunks[i].path; j++) { | |
| const char *first = nwords ? words[0] : "Source"; | |
| const char *second = fallback[(i + j) % nchunks]; | |
| if (!strcmp(first, second)) continue; | |
| snprintf(path, sizeof path, "%s%s%s.tsx", workdir, first, second); | |
| if (split_path_available(chunks, i, path)) | |
| chunks[i].path = copy_text(path, strlen(path)); | |
| } | |
| if (!chunks[i].path) fail("cannot derive unique two-word name for chunk %zu", i + 1); | |
| } | |
| for (size_t i = 0; i < nchunks; i++) { | |
| write_file(chunks[i].path, chunks[i].data, chunks[i].len); | |
| const char *base = strrchr(chunks[i].path, '/'); | |
| printf( | |
| "split ./%s symbols=%zu source_bytes=%zu output_bytes=%zu\n", | |
| base ? base + 1 : chunks[i].path, | |
| chunks[i].symbols, | |
| chunks[i].source_bytes, | |
| chunks[i].len | |
| ); | |
| } | |
| for (size_t i = 0; i < analysis->nfiles; i++) { | |
| char path[4096]; | |
| snprintf(path, sizeof path, "%s%s", workdir, analysis->files[i].path); | |
| if (unlink(path)) | |
| fail("unlink %s failed: %s", path, strerror(errno)); | |
| } | |
| printf( | |
| "split_complete files=%zu removed_sources=%zu source_bytes=%zu output_bytes=%zu max_bytes=%zu\n", | |
| nchunks, | |
| analysis->nfiles, | |
| source_bytes, | |
| output_bytes, | |
| max_bytes | |
| ); | |
| for (size_t i = 0; i < nchunks; i++) { | |
| free(chunks[i].data); | |
| free(chunks[i].path); | |
| } | |
| free(groups); | |
| free(chunks); | |
| free(units); | |
| } | |
| static size_t module_prefix_end(const char *source, size_t len) { | |
| size_t count = 0; | |
| Token *tokens = lex_ts(source, len, &count); | |
| size_t i = 0; | |
| while (i < count) { | |
| if (token_is(tokens[i], "import") || | |
| (tokens[i].kind == 2 && | |
| (tokens[i].text[0] == '\'' || tokens[i].text[0] == '"'))) { | |
| i = statement_end(tokens, count, i); | |
| continue; | |
| } | |
| size_t end = tokens[i].start; | |
| free(tokens); | |
| return end; | |
| } | |
| free(tokens); | |
| return len; | |
| } | |
| static void test_split_helpers(void) { | |
| const char *plain = "scripts/foo.ts"; | |
| const char *generated = "scripts/foo.module-001.ts"; | |
| const char source[] = | |
| "import x from 'x';\n" | |
| "import type { Y } from 'y';\n" | |
| "const value: Y = x;\n"; | |
| if (module_root_stem_len(plain, strlen(plain) - 3) != strlen("scripts/foo") || | |
| module_root_stem_len(generated, strlen(generated) - 3) != strlen("scripts/foo")) | |
| fail("self-test flat module sibling stem"); | |
| if (module_prefix_end(source, sizeof source - 1) != | |
| (size_t)(strstr(source, "const value") - source)) | |
| fail("self-test physical extraction prefix"); | |
| puts("self-test: split helpers ok"); | |
| } | |
| static int token_list_has(Token *tokens, size_t count, const char *ident) { | |
| Symbol refs = {0}; | |
| int found = 0; | |
| for (size_t i = 0; i < count; i++) { | |
| if (tokens[i].kind == 1 && token_is(tokens[i], ident) && | |
| (!i || (!token_is(tokens[i - 1], ".") && !token_is(tokens[i - 1], "?."))) && | |
| (i + 1 == count || !token_is(tokens[i + 1], ":"))) | |
| found = 1; | |
| if (tokens[i].kind == 2 && tokens[i].len && tokens[i].text[0] == '`') | |
| add_template_refs(&refs, NULL, 0, tokens[i]); | |
| } | |
| found = found || symbol_has_ref(&refs, ident); | |
| for (size_t i = 0; i < refs.nrefs; i++) free(refs.refs[i]); | |
| free(refs.refs); | |
| return found; | |
| } | |
| typedef struct { | |
| char *name; | |
| int type_only; | |
| int export_kind; | |
| int imported; | |
| } MoveBinding; | |
| static int symbol_type_only(Symbol *symbol); | |
| static void add_move_binding( | |
| MoveBinding **items, | |
| size_t *count, | |
| Symbol *symbol, | |
| int export_kind | |
| ) { | |
| for (size_t i = 0; i < *count; i++) { | |
| if (strcmp((*items)[i].name, symbol->ident)) continue; | |
| (*items)[i].type_only &= symbol_type_only(symbol); | |
| if (export_kind > (*items)[i].export_kind) | |
| (*items)[i].export_kind = export_kind; | |
| return; | |
| } | |
| *items = resize(*items, (*count + 1) * sizeof **items); | |
| (*items)[*count] = (MoveBinding){ | |
| copy_text(symbol->ident, strlen(symbol->ident)), | |
| symbol_type_only(symbol), | |
| export_kind, | |
| 0 | |
| }; | |
| (*count)++; | |
| } | |
| static int symbol_type_only(Symbol *symbol) { | |
| return !strncmp(symbol->key, "type ", 5) || !strncmp(symbol->key, "interface ", 10); | |
| } | |
| static int symbol_export_kind(File *file, Symbol *symbol) { | |
| size_t count = 0; | |
| Token *tokens = lex_ts( | |
| file->source + symbol->start, | |
| symbol->end - symbol->start, | |
| &count | |
| ); | |
| int kind = count && token_is(tokens[0], "export"); | |
| if (kind && count > 1 && token_is(tokens[1], "default")) kind = 2; | |
| free(tokens); | |
| return kind; | |
| } | |
| static void append_text(char **out, size_t *len, const char *text, size_t size) { | |
| *out = resize(*out, *len + size + 1); | |
| memcpy(*out + *len, text, size); | |
| *len += size; | |
| (*out)[*len] = 0; | |
| } | |
| static void apply_extract(Analysis *analysis, ExtractRow row) { | |
| File *file = &analysis->files[row.file]; | |
| size_t begin = file->symbols[row.first].start; | |
| size_t end = file->symbols[row.last].end; | |
| size_t prefix_end = module_prefix_end(file->source, file->len); | |
| const char *workdir = git_repository_workdir(analysis->repo); | |
| if (!workdir) fail("cannot apply in a bare repository"); | |
| char *original = resize(NULL, strlen(workdir) + strlen(file->path) + 1); | |
| sprintf(original, "%s%s", workdir, file->path); | |
| const char *slash = strrchr(file->path, '/'); | |
| const char *base = slash ? slash + 1 : file->path; | |
| const char *extension = strrchr(base, '.'); | |
| if (!extension) fail("source has no extension: %s", file->path); | |
| size_t stem_len = module_root_stem_len( | |
| file->path, | |
| (size_t)(extension - file->path) | |
| ); | |
| size_t sibling_len = stem_len + 32 + strlen(extension); | |
| char *sibling_rel = resize(NULL, sibling_len); | |
| char *sibling = resize(NULL, strlen(workdir) + sibling_len + 1); | |
| unsigned sibling_number = 0; | |
| for (;;) { | |
| snprintf( | |
| sibling_rel, | |
| sibling_len, | |
| "%.*s.module-%03u%s", | |
| (int)stem_len, | |
| file->path, | |
| ++sibling_number, | |
| extension | |
| ); | |
| sprintf(sibling, "%s%s", workdir, sibling_rel); | |
| FILE *existing = fopen(sibling, "rb"); | |
| if (!existing) { | |
| if (errno != ENOENT) | |
| fail("fopen %s failed: %s", sibling, strerror(errno)); | |
| break; | |
| } | |
| fclose(existing); | |
| if (sibling_number == 999) | |
| fail("no available module sibling for %s", file->path); | |
| } | |
| size_t retained_prefix_count = 0; | |
| Token *retained_prefix = lex_ts( | |
| file->source + prefix_end, | |
| begin - prefix_end, | |
| &retained_prefix_count | |
| ); | |
| size_t suffix_count = 0; | |
| Token *suffix = lex_ts(file->source + end, file->len - end, &suffix_count); | |
| MoveBinding *bindings = NULL; | |
| size_t binding_count = 0; | |
| for (size_t s = 0; s < file->count; s++) { | |
| Symbol *symbol = &file->symbols[s]; | |
| if (symbol->start < begin || symbol->end > end) continue; | |
| if (!strcmp(symbol->ident, "default")) | |
| fail("cannot extract anonymous default declaration from %s", file->path); | |
| int export_kind = symbol_export_kind(file, symbol); | |
| if (export_kind == 2) | |
| fail("cannot extract default export from %s", file->path); | |
| add_move_binding(&bindings, &binding_count, symbol, export_kind); | |
| } | |
| if (!binding_count) fail("candidate has no movable bindings in %s", file->path); | |
| for (size_t i = 0; i < binding_count; i++) | |
| bindings[i].imported = | |
| token_list_has(retained_prefix, retained_prefix_count, bindings[i].name) || | |
| token_list_has(suffix, suffix_count, bindings[i].name); | |
| const char *sibling_base = strrchr(sibling_rel, '/'); | |
| sibling_base = sibling_base ? sibling_base + 1 : sibling_rel; | |
| char spec[1024]; | |
| snprintf( | |
| spec, | |
| sizeof spec, | |
| "./%.*s%s", | |
| (int)(strlen(sibling_base) - strlen(extension)), | |
| sibling_base, | |
| !strcmp(extension, ".mjs") ? extension : "" | |
| ); | |
| char *module = NULL; | |
| size_t module_len = 0; | |
| size_t module_exports = 0; | |
| append_text(&module, &module_len, file->source, prefix_end); | |
| append_text(&module, &module_len, file->source + begin, end - begin); | |
| for (size_t i = 0; i < binding_count; i++) { | |
| if (!bindings[i].imported || bindings[i].export_kind) continue; | |
| append_text( | |
| &module, | |
| &module_len, | |
| module_exports ? ", " : "\nexport { ", | |
| module_exports ? 2 : 10 | |
| ); | |
| if (bindings[i].type_only) | |
| append_text(&module, &module_len, "type ", 5); | |
| append_text( | |
| &module, | |
| &module_len, | |
| bindings[i].name, | |
| strlen(bindings[i].name) | |
| ); | |
| module_exports++; | |
| } | |
| if (module_exports) append_text(&module, &module_len, " };\n", 4); | |
| char *replacement = NULL; | |
| size_t replacement_len = 0; | |
| size_t imported = 0; | |
| append_text( | |
| &replacement, | |
| &replacement_len, | |
| file->source, | |
| prefix_end | |
| ); | |
| for (size_t i = 0; i < binding_count; i++) { | |
| if (!bindings[i].imported) continue; | |
| append_text( | |
| &replacement, | |
| &replacement_len, | |
| imported ? ", " : "import { ", | |
| imported ? 2 : 9 | |
| ); | |
| if (bindings[i].type_only) | |
| append_text(&replacement, &replacement_len, "type ", 5); | |
| append_text( | |
| &replacement, | |
| &replacement_len, | |
| bindings[i].name, | |
| strlen(bindings[i].name) | |
| ); | |
| imported++; | |
| } | |
| if (imported) { | |
| append_text(&replacement, &replacement_len, " } from '", 9); | |
| append_text(&replacement, &replacement_len, spec, strlen(spec)); | |
| append_text(&replacement, &replacement_len, "';\n", 3); | |
| } else { | |
| append_text(&replacement, &replacement_len, "import '", 8); | |
| append_text(&replacement, &replacement_len, spec, strlen(spec)); | |
| append_text(&replacement, &replacement_len, "';\n", 3); | |
| } | |
| size_t reexported = 0; | |
| for (size_t i = 0; i < binding_count; i++) { | |
| if (!bindings[i].export_kind) continue; | |
| append_text( | |
| &replacement, | |
| &replacement_len, | |
| reexported ? ", " : "export { ", | |
| reexported ? 2 : 9 | |
| ); | |
| if (bindings[i].type_only) | |
| append_text(&replacement, &replacement_len, "type ", 5); | |
| append_text( | |
| &replacement, | |
| &replacement_len, | |
| bindings[i].name, | |
| strlen(bindings[i].name) | |
| ); | |
| reexported++; | |
| } | |
| if (reexported) { | |
| append_text(&replacement, &replacement_len, " } from '", 9); | |
| append_text(&replacement, &replacement_len, spec, strlen(spec)); | |
| append_text(&replacement, &replacement_len, "';\n", 3); | |
| } | |
| append_text( | |
| &replacement, | |
| &replacement_len, | |
| file->source + prefix_end, | |
| begin - prefix_end | |
| ); | |
| append_text( | |
| &replacement, | |
| &replacement_len, | |
| file->source + end, | |
| file->len - end | |
| ); | |
| while (replacement_len && | |
| (replacement[replacement_len - 1] == ' ' || | |
| replacement[replacement_len - 1] == '\t' || | |
| replacement[replacement_len - 1] == '\r' || | |
| replacement[replacement_len - 1] == '\n')) | |
| replacement_len--; | |
| append_text(&replacement, &replacement_len, "\n", 1); | |
| write_file(sibling, module, module_len); | |
| write_file(original, replacement, replacement_len); | |
| printf( | |
| "\napplied split: %s -> %s bytes=%zu bindings=%zu imported=%zu reexported=%zu\n", | |
| file->path, | |
| sibling_rel, | |
| end - begin, | |
| binding_count, | |
| imported, | |
| reexported | |
| ); | |
| for (size_t i = 0; i < binding_count; i++) free(bindings[i].name); | |
| free(bindings); | |
| free(retained_prefix); | |
| free(suffix); | |
| free(module); | |
| free(replacement); | |
| free(original); | |
| free(sibling_rel); | |
| free(sibling); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment