fix(module_doc): named parameter table detection

This commit is contained in:
Aire-One 2023-05-13 03:42:27 +02:00
parent b58384e65b
commit c2e0da3ba6
4 changed files with 379 additions and 171 deletions

View File

@ -447,131 +447,310 @@ describe("Scrap documentation", function()
assert.same(expected, ast)
end)
-- TODO : Fix the code then come back to this test, the current implementation is incomplete
-- it("should produce a Record node when the function parameter is a table", function()
-- local ast <const> = get_doc_from_page([[
-- <h2 class="section-header">
-- <a name="Static_module_functions"></a>Static module functions
-- </h2>
-- <dl class="function">
-- <dt>
-- <a
-- class="copy-link js-copy-link"
-- name="awful.screen.focused"
-- href="#awful.screen.focused"
-- >🔗</a
-- >
-- <strong
-- >awful.screen.focused
-- <span class="function_named_args"><b>{</b>[args]<b>}</b></span></strong
-- >
-- <span class="proptype"
-- ><span class="summary_type"> -&gt;&nbsp;nil <i>or</i> screen</span></span
-- >
-- <span class="baseclass"> </span>
-- </dt>
-- <dd>
-- <h3>Parameters:</h3>
-- <table class="see_also">
-- <tbody>
-- <tr class="param_header">
-- <th>Name</th>
-- <th></th>
-- <th>Type(s)</th>
-- <th>Description</th>
-- <th>Default value</th>
-- </tr>
-- <tr>
-- <td><span class="parameter">args</span></td>
-- <td><span class="chips">Optional</span></td>
-- <td>
-- <span class="types"><span class="type">table</span></span>
-- </td>
-- <td class="see_also_description"></td>
-- <td><span class="not_applicable">Undefined</span></td>
-- </tr>
-- <tr class="see_also_sublist">
-- <td><span class="parameter">client</span></td>
-- <td><span class="chips">Optional</span></td>
-- <td>
-- <span class="types"><span class="type">boolean</span></span>
-- </td>
-- <td class="see_also_description">
-- Use the client screen instead of the mouse screen.
-- </td>
-- <td>
-- <span class="default_value"><code>false</code></span>
-- </td>
-- </tr>
-- <tr class="see_also_sublist">
-- <td><span class="parameter">mouse</span></td>
-- <td><span class="chips">Optional</span></td>
-- <td>
-- <span class="types"><span class="type">boolean</span></span>
-- </td>
-- <td class="see_also_description">Use the mouse screen</td>
-- <td>
-- <span class="default_value"><code>true</code></span>
-- </td>
-- </tr>
-- </tbody>
-- </table>
-- <h3>Returns:</h3>
-- <ol>
-- <span class="types"
-- >optional
-- <a class="type" href="../core_components/screen.html#screen"
-- >screen</a
-- ></span
-- >
-- The focused screen object, or
-- <code>nil</code>
-- in case no screen is present currently.
-- </ol>
-- </dd>
-- </dl>
-- ]], "awful.screen")
-- print(require("inspect")(ast))
-- assert.same(ast, {
-- children = {
-- {
-- children = {},
-- name = "Signal",
-- token = "enum",
-- },
-- {
-- parameters = {
-- {
-- children = {
-- {
-- types = { "boolean" },
-- name = "client",
-- token = "variable",
-- },
-- {
-- types = { "boolean" },
-- name = "mouse",
-- token = "variable",
-- },
-- },
-- name = "Args",
-- token = "record",
-- },
-- {
-- types = { "Focused_Args" },
-- name = "args",
-- token = "variable",
-- },
-- },
-- return_types = { "screen" },
-- name = "focused",
-- token = "function",
-- },
-- },
-- name = "Screen",
-- token = "module",
-- })
-- end)
it("should produce a Record node when a function parameter is a named-parameter-table", function()
local ast <const> = get_doc_from_page([[
<h2 class="section-header">
<a name="Static_module_functions"></a>Static module functions
</h2>
<dl class="function">
<dt>
<a
class="copy-link js-copy-link"
name="awful.screen.focused"
href="#awful.screen.focused"
>🔗</a
>
<strong
>awful.screen.focused
<span class="function_named_args"><b>{</b>[args]<b>}</b></span></strong
>
<span class="proptype"
><span class="summary_type"> -&gt;&nbsp;nil <i>or</i> screen</span></span
>
<span class="baseclass"> </span>
</dt>
<dd>
<h3>Parameters:</h3>
<table class="see_also">
<tbody>
<tr class="param_header">
<th>Name</th>
<th></th>
<th>Type(s)</th>
<th>Description</th>
<th>Default value</th>
</tr>
<tr>
<td><span class="parameter">args</span></td>
<td><span class="chips">Optional</span></td>
<td>
<span class="types"><span class="type">table</span></span>
</td>
<td class="see_also_description"></td>
<td><span class="not_applicable">Undefined</span></td>
</tr>
<tr class="see_also_sublist">
<td><span class="parameter">client</span></td>
<td><span class="chips">Optional</span></td>
<td>
<span class="types"><span class="type">boolean</span></span>
</td>
<td class="see_also_description">
Use the client screen instead of the mouse screen.
</td>
<td>
<span class="default_value"><code>false</code></span>
</td>
</tr>
<tr class="see_also_sublist">
<td><span class="parameter">mouse</span></td>
<td><span class="chips">Optional</span></td>
<td>
<span class="types"><span class="type">boolean</span></span>
</td>
<td class="see_also_description">Use the mouse screen</td>
<td>
<span class="default_value"><code>true</code></span>
</td>
</tr>
</tbody>
</table>
<h3>Returns:</h3>
<ol>
<span class="types"
>optional
<a class="type" href="../core_components/screen.html#screen"
>screen</a
></span
>
The focused screen object, or
<code>nil</code>
in case no screen is present currently.
</ol>
</dd>
</dl>
]], "awful.screen")
assert.same(ast, {
children = {
{
children = {},
name = "Signal",
token = "enum",
},
{
children = {
{
types = { "boolean" },
name = "client",
token = "variable",
},
{
types = { "boolean" },
name = "mouse",
token = "variable",
}
},
name = "Focused_Args",
token = "record",
},
{
parameters = {
{
types = { "Focused_Args" },
name = "args",
token = "variable",
},
},
return_types = { "screen" },
name = "focused",
token = "function",
},
},
name = "Screen",
token = "module",
})
end)
it("should go back to a table typed parameter when the record is empty", function()
local ast <const> = get_doc_from_page([[
<h2 class="section-header "><a name="Static_module_functions"></a>Static module functions</h2>
<dl class="function">
<dt>
<a class="copy-link js-copy-link" name="crush" href="#crush">🔗</a>
<strong><span class="function_modname">gears.table.</span>crush <span class="function_args"> <b>(</b>target, source, <span class="optional_param">raw</span><b>)</b></span></strong>
<span class="proptype"><span class="summary_type"> -&gt;&nbsp;table</span></span>
<span class="baseclass">
</span>
</dt>
<dd>
</p><h3>Parameters:</h3>
<table class="see_also">
<tbody><tr class="param_header">
<th>Name</th>
<th></th>
<th>Type(s)</th>
<th>Description</th>
<th>Default value</th>
</tr>
<tr>
<td><span class="parameter">target</span></td>
<td></td>
<td><span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.6" target="_blank">table</a></span></td>
<td class="see_also_description"> The target table. Values from <code>source</code> will be copied
into this table.</td>
<td><span class="not_applicable" title="This parameter is mandatory">Not applicable</span></td>
</tr>
<tr>
<td><span class="parameter">source</span></td>
<td></td>
<td><span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.6" target="_blank">table</a></span></td>
<td class="see_also_description"> The source table. Its values will be copied into
<code>target</code>.</td>
<td><span class="not_applicable" title="This parameter is mandatory">Not applicable</span></td>
</tr>
<tr>
<td><span class="parameter">raw</span></td>
<td><span class="chips">Optional</span></td>
<td><span class="types"><span class="type">bool</span></span></td>
<td class="see_also_description"> If <code>true</code>, values will be assigned with <a href="https://www.lua.org/manual/5.3/manual.html#pdf-rawset" target="_blank">rawset</a>.
This will bypass metamethods on <code>target</code>.</td>
<td><span class="default_value"><code>false</code></span></td>
</tr>
</tbody></table>
<h3>Returns:</h3>
<ol>
<span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.6" target="_blank">table</a></span>
The target table.
</ol>
</dd>
</dl>
]], "gears.table")
local expected <const>: Node = {
children = {
{
children = {},
name = "Signal",
token = "enum",
},
{
parameters = {
{
types = { "table" },
name = "target",
token = "variable",
},
{
types = { "table" },
name = "source",
token = "variable",
},
{
types = { "bool" },
name = "raw",
token = "variable",
}
},
return_types = { "table" },
name = "crush",
token = "function",
}
},
name = "Table",
token = "module",
}
assert.same(expected, ast)
end)
it("should go back to a table typed parameter when the record is empty and it's the last parameter", function()
local ast <const> = get_doc_from_page([[
<h2 class="section-header"><a name="Object_methods"></a>Object methods</h2>
<dl class="function">
<dt>
<a class="copy-link js-copy-link" name="tags" href="#tags">🔗</a>
<strong
>:tags
<span class="function_args"> <b>(</b>tags_table<b>)</b></span></strong
>
<span class="proptype"
><span class="summary_type"> -&gt;&nbsp;table</span></span
>
<span class="baseclass"> · 1 signal </span>
</dt>
<dd>
<h3>Parameters:</h3>
<table class="see_also">
<tbody>
<tr class="param_header">
<th>Name</th>
<th></th>
<th>Type(s)</th>
<th>Description</th>
</tr>
<tr>
<td><span class="parameter">tags_table</span></td>
<td></td>
<td>
<span class="types"
><a
class="type"
href="https://www.lua.org/manual/5.3/manual.html#6.6"
target="_blank"
>table</a
></span
>
</td>
<td class="see_also_description">
A table with tags to set, or <code>nil</code> to get the current
tags.
</td>
</tr>
</tbody>
</table>
<h3>Returns:</h3>
<ol>
<span class="types"
><a
class="type"
href="https://www.lua.org/manual/5.3/manual.html#6.6"
target="_blank"
>table</a
></span
>
A table with all tags.
</ol>
</dd>
</dl>
]], "awful.client")
local expected <const>: Node = {
children = {
{
children = {},
name = "Signal",
token = "enum",
},
{
parameters = {
{
types = { "Client" },
name = "self",
token = "variable",
},
{
types = { "table" },
name = "tags_table",
token = "variable",
},
},
return_types = { "table" },
name = "tags",
token = "function",
},
},
name = "Client",
token = "module",
}
assert.same(expected, ast)
end)
it("should return Function nodes with the `other_nodes` list when the function module name doesn't match the module name", function()
local ast <const>, other_nodes <const> = get_doc_from_page([[

View File

@ -34,45 +34,63 @@ local function extract_item_name(item_name_node: scan.HTMLNode): string, string
return utils.sanitize_string(name), module_name and utils.sanitize_string(module_name) or nil
end
local function extract_function_parameters(table_html: string): { Node }
local current_record_parameter: Node = nil
local function extract_function_parameters(table_html: string, function_name: string): { Node }, { Node }
local parameters_types <const>: { Node } = {}
local is_populating_parameters_types: boolean = false
local current_field: Node = nil
return scraper_utils.scrape(table_html, "tr", function(tr: scan.HTMLNode): Node
local tr_html = tr:outer_html()
local name_node <const> = scraper_utils.find(tr_html, "span.parameter")[1]
local types_node <const> = scraper_utils.find(tr_html, "span.types")[1]
if not name_node or not types_node then
return nil
end
local parameters <const> = scraper_utils.scrape(table_html, "tr", function(tr: scan.HTMLNode): Node
local tr_html = tr:outer_html()
local name_node <const> = scraper_utils.find(tr_html, "span.parameter")[1]
local types_node <const> = scraper_utils.find(tr_html, "span.types")[1]
if not name_node or not types_node then
return nil
end
local name <const> = extract_node_text(name_node)
local types <const> = parse_parameter_types(extract_node_text(types_node))
if tr.attr ~= nil and tr.attr.class == "see_also_sublist" and current_record_parameter then
local field = ast.create_node("variable", name)
field.types = types
table.insert(current_record_parameter.children, field)
return nil
end
-- We wrongly tried to convert a table to a record
if current_record_parameter then
current_record_parameter.token = "variable"
current_record_parameter.name = utils.lowercase(current_record_parameter.name)
current_record_parameter.types = { "table" }
current_record_parameter.children = nil
current_record_parameter = nil
end
if #types == 1 and types[1] == "table" then
current_record_parameter = ast.create_node("record", utils.capitalize(name))
return current_record_parameter
end
local name <const> = extract_node_text(name_node)
local types <const> = parse_parameter_types(extract_node_text(types_node))
-- Add a field to the current parameter type record
if tr.attr ~= nil and tr.attr.class == "see_also_sublist" and is_populating_parameters_types then
local field = ast.create_node("variable", name)
field.types = types
return field
end)
table.insert(parameters_types[#parameters_types].children, field)
return nil
end
-- Still here and we are populating the parameter type record ?
-- Then oops, we wrongly tried to convert a table to a record
if is_populating_parameters_types then
table.remove(parameters_types, #parameters_types)
is_populating_parameters_types = false
current_field.types = { "table" }
end
-- Otherwise, add a new parameter
local field = ast.create_node("variable", name)
field.types = types
current_field = field
-- If the parameter is a table, then we try to convert it to a record
if #types == 1 and types[1] == "table" then
local record_name <const> = string.format(
"%s_%s",
utils.capitalize(function_name),
utils.capitalize(name))
table.insert(parameters_types, ast.create_node("record", record_name))
is_populating_parameters_types = true
field.types = { record_name }
end
return field
end)
if is_populating_parameters_types and #parameters_types[#parameters_types].children == 0 then
table.remove(parameters_types, #parameters_types)
current_field.types = { "table" }
end
return parameters, parameters_types
end
local function extract_function_return_types(ol_html: string): { string }
@ -109,9 +127,11 @@ local function extract_section_functions(dl: string, module_name: string | nil):
local body_html = nodes[list_query_selectors.body]:outer_html()
local parameter_node = scraper_utils.find(body_html, "table")[1]
function_node.parameters = parameter_node and
extract_function_parameters(parameter_node:outer_html()) or
{}
local parameters, parameters_types: { Node }, { Node } = {}, {}
if parameter_node then
parameters, parameters_types = extract_function_parameters(parameter_node:outer_html(), function_name)
end
function_node.parameters = parameters
local return_node = scraper_utils.find(body_html, "ol")[1]
function_node.return_types = return_node and
@ -120,8 +140,10 @@ local function extract_section_functions(dl: string, module_name: string | nil):
if module_name and function_module_name and module_name ~= function_module_name then
function_node.name = function_module_name .. "." .. function_node.name
utils.spread(other_functions, parameters_types)
table.insert(other_functions, function_node)
else
utils.spread(functions, parameters_types)
table.insert(functions, function_node)
end
end
@ -203,9 +225,11 @@ local section_scrapers <total>: { Section : function(html: string, record_name:
["Object methods"] = function(html: string, record_name: string): { Node }, { Node }, { string }
local methods <const> = extract_section_functions(html)
for _, method in ipairs(methods) do
local self_parameter = ast.create_node("variable", "self")
self_parameter.types = { record_name }
table.insert(method.parameters, 1, self_parameter)
if method.token == "function" then
local self_parameter = ast.create_node("variable", "self")
self_parameter.types = { record_name }
table.insert(method.parameters, 1, self_parameter)
end
end
return methods, {}, {}
end,

View File

@ -15,15 +15,14 @@ function scraper_utils.scrape<T>(html: string, query_selector: string, extract_c
scanner.scan_html(html, function(stack: scan.NodeStack)
if stack:is(query_selector) then
local node = stack:current()
local success, info_or_error = pcall(extract_callback, node)
local success, ret_or_error = pcall(extract_callback, node)
if not success then
local error_message = info_or_error as string
log:error(logger.message_with_metadata("Extraction error", { error = error_message }))
else
local info = info_or_error as T
table.insert(ret, info)
log:error(logger.message_with_metadata("Extraction error", { error = ret_or_error as string }))
return
end
table.insert(ret, ret_or_error as T)
end
end)

View File

@ -76,4 +76,10 @@ function utils.do_or_fail<T>(func: function<T>(...: any): (T | nil, string), ...
return res as T -- promote to T since pcall succeeded at this point
end
function utils.spread<T>(t: { T }, i: { T })
for _, v in ipairs(i) do
table.insert(t, v)
end
end
return utils