streamling errorhandler calls

This commit is contained in:
Michael Freno
2025-12-03 22:19:27 -05:00
parent 940353c1ad
commit efce61d077
18 changed files with 333 additions and 219 deletions

View File

@@ -272,7 +272,7 @@ end
---@param callback function The callback to execute ---@param callback function The callback to execute
function flexlove.deferCallback(callback) function flexlove.deferCallback(callback)
if type(callback) ~= "function" then if type(callback) ~= "function" then
flexlove._ErrorHandler:warn("FlexLove", "deferCallback expects a function") flexlove._ErrorHandler:warn("FlexLove", "CORE_001")
return return
end end
table.insert(flexlove._deferredCallbacks, callback) table.insert(flexlove._deferredCallbacks, callback)
@@ -300,7 +300,9 @@ function flexlove.executeDeferredCallbacks()
for _, callback in ipairs(callbacks) do for _, callback in ipairs(callbacks) do
local success, err = xpcall(callback, debug.traceback) local success, err = xpcall(callback, debug.traceback)
if not success then 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 end
end end
@@ -787,7 +789,9 @@ function flexlove.setGCStrategy(strategy)
if strategy == "auto" or strategy == "periodic" or strategy == "manual" or strategy == "disabled" then if strategy == "auto" or strategy == "periodic" or strategy == "manual" or strategy == "disabled" then
flexlove._gcConfig.strategy = strategy flexlove._gcConfig.strategy = strategy
else else
flexlove._ErrorHandler:warn("FlexLove", "Invalid GC strategy: " .. tostring(strategy)) flexlove._ErrorHandler:warn("FlexLove", "CORE_003", {
strategy = tostring(strategy),
})
end end
end end

View File

