mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-11 00:14:56 +03:00
541 lines
20 KiB
Python
541 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
|
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import sys
|
|
from lxml import etree
|
|
|
|
# --- Namespace maps ---
|
|
|
|
MD_NSMAP = {
|
|
"md": "http://v8.1c.ru/8.3/MDClasses",
|
|
"xr": "http://v8.1c.ru/8.3/xcf/readable",
|
|
}
|
|
|
|
FORM_NSMAP = {
|
|
"f": "http://v8.1c.ru/8.3/xcf/logform",
|
|
}
|
|
|
|
# --- Type -> directory mapping ---
|
|
|
|
CHILD_TYPE_DIR_MAP = {
|
|
"Catalog": "Catalogs",
|
|
"Document": "Documents",
|
|
"Enum": "Enums",
|
|
"CommonModule": "CommonModules",
|
|
"CommonPicture": "CommonPictures",
|
|
"CommonCommand": "CommonCommands",
|
|
"CommonTemplate": "CommonTemplates",
|
|
"ExchangePlan": "ExchangePlans",
|
|
"Report": "Reports",
|
|
"DataProcessor": "DataProcessors",
|
|
"InformationRegister": "InformationRegisters",
|
|
"AccumulationRegister": "AccumulationRegisters",
|
|
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
|
|
"ChartOfAccounts": "ChartsOfAccounts",
|
|
"AccountingRegister": "AccountingRegisters",
|
|
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
|
|
"CalculationRegister": "CalculationRegisters",
|
|
"BusinessProcess": "BusinessProcesses",
|
|
"Task": "Tasks",
|
|
"Subsystem": "Subsystems",
|
|
"Role": "Roles",
|
|
"Constant": "Constants",
|
|
"FunctionalOption": "FunctionalOptions",
|
|
"DefinedType": "DefinedTypes",
|
|
"FunctionalOptionsParameter": "FunctionalOptionsParameters",
|
|
"CommonForm": "CommonForms",
|
|
"DocumentJournal": "DocumentJournals",
|
|
"SessionParameter": "SessionParameters",
|
|
"StyleItem": "StyleItems",
|
|
"EventSubscription": "EventSubscriptions",
|
|
"ScheduledJob": "ScheduledJobs",
|
|
"SettingsStorage": "SettingsStorages",
|
|
"FilterCriterion": "FilterCriteria",
|
|
"CommandGroup": "CommandGroups",
|
|
"DocumentNumerator": "DocumentNumerators",
|
|
"Sequence": "Sequences",
|
|
"IntegrationService": "IntegrationServices",
|
|
"CommonAttribute": "CommonAttributes",
|
|
}
|
|
|
|
|
|
# --- Helper: check if object is borrowed ---
|
|
|
|
def get_object_info(obj_type, obj_name, extension_path):
|
|
if obj_type not in CHILD_TYPE_DIR_MAP:
|
|
return None
|
|
dir_name = CHILD_TYPE_DIR_MAP[obj_type]
|
|
obj_file = os.path.join(extension_path, dir_name, f"{obj_name}.xml")
|
|
|
|
if not os.path.isfile(obj_file):
|
|
return {"Borrowed": False, "File": obj_file, "Exists": False}
|
|
|
|
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
doc = etree.parse(obj_file, parser_xml)
|
|
doc_root = doc.getroot()
|
|
|
|
# Find first element child
|
|
obj_el = None
|
|
for c in doc_root:
|
|
if isinstance(c.tag, str):
|
|
obj_el = c
|
|
break
|
|
|
|
if obj_el is None:
|
|
return {"Borrowed": False, "File": obj_file, "Exists": True}
|
|
|
|
props_el = obj_el.find("md:Properties", MD_NSMAP)
|
|
ob_node = None
|
|
if props_el is not None:
|
|
ob_node = props_el.find("md:ObjectBelonging", MD_NSMAP)
|
|
|
|
borrowed = ob_node is not None and ob_node.text == "Adopted"
|
|
|
|
return {
|
|
"Borrowed": borrowed,
|
|
"File": obj_file,
|
|
"Exists": True,
|
|
"Type": obj_type,
|
|
"Name": obj_name,
|
|
"DirName": dir_name,
|
|
"ObjElement": obj_el,
|
|
}
|
|
|
|
|
|
# --- Helper: find .bsl files for object ---
|
|
|
|
def get_bsl_files(obj_type, obj_name, extension_path):
|
|
if obj_type not in CHILD_TYPE_DIR_MAP:
|
|
return []
|
|
dir_name = CHILD_TYPE_DIR_MAP[obj_type]
|
|
obj_dir = os.path.join(extension_path, dir_name, obj_name)
|
|
|
|
if not os.path.isdir(obj_dir):
|
|
return []
|
|
|
|
bsl_files = []
|
|
ext_dir = os.path.join(obj_dir, "Ext")
|
|
if os.path.isdir(ext_dir):
|
|
for item in os.listdir(ext_dir):
|
|
if item.lower().endswith(".bsl"):
|
|
bsl_files.append(os.path.join(ext_dir, item))
|
|
|
|
# Forms
|
|
forms_dir = os.path.join(obj_dir, "Forms")
|
|
if os.path.isdir(forms_dir):
|
|
for dirpath, dirnames, filenames in os.walk(forms_dir):
|
|
for fn in filenames:
|
|
if fn == "Module.bsl":
|
|
bsl_files.append(os.path.join(dirpath, fn))
|
|
|
|
return bsl_files
|
|
|
|
|
|
# --- Helper: parse interceptors from .bsl ---
|
|
|
|
def get_interceptors(bsl_path):
|
|
if not os.path.isfile(bsl_path):
|
|
return []
|
|
|
|
with open(bsl_path, "r", encoding="utf-8-sig") as fh:
|
|
lines = fh.readlines()
|
|
|
|
interceptors = []
|
|
pattern = re.compile(r'^&(\u041f\u0435\u0440\u0435\u0434|\u041f\u043e\u0441\u043b\u0435|\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c|\u0412\u043c\u0435\u0441\u0442\u043e)\("([^"]+)"\)')
|
|
# The above is: ^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)
|
|
|
|
for i, line in enumerate(lines):
|
|
stripped = line.strip()
|
|
m = pattern.match(stripped)
|
|
if m:
|
|
interceptors.append({
|
|
"Type": m.group(1),
|
|
"Method": m.group(2),
|
|
"Line": i + 1,
|
|
"File": bsl_path,
|
|
})
|
|
|
|
return interceptors
|
|
|
|
|
|
# --- Helper: extract #Вставка blocks from .bsl ---
|
|
|
|
def get_insertion_blocks(bsl_path):
|
|
if not os.path.isfile(bsl_path):
|
|
return []
|
|
|
|
with open(bsl_path, "r", encoding="utf-8-sig") as fh:
|
|
lines = fh.readlines()
|
|
|
|
blocks = []
|
|
in_block = False
|
|
block_lines = []
|
|
start_line = 0
|
|
|
|
for i, line in enumerate(lines):
|
|
stripped = line.strip()
|
|
if stripped == "\u0023\u0412\u0441\u0442\u0430\u0432\u043a\u0430":
|
|
# #Вставка
|
|
in_block = True
|
|
block_lines = []
|
|
start_line = i + 1
|
|
elif stripped == "\u0023\u041a\u043e\u043d\u0435\u0446\u0412\u0441\u0442\u0430\u0432\u043a\u0438" and in_block:
|
|
# #КонецВставки
|
|
in_block = False
|
|
blocks.append({
|
|
"StartLine": start_line,
|
|
"EndLine": i + 1,
|
|
"Code": "\n".join(block_lines).strip(),
|
|
"File": bsl_path,
|
|
})
|
|
elif in_block:
|
|
block_lines.append(line.rstrip("\n").rstrip("\r"))
|
|
|
|
return blocks
|
|
|
|
|
|
# --- Helper: analyze form for callType events and commands ---
|
|
|
|
def get_form_interceptors(form_xml_path):
|
|
if not os.path.isfile(form_xml_path):
|
|
return None
|
|
|
|
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
try:
|
|
doc = etree.parse(form_xml_path, parser_xml)
|
|
except Exception:
|
|
return None
|
|
|
|
f_root = doc.getroot()
|
|
base_form = f_root.find("f:BaseForm", FORM_NSMAP)
|
|
is_borrowed = base_form is not None
|
|
|
|
interceptors = []
|
|
|
|
# Form-level events with callType
|
|
events_node = f_root.find("f:Events", FORM_NSMAP)
|
|
if events_node is not None:
|
|
for evt in events_node.findall("f:Event", FORM_NSMAP):
|
|
ct = evt.get("callType", "")
|
|
if ct:
|
|
evt_name = evt.get("name", "")
|
|
evt_text = evt.text or ""
|
|
interceptors.append(f"Event:{evt_name} [{ct}] -> {evt_text}")
|
|
|
|
# Element-level events with callType (scan all elements recursively)
|
|
child_items = f_root.find("f:ChildItems", FORM_NSMAP)
|
|
if child_items is not None:
|
|
# Walk all descendant elements looking for Events/Event[@callType]
|
|
f_ns = FORM_NSMAP["f"]
|
|
for el in child_items.iter():
|
|
if not isinstance(el.tag, str):
|
|
continue
|
|
el_name = el.get("name", "")
|
|
if not el_name:
|
|
continue
|
|
events_sub = el.find(f"{{{f_ns}}}Events")
|
|
if events_sub is None:
|
|
continue
|
|
for evt in events_sub.findall(f"{{{f_ns}}}Event"):
|
|
ct = evt.get("callType", "")
|
|
if ct:
|
|
evt_name = evt.get("name", "")
|
|
evt_text = evt.text or ""
|
|
interceptors.append(f"Element:{el_name}.{evt_name} [{ct}] -> {evt_text}")
|
|
|
|
# Commands with callType on Action
|
|
f_ns = FORM_NSMAP["f"]
|
|
cmds_node = f_root.find(f"{{{f_ns}}}Commands")
|
|
if cmds_node is not None:
|
|
for cmd in cmds_node.findall(f"{{{f_ns}}}Command"):
|
|
cmd_name = cmd.get("name", "")
|
|
for action in cmd.findall(f"{{{f_ns}}}Action"):
|
|
ct = action.get("callType", "")
|
|
if ct:
|
|
action_text = action.text or ""
|
|
interceptors.append(f"Command:{cmd_name} [{ct}] -> {action_text}")
|
|
|
|
return {
|
|
"IsBorrowed": is_borrowed,
|
|
"Interceptors": interceptors,
|
|
}
|
|
|
|
|
|
# --- Mode A: Extension overview ---
|
|
|
|
def mode_a(objects, extension_path):
|
|
borrowed_list = []
|
|
own_list = []
|
|
|
|
for obj in objects:
|
|
info = get_object_info(obj["Type"], obj["Name"], extension_path)
|
|
if info is None:
|
|
print(f" [?] {obj['Type']}.{obj['Name']} \u2014 unknown type")
|
|
continue
|
|
if not info["Exists"]:
|
|
print(f" [?] {obj['Type']}.{obj['Name']} \u2014 file not found")
|
|
continue
|
|
|
|
if info["Borrowed"]:
|
|
borrowed_list.append(obj)
|
|
|
|
print(f" [BORROWED] {obj['Type']}.{obj['Name']}")
|
|
|
|
# Find .bsl files and interceptors
|
|
bsl_files = get_bsl_files(obj["Type"], obj["Name"], extension_path)
|
|
for bsl in bsl_files:
|
|
rel_path = bsl.replace(extension_path, "").lstrip("\\/")
|
|
interceptor_list = get_interceptors(bsl)
|
|
if len(interceptor_list) > 0:
|
|
for ic in interceptor_list:
|
|
print(f' &{ic["Type"]}("{ic["Method"]}") \u2014 line {ic["Line"]} in {rel_path}')
|
|
else:
|
|
print(f" {rel_path} (no interceptors)")
|
|
|
|
# Check for own attributes/forms in ChildObjects
|
|
obj_el = info.get("ObjElement")
|
|
if obj_el is not None:
|
|
child_obj = obj_el.find("md:ChildObjects", MD_NSMAP)
|
|
if child_obj is not None:
|
|
own_attrs = 0
|
|
own_forms = 0
|
|
own_ts = 0
|
|
borrowed_items = 0
|
|
form_names = []
|
|
for c in child_obj:
|
|
if not isinstance(c.tag, str):
|
|
continue
|
|
ln = etree.QName(c.tag).localname
|
|
c_props = c.find("md:Properties", MD_NSMAP)
|
|
if c_props is not None:
|
|
c_ob = c_props.find("md:ObjectBelonging", MD_NSMAP)
|
|
if c_ob is not None and c_ob.text == "Adopted":
|
|
borrowed_items += 1
|
|
continue
|
|
if ln == "Attribute":
|
|
own_attrs += 1
|
|
elif ln == "TabularSection":
|
|
own_ts += 1
|
|
elif ln == "Form":
|
|
form_names.append(c.text or "")
|
|
own_forms += 1
|
|
|
|
parts = []
|
|
if own_attrs > 0:
|
|
parts.append(f"{own_attrs} own attrs")
|
|
if own_ts > 0:
|
|
parts.append(f"{own_ts} own TS")
|
|
if own_forms > 0:
|
|
parts.append(f"{own_forms} own forms")
|
|
if borrowed_items > 0:
|
|
parts.append(f"{borrowed_items} borrowed items")
|
|
if len(parts) > 0:
|
|
print(f" ChildObjects: {', '.join(parts)}")
|
|
|
|
# Analyze forms
|
|
for fn in form_names:
|
|
form_xml_path = os.path.join(
|
|
extension_path, info["DirName"], info["Name"],
|
|
"Forms", fn, "Ext", "Form.xml"
|
|
)
|
|
fi = get_form_interceptors(form_xml_path)
|
|
if fi is None:
|
|
print(f" Form.{fn} (?)")
|
|
continue
|
|
form_tag = "borrowed" if fi["IsBorrowed"] else "own"
|
|
if len(fi["Interceptors"]) > 0:
|
|
print(f" Form.{fn} ({form_tag}):")
|
|
for ic in fi["Interceptors"]:
|
|
print(f" {ic}")
|
|
else:
|
|
print(f" Form.{fn} ({form_tag})")
|
|
else:
|
|
own_list.append(obj)
|
|
print(f" [OWN] {obj['Type']}.{obj['Name']}")
|
|
|
|
# Brief info for own objects
|
|
obj_el = info.get("ObjElement")
|
|
if obj_el is not None:
|
|
child_obj = obj_el.find("md:ChildObjects", MD_NSMAP)
|
|
if child_obj is not None:
|
|
attrs = 0
|
|
forms = 0
|
|
ts = 0
|
|
for c in child_obj:
|
|
if not isinstance(c.tag, str):
|
|
continue
|
|
ln = etree.QName(c.tag).localname
|
|
if ln == "Attribute":
|
|
attrs += 1
|
|
elif ln == "TabularSection":
|
|
ts += 1
|
|
elif ln == "Form":
|
|
forms += 1
|
|
parts = []
|
|
if attrs > 0:
|
|
parts.append(f"{attrs} attrs")
|
|
if ts > 0:
|
|
parts.append(f"{ts} TS")
|
|
if forms > 0:
|
|
parts.append(f"{forms} forms")
|
|
if len(parts) > 0:
|
|
print(f" {', '.join(parts)}")
|
|
|
|
print("")
|
|
print(f"=== Summary: {len(borrowed_list)} borrowed, {len(own_list)} own objects ===")
|
|
|
|
|
|
# --- Mode B: Transfer check ---
|
|
|
|
def mode_b(objects, extension_path, config_path):
|
|
transferred = 0
|
|
not_transferred = 0
|
|
needs_review = 0
|
|
|
|
for obj in objects:
|
|
info = get_object_info(obj["Type"], obj["Name"], extension_path)
|
|
if info is None or not info["Exists"] or not info["Borrowed"]:
|
|
continue
|
|
|
|
# Find .bsl files with &ИзменениеИКонтроль
|
|
bsl_files = get_bsl_files(obj["Type"], obj["Name"], extension_path)
|
|
for bsl in bsl_files:
|
|
interceptor_list = get_interceptors(bsl)
|
|
mac_interceptors = [ic for ic in interceptor_list if ic["Type"] == "\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c"]
|
|
|
|
if len(mac_interceptors) == 0:
|
|
continue
|
|
|
|
for ic in mac_interceptors:
|
|
method_name = ic["Method"]
|
|
rel_bsl = bsl.replace(extension_path, "").lstrip("\\/")
|
|
|
|
# Find #Вставка blocks in this file
|
|
insert_blocks = get_insertion_blocks(bsl)
|
|
|
|
if len(insert_blocks) == 0:
|
|
print(f' [NEEDS_REVIEW] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 no #\u0412\u0441\u0442\u0430\u0432\u043a\u0430 blocks')
|
|
needs_review += 1
|
|
continue
|
|
|
|
# Find corresponding module in config
|
|
if obj["Type"] not in CHILD_TYPE_DIR_MAP:
|
|
continue
|
|
config_bsl = bsl.replace(extension_path, config_path)
|
|
|
|
if not os.path.isfile(config_bsl):
|
|
print(f' [NEEDS_REVIEW] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 config module not found')
|
|
needs_review += 1
|
|
continue
|
|
|
|
with open(config_bsl, "r", encoding="utf-8-sig") as fh:
|
|
config_content = fh.read()
|
|
|
|
all_transferred = True
|
|
for block in insert_blocks:
|
|
code = block["Code"]
|
|
if not code:
|
|
continue
|
|
|
|
# Normalize whitespace for comparison
|
|
code_norm = re.sub(r'\s+', ' ', code)
|
|
config_norm = re.sub(r'\s+', ' ', config_content)
|
|
|
|
if code_norm not in config_norm:
|
|
all_transferred = False
|
|
|
|
if all_transferred:
|
|
print(f' [TRANSFERRED] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 {len(insert_blocks)} block(s)')
|
|
transferred += 1
|
|
else:
|
|
print(f' [NOT_TRANSFERRED] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 some blocks not found in config')
|
|
not_transferred += 1
|
|
|
|
print("")
|
|
print(f"=== Transfer check: {transferred} transferred, {not_transferred} not transferred, {needs_review} needs review ===")
|
|
|
|
|
|
# --- Main ---
|
|
|
|
def main():
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
sys.stderr.reconfigure(encoding="utf-8")
|
|
parser = argparse.ArgumentParser(description="Analyze and compare 1C configuration extension (CFE)", allow_abbrev=False)
|
|
parser.add_argument("-ExtensionPath", required=True, help="Path to extension dump root")
|
|
parser.add_argument("-ConfigPath", required=True, help="Path to base config dump root")
|
|
parser.add_argument("-Mode", choices=["A", "B"], default="A", help="A=overview, B=transfer check")
|
|
args = parser.parse_args()
|
|
|
|
extension_path = args.ExtensionPath
|
|
config_path = args.ConfigPath
|
|
mode = args.Mode
|
|
|
|
# --- Resolve paths ---
|
|
if not os.path.isabs(extension_path):
|
|
extension_path = os.path.join(os.getcwd(), extension_path)
|
|
if not os.path.isabs(config_path):
|
|
config_path = os.path.join(os.getcwd(), config_path)
|
|
if os.path.isfile(extension_path):
|
|
extension_path = os.path.dirname(extension_path)
|
|
if os.path.isfile(config_path):
|
|
config_path = os.path.dirname(config_path)
|
|
|
|
ext_cfg = os.path.join(extension_path, "Configuration.xml")
|
|
src_cfg = os.path.join(config_path, "Configuration.xml")
|
|
if not os.path.isfile(ext_cfg):
|
|
print(f"Extension Configuration.xml not found: {ext_cfg}", file=sys.stderr)
|
|
sys.exit(1)
|
|
if not os.path.isfile(src_cfg):
|
|
print(f"Config Configuration.xml not found: {src_cfg}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# --- Parse extension Configuration.xml ---
|
|
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
ext_doc = etree.parse(ext_cfg, parser_xml)
|
|
ext_root = ext_doc.getroot()
|
|
|
|
ext_props = ext_root.find(".//md:Configuration/md:Properties", MD_NSMAP)
|
|
ext_name_node = ext_props.find("md:Name", MD_NSMAP) if ext_props is not None else None
|
|
ext_name = ext_name_node.text if ext_name_node is not None and ext_name_node.text else "?"
|
|
prefix_node = ext_props.find("md:NamePrefix", MD_NSMAP) if ext_props is not None else None
|
|
name_prefix = prefix_node.text if prefix_node is not None and prefix_node.text else ""
|
|
purpose_node = ext_props.find("md:ConfigurationExtensionPurpose", MD_NSMAP) if ext_props is not None else None
|
|
purpose = purpose_node.text if purpose_node is not None and purpose_node.text else "?"
|
|
|
|
print(f"=== cfe-diff Mode {mode}: {ext_name} ({purpose}) ===")
|
|
print(f" NamePrefix: {name_prefix}")
|
|
print("")
|
|
|
|
# --- Collect ChildObjects ---
|
|
child_obj_node = ext_root.find(".//md:Configuration/md:ChildObjects", MD_NSMAP)
|
|
if child_obj_node is None:
|
|
print("[WARN] No ChildObjects in extension")
|
|
sys.exit(0)
|
|
|
|
objects = []
|
|
for child in child_obj_node:
|
|
if not isinstance(child.tag, str):
|
|
continue
|
|
ln = etree.QName(child.tag).localname
|
|
if ln == "Language":
|
|
continue
|
|
objects.append({"Type": ln, "Name": child.text or ""})
|
|
|
|
if len(objects) == 0:
|
|
print("No objects (besides Language) in extension.")
|
|
sys.exit(0)
|
|
|
|
# --- Run selected mode ---
|
|
if mode == "A":
|
|
mode_a(objects, extension_path)
|
|
elif mode == "B":
|
|
mode_b(objects, extension_path, config_path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|