Module:Module introspection: Difference between revisions

Ganaram inukshuk (talk | contribs)
remove dependency-tracking code; this will be rewritten from the ground-up
ArrowHead294 (talk | contribs)
mNo edit summary
 
(95 intermediate revisions by 2 users not shown)
Line 1: Line 1:
-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]]
-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]]
local getArgs = require("Module:Arguments").getArgs
local getArgs = require("Module:Arguments").getArgs
local iutils  = require("Module:Introspection utils")
local yesno  = require("Module:Yesno")
local p = {}
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
-- Helper function
-- Blanks comments but preserves line numbers
-- Given the output of the above function, create a mediawiki table
function p.strip_comments(content)
function p.make_dependency_table(module_deps)
if not content then return "" end


-- Table headers
local lines = {}
local lines = {}
local in_multiline = false
table.insert(lines, '{| class="wikitable sortable"')
local end_pattern
table.insert(lines, "|+ style=\"font-size: 105%;\" | " .. string.format("Lua modules required (%d)", #module_deps))
table.insert(lines, "|-")
table.insert(lines, "! Variable")
table.insert(lines, "! Module")
table.insert(lines, "! Functions used")


for line in content:gmatch("([^\n]*)\n") do
-- Table rows (assuming they're all alphabetized)
local processed = line
for _, info in ipairs(module_deps) do
local funcs_text
if in_multiline then
if #info.funcs == 0 then
-- Check for end of multi-line comment first
funcs_text = "''dependency not used''"
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
else
local start_eq = processed:match("%-%-%[(=*)%[")
local func_lines = {}
if start_eq then
for _, func in ipairs(info.funcs) do
in_multiline = true
table.insert(func_lines, string.format("<code>%s</code>", func))
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
funcs_text = table.concat(func_lines, "<br />")
end
end
table.insert(lines, processed)
end
return table.concat(lines, "\n")
end


-- Helper function
table.insert(lines, "|-")
-- Find dependencies for a module, given a preprocessed module's code
table.insert(lines, "| " .. info.var)
-- This returns a table of module names
table.insert(lines, "| [[" .. info.dep .. "]]")
-- TODO: refactor so it pairs module names with their corresponding variable
table.insert(lines, "| " .. funcs_text)
-- 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
end
return deps
end


-- Helper function
-- Table footer
-- Find functions provided by a module, ignoring nested/local functions
table.insert(lines, "|}")
function p.find_functions(preprocessed_content)


-- Iterate through file and find each function and the line found at
-- Join all lines into a single string
local funcs = {}
return table.concat(lines, "\n")
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
end


Line 140: Line 48:
-- Lists module's own functions; requires module name to produce links to each
-- Lists module's own functions; requires module name to produce links to each
-- function.
-- function.
function p.make_function_table(module_name, module_functions, main_function)
function p.make_function_table(module_name, module_funcs, descriptions, main_function)
-- Collapse table if it's larger than 20 lines
-- Check whether descriptions was passed in
local func_class = "wikitable sortable mw-collapsible"
local has_descriptions = false
if #module_functions > 20 then
if type(descriptions) == "table" then
func_class = func_class .. " mw-collapsed"
for _, _ in pairs(descriptions) do
has_descriptions = true
break
end
end
end
local func_lines = {}
-- Table headers
--table.insert(func_lines, string.format("'''Module:%s''' provides %d function(s):", module_name, #module_functions))
local lines = {}
table.insert(func_lines, "{| class=\"" .. func_class .. "\"")
--table.insert(lines, string.format("'''Module:%s''' provides %d function(s):", module_name, #module_funcs))
table.insert(func_lines, "|+ Functions provided by this module")
table.insert(lines, '{| class="wikitable sortable"')
table.insert(func_lines, "! Function")
table.insert(lines, "|+ style=\"font-size: 105%;\" | " .. string.format("Functions&nbsp;provided&nbsp;(%d)", #module_funcs))
table.insert(func_lines, "! Line")
table.insert(lines, "|-")
table.insert(lines, "! Line")
table.insert(lines, "! Function")
table.insert(lines, "! Params")
-- If there are descriptions, add a column for that
if has_descriptions then
table.insert(lines, "! Description")
end


for _, f in ipairs(module_functions) do
-- Table rows
local link = string.format("[[Module:%s#L-%d|%s]]", module_name, f.line, f.name)
for _, info in ipairs(module_funcs) do
-- Find params for that function, or say "none" if none
local params = {}
for _, param in ipairs(info.params) do
table.insert(params, param)
end
local params_string = ""
if #params == 0 then
params_string = "''none''"
else
params_string = string.format("<code>(%s)</code>", table.concat(params, ", "))
end
-- Create link to line for that function
local line_num = string.format("[[Module:%s#L-%d|%d]]", module_name, info.line, info.line)
-- Create text for function
local func = ""
if info.name then
func = string.format("<code>%s</code>", info.name)
else
func = "''none''"
end
-- 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 info.name == main_function then
link = link .. " '''(main)'''"
func = func .. " '''(main)'''"
end
-- If the function is invokable (it has one param called "frame"), add
-- "invokable" to that cell
if #params == 1 and params[1] == "frame" then
func = func .. " '''(invokable)'''"
end
end
table.insert(func_lines, "|-")
table.insert(lines, "|-")
table.insert(func_lines, "| " .. link)
table.insert(lines, "| " .. line_num)
table.insert(func_lines, "| " .. f.line)
table.insert(lines, "| " .. func)
table.insert(lines, "| " .. params_string)
if has_descriptions then
table.insert(lines, "| " .. (descriptions[info.name] or ""))
end
end
end
table.insert(func_lines, "|}")
table.insert(lines, "|}")
return func_lines
return table.concat(lines, "\n")
end
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
-- Main function; to be called by wrapper
Line 181: Line 130:
local module_name  = args["module_name"  ]
local module_name  = args["module_name"  ]
local main_function = args["main_function"]
local main_function = args["main_function"]
local descriptions  = args["descriptions" ]
local is_doc = args["is_doc"]
-- Check whether this page is a docpage
-- If so, don't bother
if is_doc then
return "''To see introspection summary, see this module's main page.''"
end
-- 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 preprocessed_content = title:getContent()
--local code = title:getContent()
preprocessed_content = p.strip_comments(preprocessed_content) -- Blank-out comments
--code = iutils.preprocess_code(code) -- Blank-out comments
local code = iutils.get_and_preprocess_content("Module", module_name)
-- Get dependencies, their functions used, then build a table
-- Get dependencies and their functions used, then build a table
local module_deps = iutils.find_dependencies(code)
local dep_table = 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(preprocessed_content)
local module_funcs = iutils.find_functions(code)
local func_lines = p.make_function_table(module_name, module_functions, main_function)
local func_table = p.make_function_table(module_name, module_funcs, descriptions, main_function)
 
-- Return the tables
-- Styling may be improved at a later time
local lines = {
'{| class="wikitable mw-collapsible"',
'|-',
'! colspan="2" style="font-size: 105%;" | ' .. string.format("Introspection summary for Module:%s&nbsp;", module_name),
"|-",
'| style="vertical-align: top; border-right: none;" | ',
func_table,
'| style="vertical-align: top; border-left: none;" | ',
dep_table,
"|}"
}
 
-- Check whether descriptions was passed in
local has_descriptions = false
if type(descriptions) == "table" then
for _, _ in pairs(descriptions) do
has_descriptions = true
break
end
end
-- If no descriptions were provided, add text below that points to the code.
if not has_descriptions then
table.insert(lines, "''No function descriptions were provided. The Lua code may have further information.''")
end


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


-- Wrapper function for modules
-- Wrapper function for modules
Line 209: Line 193:


-- Strip trailing "/doc" if the template is used on a documentation subpage
-- Strip trailing "/doc" if the template is used on a documentation subpage
module_name = module_name:gsub("/doc$", "")
--module_name = module_name:gsub("/doc$", "")
-- Check whether page is the doc page
-- To ensure proper referencing, do not display introspection on a docpage.
local is_doc = string.find(module_name, "/doc$")
-- Normalize module name so it can be used to find the main function, which
-- Normalize module name so it can be used to find the main function, which
Line 216: Line 204:
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
-- Process all function descriptions, if provided as desc_function_name
local func_descriptions = {}
for key, val in pairs(args) do
local func_name = key:match("^desc_([%w_]+)$")
if func_name and val and val ~= "" then
func_descriptions[func_name] = val
end
end


-- Return
-- Return
return p._module_introspection({
local result = p._module_introspection({
["module_name"]   = module_name,
["module_name" ] = module_name,
["main_function"] = main_function,
["main_function"] = main_function,
["descriptions" ] = func_descriptions,
["is_doc"] = is_doc
})
})
    -- Debugger option to show generated WikiText
    local wtext = yesno(frame.args["wtext"] or args["wtext"])
if wtext == true then
result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
end
return frame:preprocess(result)
end
end


return p
return p