mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-10-01 06:25:52 +03:00
feat(skd-validate): проверка схемы компоновки по XSD платформы (#101)
Если в проекте есть схемы /v8-xsd-fetch, skd-validate дополнительно
проверяет порядок элементов, типы и значения по XSD версии формата
выгрузки (штамп Configuration.xml или корня внешнего отчёта). Схемы
точной версии — ошибки, только более новой — предупреждения, иначе
проверка пропускается. Места, которые платформа пишет иначе, чем
описывает XSD, отсекаются по месту нарушения: на 2606 схемах компоновки
выгрузок БП, УНФ и ERP (форматы 2.17–2.21) — ни одного срабатывания в
обоих портах.
Runner: поле кейса requires (путь от корня репозитория, нет — skipped)
и плейсхолдер {repoRoot} — кейсы на настоящих схемах, которые в
репозиторий не входят.
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
9293305a12
commit
5465a7803c
@@ -12,6 +12,8 @@ allowed-tools:
|
||||
|
||||
Проверяет структурную корректность Template.xml схемы компоновки данных. Выявляет ошибки формата, битые ссылки, дубликаты имён.
|
||||
|
||||
Если в проекте есть XSD-схемы платформы (`/v8-xsd-fetch`), дополнительно проверяет порядок элементов, типы и значения по схеме версии формата выгрузки.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-validate v1.4 — Validate 1C DCS structure
|
||||
# skd-validate v1.5 — Validate 1C DCS structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -926,6 +926,222 @@ if ($vChecked -gt 0 -and $vOk) {
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- 18. XSD (schemas of the platform, /v8-xsd-fetch) ---
|
||||
# Порядок элементов, типы и значения — по XSD платформы, если они есть в проекте. Нет схем —
|
||||
# нет проверки. Схемы XDTO совпадают с выгрузкой конфигуратора только в СКД, и то кроме
|
||||
# нескольких мест, которые платформа пишет иначе, — они отсекаются ниже (Test-XsdPlatformNoise).
|
||||
|
||||
# Копия общего эталона (семья find_v8project, авторитет — cf-edit).
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# Штамп версии формата — атрибут version КОРНЕВОГО элемента файла. Именно корневого: в Form.xml
|
||||
# расширения ниже стоит <BaseForm version=…>. Читается только заголовок, без разбора файла.
|
||||
function Get-RootVersion([string]$xmlPath) {
|
||||
if (-not (Test-Path -LiteralPath $xmlPath -PathType Leaf)) { return $null }
|
||||
$buf = New-Object byte[] 4096
|
||||
$fs = [System.IO.File]::OpenRead($xmlPath)
|
||||
try { $len = $fs.Read($buf, 0, $buf.Length) } finally { $fs.Dispose() }
|
||||
$head = [System.Text.Encoding]::UTF8.GetString($buf, 0, $len)
|
||||
$m = [regex]::Match($head, '<[A-Za-z_][\w.:-]*(\s[^>]*)?/?>')
|
||||
if (-not $m.Success) { return $null }
|
||||
$v = [regex]::Match($m.Groups[1].Value, '(?:^|\s)version="([^"]*)"')
|
||||
if ($v.Success) { return $v.Groups[1].Value }
|
||||
return $null
|
||||
}
|
||||
|
||||
# Корень автономной внешней обработки/отчёта. Копия общего эталона (семья is_external_root,
|
||||
# авторитет — cf-edit).
|
||||
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $false }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Якорь выгрузки, в чьём дереве лежит файл: корень автономной EPF/ERF либо Configuration.xml,
|
||||
# ближайший вверх. Корень обработки проверяется первым — иначе обработка, лежащая в дереве
|
||||
# конфигурации, сверялась бы с конфигурацией. Нет якоря — сверять не с чем.
|
||||
function Find-DumpAnchor([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 15 -and $d; $i++) {
|
||||
if (Test-ExternalObjectRoot "$d.xml") { return "$d.xml" }
|
||||
$cfg = Join-Path $d "Configuration.xml"
|
||||
if (Test-Path $cfg) { return $cfg }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if (-not $parent -or $parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-FormatRank([string]$ver) {
|
||||
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||
return 0
|
||||
}
|
||||
|
||||
$xsdDcsNs = "http://v8.1c.ru/8.1/data-composition-system/schema"
|
||||
|
||||
# targetNamespace схемы — из заголовка файла, без разбора
|
||||
function Get-XsdTargetNs([string]$path) {
|
||||
$buf = New-Object byte[] 4096
|
||||
$fs = [System.IO.File]::OpenRead($path)
|
||||
try { $len = $fs.Read($buf, 0, $buf.Length) } finally { $fs.Dispose() }
|
||||
$m = [regex]::Match([System.Text.Encoding]::UTF8.GetString($buf, 0, $len), 'targetNamespace="([^"]*)"')
|
||||
if ($m.Success) { return $m.Groups[1].Value }
|
||||
return $null
|
||||
}
|
||||
|
||||
# Каталог версии годится, если в нём есть схема СКД (в 2.10–2.11 её нет)
|
||||
function Test-XsdVersionDir([string]$dir) {
|
||||
foreach ($f in Get-ChildItem -LiteralPath $dir -Filter *.xsd -File -ErrorAction SilentlyContinue) {
|
||||
if ((Get-XsdTargetNs $f.FullName) -ceq $xsdDcsNs) { return $true }
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Каталог схем: .v8-project.json — сначала от шаблона (проект, которому он принадлежит), потом
|
||||
# от текущего каталога; xsdPath из него, иначе .v8-xsd рядом с ним. Нет проекта — нет схем.
|
||||
function Resolve-XsdRoot([string]$templateDir) {
|
||||
$pj = Find-V8Project $templateDir
|
||||
if (-not $pj) { $pj = Find-V8Project (Get-Location).Path }
|
||||
if (-not $pj) { return $null }
|
||||
$pjDir = [System.IO.Path]::GetDirectoryName($pj)
|
||||
$rel = ".v8-xsd"
|
||||
try {
|
||||
$cfg = Get-Content -LiteralPath $pj -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($cfg.xsdPath) { $rel = "$($cfg.xsdPath)" }
|
||||
} catch {}
|
||||
$dir = if ([System.IO.Path]::IsPathRooted($rel)) { $rel } else { Join-Path $pjDir $rel }
|
||||
if (Test-Path -LiteralPath $dir -PathType Container) { return $dir }
|
||||
return $null
|
||||
}
|
||||
|
||||
# Места, которые платформа пишет не так, как описывает XSD, — сверено на всех схемах компоновки
|
||||
# выгрузок БП и УНФ. Правило — по месту (элемент, родитель, xsi:type), не по тексту: текст
|
||||
# зависит от движка и языка.
|
||||
function Test-XsdPlatformNoise($info) {
|
||||
$e = $info.elem; $p = $info.parent
|
||||
if (($p -ceq 'groupTemplate' -or $p -ceq 'groupHeaderTemplate') -and ($e -ceq 'templateType' -or $e -ceq 'groupName')) { return $true }
|
||||
if ($p -ceq 'right' -and $e -ceq 'lastId') { return $true }
|
||||
if ($p -ceq 'nestedSchema' -and $e -ceq 'schema') { return $true }
|
||||
if ($info.xsiNs -ceq 'http://v8.1c.ru/8.2/data/chart') { return $true }
|
||||
# Пустое выражение параметра макета платформа не пишет, схема требует
|
||||
if ($e -ceq 'parameter' -and $info.xsiLocal -ceq 'ExpressionAreaTemplateParameter') { return $true }
|
||||
return $false
|
||||
}
|
||||
|
||||
function Invoke-XsdCheck {
|
||||
$xsdRoot = Resolve-XsdRoot ([System.IO.Path]::GetDirectoryName($resolvedPath))
|
||||
if (-not $xsdRoot) { Report-OK "XSD: no schemas in project — not checked"; return }
|
||||
$anchor = Find-DumpAnchor ([System.IO.Path]::GetDirectoryName($resolvedPath))
|
||||
$ver = if ($anchor) { Get-RootVersion $anchor } else { $null }
|
||||
if (-not $ver -or (Get-FormatRank $ver) -eq 0) { Report-OK "XSD: format version unknown (template outside a dump) — not checked"; return }
|
||||
|
||||
# Точная версия — ошибки; ближайшая более новая — предупреждения; только старше — пропуск
|
||||
$cands = @(Get-ChildItem -LiteralPath $xsdRoot -Directory |
|
||||
Where-Object { $_.Name -match '^\d+\.\d+$' -and (Get-FormatRank $_.Name) -ge (Get-FormatRank $ver) } |
|
||||
Sort-Object { Get-FormatRank $_.Name } |
|
||||
Where-Object { Test-XsdVersionDir $_.FullName })
|
||||
if ($cands.Count -eq 0) { Report-Warn "XSD: no schemas for format $ver in '$xsdRoot' — not checked"; return }
|
||||
$useDir = $cands[0].FullName
|
||||
$useVer = $cands[0].Name
|
||||
$exact = ($useVer -eq $ver)
|
||||
|
||||
# Набор схем. У import нет schemaLocation — связь по namespace, поэтому в набор кладём все.
|
||||
# Корень DataCompositionSchema в схеме объявлен как dataCompositionSchema (строчная) —
|
||||
# дописываем элемент в саму схему СКД: вторую схему того же namespace без SourceUri
|
||||
# XmlSchemaSet молча отбрасывает.
|
||||
$schemas = @()
|
||||
foreach ($f in Get-ChildItem -LiteralPath $useDir -Filter *.xsd -File) {
|
||||
$fs = [System.IO.File]::OpenRead($f.FullName)
|
||||
try { $schemas += [System.Xml.Schema.XmlSchema]::Read($fs, $null) } finally { $fs.Dispose() }
|
||||
}
|
||||
$dcs = $schemas | Where-Object { $_.TargetNamespace -ceq $xsdDcsNs } | Select-Object -First 1
|
||||
$hasRoot = $false
|
||||
foreach ($it in $dcs.Items) {
|
||||
if ($it -is [System.Xml.Schema.XmlSchemaElement] -and $it.Name -ceq 'DataCompositionSchema') { $hasRoot = $true }
|
||||
}
|
||||
if (-not $hasRoot) {
|
||||
$el = New-Object System.Xml.Schema.XmlSchemaElement
|
||||
$el.Name = 'DataCompositionSchema'
|
||||
$el.SchemaTypeName = New-Object System.Xml.XmlQualifiedName('DataCompositionSchema', $xsdDcsNs)
|
||||
[void]$dcs.Items.Add($el)
|
||||
}
|
||||
$set = New-Object System.Xml.Schema.XmlSchemaSet
|
||||
$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:xsdStack = New-Object System.Collections.ArrayList
|
||||
$script:xsdFound = 0
|
||||
$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 '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 = '' }
|
||||
if ($xt) {
|
||||
$pp = $xt.Split(':')
|
||||
$info.xsiLocal = $pp[-1]
|
||||
if ($pp.Count -gt 1) { $info.xsiNs = "$($sender.LookupNamespace($pp[0]))" }
|
||||
}
|
||||
} else {
|
||||
$top = if ($st.Count -gt 0) { $st[$st.Count - 1] } else { @{ name = ''; xsiNs = ''; xsiLocal = '' } }
|
||||
$info = @{ elem = $top.name; parent = $(if ($st.Count -gt 1) { $st[$st.Count - 2].name } else { '' }); xsiNs = $top.xsiNs; xsiLocal = $top.xsiLocal }
|
||||
}
|
||||
if (Test-XsdPlatformNoise $info) { return }
|
||||
$script:xsdFound++
|
||||
$msg = "$($script:xsdTag), line $($e.Exception.LineNumber): $($info.parent)/$($info.elem): $($e.Message -replace '[\r\n]+', ' ')"
|
||||
if ($script:xsdExact) { Report-Error $msg } else { Report-Warn $msg }
|
||||
}
|
||||
$rs = New-Object System.Xml.XmlReaderSettings
|
||||
$rs.ValidationType = 'Schema'
|
||||
$rs.Schemas = $set
|
||||
$rs.DtdProcessing = 'Prohibit'
|
||||
$rs.add_ValidationEventHandler($handler)
|
||||
$r = [System.Xml.XmlReader]::Create($resolvedPath, $rs)
|
||||
try {
|
||||
while ($r.Read()) {
|
||||
if ($r.NodeType -eq 'Element' -and -not $r.IsEmptyElement) {
|
||||
$xt = $r.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$entry = @{ name = $r.LocalName; xsiNs = ''; xsiLocal = '' }
|
||||
if ($xt) {
|
||||
$pp = $xt.Split(':')
|
||||
$entry.xsiLocal = $pp[-1]
|
||||
if ($pp.Count -gt 1) { $entry.xsiNs = "$($r.LookupNamespace($pp[0]))" }
|
||||
}
|
||||
[void]$script:xsdStack.Add($entry)
|
||||
} elseif ($r.NodeType -eq 'EndElement' -and $script:xsdStack.Count -gt 0) {
|
||||
$script:xsdStack.RemoveAt($script:xsdStack.Count - 1)
|
||||
}
|
||||
if ($script:stopped) { break }
|
||||
}
|
||||
} finally { $r.Dispose() }
|
||||
if ($script:xsdFound -eq 0) { Report-OK "$($script:xsdTag): order, types and values OK" }
|
||||
}
|
||||
|
||||
Invoke-XsdCheck
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- Final output ---
|
||||
|
||||
& $finalize
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-validate v1.4 — Validate 1C DCS structure (Python port)
|
||||
# skd-validate v1.5 — Validate 1C DCS structure (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
@@ -881,6 +881,290 @@ for vn in value_nodes:
|
||||
if v_checked > 0 and v_ok:
|
||||
report_ok(f"{v_checked} <value> element(s) with xsi:type: content OK")
|
||||
|
||||
if stopped:
|
||||
finalize()
|
||||
sys.exit(1)
|
||||
|
||||
# ── 18. XSD (schemas of the platform, /v8-xsd-fetch) ──────────
|
||||
# Порядок элементов, типы и значения — по XSD платформы, если они есть в проекте. Нет схем —
|
||||
# нет проверки. Схемы XDTO совпадают с выгрузкой конфигуратора только в СКД, и то кроме
|
||||
# нескольких мест, которые платформа пишет иначе, — они отсекаются ниже (xsd_platform_noise).
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
# Штамп версии формата — атрибут version КОРНЕВОГО элемента файла. Именно корневого: в Form.xml
|
||||
# расширения ниже стоит <BaseForm version=…>. Читается только заголовок, без разбора файла.
|
||||
def root_version(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
with open(xml_path, "rb") as f:
|
||||
head = f.read(4096).decode("utf-8", errors="ignore")
|
||||
m = re.search(r'<[A-Za-z_][\w.:-]*(\s[^>]*)?/?>', head)
|
||||
if not m:
|
||||
return None
|
||||
v = re.search(r'(?:^|\s)version="([^"]*)"', m.group(1) or "")
|
||||
if v:
|
||||
return v.group(1)
|
||||
return None
|
||||
|
||||
|
||||
# Корень автономной внешней обработки/отчёта. Копия общего эталона (семья is_external_root,
|
||||
# авторитет — cf-edit).
|
||||
def _sg_is_external_root(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return False
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str):
|
||||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
# Якорь выгрузки, в чьём дереве лежит файл: корень автономной EPF/ERF либо Configuration.xml,
|
||||
# ближайший вверх. Корень обработки проверяется первым — иначе обработка, лежащая в дереве
|
||||
# конфигурации, сверялась бы с конфигурацией. Нет якоря — сверять не с чем.
|
||||
def find_dump_anchor(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(15):
|
||||
if not d:
|
||||
break
|
||||
if _sg_is_external_root(d + ".xml"):
|
||||
return d + ".xml"
|
||||
cfg = os.path.join(d, "Configuration.xml")
|
||||
if os.path.exists(cfg):
|
||||
return cfg
|
||||
parent = os.path.dirname(d)
|
||||
if not parent or parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def format_rank(ver):
|
||||
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||
|
||||
|
||||
XSD_DCS_NS = "http://v8.1c.ru/8.1/data-composition-system/schema"
|
||||
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
||||
|
||||
|
||||
def xsd_target_ns(path):
|
||||
"""targetNamespace схемы — из заголовка файла, без разбора."""
|
||||
with open(path, "rb") as f:
|
||||
head = f.read(4096).decode("utf-8", errors="ignore")
|
||||
m = re.search(r'targetNamespace="([^"]*)"', head)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def xsd_index(vdir):
|
||||
"""namespace → файл схемы в каталоге версии."""
|
||||
idx = {}
|
||||
for name in sorted(os.listdir(vdir)):
|
||||
fp = os.path.join(vdir, name)
|
||||
if name.lower().endswith(".xsd") and os.path.isfile(fp):
|
||||
ns = xsd_target_ns(fp)
|
||||
if ns and ns not in idx:
|
||||
idx[ns] = fp
|
||||
return idx
|
||||
|
||||
|
||||
def resolve_xsd_root(template_dir):
|
||||
"""Каталог схем: .v8-project.json — сначала от шаблона (проект, которому он принадлежит),
|
||||
потом от текущего каталога; xsdPath из него, иначе .v8-xsd рядом с ним. Нет проекта — нет схем."""
|
||||
pj = _sg_find_v8project(template_dir) or _sg_find_v8project(os.getcwd())
|
||||
if not pj:
|
||||
return None
|
||||
pj_dir = os.path.dirname(pj)
|
||||
rel = ".v8-xsd"
|
||||
try:
|
||||
with open(pj, encoding="utf-8-sig") as f:
|
||||
cfg = json.load(f)
|
||||
if isinstance(cfg, dict) and cfg.get("xsdPath"):
|
||||
rel = str(cfg["xsdPath"])
|
||||
except Exception:
|
||||
pass
|
||||
d = rel if os.path.isabs(rel) else os.path.join(pj_dir, rel)
|
||||
return d if os.path.isdir(d) else None
|
||||
|
||||
|
||||
def xsd_platform_noise(info):
|
||||
"""Места, которые платформа пишет не так, как описывает XSD, — сверено на всех схемах
|
||||
компоновки выгрузок БП и УНФ. Правило — по месту (элемент, родитель, xsi:type), не по тексту:
|
||||
текст зависит от движка и языка."""
|
||||
e, p = info["elem"], info["parent"]
|
||||
if p in ("groupTemplate", "groupHeaderTemplate") and e in ("templateType", "groupName"):
|
||||
return True
|
||||
if p == "right" and e == "lastId":
|
||||
return True
|
||||
if p == "nestedSchema" and e == "schema":
|
||||
return True
|
||||
if info["xsiNs"] == "http://v8.1c.ru/8.2/data/chart":
|
||||
return True
|
||||
# Пустое выражение параметра макета платформа не пишет, схема требует
|
||||
if e == "parameter" and info["xsiLocal"] == "ExpressionAreaTemplateParameter":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _xsd_build_schema(vdir, tmp):
|
||||
"""Схема для lxml. У import в схемах платформы нет schemaLocation, а libxml2 сам по namespace
|
||||
не ищет, — корневая схема импортирует замыкание зависимостей СКД с путями, листья первыми.
|
||||
Корень DataCompositionSchema в схеме объявлен как dataCompositionSchema (строчная) — схему
|
||||
СКД подключаем через обёртку: xs:include настоящего файла плюс элемент с нужным именем."""
|
||||
idx = xsd_index(vdir)
|
||||
order, seen = [], set()
|
||||
|
||||
def visit(ns):
|
||||
if ns in seen or ns not in idx:
|
||||
return
|
||||
seen.add(ns)
|
||||
doc = etree.parse(idx[ns])
|
||||
for imp in doc.getroot().iter("{%s}import" % XS_NS):
|
||||
visit(imp.get("namespace"))
|
||||
order.append(ns)
|
||||
|
||||
# Сначала замыкание СКД, затем остальные схемы каталога: значения в выгрузке ссылаются через
|
||||
# xsi:type на типы, которые схема СКД не импортирует (data/enterprise: LinkedValueChangeMode,
|
||||
# FoldersAndItemsUse …) — как и в PS-мастере, где в набор кладутся все схемы.
|
||||
visit(XSD_DCS_NS)
|
||||
for ns in idx:
|
||||
visit(ns)
|
||||
dcs_doc = etree.parse(idx[XSD_DCS_NS])
|
||||
has_root = any(el.get("name") == "DataCompositionSchema"
|
||||
for el in dcs_doc.getroot().findall("{%s}element" % XS_NS))
|
||||
locations = {ns: Path(idx[ns]).as_uri() for ns in order}
|
||||
if not has_root:
|
||||
wrapper = os.path.join(tmp, "dcs-root.xsd")
|
||||
with open(wrapper, "w", encoding="utf-8") as f:
|
||||
f.write('<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<xs:schema xmlns:xs="%s" xmlns:tns="%s" targetNamespace="%s" elementFormDefault="qualified">\n'
|
||||
' <xs:include schemaLocation="%s"/>\n'
|
||||
' <xs:element name="DataCompositionSchema" type="tns:DataCompositionSchema"/>\n'
|
||||
'</xs:schema>\n' % (XS_NS, XSD_DCS_NS, XSD_DCS_NS, locations[XSD_DCS_NS]))
|
||||
locations[XSD_DCS_NS] = Path(wrapper).as_uri()
|
||||
root = os.path.join(tmp, "root.xsd")
|
||||
with open(root, "w", encoding="utf-8") as f:
|
||||
f.write('<?xml version="1.0" encoding="UTF-8"?>\n<xs:schema xmlns:xs="%s" targetNamespace="urn:v8-xsd-root">\n' % XS_NS)
|
||||
for ns in order:
|
||||
f.write(' <xs:import namespace="%s" schemaLocation="%s"/>\n' % (ns, locations[ns]))
|
||||
f.write('</xs:schema>\n')
|
||||
return etree.XMLSchema(etree.parse(root))
|
||||
|
||||
|
||||
def _xsd_elem_info(el):
|
||||
if el is None or not isinstance(el.tag, str):
|
||||
return {"elem": "", "parent": "", "xsiNs": "", "xsiLocal": ""}
|
||||
par = el.getparent()
|
||||
info = {"elem": etree.QName(el).localname,
|
||||
"parent": etree.QName(par).localname if par is not None and isinstance(par.tag, str) else "",
|
||||
"xsiNs": "", "xsiLocal": ""}
|
||||
xt = el.get(XSI_TYPE)
|
||||
if xt:
|
||||
pp = xt.split(":")
|
||||
info["xsiLocal"] = pp[-1]
|
||||
if len(pp) > 1:
|
||||
info["xsiNs"] = el.nsmap.get(pp[0]) or ""
|
||||
return info
|
||||
|
||||
|
||||
def _xsd_error_element(err):
|
||||
"""Элемент нарушения по error.path. Путь несёт префиксы документа (dcsset:settings) — xpath
|
||||
получает их из корня; префикс, объявленный глубже, не разрешится — тогда ищем по строке и
|
||||
имени последнего шага."""
|
||||
if not err.path:
|
||||
return None
|
||||
ns = {k: v for k, v in tree.getroot().nsmap.items() if k}
|
||||
try:
|
||||
hits = tree.xpath(err.path, namespaces=ns)
|
||||
if hits and isinstance(hits[0], etree._Element):
|
||||
return hits[0]
|
||||
except Exception:
|
||||
pass
|
||||
last = re.sub(r'\[\d+\]$', '', err.path.rsplit('/', 1)[-1]).split(':')[-1]
|
||||
for el in tree.iter():
|
||||
if isinstance(el.tag, str) and el.sourceline == err.line and etree.QName(el).localname == last:
|
||||
return el
|
||||
return None
|
||||
|
||||
|
||||
def xsd_check():
|
||||
xsd_root = resolve_xsd_root(os.path.dirname(resolved_path))
|
||||
if not xsd_root:
|
||||
report_ok("XSD: no schemas in project — not checked")
|
||||
return
|
||||
anchor = find_dump_anchor(os.path.dirname(resolved_path))
|
||||
ver = root_version(anchor) if anchor else None
|
||||
if not ver or format_rank(ver) == 0:
|
||||
report_ok("XSD: format version unknown (template outside a dump) — not checked")
|
||||
return
|
||||
|
||||
# Точная версия — ошибки; ближайшая более новая — предупреждения; только старше — пропуск
|
||||
cands = sorted((d for d in os.listdir(xsd_root)
|
||||
if re.match(r'^\d+\.\d+$', d) and os.path.isdir(os.path.join(xsd_root, d))
|
||||
and format_rank(d) >= format_rank(ver)), key=format_rank)
|
||||
cands = [d for d in cands if XSD_DCS_NS in xsd_index(os.path.join(xsd_root, d))]
|
||||
if not cands:
|
||||
report_warn(f"XSD: no schemas for format {ver} in '{xsd_root}' — not checked")
|
||||
return
|
||||
use_ver = cands[0]
|
||||
exact = use_ver == ver
|
||||
|
||||
tmp = tempfile.mkdtemp(prefix="skd-xsd-")
|
||||
try:
|
||||
try:
|
||||
schema = _xsd_build_schema(os.path.join(xsd_root, use_ver), tmp)
|
||||
except Exception as ex:
|
||||
report_warn(f"XSD: schemas in '{os.path.join(xsd_root, use_ver)}' do not compile — not checked: {ex}")
|
||||
return
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
tag = f"XSD {use_ver}" if exact else f"XSD {use_ver} (no schemas for {ver})"
|
||||
schema.validate(tree)
|
||||
found = 0
|
||||
for err in schema.error_log:
|
||||
if stopped:
|
||||
break
|
||||
el = _xsd_error_element(err)
|
||||
info = _xsd_elem_info(el)
|
||||
if xsd_platform_noise(info):
|
||||
continue
|
||||
found += 1
|
||||
msg = f"{tag}, line {err.line}: {info['parent']}/{info['elem']}: {' '.join(err.message.split())}"
|
||||
if exact:
|
||||
report_error(msg)
|
||||
else:
|
||||
report_warn(msg)
|
||||
if found == 0:
|
||||
report_ok(f"{tag}: order, types and values OK")
|
||||
|
||||
|
||||
xsd_check()
|
||||
|
||||
if stopped:
|
||||
finalize()
|
||||
sys.exit(1)
|
||||
|
||||
Reference in New Issue
Block a user