Module:Sandbox/Aidan9382/CodeAnalysis

--[[

This module is designed to perform analysis on other modules. The majority of

the code here is copied from a seperate project,

https://github.com/9382/Dump/tree/main/LuaObfusactor, which in turn has code

borrowed from https://github.com/stravant/LuaMinify.

--]]

local print = mw and mw.log or print

require("strict")

local function lookupify(tb)

for _, v in pairs(tb) do

tb[v] = true

end

return tb

end

local WhiteChars = lookupify{' ', '\n', '\t', '\r'}

local LowerChars = lookupify{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',

'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',

's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}

local UpperChars = lookupify{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',

'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',

'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}

local Digits = lookupify{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}

local HexDigits = lookupify{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',

'A', 'a', 'B', 'b', 'C', 'c', 'D', 'd', 'E', 'e', 'F', 'f'}

local Symbols = lookupify{'+', '-', '*', '/', '^', '%', ',', '{', '}', '[', ']', '(', ')', ';', '#'}

local Keywords = lookupify{

'and', 'break', 'do', 'else', 'elseif',

'end', 'false', 'for', 'function', 'goto', 'if',

'in', 'local', 'nil', 'not', 'or', 'repeat',

'return', 'then', 'true', 'until', 'while',

};

local BackslashEscaping = {

a="\a", b="\b", f="\f", n="\n", r="\r", t="\t", v="\v",

["\\"]="\\", ['"']='"', ["'"]="'", ["["]="[", ["]"]="]"

}

local function LexLua(src)

--token dump

local tokens = {}

local st, err = pcall(function()

--line / char / pointer tracking

local p = 1

local line = 1

local char = 1

--get / peek functions

local function get()

local c = src:sub(p,p)

if c == '\n' then

char = 1

line = line + 1

else

char = char + 1

end

p = p + 1

return c

end

local function peek(n)

n = n or 0

return src:sub(p+n,p+n)

end

local function consume(chars)

local c = peek()

for i = 1, #chars do

if c == chars:sub(i,i) then return get() end

end

end

--shared stuff

local function generateError(err)

return error(">> :"..line..":"..char..": "..err, 0)

end

local function tryGetLongString()

local start = p

if peek() == '[' then

local equalsCount = 0

while peek(equalsCount+1) == '=' do

equalsCount = equalsCount + 1

end

if peek(equalsCount+1) == '[' then

--start parsing the string. Strip the starting bit

for _ = 0, equalsCount+1 do get() end

--get the contents

local contentStart = p

while true do

--check for eof

if peek() == '' then

generateError("Expected `]"..string.rep('=', equalsCount).."]` near .", 3)

end

--check for the end

local foundEnd = true

if peek() == ']' then

for i = 1, equalsCount do

if peek(i) ~= '=' then foundEnd = false end

end

if peek(equalsCount+1) ~= ']' then

foundEnd = false

end

else

foundEnd = false

end

--

if foundEnd then

break

else

get()

end

end

--get the interior string

local contentString = src:sub(contentStart, p-1)

--found the end. Get rid of the trailing bit

for i = 0, equalsCount+1 do get() end

--get the exterior string

local longString = src:sub(start, p-1)

--return the stuff

return contentString, longString

else

return nil

end

else

return nil

end

end

--main token emitting loop

while true do

--get leading whitespace. The leading whitespace will include any comments

--preceding the token. This prevents the parser needing to deal with comments

--separately.

local leadingWhite = ''

while true do

local c = peek()

if WhiteChars[c] then

--whitespace

leadingWhite = leadingWhite..get()

elseif c == '-' and peek(1) == '-' then

--comment

get();get()

leadingWhite = leadingWhite..'--'

local _, wholeText = tryGetLongString()

if wholeText then

leadingWhite = leadingWhite..wholeText

else

while peek() ~= '\n' and peek() ~= '' do

leadingWhite = leadingWhite..get()

end

end

else

break

end

end

--get the initial char

local thisLine = line

local thisChar = char

local c = peek()

--symbol to emit

local toEmit = nil

--branch on type

if c == '' then

--eof

toEmit = {Type = 'Eof'}

elseif UpperChars[c] or LowerChars[c] or c == '_' then

--ident or keyword

local start = p

repeat

get()

c = peek()

until not (UpperChars[c] or LowerChars[c] or Digits[c] or c == '_')

local dat = src:sub(start, p-1)

if Keywords[dat] then

toEmit = {Type = 'Keyword', Data = dat}

else

toEmit = {Type = 'Ident', Data = dat}

end

elseif Digits[c] or (peek() == '.' and Digits[peek(1)]) then

--number const

local start = p

if c == '0' and peek(1) == 'x' then

get();get()

while HexDigits[peek()] do get() end

if consume('Pp') then

consume('+-')

while Digits[peek()] do get() end

end

else

while Digits[peek()] do get() end

if consume('.') then

while Digits[peek()] do get() end

end

if consume('Ee') then

consume('+-')

while Digits[peek()] do get() end

end

end

toEmit = {Type = 'Number', Data = src:sub(start, p-1)}

elseif c == '\'' or c == '\"' then

--string const

local delim = get()

local content = ""

while true do

local c = get()

if c == '\\' then

local next = get()

local replacement = BackslashEscaping[next]

if replacement then

content = content .. replacement

else

if next == "x" then

local n1 = get()

if n1 == "" or n1 == delim or not HexDigits[n1] then

generateError("invalid escape sequence near '"..delim.."'")

end

local n2 = get()

if n2 == "" or n2 == delim or not HexDigits[n2] then

generateError("invalid escape sequence near '"..delim.."'")

end

content = content .. string.char(tonumber(n1 .. n2, 16))

elseif Digits[next] then

local num = next

while #num < 3 and Digits[peek()] do

num = num .. get()

end

content = content .. string.char(tonumber(num))

else

-- ignore the \

end

end

elseif c == delim then

break

elseif c == '' then

generateError("Unfinished string near ")

else

content = content .. c

end

end

toEmit = {Type = 'String', Data = delim .. content .. delim, Constant = content}

elseif c == '[' then

local content, wholetext = tryGetLongString()

if wholetext then

toEmit = {Type = 'String', Data = wholetext, Constant = content}

else

get()

toEmit = {Type = 'Symbol', Data = '['}

end

elseif consume('>=<') then

if consume('=') then

toEmit = {Type = 'Symbol', Data = c..'='}

else

toEmit = {Type = 'Symbol', Data = c}

end

elseif consume('~') then

if consume('=') then

toEmit = {Type = 'Symbol', Data = '~='}

else

generateError("Unexpected symbol `~` in source.", 2)

end

elseif consume('.') then

if consume('.') then

if consume('.') then

toEmit = {Type = 'Symbol', Data = '...'}

else

toEmit = {Type = 'Symbol', Data = '..'}

end

else

toEmit = {Type = 'Symbol', Data = '.'}

end

elseif consume(':') then

if consume(':') then

toEmit = {Type = 'Symbol', Data = '::'}

else

toEmit = {Type = 'Symbol', Data = ':'}

end

elseif Symbols[c] then

get()

toEmit = {Type = 'Symbol', Data = c}

else

local contents, all = tryGetLongString()

if contents then

toEmit = {Type = 'String', Data = all, Constant = contents}

else

generateError("Unexpected Symbol `"..c.."` in source.", 2)

end

end

--add the emitted symbol, after adding some common data

toEmit.LeadingWhite = leadingWhite

toEmit.Line = thisLine

toEmit.Char = thisChar

toEmit.Print = function()

return "<"..(toEmit.Type..string.rep(' ', 7-#toEmit.Type)).." "..(toEmit.Data or '').." >"

end

tokens[#tokens+1] = toEmit

--halt after eof has been emitted

if toEmit.Type == 'Eof' then break end

end

end)

if not st then

return false, err

end

--public interface:

local tok = {}

local savedP = {}

local p = 1

--getters

function tok:Peek(n)

n = n or 0

return tokens[math.min(#tokens, p+n)]

end

function tok:Get()

local t = tokens[p]

p = math.min(p + 1, #tokens)

return t

end

function tok:Is(t)

return tok:Peek().Type == t

end

--save / restore points in the stream

function tok:Save()

savedP[#savedP+1] = p

end

function tok:Commit()

savedP[#savedP] = nil

end

function tok:Restore()

p = savedP[#savedP]

savedP[#savedP] = nil

end

--either return a symbol if there is one, or return true if the requested

--symbol was gotten.

function tok:ConsumeSymbol(symb)

local t = self:Peek()

if t.Type == 'Symbol' then

if symb then

if t.Data == symb then

self:Get()

return true

else

return nil

end

else

self:Get()

return t

end

else

return nil

end

end

function tok:ConsumeKeyword(kw)

local t = self:Peek()

if t.Type == 'Keyword' and t.Data == kw then

self:Get()

return true

else

return nil

end

end

function tok:IsKeyword(kw)

local t = tok:Peek()

return t.Type == 'Keyword' and t.Data == kw

end

function tok:IsSymbol(s)

local t = tok:Peek()

return t.Type == 'Symbol' and t.Data == s

end

function tok:IsEof()

return tok:Peek().Type == 'Eof'

end

return true, tok

end

local ScopeContainer = {}

local GlobalVarGetMap = {}

local function ParseLua(src)

ScopeContainer = {}

local st, tok = LexLua(src)

if not st then

return false, tok

end

--

local function GenerateError(msg)

local err = ">> :"..tok:Peek().Line..":"..tok:Peek().Char..": "..msg.."\n"

--find the line

local lineNum = 0

for line in src:gmatch("[^\n]*\n?") do

if line:sub(-1,-1) == '\n' then line = line:sub(1,-2) end

lineNum = lineNum+1

if lineNum == tok:Peek().Line then

err = err..">> `"..line:gsub('\t',' ').."`\n"

for i = 1, tok:Peek().Char do

local c = line:sub(i,i)

if c == '\t' then

err = err..' '

else

err = err..' '

end

end

err = err.." ^---"

break

end

end

return err

end

--

GlobalVarGetMap = {}

local function CreateScope(parent)

local scope = {}

scope.Parent = parent

scope.LocalList = {}

scope.LocalMap = {}

scope.AccessedLocals = {}

scope.Line = tok:Peek().Line

function scope:GetLocal(name)

--first, try to get my variable

local my = scope.LocalMap[name]

if my then

scope.AccessedLocals[name] = true

return my

end

--next, try parent

if scope.Parent then

local par = scope.Parent:GetLocal(name)

if par then return par end

end

return nil

end

function scope:CreateLocal(name)

--create my own var

local my = {}

my.Scope = scope

my.Name = name

my.CanRename = true

--

scope.LocalList[#scope.LocalList+1] = my

scope.LocalMap[name] = my

--

return my

end

local r = math.random(1e6,1e7-1)

scope.Print = function() return "" end

ScopeContainer[#ScopeContainer+1] = scope

return scope

end

local ParseExpr;

local ParseStatementList;

local function ParseFunctionArgsAndBody(scope)

local funcScope = CreateScope(scope)

if not tok:ConsumeSymbol('(') then

return false, GenerateError("`(` expected.")

end

--arg list

local argList = {}

local isVarArg = false

while not tok:ConsumeSymbol(')') do

if tok:Is('Ident') then

local arg = funcScope:CreateLocal(tok:Get().Data)

argList[#argList+1] = arg

if not tok:ConsumeSymbol(',') then

if tok:ConsumeSymbol(')') then

break

else

return false, GenerateError("`)` expected.")

end

end

elseif tok:ConsumeSymbol('...') then

isVarArg = true

if not tok:ConsumeSymbol(')') then

return false, GenerateError("`...` must be the last argument of a function.")

end

break

else

return false, GenerateError("Argument name or `...` expected")

end

end

--body

local st, body = ParseStatementList(funcScope)

if not st then return false, body end

--end

if not tok:ConsumeKeyword('end') then

return false, GenerateError("`end` expected after function body")

end

local nodeFunc = {}

nodeFunc.AstType = 'Function'

nodeFunc.Scope = funcScope

nodeFunc.Arguments = argList

nodeFunc.Body = body

nodeFunc.VarArg = isVarArg

--

return true, nodeFunc

end

local function ParsePrimaryExpr(scope)

if tok:ConsumeSymbol('(') then

local st, ex = ParseExpr(scope)

if not st then return false, ex end

if not tok:ConsumeSymbol(')') then

return false, GenerateError("`)` Expected.")

end

--save the information about parenthesized expressions somewhere

ex.ParenCount = (ex.ParenCount or 0) + 1

return true, ex

elseif tok:Is('Ident') then

local id = tok:Get()

local var = scope:GetLocal(id.Data)

if not var then

GlobalVarGetMap[id.Data] = true

end

--

local nodePrimExp = {}

nodePrimExp.AstType = 'VarExpr'

nodePrimExp.Name = id.Data

nodePrimExp.Local = var

--

return true, nodePrimExp

else

return false, GenerateError("primary expression expected")

end

end

local function ParseSuffixedExpr(scope, onlyDotColon)

--base primary expression

local st, prim = ParsePrimaryExpr(scope)

if not st then return false, prim end

--

while true do

if tok:IsSymbol('.') or tok:IsSymbol(':') then

local symb = tok:Get().Data

if symb == ":" then

-- scope:CreateLocal("self")

end

if not tok:Is('Ident') then

return false, GenerateError(" expected.")

end

local id = tok:Get()

local nodeIndex = {}

nodeIndex.AstType = 'MemberExpr'

nodeIndex.Base = prim

nodeIndex.Indexer = symb

nodeIndex.Ident = id

--

prim = nodeIndex

elseif not onlyDotColon and tok:ConsumeSymbol('[') then

local st, ex = ParseExpr(scope)

if not st then return false, ex end

if not tok:ConsumeSymbol(']') then

return false, GenerateError("`]` expected.")

end

local nodeIndex = {}

nodeIndex.AstType = 'IndexExpr'

nodeIndex.Base = prim

nodeIndex.Index = ex

--

prim = nodeIndex

elseif not onlyDotColon and tok:ConsumeSymbol('(') then

local args = {}

while not tok:ConsumeSymbol(')') do

local st, ex = ParseExpr(scope)

if not st then return false, ex end

args[#args+1] = ex

if not tok:ConsumeSymbol(',') then

if tok:ConsumeSymbol(')') then

break

else

return false, GenerateError("`)` Expected.")

end

end

end

local nodeCall = {}

nodeCall.AstType = 'CallExpr'

nodeCall.Base = prim

nodeCall.Arguments = args

--

prim = nodeCall

elseif not onlyDotColon and tok:Is('String') then

--string call

local nodeCall = {}

nodeCall.AstType = 'StringCallExpr'

nodeCall.Base = prim

nodeCall.Arguments = {tok:Get()}

--

prim = nodeCall

elseif not onlyDotColon and tok:IsSymbol('{') then

--table call

local st, ex = ParseExpr(scope)

if not st then return false, ex end

local nodeCall = {}

nodeCall.AstType = 'TableCallExpr'

nodeCall.Base = prim

nodeCall.Arguments = {ex}

--

prim = nodeCall

else

break

end

end

return true, prim

end

local function ParseSimpleExpr(scope)

if tok:Is('Number') then

local nodeNum = {}

nodeNum.AstType = 'NumberExpr'

nodeNum.Value = tok:Get()

return true, nodeNum

elseif tok:Is('String') then

local nodeStr = {}

nodeStr.AstType = 'StringExpr'

nodeStr.Value = tok:Get()

return true, nodeStr

elseif tok:ConsumeKeyword('nil') then

local nodeNil = {}

nodeNil.AstType = 'NilExpr'

return true, nodeNil

elseif tok:IsKeyword('false') or tok:IsKeyword('true') then

local nodeBoolean = {}

nodeBoolean.AstType = 'BooleanExpr'

nodeBoolean.Value = (tok:Get().Data == 'true')

return true, nodeBoolean

elseif tok:ConsumeSymbol('...') then

local nodeDots = {}

nodeDots.AstType = 'DotsExpr'

return true, nodeDots

elseif tok:ConsumeSymbol('{') then

local v = {}

v.AstType = 'ConstructorExpr'

v.EntryList = {}

--

while true do

if tok:IsSymbol('[') then

--key

tok:Get()

local st, key = ParseExpr(scope)

if not st then

return false, GenerateError("Key Expression Expected")

end

if not tok:ConsumeSymbol(']') then

return false, GenerateError("`]` Expected")

end

if not tok:ConsumeSymbol('=') then

return false, GenerateError("`=` Expected")

end

local st, value = ParseExpr(scope)

if not st then

return false, GenerateError("Value Expression Expected")

end

v.EntryList[#v.EntryList+1] = {

Type = 'Key';

Key = key;

Value = value;

}

elseif tok:Is('Ident') then

--value or key

local lookahead = tok:Peek(1)

if lookahead.Type == 'Symbol' and lookahead.Data == '=' then

--we are a key

local key = tok:Get()

if not tok:ConsumeSymbol('=') then

return false, GenerateError("`=` Expected")

end

local st, value = ParseExpr(scope)

if not st then

return false, GenerateError("Value Expression Expected")

end

v.EntryList[#v.EntryList+1] = {

Type = 'KeyString';

Key = key.Data;

Value = value;

}

else

--we are a value

local st, value = ParseExpr(scope)

if not st then

return false, GenerateError("Value Exected")

end

v.EntryList[#v.EntryList+1] = {

Type = 'Value';

Value = value;

}

end

elseif tok:ConsumeSymbol('}') then

break

else

--value

local st, value = ParseExpr(scope)

v.EntryList[#v.EntryList+1] = {

Type = 'Value';

Value = value;

}

if not st then

return false, GenerateError("Value Expected")

end

end

if tok:ConsumeSymbol(';') or tok:ConsumeSymbol(',') then

--all is good

elseif tok:ConsumeSymbol('}') then

break

else

return false, GenerateError("`}` or table entry Expected")

end

end

return true, v

elseif tok:ConsumeKeyword('function') then

local st, func = ParseFunctionArgsAndBody(scope)

if not st then return false, func end

--

func.IsLocal = true

return true, func

else

return ParseSuffixedExpr(scope)

end

end

local unops = lookupify{'-', 'not', '#'}

local unopprio = 8

local priority = {

['+'] = {6,6};

['-'] = {6,6};

['%'] = {7,7};

['/'] = {7,7};

['*'] = {7,7};

['^'] = {10,9};

['..'] = {5,4};

['=='] = {3,3};

['<'] = {3,3};

['<='] = {3,3};

['~='] = {3,3};

['>'] = {3,3};

['>='] = {3,3};

['and'] = {2,2};

['or'] = {1,1};

}

local function ParseSubExpr(scope, level)

--base item, possibly with unop prefix

local st, exp

if unops[tok:Peek().Data] then

local op = tok:Get().Data

st, exp = ParseSubExpr(scope, unopprio)

if not st then return false, exp end

local nodeEx = {}

nodeEx.AstType = 'UnopExpr'

nodeEx.Rhs = exp

nodeEx.Op = op

exp = nodeEx

else

st, exp = ParseSimpleExpr(scope)

if not st then return false, exp end

end

--next items in chain

while true do

local prio = priority[tok:Peek().Data]

if prio and prio[1] > level then

local op = tok:Get().Data

local st, rhs = ParseSubExpr(scope, prio[2])

if not st then return false, rhs end

local nodeEx = {}

nodeEx.AstType = 'BinopExpr'

nodeEx.Lhs = exp

nodeEx.Op = op

nodeEx.Rhs = rhs

--

exp = nodeEx

else

break

end

end

return true, exp

end

ParseExpr = function(scope)

return ParseSubExpr(scope, 0)

end

local function ParseStatement(scope)

local stat = nil

if tok:ConsumeKeyword('if') then

--setup

local nodeIfStat = {}

nodeIfStat.AstType = 'IfStatement'

nodeIfStat.Clauses = {}

--clauses

repeat

local st, nodeCond = ParseExpr(scope)

if not st then return false, nodeCond end

if not tok:ConsumeKeyword('then') then

return false, GenerateError("`then` expected.")

end

local st, nodeBody = ParseStatementList(scope)

if not st then return false, nodeBody end

nodeIfStat.Clauses[#nodeIfStat.Clauses+1] = {

Condition = nodeCond;

Body = nodeBody;

}

until not tok:ConsumeKeyword('elseif')

--else clause

if tok:ConsumeKeyword('else') then

local st, nodeBody = ParseStatementList(scope)

if not st then return false, nodeBody end

nodeIfStat.Clauses[#nodeIfStat.Clauses+1] = {

Body = nodeBody;

}

end

--end

if not tok:ConsumeKeyword('end') then

return false, GenerateError("`end` expected.")

end

stat = nodeIfStat

elseif tok:ConsumeKeyword('while') then

--setup

local nodeWhileStat = {}

nodeWhileStat.AstType = 'WhileStatement'

--condition

local st, nodeCond = ParseExpr(scope)

if not st then return false, nodeCond end

--do

if not tok:ConsumeKeyword('do') then

return false, GenerateError("`do` expected.")

end

--body

local st, nodeBody = ParseStatementList(scope)

if not st then return false, nodeBody end

--end

if not tok:ConsumeKeyword('end') then

return false, GenerateError("`end` expected.")

end

--return

nodeWhileStat.Condition = nodeCond

nodeWhileStat.Body = nodeBody

stat = nodeWhileStat

elseif tok:ConsumeKeyword('do') then

--do block

local st, nodeBlock = ParseStatementList(scope)

if not st then return false, nodeBlock end

if not tok:ConsumeKeyword('end') then

return false, GenerateError("`end` expected.")

end

local nodeDoStat = {}

nodeDoStat.AstType = 'DoStatement'

nodeDoStat.Body = nodeBlock

stat = nodeDoStat

elseif tok:ConsumeKeyword('for') then

--for block

if not tok:Is('Ident') then

return false, GenerateError(" expected.")

end

local baseVarName = tok:Get()

if tok:ConsumeSymbol('=') then

--numeric for

local forScope = CreateScope(scope)

local forVar = forScope:CreateLocal(baseVarName.Data)

--

local st, startEx = ParseExpr(scope)

if not st then return false, startEx end

if not tok:ConsumeSymbol(',') then

return false, GenerateError("`,` Expected")

end

local st, endEx = ParseExpr(scope)

if not st then return false, endEx end

local st, stepEx;

if tok:ConsumeSymbol(',') then

st, stepEx = ParseExpr(scope)

if not st then return false, stepEx end

end

if not tok:ConsumeKeyword('do') then

return false, GenerateError("`do` expected")

end

--

local st, body = ParseStatementList(forScope)

if not st then return false, body end

if not tok:ConsumeKeyword('end') then

return false, GenerateError("`end` expected")

end

--

local nodeFor = {}

nodeFor.AstType = 'NumericForStatement'

nodeFor.Scope = forScope

nodeFor.Variable = forVar

nodeFor.Start = startEx

nodeFor.End = endEx

nodeFor.Step = stepEx

nodeFor.Body = body

stat = nodeFor

else

--generic for

local forScope = CreateScope(scope)

--

local varList = {forScope:CreateLocal(baseVarName.Data)}

while tok:ConsumeSymbol(',') do

if not tok:Is('Ident') then

return false, GenerateError("for variable expected.")

end

varList[#varList+1] = forScope:CreateLocal(tok:Get().Data)

end

if not tok:ConsumeKeyword('in') then

return false, GenerateError("`in` expected.")

end

local generators = {}

local st, firstGenerator = ParseExpr(scope)

if not st then return false, firstGenerator end

generators[#generators+1] = firstGenerator

while tok:ConsumeSymbol(',') do

local st, gen = ParseExpr(scope)

if not st then return false, gen end

generators[#generators+1] = gen

end

if not tok:ConsumeKeyword('do') then

return false, GenerateError("`do` expected.")

end

local st, body = ParseStatementList(forScope)

if not st then return false, body end

if not tok:ConsumeKeyword('end') then

return false, GenerateError("`end` expected.")

end

--

local nodeFor = {}

nodeFor.AstType = 'GenericForStatement'

nodeFor.Scope = forScope

nodeFor.VariableList = varList

nodeFor.Generators = generators

nodeFor.Body = body

stat = nodeFor

end

elseif tok:ConsumeKeyword('repeat') then

local st, body = ParseStatementList(scope)

if not st then return false, body end

--

if not tok:ConsumeKeyword('until') then

return false, GenerateError("`until` expected.")

end

--

local st, cond = ParseExpr(body.Scope)

if not st then return false, cond end

--

local nodeRepeat = {}

nodeRepeat.AstType = 'RepeatStatement'

nodeRepeat.Condition = cond

nodeRepeat.Body = body

stat = nodeRepeat

elseif tok:ConsumeKeyword('function') then

if not tok:Is('Ident') then

return false, GenerateError("Function name expected")

end

local st, name = ParseSuffixedExpr(scope, true) --true => only dots and colons

if not st then return false, name end

--

local st, func = ParseFunctionArgsAndBody(scope)

if not st then return false, func end

--

func.IsLocal = false

func.Name = name

stat = func

elseif tok:ConsumeKeyword('local') then

if tok:Is('Ident') then

local varList = {tok:Get().Data}

while tok:ConsumeSymbol(',') do

if not tok:Is('Ident') then

return false, GenerateError("local var name expected")

end

varList[#varList+1] = tok:Get().Data

end

local initList = {}

if tok:ConsumeSymbol('=') then

repeat

local st, ex = ParseExpr(scope)

if not st then return false, ex end

initList[#initList+1] = ex

until not tok:ConsumeSymbol(',')

end

--now patch var list

--we can't do this before getting the init list, because the init list does not

--have the locals themselves in scope.

for i, v in pairs(varList) do

varList[i] = scope:CreateLocal(v)

end

local nodeLocal = {}

nodeLocal.AstType = 'LocalStatement'

nodeLocal.LocalList = varList

nodeLocal.InitList = initList

--

stat = nodeLocal

elseif tok:ConsumeKeyword('function') then

if not tok:Is('Ident') then

return false, GenerateError("Function name expected")

end

local name = tok:Get().Data

local localVar = scope:CreateLocal(name)

--

local st, func = ParseFunctionArgsAndBody(scope)

if not st then return false, func end

--

func.Name = localVar

func.IsLocal = true

stat = func

else

return false, GenerateError("local var or function def expected")

end

elseif tok:ConsumeKeyword('return') then

local exList = {}

if not tok:IsKeyword('end') then

local st, firstEx = ParseExpr(scope)

if st then

exList[1] = firstEx

while tok:ConsumeSymbol(',') do

local st, ex = ParseExpr(scope)

if not st then return false, ex end

exList[#exList+1] = ex

end

end

end

local nodeReturn = {}

nodeReturn.AstType = 'ReturnStatement'

nodeReturn.Arguments = exList

stat = nodeReturn

elseif tok:ConsumeKeyword('break') then

local nodeBreak = {}

nodeBreak.AstType = 'BreakStatement'

stat = nodeBreak

else

--statementParseExpr

local st, suffixed = ParseSuffixedExpr(scope)

if not st then return false, suffixed end

--assignment or call?

if tok:IsSymbol(',') or tok:IsSymbol('=') then

--check that it was not parenthesized, making it not an lvalue

if (suffixed.ParenCount or 0) > 0 then

return false, GenerateError("Can not assign to parenthesized expression, is not an lvalue")

end

--more processing needed

local lhs = {suffixed}

while tok:ConsumeSymbol(',') do

local st, lhsPart = ParseSuffixedExpr(scope)

if not st then return false, lhsPart end

lhs[#lhs+1] = lhsPart

end

--equals

if not tok:ConsumeSymbol('=') then

return false, GenerateError("`=` Expected.")

end

--rhs

local rhs = {}

local st, firstRhs = ParseExpr(scope)

if not st then return false, firstRhs end

rhs[1] = firstRhs

while tok:ConsumeSymbol(',') do

local st, rhsPart = ParseExpr(scope)

if not st then return false, rhsPart end

rhs[#rhs+1] = rhsPart

end

--done

local nodeAssign = {}

nodeAssign.AstType = 'AssignmentStatement'

nodeAssign.Lhs = lhs

nodeAssign.Rhs = rhs

stat = nodeAssign

elseif suffixed.AstType == 'CallExpr' or

suffixed.AstType == 'TableCallExpr' or

suffixed.AstType == 'StringCallExpr'

then

--it's a call statement

local nodeCall = {}

nodeCall.AstType = 'CallStatement'

nodeCall.Expression = suffixed

stat = nodeCall

else

return false, GenerateError("Assignment Statement Expected")

end

end

stat.HasSemicolon = tok:ConsumeSymbol(';')

return true, stat

end

local statListCloseKeywords = lookupify{'end', 'else', 'elseif', 'until'}

ParseStatementList = function(scope)

local nodeStatlist = {}

nodeStatlist.Scope = CreateScope(scope)

nodeStatlist.AstType = 'Statlist'

--

local stats = {}

--

while not statListCloseKeywords[tok:Peek().Data] and not tok:IsEof() do

local st, nodeStatement = ParseStatement(nodeStatlist.Scope)

if not st then return false, nodeStatement end

stats[#stats+1] = nodeStatement

end

--

nodeStatlist.Body = stats

return true, nodeStatlist

end

local function mainfunc()

local topScope = CreateScope()

return ParseStatementList(topScope)

end

local st, main = mainfunc()

return st, main

end

-- Analysis code actually begins here, the above is just the parser

local DontIngoreReturns = {require=1}

local function TryFetchPageContent(obj)

local out = ""

if type(obj) == "string" then

local page = mw.title.new(obj)

if not page then

error("Failed to grab the page '"..obj.."'")

end

out = page:getContent() or ""

elseif obj == nil then

out = mw.title.getCurrentTitle().subjectPageTitle:getContent() or ""

else

error("Unrecognised page input type '"..type(obj).."'")

end

return out

end

local function GenerateMessages(ast)

local messages = {}

local function AddMessage(s,m)

messages[#messages+1] = {Scope=s, Message=m}

end

--Non-AST based checks (AKA scope checks on ScopeContainer and GlobalVarGetMap)

local ignoredUnderscores = 0

for _,scope in pairs(ScopeContainer) do

for Local,_ in pairs(scope.LocalMap) do

local scopeName = (scope.Line == 1 and "Main scope") or "Scope starting line "..scope.Line

if not scope.AccessedLocals[Local] then

if Local == "_" then

ignoredUnderscores = ignoredUnderscores + 1

else

AddMessage(scopeName, ""..Local.." is defined but never referenced")

end

elseif Local == "_" then

AddMessage(scopeName, "Variable _ is referenced, despite the name implying it is unused")

end

end

end

if ignoredUnderscores > 1 then

AddMessage("Entire script", ignoredUnderscores.." local variables called _ were defined but never referenced, likely intentionally")

elseif ignoredUnderscores == 1 then

AddMessage("Entire script", "1 local variable called _ was defined but never referenced, likely intentionally")

end

for global,_ in pairs(GlobalVarGetMap) do

if not rawget(_G,global) and global ~= "self" then --self picking up wrongly is an issue in the parser, and generally not an expected global name, so we lazily ignore

AddMessage("Entire script", "Global variable "..global.." was referenced or defined, yet isn't a standard global")

end

end

--AST based checks (E.g. ignored returns of certain calls)

local checked = {}

local function deepscan(t)

checked[t] = true

if t.AstType == "CallStatement" then

local caller = t.Expression.Base

if caller.AstType == "VarExpr" and DontIngoreReturns[caller.Name] then

local args = t.Expression.Arguments

if not(args[1] and args[1].Value and args[1].Value.Constant == "strict") then

local representation = (args[1] and args[1].Value and caller.Name.."("..args[1].Value.Data..")") or caller.Name.."()"

AddMessage("Unknown", "Return value of "..representation.." was ignored")

end

end

end

for _,v in pairs(t) do

if type(v) == "table" and not checked[v] then

deepscan(v)

end

end

end

deepscan(ast)

return messages

end

--Module entry point

local function run(C)

local C = TryFetchPageContent(C)

if C == "" then

print("No lua to parse from the given input")

return

end

local s,p = ParseLua(C)

if not s then

error("Failed to parse the lua - "..p)

end

local messages = GenerateMessages(p)

if #messages == 0 then

print("No issues found")

else

for _,message in pairs(messages) do

local filtered = string.gsub(message.Message, "(.-)", "%1") --Remove formatting tags

print(message.Scope.." || "..filtered)

end

end

print("Analysis finished")

end

--Template entry point

local function main(frame)

local page = mw.title.getCurrentTitle().subjectPageTitle.prefixedText

page = string.gsub(page, "(.+)/doc$","%1") --strip /doc

if frame.args[1] and frame.args[1] ~= "" then

page = frame.args[1]

end

local C = TryFetchPageContent(page)

if C == "" then

return "No lua to parse in page '"..page.."'"

end

local s,p = ParseLua(C)

if not s then

error("Failed to parse the lua - "..p)

end

local messages = GenerateMessages(p)

if #messages == 0 then

return '

class="wikitable"\n! style="text-align:center" | Basic code analysis for ' .. page .. '' ..

'\n

\n| style="text-align:center"| No issues found\n
'

else

local tableContent = '

class="wikitable sortable"\n! style="text-align:center" colspan=2 | Basic code analysis for ' .. page .. '' .. '\n
\n! Scope !! Message'

for _,message in pairs(messages) do

tableContent = tableContent .. "\n

\n| " .. message.Scope .. "" .. message.Message

end

return tableContent .. "\n

"

end

end

return {

run = run,

main = main,

ParseLua = ParseLua --For debugging or just general curiosity

}