Module:Module introspection: Difference between revisions

Ganaram inukshuk (talk | contribs)
remove dependency-tracking code; this will be rewritten from the ground-up
Ganaram inukshuk (talk | contribs)
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(content)
function p.strip_comments(code_unstripped)
if not content then return "" end
if not code_unstripped then return "" end


local lines = {}
local lines = {}
Line 55: Line 55:
local end_pattern
local end_pattern


for line in content:gmatch("([^\n]*)\n") do
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
-- This returns a table of module names
-- that information to find every function call for each dependency.
-- TODO: refactor so it pairs module names with their corresponding variable
function p.find_dependencies(code)
-- name; EG, for the line:
local deps = {} -- Dependencies used
-- local med = require("Module:Mediants")
-- the entry in the table is ["Mediants"] = "med"; ALTERNATIVELY, return a table
-- STEP 1
-- of two-entry tables, such that each two-entry table consists of module-var
-- For each require line, get the dependency name (dep), the variable used
-- pairs, such as { "Mediants", "med" }; this is to allow sorting.
-- for that dependency (var), and, if applicable, the function used from
function p.find_dependencies(preprocessed_module)
-- that dependency.
local deps = {}
-- A require line looks like: local var = require("Module:Dependency").func,
for dep in preprocessed_module:gmatch("require%s*%(%s*['\"]Module:(.-)['\"]%s*%)") do
-- where ".func" is optional.
table.insert(deps, dep)
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(preprocessed_content)
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 preprocessed_content then
if code then
local line_num = 0
local line_num = 0
for line in preprocessed_content:gmatch("([^\n]*)\n?") do -- Make sure gmatch does not skip blank lines
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 preprocessed_content = title:getContent()
local code = title:getContent()
preprocessed_content = p.strip_comments(preprocessed_content) -- Blank-out comments
code = p.strip_comments(code) -- Blank-out comments
-- Get dependencies, their functions used, then build a table
-- 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(preprocessed_content)
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