--
local print = mw and mw.log or printrequire("strict")
local function lookupify(tb) for _, v in pairs(tb) do tb[v] = true end return tbend
local WhiteChars = lookupifylocal LowerChars = lookupifylocal UpperChars = lookupifylocal Digits = lookupifylocal HexDigits = lookupify
local Symbols = lookupify
local Keywords = lookupify;
local BackslashEscaping =
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
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
--check for the end local foundEnd = true if peek
--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
'-' 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
elseif UpperChars[c] or LowerChars[c] or c
'_') local dat = src:sub(start, p-1) if Keywords[dat] then toEmit = else toEmit = end
elseif Digits[c] or (peek
'0' and peek(1)
elseif c
'\"' then --string const local delim = get local content = "" while true do local c = get if c
"x" then local n1 = get if n1
delim or not HexDigits[n1] then generateError("invalid escape sequence near '"..delim.."'") end local n2 = get if 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 generateError("invalid escape sequence near '"..delim.."'") end end elseif c
then generateError("Unfinished string near
elseif c
else local contents, all = tryGetLongString if contents then toEmit = 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
--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
--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
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
kw then self:Get return true else return nil end end
function tok:IsKeyword(kw) local t = tok:Peek return t.Type
kw end
function tok:IsSymbol(s) local t = tok:Peek return t.Type
s end
function tok:IsEof return tok:Peek.Type
return true, tokend
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)
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
--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 "
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
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 = -- prim = nodeCall
elseif not onlyDotColon and tok:IsSymbol('') 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 local unopprio = 8 local priority = 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] = 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] = 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("
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 = 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 = 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
'TableCallExpr' or suffixed.AstType
stat.HasSemicolon = tok:ConsumeSymbol(';') return true, stat end
local statListCloseKeywords = lookupify 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, mainend
-- Analysis code actually begins here, the above is just the parserlocal DontIngoreReturns =
local function TryFetchPageContent(obj) local out = "" if type(obj)
nil then out = mw.title.getCurrentTitle.subjectPageTitle:getContent or "" else error("Unrecognised page input type '"..type(obj).."'") end return outend
local function GenerateMessages(ast) local messages = local function AddMessage(s,m) messages[#messages+1] = 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
"_" then ignoredUnderscores = ignoredUnderscores + 1 else AddMessage(scopeName, ""..Local.."
is defined but never referenced") end elseif Local
_
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 ignoredUnderscores1 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
"VarExpr" and DontIngoreReturns[caller.Name] then local args = t.Expression.Arguments if not(args[1] and args[1].Value and args[1].Value.Constant
"..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 messagesend--Module entry pointlocal function run(C) local C = TryFetchPageContent(C)
if C
local messages = GenerateMessages(p) if #messages
--Template entry pointlocal 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
local messages = GenerateMessages(p) if #messages
Basic code analysis for ' .. page .. '' .. '\n | -\n | No issues found\n |
---|
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 |
---|
return