feat(cfe-validate): проверка 17 — версия формата согласована внутри объектов расширения

Объекты расширения платформа загружает по тому же правилу, что и объекты конфигурации: дескриптор
и его штампованные тела (у заимствованной формы — ещё и вложенный <BaseForm version=…>) только в
одной версии формата, иначе «Версия формата загружаемого файла … отличается от версии формата ранее
загруженных файлов». Проверено загрузкой расширения на 8.3.27.1859: тело формы или <BaseForm> не в
версии дескриптора — отказ; заимствованный объект целиком в 2.18 в расширении 2.20 загружается.
Первое — ошибка, второе — предупреждение о неоднородной выгрузке. Так же сверяются Configuration.xml
расширения и его Ext/*.xml.

Co-Authored-By: Sergei Pleshanov <2357qwr@gmail.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-09-26 22:35:22 +03:00
co-authored by Sergei Pleshanov Claude Opus 5.5
parent fbcf65e12d
commit 88eca0b000
103 changed files with 2943 additions and 3 deletions
@@ -1,4 +1,4 @@
# cfe-validate v1.17 — Validate 1C configuration extension structure (CFE)
# cfe-validate v1.18 — Validate 1C configuration extension structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
@@ -107,6 +107,21 @@ function Get-FormatRank([string]$ver) {
return 0
}
# Штамп версии формата — атрибут version КОРНЕВОГО элемента файла. Копия общего эталона (семья
# root_version, авторитет — meta-validate). Корневого: в Form.xml расширения ниже стоит <BaseForm version=…>.
function Get-RootVersion([string]$xmlPath) {
if (-not (Test-Path -LiteralPath $xmlPath -PathType Leaf)) { return $null }
$buf = New-Object byte[] 4096
$fs = [System.IO.File]::OpenRead($xmlPath)
try { $len = $fs.Read($buf, 0, $buf.Length) } finally { $fs.Dispose() }
$head = [System.Text.Encoding]::UTF8.GetString($buf, 0, $len)
$m = [regex]::Match($head, '<[A-Za-z_][\w.:-]*(\s[^>]*)?/?>')
if (-not $m.Success) { return $null }
$v = [regex]::Match($m.Groups[1].Value, '(?:^|\s)version="([^"]*)"')
if ($v.Success) { return $v.Groups[1].Value }
return $null
}
# --- Reference tables ---
# Модули заимствованных объектов: тип → виды модулей. Имя свойства в <xr:PropertyState>
# совпадает с базовым именем файла модуля. Копия таблицы есть в cfe-borrow (навыки автономны).
@@ -1299,6 +1314,79 @@ if ($versionRank -ge 219 -and $childObjNode) {
}
}
# --- Check 17: версия формата согласована внутри объектов расширения; сверка с Configuration.xml ---
# Дескриптор объекта и его штампованные тела X/Ext/*.xml, дескриптор формы/макета и его тело (и
# Configuration.xml с его Ext/*.xml) платформа загружает только в одной версии формата: «Версия формата
# загружаемого файла … отличается от версии формата ранее загруженных файлов». Объект целиком в другой
# версии, чем расширение, загружается — это лишь неоднородность выгрузки (типично после мержа веток,
# выгруженных разными платформами), поэтому такое расхождение только предупреждение.
if ($version) {
# Каждый элемент — пара: дескриптор и его версия, известная заранее (у Configuration.xml — своя).
$verDescriptors = @($resolvedPath)
if ($childObjNode) {
foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
if (-not $childTypeDirMap.ContainsKey($child.LocalName)) { continue }
$verObjName = $child.InnerText.Trim()
if (-not $verObjName) { continue }
$verTypeDir = Join-Path $configDir $childTypeDirMap[$child.LocalName]
$verObjFile = Join-Path $verTypeDir "$verObjName.xml"
if (-not (Test-Path $verObjFile)) { continue }
$verDescriptors += $verObjFile
foreach ($sub in @("Forms","Templates")) {
$subDir = Join-Path (Join-Path $verTypeDir $verObjName) $sub
if (-not (Test-Path $subDir -PathType Container)) { continue }
$names = @(Get-ChildItem $subDir -Filter "*.xml" -File | ForEach-Object { $_.Name })
[Array]::Sort($names, [StringComparer]::Ordinal)
foreach ($n in $names) { $verDescriptors += (Join-Path $subDir $n) }
}
}
}
$verRel = { param($p) $p.Substring($configDir.Length).TrimStart('\', '/') -replace '\\', '/' }
$verErrors = 0
$verBodiesOk = 0
$verOff = @()
foreach ($desc in $verDescriptors) {
if ($script:stopped) { break }
$isRoot = ($desc -eq $resolvedPath)
$descVer = if ($isRoot) { $version } else { Get-RootVersion $desc }
if (-not $descVer) { continue }
if (-not $isRoot -and $descVer -ne $version) { $verOff += "$(& $verRel $desc) $descVer" }
# Тела Configuration.xml лежат в <корень>/Ext, тела объекта X.xml — в X/Ext.
$extDir = if ($isRoot) { Join-Path $configDir "Ext" } else { Join-Path (Join-Path (Split-Path $desc) ([System.IO.Path]::GetFileNameWithoutExtension($desc))) "Ext" }
if (-not (Test-Path $extDir -PathType Container)) { continue }
$bodyNames = @(Get-ChildItem $extDir -Filter "*.xml" -File | ForEach-Object { $_.Name })
[Array]::Sort($bodyNames, [StringComparer]::Ordinal)
foreach ($bn in $bodyNames) {
$body = Join-Path $extDir $bn
$bodyVer = Get-RootVersion $body
if (-not $bodyVer) { continue }
# У заимствованной формы второй штамп — <BaseForm version=…> внутри тела; платформа сверяет
# с дескриптором и его.
if ($bn -ceq "Form.xml") {
$bodyText = [System.IO.File]::ReadAllText($body, [System.Text.Encoding]::UTF8)
$bfm = [regex]::Match($bodyText, '<BaseForm\s[^>]*?version="([^"]*)"')
if ($bfm.Success -and $bfm.Groups[1].Value -ne $descVer) {
$verErrors++
Report-Error "17. $(& $verRel $body) <BaseForm> is stamped $($bfm.Groups[1].Value), its descriptor $(& $verRel $desc) $descVer — the platform refuses to load parts of one object in different formats"
if ($script:stopped) { break }
}
}
if ($bodyVer -eq $descVer) { $verBodiesOk++; continue }
$verErrors++
Report-Error "17. $(& $verRel $body) is stamped $bodyVer, its descriptor $(& $verRel $desc) $descVer — the platform refuses to load parts of one object in different formats"
if ($script:stopped) { break }
}
}
if ($verOff.Count -gt 0) {
$shown = ($verOff | Select-Object -First 5) -join ", "
if ($verOff.Count -gt 5) { $shown += ", … (+$($verOff.Count - 5))" }
Report-Warn "17. Format version differs from the extension ($version): $shown — the platform loads it, but the dump is no longer uniform (typical after merging branches dumped by different platforms)"
} elseif ($verErrors -eq 0 -and $verBodiesOk -gt 0) {
Report-OK "17. Format version: $verBodiesOk stamped part(s) agree with their descriptors and the extension"
}
}
# Built-in language keywords in both spellings. The platform accepts either one in any module
# (pairs taken from the platform string tables), so a module written in English is ordinary
# source, not a broken one: we must read both and emit the spelling we read.
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-validate v1.17 — Validate 1C configuration extension XML structure (CFE)
# cfe-validate v1.18 — Validate 1C configuration extension XML structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
import sys, os, argparse, re
@@ -235,6 +235,22 @@ def format_rank(ver):
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
# Штамп версии формата — атрибут version КОРНЕВОГО элемента файла. Копия общего эталона (семья
# root_version, авторитет — meta-validate). Корневого: в Form.xml расширения ниже стоит <BaseForm version=…>.
def root_version(xml_path):
if not os.path.isfile(xml_path):
return None
with open(xml_path, "rb") as f:
head = f.read(4096).decode("utf-8", errors="ignore")
m = re.search(r'<[A-Za-z_][\w.:-]*(\s[^>]*)?/?>', head)
if not m:
return None
v = re.search(r'(?:^|\s)version="([^"]*)"', m.group(1) or "")
if v:
return v.group(1)
return None
class Reporter:
def __init__(self, max_errors, detailed=False):
self.errors = 0
@@ -1302,6 +1318,96 @@ def main():
for issue in state_issues:
r.warn(f'16. {issue}')
# --- Check 17: версия формата согласована внутри объектов расширения; сверка с Configuration.xml ---
# Дескриптор объекта и его штампованные тела X/Ext/*.xml, дескриптор формы/макета и его тело (и
# Configuration.xml с его Ext/*.xml) платформа загружает только в одной версии формата: «Версия формата
# загружаемого файла … отличается от версии формата ранее загруженных файлов». Объект целиком в другой
# версии, чем расширение, загружается — это лишь неоднородность выгрузки (типично после мержа веток,
# выгруженных разными платформами), поэтому такое расхождение только предупреждение.
if version:
ver_descriptors = [resolved_path]
if child_obj_node is not None:
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
type_name = etree.QName(child.tag).localname
if type_name not in CHILD_TYPE_DIR_MAP:
continue
ver_obj_name = (child.text or '').strip()
if not ver_obj_name:
continue
ver_type_dir = os.path.join(config_dir, CHILD_TYPE_DIR_MAP[type_name])
ver_obj_file = os.path.join(ver_type_dir, f'{ver_obj_name}.xml')
if not os.path.isfile(ver_obj_file):
continue
ver_descriptors.append(ver_obj_file)
for sub in ('Forms', 'Templates'):
sub_dir = os.path.join(ver_type_dir, ver_obj_name, sub)
if not os.path.isdir(sub_dir):
continue
names = sorted(n for n in os.listdir(sub_dir)
if n.lower().endswith('.xml') and os.path.isfile(os.path.join(sub_dir, n)))
for n in names:
ver_descriptors.append(os.path.join(sub_dir, n))
def ver_rel(p):
return p[len(config_dir):].lstrip('\\/').replace('\\', '/')
ver_errors = 0
ver_bodies_ok = 0
ver_off = []
for desc in ver_descriptors:
if r.stopped:
break
is_root = desc == resolved_path
desc_ver = version if is_root else root_version(desc)
if not desc_ver:
continue
if not is_root and desc_ver != version:
ver_off.append(f'{ver_rel(desc)} {desc_ver}')
# Тела Configuration.xml лежат в <корень>/Ext, тела объекта X.xml — в X/Ext.
if is_root:
ext_dir = os.path.join(config_dir, 'Ext')
else:
ext_dir = os.path.join(os.path.dirname(desc), os.path.splitext(os.path.basename(desc))[0], 'Ext')
if not os.path.isdir(ext_dir):
continue
body_names = sorted(n for n in os.listdir(ext_dir)
if n.lower().endswith('.xml') and os.path.isfile(os.path.join(ext_dir, n)))
for bn in body_names:
body = os.path.join(ext_dir, bn)
body_ver = root_version(body)
if not body_ver:
continue
# У заимствованной формы второй штамп — <BaseForm version=…> внутри тела; платформа сверяет
# с дескриптором и его.
if bn == 'Form.xml':
with open(body, 'r', encoding='utf-8-sig') as f:
body_text = f.read()
bfm = re.search(r'<BaseForm\s[^>]*?version="([^"]*)"', body_text)
if bfm and bfm.group(1) != desc_ver:
ver_errors += 1
r.error(f'17. {ver_rel(body)} <BaseForm> is stamped {bfm.group(1)}, its descriptor '
f'{ver_rel(desc)} {desc_ver} — the platform refuses to load parts of one object in different formats')
if r.stopped:
break
if body_ver == desc_ver:
ver_bodies_ok += 1
continue
ver_errors += 1
r.error(f'17. {ver_rel(body)} is stamped {body_ver}, its descriptor {ver_rel(desc)} {desc_ver} '
'— the platform refuses to load parts of one object in different formats')
if r.stopped:
break
if ver_off:
shown = ', '.join(ver_off[:5])
if len(ver_off) > 5:
shown += f', … (+{len(ver_off) - 5})'
r.warn(f'17. Format version differs from the extension ({version}): {shown} — the platform loads it, '
'but the dump is no longer uniform (typical after merging branches dumped by different platforms)')
elif ver_errors == 0 and ver_bodies_ok > 0:
r.ok(f'17. Format version: {ver_bodies_ok} stamped part(s) agree with their descriptors and the extension')
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
ctrl_kw = bsl_keywords()
ctrl_re = re.compile(r'^\s*&(?:' + ctrl_kw["ru"]["Control"] + '|' + ctrl_kw["en"]["Control"] + r')\(', re.IGNORECASE)