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:
Nick Shirokov
2026-08-17 20:59:39 +03:00
co-authored by Claude Opus 5
parent 89f65ca6a6
commit 8fe727e32f
112 changed files with 5045 additions and 38 deletions
@@ -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))