Search code examples
reflectionluaabstract-syntax-tree

How do I get information about function calls from a Lua script?


I have a script written in Lua 5.1 that imports third-party module and calls some functions from it. I would like to get a list of function calls from a module with their arguments (when they are known before execution).

So, I need to write another script which takes the source code of my first script, parses it, and extracts information from its code.

Consider the minimal example.

I have the following module:

local mod = {}

function mod.foo(a, ...)
    print(a, ...)
end

return mod

And the following driver code:

local M = require "mod"
M.foo('a', 1)
M.foo('b')

What is the better way to retrieve the data with the "use" occurrences of the M.foo function?

Ideally, I would like to get the information with the name of the function being called and the values of its arguments. From the example code above, it would be enough to get the mapping like this: {'foo': [('a', 1), ('b')]}.

I'm not sure if Lua has functions for reflection to retrieve this information. So probably I'll need to use one of the existing parsers for Lua to get the complete AST and find the function calls I'm interested in.

Any other suggestions?


Solution

  • If you can not modify the files, you can read the files into a strings then parse mod file and find all functions in it, then use that information to parse the target file for all uses of the mod library

    functions = {}
    
    for func in modFile:gmatch("function mod%.(%w+)") do
        functions[func] = {}
    end
    
    for func, call in targetFile:gmatch("M%.(%w+)%(([^%)]+)%)") do
        args = {}
        for arg in string.gmatch(call, "([^,]+)") do
            table.insert(args, arg)
        end
    
        table.insert(functions[func], args)
    end
    
    

    Resulting table can then be serialized

        ['foo'] = {{"'a'", " 1"}, {"'b'"}}
    
    

    3 possible gotchas:

    1. M is not a very unique name and could vary possibly match unintended function calls to another library.
    2. This example does not handle if there is a function call made inside the arg list. e.g. myfunc(getStuff(), true)
    3. The resulting table does not know the typing of the args so they are all save as strings representations.

    If modifying the target file is an option you can create a wrapper around your required module

    function log(mod)
        local calls = {}
        local wrapper = {
            __index = function(_, k)
                if mod[k] then
                    return function(...)
                        calls[k] = calls[k] or {}
                        table.insert(calls[k], {...})
    
                        return mod[k](...)
                    end
                end
            end,
        }
    
        return setmetatable({},wrapper), calls
    end
    

    then you use this function like so.

    local M, calls = log(require("mod"))
    M.foo('a', 1)
    M.foo('b')
    

    If your module is not just functions you would need to handle that in the wrapper, this wrapper assumes all indexes are a function.

    after all your calls you can serialize the calls table to get the history of all the calls made. For the example code the table looks like

    {
        ['foo'] = {{'a', 1}, {'b'}}
    }