Skip to content

Instantly share code, notes, and snippets.

@outsinre
Forked from outro56/call-graph.awk
Last active April 7, 2024 09:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save outsinre/1ca61b5c5d7b49b528f5b480a1dff96e to your computer and use it in GitHub Desktop.
Save outsinre/1ca61b5c5d7b49b528f5b480a1dff96e to your computer and use it in GitHub Desktop.
Parse lua code and print call graph
#!/usr/bin/awk -f
#
# call_graph.awk
#
# Usage:
# ./call_graph.awk my_program.lua | dot -Tsvg > call_graph.svg
#
# This is a script that generates a visual call graph for a Lua file.
# This script only shows calls made to functions defined within the
# input Lua file; that is, it excludes calls such as standard library
# or built-in functions.
#
# Use the option "-v modulet=_M" to specify the table name of the module;
# this supports module-level function definitions `function _M.f()` and calls
# like `self:f()`. Default is "_M".
#
# Use the option "-v detailed=1" to draw a detailed call graph without
# caring about function definition prefix "_M." and call prefix "self:".
#
# Use the option "-v nodirect=1" to hide direct function calls; these
# are calls made from the top level of your Lua program.
#
# Example:
# ./call_graph.awk -v modulet=_M -v detailed=1 -v nodirect=1 my_program.lua
#
# See https://gist.github.com/outsinre/1ca61b5c5d7b49b528f5b480a1dff96e
BEGIN {
identifier = "[A-Za-z_][A-Za-z0-9_.]+"
fn_call = "(return )?(self:)?" identifier "\\("
in_fn = "_direct_call_"
if (! modulet) { modulet = "_M." } else if (! match(modulet, /\.$/)) { modulet = modulet "." }
if (! detailed) detailed=0
if (! nodirect) defined_fns[in_fn] = 1
print "strict digraph {"
print " node [shape=box color=\"#FFFFFF\" fontname=\"courier\" fontsize=12];"
print " edge [color=\"#CCCCCC\" arrowsize=0.8];"
}
# Detect and ignore comments.
/^--\[\[/ { in_a_comment = 1 }
/^--\]\]/ { in_a_comment = 0 }
# Detect when we are at the top-level of the program; out of any functions.
# This detection is not foolproof; it assumes the code is consistently indented.
/^ *end/ {
indent = match($0, /[^ ]/) - 1
if (indent == in_fn_indent) {
in_fn = "_direct_call_"
}
}
# Detect and track function definitions.
# This includes only functions declared at the start of a line.
/^ *(local *)?function/ {
if (in_a_comment) next
if ($1 == "local") { fn_name = $3 } else { fn_name = $2 }
sub(/\(.*/, "", fn_name)
if (detailed) sub(modulet, "", fn_name)
defined_fns[fn_name] = 1
in_fn = fn_name
in_fn_indent = match($0, /[^ ]/) - 1
# Uncomment the following line to help debug this script.
# printf "%d: --- start function %s\n", NR, fn_name
next # Don't consider this line as a function call.
}
# Track function calls.
$0 ~ fn_call {
if (in_a_comment) next
fn_index = match($0, fn_call)
comment_index = match($0, /--/)
if (comment_index && comment_index < fn_index) next
tail = substr($0, fn_index)
fn_name = substr(tail, 1, index(tail, "(") - 1)
if (detailed) { sub(/self:/, "", fn_name) } else { sub(/self:/, modulet, fn_name) }
calls[in_fn " -> " fn_name] = 1
# Uncomment the following line to help debug this script.
# printf "%d: called_fns[%s] = %s\n", NR, fn_name, in_fn
}
END {
for (call in calls) {
split(call, fns, " -> ")
if (defined_fns[fns[1]] && defined_fns[fns[2]]) {
print " \"" fns[1] "\" -> \"" fns[2] "\""
}
}
print "}"
}
@outsinre
Copy link
Author

outsinre commented Apr 7, 2024

  1. On macOS, we need to install graphviz to include the dot command. See https://graphviz.org/download/#mac.

  2. Example:

    ~ $ call-graph.awk lib/resty/rediscluster.lua | dot -Tsvg -o ~/misc/rediscluster.svg
    

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment