Skip to content
Open
426 changes: 408 additions & 18 deletions internal/cbm/cbm.c

Large diffs are not rendered by default.

123 changes: 123 additions & 0 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,79 @@ typedef struct {
bool is_default; // ES default import (`import X from "Y"`), JS/TS only (#1916)
} CBMImport;

/* Content-only ordered Python namespace facts; no resolved target identities. */
#define CBM_PY_NAMESPACE_FACTS_VERSION 1u

typedef enum {
CBM_PY_NS_NOT_CAPTURED = 0,
CBM_PY_NS_NOT_APPLICABLE = 1,
CBM_PY_NS_COMPLETE = 2,
CBM_PY_NS_INCOMPLETE = 3
} CBMPyNamespaceStatus;
typedef enum {
CBM_PY_NS_FAILURE_NONE = 0,
CBM_PY_NS_FAILURE_INVALID_INPUT = 1,
CBM_PY_NS_FAILURE_ALLOCATION = 2,
CBM_PY_NS_FAILURE_LIMIT = 3
} CBMPyNamespaceFailure;
typedef enum {
CBM_PY_NS_OWN_DEF = 1,
CBM_PY_NS_IMPORT_MODULE = 2,
CBM_PY_NS_IMPORT_NAME = 3,
CBM_PY_NS_IMPORT_STAR = 4,
CBM_PY_NS_SHADOW = 5,
CBM_PY_NS_DELETE = 6,
CBM_PY_NS_UNKNOWN = 7,
CBM_PY_NS_ALL_SET = 8,
CBM_PY_NS_ALL_APPEND = 9,
CBM_PY_NS_ALL_UNKNOWN = 10,
CBM_PY_NS_ALL_DELETE = 11
} CBMPyNamespaceFactKind;
typedef enum {
CBM_PY_NS_DEF_NONE = 0,
CBM_PY_NS_DEF_FUNCTION = 1,
CBM_PY_NS_DEF_CLASS = 2
} CBMPyNamespaceDefKind;
typedef enum {
CBM_PY_NS_SEQUENCE_NONE = 0,
CBM_PY_NS_SEQUENCE_LIST = 1,
CBM_PY_NS_SEQUENCE_TUPLE = 2
} CBMPyNamespaceSequenceKind;
typedef enum {
CBM_PY_NS_REASON_NONE = 0,
CBM_PY_NS_REASON_VALUE = 1,
CBM_PY_NS_REASON_DECORATED = 2,
CBM_PY_NS_REASON_UNPROVEN_CLASS = 3,
CBM_PY_NS_REASON_COMPOUND = 4,
CBM_PY_NS_REASON_EFFECT = 5,
CBM_PY_NS_REASON_UNSUPPORTED = 6,
CBM_PY_NS_REASON_PARSE = 7
} CBMPyNamespaceUnknownReason;
enum { CBM_PY_NS_IMPORT_BINDS_ROOT = 1u << 0 };

typedef struct {
CBMPyNamespaceFactKind kind;
CBMPyNamespaceDefKind def_kind;
CBMPyNamespaceSequenceKind sequence_kind;
CBMPyNamespaceUnknownReason reason;
uint32_t flags;
uint32_t relative_level;
const char *local_name;
const char *module_name;
const char *member_name;
const char **names;
int name_count;
} CBMPyNamespaceFact;
typedef struct {
uint32_t version;
CBMLanguage language;
CBMPyNamespaceStatus status;
CBMPyNamespaceFailure failure;
CBMPyNamespaceFact *items;
int count;
int cap;
} CBMPyNamespaceFacts;