@@ -542,28 +542,28 @@ Animation.__index = Animation
function Animation.new(props) function Animation.new(props)
if type(props) ~= "table" then if type(props) ~= "table" then
if Animation._ErrorHandler 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 end
props = { duration = 1, start = {}, final = {} } props = { duration = 1, start = {}, final = {} }
end end
if type(props.duration) ~= "number" or props.duration <= 0 then if type(props.duration) ~= "number" or props.duration <= 0 then
if Animation._ErrorHandler 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 end
props.duration = 1 props.duration = 1
end end
if type(props.start) ~= "table" then if type(props.start) ~= "table" then
if Animation._ErrorHandler 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 end
props.start = {} props.start = {}
end end
if type(props.final) ~= "table" then if type(props.final) ~= "table" then
if Animation._ErrorHandler 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 end
props.final = {} props.final = {}
end end
@@ -925,7 +925,7 @@ end
function Animation:apply(element) function Animation:apply(element)
if not element or type(element) ~= "table" then if not element or type(element) ~= "table" then
if Animation._ErrorHandler 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 end
return return
end end
@@ -1035,7 +1035,7 @@ function Animation:chain(nextAnimation)
return nextAnimation return nextAnimation
else else
if Animation._ErrorHandler then if Animation._ErrorHandler then
Animation._ErrorHandler:warn("Animation", "chain() requires an Animation or function.") Animation._ErrorHandler:warn("Animation", "ANIM_004")
end end
return self return self
end end
@@ -1047,7 +1047,7 @@ end
function Animation:delay(seconds) function Animation:delay(seconds)
if type(seconds) ~= "number" or seconds < 0 then if type(seconds) ~= "number" or seconds < 0 then
if Animation._ErrorHandler then if Animation._ErrorHandler then
Animation._ErrorHandler:warn("Animation", "delay() requires a non-negative number. Using 0.") Animation._ErrorHandler:warn("Animation", "ANIM_005")
end end
seconds = 0 seconds = 0
end end
@@ -1062,7 +1062,7 @@ end
function Animation:repeatCount(count) function Animation:repeatCount(count)
if type(count) ~= "number" or count < 0 then if type(count) ~= "number" or count < 0 then
if Animation._ErrorHandler then if Animation._ErrorHandler then
Animation._ErrorHandler:warn("Animation", "repeatCount() requires a non-negative number. Using 0.") Animation._ErrorHandler:warn("Animation", "ANIM_006")
end end
count = 0 count = 0
end end
@@ -1138,21 +1138,21 @@ end
function Animation.keyframes(props) function Animation.keyframes(props)
if type(props) ~= "table" then if type(props) ~= "table" then
if Animation._ErrorHandler then if Animation._ErrorHandler then
Animation._ErrorHandler:warn("Animation", "Animation.keyframes() requires a table argument. Using defaults.") Animation._ErrorHandler:warn("Animation", "ANIM_007")
end end
props = { duration = 1, keyframes = {} } props = { duration = 1, keyframes = {} }
end end
if type(props.duration) ~= "number" or props.duration <= 0 then if type(props.duration) ~= "number" or props.duration <= 0 then
if Animation._ErrorHandler then if Animation._ErrorHandler then
Animation._ErrorHandler:warn("Animation", "Keyframe duration must be positive. Using 1 second.") Animation._ErrorHandler:warn("Animation", "ANIM_002")
end end
props.duration = 1 props.duration = 1
end end
if type(props.keyframes) ~= "table" or #props.keyframes < 2 then if type(props.keyframes) ~= "table" or #props.keyframes < 2 then
if Animation._ErrorHandler 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 end
props.keyframes = { props.keyframes = {
{ at = 0, values = {} }, { at = 0, values = {} },
@@ -1224,14 +1224,14 @@ AnimationGroup.__index = AnimationGroup
function AnimationGroup.new(props) function AnimationGroup.new(props)
if type(props) ~= "table" then if type(props) ~= "table" then
if Animation._ErrorHandler then if Animation._ErrorHandler then
Animation._ErrorHandler:warn("AnimationGroup", "AnimationGroup.new() requires a table. Using defaults.") Animation._ErrorHandler:warn("AnimationGroup", "ANIM_009")
end end
props = { animations = {} } props = { animations = {} }
end end
if type(props.animations) ~= "table" or #props.animations == 0 then if type(props.animations) ~= "table" or #props.animations == 0 then
if Animation._ErrorHandler then if Animation._ErrorHandler then
Animation._ErrorHandler:warn("AnimationGroup", "AnimationGroup requires at least one animation.") Animation._ErrorHandler:warn("AnimationGroup", "ANIM_010")
end end
props.animations = {} props.animations = {}
end end
@@ -1246,7 +1246,9 @@ function AnimationGroup.new(props)
if self.mode ~= "parallel" and self.mode ~= "sequence" and self.mode ~= "stagger" then if self.mode ~= "parallel" and self.mode ~= "sequence" and self.mode ~= "stagger" then
if Animation._ErrorHandler 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 end
self.mode = "parallel" self.mode = "parallel"
end end
@@ -1512,7 +1514,7 @@ end
function AnimationGroup:apply(element) function AnimationGroup:apply(element)
if not element or type(element) ~= "table" then if not element or type(element) ~= "table" then
if Animation._ErrorHandler 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 end
return return
end end

View File

@@ -355,7 +355,9 @@ local function checkLargeBlurWarning(elementId, width, height, blurType)
local suggestion = 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." "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 end
--- Create a new blur effect instance --- Create a new blur effect instance
@@ -388,7 +390,7 @@ end
function Blur:applyToRegion(intensity, x, y, width, height, drawFunc) function Blur:applyToRegion(intensity, x, y, width, height, drawFunc)
if type(drawFunc) ~= "function" then if type(drawFunc) ~= "function" then
if Blur._ErrorHandler then if Blur._ErrorHandler then
Blur._ErrorHandler:warn("Blur", "applyToRegion requires a draw function.") Blur._ErrorHandler:warn("Blur", "BLUR_001")
end end
return return
end end
@@ -467,7 +469,7 @@ end
function Blur:applyBackdrop(intensity, x, y, width, height, backdropCanvas) function Blur:applyBackdrop(intensity, x, y, width, height, backdropCanvas)
if not backdropCanvas then if not backdropCanvas then
if Blur._ErrorHandler then if Blur._ErrorHandler then
Blur._ErrorHandler:warn("Blur", "applyBackdrop requires a backdrop canvas.") Blur._ErrorHandler:warn("Blur", "BLUR_002")
end end
return return
end end
@@ -586,7 +588,7 @@ function Blur:applyBackdropCached(intensity, x, y, width, height, backdropCanvas
-- Not cached, render and cache -- Not cached, render and cache
if not backdropCanvas then if not backdropCanvas then
if Blur._ErrorHandler then if Blur._ErrorHandler then
Blur._ErrorHandler:warn("Blur", "applyBackdrop requires a backdrop canvas.") Blur._ErrorHandler:warn("Blur", "BLUR_002")
end end
return return
end end

View File

@@ -55,7 +55,7 @@ end
function Color.fromHex(hexWithTag) function Color.fromHex(hexWithTag)
-- Validate input type -- Validate input type
if type(hexWithTag) ~= "string" then if type(hexWithTag) ~= "string" then
Color._ErrorHandler:warn("Color", "VAL_004", "Invalid color format", { Color._ErrorHandler:warn("Color", "VAL_004", {
input = tostring(hexWithTag), input = tostring(hexWithTag),
issue = "not a string", issue = "not a string",
fallback = "white (#FFFFFF)", fallback = "white (#FFFFFF)",
@@ -69,7 +69,7 @@ function Color.fromHex(hexWithTag)
local g = tonumber("0x" .. hex:sub(3, 4)) local g = tonumber("0x" .. hex:sub(3, 4))
local b = tonumber("0x" .. hex:sub(5, 6)) local b = tonumber("0x" .. hex:sub(5, 6))
if not r or not g or not b then 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, input = hexWithTag,
issue = "invalid hex digits", issue = "invalid hex digits",
fallback = "white (#FFFFFF)", fallback = "white (#FFFFFF)",
@@ -83,7 +83,7 @@ function Color.fromHex(hexWithTag)
local b = tonumber("0x" .. hex:sub(5, 6)) local b = tonumber("0x" .. hex:sub(5, 6))
local a = tonumber("0x" .. hex:sub(7, 8)) local a = tonumber("0x" .. hex:sub(7, 8))
if not r or not g or not b or not a then 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, input = hexWithTag,
issue = "invalid hex digits", issue = "invalid hex digits",
fallback = "white (#FFFFFFFF)", fallback = "white (#FFFFFFFF)",
@@ -92,7 +92,7 @@ function Color.fromHex(hexWithTag)
end end
return Color.new(r / 255, g / 255, b / 255, a / 255) return Color.new(r / 255, g / 255, b / 255, a / 255)
else else
Color._ErrorHandler:warn("Color", "VAL_004", "Invalid color format", { Color._ErrorHandler:warn("Color", "VAL_004", {
input = hexWithTag, input = hexWithTag,
expected = "#RRGGBB or #RRGGBBAA", expected = "#RRGGBB or #RRGGBBAA",
hexLength = #hex, hexLength = #hex,

View File

@@ -351,7 +351,7 @@ function Element.new(props)
-- Validate property combinations: passwordMode disables multiline -- Validate property combinations: passwordMode disables multiline
if self.passwordMode and props.multiline then 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 self.multiline = false
elseif self.passwordMode then elseif self.passwordMode then
self.multiline = false self.multiline = false
@@ -743,15 +743,16 @@ function Element.new(props)
-- Pixel units -- Pixel units
self.textSize = value self.textSize = value
else else
Element._ErrorHandler:error( Element._ErrorHandler:error("Element", "ELEM_002", {
"Element", unit = unit,
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) })
)
end end
else else
-- Validate pixel textSize value -- Validate pixel textSize value
if props.textSize <= 0 then 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 end
-- Pixel textSize value -- Pixel textSize value
@@ -3071,13 +3072,15 @@ function Element:setTransition(property, config)
end end
if type(config) ~= "table" then if type(config) ~= "table" then
Element._ErrorHandler:warn("Element", "setTransition() requires a config table. Using default config.") Element._ErrorHandler:warn("Element", "ELEM_003")
config = {} config = {}
end end
-- Validate config -- Validate config
if config.duration and (type(config.duration) ~= "number" or config.duration < 0) then 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 config.duration = 0.3
end end
@@ -3095,7 +3098,7 @@ end
---@param properties table Array of property names ---@param properties table Array of property names
function Element:setTransitionGroup(groupName, config, properties) function Element:setTransitionGroup(groupName, config, properties)
if type(properties) ~= "table" then if type(properties) ~= "table" then
Element._ErrorHandler:warn("Element", "setTransitionGroup() requires a properties array. No transitions set.") Element._ErrorHandler:warn("Element", "ELEM_005")
return return
end end

View File

@@ -313,6 +313,170 @@ local ErrorCodes = {
description = "CallSite counters accumulating", description = "CallSite counters accumulating",
suggestion = "This indicates incrementFrame() may not be called properly. Check immediate mode frame management.", 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 "" return ""
end 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 module string The module name (e.g., "Element", "Units", "Theme")
---@param level string "Error" or "Warning" ---@param level string "Error" or "Warning"
---@param codeOrMessage string Error code (e.g., "VAL_001") or message ---@param code string Error code (e.g., "VAL_001")
---@param messageOrDetails string|table|nil Message or details object ---@param details table|nil Optional details object
---@param detailsOrSuggestion table|string|nil Details or suggestion
---@param suggestionOrNil string|nil Suggestion
---@return string Formatted message ---@return string Formatted message
function ErrorHandler:_formatMessage(module, level, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestionOrNil) function ErrorHandler:_formatMessage(module, level, code, details)
local code = nil local codeInfo = ErrorCodes.get(code)
local message = codeOrMessage
local details = nil
local suggestion = nil
-- Parse arguments (support multiple signatures) if not codeInfo then
if type(codeOrMessage) == "string" and ErrorCodes.get(codeOrMessage) then return string.format("[FlexLove - %s] %s: Unknown error code: %s", module, level, code)
-- 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
end end
-- Build formatted message -- Build formatted message
local parts = {} local parts = {}
-- Header: [FlexLove - Module] Level [CODE]: Message -- Header: [FlexLove - Module] Level [CODE]: Description
if code then table.insert(parts, string.format("[FlexLove - %s] %s [%s]: %s", module, level, codeInfo.code, codeInfo.description))
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
-- Details section -- Details section
if details then if details and type(details) == "table" then
table.insert(parts, self:_formatDetails(details)) table.insert(parts, self:_formatDetails(details))
end end
-- Suggestion section -- Suggestion section
if suggestion and suggestion ~= "" then if codeInfo.suggestion and codeInfo.suggestion ~= "" then
table.insert(parts, string.format("\n\nSuggestion: %s", suggestion)) table.insert(parts, string.format("\n\nSuggestion: %s", codeInfo.suggestion))
end end
return table.concat(parts, "") return table.concat(parts, "")
@@ -807,43 +940,17 @@ end
--- Throw a critical error (stops execution) --- Throw a critical error (stops execution)
---@param module string The module name ---@param module string The module name
---@param codeOrMessage string Error code or message ---@param code string Error code (e.g., "VAL_001")
---@param messageOrDetails string|table|nil Message or details ---@param details table|nil Optional details object
---@param detailsOrSuggestion table|string|nil Details or suggestion function ErrorHandler:error(module, code, details)
---@param suggestion string|nil Suggestion local formattedMessage = self:_formatMessage(module, "Error", code, details)
function ErrorHandler:error(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
local formattedMessage = self:_formatMessage(module, "Error", codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
-- Parse arguments for logging local codeInfo = ErrorCodes.get(code)
local code = nil local message = codeInfo and codeInfo.description or code
local message = codeOrMessage local suggestion = codeInfo and codeInfo.suggestion or nil
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
-- Log the error -- 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 if self.includeStackTrace then
formattedMessage = formattedMessage .. self:_formatStackTrace(3) formattedMessage = formattedMessage .. self:_formatStackTrace(3)
@@ -854,41 +961,15 @@ end
--- Print a warning (non-critical, continues execution) --- Print a warning (non-critical, continues execution)
---@param module string The module name ---@param module string The module name
---@param codeOrMessage string Warning code or message ---@param code string Warning code (e.g., "VAL_001")
---@param messageOrDetails string|table|nil Message or details ---@param details table|nil Optional details object
---@param detailsOrSuggestion table|string|nil Details or suggestion function ErrorHandler:warn(module, code, details)
---@param suggestion string|nil Suggestion local codeInfo = ErrorCodes.get(code)
function ErrorHandler:warn(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion) local message = codeInfo and codeInfo.description or code
-- Parse arguments for logging local suggestion = codeInfo and codeInfo.suggestion or nil
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
-- Log the warning -- 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 end
--- Validate that a value is not nil --- Validate that a value is not nil

View File

@@ -633,9 +633,9 @@ function EventHandler:_invokeCallback(element, event)
self.onEvent(element, event) self.onEvent(element, event)
end) end)
else else
EventHandler._ErrorHandler:error("EventHandler", "SYS_003", "FlexLove.deferCallback not available", { EventHandler._ErrorHandler:error("EventHandler", "SYS_003", {
eventType = event.type, eventType = event.type,
}, "Ensure FlexLove module is properly loaded") })
end end
else else
self.onEvent(element, event) self.onEvent(element, event)

View File

@@ -30,7 +30,7 @@ function ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, bounds
objectPosition = objectPosition or "center center" objectPosition = objectPosition or "center center"
if imageWidth <= 0 or imageHeight <= 0 or boundsWidth <= 0 or boundsHeight <= 0 then 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, imageWidth = imageWidth,
imageHeight = imageHeight, imageHeight = imageHeight,
boundsWidth = boundsWidth, boundsWidth = boundsWidth,
@@ -116,7 +116,7 @@ function ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, bounds
return ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, "contain", objectPosition) return ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, "contain", objectPosition)
end end
else 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, fitMode = fitMode,
fallback = "fill" fallback = "fill"
}) })
@@ -362,7 +362,7 @@ function ImageRenderer.drawTiled(image, x, y, width, height, repeatMode, opacity
end end
end end
else else
ErrorHandler:warn("ImageRenderer", "VAL_007", string.format("Invalid repeat mode: '%s'. Using 'no-repeat'", tostring(repeatMode)), { ErrorHandler:warn("ImageRenderer", "VAL_007", {
repeatMode = repeatMode, repeatMode = repeatMode,
fallback = "no-repeat" fallback = "no-repeat"
}) })

View File

@@ -27,11 +27,13 @@ end
---@return love.ImageData -- Scaled image data ---@return love.ImageData -- Scaled image data
function ImageScaler.scaleNearest(sourceImageData, srcX, srcY, srcW, srcH, destW, destH) function ImageScaler.scaleNearest(sourceImageData, srcX, srcY, srcW, srcH, destW, destH)
if not sourceImageData then if not sourceImageData then
ErrorHandler:error("ImageScaler", "VAL_001", "Source ImageData cannot be nil") ErrorHandler:error("ImageScaler", "VAL_001", {
parameter = "sourceImageData"
})
end end
if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then 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, srcW = srcW,
srcH = srcH, srcH = srcH,
destW = destW, destW = destW,
@@ -95,11 +97,13 @@ end
---@return love.ImageData -- Scaled image data ---@return love.ImageData -- Scaled image data
function ImageScaler.scaleBilinear(sourceImageData, srcX, srcY, srcW, srcH, destW, destH) function ImageScaler.scaleBilinear(sourceImageData, srcX, srcY, srcW, srcH, destW, destH)
if not sourceImageData then if not sourceImageData then
ErrorHandler:error("ImageScaler", "VAL_001", "Source ImageData cannot be nil") ErrorHandler:error("ImageScaler", "VAL_001", {
parameter = "sourceImageData"
})
end end
if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then 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, srcW = srcW,
srcH = srcH, srcH = srcH,
destW = destW, destW = destW,

View File

@@ -240,18 +240,18 @@ function LayoutEngine:layoutChildren()
-- Warn if child uses percentage sizing but parent has autosizing -- Warn if child uses percentage sizing but parent has autosizing
if child.units and child.units.width then if child.units and child.units.width then
if child.units.width.unit == "%" and self.element.autosizing and self.element.autosizing.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", child = child.id or "unnamed",
issue = "percentage width with parent auto-sizing", issue = "percentage width with parent auto-sizing",
}, "Use fixed or viewport units instead of percentage when parent has auto-sizing enabled") })
end end
end end
if child.units and child.units.height then if child.units and child.units.height then
if child.units.height.unit == "%" and self.element.autosizing and self.element.autosizing.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", child = child.id or "unnamed",
issue = "percentage height with parent auto-sizing", issue = "percentage height with parent auto-sizing",
}, "Use fixed or viewport units instead of percentage when parent has auto-sizing enabled") })
end end
end end
end end

