Module:Module introspection: Difference between revisions
Jump to navigation
Jump to search
fix issue with block comments prematurely halting introspection |
refactor so main function preprocesses module's code and passes it to all necessary helper functions, then uses that info to build tables (which are now done by helper functions) |
||
| Line 19: | Line 19: | ||
if in_multiline then | if in_multiline then | ||
-- | -- Check for end of multi-line comment first | ||
local s, e = processed:find(end_pattern) | local s, e = processed:find(end_pattern) | ||
if s then | if s then | ||
in_multiline = false | in_multiline = false | ||
-- | -- Replace only the comment part with spaces | ||
processed = string.rep(" ", e) .. processed:sub(e + 1) | processed = string.rep(" ", e) .. processed:sub(e + 1) | ||
else | else | ||
-- | -- Entire line is inside comment | ||
processed = processed:gsub(".", " ") | processed = processed:gsub(".", " ") | ||
end | end | ||
| Line 34: | Line 34: | ||
in_multiline = true | in_multiline = true | ||
end_pattern = "%]" .. start_eq .. "%]" | end_pattern = "%]" .. start_eq .. "%]" | ||
-- | -- Blank from the start of comment to the end of line | ||
local s, e = processed:find("%-%-%[" .. start_eq .. "%[") | local s, e = processed:find("%-%-%[" .. start_eq .. "%[") | ||
if s then | if s then | ||
| Line 53: | Line 53: | ||
-- Helper function | -- Helper function | ||
-- | -- Find dependencies for a module, given a preprocessed module's code | ||
function p. | function p.find_dependencies(preprocessed_module) | ||
local deps = {} | local deps = {} | ||
for dep in | for dep in preprocessed_module:gmatch("require%s*%(%s*['\"]Module:(.-)['\"]%s*%)") do | ||
table.insert(deps, dep) | table.insert(deps, dep) | ||
end | end | ||
| Line 73: | Line 63: | ||
-- Helper function | -- Helper function | ||
-- | -- Find functions used by each dependency | ||
function p.find_dependency_functions(preprocessed_module) | |||
function p. | |||
-- Step 1: Find all require statements with optional .function | -- Step 1: Find all require statements with optional .function | ||
local results = {} | local results = {} | ||
for var, dep, entry in | for var, dep, entry in preprocessed_module:gmatch( | ||
"local%s+([%w_]+)%s*=%s*require%s*%(%s*['\"]Module:(.-)['\"]%s*%)%.?([%w_]*)" | "local%s+([%w_]+)%s*=%s*require%s*%(%s*['\"]Module:(.-)['\"]%s*%)%.?([%w_]*)" | ||
) do | ) do | ||
| Line 100: | Line 76: | ||
if entry ~= "" then | if entry ~= "" then | ||
-- The module returned a single function: var() usage | -- The module returned a single function: var() usage | ||
for _ in | for _ in preprocessed_module:gmatch(var .. "%s*%(") do | ||
used[entry] = true | used[entry] = true | ||
end | end | ||
else | else | ||
-- The variable is a module table: var.func() usage | -- The variable is a module table: var.func() usage | ||
for func in | for func in preprocessed_module:gmatch(var .. "%.(%w+)%s*%(") do | ||
used[func] = true | used[func] = true | ||
end | end | ||
| Line 111: | Line 87: | ||
-- Step 3: Build result entry | -- Step 3: Build result entry | ||
local | local func_list = {} | ||
for f in pairs(used) do table.insert( | for f in pairs(used) do table.insert(func_list, f) end | ||
table.sort( | table.sort(func_list) | ||
results[dep] = results[dep] or {} | results[dep] = results[dep] or {} | ||
| Line 119: | Line 95: | ||
variable = var, | variable = var, | ||
entry = (entry ~= "" and entry or nil), | entry = (entry ~= "" and entry or nil), | ||
functions = | functions = func_list | ||
}) | }) | ||
end | end | ||
| Line 127: | Line 103: | ||
-- Helper function | -- Helper function | ||
-- | -- Find functions provided by a module, ignoring nested/local functions | ||
function p.find_functions(preprocessed_content) | |||
function p. | |||
-- Iterate through file and find each function and the line found at | -- Iterate through file and find each function and the line found at | ||
local funcs = {} | local funcs = {} | ||
if | if preprocessed_content then | ||
local line_num = 0 | local line_num = 0 | ||
for line in | for line in preprocessed_content:gmatch("([^\n]*)\n?") do -- Make sure gmatch does not skip blank lines | ||
line_num = line_num + 1 | line_num = line_num + 1 | ||
| Line 163: | Line 130: | ||
end | end | ||
-- | -- Helper function; builds the table of dependencies | ||
-- All dependencies are included regardless of use | |||
function p.make_dependency_table(deps, dep_functions) | |||
local dep_lines = {} | |||
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') | ||
| Line 182: | Line 140: | ||
table.insert(dep_lines, "! Variable") | table.insert(dep_lines, "! Variable") | ||
table.insert(dep_lines, "! Function(s) used") | table.insert(dep_lines, "! Function(s) used") | ||
for _, dep in ipairs(deps) do | |||
local dep_link = string.format("[[Module: %s]]", dep) | |||
for _, dep in ipairs( | 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 | ||
table.insert(dep_lines, '|}') | |||
return dep_lines | |||
end | |||
-- Helper function | |||
-- Lists module's own functions | |||
function p.make_function_table(module_name, module_functions) | |||
local func_lines = {} | local func_lines = {} | ||
local func_class = "wikitable sortable mw-collapsible" | local func_class = "wikitable sortable mw-collapsible" | ||
if #module_functions > 20 then | if #module_functions > 20 then | ||
func_class = func_class .. " mw-collapsed" | |||
end | end | ||
table.insert(func_lines, "{| class=\"" .. func_class .. "\"") | table.insert(func_lines, "{| class=\"" .. func_class .. "\"") | ||
| Line 220: | Line 183: | ||
table.insert(func_lines, "! Line") | 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 the function is the main function, add "main" to that cell | ||
if f.name == main_function then | if f.name == main_function then | ||
link = link .. " '''(main)'''" | link = link .. " '''(main)'''" | ||
end | end | ||
table.insert(func_lines, "|-") | |||
table.insert(func_lines, "| " .. link) | |||
table.insert(func_lines, "| " .. f.line) | |||
end | end | ||
table.insert(func_lines, "|}") | 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(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 | -- Return the tables as strings | ||
Revision as of 08:09, 25 October 2025
- This module should not be invoked directly; use its corresponding template instead: Template:Module introspection.
| Dependency | Variable | Function(s) used |
|---|---|---|
| Module: Arguments | getArgs | getArgs, getArgs |
| Function | Line |
|---|---|
| strip_comments | 10 |
| find_dependencies | 56 |
| find_dependency_functions | 66 |
| find_functions | 106 |
| make_dependency_table | 134 |
| make_function_table | 172 |
| _module_introspection | 203 |
| module_introspection | 228 |
-- 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(deps, dep_functions)
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")
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)
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 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(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
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