fix(meta-info): множества типов (v8:TypeSet) показываются как тип

Источники подписки на событие читались только из v8:Type. Если источник задан
множеством — определяемым типом или целым классом «все документы» — строки
«Источники» не было вовсе, и подписка выглядела так, будто ни на что не
срабатывает. В erp_8.3.24 так задана треть подписок: 151 из 492 вообще без
v8:Type, в acc_8.3.24 — 126 из 443.

Тот же дефект в типах реквизитов, и там он чаще. Вхождений
v8:TypeSet>cfg:DefinedType в Catalogs/Documents/InformationRegisters/
AccumulationRegisters ERP — 8841, из них 7575 (86%) указывают на определяемый
тип ровно с одним членом. АвансовыйОтчет печатал «Сумма
ОпределяемыйТип.ДенежнаяСуммаНеотрицательная», и было не видно, что это
Число(15,2): имя выглядит как ссылочный тип. Голый метатип-категория
(«ДокументСсылка» без точки = любой документ) отличался от конкретного
«ДокументСсылка.Заказ» только наличием точки посреди длинного имени.

Правило одно на все места: имя множества стоит в строке типа, а раскрытие —
ровно одной записью на уникальное множество, глоссарием в конце вывода.
Раскрывать в строке нельзя дважды: псевдоним повторяется в объекте десятками
раз (в АвансовомОтчете — 12), а в корпусе есть определяемые типы на 596 типов —
вывод упёрся бы в постраничник и съел хвост объекта. Выше порога
composedTypeThreshold вместо состава идут счётчик и готовая команда раскрытия.
Голый метатип получает суффикс «(все)»: разница становится словом, а не
пунктуацией. Характеристика ПВХ помечена как набор, определяемый данными, —
из выгрузки её состав не берётся в принципе.

Попутно того же класса:
- объектные типы (DocumentObject.X, *RecordSet.X, *Manager.X) печатались
  по-английски в выводе определяемого типа и по-русски в источниках подписки —
  одно понятие двумя видами; в карту добавлены недостающие виды, включая
  ConstantValueManager и ChartOfCalculationTypesObject;
- собственный вывод определяемого типа читал только v8:Type и молча терял
  v8:TypeSet — счётчик «Типы (N)» врал;
- корень конфигурации ищется климбом до Configuration.xml, общим с проверкой
  поддержки, а не фиксированным «на два уровня выше»;
- состав определяемого типа кэшируется по имени;
- нечитаемый файл типа назван нечитаемым, а не отсутствующим.

Проверено корпусом: 5129 прогонов по erp_8.3.24 и acc_8.3.24 в трёх режимах,
каждое расхождение с эталоном до правки классифицировано, необъяснённых нет.
Паритет портов — 5129 из 5129, 103912 строк совпали.

Проблему и замеры по подпискам сообщил автор PR #96.

