diff --git a/internal/cbm/lsp/py_lsp.c b/internal/cbm/lsp/py_lsp.c index 97929be65..f0bd9dce5 100644 --- a/internal/cbm/lsp/py_lsp.c +++ b/internal/cbm/lsp/py_lsp.c @@ -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 diff --git a/internal/cbm/lsp/type_registry.h b/internal/cbm/lsp/type_registry.h index d91f0cbfa..6c1516f80 100644 --- a/internal/cbm/lsp/type_registry.h +++ b/internal/cbm/lsp/type_registry.h @@ -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. diff --git a/tests/test_py_lsp.c b/tests/test_py_lsp.c index 3316714f3..f422aabbc 100644 --- a/tests/test_py_lsp.c +++ b/tests/test_py_lsp.c @@ -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); }