Module:Module introspection

From Xenharmonic Wiki
Revision as of 21:02, 26 October 2025 by Ganaram inukshuk (talk | contribs) (a colon)
Jump to navigation Jump to search
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
Functions provided (7)
Function Line
strip_comments 29
find_dependencies 76
make_dependency_table 130
find_functions 171
make_function_table 200
_module_introspection (main) 233
module_introspection 271
Modules used (1)
Variable Module Functions used
getArgs Module:Arguments getArgs

-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]]
local getArgs = require("Module:Arguments").getArgs
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
-- Given the output of the above function, create a mediawiki table
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
	local lines = {}
	table.insert(lines, '{| class="wikitable sortable"')
	table.insert(lines, "|+ Modules used " .. string.format("(%d)", num_deps))
	table.insert(lines, "! Variable")
	table.insert(lines, "! Module")
	table.insert(lines, "! Functions used")

	-- Table rows
	for var, info in pairs(module_deps) do
		local funcs_text
		if #info["funcs"] == 0 then
			funcs_text = "''dependency not used''"
		else
			funcs_text = table.concat(info.funcs, "<br />")
		end

		table.insert(lines, "|-")
		table.insert(lines, "| " .. 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
-- Find functions provided by a module, ignoring nested/local functions
function p.find_functions(code)

	-- Iterate through file and find each function and the line found at
	local funcs = {}
	if code then
		local line_num = 0
		for line in code: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

-- 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
	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, "|+ Functions&nbsp;provided&nbsp;" .. string.format("(%d)", #module_funcs))
	table.insert(lines, "! Function")
	table.insert(lines, "! Line")

	-- Table rows
	for _, info in ipairs(module_funcs) do
		local link = string.format("[[Module:%s#L-%d|%s]]", module_name, info.line, info.name)
		
		-- If the function is the main function, add "main" to that cell
		if info.name == main_function then
			link = link .. " '''(main)'''"
		end
		
		table.insert(lines, "|-")
		table.insert(lines, "| " .. link)
		table.insert(lines, "| " .. info.line)
	end
	table.insert(lines, "|}")
	
	return table.concat(lines, "\n")
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
function p._module_introspection(args)
	local args = args or {}
	local module_name   = args["module_name"  ]
	local main_function = args["main_function"]
	
	-- Preprocess module and blank-out comments
	local title = mw.title.new('Module:' .. module_name)
	local code = title:getContent()
	code = p.strip_comments(code)		-- Blank-out comments
	
	-- Get dependencies and their functions used, then build a table
	local module_deps = p.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 = p.find_functions(code)
	local func_table = p.make_function_table(module_name, module_funcs, main_function)
	
	-- Count the number of modules used


	-- Return the tables
	-- Styling may be improved at a later time
	local combined_tables = table.concat({
		'{| class="wikitable sortable mw-collapsible"',
		'! colspan="2" | Introspection summary',
		"|-",
		'| style="vertical-align:top; border-right:none"; |',
		func_table,
		'| style="vertical-align:top; border-left:none" |',
		dep_table,
		"|}"
	}, "\n")

	return combined_tables
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$", "")
	
	-- 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

	-- Return
	return p._module_introspection({
		["module_name"]   = module_name,
		["main_function"] = main_function,
	})
end

return p