mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-20 17:40:59 +03:00
Косметика вендора (пустые строки, строки-комментарии) больше не ломает классификацию: - пустая строка между якорем и уже-перенесённым кодом → раньше ДУБЛЬ, теперь ПЕРЕНЕСЕНО; - пустая/комментарий у якоря → раньше ложный конфликт, теперь переякоривание. Три шага разведены: - размещение якоря — сначала точно (комментарии/пустые включены, держит позицию вставки относительно стабильного комментария), затем fallback по значимым строкам; - поглощение — по значимым строкам (пустые/комментарии перешагиваем); вставку из одних комментариев/пустых не поглощаем; - вывод тела — всегда v2 дословно, все комментарии/пустые нового оригинала сохраняются. Пограничный случай (комментарий разработчика у поглощённого кода): значимый код поглощаем, осиротевший комментарий — строкой ⚠ в отчёте, не роняя в конфликт. Новые helper: Test-Significant/Get-SignificantProjection (+ py). Верхняя сверка АКТУАЛЕН остаётся точной (любой diff → перепись тела в v2). Зеркально ps1↔py, +3 кейса (blankskew/comment-stable/orphan-comment), 22/22 на обоих рантаймах. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1156 lines
50 KiB
Python
1156 lines
50 KiB
Python
#!/usr/bin/env python3
|
||
# cfe-patch-method v2.5 — Source-aware method interceptor for 1C extension (CFE)
|
||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||
|
||
import argparse
|
||
import os
|
||
import re
|
||
import shutil
|
||
import sys
|
||
import tempfile
|
||
import xml.etree.ElementTree as ET
|
||
|
||
TYPE_DIR_MAP = {
|
||
"Catalog": "Catalogs", "Document": "Documents", "Enum": "Enums",
|
||
"CommonModule": "CommonModules", "Report": "Reports", "DataProcessor": "DataProcessors",
|
||
"ExchangePlan": "ExchangePlans", "ChartOfAccounts": "ChartsOfAccounts",
|
||
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
|
||
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
|
||
"BusinessProcess": "BusinessProcesses", "Task": "Tasks",
|
||
"InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
|
||
"AccountingRegister": "AccountingRegisters", "CalculationRegister": "CalculationRegisters",
|
||
}
|
||
# accept plural forms too
|
||
for _v in list(TYPE_DIR_MAP.values()):
|
||
TYPE_DIR_MAP[_v] = _v
|
||
|
||
# InterceptorType -> Russian decorator keyword
|
||
DECORATOR_MAP = {
|
||
"Before": "Перед", "After": "После", "Instead": "Вместо",
|
||
"ModificationAndControl": "ИзменениеИКонтроль",
|
||
}
|
||
|
||
CONTEXT_RE = re.compile(
|
||
r'^&(НаКлиенте|НаСервере|НаСервереБезКонтекста|НаКлиентеНаСервереБезКонтекста|НаКлиентеНаСервере)\s*$'
|
||
)
|
||
|
||
|
||
def get_module_rel_path(module_path):
|
||
parts = module_path.split(".")
|
||
if len(parts) < 2:
|
||
raise ValueError(
|
||
"Invalid ModulePath format: %s. Expected: Type.Name.Module, "
|
||
"Type.Name.Form.FormName or CommonModule.Name" % module_path
|
||
)
|
||
obj_type = parts[0]
|
||
obj_name = parts[1]
|
||
if obj_type not in TYPE_DIR_MAP:
|
||
raise ValueError("Unknown object type: %s" % obj_type)
|
||
dir_name = TYPE_DIR_MAP[obj_type]
|
||
|
||
if obj_type == "CommonModule":
|
||
return [dir_name, obj_name, "Ext", "Module.bsl"]
|
||
if len(parts) >= 4 and parts[2] == "Form":
|
||
form_name = parts[3]
|
||
return [dir_name, obj_name, "Forms", form_name, "Ext", "Form", "Module.bsl"]
|
||
if len(parts) >= 3:
|
||
module_name = parts[2]
|
||
module_file_map = {
|
||
"ObjectModule": "ObjectModule.bsl",
|
||
"ManagerModule": "ManagerModule.bsl",
|
||
"RecordSetModule": "RecordSetModule.bsl",
|
||
"CommandModule": "CommandModule.bsl",
|
||
"ValueManagerModule": "ValueManagerModule.bsl",
|
||
}
|
||
return [dir_name, obj_name, "Ext", module_file_map.get(module_name, module_name + ".bsl")]
|
||
raise ValueError("Invalid ModulePath format: %s" % module_path)
|
||
|
||
|
||
def get_rel_parts_from_file_path(path):
|
||
segs = [s for s in path.replace("\\", "/").split("/") if s]
|
||
anchors = set(TYPE_DIR_MAP.values())
|
||
for i in range(len(segs)):
|
||
if segs[i] in anchors:
|
||
return segs[i:]
|
||
return None
|
||
|
||
|
||
def read_lines(path):
|
||
with open(path, "r", encoding="utf-8-sig", newline="") as f:
|
||
text = f.read()
|
||
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
||
lines = text.split("\n")
|
||
if lines and lines[-1] == "":
|
||
lines.pop()
|
||
return lines
|
||
|
||
|
||
def write_bsl(path, lines):
|
||
text = "\r\n".join(lines) + "\r\n"
|
||
with open(path, "w", encoding="utf-8-sig", newline="") as f:
|
||
f.write(text)
|
||
|
||
|
||
def split_top_level(text, sep=","):
|
||
result = []
|
||
depth = 0
|
||
in_str = False
|
||
buf = []
|
||
for ch in text:
|
||
if in_str:
|
||
buf.append(ch)
|
||
if ch == '"':
|
||
in_str = False
|
||
continue
|
||
if ch == '"':
|
||
in_str = True
|
||
buf.append(ch)
|
||
continue
|
||
if ch == '(':
|
||
depth += 1
|
||
buf.append(ch)
|
||
continue
|
||
if ch == ')':
|
||
depth -= 1
|
||
buf.append(ch)
|
||
continue
|
||
if ch == sep and depth == 0:
|
||
result.append("".join(buf))
|
||
buf = []
|
||
continue
|
||
buf.append(ch)
|
||
result.append("".join(buf))
|
||
return result
|
||
|
||
|
||
def is_context_directive(trimmed):
|
||
return bool(CONTEXT_RE.match(trimmed))
|
||
|
||
|
||
def read_signature(lines, start_idx):
|
||
"""Return (params_text, end_line_idx) or None."""
|
||
depth = 0
|
||
in_str = False
|
||
open_found = False
|
||
params = []
|
||
for li in range(start_idx, len(lines)):
|
||
line = lines[li]
|
||
for ch in line:
|
||
if in_str:
|
||
if open_found:
|
||
params.append(ch)
|
||
if ch == '"':
|
||
in_str = False
|
||
continue
|
||
if ch == '"':
|
||
in_str = True
|
||
if open_found:
|
||
params.append(ch)
|
||
continue
|
||
if ch == '(':
|
||
depth += 1
|
||
if not open_found:
|
||
open_found = True
|
||
else:
|
||
params.append(ch)
|
||
continue
|
||
if ch == ')':
|
||
depth -= 1
|
||
if depth == 0:
|
||
return ("".join(params), li)
|
||
params.append(ch)
|
||
continue
|
||
if open_found:
|
||
params.append(ch)
|
||
if open_found and depth >= 1:
|
||
params.append("\r\n")
|
||
return None
|
||
|
||
|
||
def effective_condition(frame):
|
||
conds = frame["conds"]
|
||
n = len(conds)
|
||
if frame["in_else"]:
|
||
return " И ".join("НЕ (%s)" % c for c in conds)
|
||
if n == 1:
|
||
return conds[0]
|
||
parts = ["НЕ (%s)" % conds[j] for j in range(n - 1)]
|
||
parts.append(conds[n - 1])
|
||
return " И ".join(parts)
|
||
|
||
|
||
def get_enclosing_chain(lines, target_idx):
|
||
stack = []
|
||
for i in range(target_idx):
|
||
t = lines[i].strip()
|
||
m = re.match(r'^#Область\s+(\S+)', t)
|
||
if m:
|
||
stack.append({"kind": "region", "name": m.group(1)})
|
||
continue
|
||
if re.match(r'^#КонецОбласти', t):
|
||
for k in range(len(stack) - 1, -1, -1):
|
||
if stack[k]["kind"] == "region":
|
||
del stack[k]
|
||
break
|
||
continue
|
||
m = re.match(r'^#Если\s+(.+?)\s+Тогда', t)
|
||
if m:
|
||
stack.append({"kind": "if", "conds": [m.group(1).strip()], "in_else": False})
|
||
continue
|
||
m = re.match(r'^#ИначеЕсли\s+(.+?)\s+Тогда', t)
|
||
if m:
|
||
for k in range(len(stack) - 1, -1, -1):
|
||
if stack[k]["kind"] == "if":
|
||
stack[k]["conds"].append(m.group(1).strip())
|
||
stack[k]["in_else"] = False
|
||
break
|
||
continue
|
||
if re.match(r'^#Иначе(\s|$)', t):
|
||
for k in range(len(stack) - 1, -1, -1):
|
||
if stack[k]["kind"] == "if":
|
||
stack[k]["in_else"] = True
|
||
break
|
||
continue
|
||
if re.match(r'^#КонецЕсли', t):
|
||
for k in range(len(stack) - 1, -1, -1):
|
||
if stack[k]["kind"] == "if":
|
||
del stack[k]
|
||
break
|
||
continue
|
||
chain = []
|
||
for f in stack:
|
||
if f["kind"] == "region":
|
||
chain.append({"kind": "region", "name": f["name"]})
|
||
else:
|
||
chain.append({"kind": "if", "cond": effective_condition(f)})
|
||
return chain
|
||
|
||
|
||
def extract_method(lines, method_name):
|
||
decl_re = re.compile(
|
||
r'^\s*(Асинх\s+)?(Процедура|Функция)\s+(' + re.escape(method_name) + r')\s*\(',
|
||
re.IGNORECASE,
|
||
)
|
||
for i in range(len(lines)):
|
||
m = decl_re.match(lines[i])
|
||
if not m:
|
||
continue
|
||
is_async = bool(m.group(1))
|
||
keyword = m.group(2)
|
||
canonical = m.group(3)
|
||
is_function = keyword.lower() == "функция"
|
||
|
||
sig = read_signature(lines, i)
|
||
if not sig:
|
||
raise ValueError("Не удалось разобрать сигнатуру метода '%s'" % method_name)
|
||
params_text, sig_end = sig
|
||
|
||
param_names = []
|
||
if params_text.strip():
|
||
for seg in split_top_level(params_text):
|
||
s = re.sub(r'^Знач\s+', '', seg.strip())
|
||
mm = re.match(r'^([\w]+)', s)
|
||
if mm:
|
||
param_names.append(mm.group(1))
|
||
|
||
end_re = re.compile(r'^\s*КонецФункции\b' if is_function else r'^\s*КонецПроцедуры\b',
|
||
re.IGNORECASE)
|
||
body_start = sig_end + 1
|
||
body_end = -1
|
||
for j in range(body_start, len(lines)):
|
||
if end_re.match(lines[j]):
|
||
body_end = j
|
||
break
|
||
if body_end < 0:
|
||
raise ValueError("Не найден конец метода '%s'" % method_name)
|
||
body_lines = lines[body_start:body_end]
|
||
|
||
context = ""
|
||
if i >= 1:
|
||
prev = lines[i - 1].strip()
|
||
if is_context_directive(prev):
|
||
context = prev
|
||
|
||
chain = get_enclosing_chain(lines, i)
|
||
|
||
return {
|
||
"canonical": canonical,
|
||
"is_function": is_function,
|
||
"is_async": is_async,
|
||
"params_text": params_text,
|
||
"param_names": param_names,
|
||
"context": context,
|
||
"body_lines": body_lines,
|
||
"chain": chain,
|
||
"decl_idx": i,
|
||
"sig_end_idx": sig_end,
|
||
"body_end_idx": body_end,
|
||
}
|
||
return None
|
||
|
||
|
||
def get_interceptors(lines):
|
||
result = []
|
||
pat = re.compile(r'^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)')
|
||
for i in range(len(lines)):
|
||
m = pat.match(lines[i].strip())
|
||
if m:
|
||
result.append({"type": m.group(1), "method": m.group(2), "line": i})
|
||
return result
|
||
|
||
|
||
def get_proc_names(lines):
|
||
names = []
|
||
pat = re.compile(r'^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(', re.IGNORECASE)
|
||
for line in lines:
|
||
m = pat.match(line)
|
||
if m:
|
||
names.append(m.group(1))
|
||
return names
|
||
|
||
|
||
def build_interceptor_core(method, interceptor_type, interceptor_name):
|
||
decorator_ru = DECORATOR_MAP[interceptor_type]
|
||
async_prefix = "Асинх " if method["is_async"] else ""
|
||
keyword = "Функция" if method["is_function"] else "Процедура"
|
||
end_keyword = "КонецФункции" if method["is_function"] else "КонецПроцедуры"
|
||
|
||
lines = []
|
||
if method["context"]:
|
||
lines.append(method["context"])
|
||
lines.append('&%s("%s")' % (decorator_ru, method["canonical"]))
|
||
lines.append("%s%s %s(%s)" % (async_prefix, keyword, interceptor_name, method["params_text"]))
|
||
|
||
if interceptor_type == "Before":
|
||
lines.append("\t// TODO: код перед вызовом оригинального метода")
|
||
elif interceptor_type == "After":
|
||
lines.append("\t// TODO: код после вызова оригинального метода")
|
||
elif interceptor_type == "Instead":
|
||
names_joined = ", ".join(method["param_names"])
|
||
if method["is_function"]:
|
||
lines.append("\tРезультат = ПродолжитьВызов(%s);" % names_joined)
|
||
lines.append("\t// TODO: доработать поведение")
|
||
lines.append("\tВозврат Результат;")
|
||
else:
|
||
lines.append("\tПродолжитьВызов(%s);" % names_joined)
|
||
lines.append("\t// TODO: доработать поведение")
|
||
elif interceptor_type == "ModificationAndControl":
|
||
lines.extend(method["body_lines"])
|
||
|
||
lines.append(end_keyword)
|
||
return lines
|
||
|
||
|
||
def build_wrapped_block(chain_arr, core):
|
||
"""Wrap core with region/preprocessor lines, adding blank lines around each boundary."""
|
||
b = []
|
||
for w in chain_arr:
|
||
b.append("#Область %s" % w["name"] if w["kind"] == "region" else "#Если %s Тогда" % w["cond"])
|
||
b.append("")
|
||
b.extend(core)
|
||
for w in reversed(chain_arr):
|
||
b.append("")
|
||
b.append("#КонецОбласти" if w["kind"] == "region" else "#КонецЕсли")
|
||
return b
|
||
|
||
|
||
def normalize(line):
|
||
return re.sub(r'\s+', ' ', line).strip()
|
||
|
||
|
||
def parse_marked_body(body_lines):
|
||
v1 = []
|
||
ops = []
|
||
i = 0
|
||
n = len(body_lines)
|
||
while i < n:
|
||
t = body_lines[i].strip()
|
||
if t == "#Вставка":
|
||
ins = []
|
||
i += 1
|
||
while i < n and body_lines[i].strip() != "#КонецВставки":
|
||
ins.append(body_lines[i])
|
||
i += 1
|
||
i += 1
|
||
ops.append({"kind": "insert", "after": len(v1) - 1, "lines": ins})
|
||
elif t == "#Удаление":
|
||
start_idx = len(v1)
|
||
i += 1
|
||
dels = []
|
||
while i < n and body_lines[i].strip() != "#КонецУдаления":
|
||
dels.append(body_lines[i])
|
||
v1.append(body_lines[i])
|
||
i += 1
|
||
i += 1
|
||
ops.append({"kind": "delete", "start": start_idx, "end": len(v1) - 1, "lines": dels})
|
||
else:
|
||
v1.append(body_lines[i])
|
||
i += 1
|
||
return {"v1": v1, "ops": ops}
|
||
|
||
|
||
def find_unique_index(v2norm, key):
|
||
found = -1
|
||
for k in range(len(v2norm)):
|
||
if v2norm[k] == key:
|
||
if found >= 0:
|
||
return -1
|
||
found = k
|
||
return found
|
||
|
||
|
||
def find_unique_run(v2norm, keys):
|
||
if not keys:
|
||
return -1
|
||
found = -1
|
||
for k in range(len(v2norm) - len(keys) + 1):
|
||
if v2norm[k:k + len(keys)] == keys:
|
||
if found >= 0:
|
||
return -1
|
||
found = k
|
||
return found
|
||
|
||
|
||
def test_significant(norm_line):
|
||
"""A significant line carries code: non-empty and not a whole-line comment (//).
|
||
Blank/comment-only lines are cosmetic — transparent to anchor/absorption matching."""
|
||
return norm_line != "" and not norm_line.startswith("//")
|
||
|
||
|
||
def significant_projection(norm):
|
||
"""Project normalized lines to significant-only. Returns (sig_values, orig_indices)."""
|
||
sig = []
|
||
idx = []
|
||
for i, ln in enumerate(norm):
|
||
if test_significant(ln):
|
||
sig.append(ln)
|
||
idx.append(i)
|
||
return sig, idx
|
||
|
||
|
||
def test_run_at(hay, keys, at):
|
||
"""True if normalized run `keys` sits in `hay` starting exactly at index `at` (contiguous)."""
|
||
if not keys or at < 0 or at + len(keys) > len(hay):
|
||
return False
|
||
return hay[at:at + len(keys)] == keys
|
||
|
||
|
||
def test_delete_absorbed(v2norm, before_ctx, after_ctx):
|
||
"""True if a deletion is already applied in v2: before/after context are now adjacent
|
||
(nothing between them), or the missing side sits at a body boundary (None = boundary)."""
|
||
if before_ctx is not None and after_ctx is not None:
|
||
for i in range(len(v2norm) - 1):
|
||
if v2norm[i] == before_ctx and v2norm[i + 1] == after_ctx:
|
||
return True
|
||
return False
|
||
if before_ctx is None and after_ctx is not None:
|
||
return len(v2norm) > 0 and v2norm[0] == after_ctx
|
||
if before_ctx is not None and after_ctx is None:
|
||
return len(v2norm) > 0 and v2norm[-1] == before_ctx
|
||
return False
|
||
|
||
|
||
def resolve_insertion_point_exact(v2norm, before_lines, after_lines):
|
||
"""Exact two-sided resolution over the given array.
|
||
Returns index to insert-after (-1 = top), or None if ambiguous/conflict."""
|
||
nb = len(before_lines)
|
||
na = len(after_lines)
|
||
|
||
# Tier A: adjacent pair (before[-1] then after[0] in v2), widening symmetrically
|
||
if nb >= 1 and na >= 1:
|
||
cands = [k for k in range(len(v2norm) - 1)
|
||
if v2norm[k] == before_lines[-1] and v2norm[k + 1] == after_lines[0]]
|
||
if len(cands) == 1:
|
||
return cands[0]
|
||
if len(cands) > 1:
|
||
w = 1
|
||
while len(cands) > 1 and (w < nb or w < na):
|
||
w += 1
|
||
filtered = []
|
||
for k in cands:
|
||
ok = True
|
||
if w <= nb:
|
||
if k - (w - 1) < 0 or v2norm[k - (w - 1)] != before_lines[nb - w]:
|
||
ok = False
|
||
if ok and w <= na:
|
||
if k + w >= len(v2norm) or v2norm[k + w] != after_lines[w - 1]:
|
||
ok = False
|
||
if ok:
|
||
filtered.append(k)
|
||
if not filtered:
|
||
break
|
||
cands = filtered
|
||
if len(cands) == 1:
|
||
return cands[0]
|
||
return None
|
||
|
||
# Tier B: one side changed -> single-side uniqueness
|
||
if nb >= 1:
|
||
bk = find_unique_index(v2norm, before_lines[-1])
|
||
if bk >= 0:
|
||
return bk
|
||
if na >= 1:
|
||
ak = find_unique_index(v2norm, after_lines[0])
|
||
if ak >= 0:
|
||
return ak - 1
|
||
return None
|
||
|
||
|
||
def resolve_insertion_point(v2norm, before_lines, after_lines):
|
||
"""Exact first (comments/blanks included — keeps the insert's position relative to a stable
|
||
comment); on failure, retry on significant lines only (transparent to vendor-added blanks/
|
||
comments) and map back to a full-v2 index. Returns insert-after idx or None."""
|
||
k = resolve_insertion_point_exact(v2norm, before_lines, after_lines)
|
||
if k is not None:
|
||
return k
|
||
sig, idx = significant_projection(v2norm)
|
||
bs = [x for x in before_lines if test_significant(x)]
|
||
as_ = [x for x in after_lines if test_significant(x)]
|
||
ksig = resolve_insertion_point_exact(sig, bs, as_)
|
||
if ksig is None:
|
||
return None
|
||
if ksig < 0:
|
||
return -1
|
||
return idx[ksig]
|
||
|
||
|
||
def truncate(s, n=60):
|
||
t = s.strip()
|
||
return (t[:n] + "…") if len(t) > n else t
|
||
|
||
|
||
def die(msg):
|
||
print(msg, file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
|
||
def main():
|
||
sys.stdout.reconfigure(encoding="utf-8")
|
||
sys.stderr.reconfigure(encoding="utf-8")
|
||
parser = argparse.ArgumentParser(
|
||
description="Source-aware method interceptor for 1C extension (CFE)",
|
||
allow_abbrev=False,
|
||
)
|
||
parser.add_argument("-ExtensionPath", required=True)
|
||
parser.add_argument("-ConfigPath", required=False, default="")
|
||
parser.add_argument("-ModulePath", required=False, default="")
|
||
parser.add_argument("-MethodName", required=False, default="")
|
||
parser.add_argument("-InterceptorType", required=False, default="",
|
||
choices=["", "Before", "After", "Instead", "ModificationAndControl"])
|
||
parser.add_argument("-Check", action="store_true")
|
||
parser.add_argument("-Actualize", action="store_true")
|
||
args = parser.parse_args()
|
||
|
||
extension_path = args.ExtensionPath
|
||
config_path = args.ConfigPath
|
||
module_path = args.ModulePath
|
||
method_name = args.MethodName
|
||
interceptor_type = args.InterceptorType
|
||
check_mode = args.Check
|
||
actualize_mode = args.Actualize
|
||
|
||
# --- Resolve extension path ---
|
||
if not os.path.isabs(extension_path):
|
||
extension_path = os.path.join(os.getcwd(), extension_path)
|
||
if os.path.isfile(extension_path):
|
||
extension_path = os.path.dirname(extension_path)
|
||
cfg_file = os.path.join(extension_path, "Configuration.xml")
|
||
if not os.path.isfile(cfg_file):
|
||
die("Configuration.xml не найден в расширении: %s" % extension_path)
|
||
|
||
# --- Read NamePrefix ---
|
||
ns = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||
name_prefix = "Расш_"
|
||
try:
|
||
root = ET.parse(cfg_file).getroot()
|
||
props = root.find(".//md:Configuration/md:Properties", ns)
|
||
if props is not None:
|
||
pn = props.find("md:NamePrefix", ns)
|
||
if pn is not None and pn.text:
|
||
name_prefix = pn.text
|
||
nn = props.find("md:Name", ns)
|
||
if nn is not None and nn.text:
|
||
ext_name = nn.text
|
||
except ET.ParseError:
|
||
pass
|
||
if 'ext_name' not in dir():
|
||
ext_name = "Расширение"
|
||
|
||
# --- Batch modes: -Check / -Actualize over &ИзменениеИКонтроль ---
|
||
if check_mode or actualize_mode:
|
||
if check_mode and actualize_mode:
|
||
die("Укажите либо -Check, либо -Actualize, не оба.")
|
||
if not config_path:
|
||
die("Для -Check/-Actualize нужен -ConfigPath (сверка с исходником).")
|
||
cp = config_path
|
||
if not os.path.isabs(cp):
|
||
cp = os.path.join(os.getcwd(), cp)
|
||
if os.path.isfile(cp):
|
||
cp = os.path.dirname(cp)
|
||
if not os.path.isfile(os.path.join(cp, "Configuration.xml")):
|
||
die("Configuration.xml не найден в конфигурации-источнике: %s" % cp)
|
||
report_only = check_mode
|
||
verb = "КОНТРОЛЬ" if check_mode else "АКТУАЛИЗАЦИЯ"
|
||
safe = re.sub(r'[\\/:*?"<>|]', "_", ext_name)
|
||
run_root = os.path.join(tempfile.gettempdir(), "cfe-resync", safe)
|
||
if actualize_mode and os.path.isdir(run_root):
|
||
shutil.rmtree(run_root, ignore_errors=True)
|
||
|
||
targets = []
|
||
if module_path:
|
||
rp = get_module_rel_path(module_path)
|
||
mb = os.path.join(extension_path, *rp)
|
||
if os.path.isfile(mb):
|
||
targets.append(mb)
|
||
else:
|
||
for dirpath, _dirs, files in os.walk(extension_path):
|
||
for fn in files:
|
||
if fn.endswith(".bsl"):
|
||
targets.append(os.path.join(dirpath, fn))
|
||
|
||
results = []
|
||
for tb in targets:
|
||
scan = read_lines(tb)
|
||
mnames = [ic["method"] for ic in get_interceptors(scan) if ic["type"] == "ИзменениеИКонтроль"]
|
||
if method_name:
|
||
mnames = [m for m in mnames if m.lower() == method_name.lower()]
|
||
seen = set(); uniq = []
|
||
for m in mnames:
|
||
if m.lower() not in seen:
|
||
seen.add(m.lower()); uniq.append(m)
|
||
if not uniq:
|
||
continue
|
||
rel = rel_parts_under(extension_path, tb)
|
||
logical_module = module_path_from_rel(rel)
|
||
src_bsl2 = os.path.join(cp, *rel)
|
||
rel_no_bsl = os.path.join(*rel)
|
||
if rel_no_bsl.endswith(".bsl"):
|
||
rel_no_bsl = rel_no_bsl[:-4]
|
||
for mname in uniq:
|
||
mid = "%s.%s" % (logical_module, mname)
|
||
if not os.path.isfile(src_bsl2):
|
||
results.append({"id": mid, "status": "ИСТОЧНИК-НЕ-НАЙДЕН", "ext_bsl": tb}); continue
|
||
m = extract_method(read_lines(src_bsl2), mname)
|
||
if not m:
|
||
results.append({"id": mid, "status": "МЕТОД-ИСЧЕЗ", "ext_bsl": tb}); continue
|
||
tb_lines = read_lines(tb)
|
||
ic = next((x for x in get_interceptors(tb_lines)
|
||
if x["type"] == "ИзменениеИКонтроль" and x["method"].lower() == mname.lower()), None)
|
||
if not ic:
|
||
continue
|
||
folder = os.path.join(run_root, rel_no_bsl, m["canonical"])
|
||
results.append(resync_one(tb, tb_lines, ic, m, logical_module, folder, report_only))
|
||
|
||
total = len(results)
|
||
actual = sum(1 for r in results if r["status"] == "АКТУАЛЕН")
|
||
print("[%s] %s -> %s (на контроле: %d)" % (verb, ext_name, cp, total))
|
||
listed = [r for r in results if r["status"] != "АКТУАЛЕН"]
|
||
for pr in listed:
|
||
line = " %-22s %s" % (pr["status"], pr["id"])
|
||
if pr.get("reason"):
|
||
line += " %s" % pr["reason"]
|
||
print(line)
|
||
for n in pr.get("absorbed_notes", []):
|
||
print(" ⚠ комментарий не перенесён (код в основной конфигурации): %s" % n)
|
||
transf = sum(1 for r in results if r["status"] == "ПЕРЕНЕСЕНО В ОСНОВНУЮ")
|
||
if check_mode:
|
||
drift = sum(1 for r in results if r["status"] == "ДРЕЙФ")
|
||
confl = sum(1 for r in results if r["status"] == "КОНФЛИКТ")
|
||
gone = sum(1 for r in results if r["status"] in ("МЕТОД-ИСЧЕЗ", "ИСТОЧНИК-НЕ-НАЙДЕН"))
|
||
print("Итог: %d/%d актуальны · дрейф: %d · конфликтов: %d · перенесено в основную: %d · внимания: %d"
|
||
% (actual, total, drift, confl, transf, gone))
|
||
if (drift + confl + gone) > 0:
|
||
print("Починить: /cfe-patch-method -Actualize -ExtensionPath %s -ConfigPath %s" % (extension_path, cp))
|
||
sys.exit(1)
|
||
elif transf > 0:
|
||
print("Перенесённые в основную конфигурацию правки подчистит: /cfe-patch-method -Actualize -ExtensionPath %s -ConfigPath %s" % (extension_path, cp))
|
||
sys.exit(0)
|
||
sys.exit(0)
|
||
else:
|
||
upd = sum(1 for r in results if r["status"] == "АКТУАЛИЗИРОВАН")
|
||
part = sum(1 for r in results if r["status"] == "ЧАСТИЧНО")
|
||
print("Итог: %d/%d актуальны · актуализировано: %d · частично: %d · перенесено в основную: %d" % (actual, total, upd, part, transf))
|
||
idx = write_resync_index(run_root, results, ext_name, cp, verb)
|
||
if idx:
|
||
print("Merge-воркспейс конфликтов (см. index.md): %s" % idx)
|
||
sys.exit(0)
|
||
|
||
# --- Generation mode: require ModulePath + MethodName + InterceptorType ---
|
||
if not (module_path and method_name and interceptor_type):
|
||
die("Нужны -ModulePath, -MethodName, -InterceptorType (генерация перехватчика). "
|
||
"Для проверки/актуализации контролируемых методов используйте -Check или -Actualize.")
|
||
|
||
# --- Resolve module file paths (ModulePath = logical name OR path to a .bsl) ---
|
||
has_config = bool(config_path)
|
||
if has_config:
|
||
if not os.path.isabs(config_path):
|
||
config_path = os.path.join(os.getcwd(), config_path)
|
||
if os.path.isfile(config_path):
|
||
config_path = os.path.dirname(config_path)
|
||
|
||
is_file_path = bool(re.search(r'[\\/]', module_path)) or module_path.endswith(".bsl")
|
||
|
||
if is_file_path:
|
||
mp_abs = module_path if os.path.isabs(module_path) else os.path.join(os.getcwd(), module_path)
|
||
rel_parts = get_rel_parts_from_file_path(module_path)
|
||
if not rel_parts:
|
||
die("Не удалось определить объект по пути модуля: %s\n"
|
||
"(нет распознаваемой типовой папки — Catalogs/Documents/CommonModules/…)" % module_path)
|
||
ext_bsl = os.path.join(extension_path, *rel_parts)
|
||
if has_config:
|
||
src_bsl = os.path.join(config_path, *rel_parts)
|
||
else:
|
||
ext_root_abs = os.path.normcase(os.path.abspath(extension_path)).rstrip("\\/")
|
||
mp_full = os.path.normcase(os.path.abspath(mp_abs))
|
||
if mp_full.startswith(ext_root_abs):
|
||
die("Путь модуля указывает внутрь расширения, а не на источник. "
|
||
"Укажите путь к модулю-источнику или -ConfigPath.")
|
||
src_bsl = mp_abs
|
||
else:
|
||
if not has_config:
|
||
die("Не указан -ConfigPath. Укажите путь к исходникам конфигурации "
|
||
"или передайте путь к файлу модуля в -ModulePath.")
|
||
if not os.path.isfile(os.path.join(config_path, "Configuration.xml")):
|
||
die("Configuration.xml не найден в конфигурации-источнике: %s" % config_path)
|
||
rel_parts = get_module_rel_path(module_path)
|
||
ext_bsl = os.path.join(extension_path, *rel_parts)
|
||
src_bsl = os.path.join(config_path, *rel_parts)
|
||
|
||
if not os.path.isfile(src_bsl):
|
||
die("Модуль-источник не найден: %s\n(проверьте ModulePath и ConfigPath)" % src_bsl)
|
||
|
||
# --- Extract original method ---
|
||
src_lines = read_lines(src_bsl)
|
||
method = extract_method(src_lines, method_name)
|
||
if not method:
|
||
die("Метод '%s' не найден в модуле-источнике: %s" % (method_name, src_bsl))
|
||
|
||
# --- Guard: functions cannot use Before/After ---
|
||
if method["is_function"] and interceptor_type in ("Before", "After"):
|
||
die("Метод '%s' — функция. Для функций доступны только Instead и ModificationAndControl "
|
||
"(перехват &Перед/&После к функциям неприменим)." % method_name)
|
||
|
||
decorator_ru = DECORATOR_MAP[interceptor_type]
|
||
|
||
# --- Read existing extension module (if any) ---
|
||
ext_exists = os.path.isfile(ext_bsl)
|
||
ext_lines = read_lines(ext_bsl) if ext_exists else []
|
||
|
||
existing_interceptors = get_interceptors(ext_lines) if ext_exists else []
|
||
existing_proc_names = get_proc_names(ext_lines) if ext_exists else []
|
||
|
||
dup = None
|
||
for ic in existing_interceptors:
|
||
if ic["type"] == decorator_ru and ic["method"].lower() == method_name.lower():
|
||
dup = ic
|
||
break
|
||
|
||
if dup:
|
||
if interceptor_type != "ModificationAndControl":
|
||
print('[ПРОПУЩЕН] Перехватчик &%s("%s") уже есть в модуле — дубль не создаётся.'
|
||
% (decorator_ru, method_name))
|
||
print(" Файл: %s" % ext_bsl)
|
||
sys.exit(0)
|
||
rel = rel_parts_under(extension_path, ext_bsl)
|
||
logical_module = module_path_from_rel(rel)
|
||
rel_no_bsl = os.path.join(*rel)
|
||
if rel_no_bsl.endswith(".bsl"):
|
||
rel_no_bsl = rel_no_bsl[:-4]
|
||
safe = re.sub(r'[\\/:*?"<>|]', "_", ext_name)
|
||
run_root = os.path.join(tempfile.gettempdir(), "cfe-resync", safe)
|
||
folder = os.path.join(run_root, rel_no_bsl, method["canonical"])
|
||
res = resync_one(ext_bsl, ext_lines, dup, method, logical_module, folder, False)
|
||
st = res["status"]
|
||
if st == "АКТУАЛЕН":
|
||
print('[АКТУАЛЕН] &ИзменениеИКонтроль("%s") — оригинал не менялся, изменений нет.' % method_name)
|
||
elif st == "АКТУАЛИЗИРОВАН":
|
||
msg = '[АКТУАЛИЗИРОВАН] &ИзменениеИКонтроль("%s") — тело обновлено, правок сохранено: %d' % (method_name, res.get("transferred", 0))
|
||
if res.get("absorbed", 0) > 0:
|
||
msg += ", перенесено в основную конфигурацию: %d" % res["absorbed"]
|
||
print(msg)
|
||
elif st == "ПЕРЕНЕСЕНО В ОСНОВНУЮ":
|
||
print('[ПЕРЕНЕСЕНО В ОСНОВНУЮ] &ИзменениеИКонтроль("%s") — все правки (%d) уже в основной конфигурации, перехватчик можно удалить.'
|
||
% (method_name, res.get("absorbed", 0)))
|
||
elif st == "ЧАСТИЧНО":
|
||
idx = write_resync_index(run_root, [res], ext_name, config_path, "АКТУАЛИЗАЦИЯ")
|
||
msg = '[АКТУАЛИЗИРОВАН-ЧАСТИЧНО] &ИзменениеИКонтроль("%s") — сохранено: %d, конфликтов: %d' % (method_name, res.get("transferred", 0), res.get("disputed", 0))
|
||
if res.get("absorbed", 0) > 0:
|
||
msg += ", перенесено в основную: %d" % res["absorbed"]
|
||
print(msg)
|
||
print(" Конфликт помечен // [РЕСИНК-КОНФЛИКТ]. Папка метода: %s" % res.get("conflict_dir"))
|
||
if idx:
|
||
print(" Индекс: %s" % idx)
|
||
else:
|
||
print('[%s] &ИзменениеИКонтроль("%s") — %s' % (st, method_name, res.get("reason", "")))
|
||
for n in res.get("absorbed_notes", []):
|
||
print(" ⚠ комментарий не перенесён (код в основной конфигурации): %s" % n)
|
||
print(" Файл: %s" % ext_bsl)
|
||
sys.exit(0)
|
||
|
||
# --- New interceptor ---
|
||
candidate = name_prefix + method["canonical"]
|
||
taken = [n.lower() for n in existing_proc_names]
|
||
interceptor_name = candidate
|
||
if candidate.lower() in taken:
|
||
if interceptor_type == "ModificationAndControl":
|
||
interceptor_name = candidate + "_ИзменениеИКонтроль"
|
||
else:
|
||
interceptor_name = candidate + "_" + decorator_ru
|
||
|
||
core = build_interceptor_core(method, interceptor_type, interceptor_name)
|
||
|
||
place_new(ext_bsl, ext_lines, ext_exists, method["chain"], core)
|
||
|
||
# emit summary
|
||
placement = place_new.placement
|
||
print('[OK] Перехватчик &%s("%s") — %s' % (decorator_ru, method_name, placement))
|
||
print(" Файл: %s" % ext_bsl)
|
||
print(" Процедура: %s(%s)" % (interceptor_name, normalize(method["params_text"])))
|
||
if method["context"]:
|
||
print(" Контекст: %s" % method["context"])
|
||
if method["chain"]:
|
||
desc = " > ".join(
|
||
("Область:%s" % w["name"]) if w["kind"] == "region" else ("Если:%s" % w["cond"])
|
||
for w in method["chain"]
|
||
)
|
||
print(" Обрамление: %s" % desc)
|
||
|
||
|
||
def place_new(ext_bsl, ext_lines, ext_exists, chain, core):
|
||
enc_bom = True # noqa
|
||
|
||
# find innermost region in chain that already exists in the extension module
|
||
reuse_region_idx = -1
|
||
reuse_line_idx = -1
|
||
if ext_exists:
|
||
for c in range(len(chain) - 1, -1, -1):
|
||
if chain[c]["kind"] == "region":
|
||
rname = chain[c]["name"]
|
||
rre = re.compile(r'^#Область\s+' + re.escape(rname) + r'\s*$')
|
||
for li in range(len(ext_lines)):
|
||
if rre.match(ext_lines[li].strip()):
|
||
reuse_region_idx = c
|
||
reuse_line_idx = li
|
||
break
|
||
if reuse_region_idx >= 0:
|
||
break
|
||
|
||
if reuse_region_idx >= 0:
|
||
inner_chain = chain[reuse_region_idx + 1:]
|
||
block = build_wrapped_block(inner_chain, core)
|
||
|
||
depth = 0
|
||
close_idx = -1
|
||
for li in range(reuse_line_idx, len(ext_lines)):
|
||
t = ext_lines[li].strip()
|
||
if re.match(r'^#Область\s', t):
|
||
depth += 1
|
||
elif re.match(r'^#КонецОбласти', t):
|
||
depth -= 1
|
||
if depth == 0:
|
||
close_idx = li
|
||
break
|
||
if close_idx < 0:
|
||
die("Не найден #КонецОбласти для региона (переиспользование)")
|
||
|
||
# strip trailing blank lines of the region content, then insert with air
|
||
last_content = close_idx - 1
|
||
while last_content >= 0 and ext_lines[last_content].strip() == "":
|
||
last_content -= 1
|
||
out = list(ext_lines[:last_content + 1])
|
||
out.append("")
|
||
out.extend(block)
|
||
out.append("")
|
||
out.extend(ext_lines[close_idx:])
|
||
write_bsl(ext_bsl, out)
|
||
place_new.placement = "в существующий регион '%s'" % chain[reuse_region_idx]["name"]
|
||
return
|
||
|
||
# full wrapper chain, append (or create)
|
||
block = build_wrapped_block(chain, core)
|
||
block_text = "\r\n".join(block) + "\r\n"
|
||
|
||
bsl_dir = os.path.dirname(ext_bsl)
|
||
if not os.path.isdir(bsl_dir):
|
||
os.makedirs(bsl_dir, exist_ok=True)
|
||
|
||
if ext_exists:
|
||
with open(ext_bsl, "r", encoding="utf-8-sig", newline="") as f:
|
||
existing = f.read()
|
||
if not existing.strip():
|
||
with open(ext_bsl, "w", encoding="utf-8-sig", newline="") as f:
|
||
f.write(block_text)
|
||
place_new.placement = "заполнен модуль"
|
||
else:
|
||
sep = "\r\n" if existing.endswith("\n") else "\r\n\r\n"
|
||
with open(ext_bsl, "w", encoding="utf-8-sig", newline="") as f:
|
||
f.write(existing + sep + block_text)
|
||
place_new.placement = "дописан в модуль"
|
||
else:
|
||
with open(ext_bsl, "w", encoding="utf-8-sig", newline="") as f:
|
||
f.write(block_text)
|
||
place_new.placement = "создан модуль"
|
||
|
||
|
||
DIR_TO_TYPE = {
|
||
"Catalogs": "Catalog", "Documents": "Document", "Enums": "Enum", "CommonModules": "CommonModule",
|
||
"Reports": "Report", "DataProcessors": "DataProcessor", "ExchangePlans": "ExchangePlan",
|
||
"ChartsOfAccounts": "ChartOfAccounts", "ChartsOfCharacteristicTypes": "ChartOfCharacteristicTypes",
|
||
"ChartsOfCalculationTypes": "ChartOfCalculationTypes", "BusinessProcesses": "BusinessProcess",
|
||
"Tasks": "Task", "InformationRegisters": "InformationRegister",
|
||
"AccumulationRegisters": "AccumulationRegister", "AccountingRegisters": "AccountingRegister",
|
||
"CalculationRegisters": "CalculationRegister",
|
||
}
|
||
|
||
|
||
def module_path_from_rel(rel_parts):
|
||
dir0, name = rel_parts[0], rel_parts[1]
|
||
typ = DIR_TO_TYPE.get(dir0, dir0)
|
||
if dir0 == "CommonModules":
|
||
return "CommonModule.%s" % name
|
||
if len(rel_parts) >= 7 and rel_parts[2] == "Forms":
|
||
return "%s.%s.Form.%s" % (typ, name, rel_parts[3])
|
||
mod = rel_parts[-1][:-4] if rel_parts[-1].endswith(".bsl") else rel_parts[-1]
|
||
return "%s.%s.%s" % (typ, name, mod)
|
||
|
||
|
||
def rel_parts_under(root, full_path):
|
||
rel = os.path.relpath(os.path.abspath(full_path), os.path.abspath(root))
|
||
return [s for s in rel.replace("\\", "/").split("/") if s and s != "."]
|
||
|
||
|
||
def conflict_reason(disputed):
|
||
kinds = set(d["kind"] for d in disputed)
|
||
parts = []
|
||
if "insert" in kinds:
|
||
parts.append("якорь вставки изменён")
|
||
if "delete" in kinds:
|
||
parts.append("удаляемое исчезло")
|
||
return "; ".join(parts)
|
||
|
||
|
||
def write_conflict_folder(folder, method_id, ext_bsl, existing_name, method, v1, marked_body, v2, v1norm, v2norm, disputed):
|
||
os.makedirs(folder, exist_ok=True)
|
||
write_bsl(os.path.join(folder, "base.bsl"), v1)
|
||
write_bsl(os.path.join(folder, "local.bsl"), marked_body)
|
||
write_bsl(os.path.join(folder, "remote.bsl"), v2)
|
||
md = []
|
||
md.append("# %s" % method_id)
|
||
md.append("Править: %s" % ext_bsl)
|
||
md.append('Метод: %s (&ИзменениеИКонтроль("%s"))' % (existing_name, method["canonical"]))
|
||
md.append("Причина: %s" % conflict_reason(disputed))
|
||
md.append("")
|
||
md.append("## Не размещено — перенести вручную")
|
||
cn = 0
|
||
for d in disputed:
|
||
cn += 1
|
||
md.append("")
|
||
if d["kind"] == "insert":
|
||
md.append("### Конфликт №%d — вставка" % cn)
|
||
md.append("Как блок стоял в вашей версии (local):")
|
||
if d.get("before"):
|
||
for l in d["before"]:
|
||
md.append(l)
|
||
md.append("#Вставка"); md.extend(d["lines"]); md.append("#КонецВставки")
|
||
if d.get("after"):
|
||
for l in d["after"]:
|
||
md.append(l)
|
||
md.append("")
|
||
md.append("Якорь (строки вокруг #Вставка) изменился/исчез в новом оригинале — блок не лёг автоматически (см. дифф base→remote ниже).")
|
||
md.append("В модуле расширения блок припаркован в конце метода под меткой // [РЕСИНК-КОНФЛИКТ №%d] — найди по ней." % cn)
|
||
md.append("Куда переносить: если якорного кода в новом методе больше нет — он, вероятно, вынесен/отрефакторен (ищите в диффе новый вызов/процедуру). Размести адаптацию по смыслу: например пост-обработкой после нового вызова, либо в заимствованной процедуре, куда переехал код. При правке файла сохрани кодировку (UTF-8 с BOM).")
|
||
else:
|
||
md.append("### Конфликт №%d — удаление" % cn)
|
||
md.append("Строки для удаления не найдены в новом оригинале (изменились/исчезли):")
|
||
for l in d["lines"]:
|
||
md.append(" - " + l.strip())
|
||
md.append("В модуле расширения помечено меткой // [РЕСИНК-КОНФЛИКТ №%d]." % cn)
|
||
md.append("")
|
||
md.append("## Дифф base→remote (что изменилось в оригинале)")
|
||
for l in v1:
|
||
if normalize(l) not in v2norm:
|
||
md.append("- " + l)
|
||
for l in v2:
|
||
if normalize(l) not in v1norm:
|
||
md.append("+ " + l)
|
||
md.append("")
|
||
md.append("Рядом: base.bsl / local.bsl / remote.bsl")
|
||
write_bsl(os.path.join(folder, "conflict.md"), md)
|
||
|
||
|
||
def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder, report_only):
|
||
method_id = "%s.%s" % (logical_module, method["canonical"])
|
||
dec_line = dup["line"]
|
||
sig_line_idx = dec_line + 1
|
||
name_re = re.compile(r'^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(', re.IGNORECASE)
|
||
m0 = name_re.match(ext_lines[sig_line_idx]) if sig_line_idx < len(ext_lines) else None
|
||
if not m0:
|
||
return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру перехватчика"}
|
||
existing_name = m0.group(1)
|
||
sig = read_signature(ext_lines, sig_line_idx)
|
||
if not sig:
|
||
return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру"}
|
||
_params, sig_end = sig
|
||
is_func = bool(re.match(r'^\s*(?:Асинх\s+)?Функция\b', ext_lines[sig_line_idx], re.IGNORECASE))
|
||
end_re = re.compile(r'^\s*КонецФункции\b' if is_func else r'^\s*КонецПроцедуры\b', re.IGNORECASE)
|
||
block_end = -1
|
||
for j in range(sig_end + 1, len(ext_lines)):
|
||
if end_re.match(ext_lines[j]):
|
||
block_end = j
|
||
break
|
||
if block_end < 0:
|
||
return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не найден конец перехватчика"}
|
||
|
||
marked_body = ext_lines[sig_end + 1:block_end]
|
||
parsed = parse_marked_body(marked_body)
|
||
v1 = parsed["v1"]; ops = parsed["ops"]; v2 = method["body_lines"]
|
||
v1norm = [normalize(x) for x in v1]
|
||
v2norm = [normalize(x) for x in v2]
|
||
|
||
if "\n".join(v1norm) == "\n".join(v2norm):
|
||
return {"id": method_id, "status": "АКТУАЛЕН", "ext_bsl": ext_bsl}
|
||
|
||
insert_top = []; insert_after = {}; del_start = set(); del_end = set(); disputed = []; transferred = 0; absorbed = 0; absorbed_notes = []
|
||
v2sig, v2map = significant_projection(v2norm)
|
||
for op in ops:
|
||
if op["kind"] == "insert":
|
||
after = op["after"]
|
||
before_lines = v1norm[max(0, after - 2):after + 1] if after >= 0 else []
|
||
after_lines = v1norm[after + 1:after + 4]
|
||
k = resolve_insertion_point(v2norm, before_lines, after_lines)
|
||
# Payload already in the new original -> change carried into the main config.
|
||
# Match on significant lines only, so vendor-added blanks/comments don't hide it.
|
||
payload_sig = [x for x in (normalize(y) for y in op["lines"]) if test_significant(x)]
|
||
is_absorbed = False
|
||
if payload_sig:
|
||
if k is None:
|
||
is_absorbed = find_unique_run(v2sig, payload_sig) >= 0
|
||
else:
|
||
sig_start = sum(1 for j in v2map if j <= k)
|
||
is_absorbed = test_run_at(v2sig, payload_sig, sig_start)
|
||
if is_absorbed:
|
||
absorbed += 1
|
||
for pl in op["lines"]:
|
||
if normalize(pl).startswith("//"):
|
||
absorbed_notes.append(pl.strip())
|
||
elif k is None:
|
||
dbefore = v1[max(0, after - 2):after + 1] if after >= 0 else []
|
||
dafter = v1[after + 1:after + 4]
|
||
disputed.append({"kind": "insert", "lines": op["lines"], "before": dbefore, "after": dafter})
|
||
elif k < 0:
|
||
insert_top.append(op["lines"]); transferred += 1
|
||
else:
|
||
insert_after.setdefault(k, []).append(op["lines"]); transferred += 1
|
||
else:
|
||
keys = v1norm[op["start"]:op["end"] + 1]
|
||
p = find_unique_run(v2norm, keys)
|
||
if p >= 0:
|
||
del_start.add(p); del_end.add(p + len(keys) - 1); transferred += 1
|
||
else:
|
||
# Nearest significant neighbours; adjacency in the significant projection means the
|
||
# block is already cut (blanks/comments left behind don't matter).
|
||
before_ctx = None
|
||
for z in range(op["start"] - 1, -1, -1):
|
||
if test_significant(v1norm[z]):
|
||
before_ctx = v1norm[z]; break
|
||
after_ctx = None
|
||
for z in range(op["end"] + 1, len(v1norm)):
|
||
if test_significant(v1norm[z]):
|
||
after_ctx = v1norm[z]; break
|
||
if test_delete_absorbed(v2sig, before_ctx, after_ctx):
|
||
absorbed += 1
|
||
else:
|
||
disputed.append({"kind": "delete", "lines": op["lines"]})
|
||
|
||
if report_only:
|
||
if disputed:
|
||
st = "КОНФЛИКТ"
|
||
elif transferred == 0 and absorbed > 0:
|
||
st = "ПЕРЕНЕСЕНО В ОСНОВНУЮ"
|
||
else:
|
||
st = "ДРЕЙФ"
|
||
rsn = conflict_reason(disputed) if disputed else ("все правки уже в основной конфигурации" if st == "ПЕРЕНЕСЕНО В ОСНОВНУЮ" else "")
|
||
return {"id": method_id, "status": st, "ext_bsl": ext_bsl, "transferred": transferred,
|
||
"absorbed": absorbed, "disputed": len(disputed), "reason": rsn, "absorbed_notes": absorbed_notes}
|
||
|
||
new_body = []
|
||
for blk in insert_top:
|
||
new_body.append("#Вставка"); new_body.extend(blk); new_body.append("#КонецВставки")
|
||
for k in range(len(v2)):
|
||
if k in del_start:
|
||
new_body.append("#Удаление")
|
||
new_body.append(v2[k])
|
||
if k in del_end:
|
||
new_body.append("#КонецУдаления")
|
||
if k in insert_after:
|
||
for blk in insert_after[k]:
|
||
new_body.append("#Вставка"); new_body.extend(blk); new_body.append("#КонецВставки")
|
||
if disputed:
|
||
new_body.append("\t// [РЕСИНК-КОНФЛИКТ] блоки ниже не легли автоматически — перенесите вручную (по № см. conflict.md / index.md в merge-воркспейсе, путь в выводе).")
|
||
cn = 0
|
||
for d in disputed:
|
||
cn += 1
|
||
if d["kind"] == "insert":
|
||
new_body.append("\t// [РЕСИНК-КОНФЛИКТ №%d] вставка — исходный якорь изменён в новом оригинале." % cn)
|
||
new_body.append("#Вставка"); new_body.extend(d["lines"]); new_body.append("#КонецВставки")
|
||
else:
|
||
new_body.append("\t// [РЕСИНК-КОНФЛИКТ №%d] удаление — строки не найдены в новом оригинале:" % cn)
|
||
for l in d["lines"]:
|
||
new_body.append("\t// " + l.strip())
|
||
|
||
async_prefix = "Асинх " if method["is_async"] else ""
|
||
keyword = "Функция" if method["is_function"] else "Процедура"
|
||
end_keyword = "КонецФункции" if method["is_function"] else "КонецПроцедуры"
|
||
new_block = []
|
||
if method["context"]:
|
||
new_block.append(method["context"])
|
||
new_block.append('&ИзменениеИКонтроль("%s")' % method["canonical"])
|
||
new_block.append("%s%s %s(%s)" % (async_prefix, keyword, existing_name, method["params_text"]))
|
||
new_block.extend(new_body)
|
||
new_block.append(end_keyword)
|
||
|
||
block_start = dec_line
|
||
if dec_line >= 1 and is_context_directive(ext_lines[dec_line - 1].strip()):
|
||
block_start = dec_line - 1
|
||
out = list(ext_lines[:block_start]) + new_block + list(ext_lines[block_end + 1:])
|
||
write_bsl(ext_bsl, out)
|
||
|
||
conflict_dir = None
|
||
if disputed:
|
||
conflict_dir = conflict_folder
|
||
write_conflict_folder(conflict_folder, method_id, ext_bsl, existing_name, method, v1, marked_body, v2, v1norm, v2norm, disputed)
|
||
if disputed:
|
||
status = "ЧАСТИЧНО"
|
||
elif transferred == 0 and absorbed > 0:
|
||
status = "ПЕРЕНЕСЕНО В ОСНОВНУЮ"
|
||
else:
|
||
status = "АКТУАЛИЗИРОВАН"
|
||
rsn = conflict_reason(disputed) if disputed else ("все правки уже в основной конфигурации — перехватчик можно удалить" if status == "ПЕРЕНЕСЕНО В ОСНОВНУЮ" else "")
|
||
return {"id": method_id, "status": status, "ext_bsl": ext_bsl, "transferred": transferred,
|
||
"absorbed": absorbed, "disputed": len(disputed), "conflict_dir": conflict_dir, "reason": rsn,
|
||
"absorbed_notes": absorbed_notes}
|
||
|
||
|
||
def write_resync_index(run_root, results, ext_name, config_path, verb):
|
||
conflicts = [r for r in results if r["status"] == "ЧАСТИЧНО"]
|
||
if not conflicts:
|
||
return None
|
||
os.makedirs(run_root, exist_ok=True)
|
||
total = len(results)
|
||
actual = sum(1 for r in results if r["status"] == "АКТУАЛЕН")
|
||
upd = sum(1 for r in results if r["status"] == "АКТУАЛИЗИРОВАН")
|
||
lines = []
|
||
lines.append("[%s] %s -> %s" % (verb, ext_name, config_path))
|
||
lines.append("Итог: %d/%d актуальны · актуализировано: %d · конфликтов: %d" % (actual, total, upd, len(conflicts)))
|
||
lines.append("")
|
||
lines.append("Конфликты — править .bsl расширения:")
|
||
for c in conflicts:
|
||
lines.append(" ЧАСТИЧНО %s" % c["id"])
|
||
lines.append(" -> %s (%s)" % (c["ext_bsl"], c.get("reason", "")))
|
||
write_bsl(os.path.join(run_root, "index.md"), lines)
|
||
return run_root
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|