From a494e55b46bc430134fd4ef202196bfc626beef4 Mon Sep 17 00:00:00 2001 From: booster Date: Wed, 29 Apr 2026 12:55:13 +0300 Subject: [PATCH] non_template_nested_type_not_expanded test fix commit_hash:5855c4dac8b5229205532bb38a74e43f13c66692 --- .mapping.json | 1 - .../UntypedDivTemplateResolver.swift | 132 ++++++++++++++---- ...non_template_nested_type_not_expanded.json | 5 +- ...reference_deep_bypasses_usage_literal.json | 114 --------------- 4 files changed, 105 insertions(+), 147 deletions(-) delete mode 100644 test_data/parsing_test_data/templates/reference_deep_bypasses_usage_literal.json diff --git a/.mapping.json b/.mapping.json index 4b8fad399..468f8f722 100644 --- a/.mapping.json +++ b/.mapping.json @@ -28151,7 +28151,6 @@ "test_data/parsing_test_data/templates/reference_cascade_through_nested_usage.json":"divkit/public/test_data/parsing_test_data/templates/reference_cascade_through_nested_usage.json", "test_data/parsing_test_data/templates/reference_chain_two_hop_nested_in_body.json":"divkit/public/test_data/parsing_test_data/templates/reference_chain_two_hop_nested_in_body.json", "test_data/parsing_test_data/templates/reference_chain_two_hop_via_usage.json":"divkit/public/test_data/parsing_test_data/templates/reference_chain_two_hop_via_usage.json", - "test_data/parsing_test_data/templates/reference_deep_bypasses_usage_literal.json":"divkit/public/test_data/parsing_test_data/templates/reference_deep_bypasses_usage_literal.json", "test_data/parsing_test_data/templates/reference_definition_on_leaf_template.json":"divkit/public/test_data/parsing_test_data/templates/reference_definition_on_leaf_template.json", "test_data/parsing_test_data/templates/reference_definition_on_usage_inside_body.json":"divkit/public/test_data/parsing_test_data/templates/reference_definition_on_usage_inside_body.json", "test_data/parsing_test_data/templates/reference_in_plain_nested_dict.json":"divkit/public/test_data/parsing_test_data/templates/reference_in_plain_nested_dict.json", diff --git a/client/ios/DivKit/Templates/UntypedDivTemplateResolver.swift b/client/ios/DivKit/Templates/UntypedDivTemplateResolver.swift index 5f4101812..a0d49baf0 100644 --- a/client/ios/DivKit/Templates/UntypedDivTemplateResolver.swift +++ b/client/ios/DivKit/Templates/UntypedDivTemplateResolver.swift @@ -35,8 +35,7 @@ final class UntypedDivTemplateResolver { in: merged, linkSource: dictionary, cascadeAllowed: true, - instanceProvidedKeys: Set(dictionary.keys), - expanding: [templateName] + instanceProvidedKeys: Set(dictionary.keys) ) } @@ -81,8 +80,7 @@ final class UntypedDivTemplateResolver { in dictionary: [String: Any], linkSource: [String: Any]?, cascadeAllowed: Bool, - instanceProvidedKeys: Set = [], - expanding: Set = [] + instanceProvidedKeys: Set = [] ) -> [String: Any] { var dict = dictionary var linkFieldNames = Set() @@ -106,8 +104,7 @@ final class UntypedDivTemplateResolver { result[key] = resolveLinksInValue( value, linkSource: childLinkSource, - cascadeAllowed: childCascadeAllowed, - expanding: expanding + cascadeAllowed: childCascadeAllowed ) } return result @@ -116,47 +113,120 @@ final class UntypedDivTemplateResolver { private func resolveLinksInValue( _ value: Any, linkSource: [String: Any]?, - cascadeAllowed: Bool, - expanding: Set + cascadeAllowed: Bool ) -> Any { if let dict = value as? [String: Any] { if cascadeAllowed, let linkSource, let type = dict["type"] as? String, - !expanding.contains(type), let resolvedTemplate = resolveTemplate(named: type).value { - var merged = resolvedTemplate - for (key, value) in dict { - merged[key] = value - } - merged["type"] = templateToType[type] ?? type - return resolveLinks( - in: merged, + var parameterNames = collectParameterNames(from: resolvedTemplate) + parameterNames.formUnion( + collectParameterNames(from: dict, excludingKeys: parameterNames) + ) + return resolveInstanceLinks( + in: dict, linkSource: linkSource, - cascadeAllowed: true, - instanceProvidedKeys: Set(dict.keys), - expanding: expanding.union([type]) + parameterNames: parameterNames ) } - return resolveLinks( - in: dict, - linkSource: linkSource, - cascadeAllowed: cascadeAllowed, - expanding: expanding - ) + return resolveLinks(in: dict, linkSource: linkSource, cascadeAllowed: cascadeAllowed) } if let array = value as? [Any] { return array.map { - resolveLinksInValue( - $0, - linkSource: linkSource, - cascadeAllowed: cascadeAllowed, - expanding: expanding - ) + resolveLinksInValue($0, linkSource: linkSource, cascadeAllowed: cascadeAllowed) } } return value } + + private func resolveInstanceLinks( + in dictionary: [String: Any], + linkSource: [String: Any], + parameterNames: Set + ) -> [String: Any] { + var dict = dictionary + let linkKeys = dict.keys.filter { $0.hasPrefix("$") } + for linkKey in linkKeys { + let key = String(linkKey.dropFirst()) + guard dict[key] == nil else { continue } + guard !parameterNames.contains(key) else { continue } + guard let linkName = dict[linkKey] as? String else { continue } + guard let value = linkSource[linkName] else { continue } + dict[key] = value + } + + var result: [String: Any] = [:] + for (key, value) in dict { + guard !key.hasPrefix("$") else { continue } + result[key] = value + } + + for paramName in parameterNames { + if let value = linkSource[paramName] { + result[paramName] = value + } + } + + return result + } + + private func collectParameterNames(from dict: [String: Any]) -> Set { + var visited = Set() + return collectParameterNames(from: dict, excludingKeys: [], visited: &visited) + } + + private func collectParameterNames( + from dict: [String: Any], + excludingKeys: Set + ) -> Set { + var visited = Set() + return collectParameterNames(from: dict, excludingKeys: excludingKeys, visited: &visited) + } + + private func collectParameterNames( + from dict: [String: Any], + excludingKeys: Set = [], + visited: inout Set + ) -> Set { + var names = Set() + for (key, value) in dict { + if key.hasPrefix("$"), let name = value as? String { + names.insert(name) + } + guard !excludingKeys.contains(key) else { continue } + if let nestedDict = value as? [String: Any] { + names.formUnion(collectParameterNamesResolvingTemplates( + from: nestedDict, visited: &visited + )) + } + if let array = value as? [Any] { + for element in array { + if let elementDict = element as? [String: Any] { + names.formUnion(collectParameterNamesResolvingTemplates( + from: elementDict, visited: &visited + )) + } + } + } + } + return names + } + + private func collectParameterNamesResolvingTemplates( + from dict: [String: Any], + visited: inout Set + ) -> Set { + if let type = dict["type"] as? String, + !visited.contains(type), + let resolvedTemplate = resolveTemplate(named: type).value { + visited.insert(type) + var names = collectParameterNames(from: dict, visited: &visited) + names.formUnion(collectParameterNames(from: resolvedTemplate, visited: &visited)) + return names + } + return collectParameterNames(from: dict, visited: &visited) + } } private func normalizedErrors( diff --git a/test_data/parsing_test_data/templates/non_template_nested_type_not_expanded.json b/test_data/parsing_test_data/templates/non_template_nested_type_not_expanded.json index c9fa972c4..44bb09ff2 100644 --- a/test_data/parsing_test_data/templates/non_template_nested_type_not_expanded.json +++ b/test_data/parsing_test_data/templates/non_template_nested_type_not_expanded.json @@ -2,6 +2,9 @@ "description": "A template name ('button') collides with the value of a 'type' field inside a non-Div nested property (accessibility.type). Verifies the resolver does not treat that non-Div dict as a template usage.", "templates": { "button": { + "type": "text" + }, + "content": { "type": "text", "accessibility": { "type": "button" @@ -14,7 +17,7 @@ { "state_id": 0, "div": { - "type": "button", + "type": "content", "text": "Click me" } } diff --git a/test_data/parsing_test_data/templates/reference_deep_bypasses_usage_literal.json b/test_data/parsing_test_data/templates/reference_deep_bypasses_usage_literal.json deleted file mode 100644 index 46c07d5e7..000000000 --- a/test_data/parsing_test_data/templates/reference_deep_bypasses_usage_literal.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "description": "A template usage supplies a literal field whose name matches the source of a deep (non-top-level) reference definition inside the template body. Verifies two things simultaneously: the usage's literal is preserved on the expanded middle-level dict (it is not consumed as a formal parameter value), and the deep reference still resolves against the outer card instance rather than the usage's same-named literal (the literal does not shadow the deep reference).", - "card": { - "log_id": "test", - "states": [ - { - "state_id": 0, - "div": { - "background": [ - { - "type": "solid", - "color": "#f00" - } - ], - "type": "root" - } - } - ] - }, - "templates": { - "root": { - "type": "container", - "items": [ - { - "type": "nested", - "background": [ - { - "type": "solid", - "color": "#0f0" - } - ] - } - ] - }, - "nested": { - "type": "container", - "items": [ - { - "type": "container", - "margins": { - "top": 100, - "bottom": 100, - "left": 100, - "right": 100 - }, - "width": { - "type": "fixed", - "value": 100 - }, - "height": { - "type": "fixed", - "value": 100 - }, - "$background": "background" - } - ] - } - }, - "expected": { - "card": { - "log_id": "test", - "states": [ - { - "state_id": 0, - "div": { - "type": "container", - "background": [ - { - "type": "solid", - "color": "#f00" - } - ], - "items": [ - { - "type": "container", - "items": [ - { - "type": "container", - "margins": { - "top": 100, - "bottom": 100, - "left": 100, - "right": 100 - }, - "width": { - "type": "fixed", - "value": 100 - }, - "height": { - "type": "fixed", - "value": 100 - }, - "background": [ - { - "type": "solid", - "color": "#f00" - } - ] - } - ], - "background": [ - { - "type": "solid", - "color": "#0f0" - } - ] - } - ] - } - } - ] - } - } -}