View File

@@ -134,7 +134,10 @@ function ModuleLoader.safeRequire(modulePath, isOptional)
if ModuleLoader._ErrorHandler then if ModuleLoader._ErrorHandler then
ModuleLoader._ErrorHandler:warn( ModuleLoader._ErrorHandler:warn(
"ModuleLoader", "ModuleLoader",
string.format("Optional module '%s' not found, using stub implementation", modulePath) "MOD_001",
{
modulePath = modulePath
}
) )
end end

View File

@@ -271,16 +271,13 @@ function Performance:_addWarning(name, value, level)
if now - lastWarningTime >= 60 then if now - lastWarningTime >= 60 then
if self._ErrorHandler and self._ErrorHandler.warn 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 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, metric = name,
value = string.format("%.2fms", value), value = string.format("%.2fms", value),
threshold = level == "critical" and self.criticalThresholdMs or self.warningThresholdMs, threshold = level == "critical" and self.criticalThresholdMs or self.warningThresholdMs,
}, suggestion) })
else else
local prefix = level == "critical" and "[CRITICAL]" or "[WARNING]" local prefix = level == "critical" and "[CRITICAL]" or "[WARNING]"
print(string.format("%s Performance: %s = %.2fms", prefix, name, value)) print(string.format("%s Performance: %s = %.2fms", prefix, name, value))
@@ -405,7 +402,7 @@ function Performance:logWarning(warningKey, module, message, details, suggestion
end end
if self._ErrorHandler and self._ErrorHandler.warn then 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 else
print(string.format("[FlexLove - %s] Performance Warning: %s", module, message)) print(string.format("[FlexLove - %s] Performance Warning: %s", module, message))
if suggestion then if suggestion then
@@ -529,12 +526,12 @@ function Performance:_sampleMemory()
if not self._shownWarnings[name] then if not self._shownWarnings[name] then
local message = string.format("Table '%s' growing consistently", name) local message = string.format("Table '%s' growing consistently", name)
if self._ErrorHandler and self._ErrorHandler.warn then if self._ErrorHandler and self._ErrorHandler.warn then
self._ErrorHandler:warn("Performance", "MEM_001", message, { self._ErrorHandler:warn("Performance", "MEM_001", {
table = name, table = name,
initialSize = sizes[1], initialSize = sizes[1],
currentSize = sizes[#sizes], currentSize = sizes[#sizes],
growthPercent = math.floor(((sizes[#sizes] / sizes[1]) - 1) * 100), growthPercent = math.floor(((sizes[#sizes] / sizes[1]) - 1) * 100),
}, "Check for memory leaks. Review cache eviction policies and ensure objects are released.") })
end end
self._shownWarnings[name] = true self._shownWarnings[name] = true

View File

@@ -233,11 +233,11 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten
self.cornerRadius.bottomRight self.cornerRadius.bottomRight
) )
end 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", imagePath = self.imagePath or "unknown",
cornerRadius = cornerRadiusStr, cornerRadius = cornerRadiusStr,
error = tostring(err), error = tostring(err),
}, "Ensure the active canvas has stencil=true enabled, or remove cornerRadius from images") })
-- Continue without corner radius -- Continue without corner radius
hasCornerRadius = false hasCornerRadius = false
else else
@@ -380,9 +380,9 @@ end
---@param backdropCanvas table|nil Backdrop canvas for backdrop blur ---@param backdropCanvas table|nil Backdrop canvas for backdrop blur
function Renderer:draw(element, backdropCanvas) function Renderer:draw(element, backdropCanvas)
if not element then if not element then
Renderer._ErrorHandler:warn("Renderer", "SYS_002", "Element parameter required", { Renderer._ErrorHandler:warn("Renderer", "SYS_002", {
method = "draw", method = "draw",
}, "Pass element as first parameter to draw()") })
return return
end end

