|
|
| (18 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:
| |
| -- - Identify the wrapper function, if there is one (which is usually the case
| |
| -- if there is also a main function). If a wrapper exists without an
| |
| -- accompanying main, then the wrapper is assumed to be the main function.
| |
| -- - Identify invokable functions, which are functions that accept a single
| |
| -- param called "frame".
| |
|
| |
| -- Inspects a module for its functions, its dependencies, and the functions used
| |
| -- from those dependencies.
| |
|
| |
| -- CURRENT BUGS DEEMED NON-ISSUES AT TIME OF WRITING:
| |
| -- If strings contain actual code, they will be treated as part of the code.
| |
| -- This is considered a non-issue since no modules should (nor did they at time
| |
| -- of writing) contain actual lua code as a literal string (code but enclosed in
| |
| -- quotes).
| |
|
| |
| -- Helper function: preprocess lua code
| |
| -- Blanks comments but preserves line numbers
| |
| function p.preprocess_code(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)
| |
|
| |
| -- 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.
| |
| -- Only consider dependencies that have "Module:" in its name
| |
| local raw_deps = {} -- Dependencies used; unsorted and to be processed in step 2
| |
| local pattern = [[local%s+([%w_]+)%s*=%s*require%(%s*["']([^"']+)["']%s*%)%.?([%w_%.]*)]]
| |
| for var, dep, direct_func in code:gmatch(pattern) do
| |
| if dep:match("^Module:") then
| |
| if direct_func == "" then direct_func = nil end
| |
| raw_deps[var] = {
| |
| dep = dep,
| |
| direct_func = direct_func
| |
| }
| |
| end
| |
| 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.
| |
| local deps = {} -- Final array result; to be sorted
| |
| for var, info in pairs(raw_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
| |
|
| |
| -- Add data to table
| |
| table.insert(deps, {
| |
| var = var,
| |
| dep = info.dep,
| |
| funcs = funcs
| |
| })
| |
| end
| |
|
| |
| -- STEP 3: Sort alphabetically by dependency name
| |
| table.sort(deps, function(a, b)
| |
| return a.dep:lower() < b.dep:lower()
| |
| end)
| |
|
| |
| return deps
| |
| end
| |
|
| |
| -- Helper function
| |
| -- Find functions provided by a module, ignoring nested/local functions.
| |
| -- For each function found this way, find the params for that function.
| |
| function p.find_functions(code)
| |
| local funcs = {}
| |
| if not code then return funcs end
| |
|
| |
| local line_num = 0
| |
| local module_name = nil
| |
|
| |
| -- Functions of the form module_name.func are considered.
| |
| -- module_name is usually p, but it may be something else. Find that package
| |
| -- name, or fall back to p.
| |
| for var in code:gmatch([[local%s+([%w_]+)%s*=%s*{}]]) do
| |
| module_name = var
| |
| break
| |
| end
| |
| module_name = module_name or "p"
| |
|
| |
| -- Find all functions defined in the module
| |
| for line in code:gmatch("([^\n]*)\n?") do
| |
| line_num = line_num + 1
| |
|
| |
| -- CASE 1: Match functions defined as:
| |
| -- function p.name(param1, param2)
| |
| local name, params_str = line:match("function%s+" .. module_name .. "%.([%w_]+)%s*%(([^)]*)%)")
| |
| if name then
| |
| local params = {}
| |
| for param in params_str:gmatch("([%w_]+)") do
| |
| table.insert(params, param)
| |
| end
| |
| table.insert(funcs, { name = name, line = line_num, params = params })
| |
| end
| |
|
| |
| -- CASE 2: Match functions defined as:
| |
| -- p.name = function(param1, param2)
| |
| name, params_str = line:match(module_name .. "%.([%w_]+)%s*=%s*function%s*%(([^)]*)%)")
| |
| if name then
| |
| local params = {}
| |
| for param in params_str:gmatch("([%w_]+)") do
| |
| table.insert(params, param)
| |
| end
| |
| table.insert(funcs, { name = name, line = line_num, params = params })
| |
| end
| |
| end
| |
|
| |
| -- CASE 3: If no functions were found, check the code again to see whether
| |
| -- it returns a single function.
| |
| if #funcs == 0 then
| |
| local return_line_num = 0
| |
| for line in code:gmatch("([^\n]*)\n?") do
| |
| return_line_num = return_line_num + 1
| |
| local params_str = line:match("return%s+function%s*%(([^)]*)%)")
| |
| if params_str then
| |
| local params = {}
| |
| for param in params_str:gmatch("([%w_]+)") do
| |
| table.insert(params, param)
| |
| end
| |
| table.insert(funcs, { name = nil, line = return_line_num, params = params })
| |
| break
| |
| end
| |
| end
| |
| end
| |
|
| |
| return funcs
| |
| end
| |
|
| |
|
| -- Helper function | | -- Helper function |
| Line 207: |
Line 13: |
| local lines = {} | | local lines = {} |
| table.insert(lines, '{| class="wikitable sortable"') | | table.insert(lines, '{| class="wikitable sortable"') |
| table.insert(lines, "|+ Lua modules used " .. string.format("(%d)", #module_deps)) | | 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, "! Variable") |
| table.insert(lines, "! Module") | | table.insert(lines, "! Module") |
| Line 256: |
Line 63: |
| --table.insert(lines, string.format("'''Module:%s''' provides %d function(s):", module_name, #module_funcs)) | | --table.insert(lines, string.format("'''Module:%s''' provides %d function(s):", module_name, #module_funcs)) |
| table.insert(lines, '{| class="wikitable sortable"') | | table.insert(lines, '{| class="wikitable sortable"') |
| table.insert(lines, "|+ Functions provided " .. string.format("(%d)", #module_funcs)) | | table.insert(lines, "|+ style=\"font-size: 105%;\" | " .. string.format("Functions provided (%d)", #module_funcs)) |
| | table.insert(lines, "|-") |
| table.insert(lines, "! Line") | | table.insert(lines, "! Line") |
| table.insert(lines, "! Function") | | table.insert(lines, "! Function") |
| Line 295: |
Line 103: |
| if info.name == main_function then | | if info.name == main_function then |
| func = func .. " '''(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 |
| | | |
| Line 317: |
Line 131: |
| local main_function = args["main_function"] | | local main_function = args["main_function"] |
| local descriptions = args["descriptions" ] | | 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 code = title:getContent() | | --local code = title:getContent() |
| code = p.preprocess_code(code) -- Blank-out comments | | --code = iutils.preprocess_code(code) -- Blank-out comments |
| | local code = iutils.get_and_preprocess_content("Module", module_name) |
| | | |
| -- Get dependencies and 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 module_deps = iutils.find_dependencies(code) |
| local dep_table = p.make_dependency_table(module_deps) | | 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_funcs = p.find_functions(code) | | local module_funcs = iutils.find_functions(code) |
| local func_table = p.make_function_table(module_name, module_funcs, descriptions, main_function) | | local func_table = p.make_function_table(module_name, module_funcs, descriptions, main_function) |
|
| |
|
| Line 335: |
Line 157: |
| local lines = { | | local lines = { |
| '{| class="wikitable mw-collapsible"', | | '{| class="wikitable mw-collapsible"', |
| '! colspan="2" | Introspection summary for Module:' .. module_name .. " ", | | '|-', |
| | '! colspan="2" style="font-size: 105%;" | ' .. string.format("Introspection summary for Module:%s ", module_name), |
| "|-", | | "|-", |
| '| style="vertical-align:top; border-right:none"; |', | | '| style="vertical-align: top; border-right: none;" | ', |
| func_table, | | func_table, |
| '| style="vertical-align:top; border-left:none" |', | | '| style="vertical-align: top; border-left: none;" | ', |
| dep_table, | | dep_table, |
| "|}" | | "|}" |
| Line 370: |
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 391: |
Line 218: |
| ["module_name" ] = module_name, | | ["module_name" ] = module_name, |
| ["main_function"] = main_function, | | ["main_function"] = main_function, |
| ["descriptions" ] = func_descriptions | | ["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) | | return frame:preprocess(result) |
| end | | end |
|
| |
|
| return p | | return p |