Module:Module introspection
- This module should not be invoked directly; use its corresponding template instead: Template:Module introspection.
Introspection summary: Module:Module introspection requires 1 module(s) and provides 8 functions(s).
| Dependency | Variable | Function(s) used |
|---|---|---|
| Module: Arguments | getArgs | getArgs getArgs |
| Function | Line |
|---|---|
| strip_comments | 50 |
| find_dependencies | 103 |
| find_dependency_functions | 119 |
| find_functions | 159 |
| make_dependency_table | 187 |
| make_function_table | 227 |
| _module_introspection (main) | 264 |
| module_introspection | 290 |
-- 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.
-- TODO: bugfix dependency usage
-- The following snippet should detect use of any dependency, but it should be
-- cross-referenced with the found dependencies
--[[
for match in code:gmatch("([_%a][_%w%.]*)%s*%(") do
print(match)
end
]]--
-- PROPOSED ALGORITHM FOR FINDING FUNCTIONS OF DEPENDENCIES
-- - Find every unique function call in the template, filtering out all function
-- calls that do not correspond to a dependency. For a dependency included as
-- local dep = require("Module:Dependency"), the module is called "Dependency"
-- and is called "dep" throughout the code. Function calls involving this
-- dependency will have "dep" as part of the name, as dep(), dep.func(), or
-- with any number of dots (dep.utils.formatter.func()), but zero or one dots
-- is typical.
-- - For each dependency name, place each function call with that dependency's
-- name in a table of function calls.
-- - Inspect each dependency's table. If there is at least one function call in
-- that table, then that dependency is used. If not, then that module is not
-- used.
-- - If function calls match the name of the dependency "dep", then either the
-- module returned a function or the code require("Module:Dep").func selected
-- only one function. If it's the former, then the module IS the function. If
-- it's the latter, then func from require("Module:Dep").func is the function.
-- In either case, if no function calls called "dep" are found, then the
-- dependency "dep" was not used.
-- 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
-- This returns a table of module names
-- TODO: refactor so it pairs module names with their corresponding variable
-- name; EG, for the line:
-- local med = require("Module:Mediants")
-- the entry in the table is ["Mediants"] = "med"; ALTERNATIVELY, return a table
-- of two-entry tables, such that each two-entry table consists of module-var
-- pairs, such as { "Mediants", "med" }; this is to allow sorting.
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
-- TODO: refactor so it references a table produced by the above function.
-- By matching strings of the form "([_%a][_%w%.]*)%s*%(", it can determine
-- what dependency is being used. Table produced should have entries in this
-- form:
-- ["med"] = { "find_mediants", "find_deepest" }
-- Each entry in that smaller table is a function that was used
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 = {}
--.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, "<br />")
if usage.entry then
func_str = usage.entry .. (func_str ~= "" and ("<br />" .. 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; requires module name to produce links to each
-- function.
function p.make_function_table(module_name, module_functions, main_function)
-- 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
-- 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" ] 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, main_function)
-- 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