View File

@@ -270,10 +270,10 @@ function StateManager.getState(id, defaultState)
if not ErrorHandler then if not ErrorHandler then
ErrorHandler = require("modules.ErrorHandler") ErrorHandler = require("modules.ErrorHandler")
end end
ErrorHandler.error("StateManager", "SYS_001", "Invalid state ID", { ErrorHandler.error("StateManager", "SYS_001", {
parameter = "id", parameter = "id",
value = "nil", value = "nil",
}, "Provide a valid non-nil ID string to getState()") })
end end
-- Create state if it doesn't exist -- Create state if it doesn't exist
@@ -306,10 +306,10 @@ function StateManager.setState(id, state)
if not ErrorHandler then if not ErrorHandler then
ErrorHandler = require("modules.ErrorHandler") ErrorHandler = require("modules.ErrorHandler")
end end
ErrorHandler.error("StateManager", "SYS_001", "Invalid state ID", { ErrorHandler.error("StateManager", "SYS_001", {
parameter = "id", parameter = "id",
value = "nil", value = "nil",
}, "Provide a valid non-nil ID string to setState()") })
end end
-- Create sparse state (remove default values) -- Create sparse state (remove default values)
@@ -529,12 +529,11 @@ function StateManager.getStats()
-- Warn if callSiteCounters is unexpectedly large -- Warn if callSiteCounters is unexpectedly large
if callSiteCount > 1000 then if callSiteCount > 1000 then
if ErrorHandler then if ErrorHandler then
local message = string.format("callSiteCounters has %d entries (expected near 0 per frame)", callSiteCount) ErrorHandler.warn("StateManager", "STATE_001", {
ErrorHandler.warn("StateManager", "STATE_001", message, {
count = callSiteCount, count = callSiteCount,
expected = "near 0", expected = "near 0",
frameNumber = frameNumber, frameNumber = frameNumber,
}, "This indicates incrementFrame() may not be called properly or counters aren't being reset. Check immediate mode frame management.") })
else else
print(string.format("[StateManager] WARNING: callSiteCounters has %d entries", callSiteCount)) print(string.format("[StateManager] WARNING: callSiteCounters has %d entries", callSiteCount))
end end

