Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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