diff --git a/lua/docgen/parser.lua b/lua/docgen/parser.lua index 8479e29..a7cc10d 100644 --- a/lua/docgen/parser.lua +++ b/lua/docgen/parser.lua @@ -33,6 +33,7 @@ local luacats_grammar = require("docgen.grammar.luacats") --- @field module? string --- @field modvar? string --- @field classvar? string +--- @field member_sep? '.'|':' --- @field deprecated? true --- @field async? true --- @field overloads? string[] @@ -291,6 +292,7 @@ local function process_lua_line(line, state, classes, classvars, has_indent) cur_obj.name = fun_or_meth_nm cur_obj.class = class cur_obj.classvar = parent_tbl + cur_obj.member_sep = sep -- Add self param to methods if sep == ":" then cur_obj.params = cur_obj.params or {} diff --git a/lua/docgen/renderer.lua b/lua/docgen/renderer.lua index ec9d0d9..a66a288 100644 --- a/lua/docgen/renderer.lua +++ b/lua/docgen/renderer.lua @@ -6,6 +6,18 @@ local TEXT_WIDTH = 78 local TAB_WIDTH = 4 local TAB = string.rep(" ", TAB_WIDTH) +--- True when a class member was declared with `.` on a table that is also the +--- module return value. Such members are rendered as module functions rather +--- than class methods. +---@param fun docgen.parser.fun +---@return boolean +local function is_module_fun(fun) + return fun.classvar ~= nil + and fun.member_sep == "." + and fun.modvar ~= nil + and fun.classvar == fun.modvar +end + -- luacheck: ignore 211 ---@diagnostic disable-next-line: unused-local, unused-function local function string_literal(str) @@ -330,8 +342,9 @@ end ---@param class docgen.parser.class ---@param classes table +---@param hidden_fields? table ---@return string? -local function render_class(class, classes) +local function render_class(class, classes, hidden_fields) if class.nodoc or class.inlinedoc then return end local res = {} @@ -357,7 +370,14 @@ local function render_class(class, classes) table.insert(res, "\n") end - local fields_text = render_fields_or_params(class.fields, nil, classes) + local fields = class.fields + if hidden_fields then + fields = vim.tbl_filter(function(f) + return not hidden_fields[f.name] + end, fields) + end + + local fields_text = render_fields_or_params(fields, nil, classes) if not fields_text:match("^%s*$") then table.insert(res, string.format("\n%sFields: ~\n", TAB)) table.insert(res, fields_text) @@ -369,11 +389,22 @@ end ---@param classes table ---@param all_classes table +---@param funs? docgen.parser.fun[] ---@return string -function M.render_classes(classes, all_classes) +function M.render_classes(classes, all_classes, funs) + -- Dot members of a class-as-module render as module functions, so hide them + -- from the class Fields listing. + local hidden_by_class = {} ---@type table> + for _, fun in ipairs(funs or {}) do + if is_module_fun(fun) and fun.class then + hidden_by_class[fun.class] = hidden_by_class[fun.class] or {} + hidden_by_class[fun.class][fun.name] = true + end + end + local res = {} for _, class in vim.spairs(classes) do - local class_desc = render_class(class, all_classes) + local class_desc = render_class(class, all_classes, hidden_by_class[class.name]) if class_desc and not class_desc:match("^%s*$") then table.insert(res, class_desc) end end return table.concat(res) @@ -390,14 +421,21 @@ local function render_fun_header(fun, section) if param.name ~= "self" then table.insert(params, format_field_name(param.name)) end end - local name = fun.classvar and string.format("%s:%s", fun.classvar, fun.name) - or string.format("%s.%s", section.fn_prefix, fun.name) + local module_fun = is_module_fun(fun) + local name + if module_fun then + name = fun.name + elseif fun.classvar then + name = string.format("%s:%s", fun.classvar, fun.name) + else + name = string.format("%s.%s", section.fn_prefix, fun.name) + end local param_str = table.concat(params, ", ") local proto = fun.table and name or string.format("%s(%s)", name, param_str) local fn_suffix = fun.table and "" or "()" local tag - if fun.classvar then + if fun.classvar and not module_fun then tag = string.format("*%s:%s%s*", fun.classvar, fun.name, fn_suffix) else tag = string.format("*%s.%s%s*", section.fn_tag_prefix, fun.name, fn_suffix) @@ -779,7 +817,7 @@ function M.render_section(section, briefs, funs, classes, all_classes) table.insert(res, "\n\n") end - local classes_text = M.render_classes(classes, all_classes) + local classes_text = M.render_classes(classes, all_classes, funs) if not classes_text:match("^%s*$") then table.insert(res, classes_text) table.insert(res, "\n") diff --git a/tests/parser_spec.lua b/tests/parser_spec.lua index 9818ce0..adce4bb 100644 --- a/tests/parser_spec.lua +++ b/tests/parser_spec.lua @@ -152,6 +152,54 @@ return M assert.same({ "fun(x: string): boolean", "fun(x: number): string" }, funs[1].overloads) end) + it("records member_sep for class members", function() + local input = [[ +---@class MyClass +local MyClass = {} + +--- dot member +---@param obj MyClass +function MyClass.dot_member(obj) end + +--- colon member +function MyClass:colon_member() end + +return MyClass + ]] + local _, funs, _, _ = parser.parse_str(input, "myfile.lua") + + assert.same(".", funs[1].member_sep) + assert.same("MyClass", funs[1].classvar) + assert.same("MyClass", funs[1].modvar) + assert.same("obj", funs[1].params[1].name) + + assert.same(":", funs[2].member_sep) + assert.same("self", funs[2].params[1].name) + assert.same("MyClass", funs[2].params[1].type) + end) + + it("keeps non-returned dot members as class fields", function() + local input = [[ +local M = {} +---@class Helper +local Helper = {} + +--- helper field +---@param h Helper +function Helper.field(h) end + +return M + ]] + local classes, _, _, _ = parser.parse_str(input, "myfile.lua") + + assert.same({ + name = "field", + type = "fun(h: Helper)", + desc = "helper field", + classvar = "Helper", + }, classes.Helper.fields[1]) + end) + it("@overload is preserved for class methods converted to fields", function() local input = [[ local M = {} diff --git a/tests/renderer_spec.lua b/tests/renderer_spec.lua index e6155bb..5f4854b 100644 --- a/tests/renderer_spec.lua +++ b/tests/renderer_spec.lua @@ -618,6 +618,119 @@ FOO_BAR *foo.bar* assert_section(input, expect) end) + + it("dot member renders as module fun, colon member stays a method", function() + local input = [[ +---@class M +---@field value integer some value +local M = {} + +--- Converts to cursor position. +---@param pos M +---@return integer, integer +function M.to_cursor(pos) end + +--- Instance method. +---@return integer +function M:row() end + +return M + ]] + + local expect = [[ +FOO_BAR *foo.bar* + +*M* + + Fields: ~ + • {value} (`integer`) some value + • {row} (`fun(self: M): integer`) See |M:row()|. + + +to_cursor({pos}) *foo.bar.to_cursor()* + Converts to cursor position. + + Parameters: ~ + • {pos} (`M`) See |M| + + Return (multiple): ~ + (`integer`) + (`integer`) + +M:row() *M:row()* + Instance method. + + Return: ~ + (`integer`) + ]] + + assert_section(input, expect) + end) + + it("dot member on non-module class stays in class Fields", function() + local input = [[ +local M = {} + +---@class Helper +local Helper = {} + +--- Helper fn. +---@param h Helper +function Helper.do_it(h) end + +return M + ]] + + local expect = [[ +FOO_BAR *foo.bar* + +*Helper* + + Fields: ~ + • {do_it} (`fun(h: Helper)`) See |Helper:do_it()|. + + +Helper:do_it({h}) *Helper:do_it()* + Helper fn. + + Parameters: ~ + • {h} (`Helper`) See |Helper| + ]] + + assert_section(input, expect) + end) + + it("long dot-member signature wraps using short name", function() + local input = [[ +---@class M +local M = {} + +--- long one +---@param a string +---@param b string +---@param c string +function M.this_is_a_really_long_function_name_that_should_be_wrapped(a, b, c) end + +return M + ]] + + local expect = [[ +FOO_BAR *foo.bar* + +*M* + + *foo.bar.this_is_a_really_long_function_name_that_should_be_wrapped()* +this_is_a_really_long_function_name_that_should_be_wrapped({a}, {b}, {c}) + long one + + Parameters: ~ + • {a} (`string`) + • {b} (`string`) + • {c} (`string`) + ]] + + assert_section(input, expect) + end) end) describe("render_markdown", function()