View File

@@ -370,7 +370,7 @@ local activeTheme = nil
function Theme.new(definition) function Theme.new(definition)
-- Validate input type first -- Validate input type first
if type(definition) ~= "table" then 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), error = "Theme definition must be a table, got " .. type(definition),
}) })
return Theme.new({ name = "fallback", components = {}, colors = {}, fonts = {} }) return Theme.new({ name = "fallback", components = {}, colors = {}, fonts = {} })
@@ -379,7 +379,7 @@ function Theme.new(definition)
-- Validate theme definition -- Validate theme definition
local valid, err = validateThemeDefinition(definition) local valid, err = validateThemeDefinition(definition)
if not valid then if not valid then
Theme._ErrorHandler:warn("Theme", "THM_001", "Invalid theme definition", { Theme._ErrorHandler:warn("Theme", "THM_001", {
error = tostring(err), error = tostring(err),
}) })
return Theme.new({ name = "fallback", components = {}, colors = {}, fonts = {} }) return Theme.new({ name = "fallback", components = {}, colors = {}, fonts = {} })
@@ -397,7 +397,7 @@ function Theme.new(definition)
self.atlas = image self.atlas = image
self.atlasData = imageData self.atlasData = imageData
else else
Theme._ErrorHandler:warn("Theme", "RES_001", "Failed to load global atlas", { Theme._ErrorHandler:warn("Theme", "RES_001", {
theme = definition.name, theme = definition.name,
path = resolvedPath, path = resolvedPath,
error = loaderr, error = loaderr,
@@ -425,7 +425,7 @@ function Theme.new(definition)
local contentHeight = srcHeight - 2 local contentHeight = srcHeight - 2
if contentWidth <= 0 or contentHeight <= 0 then 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, width = srcWidth,
height = srcHeight, height = srcHeight,
reason = "Image must be larger than 2x2 pixels to have content after stripping 1px border", 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.insets = parseResult.insets
comp._ninePatchData = parseResult comp._ninePatchData = parseResult
else else
Theme._ErrorHandler:warn("Theme", "RES_003", "Failed to parse nine-patch image", { Theme._ErrorHandler:warn("Theme", "RES_003", {
context = errorContext, context = errorContext,
path = resolvedPath, path = resolvedPath,
error = tostring(parseErr), error = tostring(parseErr),
@@ -481,7 +481,7 @@ function Theme.new(definition)
comp._loadedAtlasData = imageData comp._loadedAtlasData = imageData
end end
else else
Theme._ErrorHandler:warn("Theme", "RES_001", "Failed to load atlas", { Theme._ErrorHandler:warn("Theme", "RES_001", {
context = errorContext, context = errorContext,
path = resolvedPath, path = resolvedPath,
error = tostring(loaderr), error = tostring(loaderr),
@@ -575,12 +575,12 @@ function Theme.load(path)
if success then if success then
definition = result definition = result
else else
Theme._ErrorHandler:warn("Theme", "RES_004", "Failed to load theme file", { Theme._ErrorHandler:warn("Theme", "RES_004", {
theme = path, theme = path,
tried = themePath, tried = themePath,
error = tostring(result), error = tostring(result),
fallback = "nil (no theme loaded)", fallback = "nil (no theme loaded)",
}, "Check that the theme file exists in the themes/ directory or provide a valid module path") })
return nil return nil
end end
end end
@@ -607,11 +607,11 @@ function Theme.setActive(themeOrName)
end end
if not activeTheme then if not activeTheme then
Theme._ErrorHandler:warn("Theme", "THM_002", "Failed to set active theme", { Theme._ErrorHandler:warn("Theme", "THM_002", {
theme = tostring(themeOrName), theme = tostring(themeOrName),
reason = "Theme not found or not loaded", reason = "Theme not found or not loaded",
fallback = "current theme unchanged", fallback = "current theme unchanged",
}, "Ensure the theme is loaded with Theme.load() before setting it active") })
-- Keep current activeTheme unchanged (fallback behavior) -- Keep current activeTheme unchanged (fallback behavior)
end end
end end

View File

@@ -23,48 +23,48 @@ function Units.parse(value)
end end
if type(value) ~= "string" then if type(value) ~= "string" then
Units._ErrorHandler:warn("Units", "VAL_001", "Invalid property type", { Units._ErrorHandler:warn("Units", "VAL_001", {
property = "unit value", property = "unit value",
expected = "string or number", expected = "string or number",
got = type(value), got = type(value),
}, "Using fallback: 0px") })
return 0, "px" return 0, "px"
end end
-- Check for unit-only input (e.g., "px", "%", "vw" without a number) -- 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 } local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true }
if validUnits[value] then if validUnits[value] then
Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", { Units._ErrorHandler:warn("Units", "VAL_005", {
input = value, input = value,
expected = "number + unit (e.g., '50" .. 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" return 0, "px"
end end
-- Check for invalid format (space between number and unit) -- Check for invalid format (space between number and unit)
if value:match("%d%s+%a") then if value:match("%d%s+%a") then
Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", { Units._ErrorHandler:warn("Units", "VAL_005", {
input = value, input = value,
issue = "contains space between number and unit", issue = "contains space between number and unit",
}, "Remove spaces: use '50px' not '50 px'. Using fallback: 0px") })
return 0, "px" return 0, "px"
end end
-- Match number followed by optional unit -- Match number followed by optional unit
local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$") local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$")
if not numStr then if not numStr then
Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", { Units._ErrorHandler:warn("Units", "VAL_005", {
input = value, input = value,
}, "Expected format: number + unit (e.g., '50px', '10%', '2vw'). Using fallback: 0px") })
return 0, "px" return 0, "px"
end end
local num = tonumber(numStr) local num = tonumber(numStr)
if not num then if not num then
Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", { Units._ErrorHandler:warn("Units", "VAL_005", {
input = value, input = value,
issue = "numeric value cannot be parsed", issue = "numeric value cannot be parsed",
}, "Using fallback: 0px") })
return 0, "px" return 0, "px"
end end
@@ -75,11 +75,11 @@ function Units.parse(value)
-- validUnits is already defined at the top of the function -- validUnits is already defined at the top of the function
if not validUnits[unit] then if not validUnits[unit] then
Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", { Units._ErrorHandler:warn("Units", "VAL_005", {
input = value, input = value,
unit = unit, unit = unit,
validUnits = "px, %, vw, vh, ew, eh", validUnits = "px, %, vw, vh, ew, eh",
}, string.format("Treating '%s' as pixels", value)) })
return num, "px" return num, "px"
end end
@@ -99,10 +99,10 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
return value return value
elseif unit == "%" then elseif unit == "%" then
if not parentSize then if not parentSize then
Units._ErrorHandler:warn("Units", "LAY_003", "Invalid dimensions", { Units._ErrorHandler:warn("Units", "LAY_003", {
unit = "%", unit = "%",
issue = "parent dimension not available", issue = "parent dimension not available",
}, "Percentage units require a parent element with explicit dimensions. Using fallback: 0px") })
return 0 return 0
end end
return (value / 100) * parentSize return (value / 100) * parentSize
@@ -111,10 +111,10 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
elseif unit == "vh" then elseif unit == "vh" then
return (value / 100) * viewportHeight return (value / 100) * viewportHeight
else else
Units._ErrorHandler:warn("Units", "VAL_005", "Invalid unit format", { Units._ErrorHandler:warn("Units", "VAL_005", {
unit = unit, unit = unit,
validUnits = "px, %, vw, vh, ew, eh", validUnits = "px, %, vw, vh, ew, eh",
}, string.format("Unknown unit type: '%s'. Using fallback: 0px", unit)) })
return 0 return 0
end end
end end

