mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-27 07:01:02 +03:00
Auto-build: copilot (powershell) from 5472e03
This commit is contained in:
@@ -0,0 +1,566 @@
|
||||
# xdto-validate v1.1 — Validate a 1C XDTO package
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[Alias('Path')]
|
||||
[string]$PackagePath,
|
||||
|
||||
[string]$ConfigDir,
|
||||
|
||||
[switch]$Detailed,
|
||||
|
||||
[int]$MaxErrors = 20,
|
||||
|
||||
[string]$OutFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Эти пространства имён предоставляет сама платформа — пакетов в конфигурации
|
||||
# для них нет и быть не должно (выведено по корпусу: импортируются, но
|
||||
# targetNamespace с таким значением ни у одного пакета нет)
|
||||
$PLATFORM_NS = @(
|
||||
"http://v8.1c.ru/8.1/data/core",
|
||||
"http://v8.1c.ru/8.1/data/enterprise",
|
||||
"http://v8.1c.ru/8.1/data/enterprise/current-config",
|
||||
"http://v8.1c.ru/8.1/data-composition-system/settings",
|
||||
"http://v8.1c.ru/8.3/data/ext",
|
||||
"http://www.w3.org/2001/XMLSchema"
|
||||
)
|
||||
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
$XS_NS = "http://www.w3.org/2001/XMLSchema"
|
||||
$XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
$MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
||||
|
||||
# --- Reporting ---
|
||||
|
||||
$script:errors = 0
|
||||
$script:warnings = 0
|
||||
$script:okCount = 0
|
||||
$script:stopped = $false
|
||||
$script:output = New-Object System.Text.StringBuilder
|
||||
|
||||
function Out-Line([string]$s) { [void]$script:output.AppendLine($s) }
|
||||
function Report-OK([string]$msg) {
|
||||
$script:okCount++
|
||||
if ($Detailed) { Out-Line "[OK] $msg" }
|
||||
}
|
||||
function Report-Error([string]$msg) {
|
||||
$script:errors++
|
||||
Out-Line "[ERROR] $msg"
|
||||
if ($script:errors -ge $MaxErrors) { $script:stopped = $true }
|
||||
}
|
||||
function Report-Warn([string]$msg) {
|
||||
$script:warnings++
|
||||
Out-Line "[WARN] $msg"
|
||||
}
|
||||
|
||||
# --- Resolve paths ---
|
||||
|
||||
if (-not [System.IO.Path]::IsPathRooted($PackagePath)) {
|
||||
$PackagePath = Join-Path (Get-Location).Path $PackagePath
|
||||
}
|
||||
|
||||
$binPath = $null
|
||||
$mdPath = $null
|
||||
if (Test-Path $PackagePath -PathType Leaf) {
|
||||
if ([System.IO.Path]::GetFileName($PackagePath) -eq "Package.bin") {
|
||||
$binPath = $PackagePath
|
||||
$pkgDir = [System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($PackagePath))
|
||||
if (Test-Path "$pkgDir.xml") { $mdPath = "$pkgDir.xml" }
|
||||
} elseif ($PackagePath.EndsWith(".xml")) {
|
||||
$mdPath = $PackagePath
|
||||
$stem = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($PackagePath), [System.IO.Path]::GetFileNameWithoutExtension($PackagePath))
|
||||
$c = Join-Path (Join-Path $stem "Ext") "Package.bin"
|
||||
if (Test-Path $c) { $binPath = $c }
|
||||
}
|
||||
} elseif (Test-Path $PackagePath -PathType Container) {
|
||||
$c = Join-Path (Join-Path $PackagePath "Ext") "Package.bin"
|
||||
if (Test-Path $c) {
|
||||
$binPath = $c
|
||||
$m = "$($PackagePath.TrimEnd('\','/')).xml"
|
||||
if (Test-Path $m) { $mdPath = $m }
|
||||
}
|
||||
}
|
||||
|
||||
$fileName = if ($binPath) { [System.IO.Path]::GetFileName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($binPath))) } else { $PackagePath }
|
||||
|
||||
if (-not $binPath) {
|
||||
Write-Host "[ERROR] Не найден Ext/Package.bin для пути: $PackagePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Каталог конфигурации — для проверок регистрации и зависимостей
|
||||
if (-not $ConfigDir) {
|
||||
$d = [System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($binPath)))
|
||||
# .../XDTOPackages/<Имя>/Ext/Package.bin -> .../XDTOPackages -> корень
|
||||
$ConfigDir = [System.IO.Path]::GetDirectoryName($d)
|
||||
}
|
||||
|
||||
$finalize = {
|
||||
$checks = $script:okCount + $script:errors + $script:warnings
|
||||
if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) {
|
||||
$result = "=== Validation OK: $fileName ($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, $script:output.ToString(), $utf8Bom)
|
||||
}
|
||||
}
|
||||
|
||||
# --- 1. Well-formedness ---
|
||||
|
||||
$doc = New-Object System.Xml.XmlDocument
|
||||
try {
|
||||
$doc.Load($binPath)
|
||||
} catch {
|
||||
Report-Error "Package.bin не является корректным XML: $($_.Exception.Message)"
|
||||
& $finalize
|
||||
exit 1
|
||||
}
|
||||
$pkg = $doc.DocumentElement
|
||||
if ($pkg.get_LocalName() -ne "package") {
|
||||
Report-Error "Ожидался корневой <package>, найден <$($pkg.get_LocalName())>"
|
||||
& $finalize
|
||||
exit 1
|
||||
}
|
||||
Report-OK "Package.bin: корректный XML, корень <package>"
|
||||
|
||||
$targetNs = $pkg.GetAttribute("targetNamespace")
|
||||
if (-not $targetNs) {
|
||||
Report-Error "У <package> не задан targetNamespace"
|
||||
} else {
|
||||
Report-OK "targetNamespace: $targetNs"
|
||||
}
|
||||
|
||||
# --- 2. Encoding / EOL ---
|
||||
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if (-not ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF)) {
|
||||
Report-Warn "Package.bin без BOM UTF-8 — платформа пишет файл с BOM"
|
||||
} else {
|
||||
Report-OK "Кодировка: UTF-8 с BOM"
|
||||
}
|
||||
|
||||
# --- 3. Collect declarations ---
|
||||
|
||||
$imports = New-Object System.Collections.ArrayList
|
||||
$localTypes = New-Object System.Collections.Generic.HashSet[string]
|
||||
$objectTypeNames = New-Object System.Collections.Generic.HashSet[string]
|
||||
$valueTypeNames = New-Object System.Collections.Generic.HashSet[string]
|
||||
$globalProps = New-Object System.Collections.Generic.HashSet[string]
|
||||
$topSequence = New-Object System.Collections.ArrayList
|
||||
|
||||
foreach ($n in $pkg.ChildNodes) {
|
||||
if ($n.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||||
[void]$topSequence.Add($n.get_LocalName())
|
||||
switch ($n.get_LocalName()) {
|
||||
"import" { [void]$imports.Add($n.GetAttribute("namespace")) }
|
||||
"objectType" { [void]$localTypes.Add($n.GetAttribute("name")); [void]$objectTypeNames.Add($n.GetAttribute("name")) }
|
||||
"valueType" { [void]$localTypes.Add($n.GetAttribute("name")); [void]$valueTypeNames.Add($n.GetAttribute("name")) }
|
||||
"property" { if ($n.HasAttribute("name")) { [void]$globalProps.Add($n.GetAttribute("name")) } }
|
||||
}
|
||||
}
|
||||
|
||||
# --- Порядок элементов верхнего уровня ---
|
||||
# Модель требует import -> property -> valueType -> objectType. Нарушение платформа
|
||||
# не прощает: db-update падает с «Ошибка преобразования данных XDTO».
|
||||
$TOP_ORDER = @("import", "property", "valueType", "objectType")
|
||||
$prevRank = -1
|
||||
$orderOk = $true
|
||||
foreach ($t in $topSequence) {
|
||||
$rank = [array]::IndexOf($TOP_ORDER, $t)
|
||||
if ($rank -lt 0) { continue }
|
||||
if ($rank -lt $prevRank) {
|
||||
Report-Error "Нарушен порядок элементов верхнего уровня: <$t> после <$($TOP_ORDER[$prevRank])>. Модель требует import -> property -> valueType -> objectType; платформа отвергнет пакет при обновлении конфигурации"
|
||||
$orderOk = $false
|
||||
break
|
||||
}
|
||||
$prevRank = $rank
|
||||
}
|
||||
if ($orderOk) { Report-OK "Порядок элементов верхнего уровня корректен" }
|
||||
|
||||
# --- 4. Duplicate type names ---
|
||||
|
||||
$seen = @{}
|
||||
foreach ($n in $pkg.ChildNodes) {
|
||||
if ($n.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||||
if ($n.get_LocalName() -ne "objectType" -and $n.get_LocalName() -ne "valueType") { continue }
|
||||
$nm = $n.GetAttribute("name")
|
||||
if (-not $nm) { Report-Error "<$($n.get_LocalName())> без атрибута name"; continue }
|
||||
if ($seen.ContainsKey($nm)) {
|
||||
Report-Error "Дублирующееся имя типа: $nm"
|
||||
} else {
|
||||
$seen[$nm] = $true
|
||||
}
|
||||
}
|
||||
if ($localTypes.Count -gt 0) { Report-OK "$($localTypes.Count) тип(ов), имена уникальны" }
|
||||
|
||||
# --- 5. Type references resolve ---
|
||||
|
||||
$usedNamespaces = New-Object System.Collections.Generic.HashSet[string]
|
||||
$anyTypeProps = New-Object System.Collections.ArrayList
|
||||
$refAttrs = @("type", "base", "ref", "itemType")
|
||||
|
||||
function Resolve-Ref([System.Xml.XmlElement]$el, [string]$attr, [string]$raw) {
|
||||
if (-not $raw) { return }
|
||||
# Нотация Кларка {ns}local
|
||||
if ($raw.StartsWith("{")) {
|
||||
$close = $raw.IndexOf("}")
|
||||
if ($close -lt 0) { Report-Error "Некорректная нотация Кларка в $attr=`"$raw`""; return }
|
||||
$ns = $raw.Substring(1, $close - 1)
|
||||
$local = $raw.Substring($close + 1)
|
||||
} else {
|
||||
$parts = $raw.Split(":")
|
||||
if ($parts.Count -eq 2) {
|
||||
$ns = $el.GetNamespaceOfPrefix($parts[0])
|
||||
$local = $parts[1]
|
||||
if (-not $ns) {
|
||||
Report-Error "Префикс `"$($parts[0])`" не объявлен: $attr=`"$raw`" (тип $($el.get_LocalName()))"
|
||||
return
|
||||
}
|
||||
} else {
|
||||
$ns = $null
|
||||
$local = $parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
if ($ns -eq $XS_NS -or $ns -eq $XSI_NS) {
|
||||
if ($local -eq "anyType") { [void]$anyTypeProps.Add($el) }
|
||||
return
|
||||
}
|
||||
if ($ns -eq $targetNs) {
|
||||
if ($attr -eq "ref") {
|
||||
if (-not $globalProps.Contains($local)) {
|
||||
Report-Error "ref=`"$raw`" не разрешается: в пакете нет глобального свойства `"$local`""
|
||||
}
|
||||
} elseif (-not $localTypes.Contains($local)) {
|
||||
Report-Error "$attr=`"$raw`" не разрешается: в пакете нет типа `"$local`""
|
||||
}
|
||||
return
|
||||
}
|
||||
if ($ns) {
|
||||
[void]$usedNamespaces.Add($ns)
|
||||
if (-not $imports.Contains($ns)) {
|
||||
Report-Error "$attr=`"$raw`" ссылается на `"$ns`", но <import namespace=`"$ns`"/> не объявлен"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$nodeCount = 0
|
||||
foreach ($el in $pkg.SelectNodes("//*")) {
|
||||
if ($el.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||||
$nodeCount++
|
||||
foreach ($a in $refAttrs) {
|
||||
if ($el.HasAttribute($a)) { Resolve-Ref $el $a $el.GetAttribute($a) }
|
||||
}
|
||||
if ($el.HasAttribute("memberTypes")) {
|
||||
foreach ($m in ($el.GetAttribute("memberTypes") -split "\s+")) {
|
||||
if ($m) { Resolve-Ref $el "memberTypes" $m }
|
||||
}
|
||||
}
|
||||
if ($script:stopped) { break }
|
||||
}
|
||||
if (-not $script:stopped) { Report-OK "$nodeCount узлов: ссылки на типы разрешаются" }
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- 6. Silent degradation to xs:anyType ---
|
||||
# Платформа при импорте XML-схемы молча подменяет неразрешённый чужой тип на anyType.
|
||||
|
||||
if ($anyTypeProps.Count -gt 0 -and $imports.Count -gt 0) {
|
||||
$names = @()
|
||||
foreach ($p in $anyTypeProps) { if ($p.HasAttribute("name")) { $names += $p.GetAttribute("name") } }
|
||||
$shown = ($names | Select-Object -First 5) -join ", "
|
||||
Report-Warn "Свойств с type=`"xs:anyType`": $($anyTypeProps.Count) при объявленных импортах ($shown). Тип не разрешён — заполнить структурно такое свойство нельзя. Обычно это след импорта XML-схемы: платформа заменяет неразрешённый чужой тип на anyType без ошибки"
|
||||
}
|
||||
|
||||
# --- 7. Unused imports ---
|
||||
|
||||
# Сам по себе неиспользуемый импорт безвреден и встречается в четверти пакетов
|
||||
# типовых конфигураций. Сигналом он становится только вместе с anyType — тогда это
|
||||
# почти наверняка неразрешённая зависимость.
|
||||
$unused = @()
|
||||
foreach ($imp in $imports) { if (-not $usedNamespaces.Contains($imp)) { $unused += $imp } }
|
||||
if ($unused.Count -gt 0 -and $anyTypeProps.Count -gt 0) {
|
||||
Report-Warn "Импорт(ы) без единого использованного типа: $($unused -join ', ') — вместе с anyType это признак неразрешённой зависимости"
|
||||
}
|
||||
if ($imports.Count -gt 0 -and $unused.Count -eq 0) { Report-OK "$($imports.Count) импорт(ов) — все используются" }
|
||||
|
||||
# --- 8. nillable on attribute-form properties ---
|
||||
|
||||
$nillAttrs = @()
|
||||
foreach ($p in $pkg.SelectNodes("//*[local-name()='property']")) {
|
||||
if ($p.GetAttribute("form") -eq "Attribute" -and $p.GetAttribute("nillable") -eq "true") {
|
||||
$nillAttrs += $p.GetAttribute("name")
|
||||
}
|
||||
}
|
||||
if ($nillAttrs.Count -gt 0) {
|
||||
Report-Warn "Свойств с nillable=`"true`" и form=`"Attribute`": $($nillAttrs.Count) ($(($nillAttrs | Select-Object -First 5) -join ', ')). Спецификация XSD не допускает nillable у атрибутов — экспорт XML-схемы в Конфигураторе их потеряет"
|
||||
}
|
||||
|
||||
# --- 9. Facet consistency ---
|
||||
|
||||
$FACET_NUM = @("totalDigits", "fractionDigits")
|
||||
$facetChecked = 0
|
||||
foreach ($t in $pkg.SelectNodes("//*[local-name()='valueType' or local-name()='typeDef']")) {
|
||||
if ($t.get_LocalName() -eq "typeDef" -and $t.GetAttribute("type", $XSI_NS) -eq "ObjectType") { continue }
|
||||
$facetChecked++
|
||||
$nm = if ($t.HasAttribute("name")) { $t.GetAttribute("name") } else { "(анонимный тип)" }
|
||||
|
||||
$len = $t.GetAttribute("length")
|
||||
if ($len -and ($t.HasAttribute("minLength") -or $t.HasAttribute("maxLength"))) {
|
||||
# Спецификация XSD это запрещает, но платформа такие типы хранит — предупреждение, не ошибка
|
||||
Report-Warn "$nm : length задан вместе с minLength/maxLength — спецификация XSD считает их взаимоисключающими"
|
||||
}
|
||||
$minL = $t.GetAttribute("minLength"); $maxL = $t.GetAttribute("maxLength")
|
||||
if ($minL -and $maxL -and ([int]$minL -gt [int]$maxL)) {
|
||||
Report-Error "$nm : minLength ($minL) больше maxLength ($maxL)"
|
||||
}
|
||||
$td = $t.GetAttribute("totalDigits"); $fd = $t.GetAttribute("fractionDigits")
|
||||
if ($td -and $fd -and ([int]$fd -gt [int]$td)) {
|
||||
Report-Error "$nm : fractionDigits ($fd) больше totalDigits ($td)"
|
||||
}
|
||||
$ws = $t.GetAttribute("whiteSpace")
|
||||
if ($ws -and @("preserve", "replace", "collapse") -notcontains $ws) {
|
||||
Report-Error "$nm : недопустимое whiteSpace=`"$ws`""
|
||||
}
|
||||
$var = $t.GetAttribute("variety")
|
||||
if ($var -and @("Atomic", "List", "Union") -notcontains $var) {
|
||||
Report-Error "$nm : недопустимое variety=`"$var`""
|
||||
}
|
||||
if ($var -eq "List" -and -not $t.HasAttribute("itemType")) {
|
||||
Report-Warn "$nm : variety=`"List`" без itemType"
|
||||
}
|
||||
if ($script:stopped) { break }
|
||||
}
|
||||
if ($facetChecked -gt 0 -and -not $script:stopped) { Report-OK "$facetChecked простых тип(ов): фасеты согласованы" }
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- 10. property form / bounds ---
|
||||
|
||||
foreach ($p in $pkg.SelectNodes("//*[local-name()='property']")) {
|
||||
$form = $p.GetAttribute("form")
|
||||
if ($form -and @("Element", "Attribute", "Text") -notcontains $form) {
|
||||
Report-Error "Свойство `"$($p.GetAttribute('name'))`": недопустимое form=`"$form`""
|
||||
}
|
||||
$ub = $p.GetAttribute("upperBound")
|
||||
if ($ub -and $ub -ne "-1" -and ([int]$ub -lt 1)) {
|
||||
Report-Error "Свойство `"$($p.GetAttribute('name'))`": upperBound=`"$ub`" (допустимы -1 или число ≥ 1)"
|
||||
}
|
||||
$lb = $p.GetAttribute("lowerBound")
|
||||
if ($lb -and $ub -and $ub -ne "-1" -and ([int]$lb -gt [int]$ub)) {
|
||||
Report-Error "Свойство `"$($p.GetAttribute('name'))`": lowerBound ($lb) больше upperBound ($ub)"
|
||||
}
|
||||
if (-not $p.HasAttribute("name") -and -not $p.HasAttribute("ref")) {
|
||||
Report-Error "Свойство без name и без ref"
|
||||
}
|
||||
# В модели XDTO fixed — булев признак, само значение лежит в default.
|
||||
# В XML-схеме наоборот: fixed="V" совмещает признак и значение.
|
||||
if ($p.HasAttribute("fixed")) {
|
||||
$fx = $p.GetAttribute("fixed")
|
||||
$pName = if ($p.HasAttribute("name")) { $p.GetAttribute("name") } else { $p.GetAttribute("ref") }
|
||||
if (@("true", "false") -cnotcontains $fx) {
|
||||
Report-Error "Свойство `"$pName`": fixed=`"$fx`" — в модели это булев признак, значение задаётся в default (в XML-схеме признак и значение совмещены в fixed)"
|
||||
} elseif ($fx -ceq "true" -and -not $p.HasAttribute("default")) {
|
||||
Report-Error "Отсутствует фиксированное значение свойства '$pName': есть fixed=`"true`", нет default"
|
||||
}
|
||||
}
|
||||
if ($script:stopped) { break }
|
||||
}
|
||||
if (-not $script:stopped) { Report-OK "Свойства: form, кратности и фиксированные значения корректны" }
|
||||
|
||||
# --- 10b. Structural consistency ---
|
||||
|
||||
$structOk = $true
|
||||
foreach ($t in $pkg.SelectNodes("//*[local-name()='objectType' or local-name()='typeDef']")) {
|
||||
if ($t.get_LocalName() -eq "typeDef" -and $t.GetAttribute("type", $XSI_NS) -ne "ObjectType") { continue }
|
||||
$tn = if ($t.HasAttribute("name")) { $t.GetAttribute("name") } else { "(анонимный тип)" }
|
||||
$propNames = @{}
|
||||
foreach ($c in $t.ChildNodes) {
|
||||
if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element -or $c.get_LocalName() -ne "property") { continue }
|
||||
$pn = $c.GetAttribute("name")
|
||||
if ($pn) {
|
||||
if ($propNames.ContainsKey($pn)) {
|
||||
Report-Error "$tn : дублирующееся имя свойства `"$pn`""
|
||||
$structOk = $false
|
||||
}
|
||||
$propNames[$pn] = $true
|
||||
}
|
||||
}
|
||||
if ($script:stopped) { break }
|
||||
}
|
||||
|
||||
foreach ($p in $pkg.SelectNodes("//*[local-name()='property']")) {
|
||||
$pn = if ($p.HasAttribute("name")) { $p.GetAttribute("name") } else { $p.GetAttribute("ref") }
|
||||
if ($p.HasAttribute("name") -and $p.HasAttribute("ref")) {
|
||||
Report-Error "Свойство `"$pn`": заданы одновременно name и ref — допустимо только одно"
|
||||
$structOk = $false
|
||||
}
|
||||
$inlineTypeDef = $null
|
||||
foreach ($c in $p.ChildNodes) {
|
||||
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "typeDef") { $inlineTypeDef = $c; break }
|
||||
}
|
||||
if ($inlineTypeDef -and $p.HasAttribute("type")) {
|
||||
Report-Error "Свойство `"$pn`": заданы одновременно type и вложенный <typeDef> — допустимо только одно"
|
||||
$structOk = $false
|
||||
}
|
||||
if ($inlineTypeDef -and -not $inlineTypeDef.HasAttribute("type", $XSI_NS)) {
|
||||
Report-Error "Свойство `"$pn`": у вложенного <typeDef> не задан xsi:type (ValueType или ObjectType)"
|
||||
$structOk = $false
|
||||
}
|
||||
if ($script:stopped) { break }
|
||||
}
|
||||
|
||||
# Анонимный тип внутри valueType задаёт базовый тип и xsi:type не несёт
|
||||
foreach ($vt in $pkg.SelectNodes("//*[local-name()='valueType']")) {
|
||||
foreach ($c in $vt.ChildNodes) {
|
||||
if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element -or $c.get_LocalName() -ne "typeDef") { continue }
|
||||
if ($c.HasAttribute("type", $XSI_NS)) {
|
||||
Report-Warn "$($vt.GetAttribute('name')) : у <typeDef> внутри <valueType> задан xsi:type — платформа его здесь не пишет"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Род базового типа должен совпадать
|
||||
foreach ($t in $pkg.SelectNodes("//*[local-name()='objectType'][@base]")) {
|
||||
$b = $t.GetAttribute("base")
|
||||
$parts = $b.Split(":")
|
||||
if ($parts.Count -ne 2) { continue }
|
||||
$bns = $t.GetNamespaceOfPrefix($parts[0])
|
||||
if ($bns -ne $targetNs) { continue }
|
||||
if ($valueTypeNames.Contains($parts[1])) {
|
||||
Report-Error "$($t.GetAttribute('name')) : base=`"$b`" ссылается на valueType, а objectType может наследоваться только от objectType"
|
||||
$structOk = $false
|
||||
}
|
||||
}
|
||||
foreach ($t in $pkg.SelectNodes("//*[local-name()='valueType'][@base]")) {
|
||||
$b = $t.GetAttribute("base")
|
||||
$parts = $b.Split(":")
|
||||
if ($parts.Count -ne 2) { continue }
|
||||
$bns = $t.GetNamespaceOfPrefix($parts[0])
|
||||
if ($bns -ne $targetNs) { continue }
|
||||
if ($objectTypeNames.Contains($parts[1])) {
|
||||
Report-Error "$($t.GetAttribute('name')) : base=`"$b`" ссылается на objectType, а valueType может строиться только на простом типе"
|
||||
$structOk = $false
|
||||
}
|
||||
}
|
||||
|
||||
# Union без состава
|
||||
foreach ($t in $pkg.SelectNodes("//*[local-name()='valueType' or local-name()='typeDef'][@variety='Union']")) {
|
||||
if ($t.HasAttribute("memberTypes")) { continue }
|
||||
$hasMember = $false
|
||||
foreach ($c in $t.ChildNodes) {
|
||||
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "typeDef") { $hasMember = $true; break }
|
||||
}
|
||||
if (-not $hasMember) {
|
||||
$tn = if ($t.HasAttribute("name")) { $t.GetAttribute("name") } else { "(анонимный тип)" }
|
||||
Report-Warn "$tn : variety=`"Union`" без memberTypes и без вложенных типов — состав объединения пуст"
|
||||
}
|
||||
}
|
||||
|
||||
if ($structOk -and -not $script:stopped) { Report-OK "Структура типов и свойств согласована" }
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- 11. Metadata object ---
|
||||
|
||||
if ($mdPath) {
|
||||
$md = New-Object System.Xml.XmlDocument
|
||||
$md.Load($mdPath)
|
||||
$nsm = New-Object System.Xml.XmlNamespaceManager($md.NameTable)
|
||||
$nsm.AddNamespace("md", $MD_NS)
|
||||
$mdName = $md.SelectSingleNode("//md:XDTOPackage/md:Properties/md:Name", $nsm)
|
||||
$mdNs = $md.SelectSingleNode("//md:XDTOPackage/md:Properties/md:Namespace", $nsm)
|
||||
if (-not $mdName) {
|
||||
Report-Error "В объекте метаданных не задано <Name>"
|
||||
} elseif ($mdName.InnerText -ne $fileName) {
|
||||
Report-Error "<Name>$($mdName.InnerText)</Name> не совпадает с именем каталога `"$fileName`""
|
||||
} else {
|
||||
Report-OK "Объект метаданных: Name = $($mdName.InnerText)"
|
||||
}
|
||||
if ($mdNs -and $mdNs.InnerText -ne $targetNs) {
|
||||
Report-Error "<Namespace>$($mdNs.InnerText)</Namespace> не совпадает с targetNamespace пакета ($targetNs)"
|
||||
} elseif ($mdNs) {
|
||||
Report-OK "Namespace объекта метаданных совпадает с targetNamespace"
|
||||
}
|
||||
} else {
|
||||
Report-Warn "Файл объекта метаданных <Имя>.xml не найден рядом с каталогом пакета"
|
||||
}
|
||||
|
||||
# --- 12. Registration in Configuration.xml + namespace uniqueness ---
|
||||
|
||||
$configXml = Join-Path $ConfigDir "Configuration.xml"
|
||||
if (Test-Path $configXml) {
|
||||
$cfg = New-Object System.Xml.XmlDocument
|
||||
$cfg.Load($configXml)
|
||||
$nsm2 = New-Object System.Xml.XmlNamespaceManager($cfg.NameTable)
|
||||
$nsm2.AddNamespace("md", $MD_NS)
|
||||
$registered = $false
|
||||
foreach ($e in $cfg.SelectNodes("//md:Configuration/md:ChildObjects/md:XDTOPackage", $nsm2)) {
|
||||
if ($e.InnerText -eq $fileName) { $registered = $true; break }
|
||||
}
|
||||
if ($registered) {
|
||||
Report-OK "Зарегистрирован в Configuration.xml"
|
||||
} else {
|
||||
Report-Error "<XDTOPackage>$fileName</XDTOPackage> отсутствует в ChildObjects файла Configuration.xml — платформа пакет не увидит"
|
||||
}
|
||||
|
||||
# Уникальность targetNamespace среди пакетов конфигурации
|
||||
$pkgRoot = Join-Path $ConfigDir "XDTOPackages"
|
||||
if (Test-Path $pkgRoot) {
|
||||
$clash = @()
|
||||
foreach ($other in (Get-ChildItem $pkgRoot -Directory -ErrorAction SilentlyContinue)) {
|
||||
if ($other.Name -eq $fileName) { continue }
|
||||
$ob = Join-Path (Join-Path $other.FullName "Ext") "Package.bin"
|
||||
if (-not (Test-Path $ob)) { continue }
|
||||
try {
|
||||
$od = New-Object System.Xml.XmlDocument
|
||||
$od.Load($ob)
|
||||
if ($od.DocumentElement.GetAttribute("targetNamespace") -eq $targetNs) { $clash += $other.Name }
|
||||
} catch {}
|
||||
}
|
||||
# Платформа отвергает пакет, если импортируемого namespace нет в конфигурации:
|
||||
# «Ошибка проверки модели XDTO: xdto-package-3.3 … не определен»
|
||||
$knownNs = @{}
|
||||
foreach ($other in (Get-ChildItem $pkgRoot -Directory -ErrorAction SilentlyContinue)) {
|
||||
$ob = Join-Path (Join-Path $other.FullName "Ext") "Package.bin"
|
||||
if (-not (Test-Path $ob)) { continue }
|
||||
try {
|
||||
$od = New-Object System.Xml.XmlDocument
|
||||
$od.Load($ob)
|
||||
$knownNs[$od.DocumentElement.GetAttribute("targetNamespace")] = $other.Name
|
||||
} catch {}
|
||||
}
|
||||
$missing = @()
|
||||
foreach ($imp in $imports) { if (-not $knownNs.ContainsKey($imp) -and $PLATFORM_NS -notcontains $imp) { $missing += $imp } }
|
||||
if ($missing.Count -gt 0) {
|
||||
Report-Error ("Импортируемые пакеты не определены в конфигурации: " + ($missing -join ", ") +
|
||||
". Платформа отвергнет пакет при обновлении конфигурации — соберите зависимости первыми")
|
||||
} elseif ($imports.Count -gt 0) {
|
||||
Report-OK "Все импорты разрешаются в пакеты конфигурации"
|
||||
}
|
||||
|
||||
if ($clash.Count -gt 0) {
|
||||
Report-Warn "targetNamespace `"$targetNs`" объявлен также в пакет(ах): $($clash -join ', '). Платформа это допускает, но <import> на это пространство имён становится неоднозначным"
|
||||
} else {
|
||||
Report-OK "targetNamespace уникален в конфигурации"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Report-Warn "Configuration.xml не найден ($ConfigDir) — проверки регистрации и уникальности namespace пропущены"
|
||||
}
|
||||
|
||||
# --- Final ---
|
||||
|
||||
& $finalize
|
||||
if ($script:errors -gt 0) { exit 1 }
|
||||
exit 0
|
||||
@@ -0,0 +1,569 @@
|
||||
# xdto-validate v1.1 — Validate a 1C XDTO package (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from lxml import etree
|
||||
|
||||
# Эти пространства имён предоставляет сама платформа — пакетов в конфигурации
|
||||
# для них нет и быть не должно (выведено по корпусу)
|
||||
PLATFORM_NS = {
|
||||
"http://v8.1c.ru/8.1/data/core",
|
||||
"http://v8.1c.ru/8.1/data/enterprise",
|
||||
"http://v8.1c.ru/8.1/data/enterprise/current-config",
|
||||
"http://v8.1c.ru/8.1/data-composition-system/settings",
|
||||
"http://v8.1c.ru/8.3/data/ext",
|
||||
"http://www.w3.org/2001/XMLSchema",
|
||||
}
|
||||
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
||||
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||
parser.add_argument("-PackagePath", "-Path", required=True)
|
||||
parser.add_argument("-ConfigDir", default="")
|
||||
parser.add_argument("-Detailed", action="store_true")
|
||||
parser.add_argument("-MaxErrors", type=int, default=20)
|
||||
parser.add_argument("-OutFile", default="")
|
||||
args = parser.parse_args()
|
||||
|
||||
package_path = os.path.abspath(args.PackagePath)
|
||||
detailed = args.Detailed
|
||||
max_errors = args.MaxErrors
|
||||
out_file = args.OutFile
|
||||
|
||||
# ── reporting ────────────────────────────────────────────────
|
||||
|
||||
state = {"errors": 0, "warnings": 0, "ok": 0, "stopped": False}
|
||||
output = []
|
||||
|
||||
|
||||
def out_line(s):
|
||||
output.append(s)
|
||||
|
||||
|
||||
def report_ok(msg):
|
||||
state["ok"] += 1
|
||||
if detailed:
|
||||
out_line(f"[OK] {msg}")
|
||||
|
||||
|
||||
def report_error(msg):
|
||||
state["errors"] += 1
|
||||
out_line(f"[ERROR] {msg}")
|
||||
if state["errors"] >= max_errors:
|
||||
state["stopped"] = True
|
||||
|
||||
|
||||
def report_warn(msg):
|
||||
state["warnings"] += 1
|
||||
out_line(f"[WARN] {msg}")
|
||||
|
||||
|
||||
# ── resolve paths ────────────────────────────────────────────
|
||||
|
||||
bin_path = None
|
||||
md_path = None
|
||||
|
||||
if os.path.isfile(package_path):
|
||||
if os.path.basename(package_path) == "Package.bin":
|
||||
bin_path = package_path
|
||||
pkg_dir = os.path.dirname(os.path.dirname(package_path))
|
||||
if os.path.exists(pkg_dir + ".xml"):
|
||||
md_path = pkg_dir + ".xml"
|
||||
elif package_path.endswith(".xml"):
|
||||
md_path = package_path
|
||||
stem = os.path.join(os.path.dirname(package_path),
|
||||
os.path.splitext(os.path.basename(package_path))[0])
|
||||
c = os.path.join(stem, "Ext", "Package.bin")
|
||||
if os.path.exists(c):
|
||||
bin_path = c
|
||||
elif os.path.isdir(package_path):
|
||||
c = os.path.join(package_path, "Ext", "Package.bin")
|
||||
if os.path.exists(c):
|
||||
bin_path = c
|
||||
m = package_path.rstrip("\\/") + ".xml"
|
||||
if os.path.exists(m):
|
||||
md_path = m
|
||||
|
||||
if not bin_path:
|
||||
print(f"[ERROR] Не найден Ext/Package.bin для пути: {package_path}")
|
||||
sys.exit(1)
|
||||
|
||||
file_name = os.path.basename(os.path.dirname(os.path.dirname(bin_path)))
|
||||
|
||||
config_dir = args.ConfigDir
|
||||
if not config_dir:
|
||||
# .../XDTOPackages/<Имя>/Ext/Package.bin -> .../XDTOPackages -> корень
|
||||
config_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(bin_path))))
|
||||
|
||||
|
||||
def finalize():
|
||||
checks = state["ok"] + state["errors"] + state["warnings"]
|
||||
if state["errors"] == 0 and state["warnings"] == 0 and not detailed:
|
||||
result = f"=== Validation OK: {file_name} ({checks} checks) ==="
|
||||
else:
|
||||
out_line("")
|
||||
out_line(f"=== Result: {state['errors']} errors, {state['warnings']} warnings ({checks} checks) ===")
|
||||
result = "\n".join(output)
|
||||
print(result)
|
||||
if out_file:
|
||||
with open(out_file, "w", encoding="utf-8-sig", newline="") as f:
|
||||
f.write("\n".join(output))
|
||||
|
||||
|
||||
def _parse_xml(source, from_string=False):
|
||||
"""Разбор с узким отступлением для не-URI пространств имён.
|
||||
|
||||
Платформа допускает в targetNamespace произвольную строку (в выгрузке БП есть
|
||||
пакет с кириллическим «ДопФайлУниверсальный»), .NET такое принимает, а libxml2
|
||||
отвергает. Откатываемся на восстанавливающий разбор ТОЛЬКО на этой ошибке,
|
||||
иначе по-настоящему битый XML перестал бы отличаться от корректного.
|
||||
"""
|
||||
try:
|
||||
return (etree.fromstring(source) if from_string else etree.parse(source))
|
||||
except etree.XMLSyntaxError as e:
|
||||
if "is not a valid URI" not in str(e):
|
||||
raise
|
||||
p = etree.XMLParser(recover=True)
|
||||
return (etree.fromstring(source, p) if from_string else etree.parse(source, p))
|
||||
|
||||
|
||||
def local(el):
|
||||
return etree.QName(el).localname
|
||||
|
||||
|
||||
# ── 1. well-formedness ───────────────────────────────────────
|
||||
|
||||
try:
|
||||
doc = _parse_xml(bin_path)
|
||||
except Exception as e: # noqa: BLE001
|
||||
report_error(f"Package.bin не является корректным XML: {e}")
|
||||
finalize()
|
||||
sys.exit(1)
|
||||
|
||||
pkg = doc.getroot()
|
||||
if local(pkg) != "package":
|
||||
report_error(f"Ожидался корневой <package>, найден <{local(pkg)}>")
|
||||
finalize()
|
||||
sys.exit(1)
|
||||
report_ok("Package.bin: корректный XML, корень <package>")
|
||||
|
||||
target_ns = pkg.get("targetNamespace")
|
||||
if not target_ns:
|
||||
report_error("У <package> не задан targetNamespace")
|
||||
else:
|
||||
report_ok(f"targetNamespace: {target_ns}")
|
||||
|
||||
# ── 2. encoding ──────────────────────────────────────────────
|
||||
|
||||
with open(bin_path, "rb") as f:
|
||||
head = f.read(3)
|
||||
if head != b"\xef\xbb\xbf":
|
||||
report_warn("Package.bin без BOM UTF-8 — платформа пишет файл с BOM")
|
||||
else:
|
||||
report_ok("Кодировка: UTF-8 с BOM")
|
||||
|
||||
# ── 3. declarations ──────────────────────────────────────────
|
||||
|
||||
imports = []
|
||||
local_types = set()
|
||||
object_type_names = set()
|
||||
value_type_names = set()
|
||||
global_props = set()
|
||||
top_sequence = []
|
||||
|
||||
for n in pkg:
|
||||
if not isinstance(n.tag, str):
|
||||
continue
|
||||
ln = local(n)
|
||||
top_sequence.append(ln)
|
||||
if ln == "import":
|
||||
imports.append(n.get("namespace"))
|
||||
elif ln == "objectType":
|
||||
local_types.add(n.get("name"))
|
||||
object_type_names.add(n.get("name"))
|
||||
elif ln == "valueType":
|
||||
local_types.add(n.get("name"))
|
||||
value_type_names.add(n.get("name"))
|
||||
elif ln == "property" and n.get("name"):
|
||||
global_props.add(n.get("name"))
|
||||
|
||||
# ── порядок элементов верхнего уровня ────────────────────────
|
||||
# Модель требует import -> property -> valueType -> objectType. Нарушение платформа
|
||||
# не прощает: db-update падает с «Ошибка преобразования данных XDTO».
|
||||
TOP_ORDER = ["import", "property", "valueType", "objectType"]
|
||||
prev_rank = -1
|
||||
order_ok = True
|
||||
for t in top_sequence:
|
||||
if t not in TOP_ORDER:
|
||||
continue
|
||||
rank = TOP_ORDER.index(t)
|
||||
if rank < prev_rank:
|
||||
report_error(f"Нарушен порядок элементов верхнего уровня: <{t}> после <{TOP_ORDER[prev_rank]}>. "
|
||||
"Модель требует import -> property -> valueType -> objectType; "
|
||||
"платформа отвергнет пакет при обновлении конфигурации")
|
||||
order_ok = False
|
||||
break
|
||||
prev_rank = rank
|
||||
if order_ok:
|
||||
report_ok("Порядок элементов верхнего уровня корректен")
|
||||
|
||||
# ── 4. duplicate type names ──────────────────────────────────
|
||||
|
||||
seen = set()
|
||||
for n in pkg:
|
||||
if not isinstance(n.tag, str) or local(n) not in ("objectType", "valueType"):
|
||||
continue
|
||||
nm = n.get("name")
|
||||
if not nm:
|
||||
report_error(f"<{local(n)}> без атрибута name")
|
||||
continue
|
||||
if nm in seen:
|
||||
report_error(f"Дублирующееся имя типа: {nm}")
|
||||
else:
|
||||
seen.add(nm)
|
||||
if local_types:
|
||||
report_ok(f"{len(local_types)} тип(ов), имена уникальны")
|
||||
|
||||
# ── 5. type references resolve ───────────────────────────────
|
||||
|
||||
used_namespaces = set()
|
||||
any_type_props = []
|
||||
REF_ATTRS = ("type", "base", "ref", "itemType")
|
||||
|
||||
|
||||
def resolve_ref(el, attr, raw):
|
||||
if not raw:
|
||||
return
|
||||
if raw.startswith("{"):
|
||||
close = raw.find("}")
|
||||
if close < 0:
|
||||
report_error(f'Некорректная нотация Кларка в {attr}="{raw}"')
|
||||
return
|
||||
ns = raw[1:close]
|
||||
loc = raw[close + 1:]
|
||||
else:
|
||||
parts = raw.split(":")
|
||||
if len(parts) == 2:
|
||||
ns = el.nsmap.get(parts[0])
|
||||
loc = parts[1]
|
||||
if not ns:
|
||||
report_error(f'Префикс "{parts[0]}" не объявлен: {attr}="{raw}" (тип {local(el)})')
|
||||
return
|
||||
else:
|
||||
ns = None
|
||||
loc = parts[0]
|
||||
|
||||
if ns in (XS_NS, XSI_NS):
|
||||
if loc == "anyType":
|
||||
any_type_props.append(el)
|
||||
return
|
||||
if ns == target_ns:
|
||||
if attr == "ref":
|
||||
if loc not in global_props:
|
||||
report_error(f'ref="{raw}" не разрешается: в пакете нет глобального свойства "{loc}"')
|
||||
elif loc not in local_types:
|
||||
report_error(f'{attr}="{raw}" не разрешается: в пакете нет типа "{loc}"')
|
||||
return
|
||||
if ns:
|
||||
used_namespaces.add(ns)
|
||||
if ns not in imports:
|
||||
report_error(f'{attr}="{raw}" ссылается на "{ns}", но <import namespace="{ns}"/> не объявлен')
|
||||
|
||||
|
||||
node_count = 0
|
||||
for el in pkg.iter():
|
||||
if not isinstance(el.tag, str):
|
||||
continue
|
||||
node_count += 1
|
||||
for a in REF_ATTRS:
|
||||
if el.get(a) is not None:
|
||||
resolve_ref(el, a, el.get(a))
|
||||
if el.get("memberTypes"):
|
||||
for m in el.get("memberTypes").split():
|
||||
resolve_ref(el, "memberTypes", m)
|
||||
if state["stopped"]:
|
||||
break
|
||||
if not state["stopped"]:
|
||||
report_ok(f"{node_count} узлов: ссылки на типы разрешаются")
|
||||
|
||||
if state["stopped"]:
|
||||
finalize()
|
||||
sys.exit(1)
|
||||
|
||||
# ── 6. silent degradation to xs:anyType ──────────────────────
|
||||
|
||||
if any_type_props and imports:
|
||||
names = [p.get("name") for p in any_type_props if p.get("name")]
|
||||
shown = ", ".join(names[:5])
|
||||
report_warn(
|
||||
f'Свойств с type="xs:anyType": {len(any_type_props)} при объявленных импортах ({shown}). '
|
||||
"Тип не разрешён — заполнить структурно такое свойство нельзя. Обычно это след импорта XML-схемы: платформа заменяет неразрешённый "
|
||||
"чужой тип на anyType без ошибки"
|
||||
)
|
||||
|
||||
# ── 7. unused imports ────────────────────────────────────────
|
||||
|
||||
# Сам по себе неиспользуемый импорт безвреден и встречается в четверти пакетов
|
||||
# типовых конфигураций. Сигналом он становится только вместе с anyType.
|
||||
unused = [i for i in imports if i not in used_namespaces]
|
||||
if unused and any_type_props:
|
||||
report_warn(f'Импорт(ы) без единого использованного типа: {", ".join(unused)} — '
|
||||
"вместе с anyType это признак неразрешённой зависимости")
|
||||
if imports and not unused:
|
||||
report_ok(f"{len(imports)} импорт(ов) — все используются")
|
||||
|
||||
# ── 8. nillable on attribute-form properties ─────────────────
|
||||
|
||||
nill_attrs = [p.get("name") for p in pkg.iter()
|
||||
if isinstance(p.tag, str) and local(p) == "property"
|
||||
and p.get("form") == "Attribute" and p.get("nillable") == "true"]
|
||||
if nill_attrs:
|
||||
report_warn(
|
||||
f'Свойств с nillable="true" и form="Attribute": {len(nill_attrs)} ({", ".join(nill_attrs[:5])}). '
|
||||
"Спецификация XSD не допускает nillable у атрибутов — экспорт XML-схемы в Конфигураторе их потеряет"
|
||||
)
|
||||
|
||||
# ── 9. facet consistency ─────────────────────────────────────
|
||||
|
||||
facet_checked = 0
|
||||
for t in pkg.iter():
|
||||
if not isinstance(t.tag, str) or local(t) not in ("valueType", "typeDef"):
|
||||
continue
|
||||
if local(t) == "typeDef" and t.get(f"{{{XSI_NS}}}type") == "ObjectType":
|
||||
continue
|
||||
facet_checked += 1
|
||||
nm = t.get("name") or "(анонимный тип)"
|
||||
|
||||
if t.get("length") and (t.get("minLength") or t.get("maxLength")):
|
||||
# Спецификация XSD это запрещает, но платформа такие типы хранит — предупреждение, не ошибка
|
||||
report_warn(f"{nm} : length задан вместе с minLength/maxLength — "
|
||||
"спецификация XSD считает их взаимоисключающими")
|
||||
min_l, max_l = t.get("minLength"), t.get("maxLength")
|
||||
if min_l and max_l and int(min_l) > int(max_l):
|
||||
report_error(f"{nm} : minLength ({min_l}) больше maxLength ({max_l})")
|
||||
td, fd = t.get("totalDigits"), t.get("fractionDigits")
|
||||
if td and fd and int(fd) > int(td):
|
||||
report_error(f"{nm} : fractionDigits ({fd}) больше totalDigits ({td})")
|
||||
ws = t.get("whiteSpace")
|
||||
if ws and ws not in ("preserve", "replace", "collapse"):
|
||||
report_error(f'{nm} : недопустимое whiteSpace="{ws}"')
|
||||
var = t.get("variety")
|
||||
if var and var not in ("Atomic", "List", "Union"):
|
||||
report_error(f'{nm} : недопустимое variety="{var}"')
|
||||
if var == "List" and t.get("itemType") is None:
|
||||
report_warn(f'{nm} : variety="List" без itemType')
|
||||
if state["stopped"]:
|
||||
break
|
||||
if facet_checked and not state["stopped"]:
|
||||
report_ok(f"{facet_checked} простых тип(ов): фасеты согласованы")
|
||||
|
||||
if state["stopped"]:
|
||||
finalize()
|
||||
sys.exit(1)
|
||||
|
||||
# ── 10. property form / bounds ───────────────────────────────
|
||||
|
||||
for p in pkg.iter():
|
||||
if not isinstance(p.tag, str) or local(p) != "property":
|
||||
continue
|
||||
form = p.get("form")
|
||||
if form and form not in ("Element", "Attribute", "Text"):
|
||||
report_error(f'Свойство "{p.get("name")}": недопустимое form="{form}"')
|
||||
ub = p.get("upperBound")
|
||||
if ub and ub != "-1" and int(ub) < 1:
|
||||
report_error(f'Свойство "{p.get("name")}": upperBound="{ub}" (допустимы -1 или число ≥ 1)')
|
||||
lb = p.get("lowerBound")
|
||||
if lb and ub and ub != "-1" and int(lb) > int(ub):
|
||||
report_error(f'Свойство "{p.get("name")}": lowerBound ({lb}) больше upperBound ({ub})')
|
||||
if p.get("name") is None and p.get("ref") is None:
|
||||
report_error("Свойство без name и без ref")
|
||||
# В модели XDTO fixed — булев признак, само значение лежит в default.
|
||||
# В XML-схеме наоборот: fixed="V" совмещает признак и значение.
|
||||
fx = p.get("fixed")
|
||||
if fx is not None:
|
||||
p_name = p.get("name") if p.get("name") is not None else p.get("ref")
|
||||
if fx not in ("true", "false"):
|
||||
report_error(
|
||||
f'Свойство "{p_name}": fixed="{fx}" — в модели это булев признак, '
|
||||
"значение задаётся в default (в XML-схеме признак и значение совмещены в fixed)"
|
||||
)
|
||||
elif fx == "true" and p.get("default") is None:
|
||||
report_error(
|
||||
f"Отсутствует фиксированное значение свойства '{p_name}': "
|
||||
'есть fixed="true", нет default'
|
||||
)
|
||||
if state["stopped"]:
|
||||
break
|
||||
if not state["stopped"]:
|
||||
report_ok("Свойства: form, кратности и фиксированные значения корректны")
|
||||
|
||||
# ── 10b. structural consistency ──────────────────────────────
|
||||
|
||||
struct_ok = True
|
||||
for t in pkg.iter():
|
||||
if not isinstance(t.tag, str) or local(t) not in ("objectType", "typeDef"):
|
||||
continue
|
||||
if local(t) == "typeDef" and t.get(f"{{{XSI_NS}}}type") != "ObjectType":
|
||||
continue
|
||||
tn = t.get("name") or "(анонимный тип)"
|
||||
prop_names = set()
|
||||
for c in t:
|
||||
if not isinstance(c.tag, str) or local(c) != "property":
|
||||
continue
|
||||
pn = c.get("name")
|
||||
if pn:
|
||||
if pn in prop_names:
|
||||
report_error(f'{tn} : дублирующееся имя свойства "{pn}"')
|
||||
struct_ok = False
|
||||
prop_names.add(pn)
|
||||
if state["stopped"]:
|
||||
break
|
||||
|
||||
for p_ in pkg.iter():
|
||||
if not isinstance(p_.tag, str) or local(p_) != "property":
|
||||
continue
|
||||
pn = p_.get("name") or p_.get("ref")
|
||||
if p_.get("name") is not None and p_.get("ref") is not None:
|
||||
report_error(f'Свойство "{pn}": заданы одновременно name и ref — допустимо только одно')
|
||||
struct_ok = False
|
||||
inline = next((c for c in p_ if isinstance(c.tag, str) and local(c) == "typeDef"), None)
|
||||
if inline is not None and p_.get("type") is not None:
|
||||
report_error(f'Свойство "{pn}": заданы одновременно type и вложенный <typeDef> — допустимо только одно')
|
||||
struct_ok = False
|
||||
if inline is not None and inline.get(f"{{{XSI_NS}}}type") is None:
|
||||
report_error(f'Свойство "{pn}": у вложенного <typeDef> не задан xsi:type (ValueType или ObjectType)')
|
||||
struct_ok = False
|
||||
if state["stopped"]:
|
||||
break
|
||||
|
||||
# Анонимный тип внутри valueType задаёт базовый тип и xsi:type не несёт
|
||||
for vt in pkg.iter():
|
||||
if not isinstance(vt.tag, str) or local(vt) != "valueType":
|
||||
continue
|
||||
for c in vt:
|
||||
if isinstance(c.tag, str) and local(c) == "typeDef" and c.get(f"{{{XSI_NS}}}type") is not None:
|
||||
report_warn(f'{vt.get("name")} : у <typeDef> внутри <valueType> задан xsi:type — '
|
||||
"платформа его здесь не пишет")
|
||||
|
||||
# Род базового типа должен совпадать
|
||||
for t in pkg.iter():
|
||||
if not isinstance(t.tag, str) or t.get("base") is None:
|
||||
continue
|
||||
kind = local(t)
|
||||
if kind not in ("objectType", "valueType"):
|
||||
continue
|
||||
parts = t.get("base").split(":")
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
if t.nsmap.get(parts[0]) != target_ns:
|
||||
continue
|
||||
if kind == "objectType" and parts[1] in value_type_names:
|
||||
report_error(f'{t.get("name")} : base="{t.get("base")}" ссылается на valueType, '
|
||||
"а objectType может наследоваться только от objectType")
|
||||
struct_ok = False
|
||||
if kind == "valueType" and parts[1] in object_type_names:
|
||||
report_error(f'{t.get("name")} : base="{t.get("base")}" ссылается на objectType, '
|
||||
"а valueType может строиться только на простом типе")
|
||||
struct_ok = False
|
||||
|
||||
# Union без состава
|
||||
for t in pkg.iter():
|
||||
if not isinstance(t.tag, str) or local(t) not in ("valueType", "typeDef"):
|
||||
continue
|
||||
if t.get("variety") != "Union" or t.get("memberTypes") is not None:
|
||||
continue
|
||||
if not any(isinstance(c.tag, str) and local(c) == "typeDef" for c in t):
|
||||
tn = t.get("name") or "(анонимный тип)"
|
||||
report_warn(f'{tn} : variety="Union" без memberTypes и без вложенных типов — состав объединения пуст')
|
||||
|
||||
if struct_ok and not state["stopped"]:
|
||||
report_ok("Структура типов и свойств согласована")
|
||||
|
||||
if state["stopped"]:
|
||||
finalize()
|
||||
sys.exit(1)
|
||||
|
||||
# ── 11. metadata object ──────────────────────────────────────
|
||||
|
||||
if md_path:
|
||||
md = _parse_xml(md_path)
|
||||
md_name = md.find(f".//{{{MD_NS}}}XDTOPackage/{{{MD_NS}}}Properties/{{{MD_NS}}}Name")
|
||||
md_ns = md.find(f".//{{{MD_NS}}}XDTOPackage/{{{MD_NS}}}Properties/{{{MD_NS}}}Namespace")
|
||||
if md_name is None:
|
||||
report_error("В объекте метаданных не задано <Name>")
|
||||
elif (md_name.text or "") != file_name:
|
||||
report_error(f'<Name>{md_name.text}</Name> не совпадает с именем каталога "{file_name}"')
|
||||
else:
|
||||
report_ok(f"Объект метаданных: Name = {md_name.text}")
|
||||
if md_ns is not None and (md_ns.text or "") != target_ns:
|
||||
report_error(f"<Namespace>{md_ns.text}</Namespace> не совпадает с targetNamespace пакета ({target_ns})")
|
||||
elif md_ns is not None:
|
||||
report_ok("Namespace объекта метаданных совпадает с targetNamespace")
|
||||
else:
|
||||
report_warn("Файл объекта метаданных <Имя>.xml не найден рядом с каталогом пакета")
|
||||
|
||||
# ── 12. registration + namespace uniqueness ──────────────────
|
||||
|
||||
config_xml = os.path.join(config_dir, "Configuration.xml")
|
||||
if os.path.exists(config_xml):
|
||||
cfg = _parse_xml(config_xml)
|
||||
registered = any((e.text or "") == file_name
|
||||
for e in cfg.iterfind(f".//{{{MD_NS}}}Configuration/{{{MD_NS}}}ChildObjects/{{{MD_NS}}}XDTOPackage"))
|
||||
if registered:
|
||||
report_ok("Зарегистрирован в Configuration.xml")
|
||||
else:
|
||||
report_error(f"<XDTOPackage>{file_name}</XDTOPackage> отсутствует в ChildObjects файла "
|
||||
"Configuration.xml — платформа пакет не увидит")
|
||||
|
||||
pkg_root = os.path.join(config_dir, "XDTOPackages")
|
||||
if os.path.isdir(pkg_root):
|
||||
clash = []
|
||||
for other in sorted(os.listdir(pkg_root)):
|
||||
if other == file_name:
|
||||
continue
|
||||
ob = os.path.join(pkg_root, other, "Ext", "Package.bin")
|
||||
if not os.path.exists(ob):
|
||||
continue
|
||||
try:
|
||||
if _parse_xml(ob).getroot().get("targetNamespace") == target_ns:
|
||||
clash.append(other)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
# Платформа отвергает пакет, если импортируемого namespace нет в конфигурации:
|
||||
# «Ошибка проверки модели XDTO: xdto-package-3.3 … не определен»
|
||||
known_ns = {}
|
||||
for other in sorted(os.listdir(pkg_root)):
|
||||
ob = os.path.join(pkg_root, other, "Ext", "Package.bin")
|
||||
if not os.path.exists(ob):
|
||||
continue
|
||||
try:
|
||||
known_ns[_parse_xml(ob).getroot().get("targetNamespace")] = other
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
missing_imports = [i for i in imports if i not in known_ns and i not in PLATFORM_NS]
|
||||
if missing_imports:
|
||||
report_error("Импортируемые пакеты не определены в конфигурации: "
|
||||
+ ", ".join(missing_imports)
|
||||
+ ". Платформа отвергнет пакет при обновлении конфигурации — "
|
||||
"соберите зависимости первыми")
|
||||
elif imports:
|
||||
report_ok("Все импорты разрешаются в пакеты конфигурации")
|
||||
|
||||
if clash:
|
||||
report_warn(f'targetNamespace "{target_ns}" объявлен также в пакет(ах): {", ".join(clash)}. '
|
||||
"Платформа это допускает, но <import> на это пространство имён становится неоднозначным")
|
||||
else:
|
||||
report_ok("targetNamespace уникален в конфигурации")
|
||||
else:
|
||||
report_warn(f"Configuration.xml не найден ({config_dir}) — проверки регистрации и уникальности namespace пропущены")
|
||||
|
||||
finalize()
|
||||
sys.exit(1 if state["errors"] > 0 else 0)
|
||||
Reference in New Issue
Block a user