fix(skd-compile): Designer-compatible empty parameter values

Centralized empty-value handling: shorthand `=`, `= _`, `= null` and
object-form `value: null` / `""` now serialize per type, matching what 1C
Designer writes:
- ref / no-type → <value xsi:nil="true"/>
- string → <value xsi:type="xs:string"/>
- date/decimal/boolean → typed zero (0001-01-01 / 0 / false)
- StandardPeriod → Custom variant with zero dates
- @valueList → omit <value> entirely

Closes BUG-1 (StandardPeriod @autoDates) and BUG-2 (CatalogRef.X = _
producing invalid <value>_</value>) reported by titan team. New helpers
Test-EmptyValue / Emit-EmptyValue (ps1) and is_empty_value /
emit_empty_value (py) shared by Emit-ParamValue, availableValues loop,
and explicit dataParameters emit. Shorthand regex .+ → .* so trailing
`=` parses as empty.

Reference: upload/erf/ПроверкаЭкранирования (live Designer dump).
New test case empty-param-values covers all 10 type×sentinel combos;
3 existing snapshots regenerated to include the now-correct <value>
tags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-05-20 11:27:24 +03:00
parent 3eaa7ffa3b
commit 449f814d16
8 changed files with 296 additions and 28 deletions
+2
View File
@@ -147,6 +147,8 @@ Shorthand: `"Имя [Заголовок]: тип = значение @флаги"
Объектная форма: `title`, `hidden: true`, `valueListAllowed: true`, `availableAsField: false`, `denyIncompleteValues: true`, `use: "Always"`.
Если значения по умолчанию нет — пропусти `=` в shorthand или укажи `"value": null` в объектной форме.
Список допустимых значений (availableValues):
```json
@@ -1,4 +1,4 @@
# skd-compile v1.23 — Compile 1C DCS from JSON
# skd-compile v1.24 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -375,8 +375,8 @@ function Parse-ParamShorthand {
$s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim()
}
# Split "Name: Type = Value"
if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.+))?$') {
# Split "Name: Type = Value" — RHS may be empty (`= ` / `=`) → treated as empty value
if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.*))?$') {
$result.name = $Matches[1].Trim()
$result.type = Resolve-TypeStr ($Matches[2].Trim())
if ($Matches[4]) {
@@ -985,8 +985,9 @@ function Emit-SingleParam {
X "`t`t</valueType>"
}
# Value
Emit-ParamValue -type $parsed.type -val $parsed.value -indent "`t`t"
# Value — for valueListAllowed params Designer omits <value> when empty
$vla = [bool]$parsed.valueListAllowed
Emit-ParamValue -type $parsed.type -val $parsed.value -indent "`t`t" -valueListAllowed $vla
# Hidden implies useRestriction=true + availableAsField=false
if ($parsed.hidden -eq $true) {
@@ -1017,13 +1018,17 @@ function Emit-SingleParam {
# AvailableValues
if ($p -isnot [string] -and $p.availableValues) {
foreach ($av in $p.availableValues) {
$avVal = "$($av.value)"
$avType = "xs:string"
if ($avVal -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') {
$avType = "dcscor:DesignTimeValue"
}
X "`t`t<availableValue>"
X "`t`t`t<value xsi:type=`"$avType`">$(Esc-Xml $avVal)</value>"
if (Test-EmptyValue $av.value) {
Emit-EmptyValue -type $parsed.type -indent "`t`t`t" -tagPrefix "" -valueListAllowed $false
} else {
$avVal = "$($av.value)"
$avType = "xs:string"
if ($avVal -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') {
$avType = "dcscor:DesignTimeValue"
}
X "`t`t`t<value xsi:type=`"$avType`">$(Esc-Xml $avVal)</value>"
}
# `title` accepted as synonym of `presentation` — both map to the same UI label.
$avPres = if ($av.presentation) { $av.presentation } elseif ($av.title) { $av.title } else { "" }
if ($avPres) {
@@ -1107,10 +1112,52 @@ function Emit-Parameters {
}
}
function Emit-ParamValue {
param([string]$type, $val, [string]$indent)
function Test-EmptyValue {
param($v)
if ($null -eq $v) { return $true }
$s = "$v".Trim()
if ($s -eq "") { return $true }
if ($s -eq "_") { return $true }
if ($s.ToLowerInvariant() -eq "null") { return $true }
return $false
}
if ($null -eq $val) { return }
function Emit-EmptyValue {
param([string]$type, [string]$indent, [string]$tagPrefix = "", [bool]$valueListAllowed = $false)
if ($valueListAllowed) { return }
$t = if ($null -eq $type) { "" } else { "$type" }
$pf = $tagPrefix
if ($t -eq "") {
X "$indent<${pf}value xsi:nil=`"true`"/>"
} elseif ($t -eq "StandardPeriod") {
X "$indent<${pf}value xsi:type=`"v8:StandardPeriod`">"
X "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">Custom</v8:variant>"
X "$indent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
X "$indent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
X "$indent</${pf}value>"
} elseif ($t -match '^string') {
X "$indent<${pf}value xsi:type=`"xs:string`"/>"
} elseif ($t -match '^date') {
X "$indent<${pf}value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</${pf}value>"
} elseif ($t -match '^decimal') {
X "$indent<${pf}value xsi:type=`"xs:decimal`">0</${pf}value>"
} elseif ($t -eq "boolean") {
X "$indent<${pf}value xsi:type=`"xs:boolean`">false</${pf}value>"
} else {
# Ref types or unknown — safe nil
X "$indent<${pf}value xsi:nil=`"true`"/>"
}
}
function Emit-ParamValue {
param([string]$type, $val, [string]$indent, [bool]$valueListAllowed = $false)
if (Test-EmptyValue $val) {
Emit-EmptyValue -type $type -indent $indent -tagPrefix "" -valueListAllowed $valueListAllowed
return
}
$valStr = "$val"
@@ -1868,6 +1915,8 @@ function Emit-DataParameters {
# Value
if ($dp.nilValue -eq $true) {
X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>"
} elseif (Test-EmptyValue $dp.value) {
Emit-EmptyValue -type "$($dp.valueType)" -indent "$indent`t`t" -tagPrefix "dcscor:" -valueListAllowed $false
} elseif ($null -ne $dp.value) {
$vtype = "$($dp.valueType)"
if ($dp.value -is [PSCustomObject] -and $dp.value.variant) {
@@ -2206,7 +2255,7 @@ function Emit-SettingsVariants {
}
$dpItem | Add-Member -NotePropertyName "value" -NotePropertyValue @{ variant = $variant }
if ($variant -ne 'Custom') { $hasMeaningfulValue = $true }
} elseif ($null -ne $ap.value -and "$($ap.value)" -ne '') {
} elseif (-not (Test-EmptyValue $ap.value)) {
$dpItem | Add-Member -NotePropertyName "value" -NotePropertyValue $ap.value
$dpItem | Add-Member -NotePropertyName "valueType" -NotePropertyValue "$($ap.type)"
$hasMeaningfulValue = $true
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# skd-compile v1.23 — Compile 1C DCS from JSON
# skd-compile v1.24 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -282,8 +282,8 @@ def parse_param_shorthand(s):
result['title'] = m.group(1).strip()
s = re.sub(r'\s*\[[^\]]*\]\s*', ' ', s).strip()
# Split "Name: Type = Value"
m = re.match(r'^([^:]+):\s*(\S+)(\s*=\s*(.+))?$', s)
# Split "Name: Type = Value" — RHS may be empty (`= ` / `=`) → treated as empty value
m = re.match(r'^([^:]+):\s*(\S+)(\s*=\s*(.*))?$', s)
if m:
result['name'] = m.group(1).strip()
result['type'] = resolve_type_str(m.group(2).strip())
@@ -790,8 +790,49 @@ def emit_total_fields(lines, defn):
# === Parameters ===
def emit_param_value(lines, type_str, val, indent):
if val is None:
def is_empty_value(v):
if v is None:
return True
s = str(v).strip()
if s == '':
return True
if s == '_':
return True
if s.lower() == 'null':
return True
return False
def emit_empty_value(lines, type_str, indent, tag_prefix='', value_list_allowed=False):
if value_list_allowed:
return
t = type_str or ''
pf = tag_prefix
if t == '':
lines.append(f'{indent}<{pf}value xsi:nil="true"/>')
elif t == 'StandardPeriod':
lines.append(f'{indent}<{pf}value xsi:type="v8:StandardPeriod">')
lines.append(f'{indent}\t<v8:variant xsi:type="v8:StandardPeriodVariant">Custom</v8:variant>')
lines.append(f'{indent}\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>')
lines.append(f'{indent}\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>')
lines.append(f'{indent}</{pf}value>')
elif re.match(r'^string', t):
lines.append(f'{indent}<{pf}value xsi:type="xs:string"/>')
elif re.match(r'^date', t):
lines.append(f'{indent}<{pf}value xsi:type="xs:dateTime">0001-01-01T00:00:00</{pf}value>')
elif re.match(r'^decimal', t):
lines.append(f'{indent}<{pf}value xsi:type="xs:decimal">0</{pf}value>')
elif t == 'boolean':
lines.append(f'{indent}<{pf}value xsi:type="xs:boolean">false</{pf}value>')
else:
# Ref types or unknown — safe nil
lines.append(f'{indent}<{pf}value xsi:nil="true"/>')
def emit_param_value(lines, type_str, val, indent, value_list_allowed=False):
if is_empty_value(val):
emit_empty_value(lines, type_str, indent, '', value_list_allowed)
return
val_str = str(val)
@@ -847,8 +888,9 @@ def emit_single_param(lines, p, parsed):
emit_value_type(lines, parsed['type'], '\t\t\t')
lines.append('\t\t</valueType>')
# Value
emit_param_value(lines, parsed.get('type', ''), parsed.get('value'), '\t\t')
# Value — for valueListAllowed params Designer omits <value> when empty
vla = bool(parsed.get('valueListAllowed'))
emit_param_value(lines, parsed.get('type', ''), parsed.get('value'), '\t\t', vla)
# Hidden implies useRestriction=true + availableAsField=false
if parsed.get('hidden') is True:
@@ -876,12 +918,15 @@ def emit_single_param(lines, p, parsed):
# AvailableValues
if p is not None and not isinstance(p, str) and p.get('availableValues'):
for av in p['availableValues']:
av_val = str(av.get('value', ''))
av_type = 'xs:string'
if re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', av_val):
av_type = 'dcscor:DesignTimeValue'
lines.append('\t\t<availableValue>')
lines.append(f'\t\t\t<value xsi:type="{av_type}">{esc_xml(av_val)}</value>')
if is_empty_value(av.get('value')):
emit_empty_value(lines, parsed.get('type', ''), '\t\t\t', '', False)
else:
av_val = str(av.get('value', ''))
av_type = 'xs:string'
if re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', av_val):
av_type = 'dcscor:DesignTimeValue'
lines.append(f'\t\t\t<value xsi:type="{av_type}">{esc_xml(av_val)}</value>')
# `title` accepted as synonym of `presentation` — both map to the same UI label.
av_pres = av.get('presentation') or av.get('title') or ''
if av_pres:
@@ -1585,6 +1630,8 @@ def emit_data_parameters(lines, items, indent):
# Value
if dp.get('nilValue') is True:
lines.append(f'{indent}\t\t<dcscor:value xsi:nil="true"/>')
elif is_empty_value(dp.get('value')):
emit_empty_value(lines, str(dp.get('valueType') or ''), f'{indent}\t\t', 'dcscor:', False)
elif dp.get('value') is not None:
val = dp['value']
vtype = str(dp.get('valueType') or '')
@@ -1853,7 +1900,7 @@ def emit_settings_variants(lines, defn):
item['value'] = {'variant': variant}
if variant != 'Custom':
has_meaningful_value = True
elif ap.get('value') is not None and str(ap.get('value')) != '':
elif not is_empty_value(ap.get('value')):
item['value'] = ap['value']
item['valueType'] = str(ap.get('type') or '')
has_meaningful_value = True
@@ -0,0 +1,27 @@
{
"name": "Параметры с пустыми значениями (все типы, разные sentinel-формы)",
"params": { "outputPath": "Template.xml" },
"input": {
"dataSets": [{
"name": "Основной",
"query": "ВЫБРАТЬ 1 КАК Поле1",
"fields": ["Поле1: число(1,0)"]
}],
"parameters": [
"Параметр1",
"Параметр2: string =",
"ПараметрСписок: EnumRef.СтатусТеста @valueList = _",
"ПараметрСсылка: CatalogRef.ПлоскийПростой",
"ПараметрДата: date = null",
{ "name": "ПараметрЧисло", "type": "decimal", "value": null },
"ПараметрБулево: boolean = ",
"ПараметрСтандартныйПериод: StandardPeriod = _",
{ "name": "ПараметрТипНеЗадан", "value": null },
"ПараметрСписокСтрок: string @valueList"
]
},
"validatePath": "Template.xml",
"expect": {
"files": ["Template.xml"]
}
}
@@ -77,6 +77,11 @@
<valueType>
<v8:Type>v8:StandardPeriod</v8:Type>
</valueType>
<value xsi:type="v8:StandardPeriod">
<v8:variant xsi:type="v8:StandardPeriodVariant">Custom</v8:variant>
<v8:startDate>0001-01-01T00:00:00</v8:startDate>
<v8:endDate>0001-01-01T00:00:00</v8:endDate>
</value>
</parameter>
<parameter>
<name>Флаг</name>
@@ -129,6 +134,7 @@
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
<value xsi:type="xs:string"/>
</parameter>
<parameter>
<name>Валюта</name>
@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>Основной</name>
<field xsi:type="DataSetFieldField">
<dataPath>Поле1</dataPath>
<field>Поле1</field>
<valueType>
<v8:Type>xs:decimal</v8:Type>
<v8:NumberQualifiers>
<v8:Digits>1</v8:Digits>
<v8:FractionDigits>0</v8:FractionDigits>
<v8:AllowedSign>Any</v8:AllowedSign>
</v8:NumberQualifiers>
</valueType>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ 1 КАК Поле1</query>
</dataSet>
<parameter>
<name>Параметр1</name>
<value xsi:nil="true"/>
</parameter>
<parameter>
<name>Параметр2</name>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>0</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
<value xsi:type="xs:string"/>
</parameter>
<parameter>
<name>ПараметрСписок</name>
<valueType>
<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:EnumRef.СтатусТеста</v8:Type>
</valueType>
<valueListAllowed>true</valueListAllowed>
</parameter>
<parameter>
<name>ПараметрСсылка</name>
<valueType>
<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:CatalogRef.ПлоскийПростой</v8:Type>
</valueType>
<value xsi:nil="true"/>
</parameter>
<parameter>
<name>ПараметрДата</name>
<valueType>
<v8:Type>xs:dateTime</v8:Type>
<v8:DateQualifiers>
<v8:DateFractions>Date</v8:DateFractions>
</v8:DateQualifiers>
</valueType>
<value xsi:type="xs:dateTime">0001-01-01T00:00:00</value>
</parameter>
<parameter>
<name>ПараметрЧисло</name>
<valueType>
<v8:Type>decimal</v8:Type>
</valueType>
<value xsi:type="xs:decimal">0</value>
</parameter>
<parameter>
<name>ПараметрБулево</name>
<valueType>
<v8:Type>xs:boolean</v8:Type>
</valueType>
<value xsi:type="xs:boolean">false</value>
</parameter>
<parameter>
<name>ПараметрСтандартныйПериод</name>
<valueType>
<v8:Type>v8:StandardPeriod</v8:Type>
</valueType>
<value xsi:type="v8:StandardPeriod">
<v8:variant xsi:type="v8:StandardPeriodVariant">Custom</v8:variant>
<v8:startDate>0001-01-01T00:00:00</v8:startDate>
<v8:endDate>0001-01-01T00:00:00</v8:endDate>
</value>
</parameter>
<parameter>
<name>ПараметрТипНеЗадан</name>
<value xsi:nil="true"/>
</parameter>
<parameter>
<name>ПараметрСписокСтрок</name>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>0</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
<valueListAllowed>true</valueListAllowed>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:selection>
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>
@@ -71,6 +71,11 @@
<valueType>
<v8:Type>v8:StandardPeriod</v8:Type>
</valueType>
<value xsi:type="v8:StandardPeriod">
<v8:variant xsi:type="v8:StandardPeriodVariant">Custom</v8:variant>
<v8:startDate>0001-01-01T00:00:00</v8:startDate>
<v8:endDate>0001-01-01T00:00:00</v8:endDate>
</value>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
@@ -104,6 +104,7 @@
<valueType>
<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:CatalogRef.Организации</v8:Type>
</valueType>
<value xsi:nil="true"/>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>