Support table parameters and record types (#37) #38

Merged
Aire-One merged 7 commits from feat/#37 into master 2022-11-06 19:23:20 +01:00
8 changed files with 185 additions and 28 deletions

View File

@ -1,4 +1,5 @@
local List = require "pl.List" local List = require "pl.List"
local Type_Info = require "entity.Type_Info"
local record Function_Info local record Function_Info
metamethod __call: function(Function_Info): Function_Info metamethod __call: function(Function_Info): Function_Info
@ -6,7 +7,7 @@ local record Function_Info
Function_Info: Function_Info Function_Info: Function_Info
record Parameter record Parameter
name: string name: string
types: List<string> types: List<Type_Info.Type_Info>
end end
name: string name: string
@ -27,7 +28,7 @@ local __Function_Info: metatable<Function_Info> = {
end, end,
} }
function Function_Info:append_parameter(name: string, types: List<string>) function Function_Info:append_parameter(name: string, types: List<Type_Info.Type_Info>)
self.parameters:append { self.parameters:append {
name = name, name = name,
types = types, types = types,

View File

@ -9,7 +9,9 @@ local record Module_Doc
record_name: string record_name: string
constructors: List<Function_Info.Function_Info> constructors: List<Function_Info.Function_Info> -- Translates to a list of methods
constructor_param_record: List<Variable_Info.Variable_Info> -- Constructor with the __call metamethod and named parameters pattern
methods: List<Function_Info.Function_Info> methods: List<Function_Info.Function_Info>
properties: List<Variable_Info.Variable_Info> properties: List<Variable_Info.Variable_Info>
static_functions: List<Function_Info.Function_Info> static_functions: List<Function_Info.Function_Info>
@ -20,6 +22,7 @@ local __Module_Doc: metatable<Module_Doc> = {
__call = function(_: Module_Doc): Module_Doc __call = function(_: Module_Doc): Module_Doc
return { return {
constructors = List(), constructors = List(),
constructor_param_record = List(),
methods = List(), methods = List(),
properties = List(), properties = List(),
static_functions = List(), static_functions = List(),

View File

@ -0,0 +1,32 @@
local List = require "pl.List"
local Map = require "pl.Map"
local record Type_Info
metamethod __call: function(Type_Info, record_name: string): Type_Info
Type_Info: Type_Info
name: string
-- Map : name -> type
-- We can't use Variable_Info here because it's a circular dependency.
record_entries: Map<string, List<Type_Info>> | nil
end
local __Type_Info: metatable<Type_Info> = {
__call = function(_self: Type_Info, record_name: string): Type_Info
if record_name ~= nil then
return {
name = record_name,
record_entries = Map()
}
end
return {
name = "",
record_entries = nil,
}
end,
}
return setmetatable({} as Type_Info, __Type_Info)

View File

@ -1,21 +1,24 @@
local List = require "pl.List" local List = require "pl.List"
local Type_Info = require "entity.Type_Info"
local record Variable_Info local record Variable_Info
metamethod __call: function(Variable_Info): Variable_Info metamethod __call: function(self: Variable_Info, name: string | nil, types: List<Type_Info.Type_Info> | nil): Variable_Info
Variable_Info: Variable_Info Variable_Info: Variable_Info
name: string name: string
types: List<string> types: List<Type_Info.Type_Info>
constraints: List<string> constraints: List<string>
end end
local __Variable_Info: metatable<Variable_Info> = { local __Variable_Info: metatable<Variable_Info> = {
__call = function(_self: Variable_Info): Variable_Info __call = function(_self: Variable_Info, name: string | nil, types: List<Type_Info.Type_Info> | nil): Variable_Info
name = name or ""
types = types or List()
return { return {
name = "", name = name,
types = List(), types = types,
} }
end, end,
} }

View File

@ -1,24 +1,38 @@
local Function_Info = require "entity.Function_Info" local Function_Info = require "entity.Function_Info"
local List = require "pl.List" local List = require "pl.List"
local Map = require "pl.Map"
local stringx = require "pl.stringx" local stringx = require "pl.stringx"
local template = require "pl.template" local template = require "pl.template"
local Type_Info = require "entity.Type_Info"
local utils = require "utils" local utils = require "utils"
local Variable_Info = require "entity.Variable_Info" local Variable_Info = require "entity.Variable_Info"
local snippets = {} local record Module
indent: function(str: string, level: number): string
render_typed_variable: function(name: string, types: List<Type_Info.Type_Info>): string
render_anonymous_function_signature: function(item: Function_Info.Function_Info): string
render_record_functions: function(items: List<Function_Info.Function_Info>): string
render_enum: function(name: string, values: List<string>): string
render_record_properties: function(items: List<Variable_Info.Variable_Info>): string
render_record: function(name: string, items: List<Variable_Info.Variable_Info>): string
render_records_from_Parameters: function(items: List<Function_Info.Parameter>): string
end
local snippets: Module = {}
function snippets.indent(str: string, level: number): string function snippets.indent(str: string, level: number): string
level = level or 1 level = level or 1
return stringx.rstrip(stringx.indent(str, level, string.rep(" ", 3))) return stringx.rstrip(stringx.indent(str, level, string.rep(" ", 3)))
end end
function snippets.render_typed_variable(name: string, types: List<string>): string function snippets.render_typed_variable(name: string, types: List<Type_Info.Type_Info>): string
local tmpl = local tmpl =
[[$(name): $(types)]] [[$(name): $(types)]]
local tmpl_args = { local tmpl_args = {
name = name, name = name,
types = types:concat(" | "), types = types:map(function(t: Type_Info.Type_Info): string return t.name end):concat(" | "),
} }
return utils.do_or_fail(template.substitute, tmpl, tmpl_args) return utils.do_or_fail(template.substitute, tmpl, tmpl_args)
@ -39,9 +53,38 @@ function snippets.render_anonymous_function_signature(item: Function_Info.Functi
return utils.do_or_fail(template.substitute, tmpl, tmpl_args) return utils.do_or_fail(template.substitute, tmpl, tmpl_args)
end end
function snippets.render_records_from_Parameters(items: List<Function_Info.Parameter>): string
return items:map(function(param: Function_Info.Parameter): string
if #param.types == 0 then
return ""
end
return param.types:map(function(t: Type_Info.Type_Info): string
if t.record_entries and (t.record_entries as Map<string, List<Type_Info.Type_Info>>):len() > 0 then
local properties: List<Variable_Info.Variable_Info> = List()
for name, types in (t.record_entries as Map<string, List<Type_Info.Type_Info>>):iter() do
properties:append(Variable_Info(name, types))
end
return snippets.render_record(t.name, properties)
end
return ""
end):filter(
function (s: string): boolean
return not utils.is_empty(s)
end):concat("\n")
end):filter(
function (s: string): boolean
return not utils.is_empty(s)
end):concat("\n")
end
function snippets.render_record_functions(items: List<Function_Info.Function_Info>): string function snippets.render_record_functions(items: List<Function_Info.Function_Info>): string
return items:map(function(item: Function_Info.Function_Info): string return items:map(function(item: Function_Info.Function_Info): string
return snippets.render_anonymous_function_signature(item) return string.format(
"%s%s",
snippets.render_records_from_Parameters(item.parameters),
snippets.render_anonymous_function_signature(item))
end):concat("\n") end):concat("\n")
end end
@ -70,11 +113,28 @@ function snippets.render_record_properties(items: List<Variable_Info.Variable_In
end end
local enum_type = utils.capitalize(item.name) local enum_type = utils.capitalize(item.name)
local enum_type_info: Type_Info.Type_Info = { name = enum_type }
return string.format( return string.format(
"%s%s", "%s%s",
snippets.render_enum(enum_type, item.constraints), snippets.render_enum(enum_type, item.constraints),
snippets.render_typed_variable(item.name, List({ enum_type }))) snippets.render_typed_variable(item.name, List({ enum_type_info })))
end):concat("\n") end):concat("\n")
end end
function snippets.render_record(name: string, items: List<Variable_Info.Variable_Info>): string
local tmpl = [[
record $(name)
$(indent(body))
end
]]
local tmpl_args = {
name = name,
body = snippets.render_record_properties(items),
indent = snippets.indent,
}
return utils.do_or_fail(template.substitute, tmpl, tmpl_args)
end
return snippets return snippets

View File

@ -1,10 +1,12 @@
local Function_Info = require "entity.Function_Info" local Function_Info = require "entity.Function_Info"
local List = require "pl.List" local List = require "pl.List"
local logger = require "logger" local logger = require "logger"
local Map = require "pl.Map"
local Module_Doc = require "entity.Module_Doc" local Module_Doc = require "entity.Module_Doc"
local scan = require "web_sanitize.query.scan_html" local scan = require "web_sanitize.query.scan_html"
local scraper_utils = require "scraper.utils" local scraper_utils = require "scraper.utils"
local stringx = require "pl.stringx" local stringx = require "pl.stringx"
local Type_Info = require "entity.Type_Info"
local utils = require "utils" local utils = require "utils"
local Variable_Info = require "entity.Variable_Info" local Variable_Info = require "entity.Variable_Info"
@ -14,34 +16,82 @@ local function extract_node_text(node: scan.HTMLNode): string
return utils.sanitize_string(node:inner_text()) return utils.sanitize_string(node:inner_text())
end end
local function parse_parameter_types(parameter_type: string): List<string> local function parse_parameter_types(parameter_type: string): List<Type_Info.Type_Info>
if parameter_type == "" then if parameter_type == "" then
return List({ "any" }) local type_info: Type_Info.Type_Info = { name = "any" }
return List({ type_info })
end end
return stringx.split(parameter_type, " or "):map(utils.sanitize_string) return stringx.split(parameter_type, " or "):map(
function(type_name: string): Type_Info.Type_Info
return { name = utils.sanitize_string(type_name) }
end
)
end end
local function extract_item_name(item_name_node: scan.HTMLNode): string local function extract_item_name(item_name_node: scan.HTMLNode): string
return item_name_node and ((item_name_node.attr.name as string):gsub("^.*[%.:]", "")) return item_name_node and ((item_name_node.attr.name as string):gsub("^.*[%.:]", ""))
end end
local function extract_function_parameters(function_parameters_node: scan.HTMLNode): { Function_Info.Parameter } local function extract_function_parameter_Parameters(tr_node: scan.HTMLNode): { Function_Info.Parameter }
local query_selectors = { local query_selectors = {
name = "span.parameter", name = "span.parameter",
types = "span.types" types = "span.types"
} }
return scraper_utils.scrape_tuples( return scraper_utils.scrape_tuples(
function_parameters_node:outer_html(), tr_node:outer_html(),
{ query_selectors.name, query_selectors.types }, { query_selectors.name, query_selectors.types },
function(nodes: { string : scan.HTMLNode | nil }): Function_Info.Parameter function(nodes: { string : scan.HTMLNode | nil }): Function_Info.Parameter
return { return {
name = extract_node_text(nodes[query_selectors.name]), name = extract_node_text(nodes[query_selectors.name] as scan.HTMLNode),
types = parse_parameter_types(extract_node_text(nodes[query_selectors.types])), types = parse_parameter_types(extract_node_text(nodes[query_selectors.types] as scan.HTMLNode)),
} }
end end)
) end
local function extract_function_parameters(function_parameters_node: scan.HTMLNode): { Function_Info.Parameter }
local current_record_parameter: Type_Info.Type_Info | nil = nil
return scraper_utils.scrape(
function_parameters_node:outer_html(),
"tr",
function(line_node: scan.HTMLNode): Function_Info.Parameter
local parameters = extract_function_parameter_Parameters(line_node)
if #parameters == 0 then
return nil
elseif #parameters ~= 1 then
log:error(logger.message_with_metadata("Expected 1 parameter by <tr> node",
{ len = #parameters, line_node = line_node, parameters = parameters }))
error("Expected 1 parameter by <tr> node")
end
local name, types = parameters[1].name, parameters[1].types
if line_node.attr ~= nil and line_node.attr.class == "see_also_sublist" and current_record_parameter then
local record_parameter = current_record_parameter as Type_Info.Type_Info
if not record_parameter.record_entries then
record_parameter.record_entries = Map()
end
(record_parameter.record_entries as Map<string, List<Type_Info.Type_Info>>):set(name, types)
return nil
end
if #types == 1 and types[1].name == "table" then
local record_name = utils.capitalize(name)
current_record_parameter = Type_Info(record_name)
return {
name = name,
types = List({ current_record_parameter }),
}
end
return {
name = name,
types = types,
}
end)
end end
local function extract_function_return_types(function_return_types_node: scan.HTMLNode): { string } local function extract_function_return_types(function_return_types_node: scan.HTMLNode): { string }
@ -107,7 +157,8 @@ local function extract_section_variables(dl: string): { Variable_Info.Variable_I
variable_info.name = extract_item_name(nodes[query_selectors.variable_name]) variable_info.name = extract_item_name(nodes[query_selectors.variable_name])
variable_info.types = parse_parameter_types(extract_node_text(nodes[query_selectors.variable_summary_type])) variable_info.types = parse_parameter_types(extract_node_text(nodes[query_selectors.variable_summary_type]))
if variable_info.types:contains("string") then if #variable_info.types == 1 and variable_info.types[1].name == "string" then
log:debug("extract variable string with constraints, this is an enum")
variable_info.constraints = List(extract_property_constraints(nodes[query_selectors.variable_property_constraint])):map( variable_info.constraints = List(extract_property_constraints(nodes[query_selectors.variable_property_constraint])):map(
function(constraint: string): string function(constraint: string): string
return (constraint:gsub("&quot;", "")) return (constraint:gsub("&quot;", ""))
@ -141,6 +192,12 @@ function module.get_doc_from_page(html: string, module_name: string): Module_Doc
local module_doc = Module_Doc() local module_doc = Module_Doc()
module_doc.record_name = utils.capitalize((module_name:gsub(".*%.", ""))) module_doc.record_name = utils.capitalize((module_name:gsub(".*%.", "")))
local self_type: Type_Info.Type_Info = { name = module_doc.record_name }
local self_parameter: Function_Info.Parameter = {
name = "self",
types = List({ self_type }),
}
for i = 1, #nodes:get("h2.section-header") do for i = 1, #nodes:get("h2.section-header") do
local h2 = nodes:get("h2.section-header")[i] local h2 = nodes:get("h2.section-header")[i]
local section_name = utils.sanitize_string(h2:inner_text()) local section_name = utils.sanitize_string(h2:inner_text())
@ -156,10 +213,7 @@ function module.get_doc_from_page(html: string, module_name: string): Module_Doc
log:warn("Not implemented: Deprecated object properties") log:warn("Not implemented: Deprecated object properties")
elseif section_name == "Object methods" then elseif section_name == "Object methods" then
module_doc.methods = List(extract_section_functions(dl_html)):map(function(method: Function_Info.Function_Info): Function_Info.Function_Info module_doc.methods = List(extract_section_functions(dl_html)):map(function(method: Function_Info.Function_Info): Function_Info.Function_Info
method.parameters:insert(1, { method.parameters:insert(1, self_parameter)
name = "self",
types = List({ module_doc.record_name }),
})
return method return method
end) end)
elseif section_name == "Signals" then elseif section_name == "Signals" then

View File

@ -43,6 +43,10 @@ function utils.capitalize(s: string): string
return (s:gsub("^%l", string.upper)) return (s:gsub("^%l", string.upper))
end end
function utils.is_empty(s: string): boolean
return s == nil or s == ""
end
-- At some point, we should probably write a wrapper to make penlight's function work with pcalls. -- At some point, we should probably write a wrapper to make penlight's function work with pcalls.
function utils.do_or_fail<T>(func: function<T>(...: any): (T | nil, string), ...: any): T function utils.do_or_fail<T>(func: function<T>(...: any): (T | nil, string), ...: any): T
local logger = require "logger" local logger = require "logger"

View File

@ -71,7 +71,7 @@ local record pl
keys: pl.List<K> keys: pl.List<K>
values: pl.List<V> values: pl.List<V>
iter: function(pl.Map<K, V>): (function(): {K, V}) iter: function(pl.Map<K, V>): (function(): K, V)
items: function(pl.Map<K, V>): (function(): {K, V}) items: function(pl.Map<K, V>): (function(): {K, V})
get: function(pl.Map<K, V>, K): V get: function(pl.Map<K, V>, K): V