From 41e47147738980f759cba9009f1feacad7b98600 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 8 Jun 2026 13:16:12 +0300 Subject: [PATCH] =?UTF-8?q?fix(form):=20=D1=83=D0=BD=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=B8=D0=BC=D1=91?= =?UTF-8?q?=D0=BD=20=D0=B2=D0=BE=20=D0=B2=D1=81=D0=B5=D1=85=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BB=D0=BB=D0=B5=D0=BA=D1=86=D0=B8=D1=8F=D1=85=20=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=20+=20=D0=BF=D1=80=D0=B5=D1=84=D0=B8=D0=BA=D1=81?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BE=D0=BA=20=D1=81=D1=83?= =?UTF-8?q?=D0=B1=D0=BA=D0=BE=D0=BD=D1=82=D0=BE=20=D0=A7=D0=9F=D0=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверка уникальности имён элементов форм (основа — PR #21 от brake71), портированная на актуальную ветку и расширенная на все именованные коллекции. Корень проблемы: генератор формы счёта ПланаСчетов строил колонки таблицы субконто с «голыми» именами (Валютный, ТолькоОбороты, ВидСубконто), из-за чего флаг субконто сталкивался с одноимённым признаком учёта счёта → невалидный для 1С XML (форма не открывалась). Теперь имена колонок префиксуются именем таблицы (ВидыСубконтоВалютный) — как делает generic-путь табчастей и типовая 1С. - form-compile: fail-fast проверка уникальности в едином emit_element + по реквизитам, колонкам (в пределах реквизита), параметрам и командам. Хелпер вместо копипаста; проверка после нормализации синонимов. - form-validate: проверка имён симметрично существующим id-пулам (элементы, реквизиты, колонки, команды) + новый блок параметров. - form-edit: дедуп внутри JSON-определения и против существующих в форме — для элементов (рекурсивно), реквизитов (+колонки) и команд; WARN→ERROR. Каждая коллекция — свой неймспейс (имя реквизита и имя элемента могут совпадать легально). PS1 и PY — зеркальны. Версии: form-compile 1.74, form-validate 1.7, form-edit 1.1. Все тест-сеты зелёные на обоих рантаймах. Co-authored-by: brake71 <8448482+brake71@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) --- .../form-compile/scripts/form-compile.ps1 | 44 +++++++++++--- .../form-compile/scripts/form-compile.py | 36 +++++++++-- .../skills/form-edit/scripts/form-edit.ps1 | 59 +++++++++++++++++-- .claude/skills/form-edit/scripts/form-edit.py | 55 ++++++++++++++++- .../form-validate/scripts/form-validate.ps1 | 42 ++++++++++++- .../form-validate/scripts/form-validate.py | 35 ++++++++++- .../Forms/ФормаСчета/Ext/Form.xml | 24 ++++---- 7 files changed, 260 insertions(+), 35 deletions(-) diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 0313c7b3..cc234b1c 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,4 +1,4 @@ -# form-compile v1.73 — Compile 1C managed form from JSON or object metadata +# form-compile v1.74 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, @@ -1230,16 +1230,19 @@ function Generate-ChartOfAccountsItemDSL($meta, [hashtable]$p, [hashtable]$fd, [ # ExtDimensionTypes table if ($meta.MaxExtDimensionCount -gt 0) { + # Имена колонок табчасти префиксуются именем таблицы (как generic-путь и типовая 1С), + # иначе флаг субконто (напр. "Валютный") столкнётся с одноимённым признаком учёта счёта. + $edTable = "ВидыСубконто" $edCols = @() - $edCols += [ordered]@{ input = "ВидСубконто"; path = "Объект.ExtDimensionTypes.ExtDimensionType" } - $edCols += [ordered]@{ check = "ТолькоОбороты"; path = "Объект.ExtDimensionTypes.TurnoversOnly" } + $edCols += [ordered]@{ input = "${edTable}ВидСубконто"; path = "Объект.ExtDimensionTypes.ExtDimensionType" } + $edCols += [ordered]@{ check = "${edTable}ТолькоОбороты"; path = "Объект.ExtDimensionTypes.TurnoversOnly" } if ($meta.ExtDimensionAccountingFlags) { foreach ($edFlag in $meta.ExtDimensionAccountingFlags) { - $edCols += [ordered]@{ check = $edFlag.Name; path = "Объект.ExtDimensionTypes.$($edFlag.Name)" } + $edCols += [ordered]@{ check = "${edTable}$($edFlag.Name)"; path = "Объект.ExtDimensionTypes.$($edFlag.Name)" } } } $elements += [ordered]@{ - table = "ВидыСубконто" + table = $edTable path = "Объект.ExtDimensionTypes" columns = $edCols } @@ -1521,6 +1524,17 @@ function New-Id { return $id } +# Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё +# пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast. +function Assert-UniqueName { + param([string]$name, [hashtable]$seen, [string]$kind) + if ($seen.ContainsKey($name)) { + Write-Error "Duplicate $kind name '$name' — names must be unique within their collection in a 1C form (set a unique 'name')" + exit 1 + } + $seen[$name] = $true +} + # --- 3. XML helper --- $script:xml = New-Object System.Text.StringBuilder 8192 @@ -2483,6 +2497,7 @@ function Emit-Element { } $name = Get-ElementName -el $el -typeKey $typeKey + Assert-UniqueName -name $name -seen $script:seenElementNames -kind 'element' $id = New-Id switch ($typeKey) { @@ -4259,9 +4274,11 @@ function Emit-Attributes { if (-not $attrs -or $attrs.Count -eq 0) { return } X "$indent" + $seenAttrs = @{} foreach ($attr in $attrs) { $attrId = New-Id $attrName = "$($attr.name)" + Assert-UniqueName -name $attrName -seen $seenAttrs -kind 'attribute' X "$indent`t" $inner = "$indent`t`t" @@ -4333,12 +4350,20 @@ function Emit-Attributes { if ($hasDirectCols -or $hasAddCols) { X "$inner" if ($hasDirectCols) { - foreach ($col in $attr.columns) { Emit-AttrColumn -col $col -indent "$inner`t" } + $seenCols = @{} # колонки уникальны в пределах своего реквизита + foreach ($col in $attr.columns) { + Assert-UniqueName -name "$($col.name)" -seen $seenCols -kind "column of '$attrName'" + Emit-AttrColumn -col $col -indent "$inner`t" + } } if ($hasAddCols) { foreach ($ac in @($attr.additionalColumns)) { X "$inner`t" - foreach ($col in @($ac.columns)) { Emit-AttrColumn -col $col -indent "$inner`t`t" } + $seenAcCols = @{} # уникальность в пределах группы AdditionalColumns + foreach ($col in @($ac.columns)) { + Assert-UniqueName -name "$($col.name)" -seen $seenAcCols -kind "column of '$attrName'" + Emit-AttrColumn -col $col -indent "$inner`t`t" + } X "$inner`t" } } @@ -4405,7 +4430,9 @@ function Emit-Parameters { if (-not $params -or $params.Count -eq 0) { return } X "$indent" + $seenParams = @{} foreach ($param in $params) { + Assert-UniqueName -name "$($param.name)" -seen $seenParams -kind 'parameter' X "$indent`t" $inner = "$indent`t`t" @@ -4428,8 +4455,10 @@ function Emit-Commands { if (-not $cmds -or $cmds.Count -eq 0) { return } X "$indent" + $seenCmds = @{} foreach ($cmd in $cmds) { $cmdId = New-Id + Assert-UniqueName -name "$($cmd.name)" -seen $seenCmds -kind 'command' X "$indent`t" $inner = "$indent`t`t" @@ -4746,6 +4775,7 @@ X "
' X "" diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index 1c65fa27..7bf3b570 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# form-compile v1.73 — Compile 1C managed form from JSON or object metadata +# form-compile v1.74 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy @@ -1157,14 +1157,17 @@ def generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data): # ExtDimensionTypes table if meta.get('MaxExtDimensionCount', 0) > 0: + # Column names are prefixed with the table name (like the generic TS path and stock 1C), + # else a subconto flag column collides with a same-named account accounting-flag checkbox. + ed_table = '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e' ed_cols = [] - ed_cols.append(OrderedDict([('input', '\u0412\u0438\u0434\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.ExtDimensionType')])) - ed_cols.append(OrderedDict([('check', '\u0422\u043e\u043b\u044c\u043a\u043e\u041e\u0431\u043e\u0440\u043e\u0442\u044b'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.TurnoversOnly')])) + ed_cols.append(OrderedDict([('input', f"{ed_table}\u0412\u0438\u0434\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.ExtDimensionType')])) + ed_cols.append(OrderedDict([('check', f"{ed_table}\u0422\u043e\u043b\u044c\u043a\u043e\u041e\u0431\u043e\u0440\u043e\u0442\u044b"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.TurnoversOnly')])) if meta.get('ExtDimensionAccountingFlags'): for ed_flag in meta['ExtDimensionAccountingFlags']: - ed_cols.append(OrderedDict([('check', ed_flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")])) + ed_cols.append(OrderedDict([('check', f"{ed_table}{ed_flag['Name']}"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")])) elements.append(OrderedDict([ - ('table', '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'), + ('table', ed_table), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes'), ('columns', ed_cols), ])) @@ -1703,6 +1706,17 @@ def new_id(): return _next_id +# Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё +# пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast. +_seen_element_names = set() # пул имён элементов (глобально по всей форме) + +def _ensure_unique(name, seen, kind): + if name in seen: + print(f"[ERROR] Duplicate {kind} name '{name}' — names must be unique within their collection in a 1C form (set a unique 'name')", file=sys.stderr) + sys.exit(1) + seen.add(name) + + # --- Event handler name generator --- EVENT_SUFFIX_MAP = { @@ -2831,6 +2845,7 @@ def emit_element(lines, el, indent, in_cmd_bar=False): print(f"WARNING: Element '{el.get(type_key, '')}': unknown key '{p_name}' -- ignored. Check SKILL.md for valid keys.", file=sys.stderr) name = get_element_name(el, type_key) + _ensure_unique(name, _seen_element_names, 'element') eid = new_id() emitters = { @@ -3975,9 +3990,11 @@ def emit_attributes(lines, attrs, indent): return lines.append(f'{indent}') + seen_attrs = set() for attr in attrs: attr_id = new_id() attr_name = str(attr['name']) + _ensure_unique(attr_name, seen_attrs, 'attribute') lines.append(f'{indent}\t') inner = f'{indent}\t\t' @@ -4041,12 +4058,16 @@ def emit_attributes(lines, attrs, indent): if has_direct_cols or has_add_cols: lines.append(f'{inner}') if has_direct_cols: + seen_cols = set() # колонки уникальны в пределах своего реквизита for col in attr['columns']: + _ensure_unique(str(col['name']), seen_cols, f"column of '{attr_name}'") emit_attr_column(lines, col, f'{inner}\t') if has_add_cols: for ac in attr['additionalColumns']: lines.append(f'{inner}\t') + seen_ac_cols = set() # уникальность в пределах группы AdditionalColumns for col in (ac.get('columns') or []): + _ensure_unique(str(col['name']), seen_ac_cols, f"column of '{attr_name}'") emit_attr_column(lines, col, f'{inner}\t\t') lines.append(f'{inner}\t') lines.append(f'{inner}') @@ -4105,7 +4126,9 @@ def emit_parameters(lines, params, indent): return lines.append(f'{indent}') + seen_params = set() for param in params: + _ensure_unique(str(param['name']), seen_params, 'parameter') lines.append(f'{indent}\t') inner = f'{indent}\t\t' @@ -4125,8 +4148,10 @@ def emit_commands(lines, cmds, indent): return lines.append(f'{indent}') + seen_cmds = set() for cmd in cmds: cmd_id = new_id() + _ensure_unique(str(cmd['name']), seen_cmds, 'command') lines.append(f'{indent}\t') inner = f'{indent}\t\t' @@ -4607,6 +4632,7 @@ def main(): # --- 2. Main compilation --- _next_id = 0 + _seen_element_names.clear() # пул имён элементов (на случай повторного вызова в одном процессе) lines = [] lines.append('') diff --git a/.claude/skills/form-edit/scripts/form-edit.ps1 b/.claude/skills/form-edit/scripts/form-edit.ps1 index 1e953f18..a38ceb1e 100644 --- a/.claude/skills/form-edit/scripts/form-edit.ps1 +++ b/.claude/skills/form-edit/scripts/form-edit.ps1 @@ -1,4 +1,4 @@ -# form-edit v1.0 — Edit 1C managed form elements +# form-edit v1.1 — Edit 1C managed form elements # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -291,6 +291,16 @@ function Get-ElementName { return "$($el.$typeKey)" } +# Уникальность имён внутри JSON-определения (1С: своя коллекция — свой неймспейс). +function Assert-EditUnique { + param([string]$name, [hashtable]$seen, [string]$ctx) + if ($seen.ContainsKey($name)) { + Write-Host "[ERROR] Duplicate $ctx '$name' in JSON definition — names must be unique in 1C form" + exit 1 + } + $seen[$name] = $true +} + $script:knownEvents = @{ "input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange") "check" = @("OnChange") @@ -864,17 +874,31 @@ if ($def.elements -and $def.elements.Count -gt 0) { # Detect indent level $childIndent = Get-ChildIndent $targetCI - # Check for duplicate element names + # Имена элементов уникальны (требование 1С). Сначала — внутри самого JSON-определения + # (рекурсивно по children/columns). + $elemTypeKeys = @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup") + function Walk-ElemNames($el, [hashtable]$seen) { + $tk = $null + foreach ($k in $elemTypeKeys) { if ($el.$k -ne $null) { $tk = $k; break } } + if ($tk) { Assert-EditUnique -name (Get-ElementName -el $el -typeKey $tk) -seen $seen -ctx 'element name' } + if ($el.children) { foreach ($c in $el.children) { Walk-ElemNames $c $seen } } + if ($el.columns) { foreach ($c in $el.columns) { Walk-ElemNames $c $seen } } + } + $dslElemNames = @{} + foreach ($el in $def.elements) { Walk-ElemNames $el $dslElemNames } + + # Затем — против уже существующих элементов формы (дубль = битый XML, форма не откроется) foreach ($el in $def.elements) { $typeKey = $null - foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) { + foreach ($key in $elemTypeKeys) { if ($el.$key -ne $null) { $typeKey = $key; break } } if ($typeKey) { $elName = Get-ElementName -el $el -typeKey $typeKey $existing = Find-Element $rootCI $elName if ($existing) { - Write-Host "[WARN] Element '$elName' already exists in form (id=$($existing.GetAttribute('id')))" + Write-Host "[ERROR] Element '$elName' already exists in form (id=$($existing.GetAttribute('id'))) — element names must be unique" + exit 1 } } } @@ -953,6 +977,22 @@ if ($def.attributes -and $def.attributes.Count -gt 0) { $attrChildIndent = Get-ChildIndent $attrsSection if (-not $attrChildIndent -or $attrChildIndent -eq "") { $attrChildIndent = "`t`t" } + # Уникальность имён реквизитов: внутри JSON-определения (+ колонки в пределах реквизита) и + # против уже существующих реквизитов формы. + $dslAttrNames = @{} + foreach ($attr in $def.attributes) { + Assert-EditUnique -name "$($attr.name)" -seen $dslAttrNames -ctx 'attribute name' + if ($attr.columns) { + $dslColNames = @{} + foreach ($col in $attr.columns) { Assert-EditUnique -name "$($col.name)" -seen $dslColNames -ctx "column name of '$($attr.name)'" } + } + $existingAttr = $attrsSection.SelectSingleNode("f:Attribute[@name='$($attr.name)']", $nsMgr) + if ($existingAttr) { + Write-Host "[ERROR] Attribute '$($attr.name)' already exists in form — attribute names must be unique" + exit 1 + } + } + # Generate attribute fragments $script:xml = New-Object System.Text.StringBuilder 2048 X "<_F $allNsDecl>" @@ -1021,6 +1061,17 @@ if ($def.commands -and $def.commands.Count -gt 0) { $cmdChildIndent = Get-ChildIndent $cmdsSection if (-not $cmdChildIndent -or $cmdChildIndent -eq "") { $cmdChildIndent = "`t`t" } + # Уникальность имён команд: внутри JSON-определения и против существующих команд формы. + $dslCmdNames = @{} + foreach ($cmd in $def.commands) { + Assert-EditUnique -name "$($cmd.name)" -seen $dslCmdNames -ctx 'command name' + $existingCmd = $cmdsSection.SelectSingleNode("f:Command[@name='$($cmd.name)']", $nsMgr) + if ($existingCmd) { + Write-Host "[ERROR] Command '$($cmd.name)' already exists in form — command names must be unique" + exit 1 + } + } + # Generate command fragments $script:xml = New-Object System.Text.StringBuilder 1024 X "<_F $allNsDecl>" diff --git a/.claude/skills/form-edit/scripts/form-edit.py b/.claude/skills/form-edit/scripts/form-edit.py index 594b7a97..474c6cfb 100644 --- a/.claude/skills/form-edit/scripts/form-edit.py +++ b/.claude/skills/form-edit/scripts/form-edit.py @@ -1,4 +1,4 @@ -# form-edit v1.0 — Edit 1C managed form elements (Python port) +# form-edit v1.1 — Edit 1C managed form elements (Python port) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -365,6 +365,14 @@ def get_element_name(el, type_key): return str(el[type_key]) +def _assert_edit_unique(name, seen, ctx): + # Уникальность имён внутри JSON-определения (1С: своя коллекция — свой неймспейс). + if name in seen: + print(f"[ERROR] Duplicate {ctx} '{name}' in JSON definition — names must be unique in 1C form") + sys.exit(1) + seen.add(name) + + known_events = { "input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"], "check": ["OnChange"], @@ -988,7 +996,26 @@ if elements_list: # Detect indent level child_indent = get_child_indent(target_ci) - # Check for duplicate element names + # Имена элементов уникальны (требование 1С). Сначала — внутри самого JSON-определения + # (рекурсивно по children/columns). + def _walk_elem_names(el, seen): + tk = None + for key in ELEMENT_KEYS: + if key in el and el[key] is not None: + tk = key + break + if tk: + _assert_edit_unique(get_element_name(el, tk), seen, "element name") + for c in el.get("children", []): + _walk_elem_names(c, seen) + for c in el.get("columns", []): + _walk_elem_names(c, seen) + + dsl_elem_names = set() + for el in elements_list: + _walk_elem_names(el, dsl_elem_names) + + # Затем — против уже существующих элементов формы (дубль = битый XML, форма не откроется). for el in elements_list: type_key = None for key in ELEMENT_KEYS: @@ -999,7 +1026,8 @@ if elements_list: el_name = get_element_name(el, type_key) existing = find_element(root_ci, el_name) if root_ci is not None else None if existing is not None: - print(f"[WARN] Element '{el_name}' already exists in form (id={existing.get('id')})") + print(f"[ERROR] Element '{el_name}' already exists in form (id={existing.get('id')}) — element names must be unique") + sys.exit(1) # Remember starting element ID for companion counting start_elem_id = next_elem_id @@ -1055,6 +1083,19 @@ if attrs_list: if not attr_child_indent: attr_child_indent = "\t\t" + # Уникальность имён реквизитов: внутри JSON-определения (+ колонки в пределах реквизита) и + # против уже существующих реквизитов формы. + dsl_attr_names = set() + for attr in attrs_list: + _assert_edit_unique(str(attr["name"]), dsl_attr_names, "attribute name") + if attr.get("columns"): + dsl_col_names = set() + for col in attr["columns"]: + _assert_edit_unique(str(col["name"]), dsl_col_names, f"column name of '{attr['name']}'") + if attrs_section.find(f"f:Attribute[@name='{attr['name']}']", NS) is not None: + print(f"[ERROR] Attribute '{attr['name']}' already exists in form — attribute names must be unique") + sys.exit(1) + # Generate attribute fragments xml_lines.clear() X(f"<_F {ALL_NS_DECL}>") @@ -1116,6 +1157,14 @@ if cmds_list: if not cmd_child_indent: cmd_child_indent = "\t\t" + # Уникальность имён команд: внутри JSON-определения и против существующих команд формы. + dsl_cmd_names = set() + for cmd in cmds_list: + _assert_edit_unique(str(cmd["name"]), dsl_cmd_names, "command name") + if cmds_section.find(f"f:Command[@name='{cmd['name']}']", NS) is not None: + print(f"[ERROR] Command '{cmd['name']}' already exists in form — command names must be unique") + sys.exit(1) + xml_lines.clear() X(f"<_F {ALL_NS_DECL}>") for cmd in cmds_list: diff --git a/.claude/skills/form-validate/scripts/form-validate.ps1 b/.claude/skills/form-validate/scripts/form-validate.ps1 index c27c6c78..9b543cca 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.6 — Validate 1C managed form +# form-validate v1.7 — Validate 1C managed form # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -155,7 +155,8 @@ if (-not $stopped) { # --- Collect all elements with IDs --- -$elementIds = @{} # id -> name (element ID pool) +$elementIds = @{} # id -> name (element ID pool) +$elementNames = @{} # name -> id (имена элементов уникальны в пределах формы) $allElements = @() # @{Name; Tag; Id; ParentName; Node} function Collect-Elements { @@ -185,6 +186,13 @@ function Collect-Elements { } else { $elementIds[$id] = $name } + + # Имена элементов уникальны (требование 1С) + if ($elementNames.ContainsKey($name)) { + Report-Error "Duplicate element name '$name': id=${id} and id=$($elementNames[$name])" + } else { + $elementNames[$name] = $id + } } # Recurse into ChildItems @@ -229,6 +237,10 @@ foreach ($attr in $attrNodes) { $attrName = $attr.GetAttribute("name") $attrId = $attr.GetAttribute("id") if ($attrName) { + # Имена реквизитов уникальны среди реквизитов (отдельный неймспейс от элементов) + if ($attrMap.ContainsKey($attrName)) { + Report-Error "Duplicate attribute name '$attrName': id=${attrId} and id=$($attrMap[$attrName].GetAttribute('id'))" + } $attrMap[$attrName] = $attr } if ($attrId -and $attrId -ne "") { @@ -241,6 +253,7 @@ foreach ($attr in $attrNodes) { # Column IDs are a separate sub-pool per attribute — check uniqueness within parent $colIds = @{} + $colNames = @{} # имена колонок уникальны в пределах своего реквизита foreach ($col in $attr.SelectNodes("f:Columns/f:Column", $nsMgr)) { $colId = $col.GetAttribute("id") $colName = $col.GetAttribute("name") @@ -251,6 +264,13 @@ foreach ($attr in $attrNodes) { $colIds[$colId] = $colName } } + if ($colName) { + if ($colNames.ContainsKey($colName)) { + Report-Error "Duplicate column name '$colName' in '$attrName': id=${colId} and id=$($colNames[$colName])" + } else { + $colNames[$colName] = $colId + } + } } } @@ -270,6 +290,10 @@ foreach ($cmd in $cmdNodes) { $cmdName = $cmd.GetAttribute("name") $cmdId = $cmd.GetAttribute("id") if ($cmdName) { + # Имена команд уникальны среди команд (отдельный неймспейс) + if ($cmdMap.ContainsKey($cmdName)) { + Report-Error "Duplicate command name '$cmdName': id=${cmdId} and id=$($cmdMap[$cmdName].GetAttribute('id'))" + } $cmdMap[$cmdName] = $cmd } if ($cmdId -and $cmdId -ne "") { @@ -290,6 +314,20 @@ if (-not $stopped) { } } +# --- Collect parameters (separate name pool, без id) --- + +$paramNames = @{} # name -> $true (имена параметров уникальны среди параметров) +foreach ($param in $root.SelectNodes("f:Parameters/f:Parameter", $nsMgr)) { + $paramName = $param.GetAttribute("name") + if ($paramName) { + if ($paramNames.ContainsKey($paramName)) { + Report-Error "Duplicate parameter name '$paramName'" + } else { + $paramNames[$paramName] = $true + } + } +} + # --- Check 4: Companion elements --- # Define required companions per element type diff --git a/.claude/skills/form-validate/scripts/form-validate.py b/.claude/skills/form-validate/scripts/form-validate.py index 5d3b8c11..d90589f0 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.6 — Validate 1C managed form +# form-validate v1.7 — Validate 1C managed form # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -182,7 +182,8 @@ def main(): report_error("AutoCommandBar element missing") # --- Collect all elements with IDs --- - element_ids = {} # id -> name + element_ids = {} # id -> name + element_names = {} # name -> id (имена элементов уникальны в пределах формы) all_elements = [] # list of dicts {Name, Tag, Id, ParentName, Node} def collect_elements(node, parent_name): @@ -211,6 +212,12 @@ def main(): else: element_ids[eid] = name + # Имена элементов уникальны (требование 1С) + if name in element_names: + report_error(f"Duplicate element name '{name}': id={eid} and id={element_names[name]}") + else: + element_names[name] = eid + child_items = child.find(f"{{{F_NS}}}ChildItems") if child_items is not None: collect_elements(child_items, name) @@ -252,6 +259,9 @@ def main(): attr_name = attr.get("name", "") attr_id = attr.get("id", "") if attr_name: + # Имена реквизитов уникальны среди реквизитов (отдельный неймспейс от элементов) + if attr_name in attr_map: + report_error(f"Duplicate attribute name '{attr_name}': id={attr_id} and id={attr_map[attr_name].get('id', '')}") attr_map[attr_name] = attr if attr_id: if attr_id in attr_ids: @@ -261,6 +271,7 @@ def main(): # Column IDs uniqueness within parent col_ids = {} + col_names = {} # имена колонок уникальны в пределах своего реквизита columns = attr.find(f"{{{F_NS}}}Columns") if columns is not None: for col in columns.findall(f"{{{F_NS}}}Column"): @@ -271,6 +282,11 @@ def main(): report_error(f"Duplicate column id={col_id} in '{attr_name}': '{col_name}' and '{col_ids[col_id]}'") else: col_ids[col_id] = col_name + if col_name: + if col_name in col_names: + report_error(f"Duplicate column name '{col_name}' in '{attr_name}': id={col_id} and id={col_names[col_name]}") + else: + col_names[col_name] = col_id if not stopped: if attr_ids: @@ -289,6 +305,9 @@ def main(): cmd_name = cmd.get("name", "") cmd_id = cmd.get("id", "") if cmd_name: + # Имена команд уникальны среди команд (отдельный неймспейс) + if cmd_name in cmd_map: + report_error(f"Duplicate command name '{cmd_name}': id={cmd_id} and id={cmd_map[cmd_name].get('id', '')}") cmd_map[cmd_name] = cmd if cmd_id: if cmd_id in cmd_ids: @@ -300,6 +319,18 @@ def main(): if cmd_ids: report_ok(f"Unique command IDs: {len(cmd_ids)} entries") + # --- Collect parameters (separate name pool, без id) --- + param_names = {} # name -> True (имена параметров уникальны среди параметров) + params_parent = root.find(f"{{{F_NS}}}Parameters") + if params_parent is not None: + for param in params_parent.findall(f"{{{F_NS}}}Parameter"): + param_name = param.get("name", "") + if param_name: + if param_name in param_names: + report_error(f"Duplicate parameter name '{param_name}'") + else: + param_names[param_name] = True + # --- Check 4: Companion elements --- companion_rules = { "InputField": ["ContextMenu", "ExtendedTooltip"], diff --git a/tests/skills/cases/form-compile-from-object/snapshots/chartofaccounts-item-simple/ChartsOfAccounts/Хозрасчетный/Forms/ФормаСчета/Ext/Form.xml b/tests/skills/cases/form-compile-from-object/snapshots/chartofaccounts-item-simple/ChartsOfAccounts/Хозрасчетный/Forms/ФормаСчета/Ext/Form.xml index 10c8ed56..89fa7f1f 100644 --- a/tests/skills/cases/form-compile-from-object/snapshots/chartofaccounts-item-simple/ChartsOfAccounts/Хозрасчетный/Forms/ФормаСчета/Ext/Form.xml +++ b/tests/skills/cases/form-compile-from-object/snapshots/chartofaccounts-item-simple/ChartsOfAccounts/Хозрасчетный/Forms/ФормаСчета/Ext/Form.xml @@ -115,31 +115,31 @@ - + Объект.ExtDimensionTypes.ExtDimensionType - - + + - + Объект.ExtDimensionTypes.TurnoversOnly Auto Right - - + + - + Объект.ExtDimensionTypes.Валютный Auto Right - - + + - + Объект.ExtDimensionTypes.Количественный Auto Right - - + +