mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-07 04:00:20 +03:00
fix(xdto-compile,xdto-validate): не терять конструкции молча; вычитать инструкции
xdto-compile терял свойства без единого слова: на реалистичной чужой схеме из шести объявленных доезжало одно. Вложенные xs:sequence/xs:choice теперь уплощаются (модель хранит плоский список), xs:all трактуется как последовательность, xs:group и xs:attributeGroup раскрываются по ссылке — и о каждом приближении навык пишет предупреждение. Молчаливая потеря — тот же класс дефекта, что мы ловим у платформы, лечится так же: сообщением, не отказом. xdto-validate получил проверки на грабли, найденные при разработке: порядок элементов верхнего уровня (платформа отвергает пакет, не называя причины), конфликты объявлений (name+ref, type+вложенный тип, тип без разновидности), несовпадение рода базового типа, дубли имён свойств. Новые правила прогнаны по всем 760 пакетам выгрузок: всё, что породила платформа, валидно по определению, поэтому каждая ошибка там — ошибка правила. Первый прогон дал 7, и все три класса оказались реальным поведением платформы: length вместе с minLength/maxLength встречается, два пакета делят один targetNamespace (Envelope и SOAP_Envelope_1_1 в БП), form="Text" называется не только __content. Правила понижены до предупреждений либо сняты. Заодно убран шум: предупреждение о неиспользуемом import срабатывало на четверти корпуса — теперь только вместе с anyType, где оно и означает проблему. Итог: 0 ошибок на корпусе, предупреждений 53 вместо 242. Инструкции переписаны под читателя-исполнителя: убраны детали реализации и наши мерки, каталог проверок валидатора (его вывод самодостаточен), локальные пути в примерах заменены нейтральными. Таблица соответствий XSD и справочник аннотаций вынесены в xdto-compile/xsd-reference.md. Round-trip 760/760 сохранён, паритет PS/PY сохранён. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d05aef54b4
commit
7ca6dfa6b2
@@ -140,18 +140,40 @@ if (-not ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -a
|
||||
|
||||
$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")) }
|
||||
"valueType" { [void]$localTypes.Add($n.GetAttribute("name")) }
|
||||
"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 = @{}
|
||||
@@ -249,12 +271,15 @@ if ($anyTypeProps.Count -gt 0 -and $imports.Count -gt 0) {
|
||||
|
||||
# --- 7. Unused imports ---
|
||||
|
||||
foreach ($imp in $imports) {
|
||||
if (-not $usedNamespaces.Contains($imp)) {
|
||||
Report-Warn "<import namespace=`"$imp`"/> объявлен, но ни один тип из этого пространства имён не используется"
|
||||
}
|
||||
# Сам по себе неиспользуемый импорт безвреден и встречается в четверти пакетов
|
||||
# типовых конфигураций. Сигналом он становится только вместе с 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 $script:warnings -eq 0) { Report-OK "$($imports.Count) импорт(ов) — все используются" }
|
||||
if ($imports.Count -gt 0 -and $unused.Count -eq 0) { Report-OK "$($imports.Count) импорт(ов) — все используются" }
|
||||
|
||||
# --- 8. nillable on attribute-form properties ---
|
||||
|
||||
@@ -279,7 +304,8 @@ foreach ($t in $pkg.SelectNodes("//*[local-name()='valueType' or local-name()='t
|
||||
|
||||
$len = $t.GetAttribute("length")
|
||||
if ($len -and ($t.HasAttribute("minLength") -or $t.HasAttribute("maxLength"))) {
|
||||
Report-Error "$nm : length несовместим с minLength/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)) {
|
||||
@@ -328,6 +354,99 @@ foreach ($p in $pkg.SelectNodes("//*[local-name()='property']")) {
|
||||
}
|
||||
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) {
|
||||
@@ -386,7 +505,7 @@ if (Test-Path $configXml) {
|
||||
} catch {}
|
||||
}
|
||||
if ($clash.Count -gt 0) {
|
||||
Report-Error "targetNamespace `"$targetNs`" уже занят пакет(ами): $($clash -join ', '). Платформа не допускает два пакета с одним пространством имён"
|
||||
Report-Warn "targetNamespace `"$targetNs`" объявлен также в пакет(ах): $($clash -join ', '). Платформа это допускает, но <import> на это пространство имён становится неоднозначным"
|
||||
} else {
|
||||
Report-OK "targetNamespace уникален в конфигурации"
|
||||
}
|
||||
|
||||
@@ -145,19 +145,47 @@ else:
|
||||
|
||||
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 in ("objectType", "valueType"):
|
||||
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()
|
||||
@@ -254,10 +282,13 @@ if any_type_props and imports:
|
||||
|
||||
# ── 7. unused imports ────────────────────────────────────────
|
||||
|
||||
for imp in imports:
|
||||
if imp not in used_namespaces:
|
||||
report_warn(f'<import namespace="{imp}"/> объявлен, но ни один тип из этого пространства имён не используется')
|
||||
if imports and state["warnings"] == 0:
|
||||
# Сам по себе неиспользуемый импорт безвреден и встречается в четверти пакетов
|
||||
# типовых конфигураций. Сигналом он становится только вместе с 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 ─────────────────
|
||||
@@ -283,7 +314,9 @@ for t in pkg.iter():
|
||||
nm = t.get("name") or "(анонимный тип)"
|
||||
|
||||
if t.get("length") and (t.get("minLength") or t.get("maxLength")):
|
||||
report_error(f"{nm} : length несовместим с minLength/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})")
|
||||
@@ -328,6 +361,92 @@ for p in pkg.iter():
|
||||
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:
|
||||
@@ -375,8 +494,8 @@ if os.path.exists(config_xml):
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
if clash:
|
||||
report_error(f'targetNamespace "{target_ns}" уже занят пакет(ами): {", ".join(clash)}. '
|
||||
"Платформа не допускает два пакета с одним пространством имён")
|
||||
report_warn(f'targetNamespace "{target_ns}" объявлен также в пакет(ах): {", ".join(clash)}. '
|
||||
"Платформа это допускает, но <import> на это пространство имён становится неоднозначным")
|
||||
else:
|
||||
report_ok("targetNamespace уникален в конфигурации")
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user