Module:Module introspection: Difference between revisions

Ganaram inukshuk (talk | contribs)
bugfix
ArrowHead294 (talk | contribs)
mNo edit summary
 
(29 intermediate revisions by 2 users not shown)
Line 1: Line 1:
-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]]
-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]]
local getArgs = require("Module:Arguments").getArgs
local getArgs = require("Module:Arguments").getArgs
local iutils  = require("Module:Introspection utils")
local yesno  = require("Module:Yesno")


local p = {}
local p = {}
-- TODO:
-- - Identify the wrapper function, if there is one (which is usually the case).
--  If a wrapper exists without an accompanying main, then the wrapper is
--  assumed to be the main function.
-- - Identify invokable functions, which are functions that accept a single
--  param called "frame".
-- - Allow descriptions to be added to functions, as params called
--  desc_function_name. One description per function.
-- Inspects a module for its functions, its dependencies, and the functions used
-- from those dependencies.
-- CURRENT BUGS DEEMED NON-ISSUES AT TIME OF WRITING:
-- If strings contain actual code, they will be treated as part of the code.
-- This is considered a non-issue since no modules should (nor did they at time
-- of writing) contain actual lua code as a literal string (code but enclosed in
-- quotes).
-- Helper function
-- Blanks comments but preserves line numbers
function p.strip_comments(code_unstripped)
if not code_unstripped then return "" end
local lines = {}
local in_multiline = false
local end_pattern
for line in code_unstripped:gmatch("([^\n]*)\n") do
local processed = line
if in_multiline then
-- Check for end of multi-line comment first
local s, e = processed:find(end_pattern)
if s then
in_multiline = false
-- Replace only the comment part with spaces
processed = string.rep(" ", e) .. processed:sub(e + 1)
else
-- Entire line is inside comment
processed = processed:gsub(".", " ")
end
else
local start_eq = processed:match("%-%-%[(=*)%[")
if start_eq then
in_multiline = true
end_pattern = "%]" .. start_eq .. "%]"
-- Blank from the start of comment to the end of line
local s, e = processed:find("%-%-%[" .. start_eq .. "%[")
if s then
processed = string.rep(" ", #processed)
end
else
processed = processed:gsub("%-%-.*", function(s)
return string.rep(" ", #s)
end)
end
end
table.insert(lines, processed)
end
return table.concat(lines, "\n")
end
-- Helper function
-- Find dependencies for a module, given a preprocessed module's code, then use
-- that information to find every function call for each dependency.
function p.find_dependencies(code)
-- STEP 1
-- For each require line, get the dependency name (dep), the variable used
-- for that dependency (var), and, if applicable, the function used from
-- that dependency.
-- Only consider dependencies that have "Module:" in its name
local raw_deps = {} -- Dependencies used; unsorted and to be processed in step 2
for var, dep, direct_func in code:gmatch([[local%s+([%w_]+)%s*=%s*require%(%s*["']([^"']+)["']%s*%)%.?([%w_%.]*)]]) do
if dep:match("^Module:") then
if direct_func == "" then direct_func = nil end
raw_deps[var] = {
dep = dep,
direct_func = direct_func
}
end
end
-- STEP 2
-- For each dependency found, find all function calls that involve that
-- dependency, as var.func(), or var(), without duplicates. If at least one
-- function call was found, then that module is used.
local deps = {}  -- Final array result; to be sorted
for var, info in pairs(raw_deps) do
local seen  = {}  -- For detecting whether a function call was already found
local funcs = {}  -- For tracking all found function calls
if info.direct_func then
-- SPECIAL CASE 1: only one function imported from a package
if code:match(var .. "%s*%(") then
funcs = { info.direct_func }
end
else
-- EXPECTED CASE: multiple functions from package used
for func in code:gmatch(var .. "%.([%w_%.]+)%s*%(") do
if not seen[func] then
seen[func] = true
table.insert(funcs, func)
end
end
-- SPECIAL CASE 2: module returns a function and is callable
if #funcs == 0 and code:match(var .. "%s*%(") then
funcs = { var }
end
end
-- Add data to table
table.insert(deps, {
var  = var,
dep  = info.dep,
funcs = funcs
})
end
-- STEP 3: Sort alphabetically by dependency name
table.sort(deps, function(a, b)
return a.dep:lower() < b.dep:lower()
end)
return deps
end
-- Helper function
-- Find functions provided by a module, ignoring nested/local functions.
-- For each function found this way, find the params for that function.
function p.find_functions(code)
local funcs = {}
if not code then return funcs end
local line_num = 0
local module_name = nil
-- Functions of the form module_name.func are considered.
-- module_name is usually p, but it may be something else. Find that package
-- name, or fall back to p.
for var in code:gmatch([[local%s+([%w_]+)%s*=%s*{}]]) do
module_name = var
break
end
module_name = module_name or "p"
for line in code:gmatch("([^\n]*)\n?") do
line_num = line_num + 1
-- Match functions defined as: function p.name(param1, param2)
local name, params_str = line:match("function%s+" .. module_name .. "%.([%w_]+)%s*%(([^)]*)%)")
if name then
local params = {}
for param in params_str:gmatch("([%w_]+)") do
table.insert(params, param)
end
table.insert(funcs, { name = name, line = line_num, params = params })
end
-- Match functions defined as: p.name = function(param1, param2)
name, params_str = line:match(module_name .. "%.([%w_]+)%s*=%s*function%s*%(([^)]*)%)")
if name then
local params = {}
for param in params_str:gmatch("([%w_]+)") do
table.insert(params, param)
end
table.insert(funcs, { name = name, line = line_num, params = params })
end
end
return funcs
end


-- Helper function
-- Helper function
Line 187: Line 13:
local lines = {}
local lines = {}
table.insert(lines, '{| class="wikitable sortable"')
table.insert(lines, '{| class="wikitable sortable"')
table.insert(lines, "|+ Lua&nbsp;modules&nbsp;used&nbsp;" .. string.format("(%d)", #module_deps))
table.insert(lines, "|+ style=\"font-size: 105%;\" | " .. string.format("Lua&nbsp;modules&nbsp;required&nbsp;(%d)", #module_deps))
table.insert(lines, "|-")
table.insert(lines, "! Variable")
table.insert(lines, "! Variable")
table.insert(lines, "! Module")
table.insert(lines, "! Module")
Line 236: Line 63:
--table.insert(lines, string.format("'''Module:%s''' provides %d function(s):", module_name, #module_funcs))
--table.insert(lines, string.format("'''Module:%s''' provides %d function(s):", module_name, #module_funcs))
table.insert(lines, '{| class="wikitable sortable"')
table.insert(lines, '{| class="wikitable sortable"')
table.insert(lines, "|+ Functions&nbsp;provided&nbsp;" .. string.format("(%d)", #module_funcs))
table.insert(lines, "|+ style=\"font-size: 105%;\" | " .. string.format("Functions&nbsp;provided&nbsp;(%d)", #module_funcs))
table.insert(lines, "|-")
table.insert(lines, "! Line")
table.insert(lines, "! Line")
table.insert(lines, "! Function")
table.insert(lines, "! Function")
Line 265: Line 93:
-- Create text for function
-- Create text for function
local func = string.format("<code>%s</code>", info.name)
local func = ""
if info.name then
func = string.format("<code>%s</code>", info.name)
else
func = "''none''"
end
-- If the function is the main function, add "main" to that cell
-- If the function is the main function, add "main" to that cell
if info.name == main_function then
if info.name == main_function then
func = func .. " '''(main)'''"
func = func .. " '''(main)'''"
end
-- If the function is invokable (it has one param called "frame"), add
-- "invokable" to that cell
if #params == 1 and params[1] == "frame" then
func = func .. " '''(invokable)'''"
end
end
Line 285: Line 124:
return table.concat(lines, "\n")
return table.concat(lines, "\n")
end
end
-- Helper function: determines whether module has a main function; if it does,
-- it indicates that it's not a library function and provides specific function-
-- ality, usually for a template.


-- Main function; to be called by wrapper
-- Main function; to be called by wrapper
function p._module_introspection(args)
function p._module_introspection(args)
local args = args or {}
local args = args or {}
local module_name  = args["Module Name"  ]
local module_name  = args["module_name"  ]
local main_function = args["Main Function"]
local main_function = args["main_function"]
local descriptions  = args["Descriptions"]
local descriptions  = args["descriptions" ]
local is_doc = args["is_doc"]
-- Check whether this page is a docpage
-- If so, don't bother
if is_doc then
return "''To see introspection summary, see this module's main page.''"
end
-- Preprocess module and blank-out comments
-- Preprocess module and blank-out comments
local title = mw.title.new('Module:' .. module_name)
--local title = mw.title.new('Module:' .. module_name)
local code = title:getContent()
--local code = title:getContent()
code = p.strip_comments(code) -- Blank-out comments
--code = iutils.preprocess_code(code) -- Blank-out comments
local code = iutils.get_and_preprocess_content("Module", module_name)
-- Get dependencies and their functions used, then build a table
-- Get dependencies and their functions used, then build a table
local module_deps = p.find_dependencies(code)
local module_deps = iutils.find_dependencies(code)
local dep_table = p.make_dependency_table(module_deps)
local dep_table = p.make_dependency_table(module_deps)


-- Get module's functions, then build a table using that information
-- Get module's functions, then build a table using that information
local module_funcs = p.find_functions(code)
local module_funcs = iutils.find_functions(code)
local func_table = p.make_function_table(module_name, module_funcs, descriptions, main_function)
local func_table = p.make_function_table(module_name, module_funcs, descriptions, main_function)
-- Count the number of modules used


-- Return the tables
-- Return the tables
-- Styling may be improved at a later time
-- Styling may be improved at a later time
local combined_tables = table.concat({
local lines = {
'{| class="wikitable mw-collapsible"',
'{| class="wikitable mw-collapsible"',
'! colspan="2" | Introspection summary for Module:' .. module_name .. "&nbsp;",
'|-',
'! colspan="2" style="font-size: 105%;" | ' .. string.format("Introspection summary for Module:%s&nbsp;", module_name),
"|-",
"|-",
'| style="vertical-align:top; border-right:none"; |',
'| style="vertical-align: top; border-right: none;" | ',
func_table,
func_table,
'| style="vertical-align:top; border-left:none" |',
'| style="vertical-align: top; border-left: none;" | ',
dep_table,
dep_table,
"|}"
"|}"
}, "\n")
}


return combined_tables
-- Check whether descriptions was passed in
local has_descriptions = false
if type(descriptions) == "table" then
for _, _ in pairs(descriptions) do
has_descriptions = true
break
end
end
-- If no descriptions were provided, add text below that points to the code.
if not has_descriptions then
table.insert(lines, "''No function descriptions were provided. The Lua code may have further information.''")
end
 
return table.concat(lines, "\n")
end
end


Line 335: Line 190:


-- Get module name from arguments, or default to current page
-- Get module name from arguments, or default to current page
local module_name = args["Module Name"] or mw.title.getCurrentTitle().text
local module_name = args["module_name"] or mw.title.getCurrentTitle().text


-- Strip trailing "/doc" if the template is used on a documentation subpage
-- Strip trailing "/doc" if the template is used on a documentation subpage
module_name = module_name:gsub("/doc$", "")
--module_name = module_name:gsub("/doc$", "")
-- Check whether page is the doc page
-- To ensure proper referencing, do not display introspection on a docpage.
local is_doc = string.find(module_name, "/doc$")
-- Normalize module name so it can be used to find the main function, which
-- Normalize module name so it can be used to find the main function, which
Line 344: Line 203:
-- is used for function names. (If this fails, it can be entered manually.)
-- is used for function names. (If this fails, it can be entered manually.)
local normalized_name = module_name:gsub("[^%w]", "_"):lower()
local normalized_name = module_name:gsub("[^%w]", "_"):lower()
local main_function = args["Main Function"] or "_" .. normalized_name
local main_function = args["main_function"] or "_" .. normalized_name
-- Process all function descriptions, if provided as desc_function_name
-- Process all function descriptions, if provided as desc_function_name
Line 356: Line 215:


-- Return
-- Return
return p._module_introspection({
local result = p._module_introspection({
["Module Name"]   = module_name,
["module_name" ] = module_name,
["Main Function"] = main_function,
["main_function"] = main_function,
["Descriptions"] = func_descriptions
["descriptions" ] = func_descriptions,
["is_doc"] = is_doc
})
})
    -- Debugger option to show generated WikiText
    local wtext = yesno(frame.args["wtext"] or args["wtext"])
if wtext == true then
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
end
return frame:preprocess(result)
end
end


return p
return p