497 lines
18 KiB
Lua
497 lines
18 KiB
Lua
|
local CompileFile = CompileFile
|
||
|
local CompileString = CompileString
|
||
|
local debug = debug
|
||
|
local error = error
|
||
|
local file = file
|
||
|
local hook = hook
|
||
|
local include = include
|
||
|
local isfunction = isfunction
|
||
|
local math = math
|
||
|
local os = os
|
||
|
local pcall = pcall
|
||
|
local string = string
|
||
|
local table = table
|
||
|
local tonumber = tonumber
|
||
|
local unpack = unpack
|
||
|
|
||
|
-- Template for syntax errors
|
||
|
-- The [ERROR] start of it cannot be removed, because that would make the
|
||
|
-- error mechanism remove all square brackets. Only Garry can make that bullshit up.
|
||
|
local synErrTranslation = [=[[ERROR] Lua is unable to understand file "%s" because its author made a mistake around line number %i.
|
||
|
The best help I can give you is this:
|
||
|
|
||
|
%s
|
||
|
|
||
|
Hints:
|
||
|
%s
|
||
|
|
||
|
------- End of Simplerr error -------
|
||
|
]=] -- The end is a special string by which simplerr errors are internally recognised
|
||
|
|
||
|
-- Template for runtime errors
|
||
|
local runErrTranslation = [=[[ERROR] A runtime error has occurred in "%s" on line %i.
|
||
|
The best help I can give you is this:
|
||
|
|
||
|
%s
|
||
|
|
||
|
Hints:
|
||
|
%s
|
||
|
|
||
|
The responsibility for this error lies with (the authors of) one (or more) of these files:
|
||
|
%s
|
||
|
------- End of Simplerr error -------
|
||
|
]=]
|
||
|
|
||
|
-- Structure that contains syntax errors and their translations. Catches only the most common errors.
|
||
|
-- Order is important: the structure with the first match is taken.
|
||
|
local synErrs = {
|
||
|
{
|
||
|
match = "'=' expected near '(.*)'",
|
||
|
text = "Right before the '%s', Lua expected to read an '='-sign, but it didn't.",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"Did you simply forget the '='-sign?",
|
||
|
"Did you forget a comma?",
|
||
|
"Is this supposed to be a local variable?"
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "'.' expected [(]to close '([{[(])' at line ([0-9-]+)[)] near '(.*)'",
|
||
|
text = "There is an opening '%s' bracket at line %i, but this bracket is never closed or not closed in time. It was expected to be closed before the '%s' at line %i.",
|
||
|
format = function(m, l) return m[1], m[2], m[3], l end,
|
||
|
hints = {
|
||
|
"Did you forget a comma?",
|
||
|
"All open brackets ({, (, [) must have a matching closing bracket. Are you sure it's there?",
|
||
|
"Brackets must be opened and closed in the right order. This will work: ({}), but this won't: ({)}."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "'end' expected [(]to close '(.*)' at line ([0-9-]+)[)] near '(.*)'",
|
||
|
text = "An '%s' was started on line %i, but it was never ended or not ended in time. It was expected to be ended before the '%s' at line %i",
|
||
|
format = function(m, l) return m[1], m[2], m[3], l end,
|
||
|
hints = {
|
||
|
"For every if/for/do/while/function there must be an 'end' that closes it."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "unfinished string near '(.*)'",
|
||
|
text = "The string '%s' at line %i is opened, but not closed.",
|
||
|
format = function(m, l) return m[1], l end,
|
||
|
hints = {
|
||
|
"A string is a different word for literal text.",
|
||
|
"Strings must be in single or double quotation marks (e.g. 'example', \"example\")",
|
||
|
"A third option for strings is for them to be in double square brackets.",
|
||
|
"Whatever you use (quotations or square brackets), you must not forget that strings are enclosed within a pair of quotation marks/square brackets."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "unfinished long string near '(.*)'",
|
||
|
text = "Lua expected to see the end of a multiline string somewhere before the '%s' at line %i.",
|
||
|
format = function(m, l) return m[1], l end,
|
||
|
hints = {
|
||
|
"A string is a different word for literal text.",
|
||
|
"Multiline strings are strings that span over multiple lines.",
|
||
|
"Multiline strings must be enclosed by double square brackets.",
|
||
|
"Whatever you use (quotations or square brackets), you must not forget that strings are enclosed within a pair of quotation marks/square brackets.",
|
||
|
"If you used brackets, the source of the mistake may be somewhere above the reported line."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "unfinished long comment near '(.*)'",
|
||
|
text = "Lua expected to see the end of a multiline comment somewhere before the '%s' at line %i.",
|
||
|
format = function(m, l) return m[1], l end,
|
||
|
hints = {
|
||
|
"A comment is text ignored by Lua.",
|
||
|
"Multiline comments are ones that span multiple lines.",
|
||
|
"Multiline comments must be enclosed by either /* and */ or double square brackets.",
|
||
|
"Whatever you use (/**/ or square brackets), you must not forget that once you start a comment, you must end it.",
|
||
|
"The source of the mistake may be somewhere above the reported line."
|
||
|
}
|
||
|
},
|
||
|
-- Generic error messages
|
||
|
{
|
||
|
match = "function arguments expected near '(.*)'",
|
||
|
text = "A function is being called right before '%s', but its arguments are not given.",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"Did you write 'something:otherthing'? Try changing it to 'something:otherthing()'"
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "unexpected symbol near '(.*)'",
|
||
|
text = "Right before the '%s', Lua encountered something it could not make sense of.",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {"Did you forget something here? (Perhaps a closing bracket)", "Is it a typo?"}
|
||
|
},
|
||
|
{
|
||
|
match = "'(.*)' expected near '(.*)'",
|
||
|
text = "Right before the '%s', Lua expected to read a '%s', but it didn't.",
|
||
|
format = function(m) return m[2], m[1] end,
|
||
|
hints = {"Did you forget a keyword?", "Did you forget a comma?"}
|
||
|
},
|
||
|
{
|
||
|
match = "malformed number near '(.*)'",
|
||
|
text = "Lua attempted to read '%s' as a number, but failed to do so.",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"Numbers starting with '0x' are hexidecimal.",
|
||
|
"Lua can get confused when doing '<number>..\"some text\"'. Try inserting a space between the number and the '..'."
|
||
|
}
|
||
|
},
|
||
|
}
|
||
|
|
||
|
-- Similar structure for runtime errors. Catches only the most common errors.
|
||
|
-- Order is important: the structure with the first match is taken
|
||
|
local runErrs = {
|
||
|
{
|
||
|
match = "table index is nil",
|
||
|
text = "A table is being indexed by something that does not exist (table index is nil).", -- Requires improvement
|
||
|
format = function() end,
|
||
|
hints = {
|
||
|
"The thing between square brackets does not exist (is nil)."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "table index is NaN",
|
||
|
text = "A table is being indexed by something that is not really a number (table index is NaN).",
|
||
|
format = function() end,
|
||
|
hints = {
|
||
|
"Did you divide zero by zero thinking it would be funny?"
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to index global '(.*)' [(]a nil value[)]",
|
||
|
text = "'%s' is being indexed like it is a table, but in reality it does not exist (is nil).",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"You either have 'something.somethingElse', 'something[somethingElse]' or 'something:somethingElse(more)'. The 'something' here does not exist."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to index global '(.*)' [(]a (.*) value[)]",
|
||
|
text = "'%s' is being indexed like it is a table, but in reality it is a %s value.",
|
||
|
format = function(m) return m[1], m[2] end,
|
||
|
hints = {
|
||
|
"You either have 'something.somethingElse' or 'something:somethingElse(more)'. The 'something' here is not a table."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to index a nil value",
|
||
|
text = "Something is being indexed like it is a table, but in reality does not exist (is nil).",
|
||
|
format = function() end,
|
||
|
hints = {
|
||
|
"You either have 'something.somethingElse', 'something[somethingElse]' or 'something:somethingElse(more)'. The 'something' here does not exist."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to index a (.*) value",
|
||
|
text = "Something is being indexed like it is a table, but in reality it is a %s value.",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"You either have 'something.somethingElse', 'something[somethingElse]' or 'something:somethingElse(more)'. The 'something' here is not a table."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to call global '(.*)' [(]a nil value[)]",
|
||
|
text = "'%s' is being called like it is a function, but in reality does not exist (is nil).",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"You are doing something(<otherstuff>). The 'something' here does not exist."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to call a nil value",
|
||
|
text = "Something is being called like it is a function, but in reality it does not exist (is nil).",
|
||
|
format = function() end,
|
||
|
hints = {
|
||
|
"You are doing something(<otherstuff>). The 'something' here does not exist."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to call global '(.*)' [(]a (.*) value[)]",
|
||
|
text = "'%s' is being called like it is a function, but in reality it is a %s.",
|
||
|
format = function(m) return m[1], m[2] end,
|
||
|
hints = {
|
||
|
"You are doing something(<otherstuff>). The 'something' here is not a function."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to call a (.*) value",
|
||
|
text = "Something is being called like it is a function, but in reality it is a %s.",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"You are doing something(<otherstuff>). The 'something' here is not a function."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to call field '(.*)' [(]a nil value[)]",
|
||
|
text = "'%s' is being called like it is a function, but in reality it does not exist (is nil).",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"You are doing either stuff.something(<otherstuff>) or stuff:something(<otherstuff>). The 'something' here does not exist."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to call field '(.*)' [(]a (.*) value[)]",
|
||
|
text = "'%s' is being called like it is a function, but in reality it is a %s.",
|
||
|
format = function(m) return m[1], m[2] end,
|
||
|
hints = {
|
||
|
"You are doing either stuff.something(<otherstuff>) or stuff:something(<otherstuff>). The 'something' here is not a function."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to concatenate global '(.*)' [(]a nil value[)]",
|
||
|
text = "'%s' is being concatenated to something else, but '%s' does not exist (is nil).",
|
||
|
format = function(m) return m[1], m[1] end,
|
||
|
hints = {
|
||
|
"Concatenation looks like this: something .. otherThing. Either something or otherThing does not exist."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to concatenate global '(.*)' [(]a (.*) value[)]",
|
||
|
text = "'%s' is being concatenated to something else, but %s values cannot be concatenated.",
|
||
|
format = function(m) return m[1], m[2] end,
|
||
|
hints = {
|
||
|
"Concatenation looks like this: something .. otherThing. Either something or otherThing is neither string nor number."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to concatenate a nil value",
|
||
|
text = "Two (or more) things are being concatenated and one of them does not exist (is nil).",
|
||
|
format = function() end,
|
||
|
hints = {
|
||
|
"Concatenation looks like this: something .. otherThing. Either something or otherThing does not exist."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to concatenate a (.*) value",
|
||
|
text = "Two (or more) things are being concatenated and one of them is neither string nor number, but a %s.",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"Concatenation looks like this: something .. otherThing. Either something or otherThing is neither string nor number."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "stack overflow",
|
||
|
text = "The stack of function calls has overflowed",
|
||
|
format = function() end,
|
||
|
hints = {
|
||
|
"Most likely infinite recursion.",
|
||
|
"Do you have a function calling itself?"
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to compare two (.*) values",
|
||
|
text = "A comparison is being made between two %s values. They cannot be compared.",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"This error usually occurs when two incompatible things are being compared.",
|
||
|
"'comparison' in this context means one of <, >, <=, >= (smaller than, greater than, etc.)"
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to compare (.*) with (.*)",
|
||
|
text = "A comparison is being made between a %s and a %s. This is not possible.",
|
||
|
format = function(m) return m[1], m[2] end,
|
||
|
hints = {
|
||
|
"This error usually occurs when two incompatible things are being compared.",
|
||
|
"'Comparison' in this context means one of <, >, <=, >= (smaller than, greater than, etc.)"
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to perform arithmetic on a (.*) value",
|
||
|
text = "Arithmetic operations are being performed on a %s. This is not possible.",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"'Arithmetic' in this context means adding, multiplying, dividing, etc."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to get length of global '(.*)' [(]a nil value[)]",
|
||
|
text = "The length of '%s' is requested as if it is a table, but in reality it does not exist (is nil).",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"You are doing #something. The 'something' here is does not exist."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to get length of global '(.*)' [(]a (.*) value[)]",
|
||
|
text = "The length of '%s' is requested as if it is a table, but in reality it is a %s.",
|
||
|
format = function(m) return m[1], m[2] end,
|
||
|
hints = {
|
||
|
"You are doing #something. The 'something' here is not a table."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to get length of a nil value",
|
||
|
text = "The length of something is requested as if it is a table, but in reality it does not exist (is nil).",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"You are doing #something. The 'something' here is does not exist."
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
match = "attempt to get length of a (.*) value",
|
||
|
text = "The length of something is requested as if it is a table, but in reality it is a %s.",
|
||
|
format = function(m) return m[1] end,
|
||
|
hints = {
|
||
|
"You are doing #something. The 'something' here is not a table."
|
||
|
}
|
||
|
},
|
||
|
}
|
||
|
|
||
|
module("simplerr")
|
||
|
|
||
|
-- Get a nicely formatted stack trace. Start is where to start numbering
|
||
|
local function getStack(i, start)
|
||
|
i = i or 1
|
||
|
start = start or 1
|
||
|
local stack = {}
|
||
|
|
||
|
-- Invariant: stack level (i + count) >= 2 and <= last stack item
|
||
|
for count = 1, math.huge do -- user visible count
|
||
|
local info = debug.getinfo(i + count, "Sln")
|
||
|
if not info then break end
|
||
|
|
||
|
table.insert(stack, string.format("\t%i. %s on line %s", start + count - 1, info.short_src, info.currentline or "unknown"))
|
||
|
end
|
||
|
|
||
|
return table.concat(stack, "\n")
|
||
|
end
|
||
|
|
||
|
-- Translate a runtime error to simplerr format.
|
||
|
-- Decorate with e.g. wrapError to have it actually throw the error.
|
||
|
function runError(msg, stackNr, hints, path, line, stack)
|
||
|
stackNr = stackNr or 1
|
||
|
hints = hints or {"No hints, sorry."}
|
||
|
hints = "\t- " .. table.concat(hints, "\n\t- ")
|
||
|
|
||
|
if not path and not line then
|
||
|
local info = debug.getinfo(stackNr + 1, "Sln")
|
||
|
path = info.short_src
|
||
|
line = info.currentline
|
||
|
end
|
||
|
|
||
|
return false, string.format(runErrTranslation, path, line, msg, hints, stack or getStack(stackNr + 1))
|
||
|
end
|
||
|
|
||
|
-- Translate the message of an error
|
||
|
local function translateMsg(msg, path, line, errs)
|
||
|
local res
|
||
|
local hints = {"No hints, sorry."}
|
||
|
|
||
|
for i = 1, #errs do
|
||
|
local trans = errs[i]
|
||
|
if not string.find(msg, trans.match) then continue end
|
||
|
|
||
|
-- translate <eof>
|
||
|
msg = string.Replace(msg, "<eof>", "end of the file")
|
||
|
|
||
|
res = string.format(trans.text, trans.format({string.match(msg, trans.match)}, line, path))
|
||
|
hints = trans.hints
|
||
|
|
||
|
break
|
||
|
end
|
||
|
|
||
|
return res or msg, "\t- " .. table.concat(hints, "\n\t- ")
|
||
|
end
|
||
|
|
||
|
-- Translate an error into a language understandable by non-programmers
|
||
|
local function translateError(path, err, translation, errs, stack)
|
||
|
-- Using .* instead of path because path may be wrong when error is called
|
||
|
local line, msg = string.match(err, ".*:([0-9-]+): (.*)")
|
||
|
line = tonumber(line)
|
||
|
|
||
|
local msg, hints = translateMsg(msg, path, line, errs)
|
||
|
local res = string.format(translation, path, line, msg, hints, stack)
|
||
|
return res
|
||
|
end
|
||
|
|
||
|
-- Call a function and catch immediate runtime errors
|
||
|
function safeCall(f, ...)
|
||
|
local res = {pcall(f, ...)}
|
||
|
local succ, err = res[1], res[2]
|
||
|
|
||
|
if succ then return unpack(res) end
|
||
|
|
||
|
local info = debug.getinfo(f)
|
||
|
local path = info.short_src
|
||
|
|
||
|
-- Investigate the stack. Not using path in match because calls to error can give a different path
|
||
|
local line = string.match(err, ".*:([0-9-]+)")
|
||
|
local stack = string.format("\t1. %s on line %s\n", path, line) .. getStack(2, 2) -- add called func to stack
|
||
|
|
||
|
-- Line and source info aren't always in the error
|
||
|
if not line then
|
||
|
line = info.currentline
|
||
|
err = string.format("%s:%s: %s", path, line, err)
|
||
|
end
|
||
|
|
||
|
-- Skip translation if the error is already a simplerr error
|
||
|
-- This prevents nested simplerr errors when runError is called by a file loaded by runFile
|
||
|
local mustTranslate = not string.find(err, "------- End of Simplerr error -------")
|
||
|
return false, mustTranslate and translateError(path, err, runErrTranslation, runErrs, stack) or err
|
||
|
end
|
||
|
|
||
|
-- Run a file or explain its syntax errors in layman's terms
|
||
|
-- Returns bool succeed, [string error]
|
||
|
-- Do NOT use this on clientside files.
|
||
|
-- Clientside files sent by the server cannot be read using file.Read unless you're the host of a listen server
|
||
|
function runFile(path)
|
||
|
if not file.Exists(path, "LUA") then error(string.format("Could not run file '%s' (file not found)", path)) end
|
||
|
local contents = file.Read(path, "LUA")
|
||
|
|
||
|
-- Files can make a comment containing #NoSimplerr# to disable simplerr (and thus enable autorefresh)
|
||
|
if string.find(contents, "#NoSimplerr#") then include(path) return true end
|
||
|
|
||
|
-- Catch syntax errors with CompileString
|
||
|
local err = CompileString(contents, path, false)
|
||
|
|
||
|
-- CompileString returns the following string whenever a file is empty: Invalid script - or too short.
|
||
|
-- It also prints: Not running script <path> - it's too short.
|
||
|
-- If so, do nothing.
|
||
|
if err == "Invalid script - or too short." then return true end
|
||
|
|
||
|
-- No syntax errors, check for immediate runtime errors using CompileFile
|
||
|
-- Using the function CompileString returned leads to relative path trouble
|
||
|
if isfunction(err) then return safeCall(CompileFile(path), path) end
|
||
|
|
||
|
return false, translateError(path, err, synErrTranslation, synErrs)
|
||
|
end
|
||
|
|
||
|
-- Error wrapper: decorator for runFile and safeCall that throws an error on failure.
|
||
|
-- Breaks execution. Must be the last decorator.
|
||
|
function wrapError(succ, err, ...)
|
||
|
if succ then return succ, err, ... end
|
||
|
|
||
|
error(err)
|
||
|
end
|
||
|
|
||
|
-- Hook wrapper: Calls a hook on error
|
||
|
function wrapHook(succ, err, ...)
|
||
|
if not succ then hook.Call("onSimplerrError", nil, err) end
|
||
|
|
||
|
return succ, err, ...
|
||
|
end
|
||
|
|
||
|
-- Logging wrapper: decorator for runFile and safeCall that logs failures.
|
||
|
local log = {}
|
||
|
function wrapLog(succ, err, ...)
|
||
|
if succ then return succ, err, ... end
|
||
|
|
||
|
local data = {
|
||
|
err = err,
|
||
|
time = os.time()
|
||
|
}
|
||
|
|
||
|
table.insert(log, data)
|
||
|
|
||
|
return succ, err, ...
|
||
|
end
|
||
|
|
||
|
-- Retrieve the log
|
||
|
function getLog() return log end
|
||
|
|
||
|
-- Clear the log
|
||
|
function clearLog() log = {} end
|