Module:Module introspection: Difference between revisions

From Xenharmonic Wiki
Jump to navigation Jump to search
Ganaram inukshuk (talk | contribs)
No edit summary
Ganaram inukshuk (talk | contribs)
try to bugfix line count again
Line 11: Line 11:
if not content then return "" end
if not content then return "" end


-- Step 1: blank multi-line comments including --[[ ... ]] and --[=[ ... ]=]
local lines = {}
content = content:gsub("%-%-%[(=*)%[(.-)%]%1%]", function(eq, body)
local in_multiline = false
-- Replace everything except newlines with spaces
local end_pattern
local blank = body:gsub("[^\n]", " ")
return "--[[" .. blank .. "]]"
end)


-- Step 2: blank single-line comments
for line in content:gmatch("([^\n]*)\n") do
content = content:gsub("%-%-[^\n]*", function(s)
local processed = line
return s:gsub("[^\n]", " ")
end)


return content
if in_multiline then
-- inside a multi-line comment: replace everything with spaces
processed = processed:gsub(".", " ")
-- check for end of multi-line comment
if processed:find(end_pattern) then
in_multiline = false
end
else
-- check for start of multi-line comment
local start_eq = processed:match("%-%-%[(=*)%[")
if start_eq then
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
 
table.insert(lines, processed)
end
 
-- handle last line if it doesn’t end with newline
local last_line = content:match("([^\n]+)$")
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
 
return table.concat(lines, "\n")
end
end


Line 116: Line 148:
local funcs = {}
local funcs = {}
if content then
if content then
local lineNumber = 0
local line_num = 0
for line in content:gmatch("[^\n]+") do
for line in content:gmatch("([^\n]*)\n?") do -- Make sure gmatch does not skip blank lines
lineNumber = lineNumber + 1
line_num = line_num + 1


-- Match functions defined as function p.name(
-- Match functions defined as function p.name(
local name = line:match("function%s+[%w_]+%.([%w_]+)%s*%(")
local name = line:match("function%s+[%w_]+%.([%w_]+)%s*%(")
if name then
if name then
table.insert(funcs, {name = name, line = lineNumber})
table.insert(funcs, {name = name, line = line_num})
end
end


Line 129: Line 161:
name = line:match("[%w_]+%.([%w_]+)%s*=%s*function%s*%(")
name = line:match("[%w_]+%.([%w_]+)%s*=%s*function%s*%(")
if name then
if name then
table.insert(funcs, {name = name, line = lineNumber})
table.insert(funcs, {name = name, line = line_num})
end
end
end
end
Line 139: Line 171:
-- 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 module_name  = args["module_name"  ]
local args = args or {}
local module_name  = args["module_name"  ] or "MOS"
local main_function = args["main_function"]
local main_function = args["main_function"]



Revision as of 06:29, 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.

Dependencies and functions used
Dependency Variable Function(s) used
Module: Arguments getArgs getArgs, getArgs
Functions provided by this module
Function Line
strip_comments 10
list_dependencies 62
list_dependency_functions 88
list_functions 137
_module_introspection (main) 172
module_introspection 248



-- 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
			-- inside a multi-line comment: replace everything with spaces
			processed = processed:gsub(".", " ")
			-- check for end of multi-line comment
			if processed:find(end_pattern) then
				in_multiline = false
			end
		else
			-- check for start of multi-line comment
			local start_eq = processed:match("%-%-%[(=*)%[")
			if start_eq then
				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

		table.insert(lines, processed)
	end

	-- handle last line if it doesn’t end with newline
	local last_line = content:match("([^\n]+)$")
	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

	return table.concat(lines, "\n")
end

-- Helper function
-- List dependencies for a module
function p.list_dependencies(module_name)
	local module_name = module_name --or "MOS"		-- Test arg; comment out when not testing
	
	local title = mw.title.new('Module:' .. module_name)
	local content = title:getContent()
	
	-- Blank-out comments
	content = p.strip_comments(content)

	-- 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
	return deps
end

-- Helper function
-- List functions used from each dependency
-- 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
-- returns a function or var = require("Module:Dep").func_name if a specific
-- function from that module is used.
-- Optionally accepts a table of its dependencies; if not provided, it will find
-- them itself by calling list_dependencies
function p.list_dependency_functions(module_name, deps)
	local module_name = module_name --or "MOS"		-- Test arg; comment out when not testing

	-- Get dependencies
	local deps = p.list_dependencies(module_name)
	
	-- Load module
	local title = mw.title.new('Module:' .. module_name)
	local content = title:getContent()

	-- Step 1: Find all require statements with optional .function
	local results = {}
	for var, dep, entry in content: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 content:gmatch(var .. "%s*%(") do
				used[entry] = true
			end
		else
			-- The variable is a module table: var.func() usage
			for func in content:gmatch(var .. "%.(%w+)%s*%(") do
				used[func] = true
			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

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

-- Main function; to be called by wrapper
function p._module_introspection(args)
	local args = args or {}
	local module_name   = args["module_name"  ] or "MOS"
	local main_function = args["main_function"]

    -- Get module functions
    local module_functions = p.list_functions(module_name)

    -- Get dependencies and functions used from each
    local dep_functions = p.list_dependency_functions(module_name)

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

    table.insert(dep_lines, '|}')

    -- Build MediaWiki table for module's own functions
	local func_lines = {}
	local func_class = "wikitable sortable mw-collapsible"
	if #module_functions > 20 then
	    func_class = func_class .. " mw-collapsed"
	end
	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 the tables as strings
	return table.concat(dep_lines, "\n"), table.concat(func_lines, "\n")
end


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

	-- 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
	
	-- Call the introspection function
	local dep_table, func_table = p._module_introspection({
		["module_name"]   = module_name,
		["main_function"] = main_function,
	})

	-- Return combined tables
	return dep_table .. "\n" .. func_table .. "\n"
end

return p