fix(form): уникальность имён во всех коллекциях форм + префикс колонок субконто ЧПС

Проверка уникальности имён элементов форм (основа — 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) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-06-08 13:16:12 +03:00
co-authored by brake71 Claude Opus 4.8
parent ad89929efd
commit 41e4714773
7 changed files with 260 additions and 35 deletions
@@ -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<Attributes>"
$seenAttrs = @{}
foreach ($attr in $attrs) {
$attrId = New-Id
$attrName = "$($attr.name)"
Assert-UniqueName -name $attrName -seen $seenAttrs -kind 'attribute'
X "$indent`t<Attribute name=`"$attrName`" id=`"$attrId`">"
$inner = "$indent`t`t"
@@ -4333,12 +4350,20 @@ function Emit-Attributes {
if ($hasDirectCols -or $hasAddCols) {
X "$inner<Columns>"
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<AdditionalColumns table=`"$($ac.table)`">"
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</AdditionalColumns>"
}
}
@@ -4405,7 +4430,9 @@ function Emit-Parameters {
if (-not $params -or $params.Count -eq 0) { return }
X "$indent<Parameters>"
$seenParams = @{}
foreach ($param in $params) {
Assert-UniqueName -name "$($param.name)" -seen $seenParams -kind 'parameter'
X "$indent`t<Parameter name=`"$($param.name)`">"
$inner = "$indent`t`t"
@@ -4428,8 +4455,10 @@ function Emit-Commands {
if (-not $cmds -or $cmds.Count -eq 0) { return }
X "$indent<Commands>"
$seenCmds = @{}
foreach ($cmd in $cmds) {
$cmdId = New-Id
Assert-UniqueName -name "$($cmd.name)" -seen $seenCmds -kind 'command'
X "$indent`t<Command name=`"$($cmd.name)`" id=`"$cmdId`">"
$inner = "$indent`t`t"
@@ -4746,6 +4775,7 @@ X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8
# Reset and rebuild properly
$script:xml = New-Object System.Text.StringBuilder 8192
$script:nextId = 1
$script:seenElementNames = @{} # пул имён элементов (глобально по всей форме)
X '<?xml version="1.0" encoding="UTF-8"?>'
X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" 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=`"$($script:formatVersion)`">"
@@ -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}<Attributes>')
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<Attribute name="{attr_name}" id="{attr_id}">')
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}<Columns>')
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<AdditionalColumns table="{ac["table"]}">')
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</AdditionalColumns>')
lines.append(f'{inner}</Columns>')
@@ -4105,7 +4126,9 @@ def emit_parameters(lines, params, indent):
return
lines.append(f'{indent}<Parameters>')
seen_params = set()
for param in params:
_ensure_unique(str(param['name']), seen_params, 'parameter')
lines.append(f'{indent}\t<Parameter name="{param["name"]}">')
inner = f'{indent}\t\t'
@@ -4125,8 +4148,10 @@ def emit_commands(lines, cmds, indent):
return
lines.append(f'{indent}<Commands>')
seen_cmds = set()
for cmd in cmds:
cmd_id = new_id()
_ensure_unique(str(cmd['name']), seen_cmds, 'command')
lines.append(f'{indent}\t<Command name="{cmd["name"]}" id="{cmd_id}">')
inner = f'{indent}\t\t'
@@ -4607,6 +4632,7 @@ def main():
# --- 2. Main compilation ---
_next_id = 0
_seen_element_names.clear() # пул имён элементов (на случай повторного вызова в одном процессе)
lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
+55 -4
View File
@@ -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>"
+52 -3
View File
@@ -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:
@@ -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
@@ -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"],
@@ -115,31 +115,31 @@
<ExtendedTooltip name="ВидыСубконтоУправлениеПоискомРасширеннаяПодсказка" id="39"/>
</SearchControlAddition>
<ChildItems>
<InputField name="ВидСубконто" id="40">
<InputField name="ВидыСубконтоВидСубконто" id="40">
<DataPath>Объект.ExtDimensionTypes.ExtDimensionType</DataPath>
<ContextMenu name="ВидСубконтоКонтекстноеМеню" id="41"/>
<ExtendedTooltip name="ВидСубконтоРасширеннаяПодсказка" id="42"/>
<ContextMenu name="ВидыСубконтоВидСубконтоКонтекстноеМеню" id="41"/>
<ExtendedTooltip name="ВидыСубконтоВидСубконтоРасширеннаяПодсказка" id="42"/>
</InputField>
<CheckBoxField name="ТолькоОбороты" id="43">
<CheckBoxField name="ВидыСубконтоТолькоОбороты" id="43">
<DataPath>Объект.ExtDimensionTypes.TurnoversOnly</DataPath>
<CheckBoxType>Auto</CheckBoxType>
<TitleLocation>Right</TitleLocation>
<ContextMenu name="ТолькоОборотыКонтекстноеМеню" id="44"/>
<ExtendedTooltip name="ТолькоОборотыРасширеннаяПодсказка" id="45"/>
<ContextMenu name="ВидыСубконтоТолькоОборотыКонтекстноеМеню" id="44"/>
<ExtendedTooltip name="ВидыСубконтоТолькоОборотыРасширеннаяПодсказка" id="45"/>
</CheckBoxField>
<CheckBoxField name="Валютный" id="46">
<CheckBoxField name="ВидыСубконтоВалютный" id="46">
<DataPath>Объект.ExtDimensionTypes.Валютный</DataPath>
<CheckBoxType>Auto</CheckBoxType>
<TitleLocation>Right</TitleLocation>
<ContextMenu name="ВалютныйКонтекстноеМеню" id="47"/>
<ExtendedTooltip name="ВалютныйРасширеннаяПодсказка" id="48"/>
<ContextMenu name="ВидыСубконтоВалютныйКонтекстноеМеню" id="47"/>
<ExtendedTooltip name="ВидыСубконтоВалютныйРасширеннаяПодсказка" id="48"/>
</CheckBoxField>
<CheckBoxField name="Количественный" id="49">
<CheckBoxField name="ВидыСубконтоКоличественный" id="49">
<DataPath>Объект.ExtDimensionTypes.Количественный</DataPath>
<CheckBoxType>Auto</CheckBoxType>
<TitleLocation>Right</TitleLocation>
<ContextMenu name="КоличественныйКонтекстноеМеню" id="50"/>
<ExtendedTooltip name="КоличественныйРасширеннаяПодсказка" id="51"/>
<ContextMenu name="ВидыСубконтоКоличественныйКонтекстноеМеню" id="50"/>
<ExtendedTooltip name="ВидыСубконтоКоличественныйРасширеннаяПодсказка" id="51"/>
</CheckBoxField>
</ChildItems>
</Table>