Module:Module introspection: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
m add module name to table
ArrowHead294 (talk | contribs)
mNo edit summary
 
(52 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: add additional functions:
-- - Determine whether a main function exists, as _main or a function of the
--  same name as the module with an underscore.
-- - Determine whether a wrapper exists, as main or a function of the same name
--  as the module, without an underscore.
-- - If the wrapper exists without _main, the wrapper is assumed to be the main
--  function.
-- - If both wrapper and _main exists, _main is the main function.
-- - If neither function exists, then the module is a metamodule, a module that
--  generally provides functionality to other modules.
-- Inspects a module for its functions, its dependencies, and the functions used
-- from those dependencies.
-- CURRENT BUGS:
-- If strings contain actual code, they will be treated as part of the code.
-- This should be a non-issue as long as no module adds any actual code, or if
-- it does, it's broken into enough pieces that it won't be detected as proper
-- code. This is currently a non-issue since no modules (currently) have code
-- in strings.
-- 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)
local deps = {} -- Dependencies used
-- 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.
-- A require line looks like: local var = require("Module:Dependency").func,
-- where ".func" is optional.
for var, dep, func in code:gmatch([[local%s+([%w_]+)%s*=%s*require%(%s*["']([^"']+)["']%s*%)%.?([%w_%.]*)]]) do
if func == "" then func = nil end -- If func was blank, replace with nil instead
deps[var] = {
dep = dep,
funcs = { },
direct_func = func -- For detecting whether one function out of a package was used
}
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.
for var, info in pairs(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
info.funcs = funcs
end
return deps
end


-- Helper function
-- Helper function
-- Given the output of the above function, create a mediawiki table
-- Given the output of the above function, create a mediawiki table
function p.make_dependency_table(module_deps)
function p.make_dependency_table(module_deps)
-- Count the number of dependencies
local num_deps = 0
for k, v in pairs(module_deps) do
num_deps = num_deps + 1
end


-- Table headers
-- Table headers
local lines = {}
local lines = {}
table.insert(lines, '{| class="wikitable sortable"')
table.insert(lines, '{| class="wikitable sortable"')
table.insert(lines, "|+ Modules used " .. string.format("(%d)", num_deps))
table.insert(lines, "|+ style=\"font-size: 105%;\" | " .. string.format("Lua modules required (%d)", #module_deps))
table.insert(lines, "|-")
table.insert(lines, "! Variable")
table.insert(lines, "! Variable")
table.insert(lines, "! Module")
table.insert(lines, "! Module")
table.insert(lines, "! Functions used")
table.insert(lines, "! Functions used")


-- Table rows
-- Table rows (assuming they're all alphabetized)
for var, info in pairs(module_deps) do
for _, info in ipairs(module_deps) do
local funcs_text
local funcs_text
if #info["funcs"] == 0 then
if #info.funcs == 0 then
funcs_text = "''dependency not used''"
funcs_text = "''dependency not used''"
else
else
funcs_text = table.concat(info.funcs, "<br />")
local func_lines = {}
for _, func in ipairs(info.funcs) do
table.insert(func_lines, string.format("<code>%s</code>", func))
end
funcs_text = table.concat(func_lines, "<br />")
end
end


table.insert(lines, "|-")
table.insert(lines, "|-")
table.insert(lines, "| " .. var)
table.insert(lines, "| " .. info.var)
table.insert(lines, "| [[" .. info.dep .. "]]")
table.insert(lines, "| [[" .. info.dep .. "]]")
table.insert(lines, "| " .. funcs_text)
table.insert(lines, "| " .. funcs_text)
Line 165: Line 44:
return table.concat(lines, "\n")
return table.concat(lines, "\n")
end
end


-- Helper function
-- Helper function
-- Find functions provided by a module, ignoring nested/local functions
-- Lists module's own functions; requires module name to produce links to each
function p.find_functions(code)
-- function.
 
function p.make_function_table(module_name, module_funcs, descriptions, main_function)
-- Iterate through file and find each function and the line found at
local funcs = {}
-- Check whether descriptions was passed in
if code then
local has_descriptions = false
local line_num = 0
if type(descriptions) == "table" then
for line in code:gmatch("([^\n]*)\n?") do -- Make sure gmatch does not skip blank lines
for _, _ in pairs(descriptions) do
line_num = line_num + 1
has_descriptions = true
 
break
-- 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
end
end
return funcs
end
-- Helper function
-- Lists module's own functions; requires module name to produce links to each
-- function.
function p.make_function_table(module_name, module_funcs, main_function)
-- Table headers
-- Table headers
Line 204: 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, "! Function")
table.insert(lines, "! Function")
table.insert(lines, "! Line")
table.insert(lines, "! Params")
-- If there are descriptions, add a column for that
if has_descriptions then
table.insert(lines, "! Description")
end


-- Table rows
-- Table rows
for _, info in ipairs(module_funcs) do
for _, info in ipairs(module_funcs) do
local link = string.format("[[Module:%s#L-%d|%s]]", module_name, info.line, info.name)
-- Find params for that function, or say "none" if none
local params = {}
for _, param in ipairs(info.params) do
table.insert(params, param)
end
local params_string = ""
if #params == 0 then
params_string = "''none''"
else
params_string = string.format("<code>(%s)</code>", table.concat(params, ", "))
end
-- Create link to line for that function
local line_num = string.format("[[Module:%s#L-%d|%d]]", module_name, info.line, info.line)
-- Create text for function
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
link = link .. " '''(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
table.insert(lines, "|-")
table.insert(lines, "|-")
table.insert(lines, "| " .. link)
table.insert(lines, "| " .. line_num)
table.insert(lines, "| " .. info.line)
table.insert(lines, "| " .. func)
table.insert(lines, "| " .. params_string)
if has_descriptions then
table.insert(lines, "| " .. (descriptions[info.name] or ""))
end
end
end
table.insert(lines, "|}")
table.insert(lines, "|}")
Line 225: 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
Line 235: Line 130:
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 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, 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,
'|-',
'! 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")
}
 
-- 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 combined_tables
return table.concat(lines, "\n")
end
end


Line 277: 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 284: Line 204:
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
local func_descriptions = {}
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
-- 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,
["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

Latest revision as of 20:23, 3 December 2025

Module documentation[view] [edit] [history] [purge]
This module should not be invoked directly; use its corresponding template instead: Template:Module introspection.
Module:Module introspection is ready for use. This message indicates that a module is ready for use, or has recently been repaired. This message may be removed once this module has been used on several pages or once it is verified to work as intended.

Details: Edge-case observation still ongoing. Currently cannot detect data provided by a module.

Introspection summary for Module:Module introspection 
Functions provided (4)
Line Function Params
10 make_dependency_table (module_deps)
50 make_function_table (module_name, module_funcs, descriptions, main_function)
128 _module_introspection (main) (args)
187 module_introspection (invokable) (frame)
Lua modules required (3)
Variable Module Functions used
getArgs Module:Arguments getArgs
iutils Module:Introspection utils get_and_preprocess_content
find_dependencies
find_functions
yesno Module:Yesno yesno

No function descriptions were provided. The Lua code may have further information.


-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]]
local getArgs = require("Module:Arguments").getArgs
local iutils  = require("Module:Introspection utils")
local yesno   = require("Module:Yesno")

local p = {}

-- Helper function
-- Given the output of the above function, create a mediawiki table
function p.make_dependency_table(module_deps)

	-- Table headers
	local lines = {}
	table.insert(lines, '{| class="wikitable sortable"')
	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, "! Module")
	table.insert(lines, "! Functions&nbsp;used")

	-- Table rows (assuming they're all alphabetized)
	for _, info in ipairs(module_deps) do
		local funcs_text
		if #info.funcs == 0 then
			funcs_text = "''dependency not used''"
		else
			local func_lines = {}
			for _, func in ipairs(info.funcs) do
				table.insert(func_lines, string.format("<code>%s</code>", func))
			end
			funcs_text = table.concat(func_lines, "<br />")
		end

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

	-- Table footer
	table.insert(lines, "|}")

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

-- Helper function
-- Lists module's own functions; requires module name to produce links to each
-- function.
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
	
	-- Table headers
	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")
	
	-- If there are descriptions, add a column for that
	if has_descriptions then
		table.insert(lines, "! Description")
	end

	-- Table rows
	for _, info in ipairs(module_funcs) do
		
		-- Find params for that function, or say "none" if none
		local params = {}
		for _, param in ipairs(info.params) do
			table.insert(params, param)	
		end
		local params_string = ""
		if #params == 0 then
			params_string = "''none''"
		else
			params_string = string.format("<code>(%s)</code>", table.concat(params, ", "))
		end
		
		-- Create link to line for that function
		local line_num = string.format("[[Module:%s#L-%d|%d]]", module_name, info.line, info.line)
		
		-- Create text for function
		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 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
	table.insert(lines, "|}")
	
	return table.concat(lines, "\n")
end

-- Main function; to be called by wrapper
function p._module_introspection(args)
	local args = args or {}
	local module_name   = args["module_name"  ]
	local main_function = args["main_function"]
	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
	--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)

	-- 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)

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

	-- 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

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

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

	-- Strip trailing "/doc" if the template is used on a documentation subpage
	--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
	-- is assumed to be the same name as the module. Module assumes snake_case
	-- is used for function names. (If this fails, it can be entered manually.)
	local normalized_name = module_name:gsub("[^%w]", "_"):lower()
	local main_function = args["main_function"] or "_" .. normalized_name
	
	-- Process all function descriptions, if provided as desc_function_name
	local func_descriptions = {}
	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,
		["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

return p