feat(form-validate): путь на необъявленный основной реквизит заимствованной формы

Форма, которую платформа отвергала с «Неверный путь к полю - Объект.Партнер»,
проходила валидацию с вердиктом OK. Check 5 такое не видит по двум причинам:
у формы с BaseForm он пропускает базовые элементы (id < 1000000), а привязка
внутри <ChoiceParameterLinks> лежит в <xr:DataPath> и в его список тегов не
входит вовсе.

Проверка 11d: если форма не объявляет основной реквизит, любой путь с корнем
«Объект» не разрешится — ошибка. Непрозрачные формы пути (1/0:uuid), которыми
как раз и заменяет такие ссылки Конфигуратор, ошибкой не считаются.

Две фикстуры: висячая ссылка ловится, та же форма в uuid-форме проходит.
Регресс 13/13 на обоих рантаймах.
This commit is contained in:
Nick Shirokov
2026-08-12 15:58:45 +03:00
parent 1bd0c7724e
commit 9a28fbfacb
7 changed files with 188 additions and 2 deletions
@@ -1,4 +1,4 @@
# form-validate v1.10 — Validate 1C managed form
# form-validate v1.11 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -742,6 +742,36 @@ if (-not $stopped -and $isExtension) {
Report-OK "Extension ID ranges: $extAttrCount attr(s), $extCmdCount cmd(s) — all >= 1000000"
}
}
# 11d. Пути на основной реквизит, которого форма не объявляет.
# Check 5 такое пропускает: у заимствованной формы он не проверяет базовые элементы (id < 1000000),
# а привязки в <xr:Link> вообще вне его списка тегов. Между тем это ровно тот случай, на котором
# платформа отвергает загрузку: «Неверный путь к полю - Объект.X». Правило: если основной реквизит
# не объявлен в <Attributes> формы, любой путь с корнем «Объект» не разрешится.
$mainAttrDeclared = $false
foreach ($attr in $attrNodes) {
$maNode = $attr.SelectSingleNode("f:MainAttribute", $nsMgr)
if ($maNode -and $maNode.InnerText.Trim() -eq "true") { $mainAttrDeclared = $true; break }
}
if (-not $mainAttrDeclared) {
# Значения привязок ищем текстом: интересуют и обычные теги, и <xr:DataPath> внутри
# <ChoiceParameterLinks>, а те живут в чужом пространстве имён.
$rawForm = [System.IO.File]::ReadAllText($FormPath, [System.Text.Encoding]::UTF8)
$mainBase = $baseFormNode.SelectSingleNode("f:Attributes/f:Attribute[f:MainAttribute='true']", $bfNs)
$danglingPaths = @{}
foreach ($m in [regex]::Matches($rawForm, '<(?:\w+:)?\w*DataPath[^>]*>(Объект\.[^<]+)</(?:\w+:)?\w*DataPath>')) {
$danglingPaths[$m.Groups[1].Value] = $true
}
if ($danglingPaths.Count -gt 0) {
$shown = @($danglingPaths.Keys | Sort-Object)
$sample = ($shown | Select-Object -First 3) -join ", "
$suffix = if ($shown.Count -gt 3) { " (и ещё $($shown.Count - 3))" } else { "" }
Report-Error "Path(s) rooted at 'Объект' but the form declares no MainAttribute: $sample$suffix"
} elseif ($mainBase) {
Report-OK "Object paths: none dangling (MainAttribute not declared)"
}
}
}
# Check callType without BaseForm (structural warning)
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-validate v1.10 — Validate 1C managed form
# form-validate v1.11 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -708,6 +708,33 @@ def main():
if (ext_attr_count + ext_cmd_count) > 0:
report_ok(f"Extension ID ranges: {ext_attr_count} attr(s), {ext_cmd_count} cmd(s) \u2014 all >= 1000000")
# 11d. \u041f\u0443\u0442\u0438 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442, \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0444\u043e\u0440\u043c\u0430 \u043d\u0435 \u043e\u0431\u044a\u044f\u0432\u043b\u044f\u0435\u0442.
# Check 5 \u0442\u0430\u043a\u043e\u0435 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442: \u0443 \u0437\u0430\u0438\u043c\u0441\u0442\u0432\u043e\u0432\u0430\u043d\u043d\u043e\u0439 \u0444\u043e\u0440\u043c\u044b \u043e\u043d \u043d\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0431\u0430\u0437\u043e\u0432\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b (id < 1000000),
# \u0430 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u0432 <xr:Link> \u0432\u043e\u043e\u0431\u0449\u0435 \u0432\u043d\u0435 \u0435\u0433\u043e \u0441\u043f\u0438\u0441\u043a\u0430 \u0442\u0435\u0433\u043e\u0432. \u041c\u0435\u0436\u0434\u0443 \u0442\u0435\u043c \u044d\u0442\u043e \u0440\u043e\u0432\u043d\u043e \u0442\u043e\u0442 \u0441\u043b\u0443\u0447\u0430\u0439, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c
# \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u043e\u0442\u0432\u0435\u0440\u0433\u0430\u0435\u0442 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443: \u00ab\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u043f\u043e\u043b\u044e - \u041e\u0431\u044a\u0435\u043a\u0442.X\u00bb. \u041f\u0440\u0430\u0432\u0438\u043b\u043e: \u0435\u0441\u043b\u0438 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442
# \u043d\u0435 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d \u0432 <Attributes> \u0444\u043e\u0440\u043c\u044b, \u043b\u044e\u0431\u043e\u0439 \u043f\u0443\u0442\u044c \u0441 \u043a\u043e\u0440\u043d\u0435\u043c \u00ab\u041e\u0431\u044a\u0435\u043a\u0442\u00bb \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0441\u044f.
main_attr_declared = False
for attr in attr_nodes:
ma_node = attr.find(f"{{{F_NS}}}MainAttribute")
if ma_node is not None and (ma_node.text or "").strip() == "true":
main_attr_declared = True
break
if not main_attr_declared:
# \u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u0440\u0438\u0432\u044f\u0437\u043e\u043a \u0438\u0449\u0435\u043c \u0442\u0435\u043a\u0441\u0442\u043e\u043c: \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0443\u044e\u0442 \u0438 \u043e\u0431\u044b\u0447\u043d\u044b\u0435 \u0442\u0435\u0433\u0438, \u0438 <xr:DataPath> \u0432\u043d\u0443\u0442\u0440\u0438
# <ChoiceParameterLinks>, \u0430 \u0442\u0435 \u0436\u0438\u0432\u0443\u0442 \u0432 \u0447\u0443\u0436\u043e\u043c \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0435 \u0438\u043c\u0451\u043d.
with open(form_path, "r", encoding="utf-8-sig") as fh:
raw_form = fh.read()
main_base = base_form_node.find(f"{{{F_NS}}}Attributes/{{{F_NS}}}Attribute[{{{F_NS}}}MainAttribute='true']")
dangling_paths = set(re.findall(r'<(?:\w+:)?\w*DataPath[^>]*>(\u041e\u0431\u044a\u0435\u043a\u0442\.[^<]+)</(?:\w+:)?\w*DataPath>', raw_form))
if dangling_paths:
shown = sorted(dangling_paths)
sample = ", ".join(shown[:3])
suffix = f" (\u0438 \u0435\u0449\u0451 {len(shown) - 3})" if len(shown) > 3 else ""
report_error(f"Path(s) rooted at '\u041e\u0431\u044a\u0435\u043a\u0442' but the form declares no MainAttribute: {sample}{suffix}")
elif main_base is not None:
report_ok("Object paths: none dangling (MainAttribute not declared)")
# Check callType without BaseForm
if not stopped and not is_extension:
call_type_without_base = False
@@ -0,0 +1,7 @@
{
"name": "Ошибка: путь Объект.* в заимствованной форме без объявленного основного реквизита",
"setup": "fixture:borrowed-object-path-no-main",
"params": { "formPath": "Documents/Заказ/Forms/ФормаДокумента" },
"expectError": true,
"expect": { "stdoutContains": "Объект.Партнер" }
}
@@ -0,0 +1,5 @@
{
"name": "Заимствованная форма без основного реквизита: непрозрачный путь 1/0:uuid ошибкой не считается",
"setup": "fixture:borrowed-object-path-opaque",
"params": { "formPath": "Documents/Заказ/Forms/ФормаДокумента" }
}
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.20">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>false</Autofill>
</AutoCommandBar>
<ChildItems>
<InputField name="КонтактноеЛицо" id="4">
<ChoiceParameterLinks>
<xr:Link>
<xr:Name>Отбор.Владелец</xr:Name>
<xr:DataPath xsi:type="xs:string">Объект.Партнер</xr:DataPath>
<xr:ValueChange>Clear</xr:ValueChange>
</xr:Link>
</ChoiceParameterLinks>
<ContextMenu name="КонтактноеЛицоКонтекстноеМеню" id="5"/>
<ExtendedTooltip name="КонтактноеЛицоРасширеннаяПодсказка" id="6"/>
</InputField>
</ChildItems>
<Attributes/>
<BaseForm version="2.20">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>false</Autofill>
</AutoCommandBar>
<ChildItems>
<InputField name="КонтактноеЛицо" id="4">
<ChoiceParameterLinks>
<xr:Link>
<xr:Name>Отбор.Владелец</xr:Name>
<xr:DataPath xsi:type="xs:string">Объект.Партнер</xr:DataPath>
<xr:ValueChange>Clear</xr:ValueChange>
</xr:Link>
</ChoiceParameterLinks>
<ContextMenu name="КонтактноеЛицоКонтекстноеМеню" id="5"/>
<ExtendedTooltip name="КонтактноеЛицоРасширеннаяПодсказка" id="6"/>
</InputField>
</ChildItems>
<Attributes/>
</BaseForm>
</Form>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.20">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>false</Autofill>
</AutoCommandBar>
<ChildItems>
<InputField name="КонтактноеЛицо" id="4">
<ChoiceParameterLinks>
<xr:Link>
<xr:Name>Отбор.Владелец</xr:Name>
<xr:DataPath xsi:type="xs:string">1/0:8d121ce6-6d64-46b8-994d-69e60ed91a43</xr:DataPath>
<xr:ValueChange>Clear</xr:ValueChange>
</xr:Link>
</ChoiceParameterLinks>
<ContextMenu name="КонтактноеЛицоКонтекстноеМеню" id="5"/>
<ExtendedTooltip name="КонтактноеЛицоРасширеннаяПодсказка" id="6"/>
</InputField>
</ChildItems>
<Attributes/>
<BaseForm version="2.20">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>false</Autofill>
</AutoCommandBar>
<ChildItems>
<InputField name="КонтактноеЛицо" id="4">
<ChoiceParameterLinks>
<xr:Link>
<xr:Name>Отбор.Владелец</xr:Name>
<xr:DataPath xsi:type="xs:string">1/0:8d121ce6-6d64-46b8-994d-69e60ed91a43</xr:DataPath>
<xr:ValueChange>Clear</xr:ValueChange>
</xr:Link>
</ChoiceParameterLinks>
<ContextMenu name="КонтактноеЛицоКонтекстноеМеню" id="5"/>
<ExtendedTooltip name="КонтактноеЛицоРасширеннаяПодсказка" id="6"/>
</InputField>
</ChildItems>
<Attributes/>
</BaseForm>
</Form>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.20">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>false</Autofill>
</AutoCommandBar>
<ChildItems>
<InputField name="КонтактноеЛицо" id="4">
<ChoiceParameterLinks>
<xr:Link>
<xr:Name>Отбор.Владелец</xr:Name>
<xr:DataPath xsi:type="xs:string">1/0:UUID-001</xr:DataPath>
<xr:ValueChange>Clear</xr:ValueChange>
</xr:Link>
</ChoiceParameterLinks>
<ContextMenu name="КонтактноеЛицоКонтекстноеМеню" id="5"/>
<ExtendedTooltip name="КонтактноеЛицоРасширеннаяПодсказка" id="6"/>
</InputField>
</ChildItems>
<Attributes/>
<BaseForm version="2.20">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>false</Autofill>
</AutoCommandBar>
<ChildItems>
<InputField name="КонтактноеЛицо" id="4">
<ChoiceParameterLinks>
<xr:Link>
<xr:Name>Отбор.Владелец</xr:Name>
<xr:DataPath xsi:type="xs:string">1/0:UUID-001</xr:DataPath>
<xr:ValueChange>Clear</xr:ValueChange>
</xr:Link>
</ChoiceParameterLinks>
<ContextMenu name="КонтактноеЛицоКонтекстноеМеню" id="5"/>
<ExtendedTooltip name="КонтактноеЛицоРасширеннаяПодсказка" id="6"/>
</InputField>
</ChildItems>
<Attributes/>
</BaseForm>
</Form>