Skip to content
Merged
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
2 changes: 2 additions & 0 deletions lua/docgen/parser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -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 {}
Expand Down
54 changes: 46 additions & 8 deletions lua/docgen/renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -330,8 +342,9 @@ end

---@param class docgen.parser.class
---@param classes table<string, docgen.parser.class>
---@param hidden_fields? table<string, true>
---@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 = {}
Expand All @@ -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)
Expand All @@ -369,11 +389,22 @@ end

---@param classes table<string, docgen.parser.class>
---@param all_classes table<string, docgen.parser.class>
---@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<string, table<string, true>>
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)
Expand All @@ -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)
Expand Down Expand Up @@ -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")
Expand Down
48 changes: 48 additions & 0 deletions tests/parser_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down
113 changes: 113 additions & 0 deletions tests/renderer_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading