mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-20 17:25:52 +03:00
Auto-build: cline (powershell) from 77c797e
This commit is contained in:
@@ -0,0 +1,544 @@
|
||||
# cfe-diff v1.6 — Analyze and compare 1C configuration extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory, Position=0)]
|
||||
[string]$ExtensionPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ConfigPath,
|
||||
|
||||
[ValidateSet("A","B")]
|
||||
[string]$Mode = "A"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve paths ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||
}
|
||||
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
|
||||
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath
|
||||
}
|
||||
if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent }
|
||||
if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent }
|
||||
|
||||
$extCfg = Join-Path $ExtensionPath "Configuration.xml"
|
||||
$srcCfg = Join-Path $ConfigPath "Configuration.xml"
|
||||
if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 }
|
||||
if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 }
|
||||
|
||||
# --- Type -> directory mapping ---
|
||||
$childTypeDirMap = @{
|
||||
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
|
||||
"CommonModule"="CommonModules"; "CommonPicture"="CommonPictures"
|
||||
"CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates"
|
||||
"ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors"
|
||||
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
||||
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||
"Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants"
|
||||
"FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes"
|
||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"
|
||||
"CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals"
|
||||
"SessionParameter"="SessionParameters"; "StyleItem"="StyleItems"
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
||||
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
||||
"Sequence"="Sequences"; "ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
|
||||
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "XDTOPackage"="XDTOPackages"
|
||||
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||
"Bot"="Bots"
|
||||
"PaletteColor"="PaletteColors"
|
||||
}
|
||||
|
||||
# --- Parse extension Configuration.xml ---
|
||||
$extDoc = New-Object System.Xml.XmlDocument
|
||||
$extDoc.PreserveWhitespace = $false
|
||||
$extDoc.Load($extCfg)
|
||||
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable)
|
||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||
|
||||
$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns)
|
||||
$extNameNode = $extProps.SelectSingleNode("md:Name", $ns)
|
||||
$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" }
|
||||
$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns)
|
||||
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" }
|
||||
$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns)
|
||||
$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" }
|
||||
|
||||
Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ==="
|
||||
Write-Host " NamePrefix: $namePrefix"
|
||||
Write-Host ""
|
||||
|
||||
# --- Collect ChildObjects ---
|
||||
$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
|
||||
if (-not $childObjNode) {
|
||||
Write-Host "[WARN] No ChildObjects in extension"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$objects = @()
|
||||
foreach ($child in $childObjNode.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -eq "Language") { continue }
|
||||
$objects += @{ Type = $child.LocalName; Name = $child.InnerText }
|
||||
}
|
||||
|
||||
if ($objects.Count -eq 0) {
|
||||
Write-Host "No objects (besides Language) in extension."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Helper: check if object is borrowed ---
|
||||
function Get-ObjectInfo {
|
||||
param([string]$objType, [string]$objName)
|
||||
|
||||
if (-not $childTypeDirMap.ContainsKey($objType)) { return $null }
|
||||
$dirName = $childTypeDirMap[$objType]
|
||||
$objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml"
|
||||
|
||||
if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } }
|
||||
|
||||
$doc = New-Object System.Xml.XmlDocument
|
||||
$doc.PreserveWhitespace = $false
|
||||
$doc.Load($objFile)
|
||||
|
||||
$objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
|
||||
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$objEl = $null
|
||||
foreach ($c in $doc.DocumentElement.ChildNodes) {
|
||||
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
|
||||
}
|
||||
if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } }
|
||||
|
||||
$propsEl = $objEl.SelectSingleNode("md:Properties", $objNs)
|
||||
$obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null }
|
||||
|
||||
$info = @{
|
||||
Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted")
|
||||
File = $objFile
|
||||
Exists = $true
|
||||
Type = $objType
|
||||
Name = $objName
|
||||
DirName = $dirName
|
||||
ObjElement = $objEl
|
||||
ObjNs = $objNs
|
||||
}
|
||||
return $info
|
||||
}
|
||||
|
||||
# --- Helper: find .bsl files for object ---
|
||||
function Get-BslFiles {
|
||||
param([string]$objType, [string]$objName)
|
||||
|
||||
if (-not $childTypeDirMap.ContainsKey($objType)) { return @() }
|
||||
$dirName = $childTypeDirMap[$objType]
|
||||
$objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName
|
||||
|
||||
if (-not (Test-Path $objDir -PathType Container)) { return @() }
|
||||
|
||||
$bslFiles = @()
|
||||
$extDir = Join-Path $objDir "Ext"
|
||||
if (Test-Path $extDir) {
|
||||
$items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue
|
||||
foreach ($item in $items) { $bslFiles += $item.FullName }
|
||||
}
|
||||
|
||||
# Forms
|
||||
$formsDir = Join-Path $objDir "Forms"
|
||||
if (Test-Path $formsDir) {
|
||||
$formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue
|
||||
foreach ($fm in $formModules) { $bslFiles += $fm.FullName }
|
||||
}
|
||||
|
||||
return $bslFiles
|
||||
}
|
||||
|
||||
# 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="КонецЕсли"
|
||||
# Platform spells the negation "Не"; we emit "НЕ" as this skill always has —
|
||||
# the language is case-insensitive, and the lower case would churn every snapshot.
|
||||
And="И"; Or="Или"; 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"; Or="Or"; 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)
|
||||
$result = @()
|
||||
foreach ($ic in (Get-Interceptors $lines)) {
|
||||
$result += @{ Type = $ic.Type; Raw = $ic.Raw; Method = $ic.Method; Line = $ic.Line + 1; File = $bslPath }
|
||||
}
|
||||
return $result
|
||||
}
|
||||
|
||||
# --- Helper: extract #Вставка blocks from .bsl ---
|
||||
function Get-InsertionBlocks {
|
||||
param([string]$bslPath)
|
||||
|
||||
if (-not (Test-Path $bslPath)) { return @() }
|
||||
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
|
||||
$blocks = @()
|
||||
$inBlock = $false
|
||||
$blockLines = @()
|
||||
$startLine = 0
|
||||
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
$line = $lines[$i]
|
||||
$kind = Get-BslMarkerKind $line
|
||||
if ($kind -eq "Insert") {
|
||||
$inBlock = $true
|
||||
$blockLines = @()
|
||||
$startLine = $i + 1
|
||||
} elseif ($kind -eq "EndInsert" -and $inBlock) {
|
||||
$inBlock = $false
|
||||
$blocks += @{
|
||||
StartLine = $startLine
|
||||
EndLine = $i + 1
|
||||
Code = ($blockLines -join "`n").Trim()
|
||||
File = $bslPath
|
||||
}
|
||||
} elseif ($inBlock) {
|
||||
$blockLines += $lines[$i]
|
||||
}
|
||||
}
|
||||
return $blocks
|
||||
}
|
||||
|
||||
# --- Helper: analyze form for callType events and commands ---
|
||||
function Get-FormInterceptors {
|
||||
param([string]$formXmlPath)
|
||||
|
||||
if (-not (Test-Path $formXmlPath)) { return $null }
|
||||
|
||||
$formDoc = New-Object System.Xml.XmlDocument
|
||||
$formDoc.PreserveWhitespace = $false
|
||||
try { $formDoc.Load($formXmlPath) } catch { return $null }
|
||||
|
||||
$fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable)
|
||||
$fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
|
||||
|
||||
$fRoot = $formDoc.DocumentElement
|
||||
$baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs)
|
||||
$isBorrowed = ($baseForm -ne $null)
|
||||
|
||||
$interceptors = @()
|
||||
|
||||
# Form-level events with callType
|
||||
$eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs)
|
||||
if ($eventsNode) {
|
||||
foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) {
|
||||
$ct = $evt.GetAttribute("callType")
|
||||
if ($ct) {
|
||||
$interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Element-level events with callType (scan all elements recursively)
|
||||
$childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs)
|
||||
if ($childItems) {
|
||||
foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) {
|
||||
$elName = $evtNode.GetAttribute("name")
|
||||
foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) {
|
||||
$ct = $evt.GetAttribute("callType")
|
||||
$interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Commands with callType on Action
|
||||
foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) {
|
||||
$cmdName = $cmd.GetAttribute("name")
|
||||
foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) {
|
||||
$ct = $action.GetAttribute("callType")
|
||||
$interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)"
|
||||
}
|
||||
}
|
||||
|
||||
return @{
|
||||
IsBorrowed = $isBorrowed
|
||||
Interceptors = $interceptors
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE A: Extension overview
|
||||
# ============================================================
|
||||
if ($Mode -eq "A") {
|
||||
$borrowedList = @()
|
||||
$ownList = @()
|
||||
|
||||
foreach ($obj in $objects) {
|
||||
$info = Get-ObjectInfo $obj.Type $obj.Name
|
||||
if (-not $info) {
|
||||
Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type"
|
||||
continue
|
||||
}
|
||||
if (-not $info.Exists) {
|
||||
Write-Host " [?] $($obj.Type).$($obj.Name) — file not found"
|
||||
continue
|
||||
}
|
||||
|
||||
if ($info.Borrowed) {
|
||||
$borrowedList += $obj
|
||||
|
||||
Write-Host " [BORROWED] $($obj.Type).$($obj.Name)"
|
||||
|
||||
# Find .bsl files and interceptors
|
||||
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
||||
foreach ($bsl in $bslFiles) {
|
||||
$relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
||||
$interceptors = Get-FileInterceptors $bsl
|
||||
if ($interceptors.Count -gt 0) {
|
||||
foreach ($ic in $interceptors) {
|
||||
Write-Host " &$($ic.Raw)(`"$($ic.Method)`") — line $($ic.Line) in $relPath"
|
||||
}
|
||||
} else {
|
||||
Write-Host " $relPath (no interceptors)"
|
||||
}
|
||||
}
|
||||
|
||||
# Check for own attributes/forms in ChildObjects
|
||||
if ($info.ObjElement) {
|
||||
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
|
||||
if ($childObj) {
|
||||
$ownAttrs = 0
|
||||
$ownForms = 0
|
||||
$ownTS = 0
|
||||
$borrowedItems = 0
|
||||
$formNames = @()
|
||||
foreach ($c in $childObj.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
$cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs)
|
||||
if ($cProps) {
|
||||
$cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs)
|
||||
if ($cOb -and $cOb.InnerText -eq "Adopted") {
|
||||
$borrowedItems++
|
||||
continue
|
||||
}
|
||||
}
|
||||
switch ($c.LocalName) {
|
||||
"Attribute" { $ownAttrs++ }
|
||||
"TabularSection" { $ownTS++ }
|
||||
"Form" { $formNames += $c.InnerText; $ownForms++ }
|
||||
}
|
||||
}
|
||||
$parts = @()
|
||||
if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" }
|
||||
if ($ownTS -gt 0) { $parts += "$ownTS own TS" }
|
||||
if ($ownForms -gt 0) { $parts += "$ownForms own forms" }
|
||||
if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" }
|
||||
if ($parts.Count -gt 0) {
|
||||
Write-Host " ChildObjects: $($parts -join ', ')"
|
||||
}
|
||||
|
||||
# Analyze forms
|
||||
$borrowedFormCount = 0
|
||||
$ownFormCount = 0
|
||||
foreach ($fn in $formNames) {
|
||||
$formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml"
|
||||
$fi = Get-FormInterceptors $formXmlPath
|
||||
if (-not $fi) {
|
||||
Write-Host " Form.$fn (?)"
|
||||
continue
|
||||
}
|
||||
$formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ }
|
||||
if ($fi.Interceptors.Count -gt 0) {
|
||||
Write-Host " Form.$fn ($formTag):"
|
||||
foreach ($ic in $fi.Interceptors) {
|
||||
Write-Host " $ic"
|
||||
}
|
||||
} else {
|
||||
Write-Host " Form.$fn ($formTag)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$ownList += $obj
|
||||
Write-Host " [OWN] $($obj.Type).$($obj.Name)"
|
||||
|
||||
# Brief info for own objects
|
||||
if ($info.ObjElement) {
|
||||
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
|
||||
if ($childObj) {
|
||||
$attrs = 0; $forms = 0; $ts = 0
|
||||
foreach ($c in $childObj.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
switch ($c.LocalName) {
|
||||
"Attribute" { $attrs++ }
|
||||
"TabularSection" { $ts++ }
|
||||
"Form" { $forms++ }
|
||||
}
|
||||
}
|
||||
$parts = @()
|
||||
if ($attrs -gt 0) { $parts += "$attrs attrs" }
|
||||
if ($ts -gt 0) { $parts += "$ts TS" }
|
||||
if ($forms -gt 0) { $parts += "$forms forms" }
|
||||
if ($parts.Count -gt 0) {
|
||||
Write-Host " $($parts -join ', ')"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ==="
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE B: Transfer check
|
||||
# ============================================================
|
||||
if ($Mode -eq "B") {
|
||||
$transferred = 0
|
||||
$notTransferred = 0
|
||||
$needsReview = 0
|
||||
|
||||
foreach ($obj in $objects) {
|
||||
$info = Get-ObjectInfo $obj.Type $obj.Name
|
||||
if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue }
|
||||
|
||||
# Find .bsl files with &ИзменениеИКонтроль
|
||||
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
||||
foreach ($bsl in $bslFiles) {
|
||||
$interceptors = Get-FileInterceptors $bsl
|
||||
$macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" })
|
||||
|
||||
if ($macInterceptors.Count -eq 0) { continue }
|
||||
|
||||
foreach ($ic in $macInterceptors) {
|
||||
$methodName = $ic.Method
|
||||
$relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
||||
|
||||
# Find #Вставка blocks in this file
|
||||
$insertBlocks = Get-InsertionBlocks $bsl
|
||||
|
||||
if ($insertBlocks.Count -eq 0) {
|
||||
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks"
|
||||
$needsReview++
|
||||
continue
|
||||
}
|
||||
|
||||
# Find corresponding module in config
|
||||
if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue }
|
||||
$dirName = $childTypeDirMap[$obj.Type]
|
||||
$configBsl = $bsl.Replace($ExtensionPath, $ConfigPath)
|
||||
|
||||
if (-not (Test-Path $configBsl)) {
|
||||
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found"
|
||||
$needsReview++
|
||||
continue
|
||||
}
|
||||
|
||||
$configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8)
|
||||
|
||||
$allTransferred = $true
|
||||
foreach ($block in $insertBlocks) {
|
||||
$code = $block.Code
|
||||
if (-not $code) { continue }
|
||||
|
||||
# Normalize whitespace for comparison
|
||||
$codeNorm = $code -replace '\s+', ' '
|
||||
$configNorm = $configContent -replace '\s+', ' '
|
||||
|
||||
if ($configNorm.Contains($codeNorm)) {
|
||||
# Found in config
|
||||
} else {
|
||||
$allTransferred = $false
|
||||
}
|
||||
}
|
||||
|
||||
if ($allTransferred) {
|
||||
Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)"
|
||||
$transferred++
|
||||
} else {
|
||||
Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config"
|
||||
$notTransferred++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ==="
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-diff v1.6 — Analyze and compare 1C configuration extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from lxml import etree
|
||||
|
||||
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||
# регистр не различают, в argparse совпадение точное.
|
||||
def ci_parse_args(parser, argv=None):
|
||||
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||
argv = list(sys.argv[1:] if argv is None else argv)
|
||||
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||
for i, tok in enumerate(argv):
|
||||
if tok.startswith('-') and tok.lower() in names:
|
||||
argv[i] = names[tok.lower()]
|
||||
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||
choice_map = {}
|
||||
for a in parser._actions:
|
||||
if a.choices:
|
||||
for s in a.option_strings:
|
||||
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||
for i in range(len(argv) - 1):
|
||||
m = choice_map.get(argv[i])
|
||||
if m and argv[i + 1].lower() in m:
|
||||
argv[i + 1] = m[argv[i + 1].lower()]
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
# --- Namespace maps ---
|
||||
|
||||
MD_NSMAP = {
|
||||
"md": "http://v8.1c.ru/8.3/MDClasses",
|
||||
"xr": "http://v8.1c.ru/8.3/xcf/readable",
|
||||
}
|
||||
|
||||
FORM_NSMAP = {
|
||||
"f": "http://v8.1c.ru/8.3/xcf/logform",
|
||||
}
|
||||
|
||||
# --- Type -> directory mapping ---
|
||||
|
||||
CHILD_TYPE_DIR_MAP = {
|
||||
"Catalog": "Catalogs",
|
||||
"Document": "Documents",
|
||||
"Enum": "Enums",
|
||||
"CommonModule": "CommonModules",
|
||||
"CommonPicture": "CommonPictures",
|
||||
"CommonCommand": "CommonCommands",
|
||||
"CommonTemplate": "CommonTemplates",
|
||||
"ExchangePlan": "ExchangePlans",
|
||||
"Report": "Reports",
|
||||
"DataProcessor": "DataProcessors",
|
||||
"InformationRegister": "InformationRegisters",
|
||||
"AccumulationRegister": "AccumulationRegisters",
|
||||
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
|
||||
"ChartOfAccounts": "ChartsOfAccounts",
|
||||
"AccountingRegister": "AccountingRegisters",
|
||||
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
|
||||
"CalculationRegister": "CalculationRegisters",
|
||||
"BusinessProcess": "BusinessProcesses",
|
||||
"Task": "Tasks",
|
||||
"Subsystem": "Subsystems",
|
||||
"Role": "Roles",
|
||||
"Constant": "Constants",
|
||||
"FunctionalOption": "FunctionalOptions",
|
||||
"DefinedType": "DefinedTypes",
|
||||
"FunctionalOptionsParameter": "FunctionalOptionsParameters",
|
||||
"CommonForm": "CommonForms",
|
||||
"DocumentJournal": "DocumentJournals",
|
||||
"SessionParameter": "SessionParameters",
|
||||
"StyleItem": "StyleItems",
|
||||
"EventSubscription": "EventSubscriptions",
|
||||
"ScheduledJob": "ScheduledJobs",
|
||||
"SettingsStorage": "SettingsStorages",
|
||||
"FilterCriterion": "FilterCriteria",
|
||||
"CommandGroup": "CommandGroups",
|
||||
"DocumentNumerator": "DocumentNumerators",
|
||||
"Sequence": "Sequences",
|
||||
"ExternalDataSource": "ExternalDataSources",
|
||||
"IntegrationService": "IntegrationServices",
|
||||
"CommonAttribute": "CommonAttributes",
|
||||
"Style": "Styles",
|
||||
"XDTOPackage": "XDTOPackages",
|
||||
"WebService": "WebServices",
|
||||
"HTTPService": "HTTPServices",
|
||||
"WSReference": "WSReferences",
|
||||
"Bot": "Bots",
|
||||
"PaletteColor": "PaletteColors",
|
||||
}
|
||||
|
||||
|
||||
# --- Helper: check if object is borrowed ---
|
||||
|
||||
def get_object_info(obj_type, obj_name, extension_path):
|
||||
if obj_type not in CHILD_TYPE_DIR_MAP:
|
||||
return None
|
||||
dir_name = CHILD_TYPE_DIR_MAP[obj_type]
|
||||
obj_file = os.path.join(extension_path, dir_name, f"{obj_name}.xml")
|
||||
|
||||
if not os.path.isfile(obj_file):
|
||||
return {"Borrowed": False, "File": obj_file, "Exists": False}
|
||||
|
||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||
doc = etree.parse(obj_file, parser_xml)
|
||||
doc_root = doc.getroot()
|
||||
|
||||
# Find first element child
|
||||
obj_el = None
|
||||
for c in doc_root:
|
||||
if isinstance(c.tag, str):
|
||||
obj_el = c
|
||||
break
|
||||
|
||||
if obj_el is None:
|
||||
return {"Borrowed": False, "File": obj_file, "Exists": True}
|
||||
|
||||
props_el = obj_el.find("md:Properties", MD_NSMAP)
|
||||
ob_node = None
|
||||
if props_el is not None:
|
||||
ob_node = props_el.find("md:ObjectBelonging", MD_NSMAP)
|
||||
|
||||
borrowed = ob_node is not None and ob_node.text == "Adopted"
|
||||
|
||||
return {
|
||||
"Borrowed": borrowed,
|
||||
"File": obj_file,
|
||||
"Exists": True,
|
||||
"Type": obj_type,
|
||||
"Name": obj_name,
|
||||
"DirName": dir_name,
|
||||
"ObjElement": obj_el,
|
||||
}
|
||||
|
||||
|
||||
# --- Helper: find .bsl files for object ---
|
||||
|
||||
def get_bsl_files(obj_type, obj_name, extension_path):
|
||||
if obj_type not in CHILD_TYPE_DIR_MAP:
|
||||
return []
|
||||
dir_name = CHILD_TYPE_DIR_MAP[obj_type]
|
||||
obj_dir = os.path.join(extension_path, dir_name, obj_name)
|
||||
|
||||
if not os.path.isdir(obj_dir):
|
||||
return []
|
||||
|
||||
bsl_files = []
|
||||
ext_dir = os.path.join(obj_dir, "Ext")
|
||||
if os.path.isdir(ext_dir):
|
||||
for item in os.listdir(ext_dir):
|
||||
if item.lower().endswith(".bsl"):
|
||||
bsl_files.append(os.path.join(ext_dir, item))
|
||||
|
||||
# Forms
|
||||
forms_dir = os.path.join(obj_dir, "Forms")
|
||||
if os.path.isdir(forms_dir):
|
||||
for dirpath, dirnames, filenames in os.walk(forms_dir):
|
||||
for fn in filenames:
|
||||
if fn == "Module.bsl":
|
||||
bsl_files.append(os.path.join(dirpath, fn))
|
||||
|
||||
return bsl_files
|
||||
|
||||
|
||||
# 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": "КонецЕсли",
|
||||
# Platform spells the negation "Не"; we emit "НЕ" as this skill always has —
|
||||
# the language is case-insensitive, and the lower case would churn every snapshot.
|
||||
"And": "И", "Or": "Или", "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", "Or": "Or", "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()
|
||||
|
||||
|
||||
# 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()
|
||||
|
||||
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 insert-marker blocks from .bsl ---
|
||||
|
||||
def get_insertion_blocks(bsl_path):
|
||||
if not os.path.isfile(bsl_path):
|
||||
return []
|
||||
|
||||
with open(bsl_path, "r", encoding="utf-8-sig") as fh:
|
||||
lines = fh.readlines()
|
||||
|
||||
blocks = []
|
||||
in_block = False
|
||||
block_lines = []
|
||||
start_line = 0
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
kind = bsl_marker_kind(line)
|
||||
if kind == "Insert":
|
||||
in_block = True
|
||||
block_lines = []
|
||||
start_line = i + 1
|
||||
elif kind == "EndInsert" and in_block:
|
||||
in_block = False
|
||||
blocks.append({
|
||||
"StartLine": start_line,
|
||||
"EndLine": i + 1,
|
||||
"Code": "\n".join(block_lines).strip(),
|
||||
"File": bsl_path,
|
||||
})
|
||||
elif in_block:
|
||||
block_lines.append(line.rstrip("\n").rstrip("\r"))
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
# --- Helper: analyze form for callType events and commands ---
|
||||
|
||||
def get_form_interceptors(form_xml_path):
|
||||
if not os.path.isfile(form_xml_path):
|
||||
return None
|
||||
|
||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||
try:
|
||||
doc = etree.parse(form_xml_path, parser_xml)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
f_root = doc.getroot()
|
||||
base_form = f_root.find("f:BaseForm", FORM_NSMAP)
|
||||
is_borrowed = base_form is not None
|
||||
|
||||
interceptors = []
|
||||
|
||||
# Form-level events with callType
|
||||
events_node = f_root.find("f:Events", FORM_NSMAP)
|
||||
if events_node is not None:
|
||||
for evt in events_node.findall("f:Event", FORM_NSMAP):
|
||||
ct = evt.get("callType", "")
|
||||
if ct:
|
||||
evt_name = evt.get("name", "")
|
||||
evt_text = evt.text or ""
|
||||
interceptors.append(f"Event:{evt_name} [{ct}] -> {evt_text}")
|
||||
|
||||
# Element-level events with callType (scan all elements recursively)
|
||||
child_items = f_root.find("f:ChildItems", FORM_NSMAP)
|
||||
if child_items is not None:
|
||||
# Walk all descendant elements looking for Events/Event[@callType]
|
||||
f_ns = FORM_NSMAP["f"]
|
||||
for el in child_items.iter():
|
||||
if not isinstance(el.tag, str):
|
||||
continue
|
||||
el_name = el.get("name", "")
|
||||
if not el_name:
|
||||
continue
|
||||
events_sub = el.find(f"{{{f_ns}}}Events")
|
||||
if events_sub is None:
|
||||
continue
|
||||
for evt in events_sub.findall(f"{{{f_ns}}}Event"):
|
||||
ct = evt.get("callType", "")
|
||||
if ct:
|
||||
evt_name = evt.get("name", "")
|
||||
evt_text = evt.text or ""
|
||||
interceptors.append(f"Element:{el_name}.{evt_name} [{ct}] -> {evt_text}")
|
||||
|
||||
# Commands with callType on Action
|
||||
f_ns = FORM_NSMAP["f"]
|
||||
cmds_node = f_root.find(f"{{{f_ns}}}Commands")
|
||||
if cmds_node is not None:
|
||||
for cmd in cmds_node.findall(f"{{{f_ns}}}Command"):
|
||||
cmd_name = cmd.get("name", "")
|
||||
for action in cmd.findall(f"{{{f_ns}}}Action"):
|
||||
ct = action.get("callType", "")
|
||||
if ct:
|
||||
action_text = action.text or ""
|
||||
interceptors.append(f"Command:{cmd_name} [{ct}] -> {action_text}")
|
||||
|
||||
return {
|
||||
"IsBorrowed": is_borrowed,
|
||||
"Interceptors": interceptors,
|
||||
}
|
||||
|
||||
|
||||
# --- Mode A: Extension overview ---
|
||||
|
||||
def mode_a(objects, extension_path):
|
||||
borrowed_list = []
|
||||
own_list = []
|
||||
|
||||
for obj in objects:
|
||||
info = get_object_info(obj["Type"], obj["Name"], extension_path)
|
||||
if info is None:
|
||||
print(f" [?] {obj['Type']}.{obj['Name']} \u2014 unknown type")
|
||||
continue
|
||||
if not info["Exists"]:
|
||||
print(f" [?] {obj['Type']}.{obj['Name']} \u2014 file not found")
|
||||
continue
|
||||
|
||||
if info["Borrowed"]:
|
||||
borrowed_list.append(obj)
|
||||
|
||||
print(f" [BORROWED] {obj['Type']}.{obj['Name']}")
|
||||
|
||||
# Find .bsl files and interceptors
|
||||
bsl_files = get_bsl_files(obj["Type"], obj["Name"], extension_path)
|
||||
for bsl in bsl_files:
|
||||
rel_path = bsl.replace(extension_path, "").lstrip("\\/")
|
||||
interceptor_list = get_file_interceptors(bsl)
|
||||
if len(interceptor_list) > 0:
|
||||
for ic in interceptor_list:
|
||||
print(f' &{ic["Raw"]}("{ic["Method"]}") \u2014 line {ic["Line"]} in {rel_path}')
|
||||
else:
|
||||
print(f" {rel_path} (no interceptors)")
|
||||
|
||||
# Check for own attributes/forms in ChildObjects
|
||||
obj_el = info.get("ObjElement")
|
||||
if obj_el is not None:
|
||||
child_obj = obj_el.find("md:ChildObjects", MD_NSMAP)
|
||||
if child_obj is not None:
|
||||
own_attrs = 0
|
||||
own_forms = 0
|
||||
own_ts = 0
|
||||
borrowed_items = 0
|
||||
form_names = []
|
||||
for c in child_obj:
|
||||
if not isinstance(c.tag, str):
|
||||
continue
|
||||
ln = etree.QName(c.tag).localname
|
||||
c_props = c.find("md:Properties", MD_NSMAP)
|
||||
if c_props is not None:
|
||||
c_ob = c_props.find("md:ObjectBelonging", MD_NSMAP)
|
||||
if c_ob is not None and c_ob.text == "Adopted":
|
||||
borrowed_items += 1
|
||||
continue
|
||||
if ln == "Attribute":
|
||||
own_attrs += 1
|
||||
elif ln == "TabularSection":
|
||||
own_ts += 1
|
||||
elif ln == "Form":
|
||||
form_names.append(c.text or "")
|
||||
own_forms += 1
|
||||
|
||||
parts = []
|
||||
if own_attrs > 0:
|
||||
parts.append(f"{own_attrs} own attrs")
|
||||
if own_ts > 0:
|
||||
parts.append(f"{own_ts} own TS")
|
||||
if own_forms > 0:
|
||||
parts.append(f"{own_forms} own forms")
|
||||
if borrowed_items > 0:
|
||||
parts.append(f"{borrowed_items} borrowed items")
|
||||
if len(parts) > 0:
|
||||
print(f" ChildObjects: {', '.join(parts)}")
|
||||
|
||||
# Analyze forms
|
||||
for fn in form_names:
|
||||
form_xml_path = os.path.join(
|
||||
extension_path, info["DirName"], info["Name"],
|
||||
"Forms", fn, "Ext", "Form.xml"
|
||||
)
|
||||
fi = get_form_interceptors(form_xml_path)
|
||||
if fi is None:
|
||||
print(f" Form.{fn} (?)")
|
||||
continue
|
||||
form_tag = "borrowed" if fi["IsBorrowed"] else "own"
|
||||
if len(fi["Interceptors"]) > 0:
|
||||
print(f" Form.{fn} ({form_tag}):")
|
||||
for ic in fi["Interceptors"]:
|
||||
print(f" {ic}")
|
||||
else:
|
||||
print(f" Form.{fn} ({form_tag})")
|
||||
else:
|
||||
own_list.append(obj)
|
||||
print(f" [OWN] {obj['Type']}.{obj['Name']}")
|
||||
|
||||
# Brief info for own objects
|
||||
obj_el = info.get("ObjElement")
|
||||
if obj_el is not None:
|
||||
child_obj = obj_el.find("md:ChildObjects", MD_NSMAP)
|
||||
if child_obj is not None:
|
||||
attrs = 0
|
||||
forms = 0
|
||||
ts = 0
|
||||
for c in child_obj:
|
||||
if not isinstance(c.tag, str):
|
||||
continue
|
||||
ln = etree.QName(c.tag).localname
|
||||
if ln == "Attribute":
|
||||
attrs += 1
|
||||
elif ln == "TabularSection":
|
||||
ts += 1
|
||||
elif ln == "Form":
|
||||
forms += 1
|
||||
parts = []
|
||||
if attrs > 0:
|
||||
parts.append(f"{attrs} attrs")
|
||||
if ts > 0:
|
||||
parts.append(f"{ts} TS")
|
||||
if forms > 0:
|
||||
parts.append(f"{forms} forms")
|
||||
if len(parts) > 0:
|
||||
print(f" {', '.join(parts)}")
|
||||
|
||||
print("")
|
||||
print(f"=== Summary: {len(borrowed_list)} borrowed, {len(own_list)} own objects ===")
|
||||
|
||||
|
||||
# --- Mode B: Transfer check ---
|
||||
|
||||
def mode_b(objects, extension_path, config_path):
|
||||
transferred = 0
|
||||
not_transferred = 0
|
||||
needs_review = 0
|
||||
|
||||
for obj in objects:
|
||||
info = get_object_info(obj["Type"], obj["Name"], extension_path)
|
||||
if info is None or not info["Exists"] or not info["Borrowed"]:
|
||||
continue
|
||||
|
||||
# Find .bsl files with &ИзменениеИКонтроль
|
||||
bsl_files = get_bsl_files(obj["Type"], obj["Name"], extension_path)
|
||||
for bsl in bsl_files:
|
||||
interceptor_list = get_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:
|
||||
continue
|
||||
|
||||
for ic in mac_interceptors:
|
||||
method_name = ic["Method"]
|
||||
rel_bsl = bsl.replace(extension_path, "").lstrip("\\/")
|
||||
|
||||
# Find #Вставка blocks in this file
|
||||
insert_blocks = get_insertion_blocks(bsl)
|
||||
|
||||
if len(insert_blocks) == 0:
|
||||
print(f' [NEEDS_REVIEW] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 no #\u0412\u0441\u0442\u0430\u0432\u043a\u0430 blocks')
|
||||
needs_review += 1
|
||||
continue
|
||||
|
||||
# Find corresponding module in config
|
||||
if obj["Type"] not in CHILD_TYPE_DIR_MAP:
|
||||
continue
|
||||
config_bsl = bsl.replace(extension_path, config_path)
|
||||
|
||||
if not os.path.isfile(config_bsl):
|
||||
print(f' [NEEDS_REVIEW] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 config module not found')
|
||||
needs_review += 1
|
||||
continue
|
||||
|
||||
with open(config_bsl, "r", encoding="utf-8-sig") as fh:
|
||||
config_content = fh.read()
|
||||
|
||||
all_transferred = True
|
||||
for block in insert_blocks:
|
||||
code = block["Code"]
|
||||
if not code:
|
||||
continue
|
||||
|
||||
# Normalize whitespace for comparison
|
||||
code_norm = re.sub(r'\s+', ' ', code)
|
||||
config_norm = re.sub(r'\s+', ' ', config_content)
|
||||
|
||||
if code_norm not in config_norm:
|
||||
all_transferred = False
|
||||
|
||||
if all_transferred:
|
||||
print(f' [TRANSFERRED] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 {len(insert_blocks)} block(s)')
|
||||
transferred += 1
|
||||
else:
|
||||
print(f' [NOT_TRANSFERRED] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 some blocks not found in config')
|
||||
not_transferred += 1
|
||||
|
||||
print("")
|
||||
print(f"=== Transfer check: {transferred} transferred, {not_transferred} not transferred, {needs_review} needs review ===")
|
||||
|
||||
|
||||
# --- Main ---
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(description="Analyze and compare 1C configuration extension (CFE)", allow_abbrev=False)
|
||||
parser.add_argument("-ExtensionPath", required=True, help="Path to extension dump root")
|
||||
parser.add_argument("-ConfigPath", required=True, help="Path to base config dump root")
|
||||
parser.add_argument("-Mode", choices=["A", "B"], default="A", help="A=overview, B=transfer check")
|
||||
args = ci_parse_args(parser)
|
||||
|
||||
extension_path = args.ExtensionPath
|
||||
config_path = args.ConfigPath
|
||||
mode = args.Mode
|
||||
|
||||
# --- Resolve paths ---
|
||||
if not os.path.isabs(extension_path):
|
||||
extension_path = os.path.join(os.getcwd(), extension_path)
|
||||
if not os.path.isabs(config_path):
|
||||
config_path = os.path.join(os.getcwd(), config_path)
|
||||
if os.path.isfile(extension_path):
|
||||
extension_path = os.path.dirname(extension_path)
|
||||
if os.path.isfile(config_path):
|
||||
config_path = os.path.dirname(config_path)
|
||||
|
||||
ext_cfg = os.path.join(extension_path, "Configuration.xml")
|
||||
src_cfg = os.path.join(config_path, "Configuration.xml")
|
||||
if not os.path.isfile(ext_cfg):
|
||||
print(f"Extension Configuration.xml not found: {ext_cfg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not os.path.isfile(src_cfg):
|
||||
print(f"Config Configuration.xml not found: {src_cfg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Parse extension Configuration.xml ---
|
||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||
ext_doc = etree.parse(ext_cfg, parser_xml)
|
||||
ext_root = ext_doc.getroot()
|
||||
|
||||
ext_props = ext_root.find(".//md:Configuration/md:Properties", MD_NSMAP)
|
||||
ext_name_node = ext_props.find("md:Name", MD_NSMAP) if ext_props is not None else None
|
||||
ext_name = ext_name_node.text if ext_name_node is not None and ext_name_node.text else "?"
|
||||
prefix_node = ext_props.find("md:NamePrefix", MD_NSMAP) if ext_props is not None else None
|
||||
name_prefix = prefix_node.text if prefix_node is not None and prefix_node.text else ""
|
||||
purpose_node = ext_props.find("md:ConfigurationExtensionPurpose", MD_NSMAP) if ext_props is not None else None
|
||||
purpose = purpose_node.text if purpose_node is not None and purpose_node.text else "?"
|
||||
|
||||
print(f"=== cfe-diff Mode {mode}: {ext_name} ({purpose}) ===")
|
||||
print(f" NamePrefix: {name_prefix}")
|
||||
print("")
|
||||
|
||||
# --- Collect ChildObjects ---
|
||||
child_obj_node = ext_root.find(".//md:Configuration/md:ChildObjects", MD_NSMAP)
|
||||
if child_obj_node is None:
|
||||
print("[WARN] No ChildObjects in extension")
|
||||
sys.exit(0)
|
||||
|
||||
objects = []
|
||||
for child in child_obj_node:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
ln = etree.QName(child.tag).localname
|
||||
if ln == "Language":
|
||||
continue
|
||||
objects.append({"Type": ln, "Name": child.text or ""})
|
||||
|
||||
if len(objects) == 0:
|
||||
print("No objects (besides Language) in extension.")
|
||||
sys.exit(0)
|
||||
|
||||
# --- Run selected mode ---
|
||||
if mode == "A":
|
||||
mode_a(objects, extension_path)
|
||||
elif mode == "B":
|
||||
mode_b(objects, extension_path, config_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user