From 1b2c9388e8fa06eec443304ef49c7bb3285ee9ef Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 19 Sep 2026 17:49:21 +0300 Subject: [PATCH] =?UTF-8?q?feat(cfe-patch-method,cfe-diff,cfe-validate):?= =?UTF-8?q?=20=D1=80=D0=B0=D1=81=D0=BF=D0=BE=D0=B7=D0=BD=D0=B0=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B0=D0=BD=D0=B3=D0=BB=D0=B8=D0=B9=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=D1=85=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=B2=D1=8B?= =?UTF-8?q?=D1=85=20=D1=81=D0=BB=D0=BE=D0=B2=20BSL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Платформа принимает встроенный язык в двух написаниях, а навыки читали только русское: в расширении на английском перехватчики и блоки правок не находились, и отчёт выглядел чистым. -Check молчал, cfe-diff не показывал вставки, счётчик контролируемых методов в cfe-validate давал ноль. Пары ключевых слов сверены по таблицам строк платформы (backbas.dll, bsl.dll), а не по памяти. Разбор стал двуязычным везде, где навык читает модуль: аннотации, объявления и Конец*, Знач, директивы контекста, препроцессор в цепочке обрамления, поиск региона при переиспользовании. Маркеры правок распознаются по началу строки, а не сравнением целой строки, — заодно перестал теряться хвостовой комментарий (#Вставка // старая логика). Эмиссия подчиняется одному правилу: отдаём тем языком, который прочитали. Генерация берёт язык метода-источника, -Actualize — язык переписываемого перехватчика, чтобы актуализация не превращала чужие Procedure и #Insert в русские. Комментарии и вывод в консоль остаются русскими. Таблица ключевых слов и разбор аннотаций скопированы в три навыка и заведены семьями в реестре анти-дрейфа: разъехавшаяся пара слов дала бы ровно тот же тихий ложно-чистый отчёт. Проверено платформой: расширение с &ChangeAndValidate принимается (/CheckCanApplyConfigurationExtensions), после дрейфа оригинала отвергается, после -Actualize принимается снова. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/cfe-diff/scripts/cfe-diff.ps1 | 105 +++++-- .claude/skills/cfe-diff/scripts/cfe-diff.py | 115 ++++++-- .../scripts/cfe-patch-method.ps1 | 262 ++++++++++++------ .../scripts/cfe-patch-method.py | 244 +++++++++++----- .../cfe-validate/scripts/cfe-validate.ps1 | 40 ++- .../cfe-validate/scripts/cfe-validate.py | 40 ++- tests/skills/check-inline-drift.mjs | 24 ++ 7 files changed, 630 insertions(+), 200 deletions(-) diff --git a/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 b/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 index bc3820db1..e2c788072 100644 --- a/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 +++ b/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 @@ -1,4 +1,4 @@ -# cfe-diff v1.5 — Analyze and compare 1C configuration extension (CFE) +# cfe-diff v1.6 — Analyze and compare 1C configuration extension (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -162,24 +162,90 @@ function Get-BslFiles { return $bslFiles } -# --- Helper: parse interceptors from .bsl --- +# 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. +function Get-BslKeywords { + return @{ + ru = @{ + Async="Асинх"; Proc="Процедура"; EndProc="КонецПроцедуры" + Func="Функция"; EndFunc="КонецФункции"; Val="Знач" + Region="Область"; EndRegion="КонецОбласти" + If="Если"; Then="Тогда"; ElsIf="ИначеЕсли"; Else="Иначе"; EndIf="КонецЕсли" + And="И"; Not="НЕ" + Insert="Вставка"; EndInsert="КонецВставки"; Delete="Удаление"; EndDelete="КонецУдаления" + Before="Перед"; After="После"; Around="Вместо"; Control="ИзменениеИКонтроль" + Proceed="ПродолжитьВызов"; Return="Возврат" + # Not a keyword: name of the local the generated Instead-stub declares. Lives here so + # that the language of emitted text is decided in exactly one place. + ResultVar="Результат" + Directives=@("НаКлиенте", "НаСервере", "НаСервереБезКонтекста", "НаКлиентеНаСервереБезКонтекста", "НаКлиентеНаСервере") + } + en = @{ + Async="Async"; Proc="Procedure"; EndProc="EndProcedure" + Func="Function"; EndFunc="EndFunction"; Val="Val" + Region="Region"; EndRegion="EndRegion" + If="If"; Then="Then"; ElsIf="ElsIf"; Else="Else"; EndIf="EndIf" + And="And"; Not="Not" + Insert="Insert"; EndInsert="EndInsert"; Delete="Delete"; EndDelete="EndDelete" + Before="Before"; After="After"; Around="Around"; Control="ChangeAndValidate" + Proceed="ProceedWithCall"; Return="Return" + ResultVar="Result" + Directives=@("AtClient", "AtServer", "AtServerNoContext", "AtClientAtServerNoContext", "AtClientAtServer") + } + } +} + +$script:bslKw = Get-BslKeywords + +# Edit marker kind at the start of a line: Insert/Delete/EndInsert/EndDelete, $null if none. +# The marker owns the start of the line only — a trailing comment does not bother the platform +# and must not bother us either. +function Get-BslMarkerKind { + param([string]$line) + $kw = $script:bslKw + foreach ($k in @("EndInsert", "EndDelete", "Insert", "Delete")) { + if ($line -match ('^\s*#(?:' + $kw.ru[$k] + '|' + $kw.en[$k] + ')\b')) { return $k } + } + return $null +} + +# Parse interceptor annotations present in a module. Type is normalized to the Russian spelling +# so everything downstream stays single-language; Raw keeps what the file actually says. function Get-Interceptors { + param($lines) + $kw = $script:bslKw + $keys = @("Before", "After", "Control", "Around") + $alt = @() + $norm = @{} + foreach ($k in $keys) { + $alt += $kw.ru[$k]; $alt += $kw.en[$k] + $norm[$kw.ru[$k].ToLower()] = $kw.ru[$k] + $norm[$kw.en[$k].ToLower()] = $kw.ru[$k] + } + $re = '^&(' + ($alt -join '|') + ')\("([^"]+)"\)' + $result = @() + for ($i = 0; $i -lt $lines.Count; $i++) { + $t = $lines[$i].Trim() + if ($t -match $re) { + $raw = $Matches[1] + $result += @{ Type = $norm[$raw.ToLower()]; Raw = $raw; Method = $Matches[2]; Line = $i } + } + } + return $result +} + +# --- Helper: read a .bsl and parse its interceptors --- +function Get-FileInterceptors { param([string]$bslPath) if (-not (Test-Path $bslPath)) { return @() } $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) - $interceptors = @() - $i = 0 - while ($i -lt $lines.Count) { - $line = $lines[$i].Trim() - if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') { - $type = $Matches[1] - $method = $Matches[2] - $interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath } - } - $i++ + $result = @() + foreach ($ic in (Get-Interceptors $lines)) { + $result += @{ Type = $ic.Type; Raw = $ic.Raw; Method = $ic.Method; Line = $ic.Line + 1; File = $bslPath } } - return $interceptors + return $result } # --- Helper: extract #Вставка blocks from .bsl --- @@ -194,12 +260,13 @@ function Get-InsertionBlocks { $startLine = 0 for ($i = 0; $i -lt $lines.Count; $i++) { - $line = $lines[$i].Trim() - if ($line -eq "#Вставка") { + $line = $lines[$i] + $kind = Get-BslMarkerKind $line + if ($kind -eq "Insert") { $inBlock = $true $blockLines = @() $startLine = $i + 1 - } elseif ($line -eq "#КонецВставки" -and $inBlock) { + } elseif ($kind -eq "EndInsert" -and $inBlock) { $inBlock = $false $blocks += @{ StartLine = $startLine @@ -298,10 +365,10 @@ if ($Mode -eq "A") { $bslFiles = Get-BslFiles $obj.Type $obj.Name foreach ($bsl in $bslFiles) { $relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") - $interceptors = Get-Interceptors $bsl + $interceptors = Get-FileInterceptors $bsl if ($interceptors.Count -gt 0) { foreach ($ic in $interceptors) { - Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath" + Write-Host " &$($ic.Raw)(`"$($ic.Method)`") — line $($ic.Line) in $relPath" } } else { Write-Host " $relPath (no interceptors)" @@ -412,7 +479,7 @@ if ($Mode -eq "B") { # Find .bsl files with &ИзменениеИКонтроль $bslFiles = Get-BslFiles $obj.Type $obj.Name foreach ($bsl in $bslFiles) { - $interceptors = Get-Interceptors $bsl + $interceptors = Get-FileInterceptors $bsl $macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" }) if ($macInterceptors.Count -eq 0) { continue } diff --git a/.claude/skills/cfe-diff/scripts/cfe-diff.py b/.claude/skills/cfe-diff/scripts/cfe-diff.py index 869b554c4..276673bae 100644 --- a/.claude/skills/cfe-diff/scripts/cfe-diff.py +++ b/.claude/skills/cfe-diff/scripts/cfe-diff.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cfe-diff v1.5 — Analyze and compare 1C configuration extension (CFE) +# cfe-diff v1.6 — Analyze and compare 1C configuration extension (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -165,34 +165,95 @@ def get_bsl_files(obj_type, obj_name, extension_path): return bsl_files -# --- Helper: parse interceptors from .bsl --- +# 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. +def bsl_keywords(): + return { + "ru": { + "Async": "Асинх", "Proc": "Процедура", "EndProc": "КонецПроцедуры", + "Func": "Функция", "EndFunc": "КонецФункции", "Val": "Знач", + "Region": "Область", "EndRegion": "КонецОбласти", + "If": "Если", "Then": "Тогда", "ElsIf": "ИначеЕсли", "Else": "Иначе", "EndIf": "КонецЕсли", + "And": "И", "Not": "НЕ", + "Insert": "Вставка", "EndInsert": "КонецВставки", "Delete": "Удаление", "EndDelete": "КонецУдаления", + "Before": "Перед", "After": "После", "Around": "Вместо", "Control": "ИзменениеИКонтроль", + "Proceed": "ПродолжитьВызов", "Return": "Возврат", + # Not a keyword: name of the local the generated Instead-stub declares. Lives here so + # that the language of emitted text is decided in exactly one place. + "ResultVar": "Результат", + "Directives": ["НаКлиенте", "НаСервере", "НаСервереБезКонтекста", "НаКлиентеНаСервереБезКонтекста", "НаКлиентеНаСервере"], + }, + "en": { + "Async": "Async", "Proc": "Procedure", "EndProc": "EndProcedure", + "Func": "Function", "EndFunc": "EndFunction", "Val": "Val", + "Region": "Region", "EndRegion": "EndRegion", + "If": "If", "Then": "Then", "ElsIf": "ElsIf", "Else": "Else", "EndIf": "EndIf", + "And": "And", "Not": "Not", + "Insert": "Insert", "EndInsert": "EndInsert", "Delete": "Delete", "EndDelete": "EndDelete", + "Before": "Before", "After": "After", "Around": "Around", "Control": "ChangeAndValidate", + "Proceed": "ProceedWithCall", "Return": "Return", + "ResultVar": "Result", + "Directives": ["AtClient", "AtServer", "AtServerNoContext", "AtClientAtServerNoContext", "AtClientAtServer"], + }, + } -def get_interceptors(bsl_path): + +BSL_KW = bsl_keywords() + + +# Edit marker kind at the start of a line: Insert/Delete/EndInsert/EndDelete, None if none. +# The marker owns the start of the line only — a trailing comment does not bother the platform +# and must not bother us either. +def bsl_marker_kind(line): + kw = BSL_KW + for k in ("EndInsert", "EndDelete", "Insert", "Delete"): + if re.match(r'^\s*#(?:' + kw["ru"][k] + '|' + kw["en"][k] + r')\b', line, re.IGNORECASE): + return k + return None + + +# Parse interceptor annotations present in a module. Type is normalized to the Russian spelling +# so everything downstream stays single-language; raw keeps what the file actually says. +def get_interceptors(lines): + kw = BSL_KW + keys = ("Before", "After", "Control", "Around") + alt = [] + norm = {} + for k in keys: + alt.append(kw["ru"][k]) + alt.append(kw["en"][k]) + norm[kw["ru"][k].lower()] = kw["ru"][k] + norm[kw["en"][k].lower()] = kw["ru"][k] + pat = re.compile(r'^&(' + '|'.join(alt) + r')\("([^"]+)"\)', re.IGNORECASE) + result = [] + for i in range(len(lines)): + m = pat.match(lines[i].strip()) + if m: + raw = m.group(1) + result.append({"type": norm[raw.lower()], "raw": raw, "method": m.group(2), "line": i}) + return result + + +# --- Helper: read a .bsl and parse its interceptors --- + +def get_file_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 + return [{ + "Type": ic["type"], + "Raw": ic["raw"], + "Method": ic["method"], + "Line": ic["line"] + 1, + "File": bsl_path, + } for ic in get_interceptors(lines)] -# --- Helper: extract #Вставка blocks from .bsl --- +# --- Helper: extract insert-marker blocks from .bsl --- def get_insertion_blocks(bsl_path): if not os.path.isfile(bsl_path): @@ -207,14 +268,12 @@ def get_insertion_blocks(bsl_path): start_line = 0 for i, line in enumerate(lines): - stripped = line.strip() - if stripped == "\u0023\u0412\u0441\u0442\u0430\u0432\u043a\u0430": - # #Вставка + kind = bsl_marker_kind(line) + if kind == "Insert": 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: - # #КонецВставки + elif kind == "EndInsert" and in_block: in_block = False blocks.append({ "StartLine": start_line, @@ -319,10 +378,10 @@ def mode_a(objects, extension_path): 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) + interceptor_list = get_file_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}') + print(f' &{ic["Raw"]}("{ic["Method"]}") \u2014 line {ic["Line"]} in {rel_path}') else: print(f" {rel_path} (no interceptors)") @@ -434,7 +493,7 @@ def mode_b(objects, extension_path, config_path): # 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) + interceptor_list = get_file_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: diff --git a/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 index cc30aecd3..731c3b256 100644 --- a/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 +++ b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 @@ -1,4 +1,4 @@ -# cfe-patch-method v2.11 — Source-aware method interceptor for 1C extension (CFE) +# cfe-patch-method v2.12 — Source-aware method interceptor for 1C extension (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -52,6 +52,103 @@ $script:decoratorMap = @{ "ModificationAndControl"="ИзменениеИКонтроль" } +# InterceptorType -> keyword table key (emission goes through the table, the map above stays +# the normalized value the logic compares against) +$script:decoratorKey = @{ + "Before"="Before"; "After"="After"; "Instead"="Around" + "ModificationAndControl"="Control" +} + +# 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. +function Get-BslKeywords { + return @{ + ru = @{ + Async="Асинх"; Proc="Процедура"; EndProc="КонецПроцедуры" + Func="Функция"; EndFunc="КонецФункции"; Val="Знач" + Region="Область"; EndRegion="КонецОбласти" + If="Если"; Then="Тогда"; ElsIf="ИначеЕсли"; Else="Иначе"; EndIf="КонецЕсли" + And="И"; Not="НЕ" + Insert="Вставка"; EndInsert="КонецВставки"; Delete="Удаление"; EndDelete="КонецУдаления" + Before="Перед"; After="После"; Around="Вместо"; Control="ИзменениеИКонтроль" + Proceed="ПродолжитьВызов"; Return="Возврат" + # Not a keyword: name of the local the generated Instead-stub declares. Lives here so + # that the language of emitted text is decided in exactly one place. + ResultVar="Результат" + Directives=@("НаКлиенте", "НаСервере", "НаСервереБезКонтекста", "НаКлиентеНаСервереБезКонтекста", "НаКлиентеНаСервере") + } + en = @{ + Async="Async"; Proc="Procedure"; EndProc="EndProcedure" + Func="Function"; EndFunc="EndFunction"; Val="Val" + Region="Region"; EndRegion="EndRegion" + If="If"; Then="Then"; ElsIf="ElsIf"; Else="Else"; EndIf="EndIf" + And="And"; Not="Not" + Insert="Insert"; EndInsert="EndInsert"; Delete="Delete"; EndDelete="EndDelete" + Before="Before"; After="After"; Around="Around"; Control="ChangeAndValidate" + Proceed="ProceedWithCall"; Return="Return" + ResultVar="Result" + Directives=@("AtClient", "AtServer", "AtServerNoContext", "AtClientAtServerNoContext", "AtClientAtServer") + } + } +} + +$script:bslKw = Get-BslKeywords + +# Regex alternation for a keyword in both spellings +function Get-KwAlt { + param([string]$key) + return '(?:' + $script:bslKw.ru[$key] + '|' + $script:bslKw.en[$key] + ')' +} + +# Spelling language of a recognized keyword. English is the discriminator: a Russian keyword +# never equals an English one, so anything else is 'ru'. +function Get-BslLang { + param([string]$word) + $w = $word.Trim().ToLower() + foreach ($k in @("Async", "Proc", "EndProc", "Func", "EndFunc", "Region", "If", "Insert", "Delete", "Before", "After", "Around", "Control")) { + if ($script:bslKw.en[$k].ToLower() -eq $w) { return "en" } + } + return "ru" +} + +# Edit marker kind at the start of a line: Insert/Delete/EndInsert/EndDelete, $null if none. +# The marker owns the start of the line only — a trailing comment does not bother the platform +# and must not bother us either. +function Get-BslMarkerKind { + param([string]$line) + $kw = $script:bslKw + foreach ($k in @("EndInsert", "EndDelete", "Insert", "Delete")) { + if ($line -match ('^\s*#(?:' + $kw.ru[$k] + '|' + $kw.en[$k] + ')\b')) { return $k } + } + return $null +} + +# Parse interceptor annotations present in a module. Type is normalized to the Russian spelling +# so everything downstream stays single-language; Raw keeps what the file actually says. +function Get-Interceptors { + param($lines) + $kw = $script:bslKw + $keys = @("Before", "After", "Control", "Around") + $alt = @() + $norm = @{} + foreach ($k in $keys) { + $alt += $kw.ru[$k]; $alt += $kw.en[$k] + $norm[$kw.ru[$k].ToLower()] = $kw.ru[$k] + $norm[$kw.en[$k].ToLower()] = $kw.ru[$k] + } + $re = '^&(' + ($alt -join '|') + ')\("([^"]+)"\)' + $result = @() + for ($i = 0; $i -lt $lines.Count; $i++) { + $t = $lines[$i].Trim() + if ($t -match $re) { + $raw = $Matches[1] + $result += @{ Type = $norm[$raw.ToLower()]; Raw = $raw; Method = $Matches[2]; Line = $i } + } + } + return $result +} + # Compute relative .bsl path segments from ModulePath (used for both ext and src) function Get-ModuleRelPath { param([string]$modulePath) @@ -125,7 +222,8 @@ function Split-TopLevel { # Is a trimmed line a context-directive annotation? function Test-ContextDirective { param([string]$trimmed) - return $trimmed -match '^&(НаКлиенте|НаСервере|НаСервереБезКонтекста|НаКлиентеНаСервереБезКонтекста|НаКлиентеНаСервере)\s*$' + $alt = ($script:bslKw.ru.Directives + $script:bslKw.en.Directives) -join '|' + return $trimmed -match ('^&(' + $alt + ')\s*$') } # Read a method signature starting at declaration line; returns @{ ParamsText; EndLineIdx } @@ -163,46 +261,54 @@ function Read-Signature { # Compute effective condition of the current branch of an #Если frame function Get-EffectiveCondition { - param($frame) + param($frame, [string]$lang) + $kwNot = $script:bslKw[$lang].Not + $kwAnd = $script:bslKw[$lang].And $conds = $frame.Conds $n = $conds.Count if ($frame.InElse) { $parts = @() - foreach ($c in $conds) { $parts += "НЕ ($c)" } - return ($parts -join " И ") + foreach ($c in $conds) { $parts += "$kwNot ($c)" } + return ($parts -join " $kwAnd ") } if ($n -eq 1) { return $conds[0] } $parts = @() - for ($j = 0; $j -lt ($n - 1); $j++) { $parts += "НЕ ($($conds[$j]))" } + for ($j = 0; $j -lt ($n - 1); $j++) { $parts += "$kwNot ($($conds[$j]))" } $parts += $conds[$n - 1] - return ($parts -join " И ") + return ($parts -join " $kwAnd ") } # Extract the enclosing wrapper chain (regions + preprocessor) at a target line. # Returns array outer->inner of @{ Kind='region'|'if'; Name=..; Cond=.. } function Get-EnclosingChain { - param($lines, [int]$targetIdx) + param($lines, [int]$targetIdx, [string]$lang) + $reRegion = '^#' + (Get-KwAlt 'Region') + '\s+(\S+)' + $reEndRegion = '^#' + (Get-KwAlt 'EndRegion') + $reIf = '^#' + (Get-KwAlt 'If') + '\s+(.+?)\s+' + (Get-KwAlt 'Then') + $reElsIf = '^#' + (Get-KwAlt 'ElsIf') + '\s+(.+?)\s+' + (Get-KwAlt 'Then') + $reElse = '^#' + (Get-KwAlt 'Else') + '(\s|$)' + $reEndIf = '^#' + (Get-KwAlt 'EndIf') $stack = New-Object System.Collections.ArrayList for ($i = 0; $i -lt $targetIdx; $i++) { $t = $lines[$i].Trim() - if ($t -match '^#Область\s+(\S+)') { + if ($t -match $reRegion) { [void]$stack.Add(@{ Kind = 'region'; Name = $Matches[1] }) - } elseif ($t -match '^#КонецОбласти') { + } elseif ($t -match $reEndRegion) { for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'region') { $stack.RemoveAt($k); break } } - } elseif ($t -match '^#Если\s+(.+?)\s+Тогда') { + } elseif ($t -match $reIf) { [void]$stack.Add(@{ Kind = 'if'; Conds = @($Matches[1].Trim()); InElse = $false }) - } elseif ($t -match '^#ИначеЕсли\s+(.+?)\s+Тогда') { + } elseif ($t -match $reElsIf) { for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'if') { $stack[$k].Conds += $Matches[1].Trim(); $stack[$k].InElse = $false; break } } - } elseif ($t -match '^#Иначе(\s|$)') { + } elseif ($t -match $reElse) { for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'if') { $stack[$k].InElse = $true; break } } - } elseif ($t -match '^#КонецЕсли') { + } elseif ($t -match $reEndIf) { for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'if') { $stack.RemoveAt($k); break } } } } $chain = @() foreach ($f in $stack) { if ($f.Kind -eq 'region') { $chain += @{ Kind = 'region'; Name = $f.Name } } - else { $chain += @{ Kind = 'if'; Cond = (Get-EffectiveCondition $f) } } + else { $chain += @{ Kind = 'if'; Cond = (Get-EffectiveCondition $f $lang) } } } return ,$chain } @@ -210,13 +316,14 @@ function Get-EnclosingChain { # Extract a method from source .bsl lines. Returns $null if not found. function Extract-Method { param($lines, [string]$methodName) - $declRe = '^\s*(Асинх\s+)?(Процедура|Функция)\s+(' + [regex]::Escape($methodName) + ')\s*\(' + $declRe = '^\s*(' + (Get-KwAlt 'Async') + '\s+)?(' + (Get-KwAlt 'Proc') + '|' + (Get-KwAlt 'Func') + ')\s+(' + [regex]::Escape($methodName) + ')\s*\(' for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -imatch $declRe) { $isAsync = [bool]$Matches[1] $keyword = $Matches[2] $canonical = $Matches[3] - $isFunction = ($keyword -ieq "Функция") + $lang = Get-BslLang $keyword + $isFunction = ($keyword -ieq $script:bslKw.ru.Func -or $keyword -ieq $script:bslKw.en.Func) $sig = Read-Signature $lines $i if (-not $sig) { throw "Не удалось разобрать сигнатуру метода '$methodName'" } @@ -227,13 +334,13 @@ function Extract-Method { $paramNames = @() if ($paramsText.Trim().Length -gt 0) { foreach ($seg in (Split-TopLevel $paramsText)) { - $s = $seg.Trim() -replace '^Знач\s+', '' + $s = $seg.Trim() -replace ('^' + (Get-KwAlt 'Val') + '\s+'), '' if ($s -match '^([\w]+)') { $paramNames += $Matches[1] } } } # Body: from sigEnd+1 to matching Конец* - $endRe = if ($isFunction) { '^\s*КонецФункции\b' } else { '^\s*КонецПроцедуры\b' } + $endRe = if ($isFunction) { '^\s*' + (Get-KwAlt 'EndFunc') + '\b' } else { '^\s*' + (Get-KwAlt 'EndProc') + '\b' } $bodyStart = $sigEnd + 1 $bodyEnd = -1 for ($j = $bodyStart; $j -lt $lines.Count; $j++) { @@ -251,10 +358,11 @@ function Extract-Method { } # Enclosing chain (regions + preprocessor) - $chain = Get-EnclosingChain $lines $i + $chain = Get-EnclosingChain $lines $i $lang return @{ Canonical = $canonical + Lang = $lang IsFunction = $isFunction IsAsync = $isAsync ParamsText = $paramsText @@ -271,25 +379,13 @@ function Extract-Method { return $null } -# Parse interceptors present in a module (type + method) -function Get-Interceptors { - param($lines) - $result = @() - for ($i = 0; $i -lt $lines.Count; $i++) { - $t = $lines[$i].Trim() - if ($t -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') { - $result += @{ Type = $Matches[1]; Method = $Matches[2]; Line = $i } - } - } - return $result -} - # Parse declared procedure/function names in a module function Get-ProcNames { param($lines) + $re = '^\s*(?:' + (Get-KwAlt 'Async') + '\s+)?(?:' + (Get-KwAlt 'Proc') + '|' + (Get-KwAlt 'Func') + ')\s+([\w]+)\s*\(' $names = @() foreach ($line in $lines) { - if ($line -imatch '^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(') { $names += $Matches[1] } + if ($line -imatch $re) { $names += $Matches[1] } } return $names } @@ -300,14 +396,15 @@ function Get-ProcNames { function Build-InterceptorCore { param($method, [string]$interceptorType, [string]$interceptorName) - $decoratorRu = $script:decoratorMap[$interceptorType] - $asyncPrefix = if ($method.IsAsync) { "Асинх " } else { "" } - $keyword = if ($method.IsFunction) { "Функция" } else { "Процедура" } - $endKeyword = if ($method.IsFunction) { "КонецФункции" } else { "КонецПроцедуры" } + $kw = $script:bslKw[$method.Lang] + $decorator = $kw[$script:decoratorKey[$interceptorType]] + $asyncPrefix = if ($method.IsAsync) { "$($kw.Async) " } else { "" } + $keyword = if ($method.IsFunction) { $kw.Func } else { $kw.Proc } + $endKeyword = if ($method.IsFunction) { $kw.EndFunc } else { $kw.EndProc } $lines = @() if ($method.Context) { $lines += $method.Context } - $lines += "&$decoratorRu(`"$($method.Canonical)`")" + $lines += "&$decorator(`"$($method.Canonical)`")" $lines += "$asyncPrefix$keyword $interceptorName($($method.ParamsText))" switch ($interceptorType) { @@ -320,11 +417,11 @@ function Build-InterceptorCore { "Instead" { $namesJoined = ($method.ParamNames -join ", ") if ($method.IsFunction) { - $lines += "`tРезультат = ПродолжитьВызов($namesJoined);" + $lines += "`t$($kw.ResultVar) = $($kw.Proceed)($namesJoined);" $lines += "`t// TODO: доработать поведение" - $lines += "`tВозврат Результат;" + $lines += "`t$($kw.Return) $($kw.ResultVar);" } else { - $lines += "`tПродолжитьВызов($namesJoined);" + $lines += "`t$($kw.Proceed)($namesJoined);" $lines += "`t// TODO: доработать поведение" } } @@ -339,16 +436,17 @@ function Build-InterceptorCore { # Wrap core with region/preprocessor lines, adding blank lines ("air") around # each structural boundary. Empty chain -> core as-is. function Build-WrappedBlock { - param($chainArr, $core) + param($chainArr, $core, [string]$lang) + $kw = $script:bslKw[$lang] $b = @() foreach ($w in $chainArr) { - if ($w.Kind -eq 'region') { $b += "#Область $($w.Name)" } else { $b += "#Если $($w.Cond) Тогда" } + if ($w.Kind -eq 'region') { $b += "#$($kw.Region) $($w.Name)" } else { $b += "#$($kw.If) $($w.Cond) $($kw.Then)" } $b += "" } $b += $core for ($c = $chainArr.Count - 1; $c -ge 0; $c--) { $b += "" - if ($chainArr[$c].Kind -eq 'region') { $b += "#КонецОбласти" } else { $b += "#КонецЕсли" } + if ($chainArr[$c].Kind -eq 'region') { $b += "#$($kw.EndRegion)" } else { $b += "#$($kw.EndIf)" } } return $b } @@ -385,18 +483,18 @@ function Parse-MarkedBody { $ops = @() # edit operations $i = 0 while ($i -lt $bodyLines.Count) { - $t = $bodyLines[$i].Trim() - if ($t -eq '#Вставка') { + $kind = Get-BslMarkerKind $bodyLines[$i] + if ($kind -eq 'Insert') { $ins = @() $i++ - while ($i -lt $bodyLines.Count -and $bodyLines[$i].Trim() -ne '#КонецВставки') { $ins += $bodyLines[$i]; $i++ } + while ($i -lt $bodyLines.Count -and (Get-BslMarkerKind $bodyLines[$i]) -ne 'EndInsert') { $ins += $bodyLines[$i]; $i++ } $i++ # skip #КонецВставки $ops += @{ Kind = 'insert'; After = ($v1.Count - 1); Lines = $ins } - } elseif ($t -eq '#Удаление') { + } elseif ($kind -eq 'Delete') { $startIdx = $v1.Count $i++ $del = @() - while ($i -lt $bodyLines.Count -and $bodyLines[$i].Trim() -ne '#КонецУдаления') { $del += $bodyLines[$i]; $v1 += $bodyLines[$i]; $i++ } + while ($i -lt $bodyLines.Count -and (Get-BslMarkerKind $bodyLines[$i]) -ne 'EndDelete') { $del += $bodyLines[$i]; $v1 += $bodyLines[$i]; $i++ } $i++ # skip #КонецУдаления $ops += @{ Kind = 'delete'; Start = $startIdx; End = ($v1.Count - 1); Lines = $del } } else { @@ -594,7 +692,8 @@ function Get-ResyncConflictReason { # Write per-method conflict folder: conflict.md + base/local/remote function Write-ConflictFolder { - param($folder, $methodId, $extBsl, $existingName, $method, $v1, $markedBody, $v2, $v1norm, $v2norm, $disputed, $enc) + param($folder, $methodId, $extBsl, $existingName, $method, $v1, $markedBody, $v2, $v1norm, $v2norm, $disputed, [string]$lang, $enc) + $kw = $script:bslKw[$lang] if (-not (Test-Path $folder)) { New-Item -ItemType Directory -Path $folder -Force | Out-Null } [IO.File]::WriteAllText((Join-Path $folder 'base.bsl'), (($v1 -join "`r`n") + "`r`n"), $enc) [IO.File]::WriteAllText((Join-Path $folder 'local.bsl'), (($markedBody -join "`r`n") + "`r`n"), $enc) @@ -602,7 +701,7 @@ function Write-ConflictFolder { $md = @() $md += "# $methodId" $md += "Править: $extBsl" - $md += "Метод: $existingName (&ИзменениеИКонтроль(`"$($method.Canonical)`"))" + $md += "Метод: $existingName (&$($kw.Control)(`"$($method.Canonical)`"))" $md += "Причина: $(Get-ResyncConflictReason $disputed)" $md += "" $md += "## Не размещено — перенести вручную" @@ -614,10 +713,10 @@ function Write-ConflictFolder { $md += "### Конфликт №$cn — вставка" $md += "Как блок стоял в вашей версии (local):" if ($d.Before -and $d.Before.Count -gt 0) { foreach ($l in $d.Before) { $md += $l } } - $md += "#Вставка"; foreach ($l in $d.Lines) { $md += $l }; $md += "#КонецВставки" + $md += "#$($kw.Insert)"; foreach ($l in $d.Lines) { $md += $l }; $md += "#$($kw.EndInsert)" if ($d.After -and $d.After.Count -gt 0) { foreach ($l in $d.After) { $md += $l } } $md += "" - $md += "Якорь (строки вокруг #Вставка) изменился/исчез в новом оригинале — блок не лёг автоматически (см. дифф base→remote ниже)." + $md += "Якорь (строки вокруг #$($kw.Insert)) изменился/исчез в новом оригинале — блок не лёг автоматически (см. дифф base→remote ниже)." $md += "В модуле расширения блок припаркован в конце метода под меткой // [РЕСИНК-КОНФЛИКТ №$cn] — найди по ней." $md += "Куда переносить: если якорного кода в новом методе больше нет — он, вероятно, вынесен/отрефакторен (ищите в диффе новый вызов/процедуру). Размести адаптацию по смыслу: например пост-обработкой после нового вызова, либо в заимствованной процедуре, куда переехал код. При правке файла сохрани кодировку (UTF-8 с BOM)." } else { @@ -644,15 +743,20 @@ function Invoke-Resync { $methodId = "$logicalModule.$($method.Canonical)" $decLine = $dup.Line $sigLineIdx = $decLine + 1 - if ($sigLineIdx -ge $extLines.Count -or $extLines[$sigLineIdx] -notmatch '^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(') { + $declRe = '^\s*(?:' + (Get-KwAlt 'Async') + '\s+)?(' + (Get-KwAlt 'Proc') + '|' + (Get-KwAlt 'Func') + ')\s+([\w]+)\s*\(' + if ($sigLineIdx -ge $extLines.Count -or $extLines[$sigLineIdx] -notmatch $declRe) { return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не разобрать сигнатуру перехватчика' } } - $existingName = $Matches[1] + # The block we are about to rewrite is the interceptor already in the extension, so it is + # its spelling — not the source module's — that the rewritten block must keep. + $lang = Get-BslLang $Matches[1] + $kw = $script:bslKw[$lang] + $existingName = $Matches[2] $sig = Read-Signature $extLines $sigLineIdx if (-not $sig) { return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не разобрать сигнатуру' } } $sigEnd = $sig.EndLineIdx - $isFunc = ($extLines[$sigLineIdx] -imatch '^\s*(?:Асинх\s+)?Функция\b') - $endRe = if ($isFunc) { '^\s*КонецФункции\b' } else { '^\s*КонецПроцедуры\b' } + $isFunc = ($extLines[$sigLineIdx] -imatch ('^\s*(?:' + (Get-KwAlt 'Async') + '\s+)?' + (Get-KwAlt 'Func') + '\b')) + $endRe = if ($isFunc) { '^\s*' + (Get-KwAlt 'EndFunc') + '\b' } else { '^\s*' + (Get-KwAlt 'EndProc') + '\b' } $blockEnd = -1 for ($j = $sigEnd + 1; $j -lt $extLines.Count; $j++) { if ($extLines[$j] -imatch $endRe) { $blockEnd = $j; break } } if ($blockEnd -lt 0) { return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не найден конец перехватчика' } } @@ -732,12 +836,12 @@ function Invoke-Resync { # assemble new marked body $newBody = @() - foreach ($blk in $insertTop) { $newBody += "#Вставка"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#КонецВставки" } + foreach ($blk in $insertTop) { $newBody += "#$($kw.Insert)"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#$($kw.EndInsert)" } for ($k = 0; $k -lt $v2.Count; $k++) { - if ($delStart.ContainsKey($k)) { $newBody += "#Удаление" } + if ($delStart.ContainsKey($k)) { $newBody += "#$($kw.Delete)" } $newBody += $v2[$k] - if ($delEnd.ContainsKey($k)) { $newBody += "#КонецУдаления" } - if ($insertAfter.ContainsKey($k)) { foreach ($blk in $insertAfter[$k]) { $newBody += "#Вставка"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#КонецВставки" } } + if ($delEnd.ContainsKey($k)) { $newBody += "#$($kw.EndDelete)" } + if ($insertAfter.ContainsKey($k)) { foreach ($blk in $insertAfter[$k]) { $newBody += "#$($kw.Insert)"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#$($kw.EndInsert)" } } } if ($disputed.Count -gt 0) { $newBody += "`t// [РЕСИНК-КОНФЛИКТ] блоки ниже не легли автоматически — перенесите вручную (по № см. conflict.md / index.md в merge-воркспейсе, путь в выводе)." @@ -746,7 +850,7 @@ function Invoke-Resync { $cn++ if ($d.Kind -eq 'insert') { $newBody += "`t// [РЕСИНК-КОНФЛИКТ №$cn] вставка — исходный якорь изменён в новом оригинале." - $newBody += "#Вставка"; foreach ($l in $d.Lines) { $newBody += $l }; $newBody += "#КонецВставки" + $newBody += "#$($kw.Insert)"; foreach ($l in $d.Lines) { $newBody += $l }; $newBody += "#$($kw.EndInsert)" } else { $newBody += "`t// [РЕСИНК-КОНФЛИКТ №$cn] удаление — строки не найдены в новом оригинале:" @@ -754,12 +858,12 @@ function Invoke-Resync { } } } - $asyncPrefix = if ($method.IsAsync) { "Асинх " } else { "" } - $keyword = if ($method.IsFunction) { "Функция" } else { "Процедура" } - $endKeyword = if ($method.IsFunction) { "КонецФункции" } else { "КонецПроцедуры" } + $asyncPrefix = if ($method.IsAsync) { "$($kw.Async) " } else { "" } + $keyword = if ($method.IsFunction) { $kw.Func } else { $kw.Proc } + $endKeyword = if ($method.IsFunction) { $kw.EndFunc } else { $kw.EndProc } $newBlock = @() if ($method.Context) { $newBlock += $method.Context } - $newBlock += "&ИзменениеИКонтроль(`"$($method.Canonical)`")" + $newBlock += "&$($kw.Control)(`"$($method.Canonical)`")" $newBlock += "$asyncPrefix$keyword $existingName($($method.ParamsText))" $newBlock += $newBody $newBlock += $endKeyword @@ -775,7 +879,7 @@ function Invoke-Resync { $conflictDir = $null if ($disputed.Count -gt 0) { $conflictDir = $conflictFolder - Write-ConflictFolder $conflictFolder $methodId $extBsl $existingName $method $v1 $markedBody $v2 $v1norm $v2norm $disputed $enc + Write-ConflictFolder $conflictFolder $methodId $extBsl $existingName $method $v1 $markedBody $v2 $v1norm $v2norm $disputed $lang $enc } $status = if ($disputed.Count -gt 0) { 'ЧАСТИЧНО' } elseif ($transferred.Count -eq 0 -and $absorbed.Count -gt 0) { 'ПЕРЕНЕСЕНО В ОСНОВНУЮ' } else { 'АКТУАЛИЗИРОВАН' } $rsn = if ($disputed.Count -gt 0) { Get-ResyncConflictReason $disputed } elseif ($status -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { 'все правки уже в основной конфигурации — перехватчик можно удалить' } else { '' } @@ -1062,6 +1166,8 @@ if ($method.IsFunction -and ($InterceptorType -eq "Before" -or $InterceptorType } $decoratorRu = $script:decoratorMap[$InterceptorType] +# What the annotation will actually look like in the module: the source's spelling +$decoratorOut = $script:bslKw[$method.Lang][$script:decoratorKey[$InterceptorType]] # --- Read existing extension module (if any) --- $extLines = @() @@ -1078,7 +1184,7 @@ $enc = New-Object System.Text.UTF8Encoding($true) if ($dup) { if ($InterceptorType -ne "ModificationAndControl") { - Write-Host "[ПРОПУЩЕН] Перехватчик &$decoratorRu(`"$MethodName`") уже есть в модуле — дубль не создаётся." + Write-Host "[ПРОПУЩЕН] Перехватчик &$($dup.Raw)(`"$MethodName`") уже есть в модуле — дубль не создаётся." Write-Host " Файл: $extBsl" exit 0 } @@ -1123,11 +1229,7 @@ $candidate = "${namePrefix}$($method.Canonical)" $taken = @($existingProcNames | ForEach-Object { $_.ToLower() }) $interceptorName = $candidate if ($taken -contains $candidate.ToLower()) { - if ($InterceptorType -eq "ModificationAndControl") { - $interceptorName = "${candidate}_ИзменениеИКонтроль" - } else { - $interceptorName = "${candidate}_$decoratorRu" - } + $interceptorName = "${candidate}_$($script:bslKw[$method.Lang][$script:decoratorKey[$InterceptorType]])" } $core = Build-InterceptorCore $method $InterceptorType $interceptorName @@ -1142,7 +1244,7 @@ if ($extExists) { if ($chain[$c].Kind -eq 'region') { $rname = $chain[$c].Name for ($li = 0; $li -lt $extLines.Count; $li++) { - if ($extLines[$li].Trim() -match ('^#Область\s+' + [regex]::Escape($rname) + '\s*$')) { + if ($extLines[$li].Trim() -match ('^#' + (Get-KwAlt 'Region') + '\s+' + [regex]::Escape($rname) + '\s*$')) { $reuseRegionIdx = $c; $reuseLineIdx = $li; break } } @@ -1157,14 +1259,14 @@ if ($reuseRegionIdx -ge 0) { $innerChain = @() for ($c = $reuseRegionIdx + 1; $c -lt $chain.Count; $c++) { $innerChain += $chain[$c] } - $block = Build-WrappedBlock $innerChain $core + $block = Build-WrappedBlock $innerChain $core $method.Lang # find matching #КонецОбласти for the reused region $depth = 0; $closeIdx = -1 for ($li = $reuseLineIdx; $li -lt $extLines.Count; $li++) { $t = $extLines[$li].Trim() - if ($t -match '^#Область\s') { $depth++ } - elseif ($t -match '^#КонецОбласти') { $depth--; if ($depth -eq 0) { $closeIdx = $li; break } } + if ($t -match ('^#' + (Get-KwAlt 'Region') + '\s')) { $depth++ } + elseif ($t -match ('^#' + (Get-KwAlt 'EndRegion'))) { $depth--; if ($depth -eq 0) { $closeIdx = $li; break } } } if ($closeIdx -lt 0) { Write-Error "Не найден #КонецОбласти для региона (переиспользование)"; exit 1 } @@ -1181,7 +1283,7 @@ if ($reuseRegionIdx -ge 0) { $placement = "в существующий регион '$($chain[$reuseRegionIdx].Name)'" } else { # Build full wrapper chain (source order) and append (or create file). - $block = Build-WrappedBlock $chain $core + $block = Build-WrappedBlock $chain $core $method.Lang $blockText = ($block -join "`r`n") + "`r`n" $bslDir = Split-Path $extBsl -Parent @@ -1210,7 +1312,7 @@ if ($flagTarget) { Set-PropertyStateFlag $flagTarget.File $flagTarget.Property (Detect-FormatVersion $ExtensionPath) } -Write-Host "[OK] Перехватчик &$decoratorRu(`"$MethodName`") — $placement" +Write-Host "[OK] Перехватчик &$decoratorOut(`"$MethodName`") — $placement" Write-Host " Файл: $extBsl" Write-Host " Процедура: $interceptorName($($method.ParamsText -replace '\s+', ' '))" if ($method.Context) { Write-Host " Контекст: $($method.Context)" } diff --git a/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py index 01fd3f392..28f05bf34 100644 --- a/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py +++ b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cfe-patch-method v2.11 — Source-aware method interceptor for 1C extension (CFE) +# cfe-patch-method v2.12 — Source-aware method interceptor for 1C extension (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -70,8 +70,81 @@ DECORATOR_MAP = { "ModificationAndControl": "ИзменениеИКонтроль", } +# InterceptorType -> keyword table key (emission goes through the table, the map above stays +# the normalized value the logic compares against) +DECORATOR_KEY = { + "Before": "Before", "After": "After", "Instead": "Around", + "ModificationAndControl": "Control", +} + + +# 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. +def bsl_keywords(): + return { + "ru": { + "Async": "Асинх", "Proc": "Процедура", "EndProc": "КонецПроцедуры", + "Func": "Функция", "EndFunc": "КонецФункции", "Val": "Знач", + "Region": "Область", "EndRegion": "КонецОбласти", + "If": "Если", "Then": "Тогда", "ElsIf": "ИначеЕсли", "Else": "Иначе", "EndIf": "КонецЕсли", + "And": "И", "Not": "НЕ", + "Insert": "Вставка", "EndInsert": "КонецВставки", "Delete": "Удаление", "EndDelete": "КонецУдаления", + "Before": "Перед", "After": "После", "Around": "Вместо", "Control": "ИзменениеИКонтроль", + "Proceed": "ПродолжитьВызов", "Return": "Возврат", + # Not a keyword: name of the local the generated Instead-stub declares. Lives here so + # that the language of emitted text is decided in exactly one place. + "ResultVar": "Результат", + "Directives": ["НаКлиенте", "НаСервере", "НаСервереБезКонтекста", "НаКлиентеНаСервереБезКонтекста", "НаКлиентеНаСервере"], + }, + "en": { + "Async": "Async", "Proc": "Procedure", "EndProc": "EndProcedure", + "Func": "Function", "EndFunc": "EndFunction", "Val": "Val", + "Region": "Region", "EndRegion": "EndRegion", + "If": "If", "Then": "Then", "ElsIf": "ElsIf", "Else": "Else", "EndIf": "EndIf", + "And": "And", "Not": "Not", + "Insert": "Insert", "EndInsert": "EndInsert", "Delete": "Delete", "EndDelete": "EndDelete", + "Before": "Before", "After": "After", "Around": "Around", "Control": "ChangeAndValidate", + "Proceed": "ProceedWithCall", "Return": "Return", + "ResultVar": "Result", + "Directives": ["AtClient", "AtServer", "AtServerNoContext", "AtClientAtServerNoContext", "AtClientAtServer"], + }, + } + + +BSL_KW = bsl_keywords() + + +# Regex alternation for a keyword in both spellings +def kw_alt(key): + return '(?:' + BSL_KW["ru"][key] + '|' + BSL_KW["en"][key] + ')' + + +# Spelling language of a recognized keyword. English is the discriminator: a Russian keyword +# never equals an English one, so anything else is 'ru'. +def bsl_lang(word): + w = word.strip().lower() + for k in ("Async", "Proc", "EndProc", "Func", "EndFunc", "Region", "If", "Insert", "Delete", + "Before", "After", "Around", "Control"): + if BSL_KW["en"][k].lower() == w: + return "en" + return "ru" + + +# Edit marker kind at the start of a line: Insert/Delete/EndInsert/EndDelete, None if none. +# The marker owns the start of the line only — a trailing comment does not bother the platform +# and must not bother us either. +def bsl_marker_kind(line): + kw = BSL_KW + for k in ("EndInsert", "EndDelete", "Insert", "Delete"): + if re.match(r'^\s*#(?:' + kw["ru"][k] + '|' + kw["en"][k] + r')\b', line, re.IGNORECASE): + return k + return None + + CONTEXT_RE = re.compile( - r'^&(НаКлиенте|НаСервере|НаСервереБезКонтекста|НаКлиентеНаСервереБезКонтекста|НаКлиентеНаСервере)\s*$' + r'^&(' + '|'.join(BSL_KW["ru"]["Directives"] + BSL_KW["en"]["Directives"]) + r')\s*$', + re.IGNORECASE ) @@ -300,37 +373,45 @@ def read_signature(lines, start_idx): return None -def effective_condition(frame): +def effective_condition(frame, lang): + kw_not = BSL_KW[lang]["Not"] + kw_and = BSL_KW[lang]["And"] conds = frame["conds"] n = len(conds) if frame["in_else"]: - return " И ".join("НЕ (%s)" % c for c in conds) + return (" %s " % kw_and).join("%s (%s)" % (kw_not, c) for c in conds) if n == 1: return conds[0] - parts = ["НЕ (%s)" % conds[j] for j in range(n - 1)] + parts = ["%s (%s)" % (kw_not, conds[j]) for j in range(n - 1)] parts.append(conds[n - 1]) - return " И ".join(parts) + return (" %s " % kw_and).join(parts) -def get_enclosing_chain(lines, target_idx): +def get_enclosing_chain(lines, target_idx, lang): + re_region = re.compile(r'^#' + kw_alt("Region") + r'\s+(\S+)', re.IGNORECASE) + re_end_region = re.compile(r'^#' + kw_alt("EndRegion"), re.IGNORECASE) + re_if = re.compile(r'^#' + kw_alt("If") + r'\s+(.+?)\s+' + kw_alt("Then"), re.IGNORECASE) + re_elsif = re.compile(r'^#' + kw_alt("ElsIf") + r'\s+(.+?)\s+' + kw_alt("Then"), re.IGNORECASE) + re_else = re.compile(r'^#' + kw_alt("Else") + r'(\s|$)', re.IGNORECASE) + re_endif = re.compile(r'^#' + kw_alt("EndIf"), re.IGNORECASE) stack = [] for i in range(target_idx): t = lines[i].strip() - m = re.match(r'^#Область\s+(\S+)', t) + m = re_region.match(t) if m: stack.append({"kind": "region", "name": m.group(1)}) continue - if re.match(r'^#КонецОбласти', t): + if re_end_region.match(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) + m = re_if.match(t) if m: stack.append({"kind": "if", "conds": [m.group(1).strip()], "in_else": False}) continue - m = re.match(r'^#ИначеЕсли\s+(.+?)\s+Тогда', t) + m = re_elsif.match(t) if m: for k in range(len(stack) - 1, -1, -1): if stack[k]["kind"] == "if": @@ -338,13 +419,13 @@ def get_enclosing_chain(lines, target_idx): stack[k]["in_else"] = False break continue - if re.match(r'^#Иначе(\s|$)', t): + if re_else.match(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): + if re_endif.match(t): for k in range(len(stack) - 1, -1, -1): if stack[k]["kind"] == "if": del stack[k] @@ -355,13 +436,14 @@ def get_enclosing_chain(lines, target_idx): if f["kind"] == "region": chain.append({"kind": "region", "name": f["name"]}) else: - chain.append({"kind": "if", "cond": effective_condition(f)}) + chain.append({"kind": "if", "cond": effective_condition(f, lang)}) return chain def extract_method(lines, method_name): decl_re = re.compile( - r'^\s*(Асинх\s+)?(Процедура|Функция)\s+(' + re.escape(method_name) + r')\s*\(', + r'^\s*(' + kw_alt("Async") + r'\s+)?(' + kw_alt("Proc") + '|' + kw_alt("Func") + r')\s+(' + + re.escape(method_name) + r')\s*\(', re.IGNORECASE, ) for i in range(len(lines)): @@ -371,7 +453,8 @@ def extract_method(lines, method_name): is_async = bool(m.group(1)) keyword = m.group(2) canonical = m.group(3) - is_function = keyword.lower() == "функция" + lang = bsl_lang(keyword) + is_function = keyword.lower() in (BSL_KW["ru"]["Func"].lower(), BSL_KW["en"]["Func"].lower()) sig = read_signature(lines, i) if not sig: @@ -381,12 +464,12 @@ def extract_method(lines, method_name): param_names = [] if params_text.strip(): for seg in split_top_level(params_text): - s = re.sub(r'^Знач\s+', '', seg.strip()) + s = re.sub(r'^' + kw_alt("Val") + r'\s+', '', seg.strip(), flags=re.IGNORECASE) 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', + end_re = re.compile(r'^\s*' + (kw_alt("EndFunc") if is_function else kw_alt("EndProc")) + r'\b', re.IGNORECASE) body_start = sig_end + 1 body_end = -1 @@ -404,10 +487,11 @@ def extract_method(lines, method_name): if is_context_directive(prev): context = prev - chain = get_enclosing_chain(lines, i) + chain = get_enclosing_chain(lines, i, lang) return { "canonical": canonical, + "lang": lang, "is_function": is_function, "is_async": is_async, "params_text": params_text, @@ -422,19 +506,32 @@ def extract_method(lines, method_name): return None +# Parse interceptor annotations present in a module. Type is normalized to the Russian spelling +# so everything downstream stays single-language; raw keeps what the file actually says. def get_interceptors(lines): + kw = BSL_KW + keys = ("Before", "After", "Control", "Around") + alt = [] + norm = {} + for k in keys: + alt.append(kw["ru"][k]) + alt.append(kw["en"][k]) + norm[kw["ru"][k].lower()] = kw["ru"][k] + norm[kw["en"][k].lower()] = kw["ru"][k] + pat = re.compile(r'^&(' + '|'.join(alt) + r')\("([^"]+)"\)', re.IGNORECASE) 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}) + raw = m.group(1) + result.append({"type": norm[raw.lower()], "raw": raw, "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) + pat = re.compile(r'^\s*(?:' + kw_alt("Async") + r'\s+)?(?:' + kw_alt("Proc") + '|' + kw_alt("Func") + + r')\s+([\w]+)\s*\(', re.IGNORECASE) for line in lines: m = pat.match(line) if m: @@ -443,15 +540,16 @@ def get_proc_names(lines): 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 "КонецПроцедуры" + kw = BSL_KW[method["lang"]] + decorator = kw[DECORATOR_KEY[interceptor_type]] + async_prefix = (kw["Async"] + " ") if method["is_async"] else "" + keyword = kw["Func"] if method["is_function"] else kw["Proc"] + end_keyword = kw["EndFunc"] if method["is_function"] else kw["EndProc"] lines = [] if method["context"]: lines.append(method["context"]) - lines.append('&%s("%s")' % (decorator_ru, method["canonical"])) + lines.append('&%s("%s")' % (decorator, method["canonical"])) lines.append("%s%s %s(%s)" % (async_prefix, keyword, interceptor_name, method["params_text"])) if interceptor_type == "Before": @@ -461,11 +559,11 @@ def build_interceptor_core(method, interceptor_type, interceptor_name): elif interceptor_type == "Instead": names_joined = ", ".join(method["param_names"]) if method["is_function"]: - lines.append("\tРезультат = ПродолжитьВызов(%s);" % names_joined) + lines.append("\t%s = %s(%s);" % (kw["ResultVar"], kw["Proceed"], names_joined)) lines.append("\t// TODO: доработать поведение") - lines.append("\tВозврат Результат;") + lines.append("\t%s %s;" % (kw["Return"], kw["ResultVar"])) else: - lines.append("\tПродолжитьВызов(%s);" % names_joined) + lines.append("\t%s(%s);" % (kw["Proceed"], names_joined)) lines.append("\t// TODO: доработать поведение") elif interceptor_type == "ModificationAndControl": lines.extend(method["body_lines"]) @@ -474,16 +572,18 @@ def build_interceptor_core(method, interceptor_type, interceptor_name): return lines -def build_wrapped_block(chain_arr, core): +def build_wrapped_block(chain_arr, core, lang): """Wrap core with region/preprocessor lines, adding blank lines around each boundary.""" + kw = BSL_KW[lang] b = [] for w in chain_arr: - b.append("#Область %s" % w["name"] if w["kind"] == "region" else "#Если %s Тогда" % w["cond"]) + b.append(("#%s %s" % (kw["Region"], w["name"])) if w["kind"] == "region" + else ("#%s %s %s" % (kw["If"], w["cond"], kw["Then"]))) b.append("") b.extend(core) for w in reversed(chain_arr): b.append("") - b.append("#КонецОбласти" if w["kind"] == "region" else "#КонецЕсли") + b.append(("#" + kw["EndRegion"]) if w["kind"] == "region" else ("#" + kw["EndIf"])) return b @@ -512,20 +612,20 @@ def parse_marked_body(body_lines): i = 0 n = len(body_lines) while i < n: - t = body_lines[i].strip() - if t == "#Вставка": + kind = bsl_marker_kind(body_lines[i]) + if kind == "Insert": ins = [] i += 1 - while i < n and body_lines[i].strip() != "#КонецВставки": + while i < n and bsl_marker_kind(body_lines[i]) != "EndInsert": ins.append(body_lines[i]) i += 1 i += 1 ops.append({"kind": "insert", "after": len(v1) - 1, "lines": ins}) - elif t == "#Удаление": + elif kind == "Delete": start_idx = len(v1) i += 1 dels = [] - while i < n and body_lines[i].strip() != "#КонецУдаления": + while i < n and bsl_marker_kind(body_lines[i]) != "EndDelete": dels.append(body_lines[i]) v1.append(body_lines[i]) i += 1 @@ -879,6 +979,8 @@ def main(): "(перехват &Перед/&После к функциям неприменим)." % method_name) decorator_ru = DECORATOR_MAP[interceptor_type] + # What the annotation will actually look like in the module: the source's spelling + decorator_out = BSL_KW[method["lang"]][DECORATOR_KEY[interceptor_type]] # --- Read existing extension module (if any) --- ext_exists = os.path.isfile(ext_bsl) @@ -896,7 +998,7 @@ def main(): if dup: if interceptor_type != "ModificationAndControl": print('[ПРОПУЩЕН] Перехватчик &%s("%s") уже есть в модуле — дубль не создаётся.' - % (decorator_ru, method_name)) + % (dup["raw"], method_name)) print(" Файл: %s" % ext_bsl) sys.exit(0) rel = rel_parts_under(extension_path, ext_bsl) @@ -940,14 +1042,11 @@ def main(): 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 + interceptor_name = candidate + "_" + decorator_out core = build_interceptor_core(method, interceptor_type, interceptor_name) - place_new(ext_bsl, ext_lines, ext_exists, method["chain"], core) + place_new(ext_bsl, ext_lines, ext_exists, method["chain"], core, method["lang"]) # Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19). flag_target = get_module_flag_target(rel_parts, extension_path) @@ -957,7 +1056,7 @@ def main(): # emit summary placement = place_new.placement - print('[OK] Перехватчик &%s("%s") — %s' % (decorator_ru, method_name, placement)) + print('[OK] Перехватчик &%s("%s") — %s' % (decorator_out, method_name, placement)) print(" Файл: %s" % ext_bsl) print(" Процедура: %s(%s)" % (interceptor_name, normalize(method["params_text"]))) if method["context"]: @@ -970,7 +1069,7 @@ def main(): print(" Обрамление: %s" % desc) -def place_new(ext_bsl, ext_lines, ext_exists, chain, core): +def place_new(ext_bsl, ext_lines, ext_exists, chain, core, lang): enc_bom = True # noqa # find innermost region in chain that already exists in the extension module @@ -980,7 +1079,7 @@ def place_new(ext_bsl, ext_lines, ext_exists, chain, core): 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*$') + rre = re.compile(r'^#' + kw_alt("Region") + r'\s+' + re.escape(rname) + r'\s*$', re.IGNORECASE) for li in range(len(ext_lines)): if rre.match(ext_lines[li].strip()): reuse_region_idx = c @@ -991,15 +1090,15 @@ def place_new(ext_bsl, ext_lines, ext_exists, chain, core): if reuse_region_idx >= 0: inner_chain = chain[reuse_region_idx + 1:] - block = build_wrapped_block(inner_chain, core) + block = build_wrapped_block(inner_chain, core, lang) 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): + if re.match(r'^#' + kw_alt("Region") + r'\s', t, re.IGNORECASE): depth += 1 - elif re.match(r'^#КонецОбласти', t): + elif re.match(r'^#' + kw_alt("EndRegion"), t, re.IGNORECASE): depth -= 1 if depth == 0: close_idx = li @@ -1021,7 +1120,7 @@ def place_new(ext_bsl, ext_lines, ext_exists, chain, core): return # full wrapper chain, append (or create) - block = build_wrapped_block(chain, core) + block = build_wrapped_block(chain, core, lang) block_text = "\r\n".join(block) + "\r\n" bsl_dir = os.path.dirname(ext_bsl) @@ -1083,7 +1182,8 @@ def conflict_reason(disputed): return "; ".join(parts) -def write_conflict_folder(folder, method_id, ext_bsl, existing_name, method, v1, marked_body, v2, v1norm, v2norm, disputed): +def write_conflict_folder(folder, method_id, ext_bsl, existing_name, method, v1, marked_body, v2, v1norm, v2norm, disputed, lang): + kw = BSL_KW[lang] 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) @@ -1091,7 +1191,7 @@ def write_conflict_folder(folder, method_id, ext_bsl, existing_name, method, v1, md = [] md.append("# %s" % method_id) md.append("Править: %s" % ext_bsl) - md.append('Метод: %s (&ИзменениеИКонтроль("%s"))' % (existing_name, method["canonical"])) + md.append('Метод: %s (&%s("%s"))' % (existing_name, kw["Control"], method["canonical"])) md.append("Причина: %s" % conflict_reason(disputed)) md.append("") md.append("## Не размещено — перенести вручную") @@ -1105,12 +1205,12 @@ def write_conflict_folder(folder, method_id, ext_bsl, existing_name, method, v1, if d.get("before"): for l in d["before"]: md.append(l) - md.append("#Вставка"); md.extend(d["lines"]); md.append("#КонецВставки") + md.append("#" + kw["Insert"]); md.extend(d["lines"]); md.append("#" + kw["EndInsert"]) if d.get("after"): for l in d["after"]: md.append(l) md.append("") - md.append("Якорь (строки вокруг #Вставка) изменился/исчез в новом оригинале — блок не лёг автоматически (см. дифф base→remote ниже).") + md.append("Якорь (строки вокруг #%s) изменился/исчез в новом оригинале — блок не лёг автоматически (см. дифф base→remote ниже)." % kw["Insert"]) md.append("В модуле расширения блок припаркован в конце метода под меткой // [РЕСИНК-КОНФЛИКТ №%d] — найди по ней." % cn) md.append("Куда переносить: если якорного кода в новом методе больше нет — он, вероятно, вынесен/отрефакторен (ищите в диффе новый вызов/процедуру). Размести адаптацию по смыслу: например пост-обработкой после нового вызова, либо в заимствованной процедуре, куда переехал код. При правке файла сохрани кодировку (UTF-8 с BOM).") else: @@ -1136,17 +1236,23 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder, 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) + name_re = re.compile(r'^\s*(?:' + kw_alt("Async") + r'\s+)?(' + kw_alt("Proc") + '|' + kw_alt("Func") + + r')\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) + # The block we are about to rewrite is the interceptor already in the extension, so it is + # its spelling — not the source module's — that the rewritten block must keep. + lang = bsl_lang(m0.group(1)) + kw = BSL_KW[lang] + existing_name = m0.group(2) sig = read_signature(ext_lines, sig_line_idx) if not sig: return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру"} ext_params_text, 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) + is_func = bool(re.match(r'^\s*(?:' + kw_alt("Async") + r'\s+)?' + kw_alt("Func") + r'\b', + ext_lines[sig_line_idx], re.IGNORECASE)) + end_re = re.compile(r'^\s*' + (kw_alt("EndFunc") if is_func else kw_alt("EndProc")) + r'\b', re.IGNORECASE) block_end = -1 for j in range(sig_end + 1, len(ext_lines)): if end_re.match(ext_lines[j]): @@ -1241,16 +1347,16 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder, new_body = [] for blk in insert_top: - new_body.append("#Вставка"); new_body.extend(blk); new_body.append("#КонецВставки") + new_body.append("#" + kw["Insert"]); new_body.extend(blk); new_body.append("#" + kw["EndInsert"]) for k in range(len(v2)): if k in del_start: - new_body.append("#Удаление") + new_body.append("#" + kw["Delete"]) new_body.append(v2[k]) if k in del_end: - new_body.append("#КонецУдаления") + new_body.append("#" + kw["EndDelete"]) if k in insert_after: for blk in insert_after[k]: - new_body.append("#Вставка"); new_body.extend(blk); new_body.append("#КонецВставки") + new_body.append("#" + kw["Insert"]); new_body.extend(blk); new_body.append("#" + kw["EndInsert"]) if disputed: new_body.append("\t// [РЕСИНК-КОНФЛИКТ] блоки ниже не легли автоматически — перенесите вручную (по № см. conflict.md / index.md в merge-воркспейсе, путь в выводе).") cn = 0 @@ -1258,19 +1364,19 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder, cn += 1 if d["kind"] == "insert": new_body.append("\t// [РЕСИНК-КОНФЛИКТ №%d] вставка — исходный якорь изменён в новом оригинале." % cn) - new_body.append("#Вставка"); new_body.extend(d["lines"]); new_body.append("#КонецВставки") + new_body.append("#" + kw["Insert"]); new_body.extend(d["lines"]); new_body.append("#" + kw["EndInsert"]) 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 "КонецПроцедуры" + async_prefix = (kw["Async"] + " ") if method["is_async"] else "" + keyword = kw["Func"] if method["is_function"] else kw["Proc"] + end_keyword = kw["EndFunc"] if method["is_function"] else kw["EndProc"] new_block = [] if method["context"]: new_block.append(method["context"]) - new_block.append('&ИзменениеИКонтроль("%s")' % method["canonical"]) + new_block.append('&%s("%s")' % (kw["Control"], 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) @@ -1284,7 +1390,7 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder, 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) + write_conflict_folder(conflict_folder, method_id, ext_bsl, existing_name, method, v1, marked_body, v2, v1norm, v2norm, disputed, lang) if disputed: status = "ЧАСТИЧНО" elif transferred == 0 and absorbed > 0: diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 index 610660964..5881117b4 100644 --- a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 @@ -1,4 +1,4 @@ -# cfe-validate v1.16 — Validate 1C configuration extension structure (CFE) +# cfe-validate v1.17 — Validate 1C configuration extension structure (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -1299,12 +1299,48 @@ if ($versionRank -ge 219 -and $childObjNode) { } } +# 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. +function Get-BslKeywords { + return @{ + ru = @{ + Async="Асинх"; Proc="Процедура"; EndProc="КонецПроцедуры" + Func="Функция"; EndFunc="КонецФункции"; Val="Знач" + Region="Область"; EndRegion="КонецОбласти" + If="Если"; Then="Тогда"; ElsIf="ИначеЕсли"; Else="Иначе"; EndIf="КонецЕсли" + And="И"; Not="НЕ" + Insert="Вставка"; EndInsert="КонецВставки"; Delete="Удаление"; EndDelete="КонецУдаления" + Before="Перед"; After="После"; Around="Вместо"; Control="ИзменениеИКонтроль" + Proceed="ПродолжитьВызов"; Return="Возврат" + # Not a keyword: name of the local the generated Instead-stub declares. Lives here so + # that the language of emitted text is decided in exactly one place. + ResultVar="Результат" + Directives=@("НаКлиенте", "НаСервере", "НаСервереБезКонтекста", "НаКлиентеНаСервереБезКонтекста", "НаКлиентеНаСервере") + } + en = @{ + Async="Async"; Proc="Procedure"; EndProc="EndProcedure" + Func="Function"; EndFunc="EndFunction"; Val="Val" + Region="Region"; EndRegion="EndRegion" + If="If"; Then="Then"; ElsIf="ElsIf"; Else="Else"; EndIf="EndIf" + And="And"; Not="Not" + Insert="Insert"; EndInsert="EndInsert"; Delete="Delete"; EndDelete="EndDelete" + Before="Before"; After="After"; Around="Around"; Control="ChangeAndValidate" + Proceed="ProceedWithCall"; Return="Return" + ResultVar="Result" + Directives=@("AtClient", "AtServer", "AtServerNoContext", "AtClientAtServerNoContext", "AtClientAtServer") + } + } +} + # --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here --- $extRootDir = Split-Path $resolvedPath -Parent +$ctrlKw = Get-BslKeywords +$ctrlRe = '(?m)^\s*&(?:' + $ctrlKw.ru.Control + '|' + $ctrlKw.en.Control + ')\(' $ctrlCount = 0 foreach ($bslFile in (Get-ChildItem -Path $extRootDir -Recurse -Filter *.bsl -File -ErrorAction SilentlyContinue)) { $txt = [System.IO.File]::ReadAllText($bslFile.FullName, [System.Text.Encoding]::UTF8) - $ctrlCount += ([regex]::Matches($txt, '(?m)^\s*&ИзменениеИКонтроль\(')).Count + $ctrlCount += ([regex]::Matches($txt, $ctrlRe)).Count } if ($ctrlCount -gt 0) { Out-Line "[INFO] Контролируемых методов (&ИзменениеИКонтроль): $ctrlCount — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath -ConfigPath " diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.py b/.claude/skills/cfe-validate/scripts/cfe-validate.py index 46e0a9415..5317486e3 100644 --- a/.claude/skills/cfe-validate/scripts/cfe-validate.py +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cfe-validate v1.16 — Validate 1C configuration extension XML structure (CFE) +# cfe-validate v1.17 — 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 @@ -27,6 +27,40 @@ def ci_parse_args(parser, argv=None): return parser.parse_args(argv) +# 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. +def bsl_keywords(): + return { + "ru": { + "Async": "Асинх", "Proc": "Процедура", "EndProc": "КонецПроцедуры", + "Func": "Функция", "EndFunc": "КонецФункции", "Val": "Знач", + "Region": "Область", "EndRegion": "КонецОбласти", + "If": "Если", "Then": "Тогда", "ElsIf": "ИначеЕсли", "Else": "Иначе", "EndIf": "КонецЕсли", + "And": "И", "Not": "НЕ", + "Insert": "Вставка", "EndInsert": "КонецВставки", "Delete": "Удаление", "EndDelete": "КонецУдаления", + "Before": "Перед", "After": "После", "Around": "Вместо", "Control": "ИзменениеИКонтроль", + "Proceed": "ПродолжитьВызов", "Return": "Возврат", + # Not a keyword: name of the local the generated Instead-stub declares. Lives here so + # that the language of emitted text is decided in exactly one place. + "ResultVar": "Результат", + "Directives": ["НаКлиенте", "НаСервере", "НаСервереБезКонтекста", "НаКлиентеНаСервереБезКонтекста", "НаКлиентеНаСервере"], + }, + "en": { + "Async": "Async", "Proc": "Procedure", "EndProc": "EndProcedure", + "Func": "Function", "EndFunc": "EndFunction", "Val": "Val", + "Region": "Region", "EndRegion": "EndRegion", + "If": "If", "Then": "Then", "ElsIf": "ElsIf", "Else": "Else", "EndIf": "EndIf", + "And": "And", "Not": "Not", + "Insert": "Insert", "EndInsert": "EndInsert", "Delete": "Delete", "EndDelete": "EndDelete", + "Before": "Before", "After": "After", "Around": "Around", "Control": "ChangeAndValidate", + "Proceed": "ProceedWithCall", "Return": "Return", + "ResultVar": "Result", + "Directives": ["AtClient", "AtServer", "AtServerNoContext", "AtClientAtServerNoContext", "AtClientAtServer"], + }, + } + + NS = { 'md': 'http://v8.1c.ru/8.3/MDClasses', 'v8': 'http://v8.1c.ru/8.1/data/core', @@ -1267,6 +1301,8 @@ def main(): r.warn(f'16. {issue}') # --- 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')\(') ctrl_count = 0 for dp, _dn, files in os.walk(config_dir): for fn in files: @@ -1274,7 +1310,7 @@ def main(): try: with open(os.path.join(dp, fn), 'r', encoding='utf-8-sig') as f: for ln in f: - if re.match(r'^\s*&ИзменениеИКонтроль\(', ln): + if ctrl_re.match(ln): ctrl_count += 1 except OSError: pass diff --git a/tests/skills/check-inline-drift.mjs b/tests/skills/check-inline-drift.mjs index fd3ab4621..eaadd3323 100644 --- a/tests/skills/check-inline-drift.mjs +++ b/tests/skills/check-inline-drift.mjs @@ -149,6 +149,30 @@ const FAMILIES = [ ], }, + // ─── Встроенный язык 1С в двух написаниях ──────────────────────────────── + // Платформа принимает и русские, и английские ключевые слова, поэтому разбор модуля обязан + // читать оба написания. Таблица пар — данные, но живёт функцией: гард видит только функции, + // а разъехавшаяся пара слов даёт ровно тот тихий ложно-чистый отчёт, из-за которого всё это + // и делалось (ишью #97). + { + name: 'BSL: таблица ключевых слов', py: 'bsl_keywords', ps1: 'Get-BslKeywords', + variants: [ + { id: 'base', authority: 'cfe-patch-method', consumers: ['cfe-diff', 'cfe-validate'] }, + ], + }, + { + name: 'BSL: вид маркера правки', py: 'bsl_marker_kind', ps1: 'Get-BslMarkerKind', + variants: [ + { id: 'base', authority: 'cfe-patch-method', consumers: ['cfe-diff'] }, + ], + }, + { + name: 'BSL: разбор аннотаций перехвата', py: 'get_interceptors', ps1: 'Get-Interceptors', + variants: [ + { id: 'base', authority: 'cfe-patch-method', consumers: ['cfe-diff'] }, + ], + }, + // ─── Запись файла в каноне выгрузки ────────────────────────────────────── { name: 'write_xml_file', py: 'write_xml_file', ps1: 'Write-XmlFile',