Module:Module introspection: Difference between revisions
Jump to navigation
Jump to search
remove dependency-tracking code; this will be rewritten from the ground-up |
added back dependency code; to be tested... |
||
| Line 48: | Line 48: | ||
-- Helper function | -- Helper function | ||
-- Blanks comments but preserves line numbers | -- Blanks comments but preserves line numbers | ||
function p.strip_comments( | function p.strip_comments(code_unstripped) | ||
if not | if not code_unstripped then return "" end | ||
local lines = {} | local lines = {} | ||
| Line 55: | Line 55: | ||
local end_pattern | local end_pattern | ||
for line in | for line in code_unstripped:gmatch("([^\n]*)\n") do | ||
local processed = line | local processed = line | ||
| Line 93: | Line 93: | ||
-- Helper function | -- Helper function | ||
-- Find dependencies for a module, given a preprocessed module's code | -- 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) | ||
-- name | local deps = {} -- Dependencies used | ||
-- local | |||
-- | -- 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 | end | ||
return deps | return deps | ||
end | end | ||
-- Helper function | |||
-- Given the output of the above function, create a mediawiki table | |||
function p.make_dependency_table(deps) | |||
local lines = {} | |||
-- Table header | |||
table.insert(lines, '{| class="wikitable sortable left-1 left-2"') | |||
table.insert(lines, '! Variable') | |||
table.insert(lines, '! Module') | |||
table.insert(lines, '! Functions used') | |||
for var, info in pairs(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 | -- Helper function | ||
-- Find functions provided by a module, ignoring nested/local functions | -- Find functions provided by a module, ignoring nested/local functions | ||
function p.find_functions( | function p.find_functions(code) | ||
-- 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 code then | ||
local line_num = 0 | local line_num = 0 | ||
for line in | for line in code:gmatch("([^\n]*)\n?") do -- Make sure gmatch does not skip blank lines | ||
line_num = line_num + 1 | line_num = line_num + 1 | ||
| Line 184: | Line 255: | ||
-- Preprocess module and blank-out comments | -- Preprocess module and blank-out comments | ||
local title = mw.title.new('Module:' .. module_name) | local title = mw.title.new('Module:' .. module_name) | ||
local | 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_lines = p.make_dependency_table(module_deps) | |||
-- Get module's functions, then build a table using that information | -- Get module's functions, then build a table using that information | ||
local module_functions = p.find_functions( | local module_functions = p.find_functions(code) | ||
local func_lines = p.make_function_table(module_name, module_functions, main_function) | local func_lines = p.make_function_table(module_name, module_functions, main_function) | ||
-- Return the tables as strings | -- Return the tables as strings | ||
local summary = string.format("'''Introspection summary:''' Module:%s provides %d functions(s).", module_name, #module_functions) | local summary = string.format("'''Introspection summary:''' Module:%s provides %d functions(s).", module_name, #module_functions) | ||
return summary .. "\n" .. table.concat(func_lines, "\n") | return summary .. "\n" .. table.concat(func_lines, "\n") .. "\n" .. table.concat(dep_lines, "\n") | ||
end | end | ||
| Line 222: | Line 294: | ||
["main_function"] = main_function, | ["main_function"] = main_function, | ||
}) | }) | ||
end | |||
function p.tester() | |||
local sample_code_1 = [[ | |||
-- Package of functions (used) | |||
local util = require("Module:Util") | |||
util.trim(" x ") | |||
util.str.pad("y") | |||
-- Module returning a single function (used) | |||
local makeMessage = require("Module:Message") | |||
makeMessage("hello") | |||
-- Module returning a table but only one function imported (used) | |||
local trim = require("Module:StringUtils").trim | |||
trim(" world ") | |||
]] | |||
local sample_code_2 = [[ | |||
-- Package of functions (used) | |||
local util = require("Module:Util") | |||
util.trim(" x ") | |||
util.str.pad("y") | |||
-- Module returning a single function (used) | |||
local makeMessage = require("Module:Message") | |||
makeMessage("hello") | |||
-- Module returning a table but only one function imported (used) | |||
local trim = require("Module:StringUtils").trim | |||
trim(" world ") | |||
-- UNUSED MODULES | |||
-- Unused package of functions | |||
local mathx = require("Module:MathX") | |||
-- Unused module returning a single function | |||
local sendNotification = require("Module:Notify") | |||
-- Unused single imported function | |||
local pad = require("Module:Util").pad | |||
]] | |||
local title = mw.title.new("Module:Infobox MOS") | |||
local code = title:getContent() | |||
code = p.strip_comments(code) -- Blank-out comments | |||
return p.make_dependency_table(p.find_dependencies(code)) | |||
end | end | ||
return p | return p | ||
Revision as of 07:30, 26 October 2025
- This module should not be invoked directly; use its corresponding template instead: Template:Module introspection.
Lua error at line 270: bad argument #1 to 'concat' (table expected, got string).
-- 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(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(deps)
local lines = {}
-- Table header
table.insert(lines, '{| class="wikitable sortable left-1 left-2"')
table.insert(lines, '! Variable')
table.insert(lines, '! Module')
table.insert(lines, '! Functions used')
for var, info in pairs(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_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" ]
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_lines = p.make_dependency_table(module_deps)
-- Get module's functions, then build a table using that information
local module_functions = p.find_functions(code)
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 provides %d functions(s).", module_name, #module_functions)
return summary .. "\n" .. table.concat(func_lines, "\n") .. "\n" .. table.concat(dep_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
function p.tester()
local sample_code_1 = [[
-- Package of functions (used)
local util = require("Module:Util")
util.trim(" x ")
util.str.pad("y")
-- Module returning a single function (used)
local makeMessage = require("Module:Message")
makeMessage("hello")
-- Module returning a table but only one function imported (used)
local trim = require("Module:StringUtils").trim
trim(" world ")
]]
local sample_code_2 = [[
-- Package of functions (used)
local util = require("Module:Util")
util.trim(" x ")
util.str.pad("y")
-- Module returning a single function (used)
local makeMessage = require("Module:Message")
makeMessage("hello")
-- Module returning a table but only one function imported (used)
local trim = require("Module:StringUtils").trim
trim(" world ")
-- UNUSED MODULES
-- Unused package of functions
local mathx = require("Module:MathX")
-- Unused module returning a single function
local sendNotification = require("Module:Notify")
-- Unused single imported function
local pad = require("Module:Util").pad
]]
local title = mw.title.new("Module:Infobox MOS")
local code = title:getContent()
code = p.strip_comments(code) -- Blank-out comments
return p.make_dependency_table(p.find_dependencies(code))
end
return p