Co-Authored-By: Шпаков Антон Александрович <shpakov.anton.job@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-09-20 17:34:27 +03:00
co-authored by Шпаков Антон Александрович Claude Opus 5
parent 77c797e6d0
commit 7cbc8e9b7f
2 changed files with 444 additions and 55 deletions
+226 -33
View File
@@ -1,4 +1,4 @@
# meta-info v1.14 — Compact summary of 1C metadata object # meta-info v1.15 — Compact summary of 1C metadata object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)] [CmdletBinding(PositionalBinding=$false)]
param( param(
@@ -107,6 +107,9 @@ $typeNameMap = @{
"ExternalDataSource"="Внешний источник данных"; "Table"="Таблица внешнего источника" "ExternalDataSource"="Внешний источник данных"; "Table"="Таблица внешнего источника"
} }
# Долгие виды сокращаем (ПВХ, РС/РН/РБ/РР): в списке на сорок реквизитов повторяющийся префикс
# съедает колонку и прячет отличающуюся часть — имя объекта. Аббревиатуры приняты как ВВОД в
# meta-compile/meta-edit (словарь typeSynonyms), поэтому строку из вывода можно подать обратно.
$refTypeMap = @{ $refTypeMap = @{
"CatalogRef"="СправочникСсылка"; "DocumentRef"="ДокументСсылка" "CatalogRef"="СправочникСсылка"; "DocumentRef"="ДокументСсылка"
"EnumRef"="ПеречислениеСсылка"; "ChartOfAccountsRef"="ПланСчетовСсылка" "EnumRef"="ПеречислениеСсылка"; "ChartOfAccountsRef"="ПланСчетовСсылка"
@@ -146,11 +149,32 @@ $objectTypeMap = @{
"CatalogObject"="СправочникОбъект"; "DocumentObject"="ДокументОбъект" "CatalogObject"="СправочникОбъект"; "DocumentObject"="ДокументОбъект"
"ChartOfAccountsObject"="ПланСчетовОбъект" "ChartOfAccountsObject"="ПланСчетовОбъект"
"ChartOfCharacteristicTypesObject"="ПВХОбъект" "ChartOfCharacteristicTypesObject"="ПВХОбъект"
"ChartOfCalculationTypesObject"="ПВРОбъект"
"BusinessProcessObject"="БизнесПроцессОбъект"; "TaskObject"="ЗадачаОбъект" "BusinessProcessObject"="БизнесПроцессОбъект"; "TaskObject"="ЗадачаОбъект"
"ExchangePlanObject"="ПланОбменаОбъект" "ExchangePlanObject"="ПланОбменаОбъект"
"InformationRegisterRecordSet"="НаборЗаписейРС" "InformationRegisterRecordSet"="НаборЗаписейРС"
"AccumulationRegisterRecordSet"="НаборЗаписейРН" "AccumulationRegisterRecordSet"="НаборЗаписейРН"
"AccountingRegisterRecordSet"="НаборЗаписейРБ" "AccountingRegisterRecordSet"="НаборЗаписейРБ"
"CalculationRegisterRecordSet"="НаборЗаписейРР"
"SequenceRecordSet"="НаборЗаписейПоследовательности"
"RecalculationRecordSet"="НаборЗаписейПерерасчета"
# Менеджеры встречаются в источниках подписок. Сиблинга-аббревиатуры у них нет
# (единственный ориентир — КонстантаМенеджерЗначения), поэтому имена полные.
"CatalogManager"="СправочникМенеджер"; "DocumentManager"="ДокументМенеджер"
"DocumentJournalManager"="ЖурналДокументовМенеджер"
"EnumManager"="ПеречислениеМенеджер"
"ConstantValueManager"="КонстантаМенеджерЗначения"
"InformationRegisterManager"="РегистрСведенийМенеджер"
"AccumulationRegisterManager"="РегистрНакопленияМенеджер"
"AccountingRegisterManager"="РегистрБухгалтерииМенеджер"
"CalculationRegisterManager"="РегистрРасчетаМенеджер"
"ChartOfAccountsManager"="ПланСчетовМенеджер"
"ChartOfCharacteristicTypesManager"="ПВХМенеджер"
"ChartOfCalculationTypesManager"="ПВРМенеджер"
"ExchangePlanManager"="ПланОбменаМенеджер"
"BusinessProcessManager"="БизнесПроцессМенеджер"
"TaskManager"="ЗадачаМенеджер"
"DataProcessorManager"="ОбработкаМенеджер"; "ReportManager"="ОтчетМенеджер"
} }
$numberPeriodMap = @{ $numberPeriodMap = @{
@@ -173,22 +197,142 @@ function Get-MLText($node) {
return "" return ""
} }
# Тип-множество: голое имя метатипа без `.Имя` означает ВСЕ ссылки этого класса # Корень конфигурации — ближайший каталог выше объекта, где лежит Configuration.xml (или
# (см. docs/meta-dsl-spec.md §«Тип-множество»). Конкретный тип всегда пишется с точкой, # Ext/ParentConfigurations.bin у выгрузки на поддержке). Фиксированное «на два уровня выше» тут
# поэтому «СправочникСсылка» без точки читается однозначно как обобщённый. # неверно: объект бывает подан из другого места дерева, а у внешней обработки корня нет вовсе.
function Find-ConfigRootDir {
$d = [System.IO.Path]::GetDirectoryName($ObjectPath)
for ($i = 0; $i -lt 8 -and $d; $i++) {
if ((Test-Path (Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin")) -or
(Test-Path (Join-Path $d "Configuration.xml"))) { return $d }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
# Множество типов (v8:TypeSet) — индирекция: в строке типа стоит имя, а состав лежит в другом
# файле (определяемый тип) или определяется данными (характеристика ПВХ). Раскрывать состав в
# строке типа нельзя дважды: один псевдоним встречается в объекте десятками раз (в АвансовомОтчете
# `ДенежнаяСумма*` — 12 раз), а в корпусе есть определяемые типы на 596 типов — вывод упёрся бы в
# постраничник и съел хвост объекта. Поэтому имя стоит встроенно, а раскрытие идёт ровно одной
# записью на уникальное множество, глоссарием в конце вывода.
$script:typeSetGlossary = New-Object System.Collections.Specialized.OrderedDictionary
$script:definedTypeCache = @{}
$script:configRootProbed = $false
$script:configRootDir = $null
# Состав определяемого типа сам может содержать множество. Внутрь раскрытия не углубляемся: там
# имя уже достаточно, а рекурсия по самоссылочному типу не завершилась бы.
$script:inTypeSetExpansion = $false
function Get-ConfigRootDir {
if (-not $script:configRootProbed) {
$script:configRootProbed = $true
$script:configRootDir = Find-ConfigRootDir
}
return $script:configRootDir
}
function Resolve-DefinedType([string]$dtName) {
if ($script:definedTypeCache.ContainsKey($dtName)) { return $script:definedTypeCache[$dtName] }
# Broken отличаем от Found: файл на месте, но не разобрался — сказать «файла нет» было бы
# враньём, а молчать — тем же тихим отказом, от которого лечим.
$res = @{ Found = $false; Broken = $false; Members = @() }
$root = Get-ConfigRootDir
if ($root) {
$dtPath = Join-Path (Join-Path $root "DefinedTypes") "$dtName.xml"
if (Test-Path -LiteralPath $dtPath) {
try {
[xml]$dtDoc = Get-Content -LiteralPath $dtPath -Encoding UTF8
$dtNs = New-Object System.Xml.XmlNamespaceManager($dtDoc.NameTable)
$dtNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$dtNs.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
$tn = $dtDoc.SelectSingleNode("/md:MetaDataObject/md:DefinedType/md:Properties/md:Type", $dtNs)
if ($tn) {
$res.Found = $true
$members = @()
$script:inTypeSetExpansion = $true
try {
foreach ($c in $tn.ChildNodes) {
if ($c.NodeType -ne 'Element') { continue }
# .LocalName у XML-адаптера PS перекрывается одноимённым атрибутом — get_LocalName().
switch ($c.get_LocalName()) {
'Type' { $members += Format-SingleType $c.InnerText $tn }
'TypeSet' { $members += Format-SingleTypeSet $c.InnerText }
}
}
} finally { $script:inTypeSetExpansion = $false }
$res.Members = $members
}
} catch { $res.Broken = $true }
if (-not $res.Found) { $res.Broken = $true }
}
}
$script:definedTypeCache[$dtName] = $res
return $res
}
function Register-TypeSet([string]$label, [string]$kind, [string]$name) {
if ($script:inTypeSetExpansion) { return }
if ($script:typeSetGlossary.Contains($label)) { return }
$script:typeSetGlossary.Add($label, @{ Kind = $kind; Name = $name })
}
# Тип-множество: голое имя метатипа без `.Имя` означает ВСЕ объекты этого класса
# (см. docs/meta-dsl-spec.md §«Тип-множество»). Конкретный тип всегда пишется с точкой, но
# отличать «все» от «один» по наличию точки посреди длинного имени — сигнал низкой различимости,
# поэтому обобщённый метатип получает суффикс «(все)»: разница становится словом, а не пунктуацией.
function Format-SingleTypeSet([string]$raw) { function Format-SingleTypeSet([string]$raw) {
$raw = $raw -replace '^d\d+p\d+:', 'cfg:' $raw = $raw -replace '^d\d+p\d+:', 'cfg:'
if ($raw -match '^cfg:DefinedType\.(.+)$') { return "ОпределяемыйТип.$($Matches[1])" } if ($raw -match '^cfg:DefinedType\.(.+)$') {
if ($raw -match '^cfg:Characteristic\.(.+)$') { return "Характеристика.$($Matches[1])" } $label = "ОпределяемыйТип.$($Matches[1])"
Register-TypeSet $label 'DefinedType' $Matches[1]
return $label
}
if ($raw -match '^cfg:Characteristic\.(.+)$') {
$label = "Характеристика.$($Matches[1])"
Register-TypeSet $label 'Characteristic' $Matches[1]
return $label
}
if ($raw -eq 'cfg:AnyRef') { return "ЛюбаяСсылка" } if ($raw -eq 'cfg:AnyRef') { return "ЛюбаяСсылка" }
if ($raw -eq 'cfg:AnyIBRef') { return "ЛюбаяСсылкаИБ" } if ($raw -eq 'cfg:AnyIBRef') { return "ЛюбаяСсылкаИБ" }
if ($raw -match '^cfg:(\w+Ref)$' -and $refTypeMap.ContainsKey($Matches[1])) { if ($raw -match '^cfg:(\w+Ref)$' -and $refTypeMap.ContainsKey($Matches[1])) {
return $refTypeMap[$Matches[1]] return "$($refTypeMap[$Matches[1]]) (все)"
}
if ($raw -match '^cfg:(\w+)$' -and $objectTypeMap.ContainsKey($Matches[1])) {
return "$($objectTypeMap[$Matches[1]]) (все)"
} }
if ($raw -match '^cfg:(.+)$') { return $Matches[1] } if ($raw -match '^cfg:(.+)$') { return $Matches[1] }
return $raw return $raw
} }
# Глоссарий множеств: одна запись на уникальное множество, в порядке первого упоминания.
function Get-TypeSetGlossaryLines {
$lines = @()
foreach ($label in $script:typeSetGlossary.Keys) {
$e = $script:typeSetGlossary[$label]
if ($e.Kind -eq 'Characteristic') {
$lines += " $label — набор определяется данными ПВХ"
continue
}
$dt = Resolve-DefinedType $e.Name
if (-not $dt.Found) {
$lines += if ($dt.Broken) { " $label — файл типа не разобран" } else { " $label — файла типа нет в выгрузке" }
continue
}
$members = @($dt.Members)
if ($members.Count -eq 0) { $lines += " $label — состав пуст"; continue }
if ($members.Count -le $script:composedTypeThreshold) {
$lines += " $label$($members -join ' | ')"
} else {
$lines += " $label — типов: $($members.Count)"
$lines += " раскрыть: meta-info -ObjectPath DefinedTypes/$($e.Name).xml -Mode full"
}
}
return $lines
}
function Format-Type($typeNode) { function Format-Type($typeNode) {
if (-not $typeNode) { return "" } if (-not $typeNode) { return "" }
$types = @() $types = @()
@@ -261,6 +405,12 @@ function Format-SingleType([string]$raw, $parentNode) {
if ($raw -match '^cfg:DefinedType\.(.+)$') { if ($raw -match '^cfg:DefinedType\.(.+)$') {
return "ОпределяемыйТип.$($Matches[1])" return "ОпределяемыйТип.$($Matches[1])"
} }
# cfg:DocumentObject.Xxx / cfg:InformationRegisterRecordSet.Xxx — объектные типы.
# Без карты они уходили в ветку «снять cfg:» и печатались по-английски, хотя в
# источниках подписки те же типы печатались по-русски: одно понятие двумя видами.
if ($raw -match '^cfg:(\w+)\.(.+)$' -and $objectTypeMap.ContainsKey($Matches[1])) {
return "$($objectTypeMap[$Matches[1]]).$($Matches[2])"
}
# Strip cfg: prefix for unknown # Strip cfg: prefix for unknown
if ($raw -match '^cfg:(.+)$') { if ($raw -match '^cfg:(.+)$') {
return $Matches[1] return $Matches[1]
@@ -582,6 +732,25 @@ function Format-SourceType([string]$raw) {
return $raw return $raw
} }
# Источники подписки задаются и списком типов (v8:Type), и множеством (v8:TypeSet — определяемый
# тип или целый класс «все документы»). В корпусе множеством задана треть подписок (в erp_8.3.24
# 151 из 492 — вообще без v8:Type), поэтому чтение только v8:Type оставляло их без строки
# «Источники»: подписка выглядела так, будто ни на что не срабатывает. Оба узла читаем одним
# XPath, в порядке документа — так же, как meta-decompile.
#
# Множества и явные типы возвращаем врозь: явных бывает много (в корпусе до 1353 в одной
# подписке), а множеств — не больше восьми. Поэтому в сводке множества называем всегда, а
# явные типы сворачиваем в счётчик: по одному числу не видно, что подписка бьёт по целому классу.
function Get-SubscriptionSourceList($sourceNode) {
$types = @()
$sets = @()
foreach ($t in $sourceNode.SelectNodes("v8:Type|v8:TypeSet", $ns)) {
if ($t.get_LocalName() -eq 'TypeSet') { $sets += Format-SingleTypeSet $t.InnerText }
else { $types += Format-SourceType $t.InnerText }
}
return [pscustomobject]@{ Types = $types; Sets = $sets }
}
function Get-HTTPEndpoints($childObjs) { function Get-HTTPEndpoints($childObjs) {
$result = @() $result = @()
foreach ($tpl in $childObjs.SelectNodes("md:URLTemplate", $ns)) { foreach ($tpl in $childObjs.SelectNodes("md:URLTemplate", $ns)) {
@@ -646,16 +815,9 @@ function Test-ExternalObjectRoot([string]$xmlPath) {
function Get-ObjectSupportStatus([string]$objUuid) { function Get-ObjectSupportStatus([string]$objUuid) {
try { try {
if (Test-ExternalObjectRoot $ObjectPath) { return $null } if (Test-ExternalObjectRoot $ObjectPath) { return $null }
# Walk up to the config root (dir with Configuration.xml or Ext/ParentConfigurations.bin). # Корень конфигурации ищем тем же климбом, что и состав определяемых типов.
$d = [System.IO.Path]::GetDirectoryName($ObjectPath) $root = Get-ConfigRootDir
$binPath = $null $binPath = if ($root) { Join-Path (Join-Path $root "Ext") "ParentConfigurations.bin" } else { $null }
for ($i = 0; $i -lt 8 -and $d; $i++) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand; break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" } if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" }
$bytes = [System.IO.File]::ReadAllBytes($binPath) $bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" } if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" }
@@ -996,9 +1158,13 @@ if (-not $drillDone) {
if ($mdType -eq "DefinedType") { if ($mdType -eq "DefinedType") {
$typeNode2 = $props.SelectSingleNode("md:Type", $ns) $typeNode2 = $props.SelectSingleNode("md:Type", $ns)
if ($typeNode2) { if ($typeNode2) {
# Оба узла, одним XPath и в порядке документа: v8:TypeSet внутри определяемого
# типа платформа допускает, а чтение только v8:Type МОЛЧА теряло его — счётчик
# «Типы (N)» врал.
$types = @() $types = @()
foreach ($t in $typeNode2.SelectNodes("v8:Type", $ns)) { foreach ($t in $typeNode2.SelectNodes("v8:Type|v8:TypeSet", $ns)) {
$types += Format-SingleType $t.InnerText $typeNode2 if ($t.get_LocalName() -eq 'TypeSet') { $types += Format-SingleTypeSet $t.InnerText }
else { $types += Format-SingleType $t.InnerText $typeNode2 }
} }
if ($types.Count -gt 0) { if ($types.Count -gt 0) {
Out "Типы ($($types.Count)): $($types -join ', ')" Out "Типы ($($types.Count)): $($types -join ', ')"
@@ -1061,8 +1227,17 @@ if (-not $drillDone) {
} }
$source = $props.SelectSingleNode("md:Source", $ns) $source = $props.SelectSingleNode("md:Source", $ns)
if ($source) { if ($source) {
$srcCount = $source.SelectNodes("v8:Type", $ns).Count # brief называет множества, а явные типы считает: по одному числу не видно, что
if ($srcCount -gt 0) { $esParts += "Источники: $srcCount" } # подписка срабатывает на целый класс документов или на состав определяемого типа.
$src = Get-SubscriptionSourceList $source
$srcTypes = @($src.Types); $srcSets = @($src.Sets)
$total = $srcTypes.Count + $srcSets.Count
if ($total -gt 0) {
$parts2 = @()
if ($srcSets.Count -gt 0) { $parts2 += $srcSets }
if ($srcTypes.Count -gt 0) { $parts2 += "явных типов: $($srcTypes.Count)" }
$esParts += "Источники ($total): $($parts2 -join ', ')"
}
} }
if ($esParts.Count -gt 0) { Out ($esParts -join " | ") } if ($esParts.Count -gt 0) { Out ($esParts -join " | ") }
} }
@@ -1185,9 +1360,13 @@ if (-not $drillDone) {
if ($mdType -eq "DefinedType") { if ($mdType -eq "DefinedType") {
$typeNode2 = $props.SelectSingleNode("md:Type", $ns) $typeNode2 = $props.SelectSingleNode("md:Type", $ns)
if ($typeNode2) { if ($typeNode2) {
# Оба узла, одним XPath и в порядке документа: v8:TypeSet внутри определяемого
# типа платформа допускает, а чтение только v8:Type МОЛЧА теряло его — счётчик
# «Типы (N)» врал.
$types = @() $types = @()
foreach ($t in $typeNode2.SelectNodes("v8:Type", $ns)) { foreach ($t in $typeNode2.SelectNodes("v8:Type|v8:TypeSet", $ns)) {
$types += Format-SingleType $t.InnerText $typeNode2 if ($t.get_LocalName() -eq 'TypeSet') { $types += Format-SingleTypeSet $t.InnerText }
else { $types += Format-SingleType $t.InnerText $typeNode2 }
} }
if ($types.Count -gt 0) { if ($types.Count -gt 0) {
Out "Типы ($($types.Count)):" Out "Типы ($($types.Count)):"
@@ -1250,16 +1429,18 @@ if (-not $drillDone) {
} }
$source = $props.SelectSingleNode("md:Source", $ns) $source = $props.SelectSingleNode("md:Source", $ns)
if ($source) { if ($source) {
$srcTypes = @() $src = Get-SubscriptionSourceList $source
foreach ($t in $source.SelectNodes("v8:Type", $ns)) { $srcTypes = @($src.Types); $srcSets = @($src.Sets)
$srcTypes += Format-SourceType $t.InnerText $total = $srcTypes.Count + $srcSets.Count
} if ($total -gt 0) {
if ($srcTypes.Count -gt 0) { Out "Источники ($total):"
if ($Mode -eq "full") { # full печатает всё: режим для этого и нужен, а длину держит постраничник.
Out "Источники ($($srcTypes.Count)):" # В overview явные типы сворачиваем в счётчик — их бывает больше тысячи.
foreach ($s in $srcTypes) { Out " $s" } $listTypes = ($Mode -eq "full") -or ($total -le $script:composedTypeThreshold)
} else { if ($listTypes) { foreach ($s in $srcTypes) { Out " $s" } }
Out "Источники ($($srcTypes.Count))" foreach ($s in $srcSets) { Out " $s" }
if (-not $listTypes -and $srcTypes.Count -gt 0) {
Out " и ещё явных типов: $($srcTypes.Count) (-Mode full)"
} }
} }
} }
@@ -1532,6 +1713,18 @@ if (-not $drillDone) {
} }
} }
# Глоссарий множеств — после всего остального: в строках типов стоят только имена, и это
# единственное место, где имя превращается в состав. brief не глоссируем: там типов реквизитов
# нет вовсе, а множества источников подписки он называет прямо в строке.
if ($Mode -ne "brief" -and $script:typeSetGlossary.Count -gt 0) {
$glossary = @(Get-TypeSetGlossaryLines)
if ($glossary.Count -gt 0) {
Out ""
Out "Множества типов в составе:"
foreach ($l in $glossary) { Out $l }
}
}
# --- Pagination and output --- # --- Pagination and output ---
$totalLines = $script:lines.Count $totalLines = $script:lines.Count
$outLines = $script:lines $outLines = $script:lines
+218 -22
View File
@@ -1,4 +1,4 @@
# meta-info v1.14 — Compact summary of 1C metadata object (Python port) # meta-info v1.15 — Compact summary of 1C metadata object (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import os import os
@@ -180,6 +180,9 @@ type_name_map = {
"ExternalDataSource": "Внешний источник данных", "Table": "Таблица внешнего источника", "ExternalDataSource": "Внешний источник данных", "Table": "Таблица внешнего источника",
} }
# Долгие виды сокращаем (ПВХ, РС/РН/РБ/РР): в списке на сорок реквизитов повторяющийся префикс
# съедает колонку и прячет отличающуюся часть — имя объекта. Аббревиатуры приняты как ВВОД в
# meta-compile/meta-edit (словарь typeSynonyms), поэтому строку из вывода можно подать обратно.
ref_type_map = { ref_type_map = {
"CatalogRef": "СправочникСсылка", "DocumentRef": "ДокументСсылка", "CatalogRef": "СправочникСсылка", "DocumentRef": "ДокументСсылка",
"EnumRef": "ПеречислениеСсылка", "ChartOfAccountsRef": "ПланСчетовСсылка", "EnumRef": "ПеречислениеСсылка", "ChartOfAccountsRef": "ПланСчетовСсылка",
@@ -219,11 +222,32 @@ object_type_map = {
"CatalogObject": "СправочникОбъект", "DocumentObject": "ДокументОбъект", "CatalogObject": "СправочникОбъект", "DocumentObject": "ДокументОбъект",
"ChartOfAccountsObject": "ПланСчетовОбъект", "ChartOfAccountsObject": "ПланСчетовОбъект",
"ChartOfCharacteristicTypesObject": "ПВХОбъект", "ChartOfCharacteristicTypesObject": "ПВХОбъект",
"ChartOfCalculationTypesObject": "ПВРОбъект",
"BusinessProcessObject": "БизнесПроцессОбъект", "TaskObject": "ЗадачаОбъект", "BusinessProcessObject": "БизнесПроцессОбъект", "TaskObject": "ЗадачаОбъект",
"ExchangePlanObject": "ПланОбменаОбъект", "ExchangePlanObject": "ПланОбменаОбъект",
"InformationRegisterRecordSet": "НаборЗаписейРС", "InformationRegisterRecordSet": "НаборЗаписейРС",
"AccumulationRegisterRecordSet": "НаборЗаписейРН", "AccumulationRegisterRecordSet": "НаборЗаписейРН",
"AccountingRegisterRecordSet": "НаборЗаписейРБ", "AccountingRegisterRecordSet": "НаборЗаписейРБ",
"CalculationRegisterRecordSet": "НаборЗаписейРР",
"SequenceRecordSet": "НаборЗаписейПоследовательности",
"RecalculationRecordSet": "НаборЗаписейПерерасчета",
# Менеджеры встречаются в источниках подписок. Сиблинга-аббревиатуры у них нет
# (единственный ориентир — КонстантаМенеджерЗначения), поэтому имена полные.
"CatalogManager": "СправочникМенеджер", "DocumentManager": "ДокументМенеджер",
"DocumentJournalManager": "ЖурналДокументовМенеджер",
"EnumManager": "ПеречислениеМенеджер",
"ConstantValueManager": "КонстантаМенеджерЗначения",
"InformationRegisterManager": "РегистрСведенийМенеджер",
"AccumulationRegisterManager": "РегистрНакопленияМенеджер",
"AccountingRegisterManager": "РегистрБухгалтерииМенеджер",
"CalculationRegisterManager": "РегистрРасчетаМенеджер",
"ChartOfAccountsManager": "ПланСчетовМенеджер",
"ChartOfCharacteristicTypesManager": "ПВХМенеджер",
"ChartOfCalculationTypesManager": "ПВРМенеджер",
"ExchangePlanManager": "ПланОбменаМенеджер",
"BusinessProcessManager": "БизнесПроцессМенеджер",
"TaskManager": "ЗадачаМенеджер",
"DataProcessorManager": "ОбработкаМенеджер", "ReportManager": "ОтчетМенеджер",
} }
number_period_map = { number_period_map = {
@@ -251,30 +275,143 @@ def get_ml_text(node):
return "" return ""
# Тип-множество: голое имя метатипа без `.Имя` означает ВСЕ ссылки этого класса # Корень конфигурации — ближайший каталог выше объекта, где лежит Configuration.xml (или
# (см. docs/meta-dsl-spec.md §«Тип-множество»). Конкретный тип всегда пишется с точкой, # Ext/ParentConfigurations.bin у выгрузки на поддержке). Фиксированное «на два уровня выше» тут
# поэтому «СправочникСсылка» без точки читается однозначно как обобщённый. # неверно: объект бывает подан из другого места дерева, а у внешней обработки корня нет вовсе.
def find_config_root_dir():
d = os.path.dirname(object_path)
for _ in range(8):
if not d:
break
if (os.path.exists(os.path.join(d, "Ext", "ParentConfigurations.bin"))
or os.path.exists(os.path.join(d, "Configuration.xml"))):
return d
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
# Множество типов (v8:TypeSet) — индирекция: в строке типа стоит имя, а состав лежит в другом
# файле (определяемый тип) или определяется данными (характеристика ПВХ). Раскрывать состав в
# строке типа нельзя дважды: один псевдоним встречается в объекте десятками раз (в АвансовомОтчете
# `ДенежнаяСумма*` — 12 раз), а в корпусе есть определяемые типы на 596 типов — вывод упёрся бы в
# постраничник и съел хвост объекта. Поэтому имя стоит встроенно, а раскрытие идёт ровно одной
# записью на уникальное множество, глоссарием в конце вывода.
type_set_glossary = {}
defined_type_cache = {}
_config_root_state = {"probed": False, "dir": None}
# Состав определяемого типа сам может содержать множество. Внутрь раскрытия не углубляемся: там
# имя уже достаточно, а рекурсия по самоссылочному типу не завершилась бы.
_in_type_set_expansion = {"on": False}
def get_config_root_dir():
if not _config_root_state["probed"]:
_config_root_state["probed"] = True
_config_root_state["dir"] = find_config_root_dir()
return _config_root_state["dir"]
def resolve_defined_type(dt_name):
if dt_name in defined_type_cache:
return defined_type_cache[dt_name]
# broken отличаем от found: файл на месте, но не разобрался — сказать «файла нет» было бы
# враньём, а молчать — тем же тихим отказом, от которого лечим.
res = {"found": False, "broken": False, "members": []}
root = get_config_root_dir()
if root:
dt_path = os.path.join(root, "DefinedTypes", f"{dt_name}.xml")
if os.path.isfile(dt_path):
try:
dt_root = etree.parse(dt_path, parser_xml).getroot()
tn = find(dt_root, "/md:MetaDataObject/md:DefinedType/md:Properties/md:Type")
if tn is not None:
res["found"] = True
members = []
_in_type_set_expansion["on"] = True
try:
for c in tn.xpath("v8:Type|v8:TypeSet", namespaces=NS):
if local_name(c) == "TypeSet":
members.append(format_single_type_set(inner_text(c)))
else:
members.append(format_single_type(inner_text(c), tn))
finally:
_in_type_set_expansion["on"] = False
res["members"] = members
except Exception:
res["broken"] = True
if not res["found"]:
res["broken"] = True
defined_type_cache[dt_name] = res
return res
def register_type_set(label, kind, name):
if _in_type_set_expansion["on"]:
return
if label in type_set_glossary:
return
type_set_glossary[label] = {"kind": kind, "name": name}
# Тип-множество: голое имя метатипа без `.Имя` означает ВСЕ объекты этого класса
# (см. docs/meta-dsl-spec.md §«Тип-множество»). Конкретный тип всегда пишется с точкой, но
# отличать «все» от «один» по наличию точки посреди длинного имени — сигнал низкой различимости,
# поэтому обобщённый метатип получает суффикс «(все)»: разница становится словом, а не пунктуацией.
def format_single_type_set(raw): def format_single_type_set(raw):
raw = re.sub(r'^d\d+p\d+:', 'cfg:', raw) raw = re.sub(r'^d\d+p\d+:', 'cfg:', raw)
m = re.match(r'^cfg:DefinedType\.(.+)$', raw) m = re.match(r'^cfg:DefinedType\.(.+)$', raw)
if m: if m:
return f"ОпределяемыйТип.{m.group(1)}" label = f"ОпределяемыйТип.{m.group(1)}"
register_type_set(label, "DefinedType", m.group(1))
return label
m = re.match(r'^cfg:Characteristic\.(.+)$', raw) m = re.match(r'^cfg:Characteristic\.(.+)$', raw)
if m: if m:
return f"Характеристика.{m.group(1)}" label = f"Характеристика.{m.group(1)}"
register_type_set(label, "Characteristic", m.group(1))
return label
if raw == "cfg:AnyRef": if raw == "cfg:AnyRef":
return "ЛюбаяСсылка" return "ЛюбаяСсылка"
if raw == "cfg:AnyIBRef": if raw == "cfg:AnyIBRef":
return "ЛюбаяСсылкаИБ" return "ЛюбаяСсылкаИБ"
m = re.match(r'^cfg:(\w+Ref)$', raw) m = re.match(r'^cfg:(\w+Ref)$', raw)
if m and m.group(1) in ref_type_map: if m and m.group(1) in ref_type_map:
return ref_type_map[m.group(1)] return f"{ref_type_map[m.group(1)]} (все)"
m = re.match(r'^cfg:(\w+)$', raw)
if m and m.group(1) in object_type_map:
return f"{object_type_map[m.group(1)]} (все)"
m = re.match(r'^cfg:(.+)$', raw) m = re.match(r'^cfg:(.+)$', raw)
if m: if m:
return m.group(1) return m.group(1)
return raw return raw
# Глоссарий множеств: одна запись на уникальное множество, в порядке первого упоминания.
def get_type_set_glossary_lines():
lines = []
for label, e in type_set_glossary.items():
if e["kind"] == "Characteristic":
lines.append(f" {label} — набор определяется данными ПВХ")
continue
dt = resolve_defined_type(e["name"])
if not dt["found"]:
lines.append(f" {label} — файл типа не разобран" if dt["broken"]
else f" {label} — файла типа нет в выгрузке")
continue
members = dt["members"]
if len(members) == 0:
lines.append(f" {label} — состав пуст")
continue
if len(members) <= COMPOSED_TYPE_THRESHOLD:
lines.append(f" {label}{' | '.join(members)}")
else:
lines.append(f" {label} — типов: {len(members)}")
lines.append(f" раскрыть: meta-info -ObjectPath DefinedTypes/{e['name']}.xml -Mode full")
return lines
def format_type(type_node_el): def format_type(type_node_el):
if type_node_el is None: if type_node_el is None:
return "" return ""
@@ -347,6 +484,12 @@ def format_single_type(raw, parent_node):
m = re.match(r'^cfg:DefinedType\.(.+)$', raw) m = re.match(r'^cfg:DefinedType\.(.+)$', raw)
if m: if m:
return f"ОпределяемыйТип.{m.group(1)}" return f"ОпределяемыйТип.{m.group(1)}"
# cfg:DocumentObject.Xxx / cfg:InformationRegisterRecordSet.Xxx — объектные типы.
# Без карты они уходили в ветку «снять cfg:» и печатались по-английски, хотя в
# источниках подписки те же типы печатались по-русски: одно понятие двумя видами.
m = re.match(r'^cfg:(\w+)\.(.+)$', raw)
if m and m.group(1) in object_type_map:
return f"{object_type_map[m.group(1)]}.{m.group(2)}"
# Strip cfg: prefix # Strip cfg: prefix
m = re.match(r'^cfg:(.+)$', raw) m = re.match(r'^cfg:(.+)$', raw)
if m: if m:
@@ -656,6 +799,26 @@ def format_source_type(raw):
return raw return raw
# Источники подписки задаются и списком типов (v8:Type), и множеством (v8:TypeSet — определяемый
# тип или целый класс «все документы»). В корпусе множеством задана треть подписок (в erp_8.3.24
# 151 из 492 — вообще без v8:Type), поэтому чтение только v8:Type оставляло их без строки
# «Источники»: подписка выглядела так, будто ни на что не срабатывает. Оба узла читаем одним
# XPath, в порядке документа — так же, как meta-decompile.
#
# Множества и явные типы возвращаем врозь: явных бывает много (в корпусе до 1353 в одной
# подписке), а множеств — не больше восьми. Поэтому в сводке множества называем всегда, а
# явные типы сворачиваем в счётчик: по одному числу не видно, что подписка бьёт по целому классу.
def get_subscription_source_list(source_node):
types = []
sets = []
for t in find_all(source_node, "v8:Type|v8:TypeSet"):
if local_name(t) == "TypeSet":
sets.append(format_single_type_set(inner_text(t)))
else:
types.append(format_source_type(inner_text(t)))
return types, sets
def get_http_endpoints(child_objs): def get_http_endpoints(child_objs):
result = [] result = []
for tpl in find_all(child_objs, "md:URLTemplate"): for tpl in find_all(child_objs, "md:URLTemplate"):
@@ -1053,9 +1216,15 @@ if not drill_done:
if md_type == "DefinedType": if md_type == "DefinedType":
type_node2 = find(props, "md:Type") type_node2 = find(props, "md:Type")
if type_node2 is not None: if type_node2 is not None:
# Оба узла, одним XPath и в порядке документа: v8:TypeSet внутри определяемого
# типа платформа допускает, а чтение только v8:Type МОЛЧА теряло его — счётчик
# «Типы (N)» врал.
types = [] types = []
for t in find_all(type_node2, "v8:Type"): for t in find_all(type_node2, "v8:Type|v8:TypeSet"):
types.append(format_single_type(inner_text(t), type_node2)) if local_name(t) == "TypeSet":
types.append(format_single_type_set(inner_text(t)))
else:
types.append(format_single_type(inner_text(t), type_node2))
if types: if types:
out(f"Типы ({len(types)}): {', '.join(types)}") out(f"Типы ({len(types)}): {', '.join(types)}")
@@ -1114,9 +1283,15 @@ if not drill_done:
es_parts.append(f"Обработчик: {h_name}") es_parts.append(f"Обработчик: {h_name}")
source = find(props, "md:Source") source = find(props, "md:Source")
if source is not None: if source is not None:
src_count = len(find_all(source, "v8:Type")) # brief называет множества, а явные типы считает: по одному числу не видно, что
if src_count > 0: # подписка срабатывает на целый класс документов или на состав определяемого типа.
es_parts.append(f"Источники: {src_count}") src_types, src_sets = get_subscription_source_list(source)
total = len(src_types) + len(src_sets)
if total > 0:
parts2 = list(src_sets)
if src_types:
parts2.append(f"явных типов: {len(src_types)}")
es_parts.append(f"Источники ({total}): {', '.join(parts2)}")
if es_parts: if es_parts:
out(" | ".join(es_parts)) out(" | ".join(es_parts))
@@ -1221,9 +1396,15 @@ if not drill_done:
if md_type == "DefinedType": if md_type == "DefinedType":
type_node2 = find(props, "md:Type") type_node2 = find(props, "md:Type")
if type_node2 is not None: if type_node2 is not None:
# Оба узла, одним XPath и в порядке документа: v8:TypeSet внутри определяемого
# типа платформа допускает, а чтение только v8:Type МОЛЧА теряло его — счётчик
# «Типы (N)» врал.
types = [] types = []
for t in find_all(type_node2, "v8:Type"): for t in find_all(type_node2, "v8:Type|v8:TypeSet"):
types.append(format_single_type(inner_text(t), type_node2)) if local_name(t) == "TypeSet":
types.append(format_single_type_set(inner_text(t)))
else:
types.append(format_single_type(inner_text(t), type_node2))
if types: if types:
out(f"Типы ({len(types)}):") out(f"Типы ({len(types)}):")
for t in types: for t in types:
@@ -1283,16 +1464,20 @@ if not drill_done:
out(f"Обработчик: {h_name}") out(f"Обработчик: {h_name}")
source = find(props, "md:Source") source = find(props, "md:Source")
if source is not None: if source is not None:
src_types = [] src_types, src_sets = get_subscription_source_list(source)
for t in find_all(source, "v8:Type"): total = len(src_types) + len(src_sets)
src_types.append(format_source_type(inner_text(t))) if total > 0:
if src_types: out(f"Источники ({total}):")
if mode == "full": # full печатает всё: режим для этого и нужен, а длину держит постраничник.
out(f"Источники ({len(src_types)}):") # В overview явные типы сворачиваем в счётчик — их бывает больше тысячи.
list_types = mode == "full" or total <= COMPOSED_TYPE_THRESHOLD
if list_types:
for s in src_types: for s in src_types:
out(f" {s}") out(f" {s}")
else: for s in src_sets:
out(f"Источники ({len(src_types)})") out(f" {s}")
if not list_types and src_types:
out(f" и ещё явных типов: {len(src_types)} (-Mode full)")
# HTTPService # HTTPService
if md_type == "HTTPService": if md_type == "HTTPService":
@@ -1542,6 +1727,17 @@ if not drill_done:
out("") out("")
out(f"Полный состав типов: {hints}") out(f"Полный состав типов: {hints}")
# Глоссарий множеств — после всего остального: в строках типов стоят только имена, и это
# единственное место, где имя превращается в состав. brief не глоссируем: там типов реквизитов
# нет вовсе, а множества источников подписки он называет прямо в строке.
if mode != "brief" and type_set_glossary:
glossary = get_type_set_glossary_lines()
if glossary:
out("")
out("Множества типов в составе:")
for l in glossary:
out(l)
# ── Pagination and output ──────────────────────────────────── # ── Pagination and output ────────────────────────────────────
total_lines = len(lines) total_lines = len(lines)