mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
Enhance WhitespaceUtil and Reconciler to handle duplicate names and improve diffing logic
- Updated WhitespaceUtil to return distinct values for totalDiff and whitespaceDiff when differences exceed 100 lines, based on content changes. - Introduced findDuplicateNames function in Reconciler to identify and skip instances with duplicate names, ensuring reliable syncing. - Enhanced diffing logic to account for ambiguous paths and log skipped duplicates during the sync process. - Improved error handling in syncback_dir_no_meta to log and skip children with duplicate names in the file system.
This commit is contained in:
parent
06457bc0db
commit
4a09689090
3 changed files with 191 additions and 18 deletions
|
|
@ -15,6 +15,32 @@ local function isEmpty(table)
|
|||
return next(table) == nil
|
||||
end
|
||||
|
||||
-- Finds duplicate names among children and returns a set of names that are duplicated
|
||||
local function findDuplicateNames(children: { Instance } | { string }, virtualInstances: { [string]: any }?): { [string]: boolean }
|
||||
local nameCounts: { [string]: number } = {}
|
||||
local duplicates: { [string]: boolean } = {}
|
||||
|
||||
for _, child in children do
|
||||
local name: string
|
||||
if typeof(child) == "Instance" then
|
||||
name = child.Name
|
||||
elseif virtualInstances then
|
||||
-- child is a virtual instance ID
|
||||
local virtualChild = virtualInstances[child]
|
||||
name = virtualChild and virtualChild.Name or ""
|
||||
else
|
||||
continue
|
||||
end
|
||||
|
||||
nameCounts[name] = (nameCounts[name] or 0) + 1
|
||||
if nameCounts[name] > 1 then
|
||||
duplicates[name] = true
|
||||
end
|
||||
end
|
||||
|
||||
return duplicates
|
||||
end
|
||||
|
||||
local function fuzzyEq(a: number, b: number, epsilon: number): boolean
|
||||
return math.abs(a - b) < epsilon
|
||||
end
|
||||
|
|
@ -119,9 +145,86 @@ local function diff(instanceMap, virtualInstances, rootId)
|
|||
updated = {},
|
||||
}
|
||||
|
||||
-- Count of locations with duplicate-named siblings
|
||||
local skippedDuplicateCount = 0
|
||||
|
||||
-- Pre-scan to find ALL instances that are in an ambiguous path
|
||||
-- (i.e., they or any of their ancestors have duplicate-named siblings)
|
||||
local ambiguousIds: { [string]: boolean } = {}
|
||||
|
||||
local function markSubtreeAmbiguous(id)
|
||||
if ambiguousIds[id] then
|
||||
return
|
||||
end
|
||||
ambiguousIds[id] = true
|
||||
|
||||
local virtualInstance = virtualInstances[id]
|
||||
if virtualInstance then
|
||||
for _, childId in ipairs(virtualInstance.Children) do
|
||||
markSubtreeAmbiguous(childId)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Recursively scan for duplicates and mark ambiguous subtrees
|
||||
local function scanForDuplicates(id)
|
||||
local virtualInstance = virtualInstances[id]
|
||||
if not virtualInstance then
|
||||
return
|
||||
end
|
||||
|
||||
local instance = instanceMap.fromIds[id]
|
||||
|
||||
-- Detect duplicate names among real DOM children (if instance exists)
|
||||
local realDuplicates: { [string]: boolean } = {}
|
||||
if instance then
|
||||
local realChildren = instance:GetChildren()
|
||||
realDuplicates = findDuplicateNames(realChildren)
|
||||
|
||||
-- Count locations with duplicates
|
||||
if next(realDuplicates) then
|
||||
skippedDuplicateCount += 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Detect duplicate names among virtual DOM children
|
||||
local virtualDuplicates = findDuplicateNames(virtualInstance.Children, virtualInstances)
|
||||
|
||||
-- Merge duplicates from both sides
|
||||
local allDuplicates: { [string]: boolean } = {}
|
||||
for name in realDuplicates do
|
||||
allDuplicates[name] = true
|
||||
end
|
||||
for name in virtualDuplicates do
|
||||
allDuplicates[name] = true
|
||||
end
|
||||
|
||||
-- Mark all children with duplicate names (and their entire subtrees) as ambiguous
|
||||
for _, childId in ipairs(virtualInstance.Children) do
|
||||
local virtualChild = virtualInstances[childId]
|
||||
if virtualChild and allDuplicates[virtualChild.Name] then
|
||||
markSubtreeAmbiguous(childId)
|
||||
end
|
||||
end
|
||||
|
||||
-- Recurse into non-ambiguous children to find deeper duplicates
|
||||
for _, childId in ipairs(virtualInstance.Children) do
|
||||
if not ambiguousIds[childId] then
|
||||
scanForDuplicates(childId)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- First pass: scan entire tree to find all ambiguous paths
|
||||
scanForDuplicates(rootId)
|
||||
|
||||
-- Add a virtual instance and all of its descendants to the patch, marked as
|
||||
-- being added.
|
||||
-- being added. Skip ambiguous instances.
|
||||
local function markIdAdded(id)
|
||||
if ambiguousIds[id] then
|
||||
return
|
||||
end
|
||||
|
||||
local virtualInstance = virtualInstances[id]
|
||||
patch.added[id] = virtualInstance
|
||||
|
||||
|
|
@ -132,6 +235,11 @@ local function diff(instanceMap, virtualInstances, rootId)
|
|||
|
||||
-- Internal recursive kernel for diffing an instance with the given ID.
|
||||
local function diffInternal(id)
|
||||
-- Skip entirely if this instance is in an ambiguous path
|
||||
if ambiguousIds[id] then
|
||||
return true
|
||||
end
|
||||
|
||||
local virtualInstance = virtualInstances[id]
|
||||
local instance = instanceMap.fromIds[id]
|
||||
|
||||
|
|
@ -155,6 +263,11 @@ local function diff(instanceMap, virtualInstances, rootId)
|
|||
|
||||
local changedProperties = {}
|
||||
for propertyName, virtualValue in pairs(virtualInstance.Properties) do
|
||||
-- Skip CanvasPosition on ScrollingFrame (runtime state, not meaningful for sync)
|
||||
if propertyName == "CanvasPosition" and virtualInstance.ClassName == "ScrollingFrame" then
|
||||
continue
|
||||
end
|
||||
|
||||
local getProperySuccess, existingValueOrErr = getProperty(instance, propertyName)
|
||||
|
||||
if getProperySuccess then
|
||||
|
|
@ -222,8 +335,18 @@ local function diff(instanceMap, virtualInstances, rootId)
|
|||
-- corresponding virtual instance should be removed. Any instance that
|
||||
-- does have a corresponding virtual instance is recursively diffed.
|
||||
for _, childInstance in ipairs(instance:GetChildren()) do
|
||||
-- Skip TouchInterest (auto-created by Roblox when touch events are connected)
|
||||
if childInstance.ClassName == "TouchInterest" then
|
||||
continue
|
||||
end
|
||||
|
||||
local childId = instanceMap.fromInstances[childInstance]
|
||||
|
||||
-- Skip children in ambiguous paths
|
||||
if childId and ambiguousIds[childId] then
|
||||
continue
|
||||
end
|
||||
|
||||
if childId == nil then
|
||||
-- pcall to avoid security permission errors
|
||||
local success, skip = pcall(function()
|
||||
|
|
@ -235,6 +358,14 @@ local function diff(instanceMap, virtualInstances, rootId)
|
|||
continue
|
||||
end
|
||||
|
||||
-- Check if this unmapped child has a duplicate name among siblings
|
||||
-- If so, skip it (we can't reliably determine what to do with it)
|
||||
local siblings = instance:GetChildren()
|
||||
local siblingDuplicates = findDuplicateNames(siblings)
|
||||
if siblingDuplicates[childInstance.Name] then
|
||||
continue
|
||||
end
|
||||
|
||||
-- This is an existing instance not present in the virtual DOM.
|
||||
-- We can mark it for deletion unless the user has asked us not
|
||||
-- to delete unknown stuff.
|
||||
|
|
@ -253,6 +384,11 @@ local function diff(instanceMap, virtualInstances, rootId)
|
|||
-- Traverse the list of children in the virtual DOM. Any virtual
|
||||
-- instance that has no corresponding real instance should be created.
|
||||
for _, childId in ipairs(virtualInstance.Children) do
|
||||
-- Skip children in ambiguous paths
|
||||
if ambiguousIds[childId] then
|
||||
continue
|
||||
end
|
||||
|
||||
local childInstance = instanceMap.fromIds[childId]
|
||||
|
||||
if childInstance == nil then
|
||||
|
|
@ -267,6 +403,14 @@ local function diff(instanceMap, virtualInstances, rootId)
|
|||
|
||||
local diffSuccess, err = diffInternal(rootId)
|
||||
|
||||
-- Log count of skipped duplicates
|
||||
if skippedDuplicateCount > 0 then
|
||||
Log.warn(
|
||||
"Skipped {} location(s) with duplicate-named siblings (cannot reliably sync)",
|
||||
skippedDuplicateCount
|
||||
)
|
||||
end
|
||||
|
||||
if not diffSuccess then
|
||||
return false, err
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ end
|
|||
|
||||
-- Counts lines that differ between two strings (capped at 100)
|
||||
-- Returns: totalDiff, whitespaceDiff (lines that only differ in whitespace)
|
||||
-- Returns 10000, 10000 if exceeds 100 (frontend displays as "100+")
|
||||
-- Returns 10000 for totalDiff if exceeds 100 (frontend displays as "100+")
|
||||
-- whitespaceDiff is 10000 only if ALL checked differences were whitespace-only
|
||||
function WhitespaceUtil.CountLineDifferences(current: string, incoming: string): (number, number)
|
||||
if type(current) ~= "string" or type(incoming) ~= "string" then
|
||||
return 0, 0
|
||||
|
|
@ -29,6 +30,7 @@ function WhitespaceUtil.CountLineDifferences(current: string, incoming: string):
|
|||
local maxLines = math.max(#currentLines, #incomingLines)
|
||||
|
||||
local totalDiff, whitespaceDiff = 0, 0
|
||||
local hasContentChange = false
|
||||
for i = 1, maxLines do
|
||||
local currentLine = currentLines[i] or ""
|
||||
local incomingLine = incomingLines[i] or ""
|
||||
|
|
@ -37,9 +39,13 @@ function WhitespaceUtil.CountLineDifferences(current: string, incoming: string):
|
|||
totalDiff += 1
|
||||
if NormalizeLine(currentLine) == NormalizeLine(incomingLine) then
|
||||
whitespaceDiff += 1
|
||||
else
|
||||
hasContentChange = true
|
||||
end
|
||||
if totalDiff > 100 then
|
||||
return 10000, 10000
|
||||
-- Return 10000 for both only if ALL differences were whitespace-only
|
||||
-- Otherwise return different values so isWhitespaceOnly is false
|
||||
return 10000, if hasContentChange then 0 else 10000
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue