diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua new file mode 100644 index 0000000..d977f1f --- /dev/null +++ b/MacroErrorChecker.lua @@ -0,0 +1,443 @@ +--[[ + Macro Syntax Error Checker + Author: Mewtiny + License: MIT License + + Validates macro syntax and reports errors to help users debug their macros +]] +local _G = _G or getfenv(0) +local CleveRoids = _G.CleveRoids or {} + +-- Known valid conditionals +local VALID_CONDITIONALS = { + -- General + actionbar = true, noactionbar = true, + button = true, + channeling = true, nochanneling = true, + class = true, noclass = true, + cdgcd = true, nocdgcd = true, + combo = true, nocombo = true, + cooldown = true, nocooldown = true, + equipped = true, noequipped = true, + flyable = true, noflyable = true, + flying = true, noflying = true, + form = true, noform = true, + group = true, nogroup = true, + indoors = true, noindoors = true, + known = true, noknown = true, + mod = true, nomod = true, + mounted = true, nomounted = true, + mybuff = true, nomybuff = true, + mydebuff = true, nomydebuff = true, + myhp = true, nomyhp = true, + myhplost = true, nomyhplost = true, + mypower = true, nomypower = true, + mypowerlost = true, nomypowerlost = true, + myrawhp = true, nomyrawhp = true, + myrawpower = true, nomyrawpower = true, + outdoors = true, nooutdoors = true, + pet = true, nopet = true, + petbuff = true, nopetbuff = true, + petdebuff = true, nopetdebuff = true, + reactive = true, noreactive = true, + resting = true, noresting = true, + stance = true, nostance = true, + stat = true, nostat = true, + stealth = true, nostealth = true, + swimming = true, noswimming = true, + talent = true, notalent = true, + zone = true, nozone = true, + + -- Unit based + alive = true, noalive = true, + buff = true, nobuff = true, + casting = true, nocasting = true, + combat = true, nocombat = true, + dead = true, nodead = true, + debuff = true, nodebuff = true, + harm = true, noharm = true, + help = true, nohelp = true, + hp = true, nohp = true, + hplost = true, nohplost = true, + inrange = true, noinrange = true, + isnpc = true, noisnpc = true, + isplayer = true, noisplayer = true, + member = true, nomember = true, + party = true, noparty = true, + power = true, nopower = true, + powerlost = true, nopowerlost = true, + raid = true, noraid = true, + rawhp = true, norawhp = true, + rawpower = true, norawpower = true, + type = true, notype = true, + targeting = true, notargeting = true, + exists = true, noexists = true, +} + +-- Known valid commands +local VALID_COMMANDS = { + ["/cast"] = true, + ["/castpet"] = true, + ["/cancelaura"] = true, + ["/castsequence"] = true, + ["/equip"] = true, + ["/equipmh"] = true, + ["/equipoh"] = true, + ["/focus"] = true, + ["/petattack"] = true, + ["/petfollow"] = true, + ["/petwait"] = true, + ["/petpassive"] = true, + ["/petaggressive"] = true, + ["/petdefensive"] = true, + ["/print"] = true, + ["/run"] = true, + ["/runmacro"] = true, + ["/script"] = true, + ["/startattack"] = true, + ["/stopattack"] = true, + ["/stopcasting"] = true, + ["/stopmacro"] = true, + ["/target"] = true, + ["/unqueue"] = true, + ["/use"] = true, + ["/unbuff"] = true, + ["/unshift"] = true, + ["/retarget"] = true, +} + +-- Error types +local ERROR_TYPES = { + INVALID_CONDITIONAL = "Invalid conditional", + MISMATCHED_BRACKETS = "Mismatched brackets", + EMPTY_CONDITIONAL = "Empty conditional block", + INVALID_OPERATOR = "Invalid operator", + MISSING_ARGUMENT = "Missing argument", + INVALID_COMMAND = "Unknown command", + INVALID_TARGET = "Invalid target format", + MALFORMED_QUOTES = "Malformed quotes", + EMPTY_ACTION = "Empty action", + INVALID_SYNTAX = "Invalid syntax", +} + +CleveRoids.MacroErrors = {} + +-- Check if a string has balanced brackets +local function checkBrackets(text) + local openCount = 0 + local inQuotes = false + + for i = 1, string.len(text) do + local char = string.sub(text, i, i) + + if char == '"' then + inQuotes = not inQuotes + elseif not inQuotes then + if char == "[" then + openCount = openCount + 1 + elseif char == "]" then + openCount = openCount - 1 + if openCount < 0 then + return false, "Extra closing bracket" + end + end + end + end + + if openCount > 0 then + return false, "Missing closing bracket" + elseif openCount < 0 then + return false, "Extra closing bracket" + end + + return true +end + +-- Check if quotes are balanced +local function checkQuotes(text) + local quoteCount = 0 + local escaped = false + + for i = 1, string.len(text) do + local char = string.sub(text, i, i) + + if escaped then + escaped = false + elseif char == "\\" then + escaped = true + elseif char == '"' then + quoteCount = quoteCount + 1 + end + end + + local quotient = math.floor(quoteCount / 2) + if (quoteCount - (quotient * 2)) ~= 0 then + return false, "Unmatched quotes" + end + + return true +end + +-- Validate conditional syntax +local function validateConditional(conditional, args, action) + local errors = {} + + -- Check if conditional is valid + local baseCond = string.lower(conditional) + if not VALID_CONDITIONALS[baseCond] then + table.insert(errors, { + type = ERROR_TYPES.INVALID_CONDITIONAL, + conditional = conditional, + message = "Unknown conditional: " .. conditional + }) + end + + -- Check for required arguments + local needsArgs = { + combo = true, + hp = true, myhp = true, rawhp = true, myrawhp = true, + power = true, mypower = true, rawpower = true, myrawpower = true, + hplost = true, myhplost = true, + powerlost = true, mypowerlost = true, + stat = true, + talent = true, + actionbar = true, + button = true, + form = true, stance = true, + } + + if needsArgs[baseCond] and (not args or args == "") and (not action or action == "") then + table.insert(errors, { + type = ERROR_TYPES.MISSING_ARGUMENT, + conditional = conditional, + message = conditional .. " requires an argument" + }) + end + + -- Check operator syntax for numeric comparisons + if args and string.find(baseCond, "hp") or string.find(baseCond, "power") or + string.find(baseCond, "combo") or baseCond == "stat" then + local hasOperator = string.find(args, "[<>=~]+") + if args ~= "" and not hasOperator and not string.find(args, "^%d+$") then + -- Might be missing operator + if not string.find(args, "[a-zA-Z]") then + table.insert(errors, { + type = ERROR_TYPES.INVALID_OPERATOR, + conditional = conditional, + message = conditional .. " may need an operator (>, <, =, >=, <=)" + }) + end + end + end + + return errors +end + +-- Parse and validate a single line +local function validateLine(line, lineNum) + local errors = {} + + -- Skip comments and empty lines + line = CleveRoids.Trim(line) + if line == "" or string.sub(line, 1, 2) == "--" then + return errors + end + + -- Check for valid command + local _, _, cmd = string.find(line, "^(/[a-z]+)") + if cmd then + local lowerCmd = string.lower(cmd) + if not VALID_COMMANDS[lowerCmd] then + table.insert(errors, { + type = ERROR_TYPES.INVALID_COMMAND, + line = lineNum, + command = cmd, + message = "Unknown command: " .. cmd + }) + end + end + + -- Check brackets + local bracketsOk, bracketError = checkBrackets(line) + if not bracketsOk then + table.insert(errors, { + type = ERROR_TYPES.MISMATCHED_BRACKETS, + line = lineNum, + message = bracketError + }) + end + + -- Check quotes + local quotesOk, quoteError = checkQuotes(line) + if not quotesOk then + table.insert(errors, { + type = ERROR_TYPES.MALFORMED_QUOTES, + line = lineNum, + message = quoteError + }) + end + + -- Parse conditionals if present + local _, cbEnd, conditionBlock = string.find(line, "%[(.+)%]") + if conditionBlock then + if CleveRoids.Trim(conditionBlock) == "" then + table.insert(errors, { + type = ERROR_TYPES.EMPTY_CONDITIONAL, + line = lineNum, + message = "Empty conditional block []" + }) + else + -- Check for invalid @ target syntax + local _, _, target = string.find(conditionBlock, "(@[^%s,]+)") + if target and not string.find(target, "^@[a-z]+") then + table.insert(errors, { + type = ERROR_TYPES.INVALID_TARGET, + line = lineNum, + message = "Invalid target: " .. target + }) + end + + -- Parse individual conditionals + local seenConditionals = {} + for _, condGroup in CleveRoids.splitStringIgnoringQuotes(conditionBlock, {",", " "}) do + if condGroup ~= "" and condGroup ~= target then + local parts = CleveRoids.splitStringIgnoringQuotes(condGroup, ":") + local cond = string.lower(CleveRoids.Trim(parts[1] or "")) + local args = CleveRoids.Trim(parts[2] or "") + + if cond ~= "" then + seenConditionals[cond] = true + + -- Validate the conditional + local condErrors = validateConditional(cond, args, nil) + for _, err in condErrors do + err.line = lineNum + table.insert(errors, err) + end + end + end + end + end + + -- Check for action after conditionals + local actionPart = string.sub(line, (cbEnd or 0) + 1) + local _, _, action = string.find(actionPart, "^%s*[!~]?(.+)") + if not action or CleveRoids.Trim(action) == "" then + table.insert(errors, { + type = ERROR_TYPES.EMPTY_ACTION, + line = lineNum, + message = "Conditional has no action" + }) + end + end + + return errors +end + +-- Validate an entire macro +function CleveRoids.ValidateMacro(macroName) + local errors = {} + local macroID = GetMacroIndexByName(macroName) + + if not macroID or macroID == 0 then + return {{ + type = "ERROR", + message = "Macro not found: " .. tostring(macroName) + }} + end + + local name, texture, body = GetMacroInfo(macroID) + if not body or body == "" then + return {{ + type = "ERROR", + message = "Macro is empty" + }} + end + + -- Split into lines + local lines = CleveRoids.splitString(body, "\n") + + for lineNum, line in ipairs(lines) do + local lineErrors = validateLine(line, lineNum) + for _, err in lineErrors do + table.insert(errors, err) + end + end + + return errors +end + +-- Validate all macros +function CleveRoids.ValidateAllMacros() + local results = {} + local totalErrors = 0 + + local numMacros = GetNumMacros() + for i = 1, numMacros do + local name = GetMacroInfo(i) + if name then + local errors = CleveRoids.ValidateMacro(name) + if errors and table.getn(errors) > 0 then + results[name] = errors + totalErrors = totalErrors + table.getn(errors) + end + end + end + + return results, totalErrors +end + +-- Print errors for a macro +function CleveRoids.PrintMacroErrors(macroName) + local errors = CleveRoids.ValidateMacro(macroName) + + if not errors or table.getn(errors) == 0 then + CleveRoids.Print("|cff00ff00✓|r Macro '" .. macroName .. "' has no syntax errors") + return + end + + CleveRoids.Print("|cffff0000✗|r Macro '" .. macroName .. "' has " .. table.getn(errors) .. " error(s):") + + for _, err in errors do + local line = err.line and ("Line " .. err.line .. ": ") or "" + local msg = "|cffffaa00" .. line .. "|r" .. err.message + DEFAULT_CHAT_FRAME:AddMessage(" " .. msg) + end +end + +-- Print all macro errors +function CleveRoids.PrintAllMacroErrors() + local results, totalErrors = CleveRoids.ValidateAllMacros() + + if totalErrors == 0 then + CleveRoids.Print("|cff00ff00✓|r All macros are error-free!") + return + end + + CleveRoids.Print("|cffff0000Found " .. totalErrors .. " error(s) in " .. table.getn(results) .. " macro(s):|r") + + for macroName, errors in pairs(results) do + DEFAULT_CHAT_FRAME:AddMessage(" ") + DEFAULT_CHAT_FRAME:AddMessage("|cffff8800" .. macroName .. "|r (" .. table.getn(errors) .. " error(s)):") + + for _, err in errors do + local line = err.line and ("Line " .. err.line .. ": ") or "" + local msg = " |cffffaa00" .. line .. "|r" .. err.message + DEFAULT_CHAT_FRAME:AddMessage(msg) + end + end +end + +-- Slash command +SLASH_MACROCHECK1 = "/macrocheck" +SlashCmdList.MACROCHECK = function(msg) + msg = CleveRoids.Trim(msg or "") + + if msg == "" or msg == "all" then + CleveRoids.PrintAllMacroErrors() + else + CleveRoids.PrintMacroErrors(msg) + end +end + +CleveRoids.Print("Macro syntax checker loaded. Use /macrocheck [macroname] or /macrocheck all") diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index 87ea771..f29c07f 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -12,6 +12,7 @@ Core.lua Conditionals.lua Console.lua ExtensionsManager.lua +MacroErrorChecker.lua Compatibility\SuperMacro.lua Compatibility\pfUI.lua