Module:Module introspection: Difference between revisions

Ganaram inukshuk (talk | contribs)
try to bugfix line count again
ArrowHead294 (talk | contribs)
mNo edit summary
 
(108 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 = {}
-- Inspects a module for its functions, its dependencies, and the functions used
-- from those dependencies.


-- Helper function
-- Helper function
-- Blanks comments but preserves line numbers
-- Given the output of the above function, create a mediawiki table
function p.strip_comments(content)
function p.make_dependency_table(module_deps)
if not content then return "" end


-- Table headers
local lines = {}
local lines = {}
local in_multiline = false
table.insert(lines, '{| class="wikitable sortable"')
local end_pattern
table.insert(lines, "|+ style=\"font-size: 105%;\" | " .. string.format("Lua modules required (%d)", #module_deps))
 
table.insert(lines, "|-")
for line in content:gmatch("([^\n]*)\n") do
table.insert(lines, "! Variable")
local processed = line
table.insert(lines, "! Module")
table.insert(lines, "! Functions used")


if in_multiline then
-- Table rows (assuming they're all alphabetized)
-- inside a multi-line comment: replace everything with spaces
for _, info in ipairs(module_deps) do
processed = processed:gsub(".", " ")
local funcs_text
-- check for end of multi-line comment
if #info.funcs == 0 then
if processed:find(end_pattern) then
funcs_text = "''dependency not used''"
in_multiline = false
end
else
else
-- check for start of multi-line comment
local func_lines = {}
local start_eq = processed:match("%-%-%[(=*)%[")
for _, func in ipairs(info.funcs) do
if start_eq then
table.insert(func_lines, string.format("<code>%s</code>", func))
in_multiline = true
end_pattern = "%]" .. start_eq .. "%]"
processed = processed:gsub(".", " ")
else
-- replace single-line comments
processed = processed:gsub("%-%-.*", function(s)
return string.rep(" ", #s)
end)
end
end
funcs_text = table.concat(func_lines, "<br />")
end
end


table.insert(lines, processed)
table.insert(lines, "|-")
table.insert(lines, "| " .. info.var)
table.insert(lines, "| [[" .. info.dep .. "]]")
table.insert(lines, "| " .. funcs_text)
end
end


-- handle last line if it doesn’t end with newline
-- Table footer
local last_line = content:match("([^\n]+)$")
table.insert(lines, "|}")
if last_line then
local processed = last_line
if in_multiline then
processed = processed:gsub(".", " ")
else
processed = processed:gsub("%-%-.*", function(s) return string.rep(" ", #s) end)
end
table.insert(lines, processed)
end


-- Join all lines into a single string
return table.concat(lines, "\n")
return table.concat(lines, "\n")
end
end


-- Helper function
-- Helper function
-- List dependencies for a module
-- Lists module's own functions; requires module name to produce links to each
function p.list_dependencies(module_name)
-- function.
local module_name = module_name --or "MOS" -- Test arg; comment out when not testing
function p.make_function_table(module_name, module_funcs, descriptions, main_function)
-- 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
local title = mw.title.new('Module:' .. module_name)
-- Table headers
local content = title:getContent()
local lines = {}
--table.insert(lines, string.format("'''Module:%s''' provides %d function(s):", module_name, #module_funcs))
table.insert(lines, '{| class="wikitable sortable"')
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, "! Function")
table.insert(lines, "! Params")
-- Blank-out comments
-- If there are descriptions, add a column for that
content = p.strip_comments(content)
if has_descriptions then
 
table.insert(lines, "! Description")
-- Get dependencies for that module
-- It's the text from each "require('Module:aaaaa')" line.
local deps = {}
for dep in content:gmatch("require%s*%(%s*['\"]Module:(.-)['\"]%s*%)") do
table.insert(deps, dep)
end
end
return deps
end


-- Helper function
-- Table rows
-- List functions used from each dependency
for _, info in ipairs(module_funcs) do
-- This assumes dependency functions are used in the format of: var.func(),
-- or var() if the module is included as var = require("Module:Dep") if it
-- Find params for that function, or say "none" if none
-- returns a function or var = require("Module:Dep").func_name if a specific
local params = {}
-- function from that module is used.
for _, param in ipairs(info.params) do
-- Optionally accepts a table of its dependencies; if not provided, it will find
table.insert(params, param)
-- them itself by calling list_dependencies
end
function p.list_dependency_functions(module_name, deps)
local params_string = ""
local module_name = module_name --or "MOS" -- Test arg; comment out when not testing
if #params == 0 then
 
params_string = "''none''"
-- Get dependencies
else
local deps = p.list_dependencies(module_name)
params_string = string.format("<code>(%s)</code>", table.concat(params, ", "))
end
-- Load module
local title = mw.title.new('Module:' .. module_name)
-- Create link to line for that function
local content = title:getContent()
local line_num = string.format("[[Module:%s#L-%d|%d]]", module_name, info.line, info.line)
 
-- Step 1: Find all require statements with optional .function
-- Create text for function
local results = {}
local func = ""
for var, dep, entry in content:gmatch(
if info.name then
"local%s+([%w_]+)%s*=%s*require%s*%(%s*['\"]Module:(.-)['\"]%s*%)%.?([%w_]*)"
func = string.format("<code>%s</code>", info.name)
) do
local used = {}
 
-- Step 2: Check how it's used
if entry ~= "" then
-- The module returned a single function: var() usage
for _ in content:gmatch(var .. "%s*%(") do
used[entry] = true
end
else
else
-- The variable is a module table: var.func() usage
func = "''none''"
for func in content:gmatch(var .. "%.(%w+)%s*%(") do
end
used[func] = true
end
-- If the function is the main function, add "main" to that cell
if info.name == main_function then
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
table.insert(lines, "|-")
table.insert(lines, "| " .. line_num)
table.insert(lines, "| " .. func)
table.insert(lines, "| " .. params_string)
if has_descriptions then
table.insert(lines, "| " .. (descriptions[info.name] or ""))
end
end
-- Step 3: Build result entry
local funcList = {}
for f in pairs(used) do table.insert(funcList, f) end
table.sort(funcList)
results[dep] = results[dep] or {}
table.insert(results[dep], {
variable = var,
entry = (entry ~= "" and entry or nil),
functions = funcList
})
end
end
 
table.insert(lines, "|}")
return results
end
 
-- Helper function
-- List functions provided by a module
-- Only returned functions are considered, not local functions
function p.list_functions(module_name)
local module_name = module_name --or "MOS" -- Test arg; comment out when not testing
-- Load module
return table.concat(lines, "\n")
local title = mw.title.new('Module:' .. module_name)
local content = title:getContent()
-- Blank-out comments
content = p.strip_comments(content)
 
-- Iterate through file and find each function and the line found at
local funcs = {}
if content then
local line_num = 0
for line in content:gmatch("([^\n]*)\n?") do -- Make sure gmatch does not skip blank lines
line_num = line_num + 1
 
-- Match functions defined as function p.name(
local name = line:match("function%s+[%w_]+%.([%w_]+)%s*%(")
if name then
table.insert(funcs, {name = name, line = line_num})
end
 
-- Match functions defined as p.name = function(
name = line:match("[%w_]+%.([%w_]+)%s*=%s*function%s*%(")
if name then
table.insert(funcs, {name = name, line = line_num})
end
end
end
 
return funcs
end
end


Line 172: Line 128:
function p._module_introspection(args)
function p._module_introspection(args)
local args = args or {}
local args = args or {}
local module_name  = args["module_name"  ] or "MOS"
local module_name  = args["module_name"  ]
local main_function = args["main_function"]
local main_function = args["main_function"]
 
local descriptions  = args["descriptions" ]
    -- Get module functions
local is_doc = args["is_doc"]
    local module_functions = p.list_functions(module_name)
 
-- Check whether this page is a docpage
    -- Get dependencies and functions used from each
-- If so, don't bother
    local dep_functions = p.list_dependency_functions(module_name)
if is_doc then
 
return "''To see introspection summary, see this module's main page.''"
    -- Build MediaWiki table for dependencies
    local dep_lines = {}
table.insert(dep_lines, '{| class="wikitable sortable"')
table.insert(dep_lines, '|+ Dependencies and functions used')
table.insert(dep_lines, "! Dependency")
table.insert(dep_lines, "! Variable")
table.insert(dep_lines, "! Function(s) used")
 
    -- Include all dependencies even if no usage is detected
local all_deps = p.list_dependencies(module_name)
for _, dep in ipairs(all_deps) do
    local dep_link = string.format("[[Module: %s]]", dep)
    local usages = dep_functions[dep]
    if usages and #usages > 0 then
        for _, usage in ipairs(usages) do
            local func_str = table.concat(usage.functions, ", ")
            if usage.entry then
                func_str = usage.entry .. (func_str ~= "" and (", " .. func_str) or "")
            end
            table.insert(dep_lines, "|-")
            table.insert(dep_lines, "| " .. dep_link)
            table.insert(dep_lines, "| " .. usage.variable)
            table.insert(dep_lines, "| " .. (func_str ~= "" and func_str or "''dependency not used''"))
        end
    else
        table.insert(dep_lines, "|-")
        table.insert(dep_lines, "| " .. dep_link)
        table.insert(dep_lines, "| -")
        table.insert(dep_lines, "| ''dependency not used''")
    end
end
end
-- Preprocess module and blank-out comments
--local title = mw.title.new('Module:' .. module_name)
--local code = title:getContent()
--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
local module_deps = iutils.find_dependencies(code)
local dep_table = p.make_dependency_table(module_deps)


    table.insert(dep_lines, '|}')
-- Get module's functions, then build a table using that information
local module_funcs = iutils.find_functions(code)
local func_table = p.make_function_table(module_name, module_funcs, descriptions, main_function)


    -- Build MediaWiki table for module's own functions
-- Return the tables
local func_lines = {}
-- Styling may be improved at a later time
local func_class = "wikitable sortable mw-collapsible"
local lines = {
if #module_functions > 20 then
'{| class="wikitable mw-collapsible"',
    func_class = func_class .. " mw-collapsed"
'|-',
end
'! colspan="2" style="font-size: 105%;" | ' .. string.format("Introspection summary for Module:%s&nbsp;", module_name),
table.insert(func_lines, "{| class=\"" .. func_class .. "\"")
"|-",
table.insert(func_lines, "|+ Functions provided by this module")
'| style="vertical-align: top; border-right: none;" | ',
table.insert(func_lines, "! Function")
func_table,
table.insert(func_lines, "! Line")
'| style="vertical-align: top; border-left: none;" | ',
dep_table,
"|}"
}


    for _, f in ipairs(module_functions) do
-- Check whether descriptions was passed in
        local link = string.format("[[Module:%s#L-%d|%s]]", module_name, f.line, f.name)
local has_descriptions = false
       
if type(descriptions) == "table" then
-- If the function is the main function, add "main" to that cell
for _, _ in pairs(descriptions) do
if f.name == main_function then
has_descriptions = true
link = link .. " '''(main)'''"
break
end
end
       
        table.insert(func_lines, "|-")
        table.insert(func_lines, "| " .. link)
        table.insert(func_lines, "| " .. f.line)
end
end
table.insert(func_lines, "|}")
-- 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 the tables as strings
return table.concat(lines, "\n")
return table.concat(dep_lines, "\n"), table.concat(func_lines, "\n")
end
end


-- Wrapper function for modules
-- Wrapper function for modules
function p.module_introspection(frame)
function p.module_introspection(frame)
-- Extract arguments using getArgs
-- Extract arguments using getArgs
local args = getArgs(frame)
local args = getArgs(frame) or {}


-- Get module name from arguments, or default to current page
-- Get module name from arguments, or default to current page
Line 254: Line 193:


-- 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 262: Line 205:
local main_function = args["main_function"] or "_" .. normalized_name
local main_function = args["main_function"] or "_" .. normalized_name
-- Call the introspection function
-- Process all function descriptions, if provided as desc_function_name
local dep_table, func_table = p._module_introspection({
local func_descriptions = {}
["module_name"]   = module_name,
for key, val in pairs(args) do
local func_name = key:match("^desc_([%w_]+)$")
if func_name and val and val ~= "" then
func_descriptions[func_name] = val
end
end
 
-- Return
local result = p._module_introspection({
["module_name" ] = module_name,
["main_function"] = main_function,
["main_function"] = main_function,
["descriptions" ] = func_descriptions,
["is_doc"] = is_doc
})
})


-- Return combined tables
    -- Debugger option to show generated WikiText
return dep_table .. "\n" .. func_table .. "\n"
    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