Files
cc-1c-skills/.claude/skills/xdto-validate/scripts/xdto-validate.py
T
Nick ShirokovandClaude Opus 5 7ca6dfa6b2 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>
2026-07-25 21:48:48 +03:00

506 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# xdto-validate v1.0 — 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
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 local(el):
return etree.QName(el).localname
# ── 1. well-formedness ───────────────────────────────────────
try:
doc = etree.parse(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")
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 = etree.parse(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 = etree.parse(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 etree.parse(ob).getroot().get("targetNamespace") == target_ns:
clash.append(other)
except Exception: # noqa: BLE001
pass
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)