Module:Module introspection: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
mNo edit summary
Ganaram inukshuk (talk | contribs)
No edit summary
Line 135: Line 135:
local dep_lines = {}
local dep_lines = {}
table.insert(dep_lines, string.format("'''Module:%s''' requires the following dependencies:", module_name))
--.insert(dep_lines, string.format("'''Module:%s''' requires the following dependencies:", module_name))
table.insert(dep_lines, '{| class="wikitable sortable"')
table.insert(dep_lines, '{| class="wikitable sortable"')
--table.insert(dep_lines, '|+ Dependencies and functions used')
table.insert(dep_lines, '|+ Dependencies and functions used')
table.insert(dep_lines, "! Dependency")
table.insert(dep_lines, "! Dependency")
table.insert(dep_lines, "! Variable")
table.insert(dep_lines, "! Variable")
Line 180: Line 180:
local func_lines = {}
local func_lines = {}
table.insert(func_lines, string.format("'''Module:%s''' provides %d function(s):", module_name, #module_functions))
--table.insert(func_lines, string.format("'''Module:%s''' provides %d function(s):", module_name, #module_functions))
table.insert(func_lines, "{| class=\"" .. func_class .. "\"")
table.insert(func_lines, "{| class=\"" .. func_class .. "\"")
--table.insert(func_lines, "|+ Functions provided by this module")
table.insert(func_lines, "|+ Functions provided by this module")
table.insert(func_lines, "! Function")
table.insert(func_lines, "! Function")
table.insert(func_lines, "! Line")
table.insert(func_lines, "! Line")
Line 224: Line 224:


-- Return the tables as strings
-- Return the tables as strings
return table.concat(dep_lines, "\n"), table.concat(func_lines, "\n")
local summary = string.format("Introspection summary: Module:%s requires %d module(s) and provides %d functions(s).", module_name, #deps, #module_functions)
return summary .. "\n" .. table.concat(dep_lines, "\n") .. "\n" .. table.concat(func_lines, "\n")
end
end


Line 231: Line 232:
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 244: Line 245:
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
 
-- Call the introspection function
-- Return
local dep_table, func_table = p._module_introspection({
return p._module_introspection({
["module_name"]  = module_name,
["module_name"]  = module_name,
["main_function"] = main_function,
["main_function"] = main_function,
})
})
-- Return combined tables
return dep_table .. "\n" .. func_table .. "\n"
end
end


return p
return p

Revision as of 08:21, 25 October 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: Module:Module introspection requires 1 module(s) and provides 8 functions(s).

Dependencies and functions used
Dependency Variable Function(s) used
Module: Arguments getArgs getArgs, getArgs
Functions provided by this module
Function Line
strip_comments 10
find_dependencies 56
find_dependency_functions 66
find_functions 106
make_dependency_table 134
make_function_table 173
_module_introspection 206
module_introspection 232

-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]]
local getArgs = require("Module:Arguments").getArgs
local p = {}

-- Inspects a module for its functions, its dependencies, and the functions used
-- from those dependencies.

-- Helper function
-- Blanks comments but preserves line numbers
function p.strip_comments(content)
	if not content then return "" end

	local lines = {}
	local in_multiline = false
	local end_pattern

	for line in content: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
function p.find_dependencies(preprocessed_module)
	local deps = {}
	for dep in preprocessed_module:gmatch("require%s*%(%s*['\"]Module:(.-)['\"]%s*%)") do
		table.insert(deps, dep)
	end
	return deps
end

-- Helper function
-- Find functions used by each dependency
function p.find_dependency_functions(preprocessed_module)

	-- Step 1: Find all require statements with optional .function
	local results = {}
	for var, dep, entry in preprocessed_module:gmatch(
		"local%s+([%w_]+)%s*=%s*require%s*%(%s*['\"]Module:(.-)['\"]%s*%)%.?([%w_]*)"
	) do
		local used = {}

		-- Step 2: Check how it's used
		if entry ~= "" then
			-- The module returned a single function: var() usage
			for _ in preprocessed_module:gmatch(var .. "%s*%(") do
				used[entry] = true
			end
		else
			-- The variable is a module table: var.func() usage
			for func in preprocessed_module:gmatch(var .. "%.(%w+)%s*%(") do
				used[func] = true
			end
		end

		-- Step 3: Build result entry
		local func_list = {}
		for f in pairs(used) do table.insert(func_list, f) end
		table.sort(func_list)

		results[dep] = results[dep] or {}
		table.insert(results[dep], {
			variable = var,
			entry = (entry ~= "" and entry or nil),
			functions = func_list
		})
	end

	return results
end

-- Helper function
-- Find functions provided by a module, ignoring nested/local functions
function p.find_functions(preprocessed_content)

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

-- Helper function; builds the table of dependencies
-- All dependencies are included regardless of use
function p.make_dependency_table(module_name, deps, dep_functions)
	
	local dep_lines = {}
	--.insert(dep_lines, string.format("'''Module:%s''' requires the following dependencies:", module_name))
	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")
	
	for _, dep in ipairs(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

	table.insert(dep_lines, '|}')
	
	return dep_lines
end

-- Helper function
-- Lists module's own functions
function p.make_function_table(module_name, module_functions)
	
	-- Collapse table if it's larger than 20 lines
	local func_class = "wikitable sortable mw-collapsible"
	if #module_functions > 20 then
		func_class = func_class .. " mw-collapsed"
	end
	
	local func_lines = {}
	--table.insert(func_lines, string.format("'''Module:%s''' provides %d function(s):", module_name, #module_functions))
	table.insert(func_lines, "{| class=\"" .. func_class .. "\"")
	table.insert(func_lines, "|+ Functions provided by this module")
	table.insert(func_lines, "! Function")
	table.insert(func_lines, "! Line")

	for _, f in ipairs(module_functions) do
		local link = string.format("[[Module:%s#L-%d|%s]]", module_name, f.line, f.name)
		
		-- If the function is the main function, add "main" to that cell
		if f.name == main_function then
			link = link .. " '''(main)'''"
		end
		
		table.insert(func_lines, "|-")
		table.insert(func_lines, "| " .. link)
		table.insert(func_lines, "| " .. f.line)
	end
	table.insert(func_lines, "|}")
	
	return func_lines
end

-- Main function; to be called by wrapper
function p._module_introspection(args)
	local args = args or {}
	local module_name   = args["module_name"  ] or "Numlinks"
	local main_function = args["main_function"]
	
	-- Preprocess module and blank-out comments
	local title = mw.title.new('Module:' .. module_name)
	local preprocessed_content = title:getContent()
	preprocessed_content = p.strip_comments(preprocessed_content)		-- Blank-out comments
	
	-- Get dependencies, their functions used, then build a table
	local deps = p.find_dependencies(preprocessed_content)
	local dep_functions = p.find_dependency_functions(preprocessed_content)
	local dep_lines = p.make_dependency_table(module_name, deps, dep_functions)

	-- Get module's functions, then build a table using that information
	local module_functions = p.find_functions(preprocessed_content)
	local func_lines = p.make_function_table(module_name, module_functions)

	-- Return the tables as strings
	local summary = string.format("Introspection summary: Module:%s requires %d module(s) and provides %d functions(s).", module_name, #deps, #module_functions)
	return summary .. "\n" .. table.concat(dep_lines, "\n") .. "\n" .. table.concat(func_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$", "")
	
	-- 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