diff --git a/.claude/skills/cfe-validate/SKILL.md b/.claude/skills/cfe-validate/SKILL.md index 9bebc2e0..c4e013eb 100644 --- a/.claude/skills/cfe-validate/SKILL.md +++ b/.claude/skills/cfe-validate/SKILL.md @@ -28,7 +28,7 @@ powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src/Configuration.xml" ``` -## Проверки (9 шагов) +## Проверки (13 шагов) | # | Проверка | Уровень | |---|----------|---------| @@ -41,5 +41,9 @@ powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate | 7 | Файлы языков существуют | WARN | | 8 | Каталоги объектов существуют | WARN | | 9 | Заимствованные объекты: ObjectBelonging=Adopted, ExtendedConfigurationObject UUID | ERROR/WARN | +| 10 | Sub-items: Attribute, TabularSection (InternalInfo + вложенные), EnumValue, Form-ссылки | ERROR | +| 11 | Заимствованные формы: метаданные, Form.xml, Module.bsl, BaseForm (DataPath, TitleDataPath) | ERROR/WARN | +| 12 | Зависимости форм: CommonPicture, StyleItem (с whitelist платформенных), Enum DesignTimeRef | WARN | +| 13 | TypeLink: human-readable Items.* DataPath (должны быть удалены) | WARN | Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации. diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 index 76c44101..0c665f84 100644 --- a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 @@ -1,4 +1,4 @@ -# cfe-validate v1.1 — Validate 1C configuration extension structure (CFE) +# cfe-validate v1.2 — Validate 1C configuration extension structure (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -389,7 +389,7 @@ if (-not $childObjNode) { } else { $check5Ok = $true $totalCount = 0 - $typeCounts = @{} + $script:childObjectIndex = @{} $duplicates = @{} $typeFirstIndex = @{} $lastTypeOrder = -1 @@ -415,21 +415,21 @@ if (-not $childObjNode) { } } - if (-not $typeCounts.ContainsKey($typeName)) { $typeCounts[$typeName] = @{} } - if ($typeCounts[$typeName].ContainsKey($objNameVal)) { + if (-not $script:childObjectIndex.ContainsKey($typeName)) { $script:childObjectIndex[$typeName] = @{} } + if ($script:childObjectIndex[$typeName].ContainsKey($objNameVal)) { if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) { Report-Error "5. Duplicate: $typeName.$objNameVal" $duplicates["$typeName.$objNameVal"] = $true $check5Ok = $false } } else { - $typeCounts[$typeName][$objNameVal] = $true + $script:childObjectIndex[$typeName][$objNameVal] = $true } $totalCount++ } - $typeCount = $typeCounts.Count + $typeCount = $script:childObjectIndex.Count if ($check5Ok) { $orderInfo = if ($orderOk) { ", order correct" } else { "" } Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}" @@ -533,11 +533,46 @@ if ($childObjNode) { if ($script:stopped) { & $finalize; exit 1 } -# --- Check 9: Borrowed objects validation --- +# --- Check 9: Borrowed objects validation + Check 10: Sub-items --- +$script:enumValuesIndex = @{} +$script:formList = @() + +# Helper: validate a borrowed Attribute/EnumValue sub-item +function Validate-BorrowedSubItem { + param([string]$checkNum, [string]$context, [string]$subType, $subItem, $nsm) + $subProps = $subItem.SelectSingleNode("md:Properties", $nsm) + if (-not $subProps) { + Report-Error "${checkNum}. ${context}: ${subType} missing Properties" + return $false + } + $ok = $true + $subOb = $subProps.SelectSingleNode("md:ObjectBelonging", $nsm) + if (-not $subOb -or $subOb.InnerText -ne "Adopted") { + Report-Error "${checkNum}. ${context}: ${subType} ObjectBelonging must be 'Adopted'" + $ok = $false + } + $subName = $subProps.SelectSingleNode("md:Name", $nsm) + if (-not $subName -or -not $subName.InnerText) { + Report-Error "${checkNum}. ${context}: ${subType} missing Name" + $ok = $false + } + $subExt = $subProps.SelectSingleNode("md:ExtendedConfigurationObject", $nsm) + if (-not $subExt -or -not $subExt.InnerText) { + Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) missing ExtendedConfigurationObject" + $ok = $false + } elseif ($subExt.InnerText -notmatch $guidPattern) { + Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) invalid ExtendedConfigurationObject" + $ok = $false + } + return $ok +} + if ($childObjNode) { $borrowedCount = 0 $borrowedOk = 0 $check9Ok = $true + $check10Ok = $true + $subItemCount = 0 foreach ($child in $childObjNode.ChildNodes) { if ($child.NodeType -ne 'Element') { continue } @@ -577,11 +612,11 @@ if ($childObjNode) { $objProps = $objEl.SelectSingleNode("md:Properties", $objNs) if (-not $objProps) { continue } + # --- Check 9: ObjectBelonging + ExtendedConfigurationObject --- $obNode = $objProps.SelectSingleNode("md:ObjectBelonging", $objNs) if ($obNode -and $obNode.InnerText -eq "Adopted") { $borrowedCount++ - # Check ExtendedConfigurationObject $extObj = $objProps.SelectSingleNode("md:ExtendedConfigurationObject", $objNs) if (-not $extObj -or -not $extObj.InnerText) { Report-Error "9. Borrowed ${typeName}.${childName}: missing ExtendedConfigurationObject" @@ -594,6 +629,90 @@ if ($childObjNode) { } } + # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- + $objChildObjects = $objEl.SelectSingleNode("md:ChildObjects", $objNs) + if ($objChildObjects) { + $ctx = "${typeName}.${childName}" + foreach ($subItem in $objChildObjects.ChildNodes) { + if ($subItem.NodeType -ne 'Element') { continue } + $subType = $subItem.LocalName + + if ($subType -eq "Attribute") { + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" $ctx "Attribute" $subItem $objNs)) { + $check10Ok = $false + } + } + elseif ($subType -eq "TabularSection") { + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" $ctx "TabularSection" $subItem $objNs)) { + $check10Ok = $false + } else { + # Check InternalInfo GeneratedTypes + $tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs) + $tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) + $tsLabel = if ($tsName) { $tsName.InnerText } else { "?" } + if (-not $tsInfo) { + Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo" + $check10Ok = $false + } else { + $gtNodes = $tsInfo.SelectNodes("xr:GeneratedType", $objNs) + $hasTSCat = $false; $hasTSRCat = $false + foreach ($gt in $gtNodes) { + $cat = $gt.GetAttribute("category") + if ($cat -eq "TabularSection") { $hasTSCat = $true } + if ($cat -eq "TabularSectionRow") { $hasTSRCat = $true } + } + if (-not $hasTSCat -or -not $hasTSRCat) { + Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing GeneratedType (need TabularSection + TabularSectionRow)" + $check10Ok = $false + } + } + # Recurse into TS ChildObjects/Attribute + $tsChildObjs = $subItem.SelectSingleNode("md:ChildObjects", $objNs) + if ($tsChildObjs) { + foreach ($tsAttr in $tsChildObjs.ChildNodes) { + if ($tsAttr.NodeType -ne 'Element' -or $tsAttr.LocalName -ne "Attribute") { continue } + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" "${ctx}.ТЧ.${tsLabel}" "Attribute" $tsAttr $objNs)) { + $check10Ok = $false + } + } + } + } + } + elseif ($subType -eq "EnumValue" -and $typeName -eq "Enum") { + $subItemCount++ + if (Validate-BorrowedSubItem "10" $ctx "EnumValue" $subItem $objNs) { + $evName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) + if ($evName -and $evName.InnerText) { + if (-not $script:enumValuesIndex.ContainsKey($childName)) { + $script:enumValuesIndex[$childName] = @{} + } + $script:enumValuesIndex[$childName][$evName.InnerText] = $true + } + } else { + $check10Ok = $false + } + } + elseif ($subType -eq "Form") { + $formName = $subItem.InnerText + if ($formName) { + $formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $configDir $dirName) $childName) "Forms") "${formName}.xml" + if (-not (Test-Path $formMetaFile)) { + Report-Error "10. ${ctx}: Form.${formName} metadata file missing" + $check10Ok = $false + } + $script:formList += @{ + TypeName = $typeName; ObjName = $childName + FormName = $formName; DirName = $dirName + } + $subItemCount++ + } + } + } + } + if ($script:stopped) { break } } @@ -602,6 +721,208 @@ if ($childObjNode) { } elseif ($check9Ok) { Report-OK "9. Borrowed objects: $borrowedOk/$borrowedCount validated" } + + if ($subItemCount -eq 0) { + Report-OK "10. Sub-items: none found" + } elseif ($check10Ok) { + Report-OK "10. Sub-items: $subItemCount validated (Attributes, TabularSections, EnumValues, Forms)" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 11: Borrowed form structure --- +$script:borrowedFormsWithTree = @() +$check11Ok = $true +$formCount = 0 + +foreach ($fi in $script:formList) { + $formCount++ + $formBase = Join-Path (Join-Path (Join-Path (Join-Path $configDir $fi.DirName) $fi.ObjName) "Forms") $fi.FormName + $formMetaFile = Join-Path (Split-Path $formBase -Parent) "$($fi.FormName).xml" + $formXmlFile = Join-Path (Join-Path $formBase "Ext") "Form.xml" + $moduleBslFile = Join-Path (Join-Path (Join-Path $formBase "Ext") "Form") "Module.bsl" + $ctx = "$($fi.TypeName).$($fi.ObjName).Form.$($fi.FormName)" + + # Validate form metadata XML + if (Test-Path $formMetaFile) { + try { + $fmDoc = New-Object System.Xml.XmlDocument + $fmDoc.PreserveWhitespace = $false + $fmDoc.Load($formMetaFile) + $fmNs = New-Object System.Xml.XmlNamespaceManager($fmDoc.NameTable) + $fmNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $fmEl = $null + foreach ($c in $fmDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $fmEl = $c; break } + } + if ($fmEl) { + $fmProps = $fmEl.SelectSingleNode("md:Properties", $fmNs) + if ($fmProps) { + $fmOb = $fmProps.SelectSingleNode("md:ObjectBelonging", $fmNs) + $isBorrowed = $fmOb -and $fmOb.InnerText -eq "Adopted" + if ($isBorrowed) { + $fmExt = $fmProps.SelectSingleNode("md:ExtendedConfigurationObject", $fmNs) + if (-not $fmExt -or $fmExt.InnerText -notmatch $guidPattern) { + Report-Error "11. ${ctx}: invalid/missing ExtendedConfigurationObject" + $check11Ok = $false + } + } + $fmType = $fmProps.SelectSingleNode("md:FormType", $fmNs) + if ($fmType -and $fmType.InnerText -ne "Managed") { + Report-Error "11. ${ctx}: FormType must be 'Managed', got '$($fmType.InnerText)'" + $check11Ok = $false + } + } + } + } catch { + Report-Warn "11. ${ctx}: Cannot parse metadata: $($_.Exception.Message)" + } + } + + # Form.xml must exist + if (-not (Test-Path $formXmlFile)) { + Report-Error "11. ${ctx}: Ext/Form.xml missing" + $check11Ok = $false + continue + } + + # Module.bsl should exist + if (-not (Test-Path $moduleBslFile)) { + Report-Warn "11. ${ctx}: Ext/Form/Module.bsl missing" + } + + # Read Form.xml as raw text for BaseForm checks + $formRawText = [System.IO.File]::ReadAllText($formXmlFile, [System.Text.Encoding]::UTF8) + + if ($formRawText -match ']+version=') { + Report-Warn "11. ${ctx}: missing version attribute" + } + # Extract BaseForm content for DataPath/TitleDataPath checks (only inside BaseForm, not in extension elements) + $bfMatch = [regex]::Match($formRawText, '(?s)]*>(.+)') + if ($bfMatch.Success) { + $bfContent = $bfMatch.Groups[1].Value + if ($bfContent -match '[^<]+') { + Report-Warn "11. ${ctx}: found in BaseForm (should be stripped for borrowed forms)" + } + if ($bfContent -match '[^<]+') { + Report-Warn "11. ${ctx}: found in BaseForm (should be stripped)" + } + } + + $script:borrowedFormsWithTree += @{ + Path = $formXmlFile; RawText = $formRawText; Context = $ctx + } + } +} + +if ($formCount -eq 0) { + Report-OK "11. Borrowed forms: none found" +} elseif ($check11Ok) { + $bfCount = $script:borrowedFormsWithTree.Count + Report-OK "11. Borrowed forms: $formCount validated ($bfCount with BaseForm)" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 12: Form dependency references --- +$platformStyleItems = @{ + "TableHeaderBackColor"=$true; "AccentColor"=$true; "NormalTextFont"=$true + "FormBackColor"=$true; "ToolTipBackColor"=$true; "BorderColor"=$true + "FieldBackColor"=$true; "FieldTextColor"=$true; "ButtonBackColor"=$true + "ButtonTextColor"=$true; "AlternateRowColor"=$true; "SpecialTextColor"=$true + "TextFont"=$true; "ImportantColor"=$true; "FormTextColor"=$true + "SmallTextFont"=$true; "ExtraLargeTextFont"=$true; "LargeTextFont"=$true + "NormalTextColor"=$true; "GroupHeaderBackColor"=$true; "GroupHeaderFont"=$true + "ErrorColor"=$true; "SuccessColor"=$true; "WarningColor"=$true +} +$check12Ok = $true +$depCheckCount = 0 + +foreach ($bf in $script:borrowedFormsWithTree) { + $raw = $bf.RawText + $ctx = $bf.Context + $missingItems = @() + + # CommonPicture references + $cpRefs = @{} + foreach ($m in [regex]::Matches($raw, 'CommonPicture\.(\w+)')) { + $cpRefs[$m.Groups[1].Value] = $true + } + $cpIndex = $script:childObjectIndex["CommonPicture"] + foreach ($cpName in $cpRefs.Keys) { + $depCheckCount++ + if (-not $cpIndex -or -not $cpIndex.ContainsKey($cpName)) { + $missingItems += "CommonPicture.${cpName}" + } + } + + # StyleItem references + $siRefs = @{} + foreach ($m in [regex]::Matches($raw, 'style:([A-Za-z\u0410-\u044F\u0401\u0451_][A-Za-z0-9\u0410-\u044F\u0401\u0451_]*)')) { + $siRefs[$m.Groups[1].Value] = $true + } + $siIndex = $script:childObjectIndex["StyleItem"] + foreach ($siName in $siRefs.Keys) { + $depCheckCount++ + if ($platformStyleItems.ContainsKey($siName)) { continue } + if (-not $siIndex -or -not $siIndex.ContainsKey($siName)) { + $missingItems += "StyleItem.${siName}" + } + } + + # Enum DesignTimeRef references + $enumRefs = @{} + foreach ($m in [regex]::Matches($raw, 'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)')) { + $eKey = "$($m.Groups[1].Value).$($m.Groups[2].Value)" + $enumRefs[$eKey] = @{ Enum = $m.Groups[1].Value; Value = $m.Groups[2].Value } + } + $eIndex = $script:childObjectIndex["Enum"] + foreach ($entry in $enumRefs.Values) { + $depCheckCount++ + if (-not $eIndex -or -not $eIndex.ContainsKey($entry.Enum)) { + $missingItems += "Enum.$($entry.Enum)" + } elseif (-not $script:enumValuesIndex.ContainsKey($entry.Enum) -or -not $script:enumValuesIndex[$entry.Enum].ContainsKey($entry.Value)) { + $missingItems += "Enum.$($entry.Enum).EnumValue.$($entry.Value)" + } + } + + foreach ($mi in $missingItems) { + Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension" + $check12Ok = $false + } +} + +if ($script:borrowedFormsWithTree.Count -eq 0) { + Report-OK "12. Form dependencies: no borrowed forms with tree" +} elseif ($check12Ok) { + Report-OK "12. Form dependencies: $depCheckCount references checked" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 13: TypeLink with human-readable paths --- +$check13Ok = $true +$typeLinkCount = 0 + +foreach ($bf in $script:borrowedFormsWithTree) { + $raw = $bf.RawText + $ctx = $bf.Context + $matches = [regex]::Matches($raw, '\s*Items\.[^<]*') + if ($matches.Count -gt 0) { + $typeLinkCount += $matches.Count + Report-Warn "13. ${ctx}: $($matches.Count) TypeLink(s) with human-readable Items.* DataPath (should be stripped)" + $check13Ok = $false + } +} + +if ($script:borrowedFormsWithTree.Count -eq 0) { + Report-OK "13. TypeLink: no borrowed forms with tree" +} elseif ($check13Ok) { + Report-OK "13. TypeLink: clean" } # --- Final output --- diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.py b/.claude/skills/cfe-validate/scripts/cfe-validate.py index 40d24db0..4b85a9b8 100644 --- a/.claude/skills/cfe-validate/scripts/cfe-validate.py +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cfe-validate v1.1 — Validate 1C configuration extension XML structure (CFE) +# cfe-validate v1.2 — Validate 1C configuration extension XML structure (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects.""" import sys, os, argparse, re @@ -392,7 +392,7 @@ def main(): else: check5_ok = True total_count = 0 - type_counts = {} + child_object_index = {} duplicates = {} type_first_index = {} last_type_order = -1 @@ -420,20 +420,20 @@ def main(): order_ok = False last_type_order = type_idx - if type_name not in type_counts: - type_counts[type_name] = {} - if obj_name_val in type_counts[type_name]: + if type_name not in child_object_index: + child_object_index[type_name] = {} + if obj_name_val in child_object_index[type_name]: dup_key = f'{type_name}.{obj_name_val}' if dup_key not in duplicates: r.error(f'5. Duplicate: {dup_key}') duplicates[dup_key] = True check5_ok = False else: - type_counts[type_name][obj_name_val] = True + child_object_index[type_name][obj_name_val] = True total_count += 1 - type_count = len(type_counts) + type_count = len(child_object_index) if check5_ok: order_info = ', order correct' if order_ok else '' r.ok(f'5. ChildObjects: {type_count} types, {total_count} objects{order_info}') @@ -529,11 +529,43 @@ def main(): r.finalize(out_file) sys.exit(1) - # --- Check 9: Borrowed objects validation --- + # --- Check 9: Borrowed objects + Check 10: Sub-items --- + MD = NS['md'] + XR = NS['xr'] + enum_values_index = {} + form_list = [] + + def validate_borrowed_sub_item(check_num, context, sub_type, sub_item): + """Validate a borrowed Attribute/EnumValue/TabularSection sub-item.""" + sub_props = sub_item.find(f'{{{MD}}}Properties') + if sub_props is None: + r.error(f'{check_num}. {context}: {sub_type} missing Properties') + return False + ok = True + sub_ob = sub_props.find(f'{{{MD}}}ObjectBelonging') + if sub_ob is None or (sub_ob.text or '') != 'Adopted': + r.error(f"{check_num}. {context}: {sub_type} ObjectBelonging must be 'Adopted'") + ok = False + sub_name = sub_props.find(f'{{{MD}}}Name') + if sub_name is None or not (sub_name.text or ''): + r.error(f'{check_num}. {context}: {sub_type} missing Name') + ok = False + sub_ext = sub_props.find(f'{{{MD}}}ExtendedConfigurationObject') + sub_name_val = (sub_name.text or '') if sub_name is not None else '?' + if sub_ext is None or not (sub_ext.text or ''): + r.error(f'{check_num}. {context}: {sub_type}.{sub_name_val} missing ExtendedConfigurationObject') + ok = False + elif not GUID_PATTERN.match(sub_ext.text): + r.error(f'{check_num}. {context}: {sub_type}.{sub_name_val} invalid ExtendedConfigurationObject') + ok = False + return ok + if child_obj_node is not None: borrowed_count = 0 borrowed_ok_count = 0 check9_ok = True + check10_ok = True + sub_item_count = 0 for child in child_obj_node: if not isinstance(child.tag, str): @@ -552,7 +584,6 @@ def main(): continue # Parse object XML - obj_doc = None try: obj_parser = etree.XMLParser(remove_blank_text=False) obj_doc = etree.parse(obj_file, obj_parser) @@ -571,16 +602,16 @@ def main(): if obj_el is None: continue - obj_props = obj_el.find(f'{{{NS["md"]}}}Properties') + obj_props = obj_el.find(f'{{{MD}}}Properties') if obj_props is None: continue - ob_node = obj_props.find(f'{{{NS["md"]}}}ObjectBelonging') + # --- Check 9: ObjectBelonging + ExtendedConfigurationObject --- + ob_node = obj_props.find(f'{{{MD}}}ObjectBelonging') if ob_node is not None and (ob_node.text or '') == 'Adopted': borrowed_count += 1 - # Check ExtendedConfigurationObject - ext_obj = obj_props.find(f'{{{NS["md"]}}}ExtendedConfigurationObject') + ext_obj = obj_props.find(f'{{{MD}}}ExtendedConfigurationObject') if ext_obj is None or not (ext_obj.text or ''): r.error(f'9. Borrowed {type_name}.{child_name}: missing ExtendedConfigurationObject') check9_ok = False @@ -590,14 +621,256 @@ def main(): else: borrowed_ok_count += 1 + # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- + obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects') + if obj_child_objects is not None: + ctx = f'{type_name}.{child_name}' + for sub_item in obj_child_objects: + if not isinstance(sub_item.tag, str): + continue + sub_type = etree.QName(sub_item.tag).localname + + if sub_type == 'Attribute': + sub_item_count += 1 + if not validate_borrowed_sub_item('10', ctx, 'Attribute', sub_item): + check10_ok = False + + elif sub_type == 'TabularSection': + sub_item_count += 1 + if not validate_borrowed_sub_item('10', ctx, 'TabularSection', sub_item): + check10_ok = False + else: + # Check InternalInfo GeneratedTypes + ts_info = sub_item.find(f'{{{MD}}}InternalInfo') + ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name') + ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?' + if ts_info is None: + r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo') + check10_ok = False + else: + gt_nodes = ts_info.findall(f'{{{XR}}}GeneratedType') + has_ts = any(gt.get('category') == 'TabularSection' for gt in gt_nodes) + has_tsr = any(gt.get('category') == 'TabularSectionRow' for gt in gt_nodes) + if not has_ts or not has_tsr: + r.error(f'10. {ctx}: TabularSection.{ts_label} missing GeneratedType (need TabularSection + TabularSectionRow)') + check10_ok = False + # Recurse into TS ChildObjects/Attribute + ts_child_objs = sub_item.find(f'{{{MD}}}ChildObjects') + if ts_child_objs is not None: + for ts_attr in ts_child_objs: + if not isinstance(ts_attr.tag, str): + continue + if etree.QName(ts_attr.tag).localname != 'Attribute': + continue + sub_item_count += 1 + if not validate_borrowed_sub_item('10', f'{ctx}.ТЧ.{ts_label}', 'Attribute', ts_attr): + check10_ok = False + + elif sub_type == 'EnumValue' and type_name == 'Enum': + sub_item_count += 1 + if validate_borrowed_sub_item('10', ctx, 'EnumValue', sub_item): + ev_name = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name') + if ev_name is not None and (ev_name.text or ''): + if child_name not in enum_values_index: + enum_values_index[child_name] = {} + enum_values_index[child_name][ev_name.text] = True + else: + check10_ok = False + + elif sub_type == 'Form': + form_name = sub_item.text or '' + if form_name: + form_meta_file = os.path.join(config_dir, dir_name, child_name, 'Forms', form_name + '.xml') + if not os.path.exists(form_meta_file): + r.error(f'10. {ctx}: Form.{form_name} metadata file missing') + check10_ok = False + form_list.append({ + 'TypeName': type_name, 'ObjName': child_name, + 'FormName': form_name, 'DirName': dir_name, + }) + sub_item_count += 1 + if r.stopped: break if borrowed_count == 0: - pass # no borrowed objects + r.ok('9. Borrowed objects: none found') elif check9_ok: r.ok(f'9. Borrowed objects: {borrowed_ok_count}/{borrowed_count} validated') + if sub_item_count == 0: + r.ok('10. Sub-items: none found') + elif check10_ok: + r.ok(f'10. Sub-items: {sub_item_count} validated (Attributes, TabularSections, EnumValues, Forms)') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 11: Borrowed form structure --- + borrowed_forms_with_tree = [] + check11_ok = True + form_count = 0 + + for fi in form_list: + form_count += 1 + form_base = os.path.join(config_dir, fi['DirName'], fi['ObjName'], 'Forms', fi['FormName']) + form_meta_file = os.path.join(os.path.dirname(form_base), fi['FormName'] + '.xml') + form_xml_file = os.path.join(form_base, 'Ext', 'Form.xml') + module_bsl_file = os.path.join(form_base, 'Ext', 'Form', 'Module.bsl') + ctx = f"{fi['TypeName']}.{fi['ObjName']}.Form.{fi['FormName']}" + + # Validate form metadata XML + if os.path.exists(form_meta_file): + try: + fm_doc = etree.parse(form_meta_file, etree.XMLParser(remove_blank_text=False)) + fm_root = fm_doc.getroot() + fm_el = None + for c in fm_root: + if isinstance(c.tag, str): + fm_el = c + break + if fm_el is not None: + fm_props = fm_el.find(f'{{{MD}}}Properties') + if fm_props is not None: + fm_ob = fm_props.find(f'{{{MD}}}ObjectBelonging') + is_borrowed = fm_ob is not None and (fm_ob.text or '') == 'Adopted' + if is_borrowed: + fm_ext = fm_props.find(f'{{{MD}}}ExtendedConfigurationObject') + if fm_ext is None or not (fm_ext.text or '') or not GUID_PATTERN.match(fm_ext.text or ''): + r.error(f'11. {ctx}: invalid/missing ExtendedConfigurationObject') + check11_ok = False + fm_type = fm_props.find(f'{{{MD}}}FormType') + if fm_type is not None and (fm_type.text or '') != 'Managed': + r.error(f"11. {ctx}: FormType must be 'Managed', got '{fm_type.text}'") + check11_ok = False + except etree.XMLSyntaxError as e: + r.warn(f'11. {ctx}: Cannot parse metadata: {e}') + + # Form.xml must exist + if not os.path.exists(form_xml_file): + r.error(f'11. {ctx}: Ext/Form.xml missing') + check11_ok = False + continue + + # Module.bsl should exist + if not os.path.exists(module_bsl_file): + r.warn(f'11. {ctx}: Ext/Form/Module.bsl missing') + + # Read Form.xml as raw text for BaseForm checks + with open(form_xml_file, 'r', encoding='utf-8-sig') as f: + form_raw_text = f.read() + + if ']+version=', form_raw_text): + r.warn(f'11. {ctx}: missing version attribute') + # Check DataPath/TitleDataPath only inside BaseForm (not in extension-added elements) + bf_match = re.search(r'(?s)]*>(.+)', form_raw_text) + if bf_match: + bf_content = bf_match.group(1) + if re.search(r'[^<]+', bf_content): + r.warn(f'11. {ctx}: found in BaseForm (should be stripped for borrowed forms)') + if re.search(r'[^<]+', bf_content): + r.warn(f'11. {ctx}: found in BaseForm (should be stripped)') + borrowed_forms_with_tree.append({ + 'Path': form_xml_file, 'RawText': form_raw_text, 'Context': ctx, + }) + + if form_count == 0: + r.ok('11. Borrowed forms: none found') + elif check11_ok: + bf_count = len(borrowed_forms_with_tree) + r.ok(f'11. Borrowed forms: {form_count} validated ({bf_count} with BaseForm)') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 12: Form dependency references --- + PLATFORM_STYLE_ITEMS = { + 'TableHeaderBackColor', 'AccentColor', 'NormalTextFont', + 'FormBackColor', 'ToolTipBackColor', 'BorderColor', + 'FieldBackColor', 'FieldTextColor', 'ButtonBackColor', + 'ButtonTextColor', 'AlternateRowColor', 'SpecialTextColor', + 'TextFont', 'ImportantColor', 'FormTextColor', + 'SmallTextFont', 'ExtraLargeTextFont', 'LargeTextFont', + 'NormalTextColor', 'GroupHeaderBackColor', 'GroupHeaderFont', + 'ErrorColor', 'SuccessColor', 'WarningColor', + } + check12_ok = True + dep_check_count = 0 + + for bf in borrowed_forms_with_tree: + raw = bf['RawText'] + ctx = bf['Context'] + missing_items = [] + + # CommonPicture references + cp_refs = {} + for m in re.finditer(r'CommonPicture\.(\w+)', raw): + cp_refs[m.group(1)] = True + cp_index = child_object_index.get('CommonPicture', {}) + for cp_name in cp_refs: + dep_check_count += 1 + if cp_name not in cp_index: + missing_items.append(f'CommonPicture.{cp_name}') + + # StyleItem references + si_refs = {} + for m in re.finditer(r'style:([A-Za-z\u0410-\u044F\u0401\u0451_][A-Za-z0-9\u0410-\u044F\u0401\u0451_]*)', raw): + si_refs[m.group(1)] = True + si_index = child_object_index.get('StyleItem', {}) + for si_name in si_refs: + dep_check_count += 1 + if si_name in PLATFORM_STYLE_ITEMS: + continue + if si_name not in si_index: + missing_items.append(f'StyleItem.{si_name}') + + # Enum DesignTimeRef references + enum_refs = {} + for m in re.finditer(r'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)', raw): + e_key = f'{m.group(1)}.{m.group(2)}' + enum_refs[e_key] = {'Enum': m.group(1), 'Value': m.group(2)} + e_index = child_object_index.get('Enum', {}) + for entry in enum_refs.values(): + dep_check_count += 1 + if entry['Enum'] not in e_index: + missing_items.append(f"Enum.{entry['Enum']}") + elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}): + missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}") + + for mi in missing_items: + r.warn(f'12. {ctx}: references {mi} not borrowed in extension') + check12_ok = False + + if len(borrowed_forms_with_tree) == 0: + r.ok('12. Form dependencies: no borrowed forms with tree') + elif check12_ok: + r.ok(f'12. Form dependencies: {dep_check_count} references checked') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 13: TypeLink with human-readable paths --- + check13_ok = True + type_link_count = 0 + + for bf in borrowed_forms_with_tree: + raw = bf['RawText'] + ctx = bf['Context'] + matches = re.findall(r'\s*Items\.[^<]*', raw) + if matches: + type_link_count += len(matches) + r.warn(f'13. {ctx}: {len(matches)} TypeLink(s) with human-readable Items.* DataPath (should be stripped)') + check13_ok = False + + if len(borrowed_forms_with_tree) == 0: + r.ok('13. TypeLink: no borrowed forms with tree') + elif check13_ok: + r.ok('13. TypeLink: clean') + # --- Final output --- r.finalize(out_file) sys.exit(1 if r.errors > 0 else 0)