diff --git a/CHANGELOG.md b/CHANGELOG.md index 00f644f..2cde25a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). destroyed. - Error reporting should be improved with better formatting when effects invoke other effects and no more loss of stack traces. +- Nesting parent properties now work, and they are now also checked for + duplicates like other properties. ### Removed diff --git a/src/apply.luau b/src/apply.luau index 05ba98a..85020ff 100644 --- a/src/apply.luau +++ b/src/apply.luau @@ -23,6 +23,9 @@ type Cache = { Array<(Instance) -> ()> -- action callbacks >, + -- what to parent the instance to after running actions + parent: unknown, + -- cache to detect duplicate property setting at same nesting depth nested_debug: Map< number, -- depth @@ -47,6 +50,7 @@ local function borrow_cache(): Cache actions = setmetatable({} :: any, { -- lazy init __index = function(self, i) self[i] = {}; return self[i] end }), + parent = nil, nested_debug = setmetatable({} :: any, { __index = function(self, i: number) self[i] = {}; return self[i] end }), @@ -61,8 +65,6 @@ end local function process_properties(properties: Map, instance: Instance, cache: Cache, depth: number) for property, value in properties do - if property == "Parent" then continue end - if type(property) == "string" then if flags.strict then -- check for duplicate property assignment at nesting depth if cache.nested_debug[depth][property] then @@ -71,6 +73,11 @@ local function process_properties(properties: Map, instance: I cache.nested_debug[depth][property] = true end + if property == "Parent" then + cache.parent = value + continue + end + if type(value) == "function" then if typeof((instance :: any)[property]) == "RBXScriptSignal" then table.insert(cache.events, property) -- add event name to buffer @@ -106,9 +113,6 @@ local function apply(instance: T & Instance, properties: { [unknown]: unknown error "attempt to call a constructor returned by create() with no properties" end - -- queue parent assignment if any for last - local parent: unknown = properties.Parent - local caches = borrow_cache() local events = caches.events local actions = caches.actions @@ -135,6 +139,7 @@ local function apply(instance: T & Instance, properties: { [unknown]: unknown end end + local parent = caches.parent if parent then if type(parent) == "function" then implicit_effect.parent(instance, parent :: () -> Instance) @@ -145,6 +150,7 @@ local function apply(instance: T & Instance, properties: { [unknown]: unknown table.clear(events) for _, queued in next, actions do table.clear(queued) end + caches.parent = nil if flags.strict then table.clear(nested_debug) end table.clear(nested_stack) diff --git a/test/mock.luau b/test/mock.luau index 34564e6..a1f22a6 100644 --- a/test/mock.luau +++ b/test/mock.luau @@ -86,6 +86,9 @@ local Instance = {} :: any do local proxies = {} :: { [Data]: userdata? } setmetatable(proxies :: any, { __mode = "v" }) + -- allocate variables for the metamethods as __index and get_proxy cross refrences each other + local __index, __newindex + local function get_data(userdata: userdata): Data local function f(userdata: userdata): ProxyMT return getmetatable(userdata :: any) @@ -94,6 +97,19 @@ local Instance = {} :: any do return f(userdata).data end + local function get_proxy(data: Data): userdata + return proxies[data] or (function() + local userdata = newproxy(true) + local proxy = getmetatable(userdata) + proxy.proxy = userdata + proxy.data = data + proxy.__index = __index + proxy.__newindex = __newindex + proxies[data] = userdata + return userdata + end)() + end + local function is_instance(value: unknown): boolean local mt = getmetatable(value :: any) return mt and mt.data and mt.data.type == "Instance" @@ -101,16 +117,16 @@ local Instance = {} :: any do local methods = {} - local function __index(userdata: userdata, property: string): () + __index = function(userdata: userdata, property: string): () local data = get_data(userdata) return if methods[property] then methods[property] elseif property == "Name" then data.name - elseif property == "Parent" then data.parent + elseif property == "Parent" then (data.parent and get_proxy(data.parent)) elseif property == "Destroying" then data.destroying else data.properties[property] end - local function __newindex(userdata: userdata, property: string, value: unknown) + __newindex = function(userdata: userdata, property: string, value: unknown) local data = get_data(userdata) if property == "Name" then if type(value) ~= "string" then error("name must be a string", 2) end @@ -135,19 +151,6 @@ local Instance = {} :: any do end end - local function get_proxy(data: Data): userdata - return proxies[data] or (function() - local userdata = newproxy(true) - local proxy = getmetatable(userdata) - proxy.proxy = userdata - proxy.data = data - proxy.__index = __index - proxy.__newindex = __newindex - proxies[data] = userdata - return userdata - end)() - end - function Instance.new(class: string): Instance local data = { name = "UNNAMED", diff --git a/test/tests.luau b/test/tests.luau index e9365c2..14409bb 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -759,6 +759,13 @@ TEST("create()", wrap_root(function() CHECK(text.Text == "test") end + do CASE "set nested parent" + local frame = create "Frame" {} + local text = create "TextLavel" { { Parent = frame } } + CHECK(frame:GetChildren()[1] == text) + CHECK(text.Parent == frame) + end + do CASE "nested deferred" local text = create "TextLabel" { {