View File

@@ -342,7 +342,11 @@ local function validateEnum(value, enumTable, propName, moduleName)
table.sort(validOptions) table.sort(validOptions)
if ErrorHandler then 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 else
error(string.format("%s must be one of: %s. Got: '%s'", propName, table.concat(validOptions, ", "), tostring(value))) error(string.format("%s must be one of: %s. Got: '%s'", propName, table.concat(validOptions, ", "), tostring(value)))
end end
@@ -361,17 +365,22 @@ local function validateRange(value, min, max, propName, moduleName)
end end
if type(value) ~= "number" then if type(value) ~= "number" then
if ErrorHandler 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 else
error(string.format("%s must be a number, got %s", propName, type(value))) error(string.format("%s must be a number, got %s", propName, type(value)))
end end
end elseif value < min or value > max then
if value < min or value > max then
if ErrorHandler then if ErrorHandler then
ErrorHandler:error( ErrorHandler:error(moduleName or "Element", "VAL_002", {
moduleName or "Element", property = propName,
string.format("%s must be between %s and %s, got %s", propName, tostring(min), tostring(max), tostring(value)) min = tostring(min),
) max = tostring(max),
value = tostring(value),
})
else else
error(string.format("%s must be between %s and %s, got %s", propName, tostring(min), tostring(max), tostring(value))) error(string.format("%s must be between %s and %s, got %s", propName, tostring(min), tostring(max), tostring(value)))
end end
@@ -392,7 +401,11 @@ local function validateType(value, expectedType, propName, moduleName)
local actualType = type(value) local actualType = type(value)
if actualType ~= expectedType then if actualType ~= expectedType then
if ErrorHandler 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 else
error(string.format("%s must be %s, got %s", propName, expectedType, actualType)) error(string.format("%s must be %s, got %s", propName, expectedType, actualType))
end end
@@ -555,6 +568,12 @@ local function sanitizeText(text, options)
-- Limit string length (use UTF-8 character count, not byte count) -- Limit string length (use UTF-8 character count, not byte count)
local charCount = utf8.len(text) local charCount = utf8.len(text)
if charCount and charCount > maxLength then 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 -- Truncate to maxLength UTF-8 characters
local bytePos = utf8.offset(text, maxLength + 1) local bytePos = utf8.offset(text, maxLength + 1)
if bytePos then if bytePos then

