diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index d977f1f..c9923aa 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -2,7 +2,7 @@ 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) @@ -47,7 +47,7 @@ local VALID_CONDITIONALS = { swimming = true, noswimming = true, talent = true, notalent = true, zone = true, nozone = true, - + -- Unit based alive = true, noalive = true, buff = true, nobuff = true, @@ -79,13 +79,22 @@ local VALID_COMMANDS = { ["/cast"] = true, ["/castpet"] = true, ["/cancelaura"] = true, + ["/cancelform"] = true, ["/castsequence"] = true, + ["/castrandom"] = true, + ["/changeactionbar"] = true, + ["/clearfocus"] = true, + ["/cleartarget"] = true, + ["/click"] = true, + ["/dismount"] = true, ["/equip"] = true, ["/equipmh"] = true, ["/equipoh"] = true, + ["/equipslot"] = true, ["/focus"] = true, ["/petattack"] = true, ["/petfollow"] = true, + ["/petstay"] = true, ["/petwait"] = true, ["/petpassive"] = true, ["/petaggressive"] = true, @@ -98,9 +107,19 @@ local VALID_COMMANDS = { ["/stopattack"] = true, ["/stopcasting"] = true, ["/stopmacro"] = true, + ["/stopspelltarget"] = true, + ["/swapactionbar"] = true, ["/target"] = true, + ["/targetenemy"] = true, + ["/targetfriend"] = true, + ["/targetlastenemy"] = true, + ["/targetlastfriend"] = true, + ["/targetlasttarget"] = true, + ["/targetparty"] = true, + ["/targetraid"] = true, ["/unqueue"] = true, ["/use"] = true, + ["/userandom"] = true, ["/unbuff"] = true, ["/unshift"] = true, ["/retarget"] = true, @@ -126,10 +145,10 @@ CleveRoids.MacroErrors = {} 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 @@ -143,13 +162,13 @@ local function checkBrackets(text) 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 @@ -157,10 +176,10 @@ end 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 @@ -169,19 +188,19 @@ local function checkQuotes(text) 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 @@ -191,7 +210,7 @@ local function validateConditional(conditional, args, action) message = "Unknown conditional: " .. conditional }) end - + -- Check for required arguments local needsArgs = { combo = true, @@ -205,7 +224,7 @@ local function validateConditional(conditional, args, action) 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, @@ -213,9 +232,9 @@ local function validateConditional(conditional, args, action) 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 + 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 @@ -229,20 +248,20 @@ local function validateConditional(conditional, args, action) 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 + if line == "" or string.sub(line, 1, 2) == "--" or string.sub(line, 1, 1) == "#" then return errors end - + -- Check for valid command local _, _, cmd = string.find(line, "^(/[a-z]+)") if cmd then @@ -256,7 +275,7 @@ local function validateLine(line, lineNum) }) end end - + -- Check brackets local bracketsOk, bracketError = checkBrackets(line) if not bracketsOk then @@ -266,7 +285,7 @@ local function validateLine(line, lineNum) message = bracketError }) end - + -- Check quotes local quotesOk, quoteError = checkQuotes(line) if not quotesOk then @@ -276,61 +295,94 @@ local function validateLine(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) + + -- Split by semicolons to handle multiple actions per line + local actions = CleveRoids.splitStringIgnoringQuotes(line, ";") + + for _, actionPart in ipairs(actions) do + actionPart = CleveRoids.Trim(actionPart) + if actionPart ~= "" and string.sub(actionPart, 1, 1) ~= "/" then + actionPart = "/" .. actionPart -- Add leading slash if missing after split + end + + -- Parse conditionals if present - use non-greedy match + local condStart = string.find(actionPart, "%[") + local condEnd = nil + local conditionBlock = nil + + if condStart then + -- Find matching closing bracket + local depth = 0 + local inQuotes = false + for i = condStart, string.len(actionPart) do + local char = string.sub(actionPart, i, i) + if char == '"' then + inQuotes = not inQuotes + elseif not inQuotes then + if char == "[" then + depth = depth + 1 + elseif char == "]" then + depth = depth - 1 + if depth == 0 then + condEnd = i + conditionBlock = string.sub(actionPart, condStart + 1, i - 1) + break 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" - }) + + 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]+%d*") then + table.insert(errors, { + type = ERROR_TYPES.INVALID_TARGET, + line = lineNum, + message = "Invalid target: " .. target + }) + end + + -- Parse individual conditionals + 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 + -- 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 afterCond = string.sub(actionPart, (condEnd or 0) + 1) + local _, _, action = string.find(afterCond, "^%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 end - + return errors end @@ -338,14 +390,14 @@ end 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 {{ @@ -353,17 +405,17 @@ function CleveRoids.ValidateMacro(macroName) 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 @@ -371,7 +423,7 @@ end function CleveRoids.ValidateAllMacros() local results = {} local totalErrors = 0 - + local numMacros = GetNumMacros() for i = 1, numMacros do local name = GetMacroInfo(i) @@ -383,21 +435,21 @@ function CleveRoids.ValidateAllMacros() 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 @@ -408,18 +460,18 @@ 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 @@ -432,7 +484,7 @@ end SLASH_MACROCHECK1 = "/macrocheck" SlashCmdList.MACROCHECK = function(msg) msg = CleveRoids.Trim(msg or "") - + if msg == "" or msg == "all" then CleveRoids.PrintAllMacroErrors() else