mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-13 01:14:56 +03:00
Auto-build: opencode (python) from 6d119eb
This commit is contained in:
@@ -0,0 +1,939 @@
|
||||
# cfe-validate v1.4 — Validate 1C configuration extension structure (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[Alias('Path')]
|
||||
[string]$ExtensionPath,
|
||||
|
||||
[switch]$Detailed,
|
||||
|
||||
[int]$MaxErrors = 30,
|
||||
|
||||
[string]$OutFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve path ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||
}
|
||||
|
||||
if (Test-Path $ExtensionPath -PathType Container) {
|
||||
$candidate = Join-Path $ExtensionPath "Configuration.xml"
|
||||
if (Test-Path $candidate) {
|
||||
$ExtensionPath = $candidate
|
||||
} else {
|
||||
Write-Host "[ERROR] No Configuration.xml found in directory: $ExtensionPath"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path $ExtensionPath)) {
|
||||
Write-Host "[ERROR] File not found: $ExtensionPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$resolvedPath = (Resolve-Path $ExtensionPath).Path
|
||||
$configDir = Split-Path $resolvedPath -Parent
|
||||
|
||||
# --- Output infrastructure ---
|
||||
$script:errors = 0
|
||||
$script:warnings = 0
|
||||
$script:okCount = 0
|
||||
$script:stopped = $false
|
||||
$script:output = New-Object System.Text.StringBuilder 8192
|
||||
|
||||
function Out-Line {
|
||||
param([string]$msg)
|
||||
$script:output.AppendLine($msg) | Out-Null
|
||||
}
|
||||
|
||||
function Report-OK {
|
||||
param([string]$msg)
|
||||
$script:okCount++
|
||||
if ($Detailed) { Out-Line "[OK] $msg" }
|
||||
}
|
||||
|
||||
function Report-Error {
|
||||
param([string]$msg)
|
||||
$script:errors++
|
||||
Out-Line "[ERROR] $msg"
|
||||
if ($script:errors -ge $MaxErrors) {
|
||||
$script:stopped = $true
|
||||
}
|
||||
}
|
||||
|
||||
function Report-Warn {
|
||||
param([string]$msg)
|
||||
$script:warnings++
|
||||
Out-Line "[WARN] $msg"
|
||||
}
|
||||
|
||||
$finalize = {
|
||||
$checks = $script:okCount + $script:errors + $script:warnings
|
||||
if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) {
|
||||
$result = "=== Validation OK: Extension.$objName ($checks checks) ==="
|
||||
} else {
|
||||
Out-Line ""
|
||||
Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ==="
|
||||
$result = $script:output.ToString()
|
||||
}
|
||||
Write-Host $result
|
||||
|
||||
if ($OutFile) {
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding $true
|
||||
[System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom)
|
||||
Write-Host "Written to: $OutFile"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Reference tables ---
|
||||
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
|
||||
|
||||
# 7 fixed ClassIds for Configuration
|
||||
$validClassIds = @(
|
||||
"9cd510cd-abfc-11d4-9434-004095e12fc7",
|
||||
"9fcd25a0-4822-11d4-9414-008048da11f9",
|
||||
"e3687481-0a87-462c-a166-9f34594f9bba",
|
||||
"9de14907-ec23-4a07-96f0-85521cb6b53b",
|
||||
"51f2d5d8-ea4d-4064-8892-82951750031e",
|
||||
"e68182ea-4237-4383-967f-90c1e3370bc7",
|
||||
"fb282519-d103-4dd3-bc12-cb271d631dfc"
|
||||
)
|
||||
|
||||
# 44 types in canonical order
|
||||
$childObjectTypes = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","IntegrationService"
|
||||
)
|
||||
|
||||
# Type -> directory mapping
|
||||
$childTypeDirMap = @{
|
||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||
"SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
|
||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"
|
||||
"CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"; "Constant"="Constants"
|
||||
"CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents"
|
||||
"DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences"
|
||||
"DocumentJournal"="DocumentJournals"; "Enum"="Enums"; "Report"="Reports"
|
||||
"DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters"
|
||||
"AccumulationRegister"="AccumulationRegisters"
|
||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
||||
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
|
||||
"CalculationRegister"="CalculationRegisters"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||
"IntegrationService"="IntegrationServices"
|
||||
}
|
||||
|
||||
# Valid enum values for extension properties
|
||||
$validEnumValues = @{
|
||||
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
|
||||
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
|
||||
"ScriptVariant" = @("Russian","English")
|
||||
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
|
||||
}
|
||||
|
||||
# --- 1. Parse XML ---
|
||||
Out-Line ""
|
||||
|
||||
$xmlDoc = $null
|
||||
try {
|
||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$xmlDoc.PreserveWhitespace = $false
|
||||
$xmlDoc.Load($resolvedPath)
|
||||
} catch {
|
||||
Out-Line "=== Validation: Extension (parse failed) ==="
|
||||
Out-Line ""
|
||||
Report-Error "1. XML parse failed: $($_.Exception.Message)"
|
||||
& $finalize
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Register namespaces ---
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
||||
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema")
|
||||
$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core")
|
||||
|
||||
$root = $xmlDoc.DocumentElement
|
||||
|
||||
# --- Check 1: Root structure ---
|
||||
$check1Ok = $true
|
||||
$expectedNs = "http://v8.1c.ru/8.3/MDClasses"
|
||||
|
||||
if ($root.LocalName -ne "MetaDataObject") {
|
||||
Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'"
|
||||
& $finalize
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($root.NamespaceURI -ne $expectedNs) {
|
||||
Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'"
|
||||
$check1Ok = $false
|
||||
}
|
||||
|
||||
$version = $root.GetAttribute("version")
|
||||
if (-not $version) {
|
||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
||||
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
||||
}
|
||||
|
||||
# Must have Configuration child
|
||||
$cfgNode = $null
|
||||
foreach ($child in $root.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration" -and $child.NamespaceURI -eq $expectedNs) {
|
||||
$cfgNode = $child; break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $cfgNode) {
|
||||
Report-Error "1. No <Configuration> element found inside MetaDataObject"
|
||||
& $finalize
|
||||
exit 1
|
||||
}
|
||||
|
||||
# UUID
|
||||
$cfgUuid = $cfgNode.GetAttribute("uuid")
|
||||
if (-not $cfgUuid) {
|
||||
Report-Error "1. Missing uuid on <Configuration>"
|
||||
$check1Ok = $false
|
||||
} elseif ($cfgUuid -notmatch $guidPattern) {
|
||||
Report-Error "1. Invalid uuid '$cfgUuid' on <Configuration>"
|
||||
$check1Ok = $false
|
||||
}
|
||||
|
||||
# Get name early for header
|
||||
$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns)
|
||||
$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null }
|
||||
$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" }
|
||||
|
||||
$script:output.Insert(0, "=== Validation: Extension.$objName ===$([Environment]::NewLine)") | Out-Null
|
||||
|
||||
if ($check1Ok) {
|
||||
Report-OK "1. Root structure: MetaDataObject/Configuration, version $version"
|
||||
}
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- Check 2: InternalInfo ---
|
||||
$internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns)
|
||||
$check2Ok = $true
|
||||
|
||||
if (-not $internalInfo) {
|
||||
Report-Error "2. InternalInfo: missing"
|
||||
} else {
|
||||
$contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns)
|
||||
if ($contained.Count -ne 7) {
|
||||
Report-Warn "2. InternalInfo: expected 7 ContainedObject, found $($contained.Count)"
|
||||
}
|
||||
|
||||
$foundClassIds = @{}
|
||||
foreach ($co in $contained) {
|
||||
$classId = $co.SelectSingleNode("xr:ClassId", $ns)
|
||||
$objectId = $co.SelectSingleNode("xr:ObjectId", $ns)
|
||||
|
||||
if (-not $classId -or -not $classId.InnerText) {
|
||||
Report-Error "2. ContainedObject missing ClassId"
|
||||
$check2Ok = $false
|
||||
continue
|
||||
}
|
||||
|
||||
$cid = $classId.InnerText
|
||||
if ($validClassIds -notcontains $cid) {
|
||||
Report-Error "2. Unknown ClassId: $cid"
|
||||
$check2Ok = $false
|
||||
}
|
||||
|
||||
if ($foundClassIds.ContainsKey($cid)) {
|
||||
Report-Error "2. Duplicate ClassId: $cid"
|
||||
$check2Ok = $false
|
||||
}
|
||||
$foundClassIds[$cid] = $true
|
||||
|
||||
if (-not $objectId -or -not $objectId.InnerText) {
|
||||
Report-Error "2. ContainedObject missing ObjectId for ClassId $cid"
|
||||
$check2Ok = $false
|
||||
} elseif ($objectId.InnerText -notmatch $guidPattern) {
|
||||
Report-Error "2. Invalid ObjectId '$($objectId.InnerText)' for ClassId $cid"
|
||||
$check2Ok = $false
|
||||
}
|
||||
}
|
||||
|
||||
$missingIds = @($validClassIds | Where-Object { -not $foundClassIds.ContainsKey($_) })
|
||||
if ($missingIds.Count -gt 0) {
|
||||
Report-Warn "2. Missing ClassIds: $($missingIds.Count) of 7"
|
||||
}
|
||||
|
||||
if ($check2Ok) {
|
||||
Report-OK "2. InternalInfo: $($contained.Count) ContainedObject, all ClassIds valid"
|
||||
}
|
||||
}
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- Check 3: Extension-specific properties ---
|
||||
if (-not $propsNode) {
|
||||
Report-Error "3. Properties block missing"
|
||||
} else {
|
||||
$check3Ok = $true
|
||||
|
||||
# ObjectBelonging = Adopted
|
||||
$obNode = $propsNode.SelectSingleNode("md:ObjectBelonging", $ns)
|
||||
if (-not $obNode -or $obNode.InnerText -ne "Adopted") {
|
||||
Report-Error "3. ObjectBelonging must be 'Adopted', got '$($obNode.InnerText)'"
|
||||
$check3Ok = $false
|
||||
}
|
||||
|
||||
# Name
|
||||
if (-not $nameNode -or -not $nameNode.InnerText) {
|
||||
Report-Error "3. Name is missing or empty"
|
||||
$check3Ok = $false
|
||||
} else {
|
||||
$nameVal = $nameNode.InnerText
|
||||
if ($nameVal -notmatch $identPattern) {
|
||||
Report-Error "3. Name '$nameVal' is not a valid 1C identifier"
|
||||
$check3Ok = $false
|
||||
}
|
||||
}
|
||||
|
||||
# ConfigurationExtensionPurpose
|
||||
$purposeNode = $propsNode.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns)
|
||||
$validPurposes = @("Patch","Customization","AddOn")
|
||||
if (-not $purposeNode -or -not $purposeNode.InnerText) {
|
||||
Report-Error "3. ConfigurationExtensionPurpose is missing"
|
||||
$check3Ok = $false
|
||||
} elseif ($validPurposes -notcontains $purposeNode.InnerText) {
|
||||
Report-Error "3. ConfigurationExtensionPurpose '$($purposeNode.InnerText)' invalid (expected: Patch, Customization, AddOn)"
|
||||
$check3Ok = $false
|
||||
}
|
||||
|
||||
# NamePrefix
|
||||
$prefixNode = $propsNode.SelectSingleNode("md:NamePrefix", $ns)
|
||||
if (-not $prefixNode -or -not $prefixNode.InnerText) {
|
||||
Report-Warn "3. NamePrefix is empty"
|
||||
}
|
||||
|
||||
# KeepMappingToExtendedConfigurationObjectsByIDs
|
||||
$keepMapNode = $propsNode.SelectSingleNode("md:KeepMappingToExtendedConfigurationObjectsByIDs", $ns)
|
||||
if (-not $keepMapNode) {
|
||||
Report-Warn "3. KeepMappingToExtendedConfigurationObjectsByIDs is missing"
|
||||
}
|
||||
|
||||
# DefaultLanguage
|
||||
$defLangNode = $propsNode.SelectSingleNode("md:DefaultLanguage", $ns)
|
||||
$defLang = if ($defLangNode -and $defLangNode.InnerText) { $defLangNode.InnerText } else { "" }
|
||||
|
||||
if ($check3Ok) {
|
||||
$purposeVal = if ($purposeNode) { $purposeNode.InnerText } else { "?" }
|
||||
$prefixVal = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "(empty)" }
|
||||
Report-OK "3. Extension properties: Name=`"$objName`", Purpose=$purposeVal, Prefix=$prefixVal"
|
||||
}
|
||||
}
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- Check 4: Enum property values ---
|
||||
if ($propsNode) {
|
||||
$enumChecked = 0
|
||||
$check4Ok = $true
|
||||
|
||||
foreach ($propName in $validEnumValues.Keys) {
|
||||
$propNode = $propsNode.SelectSingleNode("md:$propName", $ns)
|
||||
if ($propNode -and $propNode.InnerText) {
|
||||
$val = $propNode.InnerText
|
||||
$allowed = $validEnumValues[$propName]
|
||||
if ($allowed -notcontains $val) {
|
||||
Report-Error "4. Property '$propName' has invalid value '$val'"
|
||||
$check4Ok = $false
|
||||
}
|
||||
$enumChecked++
|
||||
}
|
||||
}
|
||||
|
||||
if ($check4Ok) {
|
||||
Report-OK "4. Property values: $enumChecked enum properties checked"
|
||||
}
|
||||
} else {
|
||||
Report-Warn "4. No Properties block to check"
|
||||
}
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- Check 5: ChildObjects — valid types, no duplicates, order ---
|
||||
$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns)
|
||||
|
||||
if (-not $childObjNode) {
|
||||
Report-Error "5. ChildObjects block missing"
|
||||
} else {
|
||||
$check5Ok = $true
|
||||
$totalCount = 0
|
||||
$script:childObjectIndex = @{}
|
||||
$duplicates = @{}
|
||||
$typeFirstIndex = @{}
|
||||
$lastTypeOrder = -1
|
||||
$orderOk = $true
|
||||
|
||||
foreach ($child in $childObjNode.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
$typeName = $child.LocalName
|
||||
$objNameVal = $child.InnerText
|
||||
|
||||
$typeIdx = $childObjectTypes.IndexOf($typeName)
|
||||
if ($typeIdx -lt 0) {
|
||||
Report-Error "5. Unknown type '$typeName' in ChildObjects"
|
||||
$check5Ok = $false
|
||||
} else {
|
||||
if (-not $typeFirstIndex.ContainsKey($typeName)) {
|
||||
$typeFirstIndex[$typeName] = $typeIdx
|
||||
if ($typeIdx -lt $lastTypeOrder) {
|
||||
Report-Warn "5. Type '$typeName' is out of canonical order (after type at position $lastTypeOrder)"
|
||||
$orderOk = $false
|
||||
}
|
||||
$lastTypeOrder = $typeIdx
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
$script:childObjectIndex[$typeName][$objNameVal] = $true
|
||||
}
|
||||
|
||||
$totalCount++
|
||||
}
|
||||
|
||||
$typeCount = $script:childObjectIndex.Count
|
||||
if ($check5Ok) {
|
||||
$orderInfo = if ($orderOk) { ", order correct" } else { "" }
|
||||
Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}"
|
||||
}
|
||||
}
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- Check 6: DefaultLanguage references existing Language in ChildObjects ---
|
||||
if ($defLang -and $childObjNode) {
|
||||
$langName = $defLang
|
||||
if ($langName.StartsWith("Language.")) {
|
||||
$langName = $langName.Substring(9)
|
||||
}
|
||||
|
||||
$found = $false
|
||||
foreach ($child in $childObjNode.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language" -and $child.InnerText -eq $langName) {
|
||||
$found = $true; break
|
||||
}
|
||||
}
|
||||
|
||||
if ($found) {
|
||||
Report-OK "6. DefaultLanguage `"$defLang`" found in ChildObjects"
|
||||
} else {
|
||||
Report-Error "6. DefaultLanguage `"$defLang`" not found in ChildObjects"
|
||||
}
|
||||
} else {
|
||||
if (-not $defLang) {
|
||||
Report-Warn "6. Cannot check DefaultLanguage (empty)"
|
||||
} else {
|
||||
Report-Warn "6. Cannot check DefaultLanguage (no ChildObjects)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- Check 7: Language files exist ---
|
||||
if ($childObjNode) {
|
||||
$langNames = @()
|
||||
foreach ($child in $childObjNode.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language") {
|
||||
$langNames += $child.InnerText
|
||||
}
|
||||
}
|
||||
|
||||
if ($langNames.Count -gt 0) {
|
||||
$existCount = 0
|
||||
foreach ($ln in $langNames) {
|
||||
$langFile = Join-Path (Join-Path $configDir "Languages") "$ln.xml"
|
||||
if (Test-Path $langFile) {
|
||||
$existCount++
|
||||
} else {
|
||||
Report-Warn "7. Language file missing: Languages/$ln.xml"
|
||||
}
|
||||
}
|
||||
if ($existCount -eq $langNames.Count) {
|
||||
Report-OK "7. Language files: $existCount/$($langNames.Count) exist"
|
||||
}
|
||||
} else {
|
||||
Report-Warn "7. No Language entries in ChildObjects"
|
||||
}
|
||||
} else {
|
||||
Report-Warn "7. Cannot check language files (no ChildObjects)"
|
||||
}
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- Check 8: Object directories exist ---
|
||||
if ($childObjNode) {
|
||||
$dirsToCheck = @{}
|
||||
foreach ($child in $childObjNode.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
$typeName = $child.LocalName
|
||||
if ($typeName -eq "Language") { continue }
|
||||
if ($childTypeDirMap.ContainsKey($typeName)) {
|
||||
$dirName = $childTypeDirMap[$typeName]
|
||||
if (-not $dirsToCheck.ContainsKey($dirName)) {
|
||||
$dirsToCheck[$dirName] = 0
|
||||
}
|
||||
$dirsToCheck[$dirName] = $dirsToCheck[$dirName] + 1
|
||||
}
|
||||
}
|
||||
|
||||
$missingDirs = @()
|
||||
foreach ($dir in $dirsToCheck.Keys) {
|
||||
$dirPath = Join-Path $configDir $dir
|
||||
if (-not (Test-Path $dirPath -PathType Container)) {
|
||||
$missingDirs += "$dir ($($dirsToCheck[$dir]) objects)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($missingDirs.Count -eq 0) {
|
||||
Report-OK "8. Object directories: $($dirsToCheck.Count) directories, all exist"
|
||||
} else {
|
||||
foreach ($md in $missingDirs) {
|
||||
Report-Warn "8. Missing directory: $md"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- Check 9: Borrowed objects validation + Check 10: Sub-items ---
|
||||
$script:enumValuesIndex = @{}
|
||||
$script:formList = @()
|
||||
|
||||
# Helper: check if sub-item has explicit borrowed metadata
|
||||
function Test-BorrowedSubItem {
|
||||
param($subItem, $nsm)
|
||||
$subProps = $subItem.SelectSingleNode("md:Properties", $nsm)
|
||||
if (-not $subProps) { return $false }
|
||||
$subOb = $subProps.SelectSingleNode("md:ObjectBelonging", $nsm)
|
||||
if ($subOb -and $subOb.InnerText) { return $true }
|
||||
$subExt = $subProps.SelectSingleNode("md:ExtendedConfigurationObject", $nsm)
|
||||
return [bool]($subExt -and $subExt.InnerText)
|
||||
}
|
||||
|
||||
# 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 }
|
||||
$typeName = $child.LocalName
|
||||
$childName = $child.InnerText
|
||||
if ($typeName -eq "Language") { continue }
|
||||
|
||||
if (-not $childTypeDirMap.ContainsKey($typeName)) { continue }
|
||||
$dirName = $childTypeDirMap[$typeName]
|
||||
$objFile = Join-Path (Join-Path $configDir $dirName) "$childName.xml"
|
||||
|
||||
if (-not (Test-Path $objFile)) { continue }
|
||||
|
||||
# Parse object XML
|
||||
$objDoc = $null
|
||||
try {
|
||||
$objDoc = New-Object System.Xml.XmlDocument
|
||||
$objDoc.PreserveWhitespace = $false
|
||||
$objDoc.Load($objFile)
|
||||
} catch {
|
||||
Report-Warn "9. Cannot parse $dirName/$childName.xml: $($_.Exception.Message)"
|
||||
continue
|
||||
}
|
||||
|
||||
$objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable)
|
||||
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$objNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||
|
||||
# Find the object element (Catalog, Document, etc.)
|
||||
$objRoot = $objDoc.DocumentElement
|
||||
$objEl = $null
|
||||
foreach ($c in $objRoot.ChildNodes) {
|
||||
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
|
||||
}
|
||||
if (-not $objEl) { continue }
|
||||
|
||||
$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++
|
||||
|
||||
$extObj = $objProps.SelectSingleNode("md:ExtendedConfigurationObject", $objNs)
|
||||
if (-not $extObj -or -not $extObj.InnerText) {
|
||||
Report-Error "9. Borrowed ${typeName}.${childName}: missing ExtendedConfigurationObject"
|
||||
$check9Ok = $false
|
||||
} elseif ($extObj.InnerText -notmatch $guidPattern) {
|
||||
Report-Error "9. Borrowed ${typeName}.${childName}: invalid ExtendedConfigurationObject UUID '$($extObj.InnerText)'"
|
||||
$check9Ok = $false
|
||||
} else {
|
||||
$borrowedOk++
|
||||
}
|
||||
}
|
||||
|
||||
# --- 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") {
|
||||
if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue }
|
||||
$subItemCount++
|
||||
if (-not (Validate-BorrowedSubItem "10" $ctx "Attribute" $subItem $objNs)) {
|
||||
$check10Ok = $false
|
||||
}
|
||||
}
|
||||
elseif ($subType -eq "TabularSection") {
|
||||
if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue }
|
||||
$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 }
|
||||
if (-not (Test-BorrowedSubItem $tsAttr $objNs)) { continue }
|
||||
$subItemCount++
|
||||
if (-not (Validate-BorrowedSubItem "10" "${ctx}.ТЧ.${tsLabel}" "Attribute" $tsAttr $objNs)) {
|
||||
$check10Ok = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif ($subType -eq "EnumValue" -and $typeName -eq "Enum") {
|
||||
if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue }
|
||||
$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 }
|
||||
}
|
||||
|
||||
if ($borrowedCount -eq 0) {
|
||||
Report-OK "9. Borrowed objects: none found"
|
||||
} 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 '<BaseForm') {
|
||||
# Check BaseForm has version
|
||||
if ($formRawText -notmatch '<BaseForm[^>]+version=') {
|
||||
Report-Warn "11. ${ctx}: <BaseForm> missing version attribute"
|
||||
}
|
||||
|
||||
$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, '<xr:Ref>CommonPicture\.(\w+)</xr:Ref>')) {
|
||||
$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, '<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>')
|
||||
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 ---
|
||||
& $finalize
|
||||
|
||||
if ($script:errors -gt 0) {
|
||||
exit 1
|
||||
}
|
||||
exit 0
|
||||
@@ -0,0 +1,894 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-validate v1.4 — 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
|
||||
from lxml import etree
|
||||
|
||||
NS = {
|
||||
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
||||
'v8': 'http://v8.1c.ru/8.1/data/core',
|
||||
'xr': 'http://v8.1c.ru/8.3/xcf/readable',
|
||||
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||
'xs': 'http://www.w3.org/2001/XMLSchema',
|
||||
'app': 'http://v8.1c.ru/8.2/managed-application/core',
|
||||
}
|
||||
|
||||
GUID_PATTERN = re.compile(
|
||||
r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||
)
|
||||
IDENT_PATTERN = re.compile(
|
||||
r'^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_]'
|
||||
r'[A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
|
||||
)
|
||||
|
||||
# 7 fixed ClassIds for Configuration
|
||||
VALID_CLASS_IDS = [
|
||||
'9cd510cd-abfc-11d4-9434-004095e12fc7',
|
||||
'9fcd25a0-4822-11d4-9414-008048da11f9',
|
||||
'e3687481-0a87-462c-a166-9f34594f9bba',
|
||||
'9de14907-ec23-4a07-96f0-85521cb6b53b',
|
||||
'51f2d5d8-ea4d-4064-8892-82951750031e',
|
||||
'e68182ea-4237-4383-967f-90c1e3370bc7',
|
||||
'fb282519-d103-4dd3-bc12-cb271d631dfc',
|
||||
]
|
||||
|
||||
# 44 types in canonical order
|
||||
CHILD_OBJECT_TYPES = [
|
||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||
'Constant', 'CommonForm', 'Catalog', 'Document',
|
||||
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
||||
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
||||
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
|
||||
'ChartOfCalculationTypes', 'CalculationRegister',
|
||||
'BusinessProcess', 'Task', 'IntegrationService',
|
||||
]
|
||||
|
||||
# Type -> directory mapping
|
||||
CHILD_TYPE_DIR_MAP = {
|
||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||
'SettingsStorage': 'SettingsStorages', 'FunctionalOption': 'FunctionalOptions',
|
||||
'FunctionalOptionsParameter': 'FunctionalOptionsParameters', 'DefinedType': 'DefinedTypes',
|
||||
'CommonCommand': 'CommonCommands', 'CommandGroup': 'CommandGroups', 'Constant': 'Constants',
|
||||
'CommonForm': 'CommonForms', 'Catalog': 'Catalogs', 'Document': 'Documents',
|
||||
'DocumentNumerator': 'DocumentNumerators', 'Sequence': 'Sequences',
|
||||
'DocumentJournal': 'DocumentJournals', 'Enum': 'Enums', 'Report': 'Reports',
|
||||
'DataProcessor': 'DataProcessors', 'InformationRegister': 'InformationRegisters',
|
||||
'AccumulationRegister': 'AccumulationRegisters',
|
||||
'ChartOfCharacteristicTypes': 'ChartsOfCharacteristicTypes',
|
||||
'ChartOfAccounts': 'ChartsOfAccounts', 'AccountingRegister': 'AccountingRegisters',
|
||||
'ChartOfCalculationTypes': 'ChartsOfCalculationTypes',
|
||||
'CalculationRegister': 'CalculationRegisters',
|
||||
'BusinessProcess': 'BusinessProcesses', 'Task': 'Tasks',
|
||||
'IntegrationService': 'IntegrationServices',
|
||||
}
|
||||
|
||||
# Valid enum values for extension properties
|
||||
VALID_ENUM_VALUES = {
|
||||
'ConfigurationExtensionCompatibilityMode': [
|
||||
'DontUse', 'Version8_1', 'Version8_2_13', 'Version8_2_16',
|
||||
'Version8_3_1', 'Version8_3_2', 'Version8_3_3', 'Version8_3_4', 'Version8_3_5',
|
||||
'Version8_3_6', 'Version8_3_7', 'Version8_3_8', 'Version8_3_9', 'Version8_3_10',
|
||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
||||
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
|
||||
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
||||
],
|
||||
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
||||
'ScriptVariant': ['Russian', 'English'],
|
||||
'InterfaceCompatibilityMode': [
|
||||
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
|
||||
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
|
||||
],
|
||||
}
|
||||
|
||||
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
||||
|
||||
|
||||
class Reporter:
|
||||
def __init__(self, max_errors, detailed=False):
|
||||
self.errors = 0
|
||||
self.warnings = 0
|
||||
self.ok_count = 0
|
||||
self.stopped = False
|
||||
self.max_errors = max_errors
|
||||
self.detailed = detailed
|
||||
self.lines = []
|
||||
self.obj_name = '(unknown)'
|
||||
|
||||
def out(self, msg=''):
|
||||
self.lines.append(msg)
|
||||
|
||||
def ok(self, msg):
|
||||
self.ok_count += 1
|
||||
if self.detailed:
|
||||
self.lines.append(f'[OK] {msg}')
|
||||
|
||||
def error(self, msg):
|
||||
self.errors += 1
|
||||
self.lines.append(f'[ERROR] {msg}')
|
||||
if self.errors >= self.max_errors:
|
||||
self.stopped = True
|
||||
|
||||
def warn(self, msg):
|
||||
self.warnings += 1
|
||||
self.lines.append(f'[WARN] {msg}')
|
||||
|
||||
def text(self):
|
||||
return '\r\n'.join(self.lines) + '\r\n'
|
||||
|
||||
def finalize(self, out_file):
|
||||
checks = self.ok_count + self.errors + self.warnings
|
||||
if self.errors == 0 and self.warnings == 0 and not self.detailed:
|
||||
result = f'=== Validation OK: Extension.{self.obj_name} ({checks} checks) ==='
|
||||
else:
|
||||
self.out('')
|
||||
self.out(f'=== Result: {self.errors} errors, {self.warnings} warnings ({checks} checks) ===')
|
||||
result = self.text()
|
||||
|
||||
print(result, end='' if '\r\n' in result else '\n')
|
||||
|
||||
if out_file:
|
||||
with open(out_file, 'w', encoding='utf-8-sig', newline='') as f:
|
||||
f.write(result)
|
||||
print(f'Written to: {out_file}')
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False
|
||||
)
|
||||
parser.add_argument('-ExtensionPath', '-Path', dest='ExtensionPath', required=True)
|
||||
parser.add_argument('-Detailed', action='store_true')
|
||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||
args = parser.parse_args()
|
||||
|
||||
extension_path = args.ExtensionPath
|
||||
max_errors = args.MaxErrors
|
||||
out_file = args.OutFile
|
||||
|
||||
# --- Resolve path ---
|
||||
if not os.path.isabs(extension_path):
|
||||
extension_path = os.path.join(os.getcwd(), extension_path)
|
||||
|
||||
if os.path.isdir(extension_path):
|
||||
candidate = os.path.join(extension_path, 'Configuration.xml')
|
||||
if os.path.exists(candidate):
|
||||
extension_path = candidate
|
||||
else:
|
||||
print(f'[ERROR] No Configuration.xml found in directory: {extension_path}')
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.exists(extension_path):
|
||||
print(f'[ERROR] File not found: {extension_path}')
|
||||
sys.exit(1)
|
||||
|
||||
resolved_path = os.path.abspath(extension_path)
|
||||
config_dir = os.path.dirname(resolved_path)
|
||||
|
||||
if out_file and not os.path.isabs(out_file):
|
||||
out_file = os.path.join(os.getcwd(), out_file)
|
||||
|
||||
r = Reporter(max_errors, detailed=args.Detailed)
|
||||
r.out('')
|
||||
|
||||
# --- 1. Parse XML ---
|
||||
xml_doc = None
|
||||
try:
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
xml_doc = etree.parse(resolved_path, xml_parser)
|
||||
except etree.XMLSyntaxError as e:
|
||||
r.lines.insert(0, '=== Validation: Extension (parse failed) ===')
|
||||
r.out('')
|
||||
r.error(f'1. XML parse failed: {e}')
|
||||
r.finalize(out_file)
|
||||
sys.exit(1)
|
||||
|
||||
root = xml_doc.getroot()
|
||||
|
||||
# --- Check 1: Root structure ---
|
||||
check1_ok = True
|
||||
root_local = etree.QName(root.tag).localname
|
||||
root_ns = etree.QName(root.tag).namespace or ''
|
||||
|
||||
if root_local != 'MetaDataObject':
|
||||
r.error(f"1. Root element is '{root_local}', expected 'MetaDataObject'")
|
||||
r.finalize(out_file)
|
||||
sys.exit(1)
|
||||
|
||||
if root_ns != EXPECTED_NS:
|
||||
r.error(f"1. Root namespace is '{root_ns}', expected '{EXPECTED_NS}'")
|
||||
check1_ok = False
|
||||
|
||||
version = root.get('version', '')
|
||||
if not version:
|
||||
r.warn('1. Missing version attribute on MetaDataObject')
|
||||
elif version not in ('2.17', '2.20', '2.21'):
|
||||
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
||||
|
||||
# Must have Configuration child
|
||||
cfg_node = None
|
||||
for child in root:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
if etree.QName(child.tag).localname == 'Configuration' and etree.QName(child.tag).namespace == EXPECTED_NS:
|
||||
cfg_node = child
|
||||
break
|
||||
|
||||
if cfg_node is None:
|
||||
r.error('1. No <Configuration> element found inside MetaDataObject')
|
||||
r.finalize(out_file)
|
||||
sys.exit(1)
|
||||
|
||||
# UUID
|
||||
cfg_uuid = cfg_node.get('uuid', '')
|
||||
if not cfg_uuid:
|
||||
r.error('1. Missing uuid on <Configuration>')
|
||||
check1_ok = False
|
||||
elif not GUID_PATTERN.match(cfg_uuid):
|
||||
r.error(f"1. Invalid uuid '{cfg_uuid}' on <Configuration>")
|
||||
check1_ok = False
|
||||
|
||||
# Get name early for header
|
||||
props_node = cfg_node.find('md:Properties', NS)
|
||||
name_node = props_node.find('md:Name', NS) if props_node is not None else None
|
||||
obj_name = (name_node.text or '') if name_node is not None and name_node.text else '(unknown)'
|
||||
r.obj_name = obj_name
|
||||
|
||||
r.lines.insert(0, f'=== Validation: Extension.{obj_name} ===')
|
||||
|
||||
if check1_ok:
|
||||
r.ok(f'1. Root structure: MetaDataObject/Configuration, version {version}')
|
||||
|
||||
if r.stopped:
|
||||
r.finalize(out_file)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Check 2: InternalInfo ---
|
||||
internal_info = cfg_node.find('md:InternalInfo', NS)
|
||||
check2_ok = True
|
||||
|
||||
if internal_info is None:
|
||||
r.error('2. InternalInfo: missing')
|
||||
else:
|
||||
contained = internal_info.findall('xr:ContainedObject', NS)
|
||||
if len(contained) != 7:
|
||||
r.warn(f'2. InternalInfo: expected 7 ContainedObject, found {len(contained)}')
|
||||
|
||||
found_class_ids = {}
|
||||
for co in contained:
|
||||
class_id_el = co.find('xr:ClassId', NS)
|
||||
object_id_el = co.find('xr:ObjectId', NS)
|
||||
|
||||
if class_id_el is None or not (class_id_el.text or ''):
|
||||
r.error('2. ContainedObject missing ClassId')
|
||||
check2_ok = False
|
||||
continue
|
||||
|
||||
cid = class_id_el.text
|
||||
if cid not in VALID_CLASS_IDS:
|
||||
r.error(f'2. Unknown ClassId: {cid}')
|
||||
check2_ok = False
|
||||
|
||||
if cid in found_class_ids:
|
||||
r.error(f'2. Duplicate ClassId: {cid}')
|
||||
check2_ok = False
|
||||
found_class_ids[cid] = True
|
||||
|
||||
if object_id_el is None or not (object_id_el.text or ''):
|
||||
r.error(f'2. ContainedObject missing ObjectId for ClassId {cid}')
|
||||
check2_ok = False
|
||||
elif not GUID_PATTERN.match(object_id_el.text):
|
||||
r.error(f"2. Invalid ObjectId '{object_id_el.text}' for ClassId {cid}")
|
||||
check2_ok = False
|
||||
|
||||
missing_ids = [cid for cid in VALID_CLASS_IDS if cid not in found_class_ids]
|
||||
if len(missing_ids) > 0:
|
||||
r.warn(f'2. Missing ClassIds: {len(missing_ids)} of 7')
|
||||
|
||||
if check2_ok:
|
||||
r.ok(f'2. InternalInfo: {len(contained)} ContainedObject, all ClassIds valid')
|
||||
|
||||
if r.stopped:
|
||||
r.finalize(out_file)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Check 3: Extension-specific properties ---
|
||||
def_lang = ''
|
||||
|
||||
if props_node is None:
|
||||
r.error('3. Properties block missing')
|
||||
else:
|
||||
check3_ok = True
|
||||
|
||||
# ObjectBelonging = Adopted
|
||||
ob_node = props_node.find('md:ObjectBelonging', NS)
|
||||
ob_val = (ob_node.text or '') if ob_node is not None else ''
|
||||
if ob_val != 'Adopted':
|
||||
r.error(f"3. ObjectBelonging must be 'Adopted', got '{ob_val}'")
|
||||
check3_ok = False
|
||||
|
||||
# Name
|
||||
if name_node is None or not (name_node.text or ''):
|
||||
r.error('3. Name is missing or empty')
|
||||
check3_ok = False
|
||||
else:
|
||||
name_val = name_node.text
|
||||
if not IDENT_PATTERN.match(name_val):
|
||||
r.error(f"3. Name '{name_val}' is not a valid 1C identifier")
|
||||
check3_ok = False
|
||||
|
||||
# ConfigurationExtensionPurpose
|
||||
purpose_node = props_node.find('md:ConfigurationExtensionPurpose', NS)
|
||||
valid_purposes = ['Patch', 'Customization', 'AddOn']
|
||||
if purpose_node is None or not (purpose_node.text or ''):
|
||||
r.error('3. ConfigurationExtensionPurpose is missing')
|
||||
check3_ok = False
|
||||
elif purpose_node.text not in valid_purposes:
|
||||
r.error(f"3. ConfigurationExtensionPurpose '{purpose_node.text}' invalid (expected: Patch, Customization, AddOn)")
|
||||
check3_ok = False
|
||||
|
||||
# NamePrefix
|
||||
prefix_node = props_node.find('md:NamePrefix', NS)
|
||||
if prefix_node is None or not (prefix_node.text or ''):
|
||||
r.warn('3. NamePrefix is empty')
|
||||
|
||||
# KeepMappingToExtendedConfigurationObjectsByIDs
|
||||
keep_map_node = props_node.find('md:KeepMappingToExtendedConfigurationObjectsByIDs', NS)
|
||||
if keep_map_node is None:
|
||||
r.warn('3. KeepMappingToExtendedConfigurationObjectsByIDs is missing')
|
||||
|
||||
# DefaultLanguage
|
||||
def_lang_node = props_node.find('md:DefaultLanguage', NS)
|
||||
def_lang = (def_lang_node.text or '') if def_lang_node is not None else ''
|
||||
|
||||
if check3_ok:
|
||||
purpose_val = purpose_node.text if purpose_node is not None and purpose_node.text else '?'
|
||||
prefix_val = (prefix_node.text or '') if prefix_node is not None and prefix_node.text else '(empty)'
|
||||
r.ok(f'3. Extension properties: Name="{obj_name}", Purpose={purpose_val}, Prefix={prefix_val}')
|
||||
|
||||
if r.stopped:
|
||||
r.finalize(out_file)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Check 4: Enum property values ---
|
||||
if props_node is not None:
|
||||
enum_checked = 0
|
||||
check4_ok = True
|
||||
|
||||
for prop_name, allowed in VALID_ENUM_VALUES.items():
|
||||
prop_node = props_node.find(f'md:{prop_name}', NS)
|
||||
if prop_node is not None and prop_node.text:
|
||||
val = prop_node.text
|
||||
if val not in allowed:
|
||||
r.error(f"4. Property '{prop_name}' has invalid value '{val}'")
|
||||
check4_ok = False
|
||||
enum_checked += 1
|
||||
|
||||
if check4_ok:
|
||||
r.ok(f'4. Property values: {enum_checked} enum properties checked')
|
||||
else:
|
||||
r.warn('4. No Properties block to check')
|
||||
|
||||
if r.stopped:
|
||||
r.finalize(out_file)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Check 5: ChildObjects -- valid types, no duplicates, order ---
|
||||
child_obj_node = cfg_node.find('md:ChildObjects', NS)
|
||||
|
||||
if child_obj_node is None:
|
||||
r.error('5. ChildObjects block missing')
|
||||
else:
|
||||
check5_ok = True
|
||||
total_count = 0
|
||||
child_object_index = {}
|
||||
duplicates = {}
|
||||
type_first_index = {}
|
||||
last_type_order = -1
|
||||
order_ok = True
|
||||
|
||||
for child in child_obj_node:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
type_name = etree.QName(child.tag).localname
|
||||
obj_name_val = child.text or ''
|
||||
|
||||
if type_name in CHILD_OBJECT_TYPES:
|
||||
type_idx = CHILD_OBJECT_TYPES.index(type_name)
|
||||
else:
|
||||
type_idx = -1
|
||||
|
||||
if type_idx < 0:
|
||||
r.error(f"5. Unknown type '{type_name}' in ChildObjects")
|
||||
check5_ok = False
|
||||
else:
|
||||
if type_name not in type_first_index:
|
||||
type_first_index[type_name] = type_idx
|
||||
if type_idx < last_type_order:
|
||||
r.warn(f"5. Type '{type_name}' is out of canonical order (after type at position {last_type_order})")
|
||||
order_ok = False
|
||||
last_type_order = type_idx
|
||||
|
||||
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:
|
||||
child_object_index[type_name][obj_name_val] = True
|
||||
|
||||
total_count += 1
|
||||
|
||||
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}')
|
||||
|
||||
if r.stopped:
|
||||
r.finalize(out_file)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Check 6: DefaultLanguage references existing Language in ChildObjects ---
|
||||
if def_lang and child_obj_node is not None:
|
||||
lang_name = def_lang
|
||||
if lang_name.startswith('Language.'):
|
||||
lang_name = lang_name[9:]
|
||||
|
||||
found = False
|
||||
for child in child_obj_node:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
if etree.QName(child.tag).localname == 'Language' and (child.text or '') == lang_name:
|
||||
found = True
|
||||
break
|
||||
|
||||
if found:
|
||||
r.ok(f'6. DefaultLanguage "{def_lang}" found in ChildObjects')
|
||||
else:
|
||||
r.error(f'6. DefaultLanguage "{def_lang}" not found in ChildObjects')
|
||||
else:
|
||||
if not def_lang:
|
||||
r.warn('6. Cannot check DefaultLanguage (empty)')
|
||||
else:
|
||||
r.warn('6. Cannot check DefaultLanguage (no ChildObjects)')
|
||||
|
||||
if r.stopped:
|
||||
r.finalize(out_file)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Check 7: Language files exist ---
|
||||
if child_obj_node is not None:
|
||||
lang_names = []
|
||||
for child in child_obj_node:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
if etree.QName(child.tag).localname == 'Language':
|
||||
lang_names.append(child.text or '')
|
||||
|
||||
if len(lang_names) > 0:
|
||||
exist_count = 0
|
||||
for ln in lang_names:
|
||||
lang_file = os.path.join(config_dir, 'Languages', ln + '.xml')
|
||||
if os.path.exists(lang_file):
|
||||
exist_count += 1
|
||||
else:
|
||||
r.warn(f'7. Language file missing: Languages/{ln}.xml')
|
||||
if exist_count == len(lang_names):
|
||||
r.ok(f'7. Language files: {exist_count}/{len(lang_names)} exist')
|
||||
else:
|
||||
r.warn('7. No Language entries in ChildObjects')
|
||||
else:
|
||||
r.warn('7. Cannot check language files (no ChildObjects)')
|
||||
|
||||
if r.stopped:
|
||||
r.finalize(out_file)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Check 8: Object directories exist ---
|
||||
if child_obj_node is not None:
|
||||
dirs_to_check = {}
|
||||
for child in child_obj_node:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
type_name = etree.QName(child.tag).localname
|
||||
if type_name == 'Language':
|
||||
continue
|
||||
if type_name in CHILD_TYPE_DIR_MAP:
|
||||
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
||||
dirs_to_check[dir_name] = dirs_to_check.get(dir_name, 0) + 1
|
||||
|
||||
missing_dirs = []
|
||||
for dir_name, count in dirs_to_check.items():
|
||||
dir_path = os.path.join(config_dir, dir_name)
|
||||
if not os.path.isdir(dir_path):
|
||||
missing_dirs.append(f'{dir_name} ({count} objects)')
|
||||
|
||||
if len(missing_dirs) == 0:
|
||||
r.ok(f'8. Object directories: {len(dirs_to_check)} directories, all exist')
|
||||
else:
|
||||
for md in missing_dirs:
|
||||
r.warn(f'8. Missing directory: {md}')
|
||||
else:
|
||||
pass # no ChildObjects
|
||||
|
||||
if r.stopped:
|
||||
r.finalize(out_file)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Check 9: Borrowed objects + Check 10: Sub-items ---
|
||||
MD = NS['md']
|
||||
XR = NS['xr']
|
||||
enum_values_index = {}
|
||||
form_list = []
|
||||
|
||||
def is_borrowed_sub_item(sub_item):
|
||||
"""Check if sub-item has explicit borrowed metadata (ObjectBelonging or ExtendedConfigurationObject)."""
|
||||
sub_props = sub_item.find(f'{{{MD}}}Properties')
|
||||
if sub_props is None:
|
||||
return False
|
||||
sub_ob = sub_props.find(f'{{{MD}}}ObjectBelonging')
|
||||
if sub_ob is not None and (sub_ob.text or ''):
|
||||
return True
|
||||
sub_ext = sub_props.find(f'{{{MD}}}ExtendedConfigurationObject')
|
||||
return sub_ext is not None and bool(sub_ext.text or '')
|
||||
|
||||
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):
|
||||
continue
|
||||
type_name = etree.QName(child.tag).localname
|
||||
child_name = child.text or ''
|
||||
if type_name == 'Language':
|
||||
continue
|
||||
|
||||
if type_name not in CHILD_TYPE_DIR_MAP:
|
||||
continue
|
||||
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
||||
obj_file = os.path.join(config_dir, dir_name, child_name + '.xml')
|
||||
|
||||
if not os.path.exists(obj_file):
|
||||
continue
|
||||
|
||||
# Parse object XML
|
||||
try:
|
||||
obj_parser = etree.XMLParser(remove_blank_text=False)
|
||||
obj_doc = etree.parse(obj_file, obj_parser)
|
||||
except etree.XMLSyntaxError as e:
|
||||
r.warn(f'9. Cannot parse {dir_name}/{child_name}.xml: {e}')
|
||||
continue
|
||||
|
||||
obj_root = obj_doc.getroot()
|
||||
|
||||
# Find the object element (Catalog, Document, etc.)
|
||||
obj_el = None
|
||||
for c in obj_root:
|
||||
if isinstance(c.tag, str):
|
||||
obj_el = c
|
||||
break
|
||||
if obj_el is None:
|
||||
continue
|
||||
|
||||
obj_props = obj_el.find(f'{{{MD}}}Properties')
|
||||
if obj_props is None:
|
||||
continue
|
||||
|
||||
# --- 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
|
||||
|
||||
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
|
||||
elif not GUID_PATTERN.match(ext_obj.text):
|
||||
r.error(f"9. Borrowed {type_name}.{child_name}: invalid ExtendedConfigurationObject UUID '{ext_obj.text}'")
|
||||
check9_ok = False
|
||||
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':
|
||||
if not is_borrowed_sub_item(sub_item):
|
||||
continue
|
||||
sub_item_count += 1
|
||||
if not validate_borrowed_sub_item('10', ctx, 'Attribute', sub_item):
|
||||
check10_ok = False
|
||||
|
||||
elif sub_type == 'TabularSection':
|
||||
if not is_borrowed_sub_item(sub_item):
|
||||
continue
|
||||
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
|
||||
if not is_borrowed_sub_item(ts_attr):
|
||||
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':
|
||||
if not is_borrowed_sub_item(sub_item):
|
||||
continue
|
||||
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:
|
||||
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 '<BaseForm' in form_raw_text:
|
||||
if not re.search(r'<BaseForm[^>]+version=', form_raw_text):
|
||||
r.warn(f'11. {ctx}: <BaseForm> missing version attribute')
|
||||
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'<xr:Ref>CommonPicture\.(\w+)</xr:Ref>', 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'<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>', 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)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user