typedef enum {
CBM_USAGE_VALUE = 0,
CBM_USAGE_CALL_REFERENCE,
Expand Down Expand Up @@ -405,6 +478,17 @@ typedef struct {
CBMChannelDirection direction;
} CBMChannel;

/* Python: one annotated instance field of a class -- `x: T` or `x: T = v` in
* the class body, `self.x: T = v` in __init__, or `self.x = p` where `p` is an
* annotated __init__ parameter. Not a graph node: it only carries the field's
* declared type to the cross-file LSP, so `obj.x.m()` on a class imported
* from another file can be typed (#1277). */
typedef struct {
const char *class_qn; // QN of the owning class
const char *field_name; // attribute name
const char *type_text; // raw annotation text, resolved later per file
} CBMFieldType;

// Rust: impl Trait for Struct
typedef struct {
const char *trait_name; // trait name (raw text)
Expand Down Expand Up @@ -439,6 +523,15 @@ typedef struct {
int cap;
} CBMResolvedCallArray;

/* Refinement status: existing parser/no-op exits are separate from these
* explicit evaluator failures. The caller retains prior completed output. */
typedef enum {
CBM_LSP_COMPLETE = 0,
CBM_LSP_MEMO_FAILED,
CBM_LSP_DEPTH_EXCEEDED,
CBM_LSP_SCOPE_FAILED,
} CBMLSPStatus;

// Growable arrays used during extraction.
typedef struct {
CBMDefinition *items;
Expand Down Expand Up @@ -518,13 +611,20 @@ typedef struct {
int cap;
} CBMChannelArray;

typedef struct {
CBMFieldType *items;
int count;
int cap;
} CBMFieldTypeArray;

// Full extraction result for one file.
typedef struct CBMFileResult {
CBMArena arena; // owns local memory; composites may also retain child arenas below

CBMDefArray defs;
CBMCallArray calls;
CBMImportArray imports;
CBMPyNamespaceFacts py_namespace;
CBMUsageArray usages;
CBMThrowArray throws;
CBMRWArray rw;
Expand All @@ -536,6 +636,7 @@ typedef struct CBMFileResult {
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
CBMFieldTypeArray field_types; // Python: annotated instance fields (#1277)

const char *module_qn; // module qualified name
const char *namespace_name; // declared namespace/package (Java/Kotlin/C#/PHP), NULL if none
Expand Down Expand Up @@ -882,6 +983,15 @@ int cbm_macro_extraction_enabled(void);

// --- Internal helpers used by extractors ---

CBMPyNamespaceStatus cbm_extract_python_namespace_facts(CBMExtractCtx *ctx);
bool cbm_py_namespace_facts_valid(const CBMPyNamespaceFacts *facts);
bool cbm_py_namespace_facts_copy(CBMArena *arena, const CBMPyNamespaceFacts *source,
CBMPyNamespaceFacts *out);
#ifdef CBM_ENABLE_TEST_SEAMS
void cbm_py_namespace_test_capture_fail_after(int successful_allocations);
void cbm_py_namespace_test_copy_fail_after(int successful_allocations);
#endif

// 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);
Expand All @@ -892,9 +1002,17 @@ 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_fieldtype_push(CBMFieldTypeArray *arr, CBMArena *a, CBMFieldType ft);
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);

#ifdef CBM_ENABLE_TEST_SEAMS
/* #1527 seam: how many of result's calls get a different innermost enclosing
* Function/Method from the call-context sweep than from the reference
* per-call scan (0 = identical), or -1 when the sweep could not allocate. */
int cbm_test_enclosing_sweep_mismatches(const CBMFileResult *result);
#endif
void cbm_resolvedcall_push(CBMResolvedCallArray *arr, CBMArena *a, CBMResolvedCall rc);
void cbm_channels_push(CBMChannelArray *arr, CBMArena *a, CBMChannel ch);

Expand Down Expand Up @@ -948,4 +1066,9 @@ bool cbm_label_is_relation(const char *label);
// `label` may be NULL (returns false). Defined in helpers.c.
bool cbm_label_is_registry_symbol(const char *label);

/* Python symbol scope only: callers must establish the language explicitly.
* A trailing .__init__ is omitted when a nonempty prefix precedes it; raw
* Module/file identity is unchanged. NULL returns zero. */
size_t cbm_fqn_symbol_scope_len(const char *module_qn);

#endif // CBM_H
167 changes: 167 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -4584,6 +4584,169 @@ static bool extract_sql_ddl_class_def(CBMExtractCtx *ctx, TSNode node, const cha
return true;
}

/* ── Python annotated instance fields (#1277) ─────────────────────
* Exported as result->field_types (never graph nodes) so the cross-file LSP
* can type `obj.x` when obj's class lives in another file. Only DECLARED
* types count: a class-body annotation, a `self.x: T = v` annotation in
* __init__, or `self.x = p` where p is an annotated __init__ parameter. An
* unannotated right-hand side is never guessed at. */

static void py_push_field_type(CBMExtractCtx *ctx, const char *class_qn, TSNode name_node,
const char *type_text) {
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
if (!name || !name[0] || !type_text || !type_text[0]) {
return;
}
CBMFieldType ft = {.class_qn = class_qn, .field_name = name, .type_text = type_text};
cbm_fieldtype_push(&ctx->result->field_types, ctx->arena, ft);
}

/* The assignment inside an expression_statement, or a null node. */
static TSNode py_statement_assignment(TSNode stmt) {
TSNode null_node = {0};
if (strcmp(ts_node_type(stmt), "expression_statement") != 0 ||
ts_node_named_child_count(stmt) == 0) {
return null_node;
}
TSNode inner = ts_node_named_child(stmt, 0);
return strcmp(ts_node_type(inner), "assignment") == 0 ? inner : null_node;
}

/* Annotation text of the __init__ parameter called `name`, or NULL. */
static const char *py_init_param_annotation(CBMExtractCtx *ctx, TSNode params, const char *name) {
uint32_t n = ts_node_named_child_count(params);
for (uint32_t i = 0; i < n; i++) {
TSNode p = ts_node_named_child(params, i);
const char *pk = ts_node_type(p);
TSNode pname = {0};
if (strcmp(pk, "typed_default_parameter") == 0) {
pname = ts_node_child_by_field_name(p, TS_FIELD("name"));
} else if (strcmp(pk, "typed_parameter") == 0 && ts_node_named_child_count(p) > 0) {
pname = ts_node_named_child(p, 0);
} else {
continue;
}
TSNode ptype = ts_node_child_by_field_name(p, TS_FIELD("type"));
if (ts_node_is_null(pname) || ts_node_is_null(ptype) ||
strcmp(ts_node_type(pname), "identifier") != 0) {
continue;
}
char *pn = cbm_node_text(ctx->arena, pname, ctx->source);
if (pn && strcmp(pn, name) == 0) {
return cbm_node_text(ctx->arena, ptype, ctx->source);
}
}
return NULL;
}

/* `self.x: T = v` / `self.x = p` inside __init__ (self = its first parameter). */
static void py_init_field_assignment(CBMExtractCtx *ctx, const char *class_qn, TSNode assign,
TSNode params, const char *self_name) {
TSNode left = ts_node_child_by_field_name(assign, TS_FIELD("left"));
if (ts_node_is_null(left) || strcmp(ts_node_type(left), "attribute") != 0) {
return;
}
TSNode obj = ts_node_child_by_field_name(left, TS_FIELD("object"));
TSNode attr = ts_node_child_by_field_name(left, TS_FIELD("attribute"));
if (ts_node_is_null(obj) || ts_node_is_null(attr) ||
strcmp(ts_node_type(obj), "identifier") != 0) {
return;
}
char *obj_name = cbm_node_text(ctx->arena, obj, ctx->source);
if (!obj_name || strcmp(obj_name, self_name) != 0) {
return;
}
TSNode ann = ts_node_child_by_field_name(assign, TS_FIELD("type"));
if (!ts_node_is_null(ann)) {
py_push_field_type(ctx, class_qn, attr, cbm_node_text(ctx->arena, ann, ctx->source));
return;
}
TSNode right = ts_node_child_by_field_name(assign, TS_FIELD("right"));
if (ts_node_is_null(right) || strcmp(ts_node_type(right), "identifier") != 0) {
return;
}
char *rhs = cbm_node_text(ctx->arena, right, ctx->source);
if (rhs && strcmp(rhs, self_name) != 0) {
py_push_field_type(ctx, class_qn, attr, py_init_param_annotation(ctx, params, rhs));
}
}

/* Every `self.x` field assignment in one __init__ body, nested blocks
* included; nested functions, lambdas and classes have their own `self`. */
static void py_extract_init_fields(CBMExtractCtx *ctx, const char *class_qn, TSNode fn) {
TSNode params = ts_node_child_by_field_name(fn, TS_FIELD("parameters"));
TSNode body = ts_node_child_by_field_name(fn, TS_FIELD("body"));
if (ts_node_is_null(params) || ts_node_is_null(body) ||
ts_node_named_child_count(params) == 0) {
return;
}
TSNode self_node = ts_node_named_child(params, 0);
if (strcmp(ts_node_type(self_node), "identifier") != 0) {
return;
}
char *self_name = cbm_node_text(ctx->arena, self_node, ctx->source);
if (!self_name || !self_name[0]) {
return;
}
TSNodeStack stack;
ts_nstack_init(&stack, ctx, CBM_SZ_32);
ts_nstack_push(&stack, body);
while (stack.count > 0) {
TSNode n = ts_nstack_pop(&stack);
const char *k = ts_node_type(n);
if (strcmp(k, "function_definition") == 0 || strcmp(k, "class_definition") == 0 ||
strcmp(k, "lambda") == 0) {
continue;
}
TSNode assign = py_statement_assignment(n);
if (!ts_node_is_null(assign)) {
py_init_field_assignment(ctx, class_qn, assign, params, self_name);
continue;
}
/* Reverse push keeps source order on pop. */
for (uint32_t i = ts_node_named_child_count(n); i > 0; i--) {
ts_nstack_push(&stack, ts_node_named_child(n, i - 1));
}
}
}

static void extract_py_field_types(CBMExtractCtx *ctx, TSNode class_node, const char *class_qn) {
TSNode body = ts_node_child_by_field_name(class_node, TS_FIELD("body"));
if (ts_node_is_null(body)) {
return;
}
uint32_t count = ts_node_named_child_count(body);
/* Class-body annotations first: `x: T` / `x: T = v`. */
for (uint32_t i = 0; i < count; i++) {
TSNode assign = py_statement_assignment(ts_node_named_child(body, i));
if (ts_node_is_null(assign)) {
continue;
}
TSNode left = ts_node_child_by_field_name(assign, TS_FIELD("left"));
TSNode ann = ts_node_child_by_field_name(assign, TS_FIELD("type"));
if (!ts_node_is_null(left) && !ts_node_is_null(ann) &&
strcmp(ts_node_type(left), "identifier") == 0) {
py_push_field_type(ctx, class_qn, left, cbm_node_text(ctx->arena, ann, ctx->source));
}
}
/* Then __init__. */
for (uint32_t i = 0; i < count; i++) {
TSNode fn = ts_node_named_child(body, i);
if (strcmp(ts_node_type(fn), "decorated_definition") == 0) {
fn = ts_node_child_by_field_name(fn, TS_FIELD("definition"));
}
if (ts_node_is_null(fn) || strcmp(ts_node_type(fn), "function_definition") != 0) {
continue;
}
TSNode fname = ts_node_child_by_field_name(fn, TS_FIELD("name"));
char *fn_name =
ts_node_is_null(fname) ? NULL : cbm_node_text(ctx->arena, fname, ctx->source);
if (fn_name && strcmp(fn_name, "__init__") == 0) {
py_extract_init_fields(ctx, class_qn, fn);
}
}
}

static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
CBMArena *a = ctx->arena;
const char *kind = ts_node_type(node);
Expand Down Expand Up @@ -4970,6 +5133,10 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
// Extract class-level variables (field declarations)
extract_class_variables(ctx, node, class_qn, spec);

if (ctx->language == CBM_LANG_PYTHON) {
extract_py_field_types(ctx, node, class_qn);
}

// C# 12 primary-constructor parameters: declared on the class line
// (`class Foo(IBar bar, IBaz baz) : Base { ... }`) and bound to implicit
// captured fields accessible from any instance member. Tree-sitter c-sharp
Expand Down
Loading
Loading