mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-18 17:20:22 +03:00
feat(cfe-borrow,cfe-patch-method,cfe-validate): модули заимствованных объектов и пометка расширенного свойства
Объект заимствуют, чтобы дописать в него модуль, — теперь пустой файл модуля создаётся навыком, а не руками. Тип с единственным модулем (CommonModule, HTTPService, WebService) получает его без указаний, остальные — по -Module; -Module None отменяет. Существующий файл не перезаписывается никогда. Вместе с файлом проставляется <xr:PropertyState> со State=Extended. Замер по лестнице платформ 8.3.20…8.5.1: элемент появился в формате 2.19 (8.3.26), ниже платформа молча выбрасывает его при загрузке — отсюда гейт по версии формата. Имя свойства равно базовому имени файла модуля, у заимствованной формы — Form. Правило владения одно: пометку ставит тот, кто создал файл, поэтому её ставит и cfe-patch-method. Заодно закрыта перезапись при повторном заимствовании: прямая ветка писала XML уже заимствованного объекта начисто, унося собственные реквизиты расширения и регистрацию формы, — молча, с успешным отчётом. Повторный вызов теперь безопасен и служит способом дозаимствовать модуль. cfe-validate сверяет «файл модуля ↔ пометка» в обе стороны (предупреждение: перекос платформа принимает, но выгрузка Конфигуратора так не выглядит). Проверено: полный регресс 827/0/8 (ps1) и 824/0/11 (py), гарды 5/5, сквозной раундтрип на 8.3.26 и 8.3.27 — InternalInfo совпадает с выгрузкой платформы. Закрывает #70. Разбор и эталоны выгрузки — Romandredan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
89f65ca6a6
commit
8fe727e32f
@@ -1,4 +1,4 @@
|
||||
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
|
||||
# cfe-patch-method v2.8 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -788,6 +788,100 @@ if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $Exte
|
||||
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
|
||||
if (-not (Test-Path $cfgFile)) { Write-Error "Configuration.xml не найден в расширении: $ExtensionPath"; exit 1 }
|
||||
|
||||
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
|
||||
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
|
||||
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
|
||||
# Копии этих функций есть в cfe-borrow (навыки автономны); держать их одинаковыми — сознательно.
|
||||
function Get-FormatRank([string]$ver) {
|
||||
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||
return 0
|
||||
}
|
||||
|
||||
function Detect-FormatVersion([string]$dir) {
|
||||
$d = $dir
|
||||
while ($d) {
|
||||
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||
$extPath = "$d.xml"
|
||||
if (Test-Path $extPath) {
|
||||
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||
}
|
||||
$cfgPath = Join-Path $d "Configuration.xml"
|
||||
if (Test-Path $cfgPath) {
|
||||
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
|
||||
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
|
||||
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
|
||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||
}
|
||||
$parent = Split-Path $d -Parent
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return "2.17"
|
||||
}
|
||||
|
||||
function Build-PropertyStateXml {
|
||||
param([string]$propertyName, [string]$indent)
|
||||
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
$sb.AppendLine("${indent}<xr:PropertyState>") | Out-Null
|
||||
$sb.AppendLine("${indent}`t<xr:Property>${propertyName}</xr:Property>") | Out-Null
|
||||
$sb.AppendLine("${indent}`t<xr:State>Extended</xr:State>") | Out-Null
|
||||
$sb.Append("${indent}</xr:PropertyState>") | Out-Null
|
||||
return $sb.ToString()
|
||||
}
|
||||
|
||||
function Set-PropertyStateFlag {
|
||||
param([string]$objFile, [string]$propertyName, [string]$formatVersion)
|
||||
|
||||
if ((Get-FormatRank $formatVersion) -lt 219) { return }
|
||||
if (-not (Test-Path $objFile)) { return }
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
$text = [System.IO.File]::ReadAllText($objFile, $enc)
|
||||
$nl = if ($text -match "`r`n") { "`r`n" } else { "`n" }
|
||||
|
||||
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||
# но они лежат ниже, внутри <ChildObjects>.
|
||||
$empty = [regex]::Match($text, '([ \t]*)<InternalInfo\s*/>')
|
||||
$open = [regex]::Match($text, '(?s)([ \t]*)<InternalInfo>(.*?)</InternalInfo>')
|
||||
|
||||
if ($empty.Success -and (-not $open.Success -or $empty.Index -lt $open.Index)) {
|
||||
$ind = $empty.Groups[1].Value
|
||||
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||
$replacement = "${ind}<InternalInfo>${nl}${block}${nl}${ind}</InternalInfo>"
|
||||
$text = $text.Remove($empty.Index, $empty.Length).Insert($empty.Index, $replacement)
|
||||
} elseif ($open.Success) {
|
||||
if ($open.Groups[2].Value -match "<xr:Property>$([regex]::Escape($propertyName))</xr:Property>") { return }
|
||||
$ind = $open.Groups[1].Value
|
||||
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||
$closeAt = $open.Index + $open.Length - "</InternalInfo>".Length - $ind.Length
|
||||
$text = $text.Insert($closeAt, "${block}${nl}")
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
[System.IO.File]::WriteAllText($objFile, $text, $enc)
|
||||
}
|
||||
|
||||
# Модуль формы сюда не попадает: у формы флаг называется Form и ставится при заимствовании,
|
||||
# а не при появлении модуля (замер на 8.3.26 — пустой модуль формы платформа не выгружает).
|
||||
function Get-ModuleFlagTarget {
|
||||
param([string[]]$relParts, [string]$extRoot)
|
||||
|
||||
if ($relParts.Count -ne 4 -or $relParts[2] -ne "Ext") { return $null }
|
||||
$prop = [System.IO.Path]::GetFileNameWithoutExtension($relParts[3])
|
||||
return @{
|
||||
File = (Join-Path (Join-Path $extRoot $relParts[0]) "$($relParts[1]).xml")
|
||||
Property = $prop
|
||||
}
|
||||
}
|
||||
|
||||
# --- Read NamePrefix ---
|
||||
$cfgDoc = New-Object System.Xml.XmlDocument
|
||||
$cfgDoc.PreserveWhitespace = $false
|
||||
@@ -1084,6 +1178,12 @@ if ($reuseRegionIdx -ge 0) {
|
||||
}
|
||||
}
|
||||
|
||||
# Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19).
|
||||
$flagTarget = Get-ModuleFlagTarget $relParts $ExtensionPath
|
||||
if ($flagTarget) {
|
||||
Set-PropertyStateFlag $flagTarget.File $flagTarget.Property (Detect-FormatVersion $ExtensionPath)
|
||||
}
|
||||
|
||||
Write-Host "[OK] Перехватчик &$decoratorRu(`"$MethodName`") — $placement"
|
||||
Write-Host " Файл: $extBsl"
|
||||
Write-Host " Процедура: $interceptorName($($method.ParamsText -replace '\s+', ' '))"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
|
||||
# cfe-patch-method v2.8 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -75,6 +75,99 @@ CONTEXT_RE = re.compile(
|
||||
)
|
||||
|
||||
|
||||
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
|
||||
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
|
||||
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
|
||||
# Копии этих функций есть в cfe-borrow (навыки автономны); держать их одинаковыми — сознательно.
|
||||
def detect_format_version(d):
|
||||
while d:
|
||||
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||
ext_path = d + ".xml"
|
||||
if os.path.isfile(ext_path):
|
||||
with open(ext_path, "r", encoding="utf-8-sig") as f:
|
||||
ext_head = f.read(2000)
|
||||
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
|
||||
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
|
||||
if m:
|
||||
return m.group(1)
|
||||
cfg_path = os.path.join(d, "Configuration.xml")
|
||||
if os.path.isfile(cfg_path):
|
||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||
head = f.read(2000)
|
||||
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
|
||||
if m:
|
||||
return m.group(1)
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return "2.17"
|
||||
|
||||
|
||||
def format_rank(ver):
|
||||
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||
|
||||
|
||||
def build_property_state_xml(property_name, indent):
|
||||
return "\n".join([
|
||||
f"{indent}<xr:PropertyState>",
|
||||
f"{indent}\t<xr:Property>{property_name}</xr:Property>",
|
||||
f"{indent}\t<xr:State>Extended</xr:State>",
|
||||
f"{indent}</xr:PropertyState>",
|
||||
])
|
||||
|
||||
|
||||
def set_property_state_flag(obj_file, property_name, format_version):
|
||||
if format_rank(format_version) < 219:
|
||||
return
|
||||
if not os.path.isfile(obj_file):
|
||||
return
|
||||
|
||||
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
|
||||
text = fh.read()
|
||||
nl = "\r\n" if "\r\n" in text else "\n"
|
||||
|
||||
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||
# но они лежат ниже, внутри <ChildObjects>.
|
||||
empty = re.search(r"([ \t]*)<InternalInfo\s*/>", text)
|
||||
opened = re.search(r"([ \t]*)<InternalInfo>(.*?)</InternalInfo>", text, re.S)
|
||||
|
||||
if empty and (not opened or empty.start() < opened.start()):
|
||||
ind = empty.group(1)
|
||||
block = build_property_state_xml(property_name, ind + "\t")
|
||||
replacement = f"{ind}<InternalInfo>{nl}{block}{nl}{ind}</InternalInfo>"
|
||||
text = text[:empty.start()] + replacement + text[empty.end():]
|
||||
elif opened:
|
||||
if re.search(rf"<xr:Property>{re.escape(property_name)}</xr:Property>", opened.group(2)):
|
||||
return
|
||||
ind = opened.group(1)
|
||||
block = build_property_state_xml(property_name, ind + "\t")
|
||||
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||
close_at = opened.end() - len("</InternalInfo>") - len(ind)
|
||||
text = text[:close_at] + block + nl + text[close_at:]
|
||||
else:
|
||||
return
|
||||
|
||||
with open(obj_file, "w", encoding="utf-8-sig", newline="") as fh:
|
||||
fh.write(text)
|
||||
|
||||
|
||||
# Модуль формы сюда не попадает: у формы флаг называется Form и ставится при заимствовании,
|
||||
# а не при появлении модуля (замер на 8.3.26 — пустой модуль формы платформа не выгружает).
|
||||
def get_module_flag_target(rel_parts, ext_root):
|
||||
if len(rel_parts) != 4 or rel_parts[2] != "Ext":
|
||||
return None
|
||||
prop = os.path.splitext(rel_parts[3])[0]
|
||||
return {
|
||||
"file": os.path.join(ext_root, rel_parts[0], f"{rel_parts[1]}.xml"),
|
||||
"property": prop,
|
||||
}
|
||||
|
||||
|
||||
def get_module_rel_path(module_path):
|
||||
parts = module_path.split(".")
|
||||
if len(parts) < 2:
|
||||
@@ -841,6 +934,12 @@ def main():
|
||||
|
||||
place_new(ext_bsl, ext_lines, ext_exists, method["chain"], core)
|
||||
|
||||
# Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19).
|
||||
flag_target = get_module_flag_target(rel_parts, extension_path)
|
||||
if flag_target:
|
||||
set_property_state_flag(flag_target["file"], flag_target["property"],
|
||||
detect_format_version(extension_path))
|
||||
|
||||
# emit summary
|
||||
placement = place_new.placement
|
||||
print('[OK] Перехватчик &%s("%s") — %s' % (decorator_ru, method_name, placement))
|
||||
|
||||
Reference in New Issue
Block a user