View File

@@ -34,7 +34,7 @@ end
function TestValidationUtils:testValidateEnum_InvalidValue() function TestValidationUtils:testValidateEnum_InvalidValue()
local testEnum = { VALUE1 = "value1", VALUE2 = "value2" } local testEnum = { VALUE1 = "value1", VALUE2 = "value2" }
luaunit.assertErrorMsgContains("must be one of", function() luaunit.assertErrorMsgContains("VAL_007", function()
utils.validateEnum("invalid", testEnum, "testProp") utils.validateEnum("invalid", testEnum, "testProp")
end) end)
end end
@@ -46,10 +46,10 @@ function TestValidationUtils:testValidateRange_InRange()
end end
function TestValidationUtils:testValidateRange_OutOfRange() function TestValidationUtils:testValidateRange_OutOfRange()
luaunit.assertErrorMsgContains("must be between", function() luaunit.assertErrorMsgContains("VAL_002", function()
utils.validateRange(-1, 0, 10, "testProp") utils.validateRange(-1, 0, 10, "testProp")
end) end)
luaunit.assertErrorMsgContains("must be between", function() luaunit.assertErrorMsgContains("VAL_002", function()
utils.validateRange(11, 0, 10, "testProp") utils.validateRange(11, 0, 10, "testProp")
end) end)
end end
@@ -59,7 +59,7 @@ function TestValidationUtils:testValidateRange_NilValue()
end end
function TestValidationUtils:testValidateRange_WrongType() function TestValidationUtils:testValidateRange_WrongType()
luaunit.assertErrorMsgContains("must be a number", function() luaunit.assertErrorMsgContains("VAL_001", function()
utils.validateRange("not a number", 0, 10, "testProp") utils.validateRange("not a number", 0, 10, "testProp")
end) end)
end end
@@ -73,10 +73,10 @@ function TestValidationUtils:testValidateType_CorrectType()
end end
function TestValidationUtils:testValidateType_WrongType() function TestValidationUtils:testValidateType_WrongType()
luaunit.assertErrorMsgContains("must be string", function() luaunit.assertErrorMsgContains("VAL_001", function()
utils.validateType(123, "string", "testProp") utils.validateType(123, "string", "testProp")
end) end)
luaunit.assertErrorMsgContains("must be number", function() luaunit.assertErrorMsgContains("VAL_001", function()
utils.validateType("hello", "number", "testProp") utils.validateType("hello", "number", "testProp")
end) end)
end end