Files
cc-1c-skills/.claude/skills/form-validate/scripts/form-validate.ps1
T
Nick Shirokov c665800916 Improve form skills: validation, docs, round-trip consistency
form-compile: warn about unknown DSL keys in element definitions,
document stdCommand/command button keys and EPF-specific notes.

form-validate: check that form-level Title uses multilingual XML,
not plain text (which causes XDTO errors at build time).

form-add: warn about duplicate element names, clarify after-not-found
message when using into+after together.

form-info: show Title in header instead of Properties line, display
commands as DSL-friendly format (-> Name [cmd], -> Close [std])
instead of raw Form.Command/Form.StandardCommand paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 15:44:32 +03:00

501 lines
13 KiB
PowerShell

param(
[Parameter(Mandatory)]
[string]$FormPath,
[int]$MaxErrors = 30
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Load XML ---
if (-not (Test-Path $FormPath)) {
Write-Error "File not found: $FormPath"
exit 1
}
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $false
try {
$xmlDoc.Load((Resolve-Path $FormPath).Path)
} catch {
Write-Host "[ERROR] XML parse error: $($_.Exception.Message)"
Write-Host ""
Write-Host "---"
Write-Host "Errors: 1, Warnings: 0"
exit 1
}
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
$root = $xmlDoc.DocumentElement
# --- Counters ---
$errors = 0
$warnings = 0
$stopped = $false
function Report-OK {
param([string]$msg)
Write-Host "[OK] $msg"
}
function Report-Error {
param([string]$msg)
$script:errors++
Write-Host "[ERROR] $msg"
if ($script:errors -ge $MaxErrors) {
$script:stopped = $true
}
}
function Report-Warn {
param([string]$msg)
$script:warnings++
Write-Host "[WARN] $msg"
}
# --- Form name from path ---
$formName = [System.IO.Path]::GetFileNameWithoutExtension($FormPath)
$parentDir = [System.IO.Path]::GetDirectoryName($FormPath)
if ($parentDir) {
$extDir = [System.IO.Path]::GetFileName($parentDir)
if ($extDir -eq "Ext") {
$formDir = [System.IO.Path]::GetDirectoryName($parentDir)
if ($formDir) { $formName = [System.IO.Path]::GetFileName($formDir) }
}
}
Write-Host "=== Validation: $formName ==="
Write-Host ""
# --- Check 1: Root element and version ---
if ($root.LocalName -ne "Form") {
Report-Error "Root element is '$($root.LocalName)', expected 'Form'"
} else {
$version = $root.GetAttribute("version")
if ($version -eq "2.17") {
Report-OK "Root element: Form version=$version"
} elseif ($version) {
Report-Warn "Form version='$version' (expected 2.17)"
} else {
Report-Warn "Form version attribute missing"
}
}
# --- Check 2: AutoCommandBar ---
if (-not $stopped) {
$acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr)
if ($acb) {
$acbName = $acb.GetAttribute("name")
$acbId = $acb.GetAttribute("id")
if ($acbId -eq "-1") {
Report-OK "AutoCommandBar: name='$acbName', id=$acbId"
} else {
Report-Error "AutoCommandBar id='$acbId', expected '-1'"
}
} else {
Report-Error "AutoCommandBar element missing"
}
}
# --- Collect all elements with IDs ---
$elementIds = @{} # id -> name (element ID pool)
$allElements = @() # @{Name; Tag; Id; ParentName; Node}
function Collect-Elements {
param($node, [string]$parentName)
foreach ($child in $node.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
$name = $child.GetAttribute("name")
$id = $child.GetAttribute("id")
if ($name -and $id) {
$tag = $child.LocalName
$script:allElements += @{
Name = $name
Tag = $tag
Id = $id
ParentName = $parentName
Node = $child
}
# Track element IDs (skip AutoCommandBar which has -1)
if ($id -ne "-1") {
if ($elementIds.ContainsKey($id)) {
Report-Error "Duplicate element id=${id}: '$name' and '$($elementIds[$id])'"
} else {
$elementIds[$id] = $name
}
}
# Recurse into ChildItems
$childItems = $child.SelectSingleNode("f:ChildItems", $nsMgr)
if ($childItems) {
Collect-Elements -node $childItems -parentName $name
}
}
}
}
# Collect from ChildItems
$childItemsRoot = $root.SelectSingleNode("f:ChildItems", $nsMgr)
if ($childItemsRoot) {
Collect-Elements -node $childItemsRoot -parentName "(root)"
}
# Also collect from AutoCommandBar's ChildItems
$acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr)
if ($acb) {
$acbChildren = $acb.SelectSingleNode("f:ChildItems", $nsMgr)
if ($acbChildren) {
Collect-Elements -node $acbChildren -parentName "ФормаКоманднаяПанель"
}
}
# --- Check 3: Unique element IDs ---
if (-not $stopped) {
$dupCount = ($allElements | Group-Object { $_.Id } | Where-Object { $_.Count -gt 1 -and $_.Name -ne "-1" }).Count
if ($dupCount -eq 0) {
Report-OK "Unique element IDs: $($elementIds.Count) elements"
}
}
# --- Collect attributes (separate ID pool) ---
$attrMap = @{} # name -> node
$attrIds = @{} # id -> name
$attrNodes = $root.SelectNodes("f:Attributes/f:Attribute", $nsMgr)
foreach ($attr in $attrNodes) {
$attrName = $attr.GetAttribute("name")
$attrId = $attr.GetAttribute("id")
if ($attrName) {
$attrMap[$attrName] = $attr
}
if ($attrId -and $attrId -ne "") {
if ($attrIds.ContainsKey($attrId)) {
Report-Error "Duplicate attribute id=${attrId}: '$attrName' and '$($attrIds[$attrId])'"
} else {
$attrIds[$attrId] = $attrName
}
}
# Column IDs are a separate sub-pool per attribute — check uniqueness within parent
$colIds = @{}
foreach ($col in $attr.SelectNodes("f:Columns/f:Column", $nsMgr)) {
$colId = $col.GetAttribute("id")
$colName = $col.GetAttribute("name")
if ($colId -and $colId -ne "") {
if ($colIds.ContainsKey($colId)) {
Report-Error "Duplicate column id=${colId} in '$attrName': '$colName' and '$($colIds[$colId])'"
} else {
$colIds[$colId] = $colName
}
}
}
}
if (-not $stopped) {
$attrDupCount = ($attrIds.GetEnumerator() | Group-Object Value | Where-Object { $_.Count -gt 1 }).Count
if ($attrDupCount -eq 0 -and $attrIds.Count -gt 0) {
Report-OK "Unique attribute IDs: $($attrIds.Count) entries"
}
}
# --- Collect commands (separate ID pool) ---
$cmdMap = @{} # name -> node
$cmdIds = @{} # id -> name
$cmdNodes = $root.SelectNodes("f:Commands/f:Command", $nsMgr)
foreach ($cmd in $cmdNodes) {
$cmdName = $cmd.GetAttribute("name")
$cmdId = $cmd.GetAttribute("id")
if ($cmdName) {
$cmdMap[$cmdName] = $cmd
}
if ($cmdId -and $cmdId -ne "") {
if ($cmdIds.ContainsKey($cmdId)) {
Report-Error "Duplicate command id=${cmdId}: '$cmdName' and '$($cmdIds[$cmdId])'"
} else {
$cmdIds[$cmdId] = $cmdName
}
}
}
if (-not $stopped) {
if ($cmdIds.Count -gt 0) {
$cmdDupCount = ($cmdIds.GetEnumerator() | Group-Object Value | Where-Object { $_.Count -gt 1 }).Count
if ($cmdDupCount -eq 0) {
Report-OK "Unique command IDs: $($cmdIds.Count) entries"
}
}
}
# --- Check 4: Companion elements ---
# Define required companions per element type
$companionRules = @{
"InputField" = @("ContextMenu", "ExtendedTooltip")
"CheckBoxField" = @("ContextMenu", "ExtendedTooltip")
"LabelDecoration" = @("ContextMenu", "ExtendedTooltip")
"LabelField" = @("ContextMenu", "ExtendedTooltip")
"PictureDecoration" = @("ContextMenu", "ExtendedTooltip")
"PictureField" = @("ContextMenu", "ExtendedTooltip")
"CalendarField" = @("ContextMenu", "ExtendedTooltip")
"UsualGroup" = @("ExtendedTooltip")
"Pages" = @("ExtendedTooltip")
"Page" = @("ExtendedTooltip")
"Button" = @("ExtendedTooltip")
"Table" = @("ContextMenu", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition")
}
if (-not $stopped) {
$companionErrors = 0
$companionChecked = 0
foreach ($el in $allElements) {
if ($stopped) { break }
$tag = $el.Tag
$elName = $el.Name
$node = $el.Node
if (-not $companionRules.ContainsKey($tag)) { continue }
$required = $companionRules[$tag]
$companionChecked++
foreach ($compTag in $required) {
$compNode = $node.SelectSingleNode("f:$compTag", $nsMgr)
if (-not $compNode) {
Report-Error "[$tag] '$elName': missing companion <$compTag>"
$companionErrors++
}
}
}
if ($companionErrors -eq 0 -and $companionChecked -gt 0) {
Report-OK "Companion elements: $companionChecked elements checked"
}
}
# --- Check 5: DataPath -> Attribute references ---
if (-not $stopped) {
$pathErrors = 0
$pathChecked = 0
foreach ($el in $allElements) {
if ($stopped) { break }
$tag = $el.Tag
$elName = $el.Name
$node = $el.Node
# Skip companion elements
if ($tag -in @("ContextMenu", "ExtendedTooltip", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition")) {
continue
}
$dpNode = $node.SelectSingleNode("f:DataPath", $nsMgr)
if (-not $dpNode) { continue }
$dataPath = $dpNode.InnerText.Trim()
if (-not $dataPath) { continue }
$pathChecked++
# Extract root segment of path, strip array indices like [0]
$cleanPath = $dataPath -replace '\[\d+\]', ''
$segments = $cleanPath -split '\.'
$rootAttr = $segments[0]
if (-not $attrMap.ContainsKey($rootAttr)) {
Report-Error "[$tag] '$elName': DataPath='$dataPath' — attribute '$rootAttr' not found"
$pathErrors++
}
}
if ($pathErrors -eq 0 -and $pathChecked -gt 0) {
Report-OK "DataPath references: $pathChecked paths checked"
} elseif ($pathChecked -eq 0) {
Report-OK "DataPath references: none"
}
}
# --- Check 6: Button command references ---
if (-not $stopped) {
$cmdErrors = 0
$cmdChecked = 0
foreach ($el in $allElements) {
if ($stopped) { break }
$tag = $el.Tag
$elName = $el.Name
$node = $el.Node
if ($tag -ne "Button") { continue }
$cmdNode = $node.SelectSingleNode("f:CommandName", $nsMgr)
if (-not $cmdNode) { continue }
$cmdRef = $cmdNode.InnerText.Trim()
if (-not $cmdRef) { continue }
# Form.Command.XXX -> check command XXX exists
if ($cmdRef -match '^Form\.Command\.(.+)$') {
$cmdName = $Matches[1]
$cmdChecked++
if (-not $cmdMap.ContainsKey($cmdName)) {
Report-Error "[Button] '$elName': CommandName='$cmdRef' — command '$cmdName' not found in Commands"
$cmdErrors++
}
}
# Form.StandardCommand.XXX — skip, standard commands always exist
}
if ($cmdErrors -eq 0 -and $cmdChecked -gt 0) {
Report-OK "Command references: $cmdChecked buttons checked"
} elseif ($cmdChecked -eq 0) {
Report-OK "Command references: none"
}
}
# --- Check 7: Events have handler names ---
if (-not $stopped) {
$eventErrors = 0
$eventChecked = 0
# Form-level events
$formEvents = $root.SelectSingleNode("f:Events", $nsMgr)
if ($formEvents) {
foreach ($evt in $formEvents.SelectNodes("f:Event", $nsMgr)) {
$evtName = $evt.GetAttribute("name")
$handler = $evt.InnerText.Trim()
$eventChecked++
if (-not $handler) {
Report-Error "Form event '$evtName': empty handler name"
$eventErrors++
}
}
}
# Element-level events
foreach ($el in $allElements) {
if ($stopped) { break }
$tag = $el.Tag
$elName = $el.Name
$node = $el.Node
$eventsNode = $node.SelectSingleNode("f:Events", $nsMgr)
if (-not $eventsNode) { continue }
foreach ($evt in $eventsNode.SelectNodes("f:Event", $nsMgr)) {
$evtName = $evt.GetAttribute("name")
$handler = $evt.InnerText.Trim()
$eventChecked++
if (-not $handler) {
Report-Error "[$tag] '$elName' event '$evtName': empty handler name"
$eventErrors++
}
}
}
if ($eventErrors -eq 0 -and $eventChecked -gt 0) {
Report-OK "Event handlers: $eventChecked events checked"
} elseif ($eventChecked -eq 0) {
Report-OK "Event handlers: none"
}
}
# --- Check 8: Command actions ---
if (-not $stopped) {
$actionErrors = 0
$actionChecked = 0
foreach ($cmd in $cmdNodes) {
if ($stopped) { break }
$cmdName = $cmd.GetAttribute("name")
$actionNode = $cmd.SelectSingleNode("f:Action", $nsMgr)
$actionChecked++
if (-not $actionNode -or -not $actionNode.InnerText.Trim()) {
Report-Error "Command '$cmdName': missing or empty Action"
$actionErrors++
}
}
if ($actionErrors -eq 0 -and $actionChecked -gt 0) {
Report-OK "Command actions: $actionChecked commands checked"
} elseif ($actionChecked -eq 0) {
Report-OK "Command actions: none"
}
}
# --- Check 9: MainAttribute count ---
if (-not $stopped) {
$mainCount = 0
foreach ($attr in $attrNodes) {
$mainNode = $attr.SelectSingleNode("f:MainAttribute", $nsMgr)
if ($mainNode -and $mainNode.InnerText -eq "true") {
$mainCount++
}
}
if ($mainCount -le 1) {
$mainInfo = if ($mainCount -eq 1) { "1 main attribute" } else { "no main attribute" }
Report-OK "MainAttribute: $mainInfo"
} else {
Report-Error "Multiple MainAttribute=true ($mainCount found, expected 0 or 1)"
}
}
# --- Check 10: Title must be multilingual XML (not plain text) ---
if (-not $stopped) {
$titleNode = $root.SelectSingleNode("f:Title", $nsMgr)
if ($titleNode) {
$v8items = $titleNode.SelectNodes("v8:item", $nsMgr)
if ($v8items.Count -eq 0 -and $titleNode.InnerText.Trim() -ne "") {
Report-Error "Form Title is plain text ('$($titleNode.InnerText.Trim())') — must be multilingual XML (<v8:item>). Use top-level 'title' key in form-compile DSL."
} else {
Report-OK "Title: multilingual XML"
}
}
}
# --- Summary ---
Write-Host ""
Write-Host "---"
Write-Host "Total: $($allElements.Count) elements, $($attrNodes.Count) attributes, $($cmdNodes.Count) commands"
if ($stopped) {
Write-Host "Stopped after $MaxErrors errors. Fix and re-run."
}
if ($errors -eq 0 -and $warnings -eq 0) {
Write-Host "All checks passed."
} else {
Write-Host "Errors: $errors, Warnings: $warnings"
}
if ($errors -gt 0) {
exit 1
} else {
exit 0
}