mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-22 20:51:03 +03:00
feat(cfe-patch-method): батч -Check/-Actualize контролируемых методов (v2.2)
После обновления КФ контролируемые методы (&ИзменениеИКонтроль) молча уезжают в рассинхрон — платформа при загрузке не ругается, ошибка лишь в рантайме. Добавлены два явных режима по всему расширению (или -ModulePath/-MethodName для сужения): - -Check — отчёт: какие методы дрейфнули (ДРЕЙФ/КОНФЛИКТ/МЕТОД-ИСЧЕЗ), актуальные числом; ничего не пишет; exit 1 при наличии дрейфа. - -Actualize — чинит пачкой: авто-перенос + merge-воркспейс на конфликтах. Одиночный ресинк вынесен в общую функцию resync_one (report_only), одиночный путь и батч используют её. Зона ответственности узкая — только тело &ИзменениеИКонтроль. Merge-воркспейс переработан: тонкий index.md (список конфликтов + пути к .bsl расширения) + подпапка на метод (conflict.md с блоком/диффом + base/local/remote), без общей портянки. cfe-validate: крошка-указатель [INFO] при наличии контролируемых методов -> /cfe-patch-method -Check. Тесты: +check-clean/check-drift/actualize-batch. 16 кейсов, оба рантайма зелёные, байтовый паритет ps1<->py. verify-snapshots (реальная 1С) Windows ps+py — 16/16. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8399f13e5a
commit
9aaa3a1c1e
@@ -1,4 +1,4 @@
|
||||
# cfe-patch-method v2.1 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# cfe-patch-method v2.2 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -6,15 +6,17 @@ param(
|
||||
|
||||
[string]$ConfigPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
# Generation: logical name or path to .bsl. Optional in -Check/-Actualize (scope narrowing).
|
||||
[string]$ModulePath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$MethodName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateSet("Before","After","Instead","ModificationAndControl")]
|
||||
[string]$InterceptorType
|
||||
[ValidateSet("", "Before", "After", "Instead", "ModificationAndControl")]
|
||||
[string]$InterceptorType,
|
||||
|
||||
# Batch modes over &ИзменениеИКонтроль of the extension (no generation):
|
||||
[switch]$Check, # report drift only, no writes
|
||||
[switch]$Actualize # actualize drifted controlled methods
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -475,6 +477,199 @@ function Get-Truncated {
|
||||
return $t
|
||||
}
|
||||
|
||||
# Reverse of typeDirMap (plural dir -> singular type) for logical id display
|
||||
$script:dirToType = @{
|
||||
"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"
|
||||
}
|
||||
|
||||
# Rel-path segments (relative to ExtensionPath) -> logical ModulePath (Type.Name.Module / CommonModule.Name)
|
||||
function Get-ModulePathFromRel {
|
||||
param($relParts)
|
||||
$dir0 = $relParts[0]; $name = $relParts[1]
|
||||
$typ = if ($script:dirToType.ContainsKey($dir0)) { $script:dirToType[$dir0] } else { $dir0 }
|
||||
if ($dir0 -eq "CommonModules") { return "CommonModule.$name" }
|
||||
if ($relParts.Count -ge 7 -and $relParts[2] -eq "Forms") { return "$typ.$name.Form.$($relParts[3])" }
|
||||
$mod = $relParts[$relParts.Count - 1]
|
||||
if ($mod.EndsWith(".bsl")) { $mod = $mod.Substring(0, $mod.Length - 4) }
|
||||
return "$typ.$name.$mod"
|
||||
}
|
||||
|
||||
function Get-RelPartsUnder {
|
||||
param([string]$root, [string]$fullPath)
|
||||
$r = [IO.Path]::GetFullPath($root).TrimEnd('\', '/')
|
||||
$f = [IO.Path]::GetFullPath($fullPath)
|
||||
$rel = $f.Substring($r.Length).TrimStart('\', '/')
|
||||
return (($rel -replace '\\', '/').Split('/') | Where-Object { $_ -ne '' })
|
||||
}
|
||||
|
||||
function Get-ResyncConflictReason {
|
||||
param($disputed)
|
||||
$kinds = @($disputed | ForEach-Object { $_.Kind } | Select-Object -Unique)
|
||||
$parts = @()
|
||||
if ($kinds -contains 'insert') { $parts += 'якорь вставки изменён' }
|
||||
if ($kinds -contains 'delete') { $parts += 'удаляемое исчезло' }
|
||||
return ($parts -join '; ')
|
||||
}
|
||||
|
||||
# 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)
|
||||
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)
|
||||
[IO.File]::WriteAllText((Join-Path $folder 'remote.bsl'), (($v2 -join "`r`n") + "`r`n"), $enc)
|
||||
$md = @()
|
||||
$md += "# $methodId"
|
||||
$md += "Править: $extBsl"
|
||||
$md += "Метод: $existingName (&ИзменениеИКонтроль(`"$($method.Canonical)`"))"
|
||||
$md += "Причина: $(Get-ResyncConflictReason $disputed)"
|
||||
$md += ""
|
||||
$md += "## Не размещено — перенести в метод вручную"
|
||||
foreach ($d in $disputed) {
|
||||
if ($d.Kind -eq 'insert') { $md += '#Вставка'; foreach ($l in $d.Lines) { $md += $l }; $md += '#КонецВставки' }
|
||||
else { $md += '// не удалось найти для удаления:'; foreach ($l in $d.Lines) { $md += ('// ' + $l.Trim()) } }
|
||||
}
|
||||
$md += ""
|
||||
$md += "## Дифф base→remote (что изменилось в оригинале)"
|
||||
foreach ($l in $v1) { if ($v2norm -notcontains (Get-Normalized $l)) { $md += "- $l" } }
|
||||
foreach ($l in $v2) { if ($v1norm -notcontains (Get-Normalized $l)) { $md += "+ $l" } }
|
||||
$md += ""
|
||||
$md += "Рядом: base.bsl / local.bsl / remote.bsl"
|
||||
[IO.File]::WriteAllText((Join-Path $folder 'conflict.md'), (($md -join "`r`n") + "`r`n"), $enc)
|
||||
}
|
||||
|
||||
# Resync one &ИзменениеИКонтроль interceptor. Returns status hashtable.
|
||||
# ReportOnly: classify without writing. Otherwise: apply to module (+ conflict folder on disputes).
|
||||
function Invoke-Resync {
|
||||
param($extBsl, $extLines, $dup, $method, [string]$logicalModule, [string]$conflictFolder, [switch]$ReportOnly, $enc)
|
||||
|
||||
$methodId = "$logicalModule.$($method.Canonical)"
|
||||
$decLine = $dup.Line
|
||||
$sigLineIdx = $decLine + 1
|
||||
if ($sigLineIdx -ge $extLines.Count -or $extLines[$sigLineIdx] -notmatch '^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(') {
|
||||
return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не разобрать сигнатуру перехватчика' }
|
||||
}
|
||||
$existingName = $Matches[1]
|
||||
$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' }
|
||||
$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 = 'не найден конец перехватчика' } }
|
||||
|
||||
$markedBody = @()
|
||||
for ($j = $sigEnd + 1; $j -lt $blockEnd; $j++) { $markedBody += $extLines[$j] }
|
||||
|
||||
$parsed = Parse-MarkedBody $markedBody
|
||||
$v1 = $parsed.V1
|
||||
$ops = $parsed.Ops
|
||||
$v2 = $method.BodyLines
|
||||
$v1norm = @($v1 | ForEach-Object { Get-Normalized $_ })
|
||||
$v2norm = @($v2 | ForEach-Object { Get-Normalized $_ })
|
||||
|
||||
if (($v1norm -join "`n") -eq ($v2norm -join "`n")) {
|
||||
return @{ Id = $methodId; Status = 'АКТУАЛЕН'; ExtBsl = $extBsl }
|
||||
}
|
||||
|
||||
$insertTop = @(); $insertAfter = @{}; $delStart = @{}; $delEnd = @{}; $disputed = @(); $transferred = @()
|
||||
foreach ($op in $ops) {
|
||||
if ($op.Kind -eq 'insert') {
|
||||
$beforeLines = @()
|
||||
if ($op.After -ge 0) { $bs = [Math]::Max(0, $op.After - 2); for ($m = $bs; $m -le $op.After; $m++) { $beforeLines += $v1norm[$m] } }
|
||||
$afterLines = @()
|
||||
$ae = [Math]::Min($v1norm.Count - 1, $op.After + 3)
|
||||
for ($m = $op.After + 1; $m -le $ae; $m++) { $afterLines += $v1norm[$m] }
|
||||
$k = Resolve-InsertionPoint $v2norm $beforeLines $afterLines
|
||||
if ($null -eq $k) { $disputed += @{ Kind = 'insert'; Lines = $op.Lines } }
|
||||
elseif ($k -lt 0) { $insertTop += ,$op.Lines; $transferred += @{ Kind = 'insert' } }
|
||||
else { if (-not $insertAfter.ContainsKey($k)) { $insertAfter[$k] = @() }; $insertAfter[$k] += ,$op.Lines; $transferred += @{ Kind = 'insert' } }
|
||||
} else {
|
||||
$keys = @(); for ($m = $op.Start; $m -le $op.End; $m++) { $keys += $v1norm[$m] }
|
||||
$p = Find-UniqueRun $v2norm $keys
|
||||
if ($p -ge 0) { $delStart[$p] = $true; $delEnd[$p + $keys.Count - 1] = $true; $transferred += @{ Kind = 'delete' } }
|
||||
else { $disputed += @{ Kind = 'delete'; Lines = $op.Lines } }
|
||||
}
|
||||
}
|
||||
|
||||
if ($ReportOnly) {
|
||||
$st = if ($disputed.Count -gt 0) { 'КОНФЛИКТ' } else { 'ДРЕЙФ' }
|
||||
return @{ Id = $methodId; Status = $st; ExtBsl = $extBsl; Transferred = $transferred.Count; Disputed = $disputed.Count; Reason = (Get-ResyncConflictReason $disputed) }
|
||||
}
|
||||
|
||||
# assemble new marked body
|
||||
$newBody = @()
|
||||
foreach ($blk in $insertTop) { $newBody += "#Вставка"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#КонецВставки" }
|
||||
for ($k = 0; $k -lt $v2.Count; $k++) {
|
||||
if ($delStart.ContainsKey($k)) { $newBody += "#Удаление" }
|
||||
$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 ($disputed.Count -gt 0) {
|
||||
$newBody += "`t// [РЕСИНК-КОНФЛИКТ] перенесите блоки ниже вручную — исходный якорь изменился в новой версии оригинала."
|
||||
$newBody += "`t// Материалы: см. index.md / conflict.md в merge-воркспейсе (путь в выводе)."
|
||||
foreach ($d in $disputed) {
|
||||
if ($d.Kind -eq 'insert') { $newBody += "#Вставка"; foreach ($l in $d.Lines) { $newBody += $l }; $newBody += "#КонецВставки" }
|
||||
else { $newBody += "`t// [РЕСИНК-КОНФЛИКТ] не удалось найти для удаления:"; foreach ($l in $d.Lines) { $newBody += ("`t// " + $l.Trim()) } }
|
||||
}
|
||||
}
|
||||
$asyncPrefix = if ($method.IsAsync) { "Асинх " } else { "" }
|
||||
$keyword = if ($method.IsFunction) { "Функция" } else { "Процедура" }
|
||||
$endKeyword = if ($method.IsFunction) { "КонецФункции" } else { "КонецПроцедуры" }
|
||||
$newBlock = @()
|
||||
if ($method.Context) { $newBlock += $method.Context }
|
||||
$newBlock += "&ИзменениеИКонтроль(`"$($method.Canonical)`")"
|
||||
$newBlock += "$asyncPrefix$keyword $existingName($($method.ParamsText))"
|
||||
$newBlock += $newBody
|
||||
$newBlock += $endKeyword
|
||||
|
||||
$blockStart = $decLine
|
||||
if ($decLine -ge 1 -and (Test-ContextDirective $extLines[$decLine - 1].Trim())) { $blockStart = $decLine - 1 }
|
||||
$out = @()
|
||||
for ($j = 0; $j -lt $blockStart; $j++) { $out += $extLines[$j] }
|
||||
foreach ($l in $newBlock) { $out += $l }
|
||||
for ($j = $blockEnd + 1; $j -lt $extLines.Count; $j++) { $out += $extLines[$j] }
|
||||
[IO.File]::WriteAllText($extBsl, (($out -join "`r`n") + "`r`n"), $enc)
|
||||
|
||||
$conflictDir = $null
|
||||
if ($disputed.Count -gt 0) {
|
||||
$conflictDir = $conflictFolder
|
||||
Write-ConflictFolder $conflictFolder $methodId $extBsl $existingName $method $v1 $markedBody $v2 $v1norm $v2norm $disputed $enc
|
||||
}
|
||||
$status = if ($disputed.Count -gt 0) { 'ЧАСТИЧНО' } else { 'АКТУАЛИЗИРОВАН' }
|
||||
return @{ Id = $methodId; Status = $status; ExtBsl = $extBsl; Transferred = $transferred.Count; Disputed = $disputed.Count; ConflictDir = $conflictDir; Reason = (Get-ResyncConflictReason $disputed) }
|
||||
}
|
||||
|
||||
# Write run-root index.md (only if there are conflicts). Returns run-root or $null.
|
||||
function Write-ResyncIndex {
|
||||
param($runRoot, $results, [string]$extName, [string]$configPath, [string]$verb, $enc)
|
||||
$conflicts = @($results | Where-Object { $_.Status -eq 'ЧАСТИЧНО' })
|
||||
if ($conflicts.Count -eq 0) { return $null }
|
||||
if (-not (Test-Path $runRoot)) { New-Item -ItemType Directory -Path $runRoot -Force | Out-Null }
|
||||
$total = $results.Count
|
||||
$actual = @($results | Where-Object { $_.Status -eq 'АКТУАЛЕН' }).Count
|
||||
$upd = @($results | Where-Object { $_.Status -eq 'АКТУАЛИЗИРОВАН' }).Count
|
||||
$lines = @()
|
||||
$lines += "[$verb] $extName -> $configPath"
|
||||
$lines += "Итог: $actual/$total актуальны · $upd актуализировано · $($conflicts.Count) конфликтов"
|
||||
$lines += ""
|
||||
$lines += "Конфликты — править .bsl расширения:"
|
||||
foreach ($c in $conflicts) {
|
||||
$lines += " ЧАСТИЧНО $($c.Id)"
|
||||
$lines += " -> $($c.ExtBsl) ($($c.Reason))"
|
||||
}
|
||||
[IO.File]::WriteAllText((Join-Path $runRoot 'index.md'), (($lines -join "`r`n") + "`r`n"), $enc)
|
||||
return $runRoot
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
@@ -494,6 +689,93 @@ $cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$propsNode = $cfgDoc.SelectSingleNode("//md:Configuration/md:Properties", $cfgNs)
|
||||
$prefixNode = if ($propsNode) { $propsNode.SelectSingleNode("md:NamePrefix", $cfgNs) } else { $null }
|
||||
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "Расш_" }
|
||||
$extNameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $cfgNs) } else { $null }
|
||||
$extName = if ($extNameNode -and $extNameNode.InnerText) { $extNameNode.InnerText } else { "Расширение" }
|
||||
|
||||
# ============================================================================
|
||||
# Batch modes: -Check / -Actualize over &ИзменениеИКонтроль of the extension
|
||||
# ============================================================================
|
||||
if ($Check -or $Actualize) {
|
||||
if ($Check -and $Actualize) { Write-Error "Укажите либо -Check, либо -Actualize, не оба."; exit 1 }
|
||||
if ([string]::IsNullOrEmpty($ConfigPath)) { Write-Error "Для -Check/-Actualize нужен -ConfigPath (сверка с исходником)."; exit 1 }
|
||||
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { $ConfigPath = Join-Path (Get-Location).Path $ConfigPath }
|
||||
if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent }
|
||||
if (-not (Test-Path (Join-Path $ConfigPath "Configuration.xml"))) { Write-Error "Configuration.xml не найден в конфигурации-источнике: $ConfigPath"; exit 1 }
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
$reportOnly = [bool]$Check
|
||||
$verb = if ($Check) { "КОНТРОЛЬ" } else { "АКТУАЛИЗАЦИЯ" }
|
||||
|
||||
# target .bsl set: -ModulePath → один модуль; иначе — все .bsl расширения
|
||||
$targetBsls = @()
|
||||
if (-not [string]::IsNullOrEmpty($ModulePath)) {
|
||||
$relParts = Get-ModuleRelPath $ModulePath
|
||||
$mb = $ExtensionPath; foreach ($p in $relParts) { $mb = Join-Path $mb $p }
|
||||
if (Test-Path $mb) { $targetBsls += $mb }
|
||||
} else {
|
||||
$targetBsls = @(Get-ChildItem -Path $ExtensionPath -Recurse -Filter *.bsl -File | ForEach-Object { $_.FullName })
|
||||
}
|
||||
|
||||
$runRoot = Join-Path ([IO.Path]::GetTempPath()) (Join-Path "cfe-resync" ($extName -replace '[\\/:*?"<>|]', '_'))
|
||||
if ($Actualize -and (Test-Path $runRoot)) { Remove-Item $runRoot -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
$results = @()
|
||||
foreach ($tb in $targetBsls) {
|
||||
$scan = @([IO.File]::ReadAllLines($tb, [Text.Encoding]::UTF8))
|
||||
$mnames = @(Get-Interceptors $scan | Where-Object { $_.Type -eq 'ИзменениеИКонтроль' } | ForEach-Object { $_.Method })
|
||||
if (-not [string]::IsNullOrEmpty($MethodName)) { $mnames = @($mnames | Where-Object { $_ -ieq $MethodName }) }
|
||||
$mnames = @($mnames | Select-Object -Unique)
|
||||
if ($mnames.Count -eq 0) { continue }
|
||||
$rel = Get-RelPartsUnder $ExtensionPath $tb
|
||||
$logicalModule = Get-ModulePathFromRel $rel
|
||||
$srcBsl = $ConfigPath; foreach ($p in $rel) { $srcBsl = Join-Path $srcBsl $p }
|
||||
$relJoined = ($rel -join '\') -replace '\.bsl$', ''
|
||||
foreach ($mname in $mnames) {
|
||||
$mid = "$logicalModule.$mname"
|
||||
if (-not (Test-Path $srcBsl)) { $results += @{ Id = $mid; Status = 'ИСТОЧНИК-НЕ-НАЙДЕН'; ExtBsl = $tb }; continue }
|
||||
$srcL = [IO.File]::ReadAllLines($srcBsl, [Text.Encoding]::UTF8)
|
||||
$m = Extract-Method $srcL $mname
|
||||
if (-not $m) { $results += @{ Id = $mid; Status = 'МЕТОД-ИСЧЕЗ'; ExtBsl = $tb }; continue }
|
||||
# fresh read per method (writes shift line numbers)
|
||||
$tbLines = @([IO.File]::ReadAllLines($tb, [Text.Encoding]::UTF8))
|
||||
$ic = @(Get-Interceptors $tbLines | Where-Object { $_.Type -eq 'ИзменениеИКонтроль' -and $_.Method -ieq $mname })[0]
|
||||
if (-not $ic) { continue }
|
||||
$folder = Join-Path $runRoot (Join-Path $relJoined $m.Canonical)
|
||||
$results += (Invoke-Resync $tb $tbLines $ic $m $logicalModule $folder -ReportOnly:$reportOnly $enc)
|
||||
}
|
||||
}
|
||||
|
||||
# --- report ---
|
||||
$total = $results.Count
|
||||
$actual = @($results | Where-Object { $_.Status -eq 'АКТУАЛЕН' }).Count
|
||||
Write-Host "[$verb] $extName -> $ConfigPath (на контроле: $total)"
|
||||
$listed = @($results | Where-Object { $_.Status -ne 'АКТУАЛЕН' })
|
||||
foreach ($p in $listed) {
|
||||
$line = " {0,-16} {1}" -f $p.Status, $p.Id
|
||||
if ($p.Reason) { $line += " $($p.Reason)" }
|
||||
Write-Host $line
|
||||
}
|
||||
if ($Check) {
|
||||
$drift = @($results | Where-Object { $_.Status -eq 'ДРЕЙФ' }).Count
|
||||
$confl = @($results | Where-Object { $_.Status -eq 'КОНФЛИКТ' }).Count
|
||||
$gone = @($results | Where-Object { $_.Status -in @('МЕТОД-ИСЧЕЗ', 'ИСТОЧНИК-НЕ-НАЙДЕН') }).Count
|
||||
Write-Host "Итог: $actual/$total актуальны · $drift дрейф · $confl конфликт · $gone требуют внимания"
|
||||
if ($listed.Count -gt 0) { Write-Host "Починить: /cfe-patch-method -Actualize -ExtensionPath $ExtensionPath -ConfigPath $ConfigPath"; exit 1 } else { exit 0 }
|
||||
} else {
|
||||
$upd = @($results | Where-Object { $_.Status -eq 'АКТУАЛИЗИРОВАН' }).Count
|
||||
$part = @($results | Where-Object { $_.Status -eq 'ЧАСТИЧНО' }).Count
|
||||
Write-Host "Итог: $actual/$total актуальны · $upd актуализировано · $part частично"
|
||||
$idx = Write-ResyncIndex $runRoot $results $extName $ConfigPath $verb $enc
|
||||
if ($idx) { Write-Host "Merge-воркспейс конфликтов (см. index.md): $idx" }
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
|
||||
# --- Generation mode: require ModulePath + MethodName + InterceptorType ---
|
||||
if ([string]::IsNullOrEmpty($ModulePath) -or [string]::IsNullOrEmpty($MethodName) -or [string]::IsNullOrEmpty($InterceptorType)) {
|
||||
Write-Error "Нужны -ModulePath, -MethodName, -InterceptorType (генерация перехватчика). Для проверки/актуализации контролируемых методов используйте -Check или -Actualize."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Resolve module file paths (ModulePath = logical name OR path to a .bsl) ---
|
||||
$hasConfigPath = -not [string]::IsNullOrEmpty($ConfigPath)
|
||||
@@ -568,171 +850,23 @@ if ($dup) {
|
||||
Write-Host " Файл: $extBsl"
|
||||
exit 0
|
||||
}
|
||||
# ---- RESYNC (&ИзменениеИКонтроль) ----
|
||||
# Locate existing block: decorator line -> signature -> body -> Конец*
|
||||
$decLine = $dup.Line
|
||||
# Interceptor procedure name from existing signature (line after decorator, skipping possible async)
|
||||
$sigLineIdx = $decLine + 1
|
||||
if ($sigLineIdx -ge $extLines.Count -or $extLines[$sigLineIdx] -notmatch '^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(') {
|
||||
Write-Error "Не удалось найти сигнатуру существующего перехватчика &ИзменениеИКонтроль(`"$MethodName`")"; exit 1
|
||||
}
|
||||
$existingName = $Matches[1]
|
||||
$sig = Read-Signature $extLines $sigLineIdx
|
||||
if (-not $sig) { Write-Error "Не удалось разобрать сигнатуру существующего перехватчика"; exit 1 }
|
||||
$sigEnd = $sig.EndLineIdx
|
||||
$isFunc = ($extLines[$sigLineIdx] -imatch '^\s*(?:Асинх\s+)?Функция\b')
|
||||
$endRe = if ($isFunc) { '^\s*КонецФункции\b' } else { '^\s*КонецПроцедуры\b' }
|
||||
$blockEnd = -1
|
||||
for ($j = $sigEnd + 1; $j -lt $extLines.Count; $j++) { if ($extLines[$j] -imatch $endRe) { $blockEnd = $j; break } }
|
||||
if ($blockEnd -lt 0) { Write-Error "Не найден конец существующего перехватчика"; exit 1 }
|
||||
|
||||
# marked body
|
||||
$markedBody = @()
|
||||
for ($j = $sigEnd + 1; $j -lt $blockEnd; $j++) { $markedBody += $extLines[$j] }
|
||||
|
||||
# reconstruct v1, ops
|
||||
$parsed = Parse-MarkedBody $markedBody
|
||||
$v1 = $parsed.V1
|
||||
$ops = $parsed.Ops
|
||||
$v2 = $method.BodyLines
|
||||
|
||||
$v1norm = @($v1 | ForEach-Object { Get-Normalized $_ })
|
||||
$v2norm = @($v2 | ForEach-Object { Get-Normalized $_ })
|
||||
|
||||
# no drift?
|
||||
if (($v1norm -join "`n") -eq ($v2norm -join "`n")) {
|
||||
Write-Host "[АКТУАЛЕН] &ИзменениеИКонтроль(`"$MethodName`") — оригинал не менялся, изменений нет."
|
||||
Write-Host " Файл: $extBsl"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# transfer ops onto v2
|
||||
$insertTop = @()
|
||||
$insertAfter = @{} # v2 index -> list of blocks (each = @{Lines=..})
|
||||
$delStart = @{} # v2 index -> $true
|
||||
$delEnd = @{}
|
||||
$disputed = @()
|
||||
$transferred = @() # for the summary: @{ Kind; Anchor }
|
||||
|
||||
foreach ($op in $ops) {
|
||||
if ($op.Kind -eq 'insert') {
|
||||
# two-sided context anchor
|
||||
$beforeLines = @()
|
||||
if ($op.After -ge 0) {
|
||||
$bs = [Math]::Max(0, $op.After - 2)
|
||||
for ($m = $bs; $m -le $op.After; $m++) { $beforeLines += $v1norm[$m] }
|
||||
}
|
||||
$afterLines = @()
|
||||
$ae = [Math]::Min($v1norm.Count - 1, $op.After + 3)
|
||||
for ($m = $op.After + 1; $m -le $ae; $m++) { $afterLines += $v1norm[$m] }
|
||||
|
||||
$k = Resolve-InsertionPoint $v2norm $beforeLines $afterLines
|
||||
if ($null -eq $k) {
|
||||
$disputed += @{ Kind = 'insert'; Lines = $op.Lines }
|
||||
} elseif ($k -lt 0) {
|
||||
$insertTop += ,$op.Lines
|
||||
$transferred += @{ Kind = 'insert'; Anchor = '(в начало метода)' }
|
||||
} else {
|
||||
if (-not $insertAfter.ContainsKey($k)) { $insertAfter[$k] = @() }
|
||||
$insertAfter[$k] += ,$op.Lines
|
||||
$transferred += @{ Kind = 'insert'; Anchor = $v2[$k] }
|
||||
}
|
||||
} else {
|
||||
$keys = @()
|
||||
for ($m = $op.Start; $m -le $op.End; $m++) { $keys += $v1norm[$m] }
|
||||
$p = Find-UniqueRun $v2norm $keys
|
||||
if ($p -ge 0) {
|
||||
$delStart[$p] = $true
|
||||
$delEnd[$p + $keys.Count - 1] = $true
|
||||
$transferred += @{ Kind = 'delete'; Anchor = $op.Lines[0] }
|
||||
} else {
|
||||
$disputed += @{ Kind = 'delete'; Lines = $op.Lines }
|
||||
}
|
||||
# ---- RESYNC (&ИзменениеИКонтроль) — via Invoke-Resync ----
|
||||
$rel = Get-RelPartsUnder $ExtensionPath $extBsl
|
||||
$logicalModule = Get-ModulePathFromRel $rel
|
||||
$relJoined = ($rel -join '') -replace '.bsl$', ''
|
||||
$runRoot = Join-Path ([IO.Path]::GetTempPath()) (Join-Path "cfe-resync" ($extName -replace '[\/:*?"<>|]', '_'))
|
||||
$folder = Join-Path $runRoot (Join-Path $relJoined $method.Canonical)
|
||||
$res = Invoke-Resync $extBsl $extLines $dup $method $logicalModule $folder $enc
|
||||
switch ($res.Status) {
|
||||
'АКТУАЛЕН' { Write-Host "[АКТУАЛЕН] &ИзменениеИКонтроль(`"$MethodName`") — оригинал не менялся, изменений нет." }
|
||||
'АКТУАЛИЗИРОВАН' { Write-Host "[АКТУАЛИЗИРОВАН] &ИзменениеИКонтроль(`"$MethodName`") — тело обновлено, перенесено правок: $($res.Transferred)" }
|
||||
'ЧАСТИЧНО' {
|
||||
$idx = Write-ResyncIndex $runRoot @($res) $extName $ConfigPath "АКТУАЛИЗАЦИЯ" $enc
|
||||
Write-Host "[АКТУАЛИЗИРОВАН-ЧАСТИЧНО] &ИзменениеИКонтроль(`"$MethodName`") — перенесено: $($res.Transferred), конфликтов: $($res.Disputed)"
|
||||
Write-Host " Конфликт помечен // [РЕСИНК-КОНФЛИКТ]. Папка метода: $($res.ConflictDir)"
|
||||
if ($idx) { Write-Host " Индекс: $idx" }
|
||||
}
|
||||
}
|
||||
|
||||
# assemble new marked body
|
||||
$newBody = @()
|
||||
foreach ($blk in $insertTop) {
|
||||
$newBody += "#Вставка"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#КонецВставки"
|
||||
}
|
||||
for ($k = 0; $k -lt $v2.Count; $k++) {
|
||||
if ($delStart.ContainsKey($k)) { $newBody += "#Удаление" }
|
||||
$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 += "#КонецВставки"
|
||||
}
|
||||
}
|
||||
}
|
||||
# disputed blocks appended with conflict markers (never lost)
|
||||
if ($disputed.Count -gt 0) {
|
||||
$newBody += "`t// [РЕСИНК-КОНФЛИКТ] перенесите блоки ниже вручную — исходный якорь изменился в новой версии оригинала."
|
||||
$newBody += "`t// Материалы для анализа см. в выводе команды (файлы base/local/remote/merged)."
|
||||
foreach ($d in $disputed) {
|
||||
if ($d.Kind -eq 'insert') {
|
||||
$newBody += "#Вставка"; foreach ($l in $d.Lines) { $newBody += $l }; $newBody += "#КонецВставки"
|
||||
} else {
|
||||
$newBody += "`t// [РЕСИНК-КОНФЛИКТ] не удалось найти для удаления:"
|
||||
foreach ($l in $d.Lines) { $newBody += ("`t// " + $l.Trim()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# rebuild block: keep context (from v2), decorator, signature with existing name and v2 params
|
||||
$asyncPrefix = if ($method.IsAsync) { "Асинх " } else { "" }
|
||||
$keyword = if ($method.IsFunction) { "Функция" } else { "Процедура" }
|
||||
$endKeyword = if ($method.IsFunction) { "КонецФункции" } else { "КонецПроцедуры" }
|
||||
$newBlock = @()
|
||||
if ($method.Context) { $newBlock += $method.Context }
|
||||
$newBlock += "&ИзменениеИКонтроль(`"$($method.Canonical)`")"
|
||||
$newBlock += "$asyncPrefix$keyword $existingName($($method.ParamsText))"
|
||||
$newBlock += $newBody
|
||||
$newBlock += $endKeyword
|
||||
|
||||
# block start includes preceding context directive line if present
|
||||
$blockStart = $decLine
|
||||
if ($decLine -ge 1 -and (Test-ContextDirective $extLines[$decLine - 1].Trim())) { $blockStart = $decLine - 1 }
|
||||
|
||||
$out = @()
|
||||
for ($j = 0; $j -lt $blockStart; $j++) { $out += $extLines[$j] }
|
||||
foreach ($l in $newBlock) { $out += $l }
|
||||
for ($j = $blockEnd + 1; $j -lt $extLines.Count; $j++) { $out += $extLines[$j] }
|
||||
|
||||
[System.IO.File]::WriteAllText($extBsl, (($out -join "`r`n") + "`r`n"), $enc)
|
||||
|
||||
# transferred summary (shared by full-auto and partial)
|
||||
function Write-TransferSummary {
|
||||
param($items)
|
||||
foreach ($t in $items) {
|
||||
if ($t.Kind -eq 'insert') { Write-Host " • вставка после «$(Get-Truncated $t.Anchor)»" }
|
||||
else { Write-Host " • удаление «$(Get-Truncated $t.Anchor)»" }
|
||||
}
|
||||
}
|
||||
|
||||
if ($disputed.Count -gt 0) {
|
||||
# merge workspace (git-mergetool convention: base/local/remote/merged)
|
||||
$tmpRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("cfe-resync\" + ($ModulePath -replace '[\\/:*?"<>|]', '_') + "." + $MethodName)
|
||||
if (-not (Test-Path $tmpRoot)) { New-Item -ItemType Directory -Path $tmpRoot -Force | Out-Null }
|
||||
[System.IO.File]::WriteAllText((Join-Path $tmpRoot "base.bsl"), (($v1 -join "`r`n") + "`r`n"), $enc)
|
||||
[System.IO.File]::WriteAllText((Join-Path $tmpRoot "local.bsl"), (($markedBody -join "`r`n") + "`r`n"), $enc)
|
||||
[System.IO.File]::WriteAllText((Join-Path $tmpRoot "remote.bsl"), (($v2 -join "`r`n") + "`r`n"), $enc)
|
||||
[System.IO.File]::WriteAllText((Join-Path $tmpRoot "merged.bsl"), (($newBlock -join "`r`n") + "`r`n"), $enc)
|
||||
$diff = @()
|
||||
$diff += "--- base -> remote (что изменилось в оригинале) ---"
|
||||
foreach ($l in $v1) { if ($v2norm -notcontains (Get-Normalized $l)) { $diff += "- $l" } }
|
||||
foreach ($l in $v2) { if ($v1norm -notcontains (Get-Normalized $l)) { $diff += "+ $l" } }
|
||||
[System.IO.File]::WriteAllText((Join-Path $tmpRoot "diff.txt"), (($diff -join "`r`n") + "`r`n"), $enc)
|
||||
|
||||
Write-Host "[АКТУАЛИЗИРОВАН-ЧАСТИЧНО] &ИзменениеИКонтроль(`"$MethodName`") — перенесено: $($transferred.Count), конфликтов: $($disputed.Count)"
|
||||
Write-TransferSummary $transferred
|
||||
Write-Host " Конфликтные блоки помечены // [РЕСИНК-КОНФЛИКТ] — разместите вручную."
|
||||
Write-Host " Merge-воркспейс (base/local/remote/merged/diff):"
|
||||
Write-Host " $tmpRoot"
|
||||
} else {
|
||||
Write-Host "[АКТУАЛИЗИРОВАН] &ИзменениеИКонтроль(`"$MethodName`") — тело обновлено, перенесено правок: $($transferred.Count)"
|
||||
Write-TransferSummary $transferred
|
||||
default { Write-Host "[$($res.Status)] &ИзменениеИКонтроль(`"$MethodName`") — $($res.Reason)" }
|
||||
}
|
||||
Write-Host " Файл: $extBsl"
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user