From efce61d0773cb8f1a2d1585f6871c1ae3cb5b8e3 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Wed, 3 Dec 2025 22:19:27 -0500 Subject: [PATCH] streamling errorhandler calls --- FlexLove.lua | 10 +- modules/Animation.lua | 32 ++-- modules/Blur.lua | 10 +- modules/Color.lua | 8 +- modules/Element.lua | 21 ++- modules/ErrorHandler.lua | 301 ++++++++++++++++++++----------- modules/EventHandler.lua | 4 +- modules/ImageRenderer.lua | 6 +- modules/ImageScaler.lua | 12 +- modules/LayoutEngine.lua | 8 +- modules/ModuleLoader.lua | 5 +- modules/Performance.lua | 13 +- modules/Renderer.lua | 8 +- modules/StateManager.lua | 13 +- modules/Theme.lua | 20 +- modules/Units.lua | 32 ++-- modules/utils.lua | 37 +++- testing/__tests__/utils_test.lua | 12 +- 18 files changed, 333 insertions(+), 219 deletions(-) diff --git a/FlexLove.lua b/FlexLove.lua index 9d020ff..ab1f030 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -272,7 +272,7 @@ end ---@param callback function The callback to execute function flexlove.deferCallback(callback) if type(callback) ~= "function" then - flexlove._ErrorHandler:warn("FlexLove", "deferCallback expects a function") + flexlove._ErrorHandler:warn("FlexLove", "CORE_001") return end table.insert(flexlove._deferredCallbacks, callback) @@ -300,7 +300,9 @@ function flexlove.executeDeferredCallbacks() for _, callback in ipairs(callbacks) do local success, err = xpcall(callback, debug.traceback) if not success then - flexlove._ErrorHandler:warn("FlexLove", string.format("Deferred callback failed: %s", tostring(err))) + flexlove._ErrorHandler:warn("FlexLove", "CORE_002", { + error = tostring(err), + }) end end end @@ -787,7 +789,9 @@ function flexlove.setGCStrategy(strategy) if strategy == "auto" or strategy == "periodic" or strategy == "manual" or strategy == "disabled" then flexlove._gcConfig.strategy = strategy else - flexlove._ErrorHandler:warn("FlexLove", "Invalid GC strategy: " .. tostring(strategy)) + flexlove._ErrorHandler:warn("FlexLove", "CORE_003", { + strategy = tostring(strategy), + }) end end diff --git a/modules/Animation.lua b/modules/Animation.lua index bbcd43f..2cfdac6 100644 --- a/modules/Animation.lua +++ b/modules/Animation.lua @@ -542,28 +542,28 @@ Animation.__index = Animation function Animation.new(props) if type(props) ~= "table" then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("Animation", "Animation.new() requires a table argument. Using default values.") + Animation._ErrorHandler:warn("Animation", "ANIM_001") end props = { duration = 1, start = {}, final = {} } end if type(props.duration) ~= "number" or props.duration <= 0 then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("Animation", "Animation duration must be a positive number. Using 1 second.") + Animation._ErrorHandler:warn("Animation", "ANIM_002") end props.duration = 1 end if type(props.start) ~= "table" then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("Animation", "Animation start must be a table. Using empty table.") + Animation._ErrorHandler:warn("Animation", "ANIM_001") end props.start = {} end if type(props.final) ~= "table" then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("Animation", "Animation final must be a table. Using empty table.") + Animation._ErrorHandler:warn("Animation", "ANIM_001") end props.final = {} end @@ -925,7 +925,7 @@ end function Animation:apply(element) if not element or type(element) ~= "table" then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("Animation", "Cannot apply animation to nil or non-table element.") + Animation._ErrorHandler:warn("Animation", "ANIM_003") end return end @@ -1035,7 +1035,7 @@ function Animation:chain(nextAnimation) return nextAnimation else if Animation._ErrorHandler then - Animation._ErrorHandler:warn("Animation", "chain() requires an Animation or function.") + Animation._ErrorHandler:warn("Animation", "ANIM_004") end return self end @@ -1047,7 +1047,7 @@ end function Animation:delay(seconds) if type(seconds) ~= "number" or seconds < 0 then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("Animation", "delay() requires a non-negative number. Using 0.") + Animation._ErrorHandler:warn("Animation", "ANIM_005") end seconds = 0 end @@ -1062,7 +1062,7 @@ end function Animation:repeatCount(count) if type(count) ~= "number" or count < 0 then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("Animation", "repeatCount() requires a non-negative number. Using 0.") + Animation._ErrorHandler:warn("Animation", "ANIM_006") end count = 0 end @@ -1138,21 +1138,21 @@ end function Animation.keyframes(props) if type(props) ~= "table" then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("Animation", "Animation.keyframes() requires a table argument. Using defaults.") + Animation._ErrorHandler:warn("Animation", "ANIM_007") end props = { duration = 1, keyframes = {} } end if type(props.duration) ~= "number" or props.duration <= 0 then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("Animation", "Keyframe duration must be positive. Using 1 second.") + Animation._ErrorHandler:warn("Animation", "ANIM_002") end props.duration = 1 end if type(props.keyframes) ~= "table" or #props.keyframes < 2 then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("Animation", "Keyframes require at least 2 keyframes. Using empty animation.") + Animation._ErrorHandler:warn("Animation", "ANIM_008") end props.keyframes = { { at = 0, values = {} }, @@ -1224,14 +1224,14 @@ AnimationGroup.__index = AnimationGroup function AnimationGroup.new(props) if type(props) ~= "table" then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("AnimationGroup", "AnimationGroup.new() requires a table. Using defaults.") + Animation._ErrorHandler:warn("AnimationGroup", "ANIM_009") end props = { animations = {} } end if type(props.animations) ~= "table" or #props.animations == 0 then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("AnimationGroup", "AnimationGroup requires at least one animation.") + Animation._ErrorHandler:warn("AnimationGroup", "ANIM_010") end props.animations = {} end @@ -1246,7 +1246,9 @@ function AnimationGroup.new(props) if self.mode ~= "parallel" and self.mode ~= "sequence" and self.mode ~= "stagger" then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("AnimationGroup", string.format("Invalid mode: %s. Using 'parallel'.", tostring(self.mode))) + Animation._ErrorHandler:warn("AnimationGroup", "ANIM_011", { + mode = tostring(self.mode), + }) end self.mode = "parallel" end @@ -1512,7 +1514,7 @@ end function AnimationGroup:apply(element) if not element or type(element) ~= "table" then if Animation._ErrorHandler then - Animation._ErrorHandler:warn("AnimationGroup", "Cannot apply group to nil or non-table element.") + Animation._ErrorHandler:warn("AnimationGroup", "ANIM_003") end return end diff --git a/modules/Blur.lua b/modules/Blur.lua index c675d38..71d691e 100644 --- a/modules/Blur.lua +++ b/modules/Blur.lua @@ -355,7 +355,9 @@ local function checkLargeBlurWarning(elementId, width, height, blurType) local suggestion = "Consider using retained mode for this component to avoid recreating blur effects every frame. Large blur operations are expensive and can cause performance issues in immediate mode." - Blur._ErrorHandler:warn("Blur", "PERF_003", message, suggestion) + Blur._ErrorHandler:warn("Blur", "PERF_003", { + area = string.format("%.0fx%.0f", width or 0, height or 0), + }) end --- Create a new blur effect instance @@ -388,7 +390,7 @@ end function Blur:applyToRegion(intensity, x, y, width, height, drawFunc) if type(drawFunc) ~= "function" then if Blur._ErrorHandler then - Blur._ErrorHandler:warn("Blur", "applyToRegion requires a draw function.") + Blur._ErrorHandler:warn("Blur", "BLUR_001") end return end @@ -467,7 +469,7 @@ end function Blur:applyBackdrop(intensity, x, y, width, height, backdropCanvas) if not backdropCanvas then if Blur._ErrorHandler then - Blur._ErrorHandler:warn("Blur", "applyBackdrop requires a backdrop canvas.") + Blur._ErrorHandler:warn("Blur", "BLUR_002") end return end @@ -586,7 +588,7 @@ function Blur:applyBackdropCached(intensity, x, y, width, height, backdropCanvas -- Not cached, render and cache if not backdropCanvas then if Blur._ErrorHandler then - Blur._ErrorHandler:warn("Blur", "applyBackdrop requires a backdrop canvas.") + Blur._ErrorHandler:warn("Blur", "BLUR_002") end return end diff --git a/modules/Color.lua b/modules/Color.lua index 0c393c4..7d27520 100644 --- a/modules/Color.lua +++ b/modules/Color.lua @@ -55,7 +55,7 @@ end function Color.fromHex(hexWithTag) -- Validate input type if type(hexWithTag) ~= "string" then - Color._ErrorHandler:warn("Color", "VAL_004", "Invalid color format", { + Color._ErrorHandler:warn("Color", "VAL_004", { input = tostring(hexWithTag), issue = "not a string", fallback = "white (#FFFFFF)", @@ -69,7 +69,7 @@ function Color.fromHex(hexWithTag) local g = tonumber("0x" .. hex:sub(3, 4)) local b = tonumber("0x" .. hex:sub(5, 6)) if not r or not g or not b then - Color._ErrorHandler:warn("Color", "VAL_004", "Invalid color format", { + Color._ErrorHandler:warn("Color", "VAL_004", { input = hexWithTag, issue = "invalid hex digits", fallback = "white (#FFFFFF)", @@ -83,7 +83,7 @@ function Color.fromHex(hexWithTag) local b = tonumber("0x" .. hex:sub(5, 6)) local a = tonumber("0x" .. hex:sub(7, 8)) if not r or not g or not b or not a then - Color._ErrorHandler:warn("Color", "VAL_004", "Invalid color format", { + Color._ErrorHandler:warn("Color", "VAL_004", { input = hexWithTag, issue = "invalid hex digits", fallback = "white (#FFFFFFFF)", @@ -92,7 +92,7 @@ function Color.fromHex(hexWithTag) end return Color.new(r / 255, g / 255, b / 255, a / 255) else - Color._ErrorHandler:warn("Color", "VAL_004", "Invalid color format", { + Color._ErrorHandler:warn("Color", "VAL_004", { input = hexWithTag, expected = "#RRGGBB or #RRGGBBAA", hexLength = #hex, diff --git a/modules/Element.lua b/modules/Element.lua index 5ba6789..60b58c9 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -351,7 +351,7 @@ function Element.new(props) -- Validate property combinations: passwordMode disables multiline if self.passwordMode and props.multiline then - Element._ErrorHandler:warn("Element", "passwordMode is enabled, multiline will be disabled") + Element._ErrorHandler:warn("Element", "ELEM_006") self.multiline = false elseif self.passwordMode then self.multiline = false @@ -743,15 +743,16 @@ function Element.new(props) -- Pixel units self.textSize = value else - Element._ErrorHandler:error( - "Element", - string.format("Unknown textSize unit '%s'. Valid units: px, %%, vw, vh, ew, eh. Or use presets: xs, sm, md, lg, xl, xxl, 2xl, 3xl, 4xl", unit) - ) + Element._ErrorHandler:error("Element", "ELEM_002", { + unit = unit, + }) end else -- Validate pixel textSize value if props.textSize <= 0 then - Element._ErrorHandler:error("Element", "textSize must be greater than 0, got: " .. tostring(props.textSize)) + Element._ErrorHandler:error("Element", "ELEM_001", { + value = tostring(props.textSize), + }) end -- Pixel textSize value @@ -3071,13 +3072,15 @@ function Element:setTransition(property, config) end if type(config) ~= "table" then - Element._ErrorHandler:warn("Element", "setTransition() requires a config table. Using default config.") + Element._ErrorHandler:warn("Element", "ELEM_003") config = {} end -- Validate config if config.duration and (type(config.duration) ~= "number" or config.duration < 0) then - Element._ErrorHandler:warn("Element", "transition duration must be a non-negative number. Using 0.3 seconds.") + Element._ErrorHandler:warn("Element", "ELEM_004", { + value = tostring(config.duration), + }) config.duration = 0.3 end @@ -3095,7 +3098,7 @@ end ---@param properties table Array of property names function Element:setTransitionGroup(groupName, config, properties) if type(properties) ~= "table" then - Element._ErrorHandler:warn("Element", "setTransitionGroup() requires a properties array. No transitions set.") + Element._ErrorHandler:warn("Element", "ELEM_005") return end diff --git a/modules/ErrorHandler.lua b/modules/ErrorHandler.lua index 837517f..e0ddf02 100644 --- a/modules/ErrorHandler.lua +++ b/modules/ErrorHandler.lua @@ -313,6 +313,170 @@ local ErrorCodes = { description = "CallSite counters accumulating", suggestion = "This indicates incrementFrame() may not be called properly. Check immediate mode frame management.", }, + + -- Animation Errors (ANIM_001 - ANIM_099) + ANIM_001 = { + code = "FLEXLOVE_ANIM_001", + category = "VAL", + description = "Invalid animation configuration", + suggestion = "Animation.new() requires a table argument with duration, start, and final properties", + }, + ANIM_002 = { + code = "FLEXLOVE_ANIM_002", + category = "VAL", + description = "Invalid animation duration", + suggestion = "Animation duration must be a positive number in seconds", + }, + ANIM_003 = { + code = "FLEXLOVE_ANIM_003", + category = "VAL", + description = "Invalid animation target", + suggestion = "Animation can only be applied to table elements", + }, + ANIM_004 = { + code = "FLEXLOVE_ANIM_004", + category = "VAL", + description = "Invalid animation chain", + suggestion = "chain() requires an Animation object or function", + }, + ANIM_005 = { + code = "FLEXLOVE_ANIM_005", + category = "VAL", + description = "Invalid animation delay", + suggestion = "delay() requires a non-negative number in seconds", + }, + ANIM_006 = { + code = "FLEXLOVE_ANIM_006", + category = "VAL", + description = "Invalid repeat count", + suggestion = "repeatCount() requires a non-negative number", + }, + ANIM_007 = { + code = "FLEXLOVE_ANIM_007", + category = "VAL", + description = "Invalid keyframes configuration", + suggestion = "Animation.keyframes() requires a table with duration and keyframes array", + }, + ANIM_008 = { + code = "FLEXLOVE_ANIM_008", + category = "VAL", + description = "Insufficient keyframes", + suggestion = "Keyframe animations require at least 2 keyframes", + }, + ANIM_009 = { + code = "FLEXLOVE_ANIM_009", + category = "VAL", + description = "Invalid animation group configuration", + suggestion = "AnimationGroup.new() requires a table with animations array", + }, + ANIM_010 = { + code = "FLEXLOVE_ANIM_010", + category = "VAL", + description = "Empty animation group", + suggestion = "AnimationGroup requires at least one animation", + }, + ANIM_011 = { + code = "FLEXLOVE_ANIM_011", + category = "VAL", + description = "Invalid animation group mode", + suggestion = "AnimationGroup mode must be 'parallel' or 'sequence'", + }, + + -- Blur Errors (BLUR_001 - BLUR_099) + BLUR_001 = { + code = "FLEXLOVE_BLUR_001", + category = "VAL", + description = "Missing draw function", + suggestion = "applyToRegion requires a draw function to render the content to be blurred", + }, + BLUR_002 = { + code = "FLEXLOVE_BLUR_002", + category = "VAL", + description = "Missing backdrop canvas", + suggestion = "applyBackdrop requires a backdrop canvas parameter", + }, + + -- FlexLove Core Errors (CORE_001 - CORE_099) + CORE_001 = { + code = "FLEXLOVE_CORE_001", + category = "VAL", + description = "Invalid callback function", + suggestion = "deferCallback expects a function argument", + }, + CORE_002 = { + code = "FLEXLOVE_CORE_002", + category = "SYS", + description = "Deferred callback execution failed", + suggestion = "Check the callback function for errors. Error details included in message.", + }, + CORE_003 = { + code = "FLEXLOVE_CORE_003", + category = "VAL", + description = "Invalid garbage collection strategy", + suggestion = "GC strategy must be one of: 'default', 'aggressive', 'conservative'", + }, + + -- Element Errors (ELEM_001 - ELEM_099) + ELEM_001 = { + code = "FLEXLOVE_ELEM_001", + category = "VAL", + description = "Invalid text size", + suggestion = "textSize must be greater than 0", + }, + ELEM_002 = { + code = "FLEXLOVE_ELEM_002", + category = "VAL", + description = "Invalid text size unit", + suggestion = "textSize unit must be one of: px, %, vw, vh, ew, eh, or presets: xs, sm, md, lg, xl, xxl, 2xl, 3xl, 4xl", + }, + ELEM_003 = { + code = "FLEXLOVE_ELEM_003", + category = "VAL", + description = "Invalid transition configuration", + suggestion = "setTransition() requires a table with transition properties", + }, + ELEM_004 = { + code = "FLEXLOVE_ELEM_004", + category = "VAL", + description = "Invalid transition duration", + suggestion = "Transition duration must be a non-negative number in seconds", + }, + ELEM_005 = { + code = "FLEXLOVE_ELEM_005", + category = "VAL", + description = "Invalid transition group", + suggestion = "setTransitionGroup() requires an array of property names", + }, + ELEM_006 = { + code = "FLEXLOVE_ELEM_006", + category = "VAL", + description = "Incompatible element configuration", + suggestion = "passwordMode and multiline cannot be used together. Multiline will be disabled.", + }, + + -- Module Loader Warnings (MOD_001 - MOD_099) + MOD_001 = { + code = "FLEXLOVE_MOD_001", + category = "RES", + description = "Optional module not found", + suggestion = "Using stub implementation for optional module. This is expected if the module is not required.", + }, + + -- Utility Errors (UTIL_001 - UTIL_099) + UTIL_001 = { + code = "FLEXLOVE_UTIL_001", + category = "VAL", + description = "Text truncation warning", + suggestion = "Text was truncated to fit within the maximum allowed length", + }, + + -- Image/Rendering Errors (IMG_001 - IMG_099) + IMG_001 = { + code = "FLEXLOVE_IMG_001", + category = "REN", + description = "Stencil buffer not available", + suggestion = "Cannot apply corner radius to image without stencil buffer support. Check graphics capabilities.", + }, }, } @@ -670,64 +834,33 @@ function ErrorHandler:_formatStackTrace(level) return "" end ---- Format an error or warning message with optional error code +--- Format an error or warning message using error code lookup ---@param module string The module name (e.g., "Element", "Units", "Theme") ---@param level string "Error" or "Warning" ----@param codeOrMessage string Error code (e.g., "VAL_001") or message ----@param messageOrDetails string|table|nil Message or details object ----@param detailsOrSuggestion table|string|nil Details or suggestion ----@param suggestionOrNil string|nil Suggestion +---@param code string Error code (e.g., "VAL_001") +---@param details table|nil Optional details object ---@return string Formatted message -function ErrorHandler:_formatMessage(module, level, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestionOrNil) - local code = nil - local message = codeOrMessage - local details = nil - local suggestion = nil +function ErrorHandler:_formatMessage(module, level, code, details) + local codeInfo = ErrorCodes.get(code) - -- Parse arguments (support multiple signatures) - if type(codeOrMessage) == "string" and ErrorCodes.get(codeOrMessage) then - -- Called with error code - code = codeOrMessage - message = messageOrDetails or ErrorCodes.describe(code) - - if type(detailsOrSuggestion) == "table" then - details = detailsOrSuggestion - suggestion = suggestionOrNil or ErrorCodes.getSuggestion(code) - elseif type(detailsOrSuggestion) == "string" then - suggestion = detailsOrSuggestion - else - suggestion = ErrorCodes.getSuggestion(code) - end - else - -- Called with message only (backward compatibility) - message = codeOrMessage - if type(messageOrDetails) == "table" then - details = messageOrDetails - suggestion = detailsOrSuggestion - elseif type(messageOrDetails) == "string" then - suggestion = messageOrDetails - end + if not codeInfo then + return string.format("[FlexLove - %s] %s: Unknown error code: %s", module, level, code) end -- Build formatted message local parts = {} - -- Header: [FlexLove - Module] Level [CODE]: Message - if code then - local codeInfo = ErrorCodes.get(code) - table.insert(parts, string.format("[FlexLove - %s] %s [%s]: %s", module, level, codeInfo.code, message)) - else - table.insert(parts, string.format("[FlexLove - %s] %s: %s", module, level, message)) - end + -- Header: [FlexLove - Module] Level [CODE]: Description + table.insert(parts, string.format("[FlexLove - %s] %s [%s]: %s", module, level, codeInfo.code, codeInfo.description)) -- Details section - if details then + if details and type(details) == "table" then table.insert(parts, self:_formatDetails(details)) end -- Suggestion section - if suggestion and suggestion ~= "" then - table.insert(parts, string.format("\n\nSuggestion: %s", suggestion)) + if codeInfo.suggestion and codeInfo.suggestion ~= "" then + table.insert(parts, string.format("\n\nSuggestion: %s", codeInfo.suggestion)) end return table.concat(parts, "") @@ -807,43 +940,17 @@ end --- Throw a critical error (stops execution) ---@param module string The module name ----@param codeOrMessage string Error code or message ----@param messageOrDetails string|table|nil Message or details ----@param detailsOrSuggestion table|string|nil Details or suggestion ----@param suggestion string|nil Suggestion -function ErrorHandler:error(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion) - local formattedMessage = self:_formatMessage(module, "Error", codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion) +---@param code string Error code (e.g., "VAL_001") +---@param details table|nil Optional details object +function ErrorHandler:error(module, code, details) + local formattedMessage = self:_formatMessage(module, "Error", code, details) - -- Parse arguments for logging - local code = nil - local message = codeOrMessage - local details = nil - local logSuggestion = nil - - if type(codeOrMessage) == "string" and ErrorCodes.get(codeOrMessage) then - code = codeOrMessage - message = messageOrDetails or ErrorCodes.describe(code) - - if type(detailsOrSuggestion) == "table" then - details = detailsOrSuggestion - logSuggestion = suggestion or ErrorCodes.getSuggestion(code) - elseif type(detailsOrSuggestion) == "string" then - logSuggestion = detailsOrSuggestion - else - logSuggestion = ErrorCodes.getSuggestion(code) - end - else - message = codeOrMessage - if type(messageOrDetails) == "table" then - details = messageOrDetails - logSuggestion = detailsOrSuggestion - elseif type(messageOrDetails) == "string" then - logSuggestion = messageOrDetails - end - end + local codeInfo = ErrorCodes.get(code) + local message = codeInfo and codeInfo.description or code + local suggestion = codeInfo and codeInfo.suggestion or nil -- Log the error - self:_writeLog("ERROR", LOG_LEVEL.ERROR, module, code, message, details, logSuggestion) + self:_writeLog("ERROR", LOG_LEVEL.ERROR, module, code, message, details, suggestion) if self.includeStackTrace then formattedMessage = formattedMessage .. self:_formatStackTrace(3) @@ -854,41 +961,15 @@ end --- Print a warning (non-critical, continues execution) ---@param module string The module name ----@param codeOrMessage string Warning code or message ----@param messageOrDetails string|table|nil Message or details ----@param detailsOrSuggestion table|string|nil Details or suggestion ----@param suggestion string|nil Suggestion -function ErrorHandler:warn(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion) - -- Parse arguments for logging - local code = nil - local message = codeOrMessage - local details = nil - local logSuggestion = nil - - if type(codeOrMessage) == "string" and ErrorCodes.get(codeOrMessage) then - code = codeOrMessage - message = messageOrDetails or ErrorCodes.describe(code) - - if type(detailsOrSuggestion) == "table" then - details = detailsOrSuggestion - logSuggestion = suggestion or ErrorCodes.getSuggestion(code) - elseif type(detailsOrSuggestion) == "string" then - logSuggestion = detailsOrSuggestion - else - logSuggestion = ErrorCodes.getSuggestion(code) - end - else - message = codeOrMessage - if type(messageOrDetails) == "table" then - details = messageOrDetails - logSuggestion = detailsOrSuggestion - elseif type(messageOrDetails) == "string" then - logSuggestion = messageOrDetails - end - end +---@param code string Warning code (e.g., "VAL_001") +---@param details table|nil Optional details object +function ErrorHandler:warn(module, code, details) + local codeInfo = ErrorCodes.get(code) + local message = codeInfo and codeInfo.description or code + local suggestion = codeInfo and codeInfo.suggestion or nil -- Log the warning - self:_writeLog("WARNING", LOG_LEVEL.WARNING, module, code, message, details, logSuggestion) + self:_writeLog("WARNING", LOG_LEVEL.WARNING, module, code, message, details, suggestion) end --- Validate that a value is not nil diff --git a/modules/EventHandler.lua b/modules/EventHandler.lua index dd22566..31b590a 100644 --- a/modules/EventHandler.lua +++ b/modules/EventHandler.lua @@ -633,9 +633,9 @@ function EventHandler:_invokeCallback(element, event) self.onEvent(element, event) end) else - EventHandler._ErrorHandler:error("EventHandler", "SYS_003", "FlexLove.deferCallback not available", { + EventHandler._ErrorHandler:error("EventHandler", "SYS_003", { eventType = event.type, - }, "Ensure FlexLove module is properly loaded") + }) end else self.onEvent(element, event) diff --git a/modules/ImageRenderer.lua b/modules/ImageRenderer.lua index 9b13a95..3b7cca5 100644 --- a/modules/ImageRenderer.lua +++ b/modules/ImageRenderer.lua @@ -30,7 +30,7 @@ function ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, bounds objectPosition = objectPosition or "center center" if imageWidth <= 0 or imageHeight <= 0 or boundsWidth <= 0 or boundsHeight <= 0 then - ErrorHandler:error("ImageRenderer", "VAL_002", "Dimensions must be positive", { + ErrorHandler:error("ImageRenderer", "VAL_002", { imageWidth = imageWidth, imageHeight = imageHeight, boundsWidth = boundsWidth, @@ -116,7 +116,7 @@ function ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, bounds return ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, "contain", objectPosition) end else - ErrorHandler:warn("ImageRenderer", "VAL_007", string.format("Invalid fit mode: '%s'. Must be one of: fill, contain, cover, scale-down, none", tostring(fitMode)), { + ErrorHandler:warn("ImageRenderer", "VAL_007", { fitMode = fitMode, fallback = "fill" }) @@ -362,7 +362,7 @@ function ImageRenderer.drawTiled(image, x, y, width, height, repeatMode, opacity end end else - ErrorHandler:warn("ImageRenderer", "VAL_007", string.format("Invalid repeat mode: '%s'. Using 'no-repeat'", tostring(repeatMode)), { + ErrorHandler:warn("ImageRenderer", "VAL_007", { repeatMode = repeatMode, fallback = "no-repeat" }) diff --git a/modules/ImageScaler.lua b/modules/ImageScaler.lua index a70cd47..598a374 100644 --- a/modules/ImageScaler.lua +++ b/modules/ImageScaler.lua @@ -27,11 +27,13 @@ end ---@return love.ImageData -- Scaled image data function ImageScaler.scaleNearest(sourceImageData, srcX, srcY, srcW, srcH, destW, destH) if not sourceImageData then - ErrorHandler:error("ImageScaler", "VAL_001", "Source ImageData cannot be nil") + ErrorHandler:error("ImageScaler", "VAL_001", { + parameter = "sourceImageData" + }) end if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then - ErrorHandler:warn("ImageScaler", "VAL_002", "Dimensions must be positive", { + ErrorHandler:warn("ImageScaler", "VAL_002", { srcW = srcW, srcH = srcH, destW = destW, @@ -95,11 +97,13 @@ end ---@return love.ImageData -- Scaled image data function ImageScaler.scaleBilinear(sourceImageData, srcX, srcY, srcW, srcH, destW, destH) if not sourceImageData then - ErrorHandler:error("ImageScaler", "VAL_001", "Source ImageData cannot be nil") + ErrorHandler:error("ImageScaler", "VAL_001", { + parameter = "sourceImageData" + }) end if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then - ErrorHandler:warn("ImageScaler", "VAL_002", "Dimensions must be positive", { + ErrorHandler:warn("ImageScaler", "VAL_002", { srcW = srcW, srcH = srcH, destW = destW, diff --git a/modules/LayoutEngine.lua b/modules/LayoutEngine.lua index 36b931f..2c2c352 100644 --- a/modules/LayoutEngine.lua +++ b/modules/LayoutEngine.lua @@ -240,18 +240,18 @@ function LayoutEngine:layoutChildren() -- Warn if child uses percentage sizing but parent has autosizing if child.units and child.units.width then if child.units.width.unit == "%" and self.element.autosizing and self.element.autosizing.width then - LayoutEngine._ErrorHandler:warn("LayoutEngine", "LAY_004", "Invalid sizing combination", { + LayoutEngine._ErrorHandler:warn("LayoutEngine", "LAY_004", { child = child.id or "unnamed", issue = "percentage width with parent auto-sizing", - }, "Use fixed or viewport units instead of percentage when parent has auto-sizing enabled") + }) end end if child.units and child.units.height then if child.units.height.unit == "%" and self.element.autosizing and self.element.autosizing.height then - LayoutEngine._ErrorHandler:warn("LayoutEngine", "LAY_004", "Invalid sizing combination", { + LayoutEngine._ErrorHandler:warn("LayoutEngine", "LAY_004", { child = child.id or "unnamed", issue = "percentage height with parent auto-sizing", - }, "Use fixed or viewport units instead of percentage when parent has auto-sizing enabled") + }) end end end diff --git a/modules/ModuleLoader.lua b/modules/ModuleLoader.lua index 426250c..0a16d80 100644 --- a/modules/ModuleLoader.lua +++ b/modules/ModuleLoader.lua @@ -134,7 +134,10 @@ function ModuleLoader.safeRequire(modulePath, isOptional) if ModuleLoader._ErrorHandler then ModuleLoader._ErrorHandler:warn( "ModuleLoader", - string.format("Optional module '%s' not found, using stub implementation", modulePath) + "MOD_001", + { + modulePath = modulePath + } ) end diff --git a/modules/Performance.lua b/modules/Performance.lua index 3d8aa52..07d8dd4 100644 --- a/modules/Performance.lua +++ b/modules/Performance.lua @@ -271,16 +271,13 @@ function Performance:_addWarning(name, value, level) if now - lastWarningTime >= 60 then if self._ErrorHandler and self._ErrorHandler.warn then - local message = string.format("%s = %.2fms", name, value) local code = level == "critical" and "PERF_002" or "PERF_001" - local suggestion = level == "critical" and "This operation is causing frame drops. Consider optimizing or reducing frequency." - or "This operation is taking longer than recommended. Monitor for patterns." - self._ErrorHandler:warn("Performance", code, message, { + self._ErrorHandler:warn("Performance", code, { metric = name, value = string.format("%.2fms", value), threshold = level == "critical" and self.criticalThresholdMs or self.warningThresholdMs, - }, suggestion) + }) else local prefix = level == "critical" and "[CRITICAL]" or "[WARNING]" print(string.format("%s Performance: %s = %.2fms", prefix, name, value)) @@ -405,7 +402,7 @@ function Performance:logWarning(warningKey, module, message, details, suggestion end if self._ErrorHandler and self._ErrorHandler.warn then - self._ErrorHandler:warn(module, "PERF_001", message, details or {}, suggestion) + self._ErrorHandler:warn(module, "PERF_001", details or {}) else print(string.format("[FlexLove - %s] Performance Warning: %s", module, message)) if suggestion then @@ -529,12 +526,12 @@ function Performance:_sampleMemory() if not self._shownWarnings[name] then local message = string.format("Table '%s' growing consistently", name) if self._ErrorHandler and self._ErrorHandler.warn then - self._ErrorHandler:warn("Performance", "MEM_001", message, { + self._ErrorHandler:warn("Performance", "MEM_001", { table = name, initialSize = sizes[1], currentSize = sizes[#sizes], growthPercent = math.floor(((sizes[#sizes] / sizes[1]) - 1) * 100), - }, "Check for memory leaks. Review cache eviction policies and ensure objects are released.") + }) end self._shownWarnings[name] = true diff --git a/modules/Renderer.lua b/modules/Renderer.lua index 526cbeb..4d91ccc 100644 --- a/modules/Renderer.lua +++ b/modules/Renderer.lua @@ -233,11 +233,11 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten self.cornerRadius.bottomRight ) end - Renderer._ErrorHandler:warn("Renderer", "IMG_001", "Cannot apply corner radius to image: stencil buffer not available", { + Renderer._ErrorHandler:warn("Renderer", "IMG_001", { imagePath = self.imagePath or "unknown", cornerRadius = cornerRadiusStr, error = tostring(err), - }, "Ensure the active canvas has stencil=true enabled, or remove cornerRadius from images") + }) -- Continue without corner radius hasCornerRadius = false else @@ -380,9 +380,9 @@ end ---@param backdropCanvas table|nil Backdrop canvas for backdrop blur function Renderer:draw(element, backdropCanvas) if not element then - Renderer._ErrorHandler:warn("Renderer", "SYS_002", "Element parameter required", { + Renderer._ErrorHandler:warn("Renderer", "SYS_002", { method = "draw", - }, "Pass element as first parameter to draw()") + }) return end diff --git a/modules/StateManager.lua b/modules/StateManager.lua index 00d33e9..c30ae9c 100644 --- a/modules/StateManager.lua +++ b/modules/StateManager.lua @@ -270,10 +270,10 @@ function StateManager.getState(id, defaultState) if not ErrorHandler then ErrorHandler = require("modules.ErrorHandler") end - ErrorHandler.error("StateManager", "SYS_001", "Invalid state ID", { + ErrorHandler.error("StateManager", "SYS_001", { parameter = "id", value = "nil", - }, "Provide a valid non-nil ID string to getState()") + }) end -- Create state if it doesn't exist @@ -306,10 +306,10 @@ function StateManager.setState(id, state) if not ErrorHandler then ErrorHandler = require("modules.ErrorHandler") end - ErrorHandler.error("StateManager", "SYS_001", "Invalid state ID", { + ErrorHandler.error("StateManager", "SYS_001", { parameter = "id", value = "nil", - }, "Provide a valid non-nil ID string to setState()") + }) end -- Create sparse state (remove default values) @@ -529,12 +529,11 @@ function StateManager.getStats() -- Warn if callSiteCounters is unexpectedly large if callSiteCount > 1000 then if ErrorHandler then - local message = string.format("callSiteCounters has %d entries (expected near 0 per frame)", callSiteCount) - ErrorHandler.warn("StateManager", "STATE_001", message, { + ErrorHandler.warn("StateManager", "STATE_001", { count = callSiteCount, expected = "near 0", frameNumber = frameNumber, - }, "This indicates incrementFrame() may not be called properly or counters aren't being reset. Check immediate mode frame management.") + }) else print(string.format("[StateManager] WARNING: callSiteCounters has %d entries", callSiteCount)) end diff --git a/modules/Theme.lua b/modules/Theme.lua index f65b2b3..8ad6e32 100644 --- a/modules/Theme.lua +++ b/modules/Theme.lua @@ -370,7 +370,7 @@ local activeTheme = nil function Theme.new(definition) -- Validate input type first if type(definition) ~= "table" then - Theme._ErrorHandler:warn("Theme", "THM_001", "Invalid theme definition", { + Theme._ErrorHandler:warn("Theme", "THM_001", { error = "Theme definition must be a table, got " .. type(definition), }) return Theme.new({ name = "fallback", components = {}, colors = {}, fonts = {} }) @@ -379,7 +379,7 @@ function Theme.new(definition) -- Validate theme definition local valid, err = validateThemeDefinition(definition) if not valid then - Theme._ErrorHandler:warn("Theme", "THM_001", "Invalid theme definition", { + Theme._ErrorHandler:warn("Theme", "THM_001", { error = tostring(err), }) return Theme.new({ name = "fallback", components = {}, colors = {}, fonts = {} }) @@ -397,7 +397,7 @@ function Theme.new(definition) self.atlas = image self.atlasData = imageData else - Theme._ErrorHandler:warn("Theme", "RES_001", "Failed to load global atlas", { + Theme._ErrorHandler:warn("Theme", "RES_001", { theme = definition.name, path = resolvedPath, error = loaderr, @@ -425,7 +425,7 @@ function Theme.new(definition) local contentHeight = srcHeight - 2 if contentWidth <= 0 or contentHeight <= 0 then - Theme._ErrorHandler:warn("Theme", "RES_002", "Nine-patch image too small", { + Theme._ErrorHandler:warn("Theme", "RES_002", { width = srcWidth, height = srcHeight, reason = "Image must be larger than 2x2 pixels to have content after stripping 1px border", @@ -460,7 +460,7 @@ function Theme.new(definition) comp.insets = parseResult.insets comp._ninePatchData = parseResult else - Theme._ErrorHandler:warn("Theme", "RES_003", "Failed to parse nine-patch image", { + Theme._ErrorHandler:warn("Theme", "RES_003", { context = errorContext, path = resolvedPath, error = tostring(parseErr), @@ -481,7 +481,7 @@ function Theme.new(definition) comp._loadedAtlasData = imageData end else - Theme._ErrorHandler:warn("Theme", "RES_001", "Failed to load atlas", { + Theme._ErrorHandler:warn("Theme", "RES_001", { context = errorContext, path = resolvedPath, error = tostring(loaderr), @@ -575,12 +575,12 @@ function Theme.load(path) if success then definition = result else - Theme._ErrorHandler:warn("Theme", "RES_004", "Failed to load theme file", { + Theme._ErrorHandler:warn("Theme", "RES_004", { theme = path, tried = themePath, error = tostring(result), fallback = "nil (no theme loaded)", - }, "Check that the theme file exists in the themes/ directory or provide a valid module path") + }) return nil end end @@ -607,11 +607,11 @@ function Theme.setActive(themeOrName) end if not activeTheme then - Theme._ErrorHandler:warn("Theme", "THM_002", "Failed to set active theme", { + Theme._ErrorHandler:warn("Theme", "THM_002", { theme = tostring(themeOrName), reason = "Theme not found or not loaded", fallback = "current theme unchanged", - }, "Ensure the theme is loaded with Theme.load() before setting it active") + }) -- Keep current activeTheme unchanged (fallback behavior) end end diff --git a/modules/Units.lua b/modules/Units.lua index f504e10..92c8e60 100644 --- a/modules/Units.lua +++ b/modules/Units.lua @@ -23,48 +23,48 @@ function Units.parse(value) end if type(value) ~= "string" then - Units._ErrorHandler:warn("Units", "VAL_001", "Invalid property type", { + Units._ErrorHandler:warn("Units", "VAL_001", { property = "unit value", expected = "string or number", got = type(value), - }, "Using fallback: 0px") + }) return 0, "px" end -- Check for unit-only input (e.g., "px", "%", "vw" without a number) local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true } if validUnits[value] then - Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", { + Units._ErrorHandler:warn("Units", "VAL_005", { input = value, expected = "number + unit (e.g., '50" .. value .. "')", - }, string.format("Add a numeric value before '%s', like '50%s'. Using fallback: 0px", value, value)) + }) return 0, "px" end -- Check for invalid format (space between number and unit) if value:match("%d%s+%a") then - Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", { + Units._ErrorHandler:warn("Units", "VAL_005", { input = value, issue = "contains space between number and unit", - }, "Remove spaces: use '50px' not '50 px'. Using fallback: 0px") + }) return 0, "px" end -- Match number followed by optional unit local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$") if not numStr then - Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", { + Units._ErrorHandler:warn("Units", "VAL_005", { input = value, - }, "Expected format: number + unit (e.g., '50px', '10%', '2vw'). Using fallback: 0px") + }) return 0, "px" end local num = tonumber(numStr) if not num then - Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", { + Units._ErrorHandler:warn("Units", "VAL_005", { input = value, issue = "numeric value cannot be parsed", - }, "Using fallback: 0px") + }) return 0, "px" end @@ -75,11 +75,11 @@ function Units.parse(value) -- validUnits is already defined at the top of the function if not validUnits[unit] then - Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", { + Units._ErrorHandler:warn("Units", "VAL_005", { input = value, unit = unit, validUnits = "px, %, vw, vh, ew, eh", - }, string.format("Treating '%s' as pixels", value)) + }) return num, "px" end @@ -99,10 +99,10 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize) return value elseif unit == "%" then if not parentSize then - Units._ErrorHandler:warn("Units", "LAY_003", "Invalid dimensions", { + Units._ErrorHandler:warn("Units", "LAY_003", { unit = "%", issue = "parent dimension not available", - }, "Percentage units require a parent element with explicit dimensions. Using fallback: 0px") + }) return 0 end return (value / 100) * parentSize @@ -111,10 +111,10 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize) elseif unit == "vh" then return (value / 100) * viewportHeight else - Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", { + Units._ErrorHandler:warn("Units", "VAL_005", { unit = unit, validUnits = "px, %, vw, vh, ew, eh", - }, string.format("Unknown unit type: '%s'. Using fallback: 0px", unit)) + }) return 0 end end diff --git a/modules/utils.lua b/modules/utils.lua index 33624ae..7108ae4 100644 --- a/modules/utils.lua +++ b/modules/utils.lua @@ -342,7 +342,11 @@ local function validateEnum(value, enumTable, propName, moduleName) table.sort(validOptions) if ErrorHandler then - ErrorHandler:error(moduleName or "Element", string.format("%s must be one of: %s. Got: '%s'", propName, table.concat(validOptions, ", "), tostring(value))) + ErrorHandler:error(moduleName or "Element", "VAL_007", { + property = propName, + expected = table.concat(validOptions, ", "), + got = tostring(value), + }) else error(string.format("%s must be one of: %s. Got: '%s'", propName, table.concat(validOptions, ", "), tostring(value))) end @@ -361,17 +365,22 @@ local function validateRange(value, min, max, propName, moduleName) end if type(value) ~= "number" then if ErrorHandler then - ErrorHandler:error(moduleName or "Element", string.format("%s must be a number, got %s", propName, type(value))) + ErrorHandler:error(moduleName or "Element", "VAL_001", { + property = propName, + expected = "number", + got = type(value), + }) else error(string.format("%s must be a number, got %s", propName, type(value))) end - end - if value < min or value > max then + elseif value < min or value > max then if ErrorHandler then - ErrorHandler:error( - moduleName or "Element", - string.format("%s must be between %s and %s, got %s", propName, tostring(min), tostring(max), tostring(value)) - ) + ErrorHandler:error(moduleName or "Element", "VAL_002", { + property = propName, + min = tostring(min), + max = tostring(max), + value = tostring(value), + }) else error(string.format("%s must be between %s and %s, got %s", propName, tostring(min), tostring(max), tostring(value))) end @@ -392,7 +401,11 @@ local function validateType(value, expectedType, propName, moduleName) local actualType = type(value) if actualType ~= expectedType then if ErrorHandler then - ErrorHandler:error(moduleName or "Element", string.format("%s must be %s, got %s", propName, expectedType, actualType)) + ErrorHandler:error(moduleName or "Element", "VAL_001", { + property = propName, + expected = expectedType, + got = actualType, + }) else error(string.format("%s must be %s, got %s", propName, expectedType, actualType)) end @@ -555,6 +568,12 @@ local function sanitizeText(text, options) -- Limit string length (use UTF-8 character count, not byte count) local charCount = utf8.len(text) if charCount and charCount > maxLength then + if ErrorHandler then + ErrorHandler:warn("utils", "UTIL_001", { + original = charCount, + truncated = maxLength, + }) + end -- Truncate to maxLength UTF-8 characters local bytePos = utf8.offset(text, maxLength + 1) if bytePos then diff --git a/testing/__tests__/utils_test.lua b/testing/__tests__/utils_test.lua index 66b5095..94deed3 100644 --- a/testing/__tests__/utils_test.lua +++ b/testing/__tests__/utils_test.lua @@ -34,7 +34,7 @@ end function TestValidationUtils:testValidateEnum_InvalidValue() local testEnum = { VALUE1 = "value1", VALUE2 = "value2" } - luaunit.assertErrorMsgContains("must be one of", function() + luaunit.assertErrorMsgContains("VAL_007", function() utils.validateEnum("invalid", testEnum, "testProp") end) end @@ -46,10 +46,10 @@ function TestValidationUtils:testValidateRange_InRange() end function TestValidationUtils:testValidateRange_OutOfRange() - luaunit.assertErrorMsgContains("must be between", function() + luaunit.assertErrorMsgContains("VAL_002", function() utils.validateRange(-1, 0, 10, "testProp") end) - luaunit.assertErrorMsgContains("must be between", function() + luaunit.assertErrorMsgContains("VAL_002", function() utils.validateRange(11, 0, 10, "testProp") end) end @@ -59,7 +59,7 @@ function TestValidationUtils:testValidateRange_NilValue() end function TestValidationUtils:testValidateRange_WrongType() - luaunit.assertErrorMsgContains("must be a number", function() + luaunit.assertErrorMsgContains("VAL_001", function() utils.validateRange("not a number", 0, 10, "testProp") end) end @@ -73,10 +73,10 @@ function TestValidationUtils:testValidateType_CorrectType() end function TestValidationUtils:testValidateType_WrongType() - luaunit.assertErrorMsgContains("must be string", function() + luaunit.assertErrorMsgContains("VAL_001", function() utils.validateType(123, "string", "testProp") end) - luaunit.assertErrorMsgContains("must be number", function() + luaunit.assertErrorMsgContains("VAL_001", function() utils.validateType("hello", "number", "testProp") end) end