From 9a28fbfacb4978fc7f6578350b40c95f915878bf Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 12 Aug 2026 15:58:45 +0300 Subject: [PATCH] =?UTF-8?q?feat(form-validate):=20=D0=BF=D1=83=D1=82=D1=8C?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D0=BD=D0=B5=D0=BE=D0=B1=D1=8A=D1=8F=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=BE=D1=81=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BD=D0=BE=D0=B9=20=D1=80=D0=B5=D0=BA=D0=B2=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D1=82=20=D0=B7=D0=B0=D0=B8=D0=BC=D1=81=D1=82=D0=B2=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=BD=D0=BE=D0=B9=20=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Форма, которую платформа отвергала с «Неверный путь к полю - Объект.Партнер», проходила валидацию с вердиктом OK. Check 5 такое не видит по двум причинам: у формы с BaseForm он пропускает базовые элементы (id < 1000000), а привязка внутри лежит в и в его список тегов не входит вовсе. Проверка 11d: если форма не объявляет основной реквизит, любой путь с корнем «Объект» не разрешится — ошибка. Непрозрачные формы пути (1/0:uuid), которыми как раз и заменяет такие ссылки Конфигуратор, ошибкой не считаются. Две фикстуры: висячая ссылка ловится, та же форма в uuid-форме проходит. Регресс 13/13 на обоих рантаймах. --- .../form-validate/scripts/form-validate.ps1 | 32 ++++++++++++++- .../form-validate/scripts/form-validate.py | 29 +++++++++++++- .../borrowed-object-path-no-main.json | 7 ++++ .../borrowed-object-path-opaque.json | 5 +++ .../Заказ/Forms/ФормаДокумента/Ext/Form.xml | 39 +++++++++++++++++++ .../Заказ/Forms/ФормаДокумента/Ext/Form.xml | 39 +++++++++++++++++++ .../Заказ/Forms/ФормаДокумента/Ext/Form.xml | 39 +++++++++++++++++++ 7 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 tests/skills/cases/form-validate/borrowed-object-path-no-main.json create mode 100644 tests/skills/cases/form-validate/borrowed-object-path-opaque.json create mode 100644 tests/skills/cases/form-validate/fixtures/borrowed-object-path-no-main/Documents/Заказ/Forms/ФормаДокумента/Ext/Form.xml create mode 100644 tests/skills/cases/form-validate/fixtures/borrowed-object-path-opaque/Documents/Заказ/Forms/ФормаДокумента/Ext/Form.xml create mode 100644 tests/skills/cases/form-validate/snapshots/borrowed-object-path-opaque/Documents/Заказ/Forms/ФормаДокумента/Ext/Form.xml diff --git a/.claude/skills/form-validate/scripts/form-validate.ps1 b/.claude/skills/form-validate/scripts/form-validate.ps1 index b72996f8..89183be2 100644 --- a/.claude/skills/form-validate/scripts/form-validate.ps1 +++ b/.claude/skills/form-validate/scripts/form-validate.ps1 @@ -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), + # а привязки в вообще вне его списка тегов. Между тем это ровно тот случай, на котором + # платформа отвергает загрузку: «Неверный путь к полю - Объект.X». Правило: если основной реквизит + # не объявлен в формы, любой путь с корнем «Объект» не разрешится. + $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) { + # Значения привязок ищем текстом: интересуют и обычные теги, и внутри + # , а те живут в чужом пространстве имён. + $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[^>]*>(Объект\.[^<]+)')) { + $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) diff --git a/.claude/skills/form-validate/scripts/form-validate.py b/.claude/skills/form-validate/scripts/form-validate.py index acad4f63..5708fcdc 100644 --- a/.claude/skills/form-validate/scripts/form-validate.py +++ b/.claude/skills/form-validate/scripts/form-validate.py @@ -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 \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 \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 \u0432\u043d\u0443\u0442\u0440\u0438 + # , \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\.[^<]+)', 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 diff --git a/tests/skills/cases/form-validate/borrowed-object-path-no-main.json b/tests/skills/cases/form-validate/borrowed-object-path-no-main.json new file mode 100644 index 00000000..11e7c782 --- /dev/null +++ b/tests/skills/cases/form-validate/borrowed-object-path-no-main.json @@ -0,0 +1,7 @@ +{ + "name": "Ошибка: путь Объект.* в заимствованной форме без объявленного основного реквизита", + "setup": "fixture:borrowed-object-path-no-main", + "params": { "formPath": "Documents/Заказ/Forms/ФормаДокумента" }, + "expectError": true, + "expect": { "stdoutContains": "Объект.Партнер" } +} diff --git a/tests/skills/cases/form-validate/borrowed-object-path-opaque.json b/tests/skills/cases/form-validate/borrowed-object-path-opaque.json new file mode 100644 index 00000000..d6df9daa --- /dev/null +++ b/tests/skills/cases/form-validate/borrowed-object-path-opaque.json @@ -0,0 +1,5 @@ +{ + "name": "Заимствованная форма без основного реквизита: непрозрачный путь 1/0:uuid ошибкой не считается", + "setup": "fixture:borrowed-object-path-opaque", + "params": { "formPath": "Documents/Заказ/Forms/ФормаДокумента" } +} diff --git a/tests/skills/cases/form-validate/fixtures/borrowed-object-path-no-main/Documents/Заказ/Forms/ФормаДокумента/Ext/Form.xml b/tests/skills/cases/form-validate/fixtures/borrowed-object-path-no-main/Documents/Заказ/Forms/ФормаДокумента/Ext/Form.xml new file mode 100644 index 00000000..f55498a2 --- /dev/null +++ b/tests/skills/cases/form-validate/fixtures/borrowed-object-path-no-main/Documents/Заказ/Forms/ФормаДокумента/Ext/Form.xml @@ -0,0 +1,39 @@ + +
+ + false + + + + + + Отбор.Владелец + Объект.Партнер + Clear + + + + + + + + + + false + + + + + + Отбор.Владелец + Объект.Партнер + Clear + + + + + + + + + diff --git a/tests/skills/cases/form-validate/fixtures/borrowed-object-path-opaque/Documents/Заказ/Forms/ФормаДокумента/Ext/Form.xml b/tests/skills/cases/form-validate/fixtures/borrowed-object-path-opaque/Documents/Заказ/Forms/ФормаДокумента/Ext/Form.xml new file mode 100644 index 00000000..d506e9dd --- /dev/null +++ b/tests/skills/cases/form-validate/fixtures/borrowed-object-path-opaque/Documents/Заказ/Forms/ФормаДокумента/Ext/Form.xml @@ -0,0 +1,39 @@ + +
+ + false + + + + + + Отбор.Владелец + 1/0:8d121ce6-6d64-46b8-994d-69e60ed91a43 + Clear + + + + + + + + + + false + + + + + + Отбор.Владелец + 1/0:8d121ce6-6d64-46b8-994d-69e60ed91a43 + Clear + + + + + + + + + diff --git a/tests/skills/cases/form-validate/snapshots/borrowed-object-path-opaque/Documents/Заказ/Forms/ФормаДокумента/Ext/Form.xml b/tests/skills/cases/form-validate/snapshots/borrowed-object-path-opaque/Documents/Заказ/Forms/ФормаДокумента/Ext/Form.xml new file mode 100644 index 00000000..d833bdb0 --- /dev/null +++ b/tests/skills/cases/form-validate/snapshots/borrowed-object-path-opaque/Documents/Заказ/Forms/ФормаДокумента/Ext/Form.xml @@ -0,0 +1,39 @@ + +
+ + false + + + + + + Отбор.Владелец + 1/0:UUID-001 + Clear + + + + + + + + + + false + + + + + + Отбор.Владелец + 1/0:UUID-001 + Clear + + + + + + + + +