chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
// ac.c — Aho-Corasick multi-pattern matcher with fused LZ4 decompression.
|
||||
//
|
||||
// Custom implementation (~300 lines) because no permissive-licensed C AC
|
||||
// libraries exist (ACISM and MultiFast are LGPL).
|
||||
//
|
||||
// Key design: pre-computed goto table (ACISM matrix approach) — for each
|
||||
// (state, byte) pair the next state is a direct array lookup. Zero branches
|
||||
// during scanning. Bitmask output for ≤64 patterns.
|
||||
|
||||
#include <stddef.h> // NULL
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "ac.h"
|
||||
#include "foundation/compat.h"
|
||||
#include "lz4_store.h"
|
||||
|
||||
// ─── Data structures ───────────────────────────────────────────────────────
|
||||
|
||||
// Maximum pattern count for bitmask mode.
|
||||
#define CBM_AC_MAX_BITMASK 64
|
||||
|
||||
#define CBM_AC_BYTE_RANGE 256
|
||||
#define CBM_AC_NO_STATE (-1)
|
||||
#define CBM_AC_ROOT_STATES 1
|
||||
#define CBM_AC_ALLOC_ONE 1
|
||||
#define CBM_AC_PATTERN_BIT(p) (1ULL << (p))
|
||||
#define CBM_AC_CLEAR_LOW_BIT(b) ((b) & ((b) - 1ULL))
|
||||
|
||||
// Decompression buffer alignment mask (round up to 64KB chunks).
|
||||
#define DECOMP_BUF_ALIGN_MASK 0xFFFF
|
||||
|
||||
struct CBMAutomaton {
|
||||
int num_states;
|
||||
int num_patterns;
|
||||
int alpha_size; // 256 for raw byte, or smaller for mapped alphabet
|
||||
uint8_t alpha_map[CBM_AC_BYTE_RANGE]; // byte → mapped index (identity if alpha_size==256)
|
||||
int *go_table; // [num_states * alpha_size] — pre-computed transitions
|
||||
uint64_t *output; // [num_states] — bitmask of matching pattern IDs
|
||||
int *output_list; // [num_states] — linked list: pattern ID or -1
|
||||
int *output_next; // [num_states] — next pointer for output_list chain
|
||||
};
|
||||
|
||||
// ─── Build ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// Queue for BFS during failure function computation.
|
||||
typedef struct {
|
||||
int *data;
|
||||
int head, tail, cap;
|
||||
} Queue;
|
||||
|
||||
static void queue_init(Queue *q, int cap) {
|
||||
q->data = (int *)malloc(cap * sizeof(int));
|
||||
q->head = q->tail = 0;
|
||||
q->cap = cap;
|
||||
}
|
||||
static void queue_push(Queue *q, int v) {
|
||||
q->data[q->tail++] = v;
|
||||
}
|
||||
static int queue_pop(Queue *q) {
|
||||
return q->data[q->head++];
|
||||
}
|
||||
static int queue_empty(Queue *q) {
|
||||
return q->head >= q->tail;
|
||||
}
|
||||
static void queue_free(Queue *q) {
|
||||
free(q->data);
|
||||
}
|
||||
|
||||
// Phase 1: Build trie (goto function) from patterns. Returns state count.
|
||||
static int ac_build_trie(CBMAutomaton *ac, const char **patterns, const int *lengths, int count) {
|
||||
int alpha_size = ac->alpha_size;
|
||||
int num_states = CBM_AC_ROOT_STATES; // state 0 = root
|
||||
|
||||
for (int p = 0; p < count; p++) {
|
||||
int state = 0;
|
||||
for (int j = 0; j < lengths[p]; j++) {
|
||||
int c = ac->alpha_map[(unsigned char)patterns[p][j]];
|
||||
int idx = (state * alpha_size) + c;
|
||||
if (ac->go_table[idx] == CBM_AC_NO_STATE) {
|
||||
ac->go_table[idx] = num_states++;
|
||||
}
|
||||
state = ac->go_table[idx];
|
||||
}
|
||||
if (p < CBM_AC_MAX_BITMASK) {
|
||||
ac->output[state] |= CBM_AC_PATTERN_BIT(p);
|
||||
}
|
||||
ac->output_list[state] = p;
|
||||
}
|
||||
|
||||
// Root self-loops for unmatched bytes.
|
||||
for (int c = 0; c < alpha_size; c++) {
|
||||
if (ac->go_table[c] == CBM_AC_NO_STATE) {
|
||||
ac->go_table[c] = 0;
|
||||
}
|
||||
}
|
||||
return num_states;
|
||||
}
|
||||
|
||||
// Phase 2: Build failure function via BFS + compute full goto table.
|
||||
static void ac_build_failure(CBMAutomaton *ac, int num_states) {
|
||||
int alpha_size = ac->alpha_size;
|
||||
int *fail = (int *)calloc(num_states, sizeof(int));
|
||||
|
||||
Queue q;
|
||||
queue_init(&q, num_states);
|
||||
|
||||
for (int c = 0; c < alpha_size; c++) {
|
||||
int s = ac->go_table[c];
|
||||
if (s != 0) {
|
||||
fail[s] = 0;
|
||||
queue_push(&q, s);
|
||||
}
|
||||
}
|
||||
|
||||
while (!queue_empty(&q)) {
|
||||
int r = queue_pop(&q);
|
||||
for (int c = 0; c < alpha_size; c++) {
|
||||
int idx = (r * alpha_size) + c;
|
||||
int s = ac->go_table[idx];
|
||||
if (s != CBM_AC_NO_STATE) {
|
||||
fail[s] = ac->go_table[(fail[r] * alpha_size) + c];
|
||||
ac->output[s] |= ac->output[fail[s]];
|
||||
if (ac->output_next[s] == CBM_AC_NO_STATE &&
|
||||
ac->output_list[fail[s]] != CBM_AC_NO_STATE) {
|
||||
ac->output_next[s] = fail[s];
|
||||
}
|
||||
queue_push(&q, s);
|
||||
} else {
|
||||
ac->go_table[idx] = ac->go_table[(fail[r] * alpha_size) + c];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(fail);
|
||||
queue_free(&q);
|
||||
}
|
||||
|
||||
// Shrink allocations to exact state count.
|
||||
static void ac_shrink_tables(CBMAutomaton *ac, int num_states, int max_states) {
|
||||
if (num_states >= max_states) {
|
||||
return;
|
||||
}
|
||||
int alpha_size = ac->alpha_size;
|
||||
void *tmp;
|
||||
tmp = realloc(ac->go_table, (size_t)num_states * alpha_size * sizeof(int));
|
||||
if (tmp) {
|
||||
ac->go_table = (int *)tmp;
|
||||
}
|
||||
tmp = realloc(ac->output, (size_t)num_states * sizeof(uint64_t));
|
||||
if (tmp) {
|
||||
ac->output = (uint64_t *)tmp;
|
||||
}
|
||||
tmp = realloc(ac->output_list, (size_t)num_states * sizeof(int));
|
||||
if (tmp) {
|
||||
ac->output_list = (int *)tmp;
|
||||
}
|
||||
tmp = realloc(ac->output_next, (size_t)num_states * sizeof(int));
|
||||
if (tmp) {
|
||||
ac->output_next = (int *)tmp;
|
||||
}
|
||||
}
|
||||
|
||||
// cbm_ac_build constructs an Aho-Corasick automaton from a set of patterns.
|
||||
//
|
||||
// Parameters:
|
||||
// patterns — array of pattern pointers (not necessarily NUL-terminated)
|
||||
// lengths — length of each pattern
|
||||
// count — number of patterns (max 64 for bitmask mode)
|
||||
// alpha_map — byte→index mapping (NULL = identity/256). For compact alphabets,
|
||||
// map relevant chars to 1..N and everything else to 0.
|
||||
// alpha_size — alphabet size (256 if alpha_map is NULL)
|
||||
//
|
||||
// Returns a heap-allocated automaton. Caller must call cbm_ac_free().
|
||||
CBMAutomaton *cbm_ac_build(const char **patterns, const int *lengths, int count,
|
||||
const uint8_t *alpha_map, int alpha_size) {
|
||||
if (count <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
if (alpha_size <= 0) {
|
||||
alpha_size = CBM_AC_BYTE_RANGE;
|
||||
}
|
||||
|
||||
int max_states = CBM_AC_ROOT_STATES;
|
||||
for (int i = 0; i < count; i++) {
|
||||
max_states += lengths[i];
|
||||
}
|
||||
|
||||
CBMAutomaton *ac = (CBMAutomaton *)calloc(CBM_AC_ALLOC_ONE, sizeof(CBMAutomaton));
|
||||
ac->alpha_size = alpha_size;
|
||||
ac->num_patterns = count;
|
||||
|
||||
if (alpha_map) {
|
||||
memcpy(ac->alpha_map, alpha_map, CBM_AC_BYTE_RANGE);
|
||||
} else {
|
||||
for (int i = 0; i < CBM_AC_BYTE_RANGE; i++) {
|
||||
ac->alpha_map[i] = (uint8_t)i;
|
||||
}
|
||||
}
|
||||
|
||||
ac->go_table = (int *)malloc((size_t)max_states * alpha_size * sizeof(int));
|
||||
memset(ac->go_table, CBM_AC_NO_STATE, (size_t)max_states * alpha_size * sizeof(int));
|
||||
ac->output = (uint64_t *)calloc(max_states, sizeof(uint64_t));
|
||||
ac->output_list = (int *)malloc(max_states * sizeof(int));
|
||||
ac->output_next = (int *)malloc(max_states * sizeof(int));
|
||||
for (int i = 0; i < max_states; i++) {
|
||||
ac->output_list[i] = CBM_AC_NO_STATE;
|
||||
ac->output_next[i] = CBM_AC_NO_STATE;
|
||||
}
|
||||
|
||||
int num_states = ac_build_trie(ac, patterns, lengths, count);
|
||||
ac_build_failure(ac, num_states);
|
||||
ac->num_states = num_states;
|
||||
ac_shrink_tables(ac, num_states, max_states);
|
||||
|
||||
return ac;
|
||||
}
|
||||
|
||||
// cbm_ac_free releases all memory for an automaton.
|
||||
void cbm_ac_free(CBMAutomaton *ac) {
|
||||
if (!ac) {
|
||||
return;
|
||||
}
|
||||
free(ac->go_table);
|
||||
free(ac->output);
|
||||
free(ac->output_list);
|
||||
free(ac->output_next);
|
||||
free(ac);
|
||||
}
|
||||
|
||||
// ─── Scan functions ────────────────────────────────────────────────────────
|
||||
|
||||
// cbm_ac_scan_bitmask scans text through the automaton and returns a bitmask
|
||||
// of all matched pattern IDs (patterns 0..63).
|
||||
uint64_t cbm_ac_scan_bitmask(const CBMAutomaton *ac, const char *text, int text_len) {
|
||||
uint64_t result = 0;
|
||||
int state = 0;
|
||||
const int alpha_size = ac->alpha_size;
|
||||
const int *go_table = ac->go_table;
|
||||
const uint64_t *output = ac->output;
|
||||
|
||||
for (int i = 0; i < text_len; i++) {
|
||||
int c = ac->alpha_map[(unsigned char)text[i]];
|
||||
state = go_table[(state * alpha_size) + c];
|
||||
result |= output[state];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── Fused LZ4 + AC scan ──────────────────────────────────────────────────
|
||||
|
||||
// Thread-local reusable decompression buffer to avoid repeated malloc/free.
|
||||
// Each goroutine gets its own OS thread (via CGo), so CBM_TLS is safe.
|
||||
static CBM_TLS char *tls_decomp_buf = NULL;
|
||||
static CBM_TLS int tls_decomp_cap = 0;
|
||||
|
||||
static char *get_decomp_buf(int needed) {
|
||||
if (needed > tls_decomp_cap) {
|
||||
free(tls_decomp_buf);
|
||||
// Round up to 64KB chunks for reuse.
|
||||
int cap = (needed + DECOMP_BUF_ALIGN_MASK) & ~DECOMP_BUF_ALIGN_MASK;
|
||||
tls_decomp_buf = (cap > 0) ? (char *)malloc((size_t)cap) : NULL;
|
||||
tls_decomp_cap = cap;
|
||||
}
|
||||
return tls_decomp_buf;
|
||||
}
|
||||
|
||||
// cbm_ac_scan_lz4_bitmask decompresses LZ4 data into a thread-local buffer
|
||||
// and scans it through the AC automaton. Returns bitmask of matched patterns.
|
||||
// Zero Go heap allocation — the decompression buffer lives in C.
|
||||
uint64_t cbm_ac_scan_lz4_bitmask(const CBMAutomaton *ac, const char *compressed, int compressed_len,
|
||||
int original_len) {
|
||||
if (!ac || !compressed || compressed_len <= 0 || original_len <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *buf = get_decomp_buf(original_len);
|
||||
if (!buf) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int decompressed = cbm_lz4_decompress(compressed, compressed_len, buf, original_len);
|
||||
if (decompressed < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return cbm_ac_scan_bitmask(ac, buf, decompressed);
|
||||
}
|
||||
|
||||
// ─── Batch LZ4 + AC scan ───────────────────────────────────────────────────
|
||||
|
||||
// CBMLz4Entry and CBMLz4Match defined in ac.h.
|
||||
|
||||
// cbm_ac_scan_lz4_batch decompresses and scans multiple files in one call.
|
||||
// Returns the number of matching files written to out_matches.
|
||||
// Uses a single reusable decompression buffer across all files.
|
||||
int cbm_ac_scan_lz4_batch(const CBMAutomaton *ac, const CBMLz4Entry *entries, int num_entries,
|
||||
CBMLz4Match *out_matches, int max_matches) {
|
||||
if (!ac || !entries || num_entries <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Allocate decompression buffer sized to the largest file.
|
||||
int max_orig = 0;
|
||||
for (int i = 0; i < num_entries; i++) {
|
||||
if (entries[i].original_len > max_orig) {
|
||||
max_orig = entries[i].original_len;
|
||||
}
|
||||
}
|
||||
char *buf = get_decomp_buf(max_orig);
|
||||
if (!buf) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int alpha_size = ac->alpha_size;
|
||||
const int *go_table = ac->go_table;
|
||||
const uint64_t *output = ac->output;
|
||||
int total = 0;
|
||||
|
||||
for (int i = 0; i < num_entries && total < max_matches; i++) {
|
||||
if (!entries[i].data || entries[i].compressed_len <= 0 || entries[i].original_len <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int decompressed = cbm_lz4_decompress(entries[i].data, entries[i].compressed_len, buf,
|
||||
entries[i].original_len);
|
||||
if (decompressed <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Inline AC scan for speed (avoid function call overhead per file).
|
||||
uint64_t result = 0;
|
||||
int state = 0;
|
||||
for (int j = 0; j < decompressed; j++) {
|
||||
int c = ac->alpha_map[(unsigned char)buf[j]];
|
||||
state = go_table[(state * alpha_size) + c];
|
||||
result |= output[state];
|
||||
}
|
||||
|
||||
if (result != 0) {
|
||||
out_matches[total].file_index = i;
|
||||
out_matches[total].bitmask = result;
|
||||
total++;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
// ─── Batch scan for configlinker ───────────────────────────────────────────
|
||||
|
||||
// CBMMatchResult defined in ac.h.
|
||||
|
||||
// cbm_ac_scan_batch scans multiple NUL-separated names through the automaton.
|
||||
// For each name, reports all unique matched pattern IDs.
|
||||
// Returns total number of matches written to out_matches.
|
||||
//
|
||||
// Parameters:
|
||||
// ac — automaton
|
||||
// names_buf — concatenated names separated by NUL bytes
|
||||
// name_offsets — start offset of each name in names_buf
|
||||
// name_lengths — length of each name
|
||||
// num_names — number of names
|
||||
// out_matches — output buffer for (name_index, pattern_id) pairs
|
||||
// max_matches — capacity of out_matches
|
||||
int cbm_ac_scan_batch(const CBMAutomaton *ac, const char *names_buf, const int *name_offsets,
|
||||
const int *name_lengths, int num_names, CBMMatchResult *out_matches,
|
||||
int max_matches) {
|
||||
int total = 0;
|
||||
const int alpha_size = ac->alpha_size;
|
||||
const int *go_table = ac->go_table;
|
||||
|
||||
for (int n = 0; n < num_names && total < max_matches; n++) {
|
||||
const char *text = names_buf + name_offsets[n];
|
||||
int text_len = name_lengths[n];
|
||||
int state = 0;
|
||||
|
||||
// Track which patterns matched for this name (deduplicate).
|
||||
uint64_t seen = 0;
|
||||
|
||||
for (int i = 0; i < text_len; i++) {
|
||||
int c = ac->alpha_map[(unsigned char)text[i]];
|
||||
state = go_table[(state * alpha_size) + c];
|
||||
|
||||
// Walk output chain for >64 patterns.
|
||||
int s = state;
|
||||
while (s > 0 && total < max_matches) {
|
||||
// Bitmask fast path for first 64 patterns.
|
||||
uint64_t bits = ac->output[s] & ~seen;
|
||||
while (bits && total < max_matches) {
|
||||
int pid = __builtin_ctzll(bits);
|
||||
out_matches[total].name_index = n;
|
||||
out_matches[total].pattern_id = pid;
|
||||
total++;
|
||||
seen |= CBM_AC_PATTERN_BIT(pid);
|
||||
bits = CBM_AC_CLEAR_LOW_BIT(bits);
|
||||
}
|
||||
|
||||
// Follow output_next for patterns beyond bitmask range.
|
||||
int next_state = ac->output_next[s];
|
||||
if (next_state == CBM_AC_NO_STATE || next_state == s) {
|
||||
break;
|
||||
}
|
||||
s = next_state;
|
||||
}
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// ─── Info ──────────────────────────────────────────────────────────────────
|
||||
|
||||
int cbm_ac_num_states(const CBMAutomaton *ac) {
|
||||
return ac ? ac->num_states : 0;
|
||||
}
|
||||
|
||||
int cbm_ac_num_patterns(const CBMAutomaton *ac) {
|
||||
return ac ? ac->num_patterns : 0;
|
||||
}
|
||||
|
||||
// cbm_ac_table_bytes returns the approximate memory used by the goto table.
|
||||
int cbm_ac_table_bytes(const CBMAutomaton *ac) {
|
||||
if (!ac) {
|
||||
return 0;
|
||||
}
|
||||
return ac->num_states * ac->alpha_size * (int)sizeof(int);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef CBM_AC_H
|
||||
#define CBM_AC_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Forward declaration — full struct in ac.c
|
||||
typedef struct CBMAutomaton CBMAutomaton;
|
||||
|
||||
// Input for batch LZ4 scanning.
|
||||
typedef struct {
|
||||
const char *data;
|
||||
int compressed_len;
|
||||
int original_len;
|
||||
} CBMLz4Entry;
|
||||
|
||||
// Output for batch LZ4 scanning.
|
||||
typedef struct {
|
||||
int file_index;
|
||||
uint64_t bitmask;
|
||||
} CBMLz4Match;
|
||||
|
||||
// Output for batch name scanning.
|
||||
typedef struct {
|
||||
int name_index;
|
||||
int pattern_id;
|
||||
} CBMMatchResult;
|
||||
|
||||
// Build an Aho-Corasick automaton from patterns.
|
||||
CBMAutomaton *cbm_ac_build(const char **patterns, const int *lengths, int count,
|
||||
const uint8_t *alpha_map, int alpha_size);
|
||||
void cbm_ac_free(CBMAutomaton *ac);
|
||||
|
||||
// Single-text scanning (returns bitmask of matched pattern IDs).
|
||||
uint64_t cbm_ac_scan_bitmask(const CBMAutomaton *ac, const char *text, int text_len);
|
||||
|
||||
// LZ4-compressed scanning.
|
||||
uint64_t cbm_ac_scan_lz4_bitmask(const CBMAutomaton *ac, const char *compressed, int compressed_len,
|
||||
int original_len);
|
||||
int cbm_ac_scan_lz4_batch(const CBMAutomaton *ac, const CBMLz4Entry *entries, int num_entries,
|
||||
CBMLz4Match *out_matches, int max_matches);
|
||||
|
||||
// Batch name scanning.
|
||||
int cbm_ac_scan_batch(const CBMAutomaton *ac, const char *names_buf, const int *name_offsets,
|
||||
const int *name_lengths, int num_names, CBMMatchResult *out_matches,
|
||||
int max_matches);
|
||||
|
||||
// Introspection.
|
||||
int cbm_ac_num_states(const CBMAutomaton *ac);
|
||||
int cbm_ac_num_patterns(const CBMAutomaton *ac);
|
||||
int cbm_ac_table_bytes(const CBMAutomaton *ac);
|
||||
|
||||
#endif // CBM_AC_H
|
||||
@@ -0,0 +1,107 @@
|
||||
#include "arena.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
|
||||
void cbm_arena_init(CBMArena *a) {
|
||||
memset(a, 0, sizeof(*a));
|
||||
a->block_size = CBM_ARENA_DEFAULT_BLOCK_SIZE;
|
||||
a->blocks[0] = (char *)malloc(a->block_size);
|
||||
if (a->blocks[0]) {
|
||||
a->nblocks = SKIP_ONE;
|
||||
}
|
||||
}
|
||||
|
||||
static int arena_grow(CBMArena *a, size_t min_size) {
|
||||
if (a->nblocks >= CBM_ARENA_MAX_BLOCKS) {
|
||||
return 0;
|
||||
}
|
||||
size_t new_size = a->block_size * PAIR_LEN;
|
||||
if (new_size < min_size) {
|
||||
new_size = min_size;
|
||||
}
|
||||
char *block = (char *)malloc(new_size);
|
||||
if (!block) {
|
||||
return 0;
|
||||
}
|
||||
a->blocks[a->nblocks] = block;
|
||||
a->nblocks++;
|
||||
a->block_size = new_size;
|
||||
a->used = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void *cbm_arena_alloc(CBMArena *a, size_t n) {
|
||||
if (!a || n == 0) {
|
||||
return NULL;
|
||||
}
|
||||
// 8-byte alignment
|
||||
n = (n + 7) & ~(size_t)7;
|
||||
|
||||
if (a->nblocks == 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (a->used + n > a->block_size) {
|
||||
if (!arena_grow(a, n)) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
char *ptr = a->blocks[a->nblocks - SKIP_ONE] + a->used;
|
||||
a->used += n;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
char *cbm_arena_strdup(CBMArena *a, const char *s) {
|
||||
if (!s)
|
||||
return NULL;
|
||||
size_t len = strlen(s);
|
||||
char *dst = (char *)cbm_arena_alloc(a, len + SKIP_ONE);
|
||||
if (dst) {
|
||||
memcpy(dst, s, len + SKIP_ONE);
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
char *cbm_arena_strndup(CBMArena *a, const char *s, size_t len) {
|
||||
if (!s)
|
||||
return NULL;
|
||||
char *dst = (char *)cbm_arena_alloc(a, len + SKIP_ONE);
|
||||
if (dst) {
|
||||
memcpy(dst, s, len);
|
||||
dst[len] = '\0';
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
char *cbm_arena_sprintf(CBMArena *a, const char *fmt, ...) {
|
||||
// First pass: compute length
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
int needed = vsnprintf(NULL, 0, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (needed < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *dst = (char *)cbm_arena_alloc(a, (size_t)needed + SKIP_ONE);
|
||||
if (!dst) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
va_start(args, fmt);
|
||||
vsnprintf(dst, (size_t)needed + SKIP_ONE, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
void cbm_arena_destroy(CBMArena *a) {
|
||||
for (int i = 0; i < a->nblocks; i++) {
|
||||
free(a->blocks[i]);
|
||||
}
|
||||
memset(a, 0, sizeof(*a));
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef CBM_ARENA_H
|
||||
#define CBM_ARENA_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
// CBMArena is a simple bump allocator that allocates from fixed-size blocks.
|
||||
// All memory is freed at once via cbm_arena_destroy(). Individual frees are not
|
||||
// supported — this is by design for per-file extraction where all data has the
|
||||
// same lifetime.
|
||||
#define CBM_ARENA_MAX_BLOCKS 256
|
||||
#define CBM_ARENA_DEFAULT_BLOCK_SIZE (64 * 1024) // 64KB initial
|
||||
|
||||
typedef struct {
|
||||
char *blocks[CBM_ARENA_MAX_BLOCKS];
|
||||
size_t block_sizes[CBM_ARENA_MAX_BLOCKS]; // per-block sizes (for stats)
|
||||
int nblocks;
|
||||
size_t block_size;
|
||||
size_t used; // bytes used in current block
|
||||
size_t total_alloc; // cumulative bytes allocated (for stats)
|
||||
} CBMArena;
|
||||
|
||||
// Initialize an arena with the default block size.
|
||||
void cbm_arena_init(CBMArena *a);
|
||||
|
||||
// Allocate n bytes from the arena. Returns NULL on OOM or block exhaustion.
|
||||
// All returned pointers are 8-byte aligned.
|
||||
void *cbm_arena_alloc(CBMArena *a, size_t n);
|
||||
|
||||
// Duplicate a string into arena memory. Returns arena-owned copy.
|
||||
char *cbm_arena_strdup(CBMArena *a, const char *s);
|
||||
|
||||
// Duplicate a string of known length into arena memory. NUL-terminates.
|
||||
char *cbm_arena_strndup(CBMArena *a, const char *s, size_t len);
|
||||
|
||||
// sprintf into arena memory. Returns arena-owned string.
|
||||
char *cbm_arena_sprintf(CBMArena *a, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
|
||||
// Free all blocks. Arena is invalid after this call.
|
||||
void cbm_arena_destroy(CBMArena *a);
|
||||
|
||||
#endif // CBM_ARENA_H
|
||||
+1347
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,650 @@
|
||||
#ifndef CBM_H
|
||||
#define CBM_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "arena.h"
|
||||
#include "tree_sitter/api.h"
|
||||
|
||||
// Language enum mirrors lang.Language in Go.
|
||||
// Order must match lang_specs.c tables.
|
||||
typedef enum {
|
||||
CBM_LANG_GO = 0,
|
||||
CBM_LANG_PYTHON,
|
||||
CBM_LANG_JAVASCRIPT,
|
||||
CBM_LANG_TYPESCRIPT,
|
||||
CBM_LANG_TSX,
|
||||
CBM_LANG_RUST,
|
||||
CBM_LANG_JAVA,
|
||||
CBM_LANG_CPP,
|
||||
CBM_LANG_CSHARP,
|
||||
CBM_LANG_PHP,
|
||||
CBM_LANG_LUA,
|
||||
CBM_LANG_SCALA,
|
||||
CBM_LANG_KOTLIN,
|
||||
CBM_LANG_RUBY,
|
||||
CBM_LANG_C,
|
||||
CBM_LANG_BASH,
|
||||
CBM_LANG_ZIG,
|
||||
CBM_LANG_ELIXIR,
|
||||
CBM_LANG_HASKELL,
|
||||
CBM_LANG_OCAML,
|
||||
CBM_LANG_OBJC,
|
||||
CBM_LANG_SWIFT,
|
||||
CBM_LANG_DART,
|
||||
CBM_LANG_PERL,
|
||||
CBM_LANG_GROOVY,
|
||||
CBM_LANG_ERLANG,
|
||||
CBM_LANG_R,
|
||||
CBM_LANG_HTML,
|
||||
CBM_LANG_CSS,
|
||||
CBM_LANG_SCSS,
|
||||
CBM_LANG_YAML,
|
||||
CBM_LANG_TOML,
|
||||
CBM_LANG_HCL,
|
||||
CBM_LANG_SQL,
|
||||
CBM_LANG_DOCKERFILE,
|
||||
// New languages (v0.5 expansion)
|
||||
CBM_LANG_CLOJURE,
|
||||
CBM_LANG_FSHARP,
|
||||
CBM_LANG_JULIA,
|
||||
CBM_LANG_VIMSCRIPT,
|
||||
CBM_LANG_NIX,
|
||||
CBM_LANG_COMMONLISP,
|
||||
CBM_LANG_ELM,
|
||||
CBM_LANG_FORTRAN,
|
||||
CBM_LANG_CUDA,
|
||||
CBM_LANG_COBOL,
|
||||
CBM_LANG_VERILOG,
|
||||
CBM_LANG_EMACSLISP,
|
||||
CBM_LANG_JSON,
|
||||
CBM_LANG_XML,
|
||||
CBM_LANG_MARKDOWN,
|
||||
CBM_LANG_MAKEFILE,
|
||||
CBM_LANG_CMAKE,
|
||||
CBM_LANG_PROTOBUF,
|
||||
CBM_LANG_GRAPHQL,
|
||||
CBM_LANG_VUE,
|
||||
CBM_LANG_SVELTE,
|
||||
CBM_LANG_MESON,
|
||||
CBM_LANG_GLSL,
|
||||
CBM_LANG_INI,
|
||||
// Scientific/math languages
|
||||
CBM_LANG_MATLAB,
|
||||
CBM_LANG_LEAN,
|
||||
CBM_LANG_FORM,
|
||||
CBM_LANG_MAGMA,
|
||||
CBM_LANG_WOLFRAM,
|
||||
CBM_LANG_SOLIDITY,
|
||||
CBM_LANG_TYPST,
|
||||
CBM_LANG_GDSCRIPT,
|
||||
CBM_LANG_GLEAM,
|
||||
CBM_LANG_POWERSHELL,
|
||||
CBM_LANG_PASCAL,
|
||||
CBM_LANG_DLANG,
|
||||
CBM_LANG_NIM,
|
||||
CBM_LANG_SCHEME,
|
||||
CBM_LANG_FENNEL,
|
||||
CBM_LANG_FISH,
|
||||
CBM_LANG_AWK,
|
||||
CBM_LANG_ZSH,
|
||||
CBM_LANG_TCL,
|
||||
CBM_LANG_ADA,
|
||||
CBM_LANG_AGDA,
|
||||
CBM_LANG_RACKET,
|
||||
CBM_LANG_ODIN,
|
||||
CBM_LANG_RESCRIPT,
|
||||
CBM_LANG_PURESCRIPT,
|
||||
CBM_LANG_NICKEL,
|
||||
CBM_LANG_CRYSTAL,
|
||||
CBM_LANG_TEAL,
|
||||
CBM_LANG_HARE,
|
||||
CBM_LANG_PONY,
|
||||
CBM_LANG_LUAU,
|
||||
CBM_LANG_JANET,
|
||||
CBM_LANG_SWAY,
|
||||
CBM_LANG_NASM,
|
||||
CBM_LANG_ASSEMBLY,
|
||||
CBM_LANG_ASTRO,
|
||||
CBM_LANG_BLADE,
|
||||
CBM_LANG_JUST,
|
||||
CBM_LANG_GOTEMPLATE,
|
||||
CBM_LANG_TEMPL,
|
||||
CBM_LANG_LIQUID,
|
||||
CBM_LANG_JINJA2,
|
||||
CBM_LANG_PRISMA,
|
||||
CBM_LANG_HYPRLANG,
|
||||
CBM_LANG_DOTENV,
|
||||
CBM_LANG_DIFF,
|
||||
CBM_LANG_WGSL,
|
||||
CBM_LANG_KDL,
|
||||
CBM_LANG_JSON5,
|
||||
CBM_LANG_JSONNET,
|
||||
CBM_LANG_RON,
|
||||
CBM_LANG_THRIFT,
|
||||
CBM_LANG_CAPNP,
|
||||
CBM_LANG_PROPERTIES,
|
||||
CBM_LANG_SSHCONFIG,
|
||||
CBM_LANG_BIBTEX,
|
||||
CBM_LANG_STARLARK,
|
||||
CBM_LANG_BICEP,
|
||||
CBM_LANG_CSV,
|
||||
CBM_LANG_REQUIREMENTS,
|
||||
CBM_LANG_HLSL,
|
||||
CBM_LANG_VHDL,
|
||||
CBM_LANG_SYSTEMVERILOG,
|
||||
CBM_LANG_DEVICETREE,
|
||||
CBM_LANG_LINKERSCRIPT,
|
||||
CBM_LANG_GN,
|
||||
CBM_LANG_KCONFIG,
|
||||
CBM_LANG_BITBAKE,
|
||||
CBM_LANG_SMALI,
|
||||
CBM_LANG_TABLEGEN,
|
||||
CBM_LANG_ISPC,
|
||||
CBM_LANG_CAIRO,
|
||||
CBM_LANG_MOVE,
|
||||
CBM_LANG_SQUIRREL,
|
||||
CBM_LANG_FUNC,
|
||||
CBM_LANG_REGEX,
|
||||
CBM_LANG_JSDOC,
|
||||
CBM_LANG_RST,
|
||||
CBM_LANG_BEANCOUNT,
|
||||
CBM_LANG_MERMAID,
|
||||
CBM_LANG_PUPPET,
|
||||
CBM_LANG_PO,
|
||||
CBM_LANG_GITATTRIBUTES,
|
||||
CBM_LANG_GITIGNORE,
|
||||
CBM_LANG_SLANG,
|
||||
CBM_LANG_LLVM_IR,
|
||||
CBM_LANG_SMITHY,
|
||||
CBM_LANG_WIT,
|
||||
CBM_LANG_TLAPLUS,
|
||||
CBM_LANG_PKL,
|
||||
CBM_LANG_GOMOD,
|
||||
CBM_LANG_APEX,
|
||||
CBM_LANG_SOQL,
|
||||
CBM_LANG_SOSL,
|
||||
CBM_LANG_KUSTOMIZE, // kustomization.yaml — Kubernetes overlay tool
|
||||
CBM_LANG_K8S, // Generic Kubernetes manifest (apiVersion: detected)
|
||||
CBM_LANG_PINE, // Pine Script (TradingView indicator / strategy language)
|
||||
CBM_LANG_QML, // Qt QML (Qt Modeling Language — declarative UI + embedded JS)
|
||||
CBM_LANG_CFSCRIPT, // CFML script dialect (.cfc components — Lucee/ColdFusion)
|
||||
CBM_LANG_CFML, // CFML tag dialect (.cfm templates — Lucee/ColdFusion)
|
||||
CBM_LANG_MOJO, // Mojo
|
||||
CBM_LANG_COUNT
|
||||
} CBMLanguage;
|
||||
|
||||
// --- Extraction result structs ---
|
||||
|
||||
typedef struct {
|
||||
const char *name; // short name
|
||||
const char *qualified_name; // project.path.name
|
||||
const char *label; // "Function", "Method", "Class", "Variable", "Module"
|
||||
const char *file_path; // relative path
|
||||
uint32_t start_line;
|
||||
uint32_t end_line;
|
||||
const char *signature; // parameter text (NULL if none)
|
||||
const char *return_type; // return type text (NULL if none)
|
||||
const char *receiver; // Go method receiver (NULL if none)
|
||||
const char *docstring; // leading doc comment (NULL if none)
|
||||
const char *parent_class; // enclosing class QN for methods (NULL if none)
|
||||
const char **decorators; // NULL-terminated array (NULL if none)
|
||||
const char **base_classes; // NULL-terminated array (NULL if none)
|
||||
const char **param_names; // NULL-terminated array (NULL if none)
|
||||
const char **param_types; // NULL-terminated array (NULL if none)
|
||||
const char **return_types; // NULL-terminated array (NULL if none)
|
||||
const char *route_path; // HTTP route path from decorator (e.g., "/api/users") or NULL
|
||||
const char *route_method; // HTTP method from decorator (e.g., "POST") or NULL
|
||||
int complexity; // cyclomatic complexity
|
||||
int cognitive; // cognitive complexity (nesting-weighted)
|
||||
int loop_count; // number of loop constructs in the body
|
||||
int loop_depth; // max nested-loop depth (bottleneck proxy)
|
||||
bool is_recursive; // body contains a direct self-call (seed for "recursive")
|
||||
int param_count; // number of parameters (large = complexity smell)
|
||||
int max_access_depth; // deepest chained member/subscript access (a.b.c.d)
|
||||
int linear_scan_in_loop; // count of linear-scan calls (find/contains/indexOf) inside loops
|
||||
int alloc_in_loop; // count of allocation/append calls inside loops
|
||||
bool recursion_in_loop; // a self-call occurs inside a loop body
|
||||
bool unguarded_recursion; // recursive with no self-call guarded by a conditional
|
||||
int lines; // body line count
|
||||
uint32_t *fingerprint; // MinHash fingerprint (arena-allocated, K values) or NULL
|
||||
int fingerprint_k; // number of hash values (CBM_MINHASH_K or 0)
|
||||
bool is_exported;
|
||||
bool is_abstract;
|
||||
bool is_test;
|
||||
bool is_entry_point;
|
||||
const char *structural_profile; // AST structural profile (arena-allocated) or NULL
|
||||
const char *body_tokens; // space-separated raw identifier tokens from body (arena) or NULL
|
||||
} CBMDefinition;
|
||||
|
||||
/* Argument captured from a call expression */
|
||||
typedef struct {
|
||||
const char *expr; // raw expression text ("payload.info", "MY_URL", "'hello'")
|
||||
const char *value; // resolved string value or NULL (constant propagation)
|
||||
const char *keyword; // keyword name if keyword arg ("url", "topic_id"), NULL if positional
|
||||
int index; // positional index (0-based)
|
||||
} CBMCallArg;
|
||||
|
||||
#define CBM_MAX_CALL_ARGS 8
|
||||
|
||||
typedef struct {
|
||||
const char *callee_name; // raw callee text ("pkg.Func", "foo")
|
||||
const char *enclosing_func_qn; // QN of enclosing function (or module QN)
|
||||
const char *first_string_arg; // first string literal argument (URL, topic, key) or NULL
|
||||
const char *second_arg_name; // second argument identifier (handler ref) or NULL
|
||||
CBMCallArg args[CBM_MAX_CALL_ARGS]; // first N arguments with expressions
|
||||
int arg_count; // number of captured arguments
|
||||
int loop_depth; // enclosing loop nesting at the call site
|
||||
int branch_depth; // enclosing branch nesting at the call site
|
||||
int start_line; // 1-based source line of the call (for def range-match)
|
||||
bool is_method; // method/member call with a non-self receiver. Perl:
|
||||
// arrow/method call ($obj->m). TS/JS/TSX: member call
|
||||
// x.foo() whose receiver is not this/super. Default false.
|
||||
} CBMCall;
|
||||
|
||||
typedef struct {
|
||||
const char *local_name; // local alias or name
|
||||
const char *module_path; // resolved module path / QN
|
||||
} CBMImport;
|
||||
|
||||
typedef struct {
|
||||
const char *ref_name; // referenced identifier
|
||||
const char *enclosing_func_qn; // QN of enclosing function (or module QN)
|
||||
} CBMUsage;
|
||||
|
||||
typedef struct {
|
||||
const char *exception_name; // exception class/type name
|
||||
const char *enclosing_func_qn; // QN of enclosing function
|
||||
} CBMThrow;
|
||||
|
||||
typedef struct {
|
||||
const char *var_name; // variable name
|
||||
const char *enclosing_func_qn; // QN of enclosing function
|
||||
bool is_write; // true = write, false = read
|
||||
} CBMReadWrite;
|
||||
|
||||
typedef struct {
|
||||
const char *type_name; // referenced type/class name
|
||||
const char *enclosing_func_qn; // QN of enclosing function
|
||||
} CBMTypeRef;
|
||||
|
||||
typedef struct {
|
||||
const char *env_key; // environment variable key
|
||||
const char *enclosing_func_qn; // QN of enclosing function
|
||||
} CBMEnvAccess;
|
||||
|
||||
typedef struct {
|
||||
const char *var_name; // variable being assigned
|
||||
const char *type_name; // class/type name of RHS constructor
|
||||
const char *enclosing_func_qn; // QN of enclosing function
|
||||
} CBMTypeAssign;
|
||||
|
||||
// String reference: URL, config key, or async target found in source.
|
||||
// Extracted from string literals during AST walk.
|
||||
typedef enum {
|
||||
CBM_STRREF_URL = 0, // REST path or full URL
|
||||
CBM_STRREF_CONFIG = 1, // config file path or env var key
|
||||
} CBMStringRefKind;
|
||||
|
||||
typedef struct {
|
||||
const char *value; // the string literal content
|
||||
const char *enclosing_func_qn; // QN of enclosing function
|
||||
const char *key_path; // dotted key path from YAML/JSON nesting (NULL if flat)
|
||||
CBMStringRefKind kind; // URL, CONFIG
|
||||
} CBMStringRef;
|
||||
|
||||
/* Infrastructure binding: topic/queue → endpoint URL.
|
||||
* Extracted from YAML/HCL/JSON subscription/scheduler configs.
|
||||
* Used by pass_route_nodes to connect async Route nodes to handler services. */
|
||||
typedef struct {
|
||||
const char *source_name; // topic, queue, or schedule name
|
||||
const char *target_url; // push_endpoint, uri, or http_target URL
|
||||
const char *broker; // "pubsub", "cloud_tasks", "cloud_scheduler", "sqs", "kafka"
|
||||
} CBMInfraBinding;
|
||||
|
||||
/* Pub/sub channel participation. One record per emit() or on()/addListener()
|
||||
* call detected in source — the receiver (e.g. Socket.IO client, EventEmitter
|
||||
* instance) is intentionally NOT identified; matching is by channel_name
|
||||
* across files, which captures the common pattern of one logical bus per
|
||||
* service. Transport disambiguates Socket.IO vs EventEmitter vs future
|
||||
* detectors (Kafka, Cloud Pub/Sub, etc.). */
|
||||
typedef enum {
|
||||
CBM_CHANNEL_EMIT = 0,
|
||||
CBM_CHANNEL_LISTEN = 1,
|
||||
} CBMChannelDirection;
|
||||
|
||||
typedef struct {
|
||||
const char *channel_name; // literal channel name (e.g. "user.created")
|
||||
const char *transport; // "socketio", "event_emitter", ...
|
||||
const char *enclosing_func_qn; // QN of the function containing the emit/on call
|
||||
CBMChannelDirection direction;
|
||||
} CBMChannel;
|
||||
|
||||
// Rust: impl Trait for Struct
|
||||
typedef struct {
|
||||
const char *trait_name; // trait name (raw text)
|
||||
const char *struct_name; // struct/type name (raw text)
|
||||
} CBMImplTrait;
|
||||
|
||||
// LSP-resolved call: high-confidence type-aware call resolution
|
||||
typedef struct {
|
||||
const char *caller_qn; // enclosing function QN
|
||||
const char *callee_qn; // resolved target QN (fully qualified)
|
||||
const char *strategy; // "lsp_type_dispatch", "lsp_direct", etc.
|
||||
float confidence; // 0.90-0.95
|
||||
const char *reason; // diagnostic label for unresolved calls (NULL if resolved)
|
||||
} CBMResolvedCall;
|
||||
|
||||
typedef struct {
|
||||
CBMResolvedCall *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMResolvedCallArray;
|
||||
|
||||
// Growable arrays used during extraction.
|
||||
typedef struct {
|
||||
CBMDefinition *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMDefArray;
|
||||
|
||||
typedef struct {
|
||||
CBMCall *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMCallArray;
|
||||
|
||||
typedef struct {
|
||||
CBMImport *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMImportArray;
|
||||
|
||||
typedef struct {
|
||||
CBMUsage *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMUsageArray;
|
||||
|
||||
typedef struct {
|
||||
CBMThrow *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMThrowArray;
|
||||
|
||||
typedef struct {
|
||||
CBMReadWrite *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMRWArray;
|
||||
|
||||
typedef struct {
|
||||
CBMTypeRef *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMTypeRefArray;
|
||||
|
||||
typedef struct {
|
||||
CBMEnvAccess *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMEnvAccessArray;
|
||||
|
||||
typedef struct {
|
||||
CBMTypeAssign *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMTypeAssignArray;
|
||||
|
||||
typedef struct {
|
||||
CBMStringRef *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMStringRefArray;
|
||||
|
||||
typedef struct {
|
||||
CBMInfraBinding *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMInfraBindingArray;
|
||||
|
||||
typedef struct {
|
||||
CBMImplTrait *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMImplTraitArray;
|
||||
|
||||
typedef struct {
|
||||
CBMChannel *items;
|
||||
int count;
|
||||
int cap;
|
||||
} CBMChannelArray;
|
||||
|
||||
// Full extraction result for one file.
|
||||
typedef struct {
|
||||
CBMArena arena; // owns all string memory
|
||||
|
||||
CBMDefArray defs;
|
||||
CBMCallArray calls;
|
||||
CBMImportArray imports;
|
||||
CBMUsageArray usages;
|
||||
CBMThrowArray throws;
|
||||
CBMRWArray rw;
|
||||
CBMTypeRefArray type_refs;
|
||||
CBMEnvAccessArray env_accesses;
|
||||
CBMTypeAssignArray type_assigns;
|
||||
CBMImplTraitArray impl_traits; // Rust: impl Trait for Struct pairs
|
||||
CBMResolvedCallArray resolved_calls; // LSP-resolved calls (high confidence)
|
||||
CBMStringRefArray string_refs; // URL/config string literals from AST
|
||||
CBMInfraBindingArray infra_bindings; // topic→URL pairs from IaC configs
|
||||
CBMChannelArray channels; // Socket.IO / EventEmitter pub/sub participation
|
||||
|
||||
const char *module_qn; // module qualified name
|
||||
const char *namespace_name; // declared namespace/package (Java/Kotlin/C#/PHP), NULL if none
|
||||
const char **exports; // NULL-terminated (NULL if none)
|
||||
const char **constants; // NULL-terminated (NULL if none)
|
||||
const char **global_vars; // NULL-terminated (NULL if none)
|
||||
const char **macros; // NULL-terminated, C/C++ only (NULL if none)
|
||||
|
||||
bool has_error;
|
||||
const char *error_msg;
|
||||
/* Best-effort parse-coverage signal (experimental). parse_incomplete is true
|
||||
* when the parse tree contains tree-sitter ERROR/MISSING nodes — constructs
|
||||
* in those regions are silently absent from the graph. error_ranges is a
|
||||
* compact "start-end,start-end" list of 1-based line ranges (arena-owned) or
|
||||
* NULL. This only marks what we can DETECT: the absence of a flag is NOT a
|
||||
* completeness guarantee. Callers should treat a flagged file as "prefer
|
||||
* grep here", never treat an unflagged file as provably complete. */
|
||||
bool parse_incomplete;
|
||||
const char *error_ranges;
|
||||
int error_region_count;
|
||||
bool is_test_file;
|
||||
int imports_count;
|
||||
TSTree *cached_tree; // retained parse tree (caller frees via cbm_free_tree)
|
||||
CBMLanguage cached_lang; // language of cached tree (for parser selection)
|
||||
|
||||
// Retained source bytes — copied into `arena` by the parallel
|
||||
// extract pass so the fused cross-file LSP step in resolve_worker
|
||||
// can run without re-reading the file from disk. NULL when the
|
||||
// file exceeded the per-file (100 MB) or total (2 GB) retention
|
||||
// cap; in that case the cross-file LSP step is skipped for this
|
||||
// file (defs/calls already extracted are unaffected).
|
||||
const char *source;
|
||||
int source_len;
|
||||
} CBMFileResult;
|
||||
|
||||
// --- Enclosing function cache ---
|
||||
// Avoids repeated parent-chain walks for nodes within the same function body.
|
||||
// Each entry records a function's byte range and its precomputed QN.
|
||||
#define EFC_SIZE 64 // power of 2 for fast modulo
|
||||
|
||||
typedef struct {
|
||||
uint32_t start_byte;
|
||||
uint32_t end_byte;
|
||||
const char *qn;
|
||||
} EFCEntry;
|
||||
|
||||
typedef struct {
|
||||
EFCEntry entries[EFC_SIZE];
|
||||
int count;
|
||||
} EFCache;
|
||||
|
||||
// --- Extraction context passed to sub-extractors ---
|
||||
|
||||
// Module-level string constant map (for constant propagation)
|
||||
#define CBM_MAX_STRING_CONSTANTS 256
|
||||
typedef struct {
|
||||
const char *names[CBM_MAX_STRING_CONSTANTS];
|
||||
const char *values[CBM_MAX_STRING_CONSTANTS];
|
||||
int count;
|
||||
} CBMStringConstantMap;
|
||||
|
||||
typedef struct {
|
||||
CBMArena *arena;
|
||||
CBMFileResult *result;
|
||||
const char *source;
|
||||
int source_len;
|
||||
CBMLanguage language;
|
||||
const char *project;
|
||||
const char *rel_path;
|
||||
const char *module_qn;
|
||||
TSNode root;
|
||||
EFCache ef_cache; // enclosing function cache
|
||||
const char *enclosing_class_qn; // for nested class QN computation
|
||||
CBMStringConstantMap string_constants; // module-level NAME = "value" pairs
|
||||
} CBMExtractCtx;
|
||||
|
||||
// --- Public API ---
|
||||
|
||||
// Bind third-party allocators (tree-sitter, sqlite3) to mimalloc as
|
||||
// defense-in-depth, so they never depend on the fragile MI_OVERRIDE symbol
|
||||
// override (#424). MUST be called as the very first statement of main(), before
|
||||
// any sqlite3_open*/sqlite3_initialize (SQLITE_CONFIG_MALLOC returns
|
||||
// SQLITE_MISUSE once sqlite has initialized).
|
||||
// Idempotent (static guard); intended for single-threaded startup. cbm_init()
|
||||
// also calls it so non-main entry points (pipeline passes) still get the binds.
|
||||
// In the test build (no CBM_BIND_TS_ALLOCATOR) this is a no-op.
|
||||
void cbm_alloc_init(void);
|
||||
|
||||
// Initialize the library. Call once at startup. Returns 0 on success.
|
||||
int cbm_init(void);
|
||||
|
||||
// True when rel_path is in the crash-quarantine set — the newline-delimited list
|
||||
// of files (CBM_INDEX_QUARANTINE_FILE) the crash supervisor pinned as crashers
|
||||
// during its single-threaded recovery re-run. Loaded once, lazily; read-only
|
||||
// after load. cbm_extract_file short-circuits such files to an empty result so no
|
||||
// pass can crash on them; the pipeline extract loops call this to also REPORT the
|
||||
// skip as phase="crash". Always false (cheap no-op) when the env var is unset.
|
||||
bool cbm_index_is_quarantined(const char *rel_path);
|
||||
|
||||
// Phase a quarantined file was pinned under: "crash" (a fault signal) or "hang"
|
||||
// (killed for making no progress). Returns NULL when rel_path is not quarantined.
|
||||
// Drives the same lazy once-load as cbm_index_is_quarantined. Used by the pipeline
|
||||
// extract loops to report the skip's phase in skipped[] (falls back to "crash").
|
||||
const char *cbm_index_quarantine_phase(const char *rel_path);
|
||||
|
||||
// Crash-supervisor marker journal (parallel-safe): appends "S <rel_path>" /
|
||||
// "D <rel_path>" to CBM_INDEX_MARKER_FILE. Files with an S but no D form the
|
||||
// parent's crash/hang suspect set. No-ops when the env var is unset.
|
||||
// cbm_extract_file journals its own start/done; long-running per-file phases
|
||||
// (cross-LSP resolve) call these around their per-file work so a hang there
|
||||
// is attributed to the RIGHT file instead of a stale extraction marker.
|
||||
void cbm_index_mark_start(const char *rel_path);
|
||||
void cbm_index_mark_done(const char *rel_path);
|
||||
|
||||
// Extract all data from one file. Caller must call cbm_free_result().
|
||||
// source must remain valid for the duration of the call.
|
||||
// timeout_micros: per-file parse timeout in microseconds (0 = no timeout).
|
||||
CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage language,
|
||||
const char *project, const char *rel_path, int64_t timeout_micros,
|
||||
const char **extra_defines, // NULL-terminated, or NULL
|
||||
const char **include_paths // NULL-terminated, or NULL
|
||||
);
|
||||
|
||||
// Free all memory associated with a result.
|
||||
void cbm_free_result(CBMFileResult *result);
|
||||
|
||||
// Free only the cached tree from a result (caller retained it for reuse).
|
||||
void cbm_free_tree(CBMFileResult *result);
|
||||
|
||||
// Free a standalone TSTree pointer (for Go layer cleanup).
|
||||
void cbm_free_tree_ptr(TSTree *tree);
|
||||
|
||||
// Reset the thread-local parser's internal state, releasing slab-allocated
|
||||
// subtrees. Must be called BEFORE cbm_slab_reset_thread() so the slab rebuild
|
||||
// doesn't corrupt live parser state.
|
||||
void cbm_reset_thread_parser(void);
|
||||
|
||||
// Destroy the thread-local parser. Call on worker thread exit.
|
||||
void cbm_destroy_thread_parser(void);
|
||||
|
||||
// Shutdown the library. Call once at exit.
|
||||
void cbm_shutdown(void);
|
||||
|
||||
// Profiling: get accumulated parse/extraction times and file count.
|
||||
typedef struct {
|
||||
uint64_t *parse_ns;
|
||||
uint64_t *extract_ns;
|
||||
uint64_t *files;
|
||||
} cbm_profile_out_t;
|
||||
void cbm_get_profile(cbm_profile_out_t out);
|
||||
uint64_t cbm_get_lsp_ns(void);
|
||||
uint64_t cbm_get_preprocess_ns(void);
|
||||
uint64_t cbm_get_files_preprocessed(void);
|
||||
void cbm_reset_profile(void);
|
||||
|
||||
// Toggle C/C++ preprocessor Macro-node extraction (#375). The pipeline enables
|
||||
// it only for full/advanced index modes (it dominates extraction on macro-dense
|
||||
// codebases). Default ON. Set before extraction; read-only during.
|
||||
void cbm_set_macro_extraction(int enabled);
|
||||
int cbm_macro_extraction_enabled(void);
|
||||
|
||||
// --- Internal helpers used by extractors ---
|
||||
|
||||
// Growable array push functions (arena-allocated, no individual free needed).
|
||||
void cbm_defs_push(CBMDefArray *arr, CBMArena *a, CBMDefinition def);
|
||||
void cbm_calls_push(CBMCallArray *arr, CBMArena *a, CBMCall call);
|
||||
void cbm_imports_push(CBMImportArray *arr, CBMArena *a, CBMImport imp);
|
||||
void cbm_usages_push(CBMUsageArray *arr, CBMArena *a, CBMUsage usage);
|
||||
void cbm_throws_push(CBMThrowArray *arr, CBMArena *a, CBMThrow thr);
|
||||
void cbm_rw_push(CBMRWArray *arr, CBMArena *a, CBMReadWrite rw);
|
||||
void cbm_typerefs_push(CBMTypeRefArray *arr, CBMArena *a, CBMTypeRef tr);
|
||||
void cbm_envaccess_push(CBMEnvAccessArray *arr, CBMArena *a, CBMEnvAccess ea);
|
||||
void cbm_typeassign_push(CBMTypeAssignArray *arr, CBMArena *a, CBMTypeAssign ta);
|
||||
void cbm_stringref_push(CBMStringRefArray *arr, CBMArena *a, CBMStringRef sr);
|
||||
void cbm_infrabinding_push(CBMInfraBindingArray *arr, CBMArena *a, CBMInfraBinding ib);
|
||||
void cbm_impltrait_push(CBMImplTraitArray *arr, CBMArena *a, CBMImplTrait it);
|
||||
void cbm_resolvedcall_push(CBMResolvedCallArray *arr, CBMArena *a, CBMResolvedCall rc);
|
||||
void cbm_channels_push(CBMChannelArray *arr, CBMArena *a, CBMChannel ch);
|
||||
|
||||
// --- Sub-extractor entry points ---
|
||||
|
||||
void cbm_extract_definitions(CBMExtractCtx *ctx);
|
||||
void cbm_extract_imports(CBMExtractCtx *ctx);
|
||||
void cbm_extract_usages(CBMExtractCtx *ctx);
|
||||
void cbm_extract_semantic(CBMExtractCtx *ctx);
|
||||
void cbm_extract_type_refs(CBMExtractCtx *ctx);
|
||||
void cbm_extract_env_accesses(CBMExtractCtx *ctx);
|
||||
void cbm_extract_type_assigns(CBMExtractCtx *ctx);
|
||||
void cbm_extract_channels(CBMExtractCtx *ctx);
|
||||
|
||||
// Single-pass unified extraction (replaces the 7 calls above except defs+imports).
|
||||
void cbm_extract_unified(CBMExtractCtx *ctx);
|
||||
|
||||
// K8s / Kustomize semantic extractor (called when language is CBM_LANG_K8S or CBM_LANG_KUSTOMIZE).
|
||||
void cbm_extract_k8s(CBMExtractCtx *ctx);
|
||||
|
||||
// --- Label predicates ---
|
||||
|
||||
// True when `label` names a TYPE-LIKE container definition — a node that can own
|
||||
// methods/fields, be a base/embedded type, satisfy/declare an interface, and be a
|
||||
// target of name→type resolution. The canonical set is:
|
||||
// Class, Struct, Interface, Enum, Type, Trait.
|
||||
// Single source of truth for every type-resolution / registry-seeding /
|
||||
// INHERITS·IMPLEMENTS / LSP-type-registrar consumer, so adding a new type-like
|
||||
// label (e.g. "Struct" for Rust/Go/Swift/D structs) updates them all at once
|
||||
// instead of scattering `|| strcmp(label,"Struct")==0` across the tree.
|
||||
// `label` may be NULL (returns false). Defined in helpers.c.
|
||||
bool cbm_label_is_type_like(const char *label);
|
||||
|
||||
#endif // CBM_H
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,211 @@
|
||||
#include "cbm.h"
|
||||
#include "arena.h" // CBMArena, cbm_arena_strndup
|
||||
#include "helpers.h"
|
||||
#include "lang_specs.h"
|
||||
#include "extract_unified.h"
|
||||
#include "foundation/constants.h"
|
||||
#include "extract_node_stack.h"
|
||||
#include "tree_sitter/api.h" // TSNode, ts_node_*
|
||||
#include <stdint.h> // uint32_t
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
/* Minimum length for an env var name (e.g., "DB"). */
|
||||
enum { MIN_ENV_NAME_LEN = 2 };
|
||||
|
||||
// Unquote a string literal: "foo" -> foo, 'foo' -> foo
|
||||
static const char *unquote(CBMArena *a, const char *s) {
|
||||
if (!s || !s[0]) {
|
||||
return NULL;
|
||||
}
|
||||
// Trim whitespace
|
||||
while (*s == ' ' || *s == '\t') {
|
||||
s++;
|
||||
}
|
||||
size_t len = strlen(s);
|
||||
if (len >= CBM_QUOTE_PAIR) {
|
||||
char first = s[0];
|
||||
char last = s[len - CBM_QUOTE_OFFSET];
|
||||
if ((first == '"' && last == '"') || (first == '\'' && last == '\'') ||
|
||||
(first == '`' && last == '`')) {
|
||||
return cbm_arena_strndup(a, s + CBM_QUOTE_OFFSET, len - CBM_QUOTE_PAIR);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// Extract env key from a function call like os.Getenv("KEY").
|
||||
static const char *extract_env_key_from_call(CBMExtractCtx *ctx, TSNode node,
|
||||
const CBMLangSpec *spec) {
|
||||
if (!spec->env_access_functions || !spec->env_access_functions[0]) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
TSNode func_node = ts_node_child_by_field_name(node, TS_FIELD("function"));
|
||||
if (ts_node_is_null(func_node)) {
|
||||
return NULL;
|
||||
}
|
||||
char *callee = cbm_node_text(ctx->arena, func_node, ctx->source);
|
||||
if (!callee) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Check if callee matches any env access function
|
||||
bool match = false;
|
||||
for (const char **ef = spec->env_access_functions; *ef; ef++) {
|
||||
if (strcmp(callee, *ef) == 0) {
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!match) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get first argument (the env key)
|
||||
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
|
||||
if (ts_node_is_null(args)) {
|
||||
return NULL;
|
||||
}
|
||||
// Find first named child (skip parentheses)
|
||||
for (uint32_t i = 0; i < ts_node_child_count(args); i++) {
|
||||
TSNode child = ts_node_child(args, i);
|
||||
const char *ck = ts_node_type(child);
|
||||
if (strcmp(ck, "(") == 0 || strcmp(ck, ")") == 0 || strcmp(ck, ",") == 0) {
|
||||
continue;
|
||||
}
|
||||
char *arg_text = cbm_node_text(ctx->arena, child, ctx->source);
|
||||
return unquote(ctx->arena, arg_text);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Extract env key from member access like process.env.KEY or os.environ["KEY"].
|
||||
static const char *extract_env_key_from_member(CBMExtractCtx *ctx, TSNode node,
|
||||
const CBMLangSpec *spec) {
|
||||
if (!spec->env_access_member_patterns || !spec->env_access_member_patterns[0]) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *text = cbm_node_text(ctx->arena, node, ctx->source);
|
||||
if (!text || !text[0]) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (const char **pat = spec->env_access_member_patterns; *pat; pat++) {
|
||||
size_t plen = strlen(*pat);
|
||||
|
||||
// Dot access: pattern.KEY
|
||||
if (strncmp(text, *pat, plen) == 0 && text[plen] == '.') {
|
||||
const char *key = text + plen + CBM_QUOTE_OFFSET;
|
||||
// Validate: no further dots/brackets
|
||||
if (key[0] && !strchr(key, '.') && !strchr(key, '[')) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscript: pattern["KEY"]
|
||||
if (strncmp(text, *pat, plen) == 0 && text[plen] == '[') {
|
||||
const char *inner = text + plen + CBM_QUOTE_OFFSET;
|
||||
size_t ilen = strlen(inner);
|
||||
if (ilen > 0 && inner[ilen - CBM_QUOTE_OFFSET] == ']') {
|
||||
char *bracket_content =
|
||||
cbm_arena_strndup(ctx->arena, inner, ilen - CBM_QUOTE_OFFSET);
|
||||
return unquote(ctx->arena, bracket_content);
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Check if an env key name looks like an environment variable (uppercase + underscores).
|
||||
static bool is_env_var_name(const char *s) {
|
||||
if (!s || strlen(s) < MIN_ENV_NAME_LEN) {
|
||||
return false;
|
||||
}
|
||||
bool has_upper = false;
|
||||
for (const char *p = s; *p; p++) {
|
||||
if (*p >= 'A' && *p <= 'Z') {
|
||||
{
|
||||
has_upper = true;
|
||||
}
|
||||
} else if (*p == '_' || (*p >= '0' && *p <= '9')) { /* ok */
|
||||
} else {
|
||||
{ return false; }
|
||||
}
|
||||
}
|
||||
return has_upper;
|
||||
}
|
||||
|
||||
// Iterative env access walker — explicit stack
|
||||
static void walk_env_accesses(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
|
||||
TSNodeStack stack;
|
||||
ts_nstack_init(&stack, ctx->arena, 4096);
|
||||
ts_nstack_push(&stack, ctx->arena, root);
|
||||
|
||||
while (stack.count > 0) {
|
||||
TSNode node = ts_nstack_pop(&stack);
|
||||
const char *kind = ts_node_type(node);
|
||||
const char *env_key = NULL;
|
||||
|
||||
if (cbm_kind_in_set(node, spec->call_node_types)) {
|
||||
env_key = extract_env_key_from_call(ctx, node, spec);
|
||||
} else if (strcmp(kind, "member_expression") == 0 || strcmp(kind, "subscript") == 0 ||
|
||||
strcmp(kind, "attribute") == 0) {
|
||||
env_key = extract_env_key_from_member(ctx, node, spec);
|
||||
}
|
||||
|
||||
if (env_key && env_key[0] && is_env_var_name(env_key)) {
|
||||
CBMEnvAccess ea;
|
||||
ea.env_key = env_key;
|
||||
ea.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node);
|
||||
cbm_envaccess_push(&ctx->result->env_accesses, ctx->arena, ea);
|
||||
continue; // don't push children (avoid double-counting)
|
||||
}
|
||||
|
||||
ts_nstack_push_children(&stack, ctx->arena, node);
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_extract_env_accesses(CBMExtractCtx *ctx) {
|
||||
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
|
||||
if (!spec) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool has_funcs = spec->env_access_functions && spec->env_access_functions[0];
|
||||
bool has_members = spec->env_access_member_patterns && spec->env_access_member_patterns[0];
|
||||
if (!has_funcs && !has_members) {
|
||||
return;
|
||||
}
|
||||
|
||||
walk_env_accesses(ctx, ctx->root, spec);
|
||||
}
|
||||
|
||||
// --- Unified handler ---
|
||||
|
||||
void handle_env_accesses(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
|
||||
WalkState *state) {
|
||||
bool has_funcs = spec->env_access_functions && spec->env_access_functions[0];
|
||||
bool has_members = spec->env_access_member_patterns && spec->env_access_member_patterns[0];
|
||||
if (!has_funcs && !has_members) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *kind = ts_node_type(node);
|
||||
const char *env_key = NULL;
|
||||
|
||||
if (has_funcs && spec->call_node_types && cbm_kind_in_set(node, spec->call_node_types)) {
|
||||
env_key = extract_env_key_from_call(ctx, node, spec);
|
||||
} else if (has_members && (strcmp(kind, "member_expression") == 0 ||
|
||||
strcmp(kind, "subscript") == 0 || strcmp(kind, "attribute") == 0)) {
|
||||
env_key = extract_env_key_from_member(ctx, node, spec);
|
||||
}
|
||||
|
||||
if (env_key && env_key[0] && is_env_var_name(env_key)) {
|
||||
CBMEnvAccess ea;
|
||||
ea.env_key = env_key;
|
||||
ea.enclosing_func_qn = state->enclosing_func_qn;
|
||||
cbm_envaccess_push(&ctx->result->env_accesses, ctx->arena, ea);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,360 @@
|
||||
// extract_k8s.c — K8s manifest and Kustomize file extractor.
|
||||
//
|
||||
// For CBM_LANG_KUSTOMIZE: walks top-level block_mapping_pair nodes whose key
|
||||
// matches "resources", "bases", "patches", "components", or
|
||||
// "patchesStrategicMerge", then emits one CBMImport per block_sequence item.
|
||||
//
|
||||
// For CBM_LANG_K8S: finds apiVersion, kind, and metadata.name scalars in the
|
||||
// first document's block_mapping and emits one CBMDefinition with label
|
||||
// "Resource" and name "Kind/metadata-name".
|
||||
|
||||
#include "cbm.h"
|
||||
#include "arena.h"
|
||||
#include "helpers.h"
|
||||
#include "tree_sitter/api.h"
|
||||
#include "foundation/constants.h"
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Local constants. */
|
||||
enum {
|
||||
K8S_BUF_SIZE = 256,
|
||||
RESULT_BUF_SIZE = 512,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Return the raw source text for a scalar node (plain, single-quoted, or
|
||||
// double-quoted). Surrounding quote characters are stripped for quoted forms.
|
||||
// Handles flow_node wrappers transparently by descending into the first named
|
||||
// child (the tree-sitter YAML grammar often wraps scalars in flow_node).
|
||||
// Returns NULL for non-scalar node types.
|
||||
static const char *get_scalar_text(CBMArena *a, TSNode node, const char *source) {
|
||||
enum { MAX_UNWRAP = 4 };
|
||||
for (int depth = 0; depth < MAX_UNWRAP; depth++) {
|
||||
const char *type = ts_node_type(node);
|
||||
if (strcmp(type, "flow_node") == 0) {
|
||||
TSNode inner = ts_node_named_child(node, 0);
|
||||
if (ts_node_is_null(inner)) {
|
||||
return NULL;
|
||||
}
|
||||
node = inner;
|
||||
continue;
|
||||
}
|
||||
if (strcmp(type, "plain_scalar") == 0) {
|
||||
return cbm_node_text(a, node, source);
|
||||
}
|
||||
if (strcmp(type, "double_quote_scalar") == 0 || strcmp(type, "single_quote_scalar") == 0) {
|
||||
const char *raw = cbm_node_text(a, node, source);
|
||||
if (!raw) {
|
||||
return NULL;
|
||||
}
|
||||
size_t len = strlen(raw);
|
||||
if (len >= PAIR_LEN) {
|
||||
return cbm_arena_strndup(a, raw + SKIP_ONE, len - PAIR_LEN);
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Return true if the key text of a block_mapping_pair matches one of the
|
||||
// Kustomize resource-list field names.
|
||||
static int is_kustomize_list_key(const char *key) {
|
||||
return (strcmp(key, "resources") == 0 || strcmp(key, "bases") == 0 ||
|
||||
strcmp(key, "patches") == 0 || strcmp(key, "components") == 0 ||
|
||||
strcmp(key, "patchesStrategicMerge") == 0 || strcmp(key, "crds") == 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kustomize extraction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Walk a block_sequence node and emit one CBMImport per block_sequence_item
|
||||
// scalar child, using key_name as the local_name.
|
||||
static void emit_kustomize_sequence(CBMExtractCtx *ctx, TSNode seq_node, const char *key_name) {
|
||||
CBMArena *a = ctx->arena;
|
||||
uint32_t n = ts_node_child_count(seq_node);
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
TSNode item = ts_node_child(seq_node, i);
|
||||
if (strcmp(ts_node_type(item), "block_sequence_item") != 0) {
|
||||
continue;
|
||||
}
|
||||
// block_sequence_item has one named child: the value
|
||||
uint32_t ic = ts_node_child_count(item);
|
||||
for (uint32_t j = 0; j < ic; j++) {
|
||||
TSNode val = ts_node_child(item, j);
|
||||
const char *scalar = get_scalar_text(a, val, ctx->source);
|
||||
if (!scalar) {
|
||||
continue;
|
||||
}
|
||||
CBMImport imp = {
|
||||
.local_name = cbm_arena_strdup(a, key_name),
|
||||
.module_path = cbm_arena_strdup(a, scalar),
|
||||
};
|
||||
cbm_imports_push(&ctx->result->imports, a, imp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap a YAML node through optional block_node wrapper to get block_mapping.
|
||||
// Returns null node if not a block_mapping.
|
||||
static TSNode unwrap_block_mapping(TSNode doc_child) {
|
||||
TSNode mapping = ts_node_named_child(doc_child, 0);
|
||||
if (ts_node_is_null(mapping)) {
|
||||
return mapping;
|
||||
}
|
||||
if (strcmp(ts_node_type(mapping), "block_node") == 0) {
|
||||
mapping = ts_node_named_child(mapping, 0);
|
||||
}
|
||||
if (ts_node_is_null(mapping) || strcmp(ts_node_type(mapping), "block_mapping") != 0) {
|
||||
TSNode null_node = {0};
|
||||
return null_node;
|
||||
}
|
||||
return mapping;
|
||||
}
|
||||
|
||||
// Process a single block_mapping_pair for kustomize list keys.
|
||||
static void process_kustomize_pair(CBMExtractCtx *ctx, TSNode pair) {
|
||||
if (strcmp(ts_node_type(pair), "block_mapping_pair") != 0) {
|
||||
return;
|
||||
}
|
||||
TSNode key_node = ts_node_named_child(pair, 0);
|
||||
if (ts_node_is_null(key_node)) {
|
||||
return;
|
||||
}
|
||||
const char *key_text = get_scalar_text(ctx->arena, key_node, ctx->source);
|
||||
if (!key_text || !is_kustomize_list_key(key_text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
TSNode val_node = ts_node_named_child(pair, SKIP_ONE);
|
||||
if (ts_node_is_null(val_node)) {
|
||||
return;
|
||||
}
|
||||
if (strcmp(ts_node_type(val_node), "block_node") == 0) {
|
||||
val_node = ts_node_named_child(val_node, 0);
|
||||
}
|
||||
if (ts_node_is_null(val_node) || strcmp(ts_node_type(val_node), "block_sequence") != 0) {
|
||||
return;
|
||||
}
|
||||
emit_kustomize_sequence(ctx, val_node, key_text);
|
||||
}
|
||||
|
||||
// Forward declaration: defined with the K8s-manifest helpers below.
|
||||
static TSNode unwrap_pair_value(TSNode pair);
|
||||
|
||||
// Emit a "Class" def named after the document's `kind` scalar. A kustomization
|
||||
// file has no metadata.name, so the def name is the bare kind ("Kustomization").
|
||||
// Mirrors the K8s manifest kind-def so Kustomize resources are also discoverable.
|
||||
static void emit_kustomize_kind_def(CBMExtractCtx *ctx, TSNode mapping) {
|
||||
CBMArena *a = ctx->arena;
|
||||
uint32_t pair_n = ts_node_child_count(mapping);
|
||||
for (uint32_t pi = 0; pi < pair_n; pi++) {
|
||||
TSNode pair = ts_node_child(mapping, pi);
|
||||
if (strcmp(ts_node_type(pair), "block_mapping_pair") != 0) {
|
||||
continue;
|
||||
}
|
||||
TSNode key_node = ts_node_named_child(pair, 0);
|
||||
if (ts_node_is_null(key_node)) {
|
||||
continue;
|
||||
}
|
||||
const char *key = get_scalar_text(a, key_node, ctx->source);
|
||||
if (!key || strcmp(key, "kind") != 0) {
|
||||
continue;
|
||||
}
|
||||
TSNode val_node = unwrap_pair_value(pair);
|
||||
if (ts_node_is_null(val_node)) {
|
||||
continue;
|
||||
}
|
||||
const char *kind = get_scalar_text(a, val_node, ctx->source);
|
||||
if (!kind || !kind[0]) {
|
||||
continue;
|
||||
}
|
||||
CBMDefinition def = {0};
|
||||
def.name = cbm_arena_strdup(a, kind);
|
||||
def.qualified_name = cbm_arena_sprintf(a, "%s.%s", ctx->module_qn, kind);
|
||||
def.label = cbm_arena_strdup(a, "Resource");
|
||||
def.file_path = ctx->rel_path;
|
||||
def.start_line = ts_node_start_point(mapping).row + TS_LINE_OFFSET;
|
||||
def.end_line = ts_node_end_point(mapping).row + TS_LINE_OFFSET;
|
||||
cbm_defs_push(&ctx->result->defs, a, def);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void extract_kustomize(CBMExtractCtx *ctx) {
|
||||
TSNode root = ctx->root;
|
||||
uint32_t root_n = ts_node_child_count(root);
|
||||
for (uint32_t si = 0; si < root_n; si++) {
|
||||
TSNode stream_child = ts_node_child(root, si);
|
||||
if (strcmp(ts_node_type(stream_child), "document") != 0) {
|
||||
continue;
|
||||
}
|
||||
TSNode mapping = unwrap_block_mapping(stream_child);
|
||||
if (ts_node_is_null(mapping)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
emit_kustomize_kind_def(ctx, mapping);
|
||||
|
||||
uint32_t pair_n = ts_node_child_count(mapping);
|
||||
for (uint32_t pi = 0; pi < pair_n; pi++) {
|
||||
process_kustomize_pair(ctx, ts_node_child(mapping, pi));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// K8s manifest extraction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Extract the "name" scalar from a metadata block_mapping.
|
||||
static void extract_metadata_name(CBMArena *a, TSNode meta_mapping, const char *source,
|
||||
char *meta_name_buf, size_t meta_sz) {
|
||||
if (ts_node_is_null(meta_mapping) || strcmp(ts_node_type(meta_mapping), "block_mapping") != 0) {
|
||||
return;
|
||||
}
|
||||
uint32_t mn = ts_node_child_count(meta_mapping);
|
||||
for (uint32_t mi = 0; mi < mn; mi++) {
|
||||
TSNode mpair = ts_node_child(meta_mapping, mi);
|
||||
if (strcmp(ts_node_type(mpair), "block_mapping_pair") != 0) {
|
||||
continue;
|
||||
}
|
||||
TSNode mkey = ts_node_named_child(mpair, 0);
|
||||
if (ts_node_is_null(mkey)) {
|
||||
continue;
|
||||
}
|
||||
const char *mkey_text = get_scalar_text(a, mkey, source);
|
||||
if (!mkey_text || strcmp(mkey_text, "name") != 0) {
|
||||
continue;
|
||||
}
|
||||
TSNode mval = ts_node_named_child(mpair, SKIP_ONE);
|
||||
if (ts_node_is_null(mval)) {
|
||||
continue;
|
||||
}
|
||||
const char *meta_name = get_scalar_text(a, mval, source);
|
||||
if (meta_name) {
|
||||
snprintf(meta_name_buf, meta_sz, "%s", meta_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap a block_mapping_pair value through optional block_node.
|
||||
static TSNode unwrap_pair_value(TSNode pair) {
|
||||
TSNode val_node = ts_node_named_child(pair, SKIP_ONE);
|
||||
if (ts_node_is_null(val_node)) {
|
||||
return val_node;
|
||||
}
|
||||
if (strcmp(ts_node_type(val_node), "block_node") == 0) {
|
||||
val_node = ts_node_named_child(val_node, 0);
|
||||
}
|
||||
return val_node;
|
||||
}
|
||||
|
||||
// Descend into the first block_mapping of a document and extract
|
||||
// kind and metadata.name. Returns void; fills kind_buf and meta_name_buf.
|
||||
static void extract_k8s_scalars(CBMExtractCtx *ctx, TSNode mapping, char *kind_buf, size_t kind_sz,
|
||||
char *meta_name_buf, size_t meta_sz) {
|
||||
CBMArena *a = ctx->arena;
|
||||
kind_buf[0] = '\0';
|
||||
meta_name_buf[0] = '\0';
|
||||
|
||||
uint32_t n = ts_node_child_count(mapping);
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
TSNode pair = ts_node_child(mapping, i);
|
||||
if (strcmp(ts_node_type(pair), "block_mapping_pair") != 0) {
|
||||
continue;
|
||||
}
|
||||
TSNode key_node = ts_node_named_child(pair, 0);
|
||||
if (ts_node_is_null(key_node)) {
|
||||
continue;
|
||||
}
|
||||
const char *key = get_scalar_text(a, key_node, ctx->source);
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TSNode val_node = unwrap_pair_value(pair);
|
||||
if (ts_node_is_null(val_node)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(key, "kind") == 0) {
|
||||
const char *v = get_scalar_text(a, val_node, ctx->source);
|
||||
if (v) {
|
||||
snprintf(kind_buf, kind_sz, "%s", v);
|
||||
}
|
||||
} else if (strcmp(key, "metadata") == 0) {
|
||||
extract_metadata_name(a, val_node, ctx->source, meta_name_buf, meta_sz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void extract_k8s_manifest(CBMExtractCtx *ctx) {
|
||||
CBMArena *a = ctx->arena;
|
||||
|
||||
TSNode root = ctx->root;
|
||||
uint32_t root_n = ts_node_child_count(root);
|
||||
for (uint32_t si = 0; si < root_n; si++) {
|
||||
TSNode stream_child = ts_node_child(root, si);
|
||||
if (strcmp(ts_node_type(stream_child), "document") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TSNode mapping = ts_node_named_child(stream_child, 0);
|
||||
if (ts_node_is_null(mapping)) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(ts_node_type(mapping), "block_node") == 0) {
|
||||
mapping = ts_node_named_child(mapping, 0);
|
||||
}
|
||||
if (ts_node_is_null(mapping) || strcmp(ts_node_type(mapping), "block_mapping") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char kind_buf[K8S_BUF_SIZE] = {0};
|
||||
char meta_name_buf[K8S_BUF_SIZE] = {0};
|
||||
extract_k8s_scalars(ctx, mapping, kind_buf, sizeof(kind_buf), meta_name_buf,
|
||||
sizeof(meta_name_buf));
|
||||
|
||||
// Skip malformed manifests (no kind or no metadata.name)
|
||||
if (kind_buf[0] == '\0' || meta_name_buf[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
char def_name[RESULT_BUF_SIZE];
|
||||
snprintf(def_name, sizeof(def_name), "%s/%s", kind_buf, meta_name_buf);
|
||||
|
||||
CBMDefinition def = {0};
|
||||
def.name = cbm_arena_strdup(a, def_name);
|
||||
def.qualified_name = cbm_arena_sprintf(a, "%s.%s", ctx->module_qn, def_name);
|
||||
// "Resource" is the canonical def label for a K8s resource kind. It is a
|
||||
// valid graph label and is what the K8s pipeline pass (pass_k8s.c) filters
|
||||
// on to upsert Resource nodes and emit INFRA_MAPS edges.
|
||||
def.label = cbm_arena_strdup(a, "Resource");
|
||||
def.file_path = ctx->rel_path;
|
||||
def.start_line = ts_node_start_point(mapping).row + TS_LINE_OFFSET;
|
||||
def.end_line = ts_node_end_point(mapping).row + TS_LINE_OFFSET;
|
||||
cbm_defs_push(&ctx->result->defs, a, def);
|
||||
|
||||
break; // Only the first document per file
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void cbm_extract_k8s(CBMExtractCtx *ctx) {
|
||||
if (ctx->language == CBM_LANG_KUSTOMIZE) {
|
||||
extract_kustomize(ctx);
|
||||
} else if (ctx->language == CBM_LANG_K8S) {
|
||||
extract_k8s_manifest(ctx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* extract_node_stack.h — Growable TSNode stack for AST traversal.
|
||||
*
|
||||
* Replaces fixed-size TSNode stack[] arrays that silently drop AST subtrees
|
||||
* when the stack overflows (GitHub issue #199).
|
||||
*
|
||||
* Uses the arena allocator for zero-fragmentation growth: old blocks are
|
||||
* abandoned (freed when the arena is destroyed at end of file extraction).
|
||||
* Initial capacity matches the previous fixed caps so small files allocate
|
||||
* no extra memory.
|
||||
*/
|
||||
#ifndef CBM_EXTRACT_NODE_STACK_H
|
||||
#define CBM_EXTRACT_NODE_STACK_H
|
||||
|
||||
#include "arena.h"
|
||||
#include "tree_sitter/api.h"
|
||||
#include <string.h> /* memcpy */
|
||||
|
||||
typedef struct {
|
||||
TSNode *items;
|
||||
int count;
|
||||
int cap;
|
||||
} TSNodeStack;
|
||||
|
||||
/* Initialize a stack with the given initial capacity, arena-allocated. */
|
||||
static inline void ts_nstack_init(TSNodeStack *s, CBMArena *arena, int initial_cap) {
|
||||
s->items = (TSNode *)cbm_arena_alloc(arena, (size_t)initial_cap * sizeof(TSNode));
|
||||
s->count = 0;
|
||||
s->cap = s->items ? initial_cap : 0;
|
||||
}
|
||||
|
||||
/* Push a node onto the stack, growing 2x if needed. */
|
||||
static inline void ts_nstack_push(TSNodeStack *s, CBMArena *arena, TSNode node) {
|
||||
if (s->count >= s->cap) {
|
||||
int new_cap = s->cap ? s->cap * 2 : 512;
|
||||
TSNode *new_items = (TSNode *)cbm_arena_alloc(arena, (size_t)new_cap * sizeof(TSNode));
|
||||
if (!new_items)
|
||||
return; /* OOM: best-effort, stop growing */
|
||||
if (s->items && s->count > 0) {
|
||||
memcpy(new_items, s->items, (size_t)s->count * sizeof(TSNode));
|
||||
}
|
||||
/* Old s->items is abandoned in the arena — freed on arena_destroy. */
|
||||
s->items = new_items;
|
||||
s->cap = new_cap;
|
||||
}
|
||||
s->items[s->count++] = node;
|
||||
}
|
||||
|
||||
/* Pop a node from the stack. Caller must check s->count > 0. */
|
||||
static inline TSNode ts_nstack_pop(TSNodeStack *s) {
|
||||
return s->items[--s->count];
|
||||
}
|
||||
|
||||
/*
|
||||
* Push all children of `node` so they POP in forward (source) order — a drop-in
|
||||
* replacement for the common idiom:
|
||||
* for (int i = (int)count - 1; i >= 0; i--) ts_nstack_push(s, a, ts_node_child(node, i));
|
||||
*
|
||||
* That idiom calls ts_node_child(node, i) once per index, and ts_node_child is
|
||||
* O(i) in tree-sitter (it walks the child iterator from the first child each
|
||||
* time). Over a node with N children that is O(N^2) — catastrophic on a program
|
||||
* root holding hundreds of thousands of top-level nodes (e.g. fixture/generated
|
||||
* files). This helper enumerates children in a single O(N) cursor pass, then
|
||||
* reverses the just-pushed segment so pop order is identical to the old idiom.
|
||||
*/
|
||||
static inline void ts_nstack_push_children(TSNodeStack *s, CBMArena *arena, TSNode node) {
|
||||
int base = s->count;
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(node);
|
||||
if (ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
do {
|
||||
ts_nstack_push(s, arena, ts_tree_cursor_current_node(&cursor));
|
||||
} while (ts_tree_cursor_goto_next_sibling(&cursor));
|
||||
}
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
/* Reverse [base, count) so the first child pops first (forward order). */
|
||||
int lo = base, hi = s->count - 1;
|
||||
while (lo < hi) {
|
||||
TSNode tmp = s->items[lo];
|
||||
s->items[lo] = s->items[hi];
|
||||
s->items[hi] = tmp;
|
||||
lo++;
|
||||
hi--;
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CBM_EXTRACT_NODE_STACK_H */
|
||||
@@ -0,0 +1,342 @@
|
||||
#include "cbm.h"
|
||||
#include "arena.h"
|
||||
#include "helpers.h"
|
||||
#include "lang_specs.h"
|
||||
#include "extract_unified.h"
|
||||
#include "tree_sitter/api.h" // TSNode, ts_node_*
|
||||
#include "foundation/constants.h"
|
||||
#include "extract_node_stack.h"
|
||||
|
||||
enum { MAX_EXCEPTION_NAME_LEN = 100, LAST_IDX = 1 };
|
||||
#include <stdint.h> // uint32_t
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
// Field name length for ts_node_child_by_field_name() calls.
|
||||
#define FIELD_LEN_CONSTRUCTOR 11 // strlen("constructor")
|
||||
|
||||
// --- Throw/Raise extraction ---
|
||||
|
||||
// Detect whether a node is a throw node for the given spec.
|
||||
//
|
||||
// Kotlin special case: this grammar models `throw X(...)` as a `jump_expression`
|
||||
// (the same node also covers return/break/continue), NOT a `throw_expression`,
|
||||
// so the spec's throw_node_types ("throw_expression") never matches. We treat a
|
||||
// Kotlin `jump_expression` as a throw only when its first child is the `throw`
|
||||
// keyword, which excludes return/break/continue without false positives.
|
||||
static bool is_throw_node(TSNode node, const CBMLangSpec *spec) {
|
||||
if (cbm_kind_in_set(node, spec->throw_node_types)) {
|
||||
return true;
|
||||
}
|
||||
if (spec->language == CBM_LANG_KOTLIN && strcmp(ts_node_type(node), "jump_expression") == 0) {
|
||||
uint32_t nc = ts_node_child_count(node);
|
||||
if (nc > 0 && strcmp(ts_node_type(ts_node_child(node, 0)), "throw") == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve exception name from the first meaningful child of a throw/raise node.
|
||||
static char *resolve_exception_name(CBMArena *a, TSNode throw_node, const char *source) {
|
||||
uint32_t nc = ts_node_child_count(throw_node);
|
||||
for (uint32_t i = 0; i < nc; i++) {
|
||||
TSNode child = ts_node_child(throw_node, i);
|
||||
const char *ck = ts_node_type(child);
|
||||
if (strcmp(ck, "raise") == 0 || strcmp(ck, "throw") == 0) {
|
||||
continue;
|
||||
}
|
||||
if (ck[0] == ';' || ck[0] == '(' || ck[0] == ')') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(ck, "call") == 0 || strcmp(ck, "call_expression") == 0 ||
|
||||
strcmp(ck, "new_expression") == 0 || strcmp(ck, "object_creation_expression") == 0 ||
|
||||
strcmp(ck, "instance_expression") == 0) {
|
||||
TSNode fn = ts_node_child_by_field_name(child, TS_FIELD("function"));
|
||||
if (ts_node_is_null(fn)) {
|
||||
fn = ts_node_child_by_field_name(child, "constructor", FIELD_LEN_CONSTRUCTOR);
|
||||
}
|
||||
if (ts_node_is_null(fn)) {
|
||||
fn = ts_node_child_by_field_name(child, TS_FIELD("type"));
|
||||
}
|
||||
if (ts_node_is_null(fn) && ts_node_named_child_count(child) > 0) {
|
||||
fn = ts_node_named_child(child, 0);
|
||||
}
|
||||
if (!ts_node_is_null(fn)) {
|
||||
return cbm_node_text(a, fn, source);
|
||||
}
|
||||
} else {
|
||||
return cbm_node_text(a, child, source);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Extract exception types from a Java-style throws clause.
|
||||
static void extract_throws_clause(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
|
||||
const char *func_qn) {
|
||||
if (!spec->throws_clause_field || !spec->throws_clause_field[0]) {
|
||||
return;
|
||||
}
|
||||
const char *kind = ts_node_type(node);
|
||||
if (strcmp(kind, "method_declaration") != 0 && strcmp(kind, "constructor_declaration") != 0) {
|
||||
return;
|
||||
}
|
||||
TSNode throws_clause = ts_node_child_by_field_name(node, spec->throws_clause_field,
|
||||
(uint32_t)strlen(spec->throws_clause_field));
|
||||
if (ts_node_is_null(throws_clause)) {
|
||||
return;
|
||||
}
|
||||
uint32_t nc = ts_node_child_count(throws_clause);
|
||||
for (uint32_t i = 0; i < nc; i++) {
|
||||
TSNode child = ts_node_child(throws_clause, i);
|
||||
const char *ck = ts_node_type(child);
|
||||
if (strcmp(ck, "type_identifier") == 0 || strcmp(ck, "identifier") == 0 ||
|
||||
strcmp(ck, "scoped_type_identifier") == 0) {
|
||||
char *exc = cbm_node_text(ctx->arena, child, ctx->source);
|
||||
if (exc && exc[0]) {
|
||||
CBMThrow thr = {.exception_name = exc, .enclosing_func_qn = func_qn};
|
||||
cbm_throws_push(&ctx->result->throws, ctx->arena, thr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process a single node for throw extraction (called from iterative walker).
|
||||
static void process_throw_node(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
|
||||
if (is_throw_node(node, spec)) {
|
||||
char *exc_name = resolve_exception_name(ctx->arena, node, ctx->source);
|
||||
if (exc_name && exc_name[0]) {
|
||||
if (strlen(exc_name) > MAX_EXCEPTION_NAME_LEN) {
|
||||
exc_name[MAX_EXCEPTION_NAME_LEN] = '\0';
|
||||
}
|
||||
CBMThrow thr;
|
||||
thr.exception_name = exc_name;
|
||||
thr.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node);
|
||||
cbm_throws_push(&ctx->result->throws, ctx->arena, thr);
|
||||
}
|
||||
}
|
||||
|
||||
extract_throws_clause(ctx, node, spec, cbm_enclosing_func_qn_cached(ctx, node));
|
||||
}
|
||||
|
||||
// Iterative throw walker
|
||||
static void walk_throws(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
|
||||
TSNodeStack stack;
|
||||
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
|
||||
ts_nstack_push(&stack, ctx->arena, root);
|
||||
while (stack.count > 0) {
|
||||
TSNode node = ts_nstack_pop(&stack);
|
||||
process_throw_node(ctx, node, spec);
|
||||
uint32_t count = ts_node_child_count(node);
|
||||
for (int i = (int)count - LAST_IDX; i >= 0; i--) {
|
||||
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Read/Write detection (iterative) ---
|
||||
|
||||
// Resolve an assignment LHS node to the bare name being written. Handles
|
||||
// common wrappers so WRITES resolve for more languages than a raw identifier:
|
||||
// - expression_list (Go `x = ...` desugars left to expression_list[x])
|
||||
// - index/subscript (`cache[k] = v` → write the base var `cache`)
|
||||
// - field/member/selector access (`self.total = ...`, `obj.Field = ...` →
|
||||
// write the trailing field name `total`/`Field`)
|
||||
// Returns NULL if no simple write target can be determined.
|
||||
static char *resolve_lhs_write_name(CBMExtractCtx *ctx, TSNode left) {
|
||||
// Unwrap a single-element expression_list (Go).
|
||||
if (strcmp(ts_node_type(left), "expression_list") == 0) {
|
||||
if (ts_node_named_child_count(left) != 1) {
|
||||
return NULL; // multi-assign: ambiguous, skip
|
||||
}
|
||||
left = ts_node_named_child(left, 0);
|
||||
}
|
||||
const char *lk = ts_node_type(left);
|
||||
if (strcmp(lk, "identifier") == 0 || strcmp(lk, "simple_identifier") == 0) {
|
||||
return cbm_node_text(ctx->arena, left, ctx->source);
|
||||
}
|
||||
// Indexed write: write the base operand's identifier (`cache[k]` → cache).
|
||||
if (strcmp(lk, "index_expression") == 0 || strcmp(lk, "subscript_expression") == 0) {
|
||||
TSNode base = ts_node_child_by_field_name(left, TS_FIELD("operand"));
|
||||
if (ts_node_is_null(base)) {
|
||||
base = ts_node_child_by_field_name(left, TS_FIELD("object"));
|
||||
}
|
||||
if (ts_node_is_null(base) && ts_node_named_child_count(left) > 0) {
|
||||
base = ts_node_named_child(left, 0);
|
||||
}
|
||||
if (!ts_node_is_null(base)) {
|
||||
const char *bk = ts_node_type(base);
|
||||
if (strcmp(bk, "identifier") == 0 || strcmp(bk, "simple_identifier") == 0) {
|
||||
return cbm_node_text(ctx->arena, base, ctx->source);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
// Field/member write: write the trailing field name (`self.total` → total,
|
||||
// `obj.Field` → Field). Covers Rust field_expression, C#/Java member access.
|
||||
if (strcmp(lk, "field_expression") == 0 || strcmp(lk, "member_access_expression") == 0 ||
|
||||
strcmp(lk, "field_access") == 0 || strcmp(lk, "selector_expression") == 0) {
|
||||
TSNode fld = ts_node_child_by_field_name(left, TS_FIELD("field"));
|
||||
if (ts_node_is_null(fld)) {
|
||||
fld = ts_node_child_by_field_name(left, TS_FIELD("name"));
|
||||
}
|
||||
if (!ts_node_is_null(fld)) {
|
||||
return cbm_node_text(ctx->arena, fld, ctx->source);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Resolve the write target of a node in an assignment_node_types set. For a
|
||||
// plain assignment the target is the "left" field (or first child). For an
|
||||
// increment/decrement unary expression (`x++`, `++x`, C#
|
||||
// postfix_/prefix_unary_expression) there is no "left" field and the operand
|
||||
// may sit on either side of the operator token, so scan named children for the
|
||||
// first identifier / member-style operand. Returns a null node when no simple
|
||||
// target is found.
|
||||
static TSNode resolve_write_lhs_node(TSNode node) {
|
||||
TSNode left = ts_node_child_by_field_name(node, TS_FIELD("left"));
|
||||
if (!ts_node_is_null(left)) {
|
||||
return left;
|
||||
}
|
||||
const char *nk = ts_node_type(node);
|
||||
if (strcmp(nk, "postfix_unary_expression") == 0 || strcmp(nk, "prefix_unary_expression") == 0 ||
|
||||
strcmp(nk, "update_expression") == 0) {
|
||||
// Only ++/-- mutate their operand. Other unary postfix/prefix forms
|
||||
// (C# null-forgiving `x!`, address-of `&x`, deref `*x`, logical `!x`)
|
||||
// READ the operand — never treat them as writes.
|
||||
bool is_incdec = false;
|
||||
uint32_t total = ts_node_child_count(node);
|
||||
for (uint32_t i = 0; i < total; i++) {
|
||||
TSNode c = ts_node_child(node, i);
|
||||
if (ts_node_is_named(c)) {
|
||||
continue; // operator is an anonymous token
|
||||
}
|
||||
const char *op = ts_node_type(c);
|
||||
if (strcmp(op, "++") == 0 || strcmp(op, "--") == 0) {
|
||||
is_incdec = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_incdec) {
|
||||
return (TSNode){0};
|
||||
}
|
||||
uint32_t cnc = ts_node_named_child_count(node);
|
||||
for (uint32_t i = 0; i < cnc; i++) {
|
||||
TSNode c = ts_node_named_child(node, i);
|
||||
const char *ck = ts_node_type(c);
|
||||
if (strcmp(ck, "identifier") == 0 || strcmp(ck, "simple_identifier") == 0 ||
|
||||
strcmp(ck, "member_access_expression") == 0 ||
|
||||
strcmp(ck, "field_expression") == 0 || strcmp(ck, "field_access") == 0 ||
|
||||
strcmp(ck, "selector_expression") == 0 || strcmp(ck, "subscript_expression") == 0 ||
|
||||
strcmp(ck, "index_expression") == 0) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return (TSNode){0};
|
||||
}
|
||||
if (ts_node_child_count(node) > 0) {
|
||||
return ts_node_child(node, 0);
|
||||
}
|
||||
return (TSNode){0};
|
||||
}
|
||||
|
||||
// Try to emit a write for an assignment node.
|
||||
static void try_emit_assignment_write(CBMExtractCtx *ctx, TSNode node, const char *func_qn) {
|
||||
TSNode left = resolve_write_lhs_node(node);
|
||||
if (ts_node_is_null(left)) {
|
||||
return;
|
||||
}
|
||||
char *name = resolve_lhs_write_name(ctx, left);
|
||||
if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
|
||||
CBMReadWrite rw;
|
||||
rw.var_name = name;
|
||||
rw.is_write = true;
|
||||
rw.enclosing_func_qn = func_qn;
|
||||
cbm_rw_push(&ctx->result->rw, ctx->arena, rw);
|
||||
}
|
||||
}
|
||||
|
||||
static void walk_readwrites(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
|
||||
TSNodeStack stack;
|
||||
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
|
||||
ts_nstack_push(&stack, ctx->arena, root);
|
||||
while (stack.count > 0) {
|
||||
TSNode node = ts_nstack_pop(&stack);
|
||||
if (cbm_kind_in_set(node, spec->assignment_node_types)) {
|
||||
try_emit_assignment_write(ctx, node, cbm_enclosing_func_qn_cached(ctx, node));
|
||||
}
|
||||
uint32_t count = ts_node_child_count(node);
|
||||
for (int i = (int)count - LAST_IDX; i >= 0; i--) {
|
||||
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_extract_semantic(CBMExtractCtx *ctx) {
|
||||
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
|
||||
if (!spec) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Throws
|
||||
if ((spec->throw_node_types && spec->throw_node_types[0]) ||
|
||||
(spec->throws_clause_field && spec->throws_clause_field[0])) {
|
||||
walk_throws(ctx, ctx->root, spec);
|
||||
}
|
||||
|
||||
// Reads/Writes
|
||||
if (spec->assignment_node_types && spec->assignment_node_types[0]) {
|
||||
walk_readwrites(ctx, ctx->root, spec);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Unified handlers ---
|
||||
|
||||
void handle_throws(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) {
|
||||
bool has_throws = spec->throw_node_types && spec->throw_node_types[0];
|
||||
bool has_clause = spec->throws_clause_field && spec->throws_clause_field[0];
|
||||
if (!has_throws && !has_clause) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (has_throws && is_throw_node(node, spec)) {
|
||||
char *exc_name = resolve_exception_name(ctx->arena, node, ctx->source);
|
||||
if (exc_name && exc_name[0]) {
|
||||
if (strlen(exc_name) > MAX_EXCEPTION_NAME_LEN) {
|
||||
exc_name[MAX_EXCEPTION_NAME_LEN] = '\0';
|
||||
}
|
||||
CBMThrow thr;
|
||||
thr.exception_name = exc_name;
|
||||
thr.enclosing_func_qn = state->enclosing_func_qn;
|
||||
cbm_throws_push(&ctx->result->throws, ctx->arena, thr);
|
||||
}
|
||||
}
|
||||
|
||||
extract_throws_clause(ctx, node, spec, state->enclosing_func_qn);
|
||||
}
|
||||
|
||||
void handle_readwrites(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) {
|
||||
if (!spec->assignment_node_types || !spec->assignment_node_types[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cbm_kind_in_set(node, spec->assignment_node_types)) {
|
||||
TSNode left = resolve_write_lhs_node(node);
|
||||
|
||||
if (!ts_node_is_null(left)) {
|
||||
char *name = resolve_lhs_write_name(ctx, left);
|
||||
if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
|
||||
CBMReadWrite rw;
|
||||
rw.var_name = name;
|
||||
rw.is_write = true;
|
||||
rw.enclosing_func_qn = state->enclosing_func_qn;
|
||||
cbm_rw_push(&ctx->result->rw, ctx->arena, rw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
#include "cbm.h"
|
||||
#include "arena.h" // CBMArena
|
||||
#include "helpers.h"
|
||||
#include "lang_specs.h"
|
||||
#include "extract_unified.h"
|
||||
#include "tree_sitter/api.h" // TSNode, ts_node_*
|
||||
#include "foundation/constants.h"
|
||||
#include "extract_node_stack.h"
|
||||
#include <stdint.h> // uint32_t
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
// Extract type from new_expression / object_creation_expression.
|
||||
static const char *extract_new_expr_type(CBMArena *a, TSNode rhs, const char *source) {
|
||||
TSNode type_node = ts_node_child_by_field_name(rhs, TS_FIELD("type"));
|
||||
if (!ts_node_is_null(type_node)) {
|
||||
const char *tk = ts_node_type(type_node);
|
||||
if (strcmp(tk, "type_identifier") == 0 || strcmp(tk, "identifier") == 0 ||
|
||||
strcmp(tk, "simple_identifier") == 0) {
|
||||
return cbm_node_text(a, type_node, source);
|
||||
}
|
||||
if (strcmp(tk, "generic_type") == 0 && ts_node_child_count(type_node) > 0) {
|
||||
return cbm_node_text(a, ts_node_child(type_node, 0), source);
|
||||
}
|
||||
return cbm_node_text(a, type_node, source);
|
||||
}
|
||||
// Fallback: first identifier child
|
||||
for (uint32_t i = 0; i < ts_node_child_count(rhs); i++) {
|
||||
TSNode child = ts_node_child(rhs, i);
|
||||
const char *ck = ts_node_type(child);
|
||||
if (strcmp(ck, "identifier") == 0 || strcmp(ck, "type_identifier") == 0 ||
|
||||
strcmp(ck, "simple_identifier") == 0) {
|
||||
return cbm_node_text(a, child, source);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Extract class/type name from a constructor expression.
|
||||
// e.g., new Foo() -> "Foo", Foo() -> "Foo" (if uppercase), Foo{} -> "Foo"
|
||||
static const char *extract_constructor_type(CBMArena *a, TSNode rhs, const char *source,
|
||||
CBMLanguage lang) {
|
||||
const char *kind = ts_node_type(rhs);
|
||||
|
||||
if (strcmp(kind, "new_expression") == 0 || strcmp(kind, "object_creation_expression") == 0) {
|
||||
return extract_new_expr_type(a, rhs, source);
|
||||
}
|
||||
|
||||
if (strcmp(kind, "call") == 0 || strcmp(kind, "call_expression") == 0) {
|
||||
TSNode func = ts_node_child_by_field_name(rhs, TS_FIELD("function"));
|
||||
if (ts_node_is_null(func) && ts_node_child_count(rhs) > 0) {
|
||||
func = ts_node_child(rhs, 0);
|
||||
}
|
||||
if (!ts_node_is_null(func)) {
|
||||
char *fname = cbm_node_text(a, func, source);
|
||||
if (fname && fname[0] >= 'A' && fname[0] <= 'Z') {
|
||||
return fname;
|
||||
}
|
||||
/* Lower-cased package prefix: Go-style `pb.NewFooClient(...)` and
|
||||
* Java-style `fooGrpc.newBlockingStub(...)`. Accept the qualified
|
||||
* name when the last segment matches a typed-stub factory pattern.
|
||||
* Downstream passes can use this to infer the constructed type. */
|
||||
if (fname && fname[0]) {
|
||||
const char *last = strrchr(fname, '.');
|
||||
last = last ? last + 1 : fname;
|
||||
bool is_factory = false;
|
||||
if ((strncmp(last, "New", 3) == 0 || strncmp(last, "new", 3) == 0) && last[3]) {
|
||||
size_t llen = strlen(last);
|
||||
if ((llen > 6 && strcmp(last + llen - 6, "Client") == 0) ||
|
||||
(llen > 4 && strcmp(last + llen - 4, "Stub") == 0)) {
|
||||
is_factory = true;
|
||||
}
|
||||
}
|
||||
if (is_factory) {
|
||||
return fname;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (strcmp(kind, "composite_literal") == 0) {
|
||||
TSNode type_node = ts_node_child_by_field_name(rhs, TS_FIELD("type"));
|
||||
if (!ts_node_is_null(type_node)) {
|
||||
return cbm_node_text(a, type_node, source);
|
||||
}
|
||||
}
|
||||
|
||||
if (lang == CBM_LANG_RUST && strcmp(kind, "struct_expression") == 0) {
|
||||
TSNode name = ts_node_child_by_field_name(rhs, TS_FIELD("name"));
|
||||
if (!ts_node_is_null(name)) {
|
||||
return cbm_node_text(a, name, source);
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Emit a type assignment if var_name and constructor type are valid.
|
||||
static void try_emit_type_assign(CBMExtractCtx *ctx, TSNode var_node, TSNode rhs_node,
|
||||
const char *func_qn) {
|
||||
char *var_name = cbm_node_text(ctx->arena, var_node, ctx->source);
|
||||
const char *type_name =
|
||||
extract_constructor_type(ctx->arena, rhs_node, ctx->source, ctx->language);
|
||||
if (var_name && var_name[0] && type_name && type_name[0]) {
|
||||
CBMTypeAssign ta;
|
||||
ta.var_name = var_name;
|
||||
ta.type_name = type_name;
|
||||
ta.enclosing_func_qn = func_qn;
|
||||
cbm_typeassign_push(&ctx->result->type_assigns, ctx->arena, ta);
|
||||
}
|
||||
}
|
||||
|
||||
// Process assignment-type nodes (left/right fields with identifier check).
|
||||
static void process_assignment_type_assign(CBMExtractCtx *ctx, TSNode node, const char *func_qn) {
|
||||
TSNode left = ts_node_child_by_field_name(node, TS_FIELD("left"));
|
||||
TSNode right = ts_node_child_by_field_name(node, TS_FIELD("right"));
|
||||
if (ts_node_is_null(right)) {
|
||||
right = ts_node_child_by_field_name(node, TS_FIELD("value"));
|
||||
}
|
||||
if (!ts_node_is_null(left) && !ts_node_is_null(right)) {
|
||||
const char *lk = ts_node_type(left);
|
||||
if (strcmp(lk, "identifier") == 0 || strcmp(lk, "simple_identifier") == 0) {
|
||||
try_emit_type_assign(ctx, left, right, func_qn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process Go short_var_declaration/var_spec nodes.
|
||||
static void process_go_var_type_assign(CBMExtractCtx *ctx, TSNode node, const char *func_qn) {
|
||||
TSNode left = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
||||
if (ts_node_is_null(left)) {
|
||||
left = ts_node_child_by_field_name(node, TS_FIELD("left"));
|
||||
}
|
||||
TSNode right = ts_node_child_by_field_name(node, TS_FIELD("value"));
|
||||
if (ts_node_is_null(right)) {
|
||||
right = ts_node_child_by_field_name(node, TS_FIELD("right"));
|
||||
}
|
||||
if (!ts_node_is_null(left) && !ts_node_is_null(right)) {
|
||||
try_emit_type_assign(ctx, left, right, func_qn);
|
||||
}
|
||||
}
|
||||
|
||||
// Process JS/TS variable_declarator nodes (name + value with identifier check).
|
||||
static void process_declarator_type_assign(CBMExtractCtx *ctx, TSNode node, const char *func_qn) {
|
||||
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
||||
TSNode value_node = ts_node_child_by_field_name(node, TS_FIELD("value"));
|
||||
if (!ts_node_is_null(name_node) && !ts_node_is_null(value_node)) {
|
||||
const char *nk = ts_node_type(name_node);
|
||||
if (strcmp(nk, "identifier") == 0 || strcmp(nk, "simple_identifier") == 0) {
|
||||
try_emit_type_assign(ctx, name_node, value_node, func_qn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process Rust let_declaration nodes (pattern + value).
|
||||
static void process_rust_let_type_assign(CBMExtractCtx *ctx, TSNode node, const char *func_qn) {
|
||||
TSNode pat = ts_node_child_by_field_name(node, TS_FIELD("pattern"));
|
||||
TSNode val = ts_node_child_by_field_name(node, TS_FIELD("value"));
|
||||
if (!ts_node_is_null(pat) && !ts_node_is_null(val)) {
|
||||
if (strcmp(ts_node_type(pat), "identifier") == 0) {
|
||||
try_emit_type_assign(ctx, pat, val, func_qn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process assignment nodes (assignment, short_var_declaration, variable_declarator,
|
||||
// let_declaration).
|
||||
static void process_type_assign_node(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
|
||||
const char *func_qn) {
|
||||
const char *kind = ts_node_type(node);
|
||||
|
||||
if (cbm_kind_in_set(node, spec->assignment_node_types)) {
|
||||
process_assignment_type_assign(ctx, node, func_qn);
|
||||
}
|
||||
if (strcmp(kind, "short_var_declaration") == 0 || strcmp(kind, "var_spec") == 0) {
|
||||
process_go_var_type_assign(ctx, node, func_qn);
|
||||
}
|
||||
if (strcmp(kind, "variable_declarator") == 0) {
|
||||
process_declarator_type_assign(ctx, node, func_qn);
|
||||
}
|
||||
if (strcmp(kind, "let_declaration") == 0 && ctx->language == CBM_LANG_RUST) {
|
||||
process_rust_let_type_assign(ctx, node, func_qn);
|
||||
}
|
||||
}
|
||||
|
||||
// Walk AST for assignment patterns where RHS is a constructor call.
|
||||
static void walk_type_assigns(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
|
||||
TSNodeStack stack;
|
||||
ts_nstack_init(&stack, ctx->arena, 4096);
|
||||
ts_nstack_push(&stack, ctx->arena, root);
|
||||
while (stack.count > 0) {
|
||||
TSNode node = ts_nstack_pop(&stack);
|
||||
process_type_assign_node(ctx, node, spec, cbm_enclosing_func_qn_cached(ctx, node));
|
||||
ts_nstack_push_children(&stack, ctx->arena, node);
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_extract_type_assigns(CBMExtractCtx *ctx) {
|
||||
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
|
||||
if (!spec) {
|
||||
return;
|
||||
}
|
||||
|
||||
walk_type_assigns(ctx, ctx->root, spec);
|
||||
}
|
||||
|
||||
// --- Unified handler ---
|
||||
|
||||
void handle_type_assigns(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
|
||||
WalkState *state) {
|
||||
process_type_assign_node(ctx, node, spec, state->enclosing_func_qn);
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
#include "cbm.h"
|
||||
#include "arena.h" // CBMArena, cbm_arena_sprintf/strndup
|
||||
#include "helpers.h"
|
||||
#include "lang_specs.h"
|
||||
#include "extract_unified.h"
|
||||
#include "tree_sitter/api.h" // TSNode, ts_node_*
|
||||
#include "foundation/constants.h"
|
||||
#include "extract_node_stack.h"
|
||||
|
||||
enum { MIN_TYPE_LEN = 1 };
|
||||
|
||||
#include <stdint.h> // uint32_t
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
// Builtin types that should not generate USES_TYPE edges.
|
||||
static bool is_builtin_type(const char *name) {
|
||||
if (!name || !name[0]) {
|
||||
return true;
|
||||
}
|
||||
if (strlen(name) <= MIN_TYPE_LEN) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Common primitives
|
||||
if (strcmp(name, "int") == 0 || strcmp(name, "string") == 0 || strcmp(name, "bool") == 0 ||
|
||||
strcmp(name, "float") == 0 || strcmp(name, "float32") == 0 ||
|
||||
strcmp(name, "float64") == 0 || strcmp(name, "int8") == 0 || strcmp(name, "int16") == 0 ||
|
||||
strcmp(name, "int32") == 0 || strcmp(name, "int64") == 0 || strcmp(name, "uint") == 0 ||
|
||||
strcmp(name, "uint8") == 0 || strcmp(name, "uint16") == 0 || strcmp(name, "uint32") == 0 ||
|
||||
strcmp(name, "uint64") == 0 || strcmp(name, "uintptr") == 0 || strcmp(name, "byte") == 0 ||
|
||||
strcmp(name, "rune") == 0 || strcmp(name, "void") == 0 || strcmp(name, "char") == 0 ||
|
||||
strcmp(name, "double") == 0 || strcmp(name, "long") == 0 || strcmp(name, "short") == 0 ||
|
||||
strcmp(name, "unsigned") == 0 || strcmp(name, "error") == 0 || strcmp(name, "any") == 0 ||
|
||||
strcmp(name, "interface") == 0 || strcmp(name, "object") == 0 ||
|
||||
strcmp(name, "Object") == 0 || strcmp(name, "None") == 0 || strcmp(name, "nil") == 0 ||
|
||||
strcmp(name, "null") == 0 || strcmp(name, "undefined") == 0 ||
|
||||
strcmp(name, "number") == 0 || strcmp(name, "boolean") == 0 || strcmp(name, "str") == 0 ||
|
||||
strcmp(name, "dict") == 0 || strcmp(name, "list") == 0 || strcmp(name, "tuple") == 0 ||
|
||||
strcmp(name, "set") == 0 || strcmp(name, "complex128") == 0 ||
|
||||
strcmp(name, "complex64") == 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Strip pointer/reference/slice/optional markers from a type name.
|
||||
static const char *clean_type_name(CBMArena *a, const char *name) {
|
||||
if (!name || !name[0]) {
|
||||
return name;
|
||||
}
|
||||
// Skip leading *, &, [], ?
|
||||
while (*name == '*' || *name == '&' || *name == '?' || *name == '[' || *name == ']') {
|
||||
name++;
|
||||
}
|
||||
if (!*name) {
|
||||
return NULL;
|
||||
}
|
||||
// Take only the base type before any < or [
|
||||
size_t len = strlen(name);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (name[i] == '<' || name[i] == '[') {
|
||||
return cbm_arena_strndup(a, name, i);
|
||||
}
|
||||
}
|
||||
// Strip trailing ? (optionals)
|
||||
if (len > 0 && name[len - SKIP_ONE] == '?') {
|
||||
return cbm_arena_strndup(a, name, len - SKIP_ONE);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
// Extract type name from a type annotation node.
|
||||
static const char *extract_type_text(CBMArena *a, TSNode node, const char *source) {
|
||||
enum { MAX_UNWRAP = 8 };
|
||||
for (int depth = 0; depth < MAX_UNWRAP; depth++) {
|
||||
const char *kind = ts_node_type(node);
|
||||
if (strcmp(kind, "type_identifier") == 0 || strcmp(kind, "identifier") == 0 ||
|
||||
strcmp(kind, "simple_identifier") == 0 || strcmp(kind, "name") == 0) {
|
||||
return cbm_node_text(a, node, source);
|
||||
}
|
||||
if (strcmp(kind, "generic_type") == 0 || strcmp(kind, "parameterized_type") == 0) {
|
||||
if (ts_node_child_count(node) > 0) {
|
||||
return cbm_node_text(a, ts_node_child(node, 0), source);
|
||||
}
|
||||
}
|
||||
if (strcmp(kind, "pointer_type") == 0 || strcmp(kind, "reference_type") == 0 ||
|
||||
strcmp(kind, "slice_type") == 0 || strcmp(kind, "array_type") == 0) {
|
||||
TSNode elem = ts_node_child_by_field_name(node, TS_FIELD("element"));
|
||||
if (!ts_node_is_null(elem)) {
|
||||
node = elem;
|
||||
continue;
|
||||
}
|
||||
TSNode type_node = ts_node_child_by_field_name(node, TS_FIELD("type"));
|
||||
if (!ts_node_is_null(type_node)) {
|
||||
node = type_node;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return clean_type_name(a, cbm_node_text(a, node, source));
|
||||
}
|
||||
|
||||
// Add a type reference for a function.
|
||||
static void add_type_ref(CBMExtractCtx *ctx, const char *type_name, const char *func_qn) {
|
||||
if (!type_name || !type_name[0]) {
|
||||
return;
|
||||
}
|
||||
type_name = clean_type_name(ctx->arena, type_name);
|
||||
if (!type_name || !type_name[0]) {
|
||||
return;
|
||||
}
|
||||
if (is_builtin_type(type_name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
CBMTypeRef tr;
|
||||
tr.type_name = type_name;
|
||||
tr.enclosing_func_qn = func_qn;
|
||||
cbm_typerefs_push(&ctx->result->type_refs, ctx->arena, tr);
|
||||
}
|
||||
|
||||
// Extract parameter types from a parameters/formal_parameters node.
|
||||
static void extract_param_type_refs(CBMExtractCtx *ctx, TSNode params, const char *func_qn) {
|
||||
uint32_t count = ts_node_child_count(params);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
TSNode child = ts_node_child(params, i);
|
||||
TSNode type_node = ts_node_child_by_field_name(child, TS_FIELD("type"));
|
||||
if (!ts_node_is_null(type_node)) {
|
||||
const char *tname = extract_type_text(ctx->arena, type_node, ctx->source);
|
||||
add_type_ref(ctx, tname, func_qn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract return type references.
|
||||
static void extract_return_type_refs(CBMExtractCtx *ctx, TSNode func_node, const char *func_qn) {
|
||||
const char *fields[] = {"result", "return_type", "type", NULL};
|
||||
for (const char **f = fields; *f; f++) {
|
||||
TSNode rt = ts_node_child_by_field_name(func_node, *f, (uint32_t)strlen(*f));
|
||||
if (!ts_node_is_null(rt)) {
|
||||
const char *tname = extract_type_text(ctx->arena, rt, ctx->source);
|
||||
add_type_ref(ctx, tname, func_qn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract type ref from a node whose "type" field contains the type.
|
||||
static void extract_type_field_ref(CBMExtractCtx *ctx, TSNode node, const char *func_qn) {
|
||||
TSNode type_node = ts_node_child_by_field_name(node, TS_FIELD("type"));
|
||||
if (!ts_node_is_null(type_node)) {
|
||||
add_type_ref(ctx, extract_type_text(ctx->arena, type_node, ctx->source), func_qn);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract type refs from TS/JS type_arguments or type_annotation children.
|
||||
static void extract_ts_body_type_refs(CBMExtractCtx *ctx, TSNode node, const char *kind,
|
||||
const char *func_qn) {
|
||||
if (strcmp(kind, "as_expression") == 0 || strcmp(kind, "satisfies_expression") == 0) {
|
||||
extract_type_field_ref(ctx, node, func_qn);
|
||||
} else if (strcmp(kind, "type_arguments") == 0) {
|
||||
uint32_t nc = ts_node_child_count(node);
|
||||
for (uint32_t i = 0; i < nc; i++) {
|
||||
TSNode child = ts_node_child(node, i);
|
||||
if (strcmp(ts_node_type(child), "type_identifier") == 0 ||
|
||||
strcmp(ts_node_type(child), "identifier") == 0) {
|
||||
add_type_ref(ctx, cbm_node_text(ctx->arena, child, ctx->source), func_qn);
|
||||
}
|
||||
}
|
||||
} else if (strcmp(kind, "variable_declarator") == 0) {
|
||||
uint32_t nc = ts_node_child_count(node);
|
||||
for (uint32_t i = 0; i < nc; i++) {
|
||||
TSNode child = ts_node_child(node, i);
|
||||
if (strcmp(ts_node_type(child), "type_annotation") == 0) {
|
||||
if (ts_node_child_count(child) > 0) {
|
||||
TSNode inner = ts_node_child(child, ts_node_child_count(child) - SKIP_ONE);
|
||||
add_type_ref(ctx, extract_type_text(ctx->arena, inner, ctx->source), func_qn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract type refs from Java generic_type children.
|
||||
static void extract_java_body_type_refs(CBMExtractCtx *ctx, TSNode node, const char *kind,
|
||||
const char *func_qn) {
|
||||
if (strcmp(kind, "local_variable_declaration") == 0 || strcmp(kind, "cast_expression") == 0) {
|
||||
extract_type_field_ref(ctx, node, func_qn);
|
||||
} else if (strcmp(kind, "generic_type") == 0) {
|
||||
uint32_t nc = ts_node_child_count(node);
|
||||
for (uint32_t i = 0; i < nc; i++) {
|
||||
TSNode child = ts_node_child(node, i);
|
||||
if (strcmp(ts_node_type(child), "type_arguments") == 0) {
|
||||
uint32_t nta = ts_node_child_count(child);
|
||||
for (uint32_t j = 0; j < nta; j++) {
|
||||
TSNode ta = ts_node_child(child, j);
|
||||
if (strcmp(ts_node_type(ta), "type_identifier") == 0) {
|
||||
add_type_ref(ctx, cbm_node_text(ctx->arena, ta, ctx->source), func_qn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process a single node for body-level type references.
|
||||
static void process_body_type_ref(CBMExtractCtx *ctx, TSNode node, const char *func_qn) {
|
||||
const char *kind = ts_node_type(node);
|
||||
|
||||
switch (ctx->language) {
|
||||
case CBM_LANG_GO:
|
||||
if (strcmp(kind, "var_spec") == 0 || strcmp(kind, "type_assertion") == 0 ||
|
||||
strcmp(kind, "type_conversion_expression") == 0 ||
|
||||
strcmp(kind, "composite_literal") == 0) {
|
||||
extract_type_field_ref(ctx, node, func_qn);
|
||||
}
|
||||
break;
|
||||
case CBM_LANG_TYPESCRIPT:
|
||||
case CBM_LANG_TSX:
|
||||
extract_ts_body_type_refs(ctx, node, kind, func_qn);
|
||||
break;
|
||||
case CBM_LANG_JAVA:
|
||||
extract_java_body_type_refs(ctx, node, kind, func_qn);
|
||||
break;
|
||||
case CBM_LANG_PYTHON:
|
||||
if (strcmp(kind, "assignment") == 0) {
|
||||
TSNode type_node = ts_node_child_by_field_name(node, TS_FIELD("type"));
|
||||
if (!ts_node_is_null(type_node)) {
|
||||
add_type_ref(ctx, cbm_node_text(ctx->arena, type_node, ctx->source), func_qn);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CBM_LANG_RUST:
|
||||
if (strcmp(kind, "let_declaration") == 0 || strcmp(kind, "type_cast_expression") == 0) {
|
||||
extract_type_field_ref(ctx, node, func_qn);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk function body for type references (casts, type assertions, local var types, generics).
|
||||
static void walk_body_type_refs(CBMExtractCtx *ctx, TSNode root, const char *func_qn) {
|
||||
TSNodeStack stack;
|
||||
ts_nstack_init(&stack, ctx->arena, 4096);
|
||||
ts_nstack_push(&stack, ctx->arena, root);
|
||||
while (stack.count > 0) {
|
||||
TSNode node = ts_nstack_pop(&stack);
|
||||
process_body_type_ref(ctx, node, func_qn);
|
||||
uint32_t count = ts_node_child_count(node);
|
||||
for (int i = (int)count - SKIP_ONE; i >= 0; i--) {
|
||||
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Walk AST for function nodes, extract type references from signatures and bodies.
|
||||
/* Process a single function node: extract type refs from signature + body. */
|
||||
static void process_func_type_refs(CBMExtractCtx *ctx, TSNode node) {
|
||||
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
||||
if (ts_node_is_null(name_node)) {
|
||||
return;
|
||||
}
|
||||
char *func_name = cbm_node_text(ctx->arena, name_node, ctx->source);
|
||||
if (!func_name || !func_name[0]) {
|
||||
return;
|
||||
}
|
||||
const char *func_qn = cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, func_name);
|
||||
TSNode params = ts_node_child_by_field_name(node, TS_FIELD("parameters"));
|
||||
if (!ts_node_is_null(params)) {
|
||||
extract_param_type_refs(ctx, params, func_qn);
|
||||
}
|
||||
extract_return_type_refs(ctx, node, func_qn);
|
||||
TSNode body = ts_node_child_by_field_name(node, TS_FIELD("body"));
|
||||
if (ts_node_is_null(body)) {
|
||||
body = ts_node_child_by_field_name(node, TS_FIELD("block"));
|
||||
}
|
||||
if (!ts_node_is_null(body)) {
|
||||
walk_body_type_refs(ctx, body, func_qn);
|
||||
}
|
||||
}
|
||||
|
||||
static void walk_type_refs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
|
||||
if (!spec->function_node_types || !spec->function_node_types[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
TSNodeStack stack;
|
||||
ts_nstack_init(&stack, ctx->arena, 4096);
|
||||
ts_nstack_push(&stack, ctx->arena, root);
|
||||
|
||||
while (stack.count > 0) {
|
||||
TSNode node = ts_nstack_pop(&stack);
|
||||
|
||||
if (cbm_kind_in_set(node, spec->function_node_types)) {
|
||||
process_func_type_refs(ctx, node);
|
||||
continue;
|
||||
}
|
||||
uint32_t count = ts_node_child_count(node);
|
||||
for (int i = (int)count - SKIP_ONE; i >= 0; i--) {
|
||||
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_extract_type_refs(CBMExtractCtx *ctx) {
|
||||
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
|
||||
if (!spec) {
|
||||
return;
|
||||
}
|
||||
|
||||
walk_type_refs(ctx, ctx->root, spec);
|
||||
}
|
||||
|
||||
// --- Unified handler ---
|
||||
// For function nodes: extract signature type refs (params + return).
|
||||
// For body-level type-bearing nodes: extract body type refs.
|
||||
// The cursor visits both, so this single handler replaces the old
|
||||
// walk_type_refs + walk_body_type_refs split.
|
||||
|
||||
// Extract signature type refs from a function node.
|
||||
static void extract_signature_type_refs(CBMExtractCtx *ctx, TSNode node, WalkState *state) {
|
||||
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
||||
if (ts_node_is_null(name_node)) {
|
||||
return;
|
||||
}
|
||||
char *func_name = cbm_node_text(ctx->arena, name_node, ctx->source);
|
||||
if (!func_name || !func_name[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *func_qn;
|
||||
if (state->enclosing_class_qn) {
|
||||
func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, func_name);
|
||||
} else {
|
||||
func_qn = cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, func_name);
|
||||
}
|
||||
|
||||
TSNode params = ts_node_child_by_field_name(node, TS_FIELD("parameters"));
|
||||
if (!ts_node_is_null(params)) {
|
||||
extract_param_type_refs(ctx, params, func_qn);
|
||||
}
|
||||
extract_return_type_refs(ctx, node, func_qn);
|
||||
}
|
||||
|
||||
void handle_type_refs(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) {
|
||||
if (!spec->function_node_types || !spec->function_node_types[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cbm_kind_in_set(node, spec->function_node_types)) {
|
||||
extract_signature_type_refs(ctx, node, state);
|
||||
return;
|
||||
}
|
||||
|
||||
process_body_type_ref(ctx, node, state->enclosing_func_qn);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
#ifndef CBM_EXTRACT_UNIFIED_H
|
||||
#define CBM_EXTRACT_UNIFIED_H
|
||||
|
||||
#include "cbm.h"
|
||||
#include "lang_specs.h"
|
||||
|
||||
// Scope kinds for the walk state stack.
|
||||
#define SCOPE_FUNC 1
|
||||
#define SCOPE_CLASS 2
|
||||
#define SCOPE_CALL 3
|
||||
#define SCOPE_IMPORT 4
|
||||
#define SCOPE_LOOP 5
|
||||
#define SCOPE_BRANCH 6
|
||||
|
||||
#define MAX_SCOPES 64
|
||||
|
||||
// WalkState tracks scope context during the unified cursor walk.
|
||||
// Replaces parent-chain walks for enclosing_func_qn, inside_call, etc.
|
||||
typedef struct {
|
||||
const char *enclosing_func_qn; // current function QN (module_qn at top level)
|
||||
const char *enclosing_class_qn; // current class QN (NULL outside class)
|
||||
bool inside_call; // within a call_node_types subtree
|
||||
bool inside_import; // within an import_node_types subtree
|
||||
int loop_depth; // count of enclosing loop scopes (for bottleneck metrics)
|
||||
int branch_depth; // count of enclosing branch scopes
|
||||
|
||||
struct {
|
||||
const char *qn;
|
||||
uint32_t depth;
|
||||
uint8_t kind;
|
||||
} scopes[MAX_SCOPES];
|
||||
int scope_top;
|
||||
} WalkState;
|
||||
|
||||
// Per-node handler prototypes. Each is called once per node during the
|
||||
// unified cursor walk, replacing the old recursive walk_* functions.
|
||||
void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
|
||||
void handle_usages(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
|
||||
void handle_throws(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
|
||||
void handle_readwrites(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
|
||||
void handle_type_refs(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state);
|
||||
void handle_env_accesses(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
|
||||
WalkState *state);
|
||||
void handle_type_assigns(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
|
||||
WalkState *state);
|
||||
|
||||
// Single-pass extraction using TSTreeCursor. Visits every node once,
|
||||
// dispatching to all handlers per node. Replaces the 7 separate walk_*
|
||||
// functions for calls/usages/throws/readwrites/type_refs/env_accesses/type_assigns.
|
||||
// Definitions and imports stay as separate passes (different recursion patterns).
|
||||
void cbm_extract_unified(CBMExtractCtx *ctx);
|
||||
|
||||
#endif // CBM_EXTRACT_UNIFIED_H
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "cbm.h"
|
||||
#include "helpers.h"
|
||||
#include "lang_specs.h"
|
||||
#include "extract_unified.h"
|
||||
#include "tree_sitter/api.h" // TSNode, ts_node_*
|
||||
#include "foundation/constants.h"
|
||||
#include "extract_node_stack.h"
|
||||
|
||||
enum { MAX_PARENT_DEPTH = 10, LAST_IDX = 1 };
|
||||
#include <stdint.h> // uint32_t
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
// Forward declaration
|
||||
static void walk_usages(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec);
|
||||
|
||||
// Check if a node is inside a call expression (to avoid double-counting as usage)
|
||||
static bool is_inside_call(TSNode node, const CBMLangSpec *spec) {
|
||||
TSNode cur = ts_node_parent(node);
|
||||
int depth = 0;
|
||||
while (!ts_node_is_null(cur) && depth < MAX_PARENT_DEPTH) {
|
||||
if (cbm_kind_in_set(cur, spec->call_node_types)) {
|
||||
return true;
|
||||
}
|
||||
cur = ts_node_parent(cur);
|
||||
depth++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if a node is inside an import statement
|
||||
static bool is_inside_import(TSNode node, const CBMLangSpec *spec) {
|
||||
if (!spec->import_node_types || !spec->import_node_types[0]) {
|
||||
return false;
|
||||
}
|
||||
TSNode cur = ts_node_parent(node);
|
||||
int depth = 0;
|
||||
while (!ts_node_is_null(cur) && depth < MAX_PARENT_DEPTH) {
|
||||
if (cbm_kind_in_set(cur, spec->import_node_types)) {
|
||||
return true;
|
||||
}
|
||||
cur = ts_node_parent(cur);
|
||||
depth++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is this an identifier-like node that represents a reference?
|
||||
static bool is_reference_node(TSNode node, CBMLanguage lang) {
|
||||
const char *kind = ts_node_type(node);
|
||||
|
||||
// Common identifier types across languages
|
||||
if (strcmp(kind, "identifier") == 0 || strcmp(kind, "simple_identifier") == 0 ||
|
||||
strcmp(kind, "type_identifier") == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Language-specific reference types
|
||||
switch (lang) {
|
||||
case CBM_LANG_GO:
|
||||
return strcmp(kind, "field_identifier") == 0 || strcmp(kind, "package_identifier") == 0;
|
||||
case CBM_LANG_PYTHON:
|
||||
return strcmp(kind, "attribute") == 0;
|
||||
case CBM_LANG_RUST:
|
||||
return strcmp(kind, "field_identifier") == 0 || strcmp(kind, "scoped_identifier") == 0;
|
||||
case CBM_LANG_HASKELL:
|
||||
return strcmp(kind, "variable") == 0 || strcmp(kind, "constructor") == 0;
|
||||
case CBM_LANG_OCAML:
|
||||
return strcmp(kind, "value_path") == 0 || strcmp(kind, "constructor_path") == 0;
|
||||
case CBM_LANG_ERLANG:
|
||||
return strcmp(kind, "atom") == 0 || strcmp(kind, "var") == 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a reference node is a definition name (the "name" field of its parent).
|
||||
static bool is_definition_name(TSNode node) {
|
||||
TSNode parent = ts_node_parent(node);
|
||||
if (ts_node_is_null(parent)) {
|
||||
return false;
|
||||
}
|
||||
TSNode name_field = ts_node_child_by_field_name(parent, TS_FIELD("name"));
|
||||
return !ts_node_is_null(name_field) &&
|
||||
ts_node_start_byte(name_field) == ts_node_start_byte(node) &&
|
||||
ts_node_end_byte(name_field) == ts_node_end_byte(node);
|
||||
}
|
||||
|
||||
// Try to emit a usage for a reference node. Returns early if the node should be skipped.
|
||||
static void try_emit_usage(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
|
||||
if (!is_reference_node(node, ctx->language)) {
|
||||
return;
|
||||
}
|
||||
if (is_inside_call(node, spec) || is_inside_import(node, spec)) {
|
||||
return;
|
||||
}
|
||||
if (is_definition_name(node)) {
|
||||
return;
|
||||
}
|
||||
char *name = cbm_node_text(ctx->arena, node, ctx->source);
|
||||
if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
|
||||
CBMUsage usage;
|
||||
usage.ref_name = name;
|
||||
usage.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node);
|
||||
cbm_usages_push(&ctx->result->usages, ctx->arena, usage);
|
||||
}
|
||||
}
|
||||
|
||||
// Iterative usage walker — explicit stack
|
||||
static void walk_usages(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
|
||||
TSNodeStack stack;
|
||||
ts_nstack_init(&stack, ctx->arena, 4096);
|
||||
ts_nstack_push(&stack, ctx->arena, root);
|
||||
|
||||
while (stack.count > 0) {
|
||||
TSNode node = ts_nstack_pop(&stack);
|
||||
try_emit_usage(ctx, node, spec);
|
||||
uint32_t count = ts_node_child_count(node);
|
||||
for (int i = (int)count - LAST_IDX; i >= 0; i--) {
|
||||
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_extract_usages(CBMExtractCtx *ctx) {
|
||||
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
|
||||
if (!spec) {
|
||||
return;
|
||||
}
|
||||
|
||||
walk_usages(ctx, ctx->root, spec);
|
||||
}
|
||||
|
||||
// --- Unified handler: called once per node by the cursor walk ---
|
||||
// Uses WalkState flags instead of parent-chain walks for O(1) context checks.
|
||||
|
||||
void handle_usages(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) {
|
||||
(void)spec;
|
||||
if (!is_reference_node(node, ctx->language)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if inside a call (already counted as CALLS edge) — O(1) via state
|
||||
if (state->inside_call) {
|
||||
return;
|
||||
}
|
||||
// Skip if inside an import
|
||||
if (state->inside_import) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if it's a definition name (left side of assignment, function name)
|
||||
TSNode parent = ts_node_parent(node);
|
||||
if (!ts_node_is_null(parent)) {
|
||||
TSNode name_field = ts_node_child_by_field_name(parent, TS_FIELD("name"));
|
||||
if (!ts_node_is_null(name_field) &&
|
||||
ts_node_start_byte(name_field) == ts_node_start_byte(node) &&
|
||||
ts_node_end_byte(name_field) == ts_node_end_byte(node)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
char *name = cbm_node_text(ctx->arena, node, ctx->source);
|
||||
if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
|
||||
CBMUsage usage;
|
||||
usage.ref_name = name;
|
||||
usage.enclosing_func_qn = state->enclosing_func_qn;
|
||||
cbm_usages_push(&ctx->result->usages, ctx->arena, usage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: ada
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/ada/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: agda
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/agda/parser.c"
|
||||
#include "vendored/grammars/agda/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: apex
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/apex/parser.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: assembly
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/assembly/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: astro
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/astro/parser.c"
|
||||
#include "vendored/grammars/astro/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: awk
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/awk/parser.c"
|
||||
#include "vendored/grammars/awk/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: bash
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/bash/parser.c"
|
||||
#include "vendored/grammars/bash/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: beancount
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/beancount/parser.c"
|
||||
#include "vendored/grammars/beancount/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: bibtex
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/bibtex/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: bicep
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/bicep/parser.c"
|
||||
#include "vendored/grammars/bicep/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: bitbake
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/bitbake/parser.c"
|
||||
#include "vendored/grammars/bitbake/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: blade
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/blade/parser.c"
|
||||
#include "vendored/grammars/blade/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: c
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/c/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: c_sharp
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/c_sharp/parser.c"
|
||||
#include "vendored/grammars/c_sharp/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: cairo
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/cairo/parser.c"
|
||||
#include "vendored/grammars/cairo/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: capnp
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/capnp/parser.c"
|
||||
@@ -0,0 +1,6 @@
|
||||
// Vendored tree-sitter grammar: cfml (CFML tag dialect — .cfm templates)
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
// scanner.c pulls in the shared ../../common/scanner.h (and tag.h), which is
|
||||
// all-static; tag.c is therefore NOT included here (it would redefine it).
|
||||
#include "vendored/grammars/cfml/parser.c"
|
||||
#include "vendored/grammars/cfml/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: cfscript (CFML script dialect — .cfc components)
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/cfscript/parser.c"
|
||||
#include "vendored/grammars/cfscript/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: clojure
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/clojure/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: cmake
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/cmake/parser.c"
|
||||
#include "vendored/grammars/cmake/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: cobol
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/cobol/parser.c"
|
||||
#include "vendored/grammars/cobol/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: commonlisp
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/commonlisp/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: cpp
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/cpp/parser.c"
|
||||
#include "vendored/grammars/cpp/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: crystal
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/crystal/parser.c"
|
||||
#include "vendored/grammars/crystal/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: css
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/css/parser.c"
|
||||
#include "vendored/grammars/css/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: csv
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/csv/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: cuda
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/cuda/parser.c"
|
||||
#include "vendored/grammars/cuda/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: d
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/d/parser.c"
|
||||
#include "vendored/grammars/d/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: dart
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/dart/parser.c"
|
||||
#include "vendored/grammars/dart/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: devicetree
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/devicetree/parser.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: diff
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/diff/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: dockerfile
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/dockerfile/parser.c"
|
||||
#include "vendored/grammars/dockerfile/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: dotenv
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/dotenv/parser.c"
|
||||
#include "vendored/grammars/dotenv/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: elisp (Emacs Lisp)
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/elisp/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: elixir
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/elixir/parser.c"
|
||||
#include "vendored/grammars/elixir/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: elm
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/elm/parser.c"
|
||||
#include "vendored/grammars/elm/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: erlang
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/erlang/parser.c"
|
||||
#include "vendored/grammars/erlang/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: fennel
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/fennel/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: fish
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/fish/parser.c"
|
||||
#include "vendored/grammars/fish/scanner.c"
|
||||
@@ -0,0 +1,2 @@
|
||||
// Vendored tree-sitter grammar: form
|
||||
#include "vendored/grammars/form/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: fortran
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/fortran/parser.c"
|
||||
#include "vendored/grammars/fortran/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: fsharp
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/fsharp/parser.c"
|
||||
#include "vendored/grammars/fsharp/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: func
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/func/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: gdscript
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/gdscript/parser.c"
|
||||
#include "vendored/grammars/gdscript/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: gitattributes
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/gitattributes/parser.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: gitignore
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/gitignore/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: gleam
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/gleam/parser.c"
|
||||
#include "vendored/grammars/gleam/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: glsl
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/glsl/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: gn
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/gn/parser.c"
|
||||
#include "vendored/grammars/gn/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: go
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/go/parser.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: gomod
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/gomod/parser.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: gotemplate
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/gotemplate/parser.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: graphql
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/graphql/parser.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: groovy
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/groovy/parser.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: hare
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/hare/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: haskell
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/haskell/parser.c"
|
||||
#include "vendored/grammars/haskell/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: hcl
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/hcl/parser.c"
|
||||
#include "vendored/grammars/hcl/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: hlsl
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/hlsl/parser.c"
|
||||
#include "vendored/grammars/hlsl/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: html
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/html/parser.c"
|
||||
#include "vendored/grammars/html/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: hyprlang
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/hyprlang/parser.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: ini
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/ini/parser.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: ispc
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/ispc/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: janet
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/janet/parser.c"
|
||||
#include "vendored/grammars/janet/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: java
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/java/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: javascript
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/javascript/parser.c"
|
||||
#include "vendored/grammars/javascript/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: jinja2
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/jinja2/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: jsdoc
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/jsdoc/parser.c"
|
||||
#include "vendored/grammars/jsdoc/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: json
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/json/parser.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: json5
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/json5/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: jsonnet
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/jsonnet/parser.c"
|
||||
#include "vendored/grammars/jsonnet/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: julia
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/julia/parser.c"
|
||||
#include "vendored/grammars/julia/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: just
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/just/parser.c"
|
||||
#include "vendored/grammars/just/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: kconfig
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/kconfig/parser.c"
|
||||
#include "vendored/grammars/kconfig/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: kdl
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/kdl/parser.c"
|
||||
#include "vendored/grammars/kdl/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: kotlin
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/kotlin/parser.c"
|
||||
#include "vendored/grammars/kotlin/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: lean
|
||||
#include "vendored/grammars/lean/parser.c"
|
||||
#include "vendored/grammars/lean/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: linkerscript
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/linkerscript/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: liquid
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/liquid/parser.c"
|
||||
#include "vendored/grammars/liquid/scanner.c"
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: llvm
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/llvm/parser.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: lua
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/lua/parser.c"
|
||||
#include "vendored/grammars/lua/scanner.c"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Vendored tree-sitter grammar: luau
|
||||
// Each grammar compiled as separate unit (conflicting static symbols).
|
||||
#include "vendored/grammars/luau/parser.c"
|
||||
#include "vendored/grammars/luau/scanner.c"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user