mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-30 05:55:53 +03:00
feat(skd-validate): подсказки XSD-проверки — что именно исправить
Текст валидатора про порядок («недопустимый дочерний X, ожидается A, B») читался как «добавь A». Теперь для нарушений порядка скрипт сам строит подсказку по XSD: must come before <S>, duplicate, not allowed in <P>. Обход схемы свой в обоих портах — подсказки совпадают побайтно. Остальные сообщения — без пространств имён. Ошибка атрибута в PS привязывается к своему элементу, а не к родителю. В SKILL.md — что делать со строками XSD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dc2yLQfw6wppbopM1Y4Ps
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5465a7803c
commit
8c15c84f73
@@ -12,7 +12,6 @@ allowed-tools:
|
||||
|
||||
Проверяет структурную корректность Template.xml схемы компоновки данных. Выявляет ошибки формата, битые ссылки, дубликаты имён.
|
||||
|
||||
Если в проекте есть XSD-схемы платформы (`/v8-xsd-fetch`), дополнительно проверяет порядок элементов, типы и значения по схеме версии формата выгрузки.
|
||||
|
||||
## Параметры
|
||||
|
||||
@@ -29,3 +28,12 @@ allowed-tools:
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-validate.ps1" -TemplatePath "src/МойОтчёт/Templates/ОсновнаяСхема"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-validate.ps1" -TemplatePath "Catalogs/Номенклатура/Templates/СКД/Ext/Template.xml"
|
||||
```
|
||||
|
||||
## Проверка по XSD
|
||||
|
||||
Если в проекте есть XSD-схемы платформы (`/v8-xsd-fetch`), дополнительно проверяет порядок элементов, типы и значения по схеме версии формата выгрузки. Строки вида `XSD 2.20, line N: <родитель>/<элемент>: …`:
|
||||
|
||||
- `must come before <X>` — элемент не на своём месте: перенести его перед `<X>`, содержимое не менять. `duplicate` — лишний повтор. `not allowed in <P>` — в этом месте такого элемента быть не должно. Остальное — недопустимое значение, текст говорит какое.
|
||||
- `[ERROR]` — по схемам своей версии формата. `[WARN]` с `(no schemas for X)` — по более новой версии, ориентир, а не окончательный вердикт.
|
||||
- Шаблон собран `/skd-compile` или изменён `/skd-edit` — нарушение означает дефект навыка: сообщи пользователю, а не правь XML вручную молча.
|
||||
- `no schemas for format X — not checked` — предложи `/v8-xsd-fetch -Versions X`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-validate v1.5 — Validate 1C DCS structure
|
||||
# skd-validate v1.6 — Validate 1C DCS structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -1044,6 +1044,151 @@ function Test-XsdPlatformNoise($info) {
|
||||
return $false
|
||||
}
|
||||
|
||||
# --- Подсказка по схеме: что именно исправить ---
|
||||
# Текст валидатора про порядок («недопустимый дочерний X, ожидается A, B») читается как «добавь A»,
|
||||
# а означает «X стоит слишком поздно». Поэтому для нарушений порядка строим свою подсказку: тип
|
||||
# родителя и порядок его детей берём из XSD, сравниваем с соседями нарушителя в документе. Обход
|
||||
# схемы — свой, по XSD-документам (не PSVI .NET): так подсказки портов совпадают.
|
||||
|
||||
function Get-XsdModel([string]$dir) {
|
||||
$m = @{
|
||||
types = New-Object 'System.Collections.Generic.Dictionary[string,object]' ([StringComparer]::Ordinal)
|
||||
elems = New-Object 'System.Collections.Generic.Dictionary[string,object]' ([StringComparer]::Ordinal)
|
||||
orders = New-Object 'System.Collections.Generic.Dictionary[string,object]' ([StringComparer]::Ordinal)
|
||||
}
|
||||
foreach ($f in Get-ChildItem -LiteralPath $dir -Filter *.xsd -File) {
|
||||
$doc = New-Object System.Xml.XmlDocument
|
||||
$doc.Load($f.FullName)
|
||||
$tns = $doc.DocumentElement.GetAttribute("targetNamespace")
|
||||
foreach ($n in $doc.DocumentElement.ChildNodes) {
|
||||
if ($n.NodeType -ne 'Element' -or $n.NamespaceURI -cne 'http://www.w3.org/2001/XMLSchema') { continue }
|
||||
$name = $n.GetAttribute("name")
|
||||
if (-not $name) { continue }
|
||||
if ($n.get_LocalName() -ceq 'complexType' -and -not $m.types.ContainsKey("$tns|$name")) { $m.types["$tns|$name"] = $n }
|
||||
elseif ($n.get_LocalName() -ceq 'element' -and -not $m.elems.ContainsKey("$tns|$name")) { $m.elems["$tns|$name"] = $n }
|
||||
}
|
||||
}
|
||||
return $m
|
||||
}
|
||||
|
||||
# QName из атрибута схемы → "ns|имя" по пространствам имён узла XSD
|
||||
function Resolve-XsdQName($node, [string]$qn) {
|
||||
if (-not $qn) { return $null }
|
||||
$pp = $qn.Split(':')
|
||||
$prefix = if ($pp.Count -gt 1) { $pp[0] } else { '' }
|
||||
return "$($node.GetNamespaceOfPrefix($prefix))|$($pp[-1])"
|
||||
}
|
||||
|
||||
# Дети типа по порядку: @{ name; type; maxOne }. База расширения — первой. Порядок не определён
|
||||
# (choice, all, any, group) или тип неизвестен — $null.
|
||||
function Get-XsdChildOrder($model, [string]$typeKey) {
|
||||
if (-not $typeKey) { return $null }
|
||||
# Список отдаём через «,»: иначе PS развернёт его — пустой станет $null, одиночный — самим элементом
|
||||
if ($model.orders.ContainsKey($typeKey)) { $v = $model.orders[$typeKey]; if ($null -eq $v) { return $null }; return ,$v }
|
||||
$model.orders[$typeKey] = $null # защита от циклов
|
||||
$ct = if ($model.types.ContainsKey($typeKey)) { $model.types[$typeKey] } else { $null }
|
||||
if (-not $ct) { return $null }
|
||||
$list = New-Object System.Collections.ArrayList
|
||||
$holder = $ct
|
||||
foreach ($c in $ct.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
if ($c.get_LocalName() -ceq 'simpleContent') { $model.orders[$typeKey] = $list; return ,$list }
|
||||
if ($c.get_LocalName() -ceq 'complexContent') {
|
||||
$ext = $null
|
||||
foreach ($x in $c.ChildNodes) { if ($x.NodeType -eq 'Element' -and $x.get_LocalName() -ceq 'extension') { $ext = $x } }
|
||||
if (-not $ext) { return $null }
|
||||
$baseKey = Resolve-XsdQName $ext $ext.GetAttribute("base")
|
||||
$base = Get-XsdChildOrder $model $baseKey
|
||||
# База известна, но порядок у неё не определён — не определён и здесь
|
||||
if ($null -eq $base -and $model.types.ContainsKey($baseKey)) { return $null }
|
||||
if ($base) { foreach ($b in $base) { [void]$list.Add($b) } }
|
||||
$holder = $ext
|
||||
}
|
||||
}
|
||||
foreach ($c in $holder.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
$ln = $c.get_LocalName()
|
||||
if ($ln -ceq 'sequence') { if (-not (Add-XsdSequence $model $c $list)) { return $null } }
|
||||
elseif ($ln -ceq 'choice' -or $ln -ceq 'all' -or $ln -ceq 'group' -or $ln -ceq 'any') { return $null }
|
||||
}
|
||||
$model.orders[$typeKey] = $list
|
||||
return ,$list
|
||||
}
|
||||
|
||||
function Add-XsdSequence($model, $seq, $list) {
|
||||
foreach ($c in $seq.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
$ln = $c.get_LocalName()
|
||||
if ($ln -ceq 'element') {
|
||||
$ref = $c.GetAttribute("ref")
|
||||
if ($ref) {
|
||||
$rk = Resolve-XsdQName $c $ref
|
||||
$ge = if ($model.elems.ContainsKey($rk)) { $model.elems[$rk] } else { $null }
|
||||
$name = $rk.Split('|')[-1]
|
||||
$type = if ($ge -and $ge.GetAttribute("type")) { Resolve-XsdQName $ge $ge.GetAttribute("type") } else { $null }
|
||||
} else {
|
||||
$name = $c.GetAttribute("name")
|
||||
$type = if ($c.GetAttribute("type")) { Resolve-XsdQName $c $c.GetAttribute("type") } else { $null }
|
||||
}
|
||||
$mx = $c.GetAttribute("maxOccurs")
|
||||
[void]$list.Add(@{ name = $name; type = $type; maxOne = (-not $mx -or $mx -ceq '1') })
|
||||
} elseif ($ln -ceq 'sequence') {
|
||||
if (-not (Add-XsdSequence $model $c $list)) { return $false }
|
||||
} elseif ($ln -ceq 'annotation') {
|
||||
continue
|
||||
} else {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
# Тип родителя нарушителя: спуск от корня по цепочке предков (@{ name; ns; xsiKey })
|
||||
function Resolve-XsdParentType($model, $ancestors) {
|
||||
if ($ancestors.Count -eq 0) { return $null }
|
||||
$root = $ancestors[0]
|
||||
$rk = "$($root.ns)|$($root.name)"
|
||||
if ($root.xsiKey) { $type = $root.xsiKey }
|
||||
elseif ($model.elems.ContainsKey($rk) -and $model.elems[$rk].GetAttribute("type")) { $type = Resolve-XsdQName $model.elems[$rk] $model.elems[$rk].GetAttribute("type") }
|
||||
elseif ($model.types.ContainsKey($rk)) { $type = $rk }
|
||||
else { return $null }
|
||||
for ($i = 1; $i -lt $ancestors.Count; $i++) {
|
||||
$a = $ancestors[$i]
|
||||
if ($a.xsiKey) { $type = $a.xsiKey; continue }
|
||||
$order = Get-XsdChildOrder $model $type
|
||||
if ($null -eq $order) { return $null }
|
||||
$hit = $null
|
||||
foreach ($o in $order) { if ($o.name -ceq $a.name) { $hit = $o; break } }
|
||||
if (-not $hit -or -not $hit.type) { return $null }
|
||||
$type = $hit.type
|
||||
}
|
||||
return $type
|
||||
}
|
||||
|
||||
function Get-XsdOrderHint($model, $ancestors, [string]$name, $preceding) {
|
||||
$pt = Resolve-XsdParentType $model $ancestors
|
||||
$order = Get-XsdChildOrder $model $pt
|
||||
if ($null -eq $order) { return $null }
|
||||
$names = [string[]]@($order | ForEach-Object { $_.name })
|
||||
$idx = [array]::IndexOf($names, $name)
|
||||
if ($idx -lt 0) { return "not allowed in <$($ancestors[$ancestors.Count - 1].name)>" }
|
||||
if ($order[$idx].maxOne -and ([string[]]@($preceding) -ccontains $name)) { return "duplicate — only one <$name> allowed" }
|
||||
foreach ($sib in $preceding) {
|
||||
if ([array]::IndexOf($names, [string]$sib) -gt $idx) { return "must come before <$sib>" }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# Текст валидатора без пространств имён: остаются имена и значения
|
||||
function Compress-XsdMessage([string]$m) {
|
||||
$m = $m -replace '[\r\n]+', ' '
|
||||
$m = $m -replace '\s(в пространстве имен|in namespace)\s"[^"]*"', ''
|
||||
$m = $m -replace "\s(в пространстве имен|in namespace)\s'[^']*'", ''
|
||||
$m = $m -replace '\{[^}]*\}', ''
|
||||
$m = $m -replace '(["''])https?://[^"'']*:', '$1'
|
||||
return $m.Trim()
|
||||
}
|
||||
|
||||
function Invoke-XsdCheck {
|
||||
$xsdRoot = Resolve-XsdRoot ([System.IO.Path]::GetDirectoryName($resolvedPath))
|
||||
if (-not $xsdRoot) { Report-OK "XSD: no schemas in project — not checked"; return }
|
||||
@@ -1085,16 +1230,24 @@ function Invoke-XsdCheck {
|
||||
$set.XmlResolver = $null
|
||||
foreach ($sc in $schemas) { [void]$set.Add($sc) }
|
||||
try { $set.Compile() } catch { Report-Warn "XSD: schemas in '$useDir' do not compile — not checked: $($_.Exception.Message)"; return }
|
||||
$script:xsdModel = Get-XsdModel $useDir
|
||||
|
||||
# Своя цепочка открытых элементов: в обработчике нужен родитель нарушителя
|
||||
$script:xsdStack = New-Object System.Collections.ArrayList
|
||||
$script:xsdFound = 0
|
||||
# Ошибку атрибута .NET сообщает, стоя на атрибуте: имени элемента-владельца в этот момент нет.
|
||||
# Откладываем и выводим после Read(), когда читатель уже на элементе.
|
||||
$script:xsdPending = New-Object System.Collections.ArrayList
|
||||
$script:xsdTag = if ($exact) { "XSD $useVer" } else { "XSD $useVer (no schemas for $ver)" }
|
||||
$script:xsdExact = $exact
|
||||
$handler = {
|
||||
param($sender, $e)
|
||||
if ($script:stopped) { return }
|
||||
$st = $script:xsdStack
|
||||
if ($sender.NodeType -eq 'Attribute') {
|
||||
[void]$script:xsdPending.Add(@{ line = $e.Exception.LineNumber; text = (Compress-XsdMessage $e.Message) })
|
||||
return
|
||||
}
|
||||
if ($sender.NodeType -eq 'Element') {
|
||||
$xt = $sender.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$info = @{ elem = $sender.LocalName; parent = $(if ($st.Count -gt 0) { $st[$st.Count - 1].name } else { '' }); xsiNs = ''; xsiLocal = '' }
|
||||
@@ -1109,7 +1262,13 @@ function Invoke-XsdCheck {
|
||||
}
|
||||
if (Test-XsdPlatformNoise $info) { return }
|
||||
$script:xsdFound++
|
||||
$msg = "$($script:xsdTag), line $($e.Exception.LineNumber): $($info.parent)/$($info.elem): $($e.Message -replace '[\r\n]+', ' ')"
|
||||
$hint = $null
|
||||
if ($sender.NodeType -eq 'Element') {
|
||||
$prec = if ($st.Count -gt 0) { $st[$st.Count - 1].children } else { @() }
|
||||
$hint = Get-XsdOrderHint $script:xsdModel $st $sender.LocalName $prec
|
||||
}
|
||||
$text = if ($hint) { $hint } else { Compress-XsdMessage $e.Message }
|
||||
$msg = "$($script:xsdTag), line $($e.Exception.LineNumber): $($info.parent)/$($info.elem): $text"
|
||||
if ($script:xsdExact) { Report-Error $msg } else { Report-Warn $msg }
|
||||
}
|
||||
$rs = New-Object System.Xml.XmlReaderSettings
|
||||
@@ -1120,13 +1279,32 @@ function Invoke-XsdCheck {
|
||||
$r = [System.Xml.XmlReader]::Create($resolvedPath, $rs)
|
||||
try {
|
||||
while ($r.Read()) {
|
||||
if ($r.NodeType -eq 'Element' -and -not $r.IsEmptyElement) {
|
||||
if ($r.NodeType -eq 'Element') {
|
||||
# Имя — в дети родителя (и у пустого элемента: он тоже сосед для подсказки)
|
||||
if ($script:xsdStack.Count -gt 0) { [void]$script:xsdStack[$script:xsdStack.Count - 1].children.Add($r.LocalName) }
|
||||
foreach ($pa in $script:xsdPending) {
|
||||
$xt = $r.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$info = @{ elem = $r.LocalName; parent = $(if ($script:xsdStack.Count -gt 0) { $script:xsdStack[$script:xsdStack.Count - 1].name } else { '' }); xsiNs = ''; xsiLocal = '' }
|
||||
if ($xt) {
|
||||
$pp = $xt.Split(':')
|
||||
$info.xsiLocal = $pp[-1]
|
||||
if ($pp.Count -gt 1) { $info.xsiNs = "$($r.LookupNamespace($pp[0]))" }
|
||||
}
|
||||
if (Test-XsdPlatformNoise $info) { continue }
|
||||
$script:xsdFound++
|
||||
$msg = "$($script:xsdTag), line $($pa.line): $($info.parent)/$($info.elem): $($pa.text)"
|
||||
if ($script:xsdExact) { Report-Error $msg } else { Report-Warn $msg }
|
||||
}
|
||||
$script:xsdPending.Clear()
|
||||
if ($script:stopped) { break }
|
||||
if ($r.IsEmptyElement) { continue }
|
||||
$xt = $r.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$entry = @{ name = $r.LocalName; xsiNs = ''; xsiLocal = '' }
|
||||
$entry = @{ name = $r.LocalName; ns = $r.NamespaceURI; xsiNs = ''; xsiLocal = ''; xsiKey = $null; children = New-Object System.Collections.ArrayList }
|
||||
if ($xt) {
|
||||
$pp = $xt.Split(':')
|
||||
$entry.xsiLocal = $pp[-1]
|
||||
if ($pp.Count -gt 1) { $entry.xsiNs = "$($r.LookupNamespace($pp[0]))" }
|
||||
$entry.xsiNs = if ($pp.Count -gt 1) { "$($r.LookupNamespace($pp[0]))" } else { "$($r.LookupNamespace(''))" }
|
||||
$entry.xsiKey = "$($entry.xsiNs)|$($entry.xsiLocal)"
|
||||
}
|
||||
[void]$script:xsdStack.Add($entry)
|
||||
} elseif ($r.NodeType -eq 'EndElement' -and $script:xsdStack.Count -gt 0) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-validate v1.5 — Validate 1C DCS structure (Python port)
|
||||
# skd-validate v1.6 — Validate 1C DCS structure (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
@@ -1111,6 +1111,182 @@ def _xsd_error_element(err):
|
||||
return None
|
||||
|
||||
|
||||
# ── Подсказка по схеме: что именно исправить ──
|
||||
# Текст валидатора про порядок («недопустимый дочерний X, ожидается A, B») читается как «добавь A»,
|
||||
# а означает «X стоит слишком поздно». Поэтому для нарушений порядка строим свою подсказку: тип
|
||||
# родителя и порядок его детей берём из XSD, сравниваем с соседями нарушителя в документе. Обход
|
||||
# схемы — свой, по XSD-документам, как в PS-мастере: так подсказки портов совпадают.
|
||||
|
||||
def xsd_model(vdir):
|
||||
m = {"types": {}, "elems": {}, "orders": {}}
|
||||
for name in sorted(os.listdir(vdir)):
|
||||
fp = os.path.join(vdir, name)
|
||||
if not (name.lower().endswith(".xsd") and os.path.isfile(fp)):
|
||||
continue
|
||||
root_el = etree.parse(fp).getroot()
|
||||
tns = root_el.get("targetNamespace") or ""
|
||||
for n in root_el:
|
||||
if not isinstance(n.tag, str) or etree.QName(n).namespace != XS_NS or not n.get("name"):
|
||||
continue
|
||||
key = f"{tns}|{n.get('name')}"
|
||||
ln = etree.QName(n).localname
|
||||
if ln == "complexType":
|
||||
m["types"].setdefault(key, n)
|
||||
elif ln == "element":
|
||||
m["elems"].setdefault(key, n)
|
||||
return m
|
||||
|
||||
|
||||
def xsd_resolve_qname(node, qn):
|
||||
"""QName из атрибута схемы → "ns|имя" по пространствам имён узла XSD."""
|
||||
if not qn:
|
||||
return None
|
||||
pp = qn.split(":")
|
||||
prefix = pp[0] if len(pp) > 1 else None
|
||||
return f"{node.nsmap.get(prefix) or ''}|{pp[-1]}"
|
||||
|
||||
|
||||
def xsd_child_order(model, type_key):
|
||||
"""Дети типа по порядку: {name, type, maxOne}. База расширения — первой. Порядок не определён
|
||||
(choice, all, any, group) или тип неизвестен — None."""
|
||||
if not type_key:
|
||||
return None
|
||||
if type_key in model["orders"]:
|
||||
return model["orders"][type_key]
|
||||
model["orders"][type_key] = None # защита от циклов
|
||||
ct = model["types"].get(type_key)
|
||||
if ct is None:
|
||||
return None
|
||||
lst = []
|
||||
holder = ct
|
||||
for c in ct:
|
||||
if not isinstance(c.tag, str):
|
||||
continue
|
||||
ln = etree.QName(c).localname
|
||||
if ln == "simpleContent":
|
||||
model["orders"][type_key] = lst
|
||||
return lst
|
||||
if ln == "complexContent":
|
||||
ext = None
|
||||
for x in c:
|
||||
if isinstance(x.tag, str) and etree.QName(x).localname == "extension":
|
||||
ext = x
|
||||
if ext is None:
|
||||
return None
|
||||
base_key = xsd_resolve_qname(ext, ext.get("base"))
|
||||
base = xsd_child_order(model, base_key)
|
||||
# База известна, но порядок у неё не определён — не определён и здесь
|
||||
if base is None and base_key in model["types"]:
|
||||
return None
|
||||
if base:
|
||||
lst.extend(base)
|
||||
holder = ext
|
||||
for c in holder:
|
||||
if not isinstance(c.tag, str):
|
||||
continue
|
||||
ln = etree.QName(c).localname
|
||||
if ln == "sequence":
|
||||
if not _xsd_add_sequence(model, c, lst):
|
||||
return None
|
||||
elif ln in ("choice", "all", "group", "any"):
|
||||
return None
|
||||
model["orders"][type_key] = lst
|
||||
return lst
|
||||
|
||||
|
||||
def _xsd_add_sequence(model, seq, lst):
|
||||
for c in seq:
|
||||
if not isinstance(c.tag, str):
|
||||
continue
|
||||
ln = etree.QName(c).localname
|
||||
if ln == "element":
|
||||
ref = c.get("ref")
|
||||
if ref:
|
||||
rk = xsd_resolve_qname(c, ref)
|
||||
ge = model["elems"].get(rk)
|
||||
name = rk.split("|")[-1]
|
||||
typ = xsd_resolve_qname(ge, ge.get("type")) if ge is not None and ge.get("type") else None
|
||||
else:
|
||||
name = c.get("name")
|
||||
typ = xsd_resolve_qname(c, c.get("type")) if c.get("type") else None
|
||||
mx = c.get("maxOccurs")
|
||||
lst.append({"name": name, "type": typ, "maxOne": (not mx or mx == "1")})
|
||||
elif ln == "sequence":
|
||||
if not _xsd_add_sequence(model, c, lst):
|
||||
return False
|
||||
elif ln == "annotation":
|
||||
continue
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _xsd_ancestor(el):
|
||||
"""Предок для спуска по схеме: {name, ns, xsiKey}."""
|
||||
q = etree.QName(el)
|
||||
e = {"name": q.localname, "ns": q.namespace or "", "xsiKey": None}
|
||||
xt = el.get(XSI_TYPE)
|
||||
if xt:
|
||||
pp = xt.split(":")
|
||||
prefix = pp[0] if len(pp) > 1 else None
|
||||
e["xsiKey"] = f"{el.nsmap.get(prefix) or ''}|{pp[-1]}"
|
||||
return e
|
||||
|
||||
|
||||
def xsd_parent_type(model, ancestors):
|
||||
"""Тип родителя нарушителя: спуск от корня по цепочке предков."""
|
||||
if not ancestors:
|
||||
return None
|
||||
root = ancestors[0]
|
||||
rk = f"{root['ns']}|{root['name']}"
|
||||
if root["xsiKey"]:
|
||||
typ = root["xsiKey"]
|
||||
elif rk in model["elems"] and model["elems"][rk].get("type"):
|
||||
typ = xsd_resolve_qname(model["elems"][rk], model["elems"][rk].get("type"))
|
||||
elif rk in model["types"]:
|
||||
typ = rk
|
||||
else:
|
||||
return None
|
||||
for a in ancestors[1:]:
|
||||
if a["xsiKey"]:
|
||||
typ = a["xsiKey"]
|
||||
continue
|
||||
order = xsd_child_order(model, typ)
|
||||
if order is None:
|
||||
return None
|
||||
hit = next((o for o in order if o["name"] == a["name"]), None)
|
||||
if hit is None or not hit["type"]:
|
||||
return None
|
||||
typ = hit["type"]
|
||||
return typ
|
||||
|
||||
|
||||
def xsd_order_hint(model, ancestors, name, preceding):
|
||||
order = xsd_child_order(model, xsd_parent_type(model, ancestors))
|
||||
if order is None:
|
||||
return None
|
||||
names = [o["name"] for o in order]
|
||||
if name not in names:
|
||||
return f"not allowed in <{ancestors[-1]['name']}>"
|
||||
idx = names.index(name)
|
||||
if order[idx]["maxOne"] and name in preceding:
|
||||
return f"duplicate — only one <{name}> allowed"
|
||||
for sib in preceding:
|
||||
if sib in names and names.index(sib) > idx:
|
||||
return f"must come before <{sib}>"
|
||||
return None
|
||||
|
||||
|
||||
def compress_xsd_message(m):
|
||||
"""Текст валидатора без пространств имён: остаются имена и значения."""
|
||||
m = re.sub(r'[\r\n]+', ' ', m)
|
||||
m = re.sub(r'\s(в пространстве имен|in namespace)\s"[^"]*"', '', m)
|
||||
m = re.sub(r"\s(в пространстве имен|in namespace)\s'[^']*'", '', m)
|
||||
m = re.sub(r'\{[^}]*\}', '', m)
|
||||
m = re.sub(r'(["\'])https?://[^"\']*:', r'\1', m)
|
||||
return ' '.join(m.split())
|
||||
|
||||
|
||||
def xsd_check():
|
||||
xsd_root = resolve_xsd_root(os.path.dirname(resolved_path))
|
||||
if not xsd_root:
|
||||
@@ -1133,6 +1309,7 @@ def xsd_check():
|
||||
use_ver = cands[0]
|
||||
exact = use_ver == ver
|
||||
|
||||
model = xsd_model(os.path.join(xsd_root, use_ver))
|
||||
tmp = tempfile.mkdtemp(prefix="skd-xsd-")
|
||||
try:
|
||||
try:
|
||||
@@ -1154,7 +1331,15 @@ def xsd_check():
|
||||
if xsd_platform_noise(info):
|
||||
continue
|
||||
found += 1
|
||||
msg = f"{tag}, line {err.line}: {info['parent']}/{info['elem']}: {' '.join(err.message.split())}"
|
||||
hint = None
|
||||
# «Лишний/не на месте» у libxml2 — «This element is not expected» (текст не локализуется)
|
||||
if el is not None and "This element is not expected" in err.message:
|
||||
ancestors = [_xsd_ancestor(a) for a in reversed(list(el.iterancestors()))]
|
||||
preceding = [etree.QName(x).localname for x in reversed(list(el.itersiblings(preceding=True)))
|
||||
if isinstance(x.tag, str)]
|
||||
hint = xsd_order_hint(model, ancestors, etree.QName(el).localname, preceding)
|
||||
text = hint or compress_xsd_message(err.message)
|
||||
msg = f"{tag}, line {err.line}: {info['parent']}/{info['elem']}: {text}"
|
||||
if exact:
|
||||
report_error(msg)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" version="2.20">
|
||||
<Configuration uuid="00000000-0000-0000-0000-000000000001"/>
|
||||
</MetaDataObject>
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<dataSource>
|
||||
<name>ИсточникДанных1</name>
|
||||
<dataSourceType>Local</dataSourceType>
|
||||
</dataSource>
|
||||
<dataSet xsi:type="DataSetQuery">
|
||||
<name>НаборДанных1</name>
|
||||
<field xsi:type="DataSetFieldField">
|
||||
<dataPath>Число</dataPath>
|
||||
<field>Число</field>
|
||||
<presentationExpression>Формат(Число, "ЧГ=")</presentationExpression>
|
||||
<presentationExpression>Формат(Число, "ЧГ=")</presentationExpression>
|
||||
<valueType>
|
||||
<v8:Type>xs:decimal</v8:Type>
|
||||
<v8:NumberQualifiers>
|
||||
<v8:Digits>10</v8:Digits>
|
||||
<v8:FractionDigits>0</v8:FractionDigits>
|
||||
<v8:AllowedSign>Any</v8:AllowedSign>
|
||||
</v8:NumberQualifiers>
|
||||
</valueType>
|
||||
</field>
|
||||
<field xsi:type="DataSetFieldField">
|
||||
<dataPath>Флаг</dataPath>
|
||||
<field>Флаг</field>
|
||||
<fooBar>x</fooBar>
|
||||
<valueType>
|
||||
<v8:Type>xs:boolean</v8:Type>
|
||||
</valueType>
|
||||
</field>
|
||||
<dataSource>ИсточникДанных1</dataSource>
|
||||
<query>ВЫБРАТЬ 1 КАК Число, ИСТИНА КАК Флаг</query>
|
||||
</dataSet>
|
||||
<calculatedField>
|
||||
<dataPath>Удвоенное</dataPath>
|
||||
<expression>Число * 2</expression>
|
||||
<useRestriction>
|
||||
<group>true</group>
|
||||
</useRestriction>
|
||||
<valueType>
|
||||
<v8:Type>xs:decimal</v8:Type>
|
||||
<v8:NumberQualifiers>
|
||||
<v8:Digits>10</v8:Digits>
|
||||
<v8:FractionDigits>0</v8:FractionDigits>
|
||||
<v8:AllowedSign>Any</v8:AllowedSign>
|
||||
</v8:NumberQualifiers>
|
||||
</valueType>
|
||||
</calculatedField>
|
||||
<parameter>
|
||||
<name>П</name>
|
||||
<valueType>
|
||||
<v8:Type>xs:boolean</v8:Type>
|
||||
</valueType>
|
||||
<value xsi:type="xs:boolean">false</value>
|
||||
<useRestriction>false</useRestriction>
|
||||
</parameter>
|
||||
<settingsVariant>
|
||||
<dcsset:name>Основной</dcsset:name>
|
||||
<dcsset:presentation xsi:type="v8:LocalStringType">
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>Основной</v8:content>
|
||||
</v8:item>
|
||||
</dcsset:presentation>
|
||||
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
|
||||
<dcsset:selection>
|
||||
<dcsset:item xsi:type="dcsset:SelectedItemField">
|
||||
<dcsset:field>Число</dcsset:field>
|
||||
</dcsset:item>
|
||||
</dcsset:selection>
|
||||
<dcsset:filter>
|
||||
<dcsset:item xsi:type="dcsset:FilterItemComparison">
|
||||
<dcsset:left xsi:type="dcscor:Field">Флаг</dcsset:left>
|
||||
<dcsset:comparisonType>Equal</dcsset:comparisonType>
|
||||
<dcsset:right xsi:type="xs:boolean">true</dcsset:right>
|
||||
</dcsset:item>
|
||||
</dcsset:filter>
|
||||
<dcsset:dataParameters>
|
||||
<dcscor:item xsi:type="dcsset:SettingsParameterValue">
|
||||
<dcscor:parameter>П</dcscor:parameter>
|
||||
<dcscor:value xsi:type="xs:boolean">false</dcscor:value>
|
||||
<dcsset:userSettingID>0eb70b37-3a01-40a0-81c1-98028ef06a05</dcsset:userSettingID>
|
||||
</dcscor:item>
|
||||
</dcsset:dataParameters>
|
||||
<dcsset:outputParameters>
|
||||
<dcscor:item xsi:type="dcsset:SettingsParameterValue">
|
||||
<dcscor:parameter>Заголовок</dcscor:parameter>
|
||||
<dcscor:value xsi:type="v8:LocalStringType">
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>Пример</v8:content>
|
||||
</v8:item>
|
||||
</dcscor:value>
|
||||
</dcscor:item>
|
||||
</dcsset:outputParameters>
|
||||
<dcsset:item xsi:type="dcsset:StructureItemGroup">
|
||||
<dcsset:order>
|
||||
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
|
||||
</dcsset:order>
|
||||
<dcsset:selection>
|
||||
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
|
||||
</dcsset:selection>
|
||||
</dcsset:item>
|
||||
</dcsset:settings>
|
||||
</settingsVariant>
|
||||
</DataCompositionSchema>
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "XSD точной версии: нарушенный порядок и False — ошибки с местом нарушения",
|
||||
"name": "XSD точной версии: подсказки по порядку и сжатый текст без пространств имён",
|
||||
"setup": "fixture:xsd-bad-220",
|
||||
"requires": ".v8-xsd/2.20",
|
||||
"preRun": [
|
||||
@@ -18,11 +18,12 @@
|
||||
"expectError": true,
|
||||
"expect": {
|
||||
"stdoutContains": [
|
||||
"[ERROR] XSD 2.20, line 20: field/presentationExpression",
|
||||
"[ERROR] XSD 2.20, line 43: calculatedField/useRestriction",
|
||||
"[ERROR] XSD 2.20, line 52: parameter/value",
|
||||
"[ERROR] XSD 2.20, line 87: settings/dataParameters"
|
||||
]
|
||||
"[ERROR] XSD 2.20, line 20: field/presentationExpression: must come before <valueType>",
|
||||
"[ERROR] XSD 2.20, line 43: calculatedField/useRestriction: must come before <valueType>",
|
||||
"[ERROR] XSD 2.20, line 52: parameter/value:",
|
||||
"[ERROR] XSD 2.20, line 87: settings/dataParameters: must come before <outputParameters>"
|
||||
],
|
||||
"stdoutNotContains": "http://v8.1c.ru"
|
||||
},
|
||||
"noSnapshot": "результат — только вывод; схемы 1С не должны попасть в эталон"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "Подсказки XSD: повтор элемента и элемент, которого нет в модели родителя",
|
||||
"setup": "fixture:xsd-hints-220",
|
||||
"requires": ".v8-xsd/2.20",
|
||||
"preRun": [{ "writeFile": { "path": ".v8-project.json", "content": { "xsdPath": "{repoRoot}/.v8-xsd" } } }],
|
||||
"params": { "templatePath": "Reports/Отчет/Templates/Схема/Ext/Template.xml" },
|
||||
"expectError": true,
|
||||
"expect": {
|
||||
"stdoutContains": [
|
||||
"field/presentationExpression: duplicate — only one <presentationExpression> allowed",
|
||||
"field/fooBar: not allowed in <field>"
|
||||
]
|
||||
},
|
||||
"noSnapshot": "результат — только вывод; схемы 1С не должны попасть в эталон"
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"expect": {
|
||||
"stdoutContains": [
|
||||
"[WARN] XSD 2.12 (no schemas for 2.11), line 20: field/presentationExpression"
|
||||
"[WARN] XSD 2.12 (no schemas for 2.11), line 20: field/presentationExpression: must come before <valueType>"
|
||||
],
|
||||
"stdoutNotContains": "[ERROR]"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user