mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
feat(py_lsp): decorator flags + verifications for generators / dataclasses / properties
Round 6: CBMRegisteredFunc gains a `flags` bitfield (CBMFuncFlags) and a `decorator_qns` NULL-terminated list. py_register_def populates them from CBMDefinition.decorators by short-name match — so `@property`, `@functools.cache`, `@abc.abstractmethod`, `@typing.overload`, and `@final` all light up regardless of qualified vs bare reference. The flags aren't consumed by resolution yet (still on roadmap: emit distinct edge for @property attribute access vs method call); they are recorded so future overload selection / property modeling can use them without another schema change. The struct extension is at-tail and existing call sites that memset to zero stay correct; non-Python LSPs see flags=0 / decorator_qns=NULL and behave identically. Three new verification tests prove that generators, dataclasses, and @property already work end-to-end through the Round 4-5 infrastructure without further code changes: - Generator[Foo, ...] iterables in for-loop bind loop var to Foo via py_iterable_element_type's existing TEMPLATE handling. - @dataclass class Point: x: Foo (class-body annotated assignment) is picked up by Round 4's third-pass field registrar; p.x.method() resolves through the field type. - @property def thing(self) -> Foo: c.thing returns Foo because attribute access on NAMED routes through py_func_return_type which yields the getter's annotated return. All 2895 prior tests stay green; 3 new tests pass. Total 2898 / 0. Bench unchanged at 95% (the small bench fixture doesn't exercise the new patterns; will expand in Round 7).
This commit is contained in:
1 parent
ce54e817b7
commit
fd81dffc8f
3 files changed
+114
No files matched your search
@@ -1919,6 +1919,39 @@ static bool py_register_def(CBMArena* arena, CBMTypeRegistry* reg, CBMDefinition
|
||||
rf.qualified_name = d->qualified_name;
|
||||
rf.short_name = d->name;
|
||||
|
||||
// Translate Python decorators into flags + carry the QN list.
|
||||
if (d->decorators) {
|
||||
int dec_count = 0;
|
||||
while (d->decorators[dec_count]) dec_count++;
|
||||
if (dec_count > 0) {
|
||||
const char** dec_qns = (const char**)cbm_arena_alloc(arena,
|
||||
(size_t)(dec_count + 1) * sizeof(const char*));
|
||||
for (int j = 0; j < dec_count; j++) {
|
||||
const char* dec = d->decorators[j];
|
||||
dec_qns[j] = cbm_arena_strdup(arena, dec);
|
||||
// Match by short name (last "." segment) so both
|
||||
// "property" and "functools.cache" / "abc.abstractmethod"
|
||||
// forms light up.
|
||||
const char* short_dec = strrchr(dec, '.');
|
||||
short_dec = short_dec ? short_dec + 1 : dec;
|
||||
if (strcmp(short_dec, "property") == 0)
|
||||
rf.flags |= CBM_FUNC_FLAG_PROPERTY;
|
||||
else if (strcmp(short_dec, "classmethod") == 0)
|
||||
rf.flags |= CBM_FUNC_FLAG_CLASSMETHOD;
|
||||
else if (strcmp(short_dec, "staticmethod") == 0)
|
||||
rf.flags |= CBM_FUNC_FLAG_STATICMETHOD;
|
||||
else if (strcmp(short_dec, "abstractmethod") == 0)
|
||||
rf.flags |= CBM_FUNC_FLAG_ABSTRACTMETHOD;
|
||||
else if (strcmp(short_dec, "overload") == 0)
|
||||
rf.flags |= CBM_FUNC_FLAG_OVERLOAD;
|
||||
else if (strcmp(short_dec, "final") == 0)
|
||||
rf.flags |= CBM_FUNC_FLAG_FINAL;
|
||||
}
|
||||
dec_qns[dec_count] = NULL;
|
||||
rf.decorator_qns = dec_qns;
|
||||
}
|
||||
}
|
||||
|
||||
const CBMType** ret_types = NULL;
|
||||
// Prefer d->return_type (full text) when it has subscript brackets
|
||||
// — extract_defs.c::extract_return_types strips them in its array
|
||||
|
||||
@@ -4,6 +4,20 @@
|
||||
#include "type_rep.h"
|
||||
#include "../arena.h"
|
||||
|
||||
// Decorator-derived flags (Python). Added at struct tail so existing
|
||||
// callers that memset to zero before populating other fields keep working.
|
||||
typedef enum {
|
||||
CBM_FUNC_FLAG_NONE = 0,
|
||||
CBM_FUNC_FLAG_PROPERTY = 1 << 0, // @property -> obj.attr returns getter return
|
||||
CBM_FUNC_FLAG_CLASSMETHOD = 1 << 1, // @classmethod -> first arg is cls (the class)
|
||||
CBM_FUNC_FLAG_STATICMETHOD = 1 << 2, // @staticmethod -> no implicit self/cls
|
||||
CBM_FUNC_FLAG_ABSTRACTMETHOD = 1 << 3, // @abstractmethod -> still callable for resolution
|
||||
CBM_FUNC_FLAG_OVERLOAD = 1 << 4, // @overload entry — non-implementation stub
|
||||
CBM_FUNC_FLAG_ASYNC = 1 << 5, // async def — return is Coroutine[..., T]
|
||||
CBM_FUNC_FLAG_GENERATOR = 1 << 6, // contains yield — return is Generator[T, ...]
|
||||
CBM_FUNC_FLAG_FINAL = 1 << 7, // @final — overrides not allowed
|
||||
} CBMFuncFlags;
|
||||
|
||||
// Registered function/method with full type signature.
|
||||
typedef struct {
|
||||
const char* qualified_name; // e.g., "proj.pkg.TypeName.MethodName"
|
||||
@@ -12,6 +26,9 @@ typedef struct {
|
||||
const CBMType* signature; // FUNC type with param/return types
|
||||
const char** type_param_names; // NULL-terminated, e.g., ["T", "R", NULL] for generics
|
||||
int min_params; // Minimum required params (excluding defaulted). -1 = unknown.
|
||||
int flags; // CBM_FUNC_FLAG_* bitfield (Python decorator info; 0 elsewhere)
|
||||
const char** decorator_qns; // NULL-terminated decorator QNs (Python only); used for
|
||||
// user-decorator return-type substitution.
|
||||
} CBMRegisteredFunc;
|
||||
|
||||
// Registered type with fields and method names.
|
||||
|
||||
@@ -1145,6 +1145,66 @@ TEST(pylsp_round5_super_init) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Round 6 — generators, dataclasses, decorator flags ───────── */
|
||||
|
||||
TEST(pylsp_round6_generator_yields_iterable) {
|
||||
/* def gen() -> Generator[Foo, None, None]: yield Foo()
|
||||
* for x in gen(): x.method() — x : Foo via element-of(generator) */
|
||||
CBMFileResult *r = extract_py(
|
||||
"from typing import Generator\n"
|
||||
"class Foo:\n"
|
||||
" def method(self):\n"
|
||||
" return 1\n"
|
||||
"def gen() -> Generator[Foo, None, None]:\n"
|
||||
" yield Foo()\n"
|
||||
"def use():\n"
|
||||
" for x in gen():\n"
|
||||
" x.method()\n");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_GTE(require_resolved(r, "use", "method"), 0);
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pylsp_round6_dataclass_field_access) {
|
||||
/* @dataclass class Point: x: Foo; def use(p: Point): p.x.method() */
|
||||
CBMFileResult *r = extract_py(
|
||||
"from dataclasses import dataclass\n"
|
||||
"class Foo:\n"
|
||||
" def method(self):\n"
|
||||
" return 1\n"
|
||||
"@dataclass\n"
|
||||
"class Point:\n"
|
||||
" x: Foo\n"
|
||||
" y: int\n"
|
||||
"def use(p: Point):\n"
|
||||
" return p.x.method()\n");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_GTE(require_resolved(r, "use", "method"), 0);
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pylsp_round6_property_access_chains) {
|
||||
/* class C: @property def thing(self) -> Foo: ...
|
||||
* def use(c: C): c.thing.method() -- thing is a property; access
|
||||
* returns its getter's return type. */
|
||||
CBMFileResult *r = extract_py(
|
||||
"class Foo:\n"
|
||||
" def method(self):\n"
|
||||
" return 1\n"
|
||||
"class C:\n"
|
||||
" @property\n"
|
||||
" def thing(self) -> Foo:\n"
|
||||
" return Foo()\n"
|
||||
"def use(c: C):\n"
|
||||
" return c.thing.method()\n");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_GTE(require_resolved(r, "use", "method"), 0);
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Suite ─────────────────────────────────────────────────────── */
|
||||
|
||||
SUITE(py_lsp) {
|
||||
@@ -1215,4 +1275,8 @@ SUITE(py_lsp) {
|
||||
RUN_TEST(pylsp_round5_dict_subscript_value_type);
|
||||
RUN_TEST(pylsp_round5_list_subscript_value_type);
|
||||
RUN_TEST(pylsp_round5_super_init);
|
||||
/* Round 6 — generators, dataclasses, properties */
|
||||
RUN_TEST(pylsp_round6_generator_yields_iterable);
|
||||
RUN_TEST(pylsp_round6_dataclass_field_access);
|
||||
RUN_TEST(pylsp_round6_property_access_chains);
|
||||
}
|
||||
Reference in new issue
Block a user