From 7f06f3ca4c7d6be67df866403fd99581113a83d6 Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Mon, 3 Aug 2026 07:55:57 +0300 Subject: [PATCH] vfp_symbols.ps1: index de simboluri peste cache-ul text si .prg-urile proiectului Retine intervalul de linii al fiecarei clase/metode/proceduri, ca o potrivire de grep sa poata fi atribuita clasei si metodei care o contine - ce grep nu poate spune intr-un .vc2, unde antetul clasei poate fi cu mii de linii deasupra potrivirii si ~2/3 din fisier sunt proprietati si metadata. Moduri: -Grep (eticheteaza rezultatele, -CodeOnly taie metadata), -Where fisier:linie, -Find (unde e definit), -Class (lant de mostenire + metode cu intervale). Indexul se reconstruieste singur cand cache-ul e mai nou. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012ceaeZUhPwyDG4LXPzigUJ --- CLAUDE.md | 11 ++ vfp_symbols.ps1 | 306 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 317 insertions(+) create mode 100644 vfp_symbols.ps1 diff --git a/CLAUDE.md b/CLAUDE.md index 406fe6e..fd5c875 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,6 +22,7 @@ It serves two purposes: - **`git_sync.ps1`** — the **in-tree** flow (primary for migrated ROA projects): folder-mode recursive over the whole project tree (incl. `COMUN\`), generates the `.??2` text **next to each binary** (not in an external cache) so git tracks readable text instead of binaries. Incremental, converts in a temp staging area and copies back only the text (never touches the SVN working-copy binaries), cleans up orphan `.??2`, continues past per-file failures and exits nonzero on any. Params: `-ProjectRoot`, `-DbfList` (tables converted to `.db2` with data), `-RoundtripExempt`, `-SubDir`. Run at session start and before every `git commit`; do not commit while it reports failures. - **`vcx2txt.ps1`** — PowerShell script that converts the VFP binaries of a project (read from its `.pjx`) into text under a cache folder, so the code inside `.vcx/.scx/.frx/.mnx/.dbc` binaries becomes greppable. Incremental (skips up-to-date files), does not need the VFP IDE open (uses the compiled `FoxBin2Prg.EXE`). Search-only / non-migrated projects; migrated projects use `git_sync.ps1` instead. +- **`vfp_symbols.ps1`** — symbol index over the text cache and the project's `.prg`: for every class/method/procedure it records the **line range**, so a hit can be attributed to its owner. Answers what grep cannot in a `.vc2` (where the class header may be 13k lines above the hit and ~2/3 of the file is properties/metadata): `-Grep ` labels each match with `class.method` (`-CodeOnly` drops metadata hits), `-Where 'file:line'` names the enclosing member, `-Find ` locates the **definition**, `-Class ` prints the inheritance chain plus members with their ranges. Rebuilds automatically when the cache is newer (~4s); index is a regenerable `_symbols.tsv` in the cache root. - **`txt2vcx.ps1`** — the write-back counterpart: regenerates `.vcx/.scx` binaries from edited `.vc2/.sc2` text and copies them back into the project (see "Golden rule" below for scope/limits). `-CacheRoot` defaults to `-ProjectRoot` (in-tree flow: cache→project mapping is identity); pass a separate root only for the old external-cache flow. - **`vfp_filemap.ps1`** — shared bin↔memo↔text extension map (`.vcx`/`.vct`/`.vc2`, `.scx`/`.sct`/`.sc2`, `.pjx`/`.pjt`/`.pj2`, `.dbf`/`.fpt`/`.db2`, …), dot-sourced by `git_sync.ps1`, `vcx2txt.ps1` and `txt2vcx.ps1` so the mapping stays in one place. Also hosts `Invoke-FoxBin2PrgSafe` (timeout + `taskkill`). - **`test_roundtrip.ps1`** — standalone fidelity/regression test for the bin→text→bin→text roundtrip and for `txt2vcx.ps1`'s guard rails (staleness, COMUN, missing binary, negative/corrupt input, multi-file runs); parametrized per project. See examples below. @@ -51,6 +52,16 @@ It serves two purposes: Then search the cache with Grep (e.g. `PROCEDURE do_salvare`), citing `file:line` from the cache. +Attribute hits to their owning class/method (build the index first, or let it build itself): + +```powershell +& 'D:\ROA\UTIL\foxbin2prg\vfp_symbols.ps1' -CacheRoot 'D:\ROA\_vfp_textcache\roagest' -ProjectRoot 'D:\ROA\ROAGEST' -Grep 'do_salvare' -CodeOnly + +# -Where 'oavize.vc2:8412' what class/method owns that line (needed before a txt2vcx edit) +# -Find 'do_salvare' where it is DEFINED (grep also returns the call sites) +# -Class frm_avize inheritance chain + members with line ranges +``` + Write back an edited `.vc2`/`.sc2` (after reviewing the diff on text): ```powershell diff --git a/vfp_symbols.ps1 b/vfp_symbols.ps1 new file mode 100644 index 0000000..2da9f5c --- /dev/null +++ b/vfp_symbols.ps1 @@ -0,0 +1,306 @@ +<# +.SYNOPSIS + Index de simboluri (clase, metode, proceduri) peste cache-ul text VFP si peste .prg-urile + proiectului. Raspunde la "in ce clasa/metoda e linia N" si "unde e definit X". + +.DESCRIPTION + Grep-ul gaseste textul, dar intr-un .vc2 de 18.000 de linii nu spune in ce metoda a carei clase + a nimerit (header-ul clasei poate fi cu 13.000 de linii mai sus, iar ~2/3 din fisier sunt + proprietati si metadata, nu cod). Scriptul construieste un index cu intervalul de linii al + fiecarei clase/metode si il foloseste ca sa etichete rezultatele. + + Indexul e derivat 100% din cache-ul text (vezi vcx2txt.ps1) - se poate sterge oricand. + +.PARAMETER Grep + Cauta expresia (regex) in fisierele indexate si eticheteaza fiecare rezultat cu clasa.metoda. + +.PARAMETER CodeOnly + Cu -Grep: arata doar potrivirile din corpul metodelor/procedurilor (taie metadata si listele + de proprietati din .vc2/.sc2). + +.PARAMETER Where + "fisier:linie" -> ce clasa/metoda contine linia respectiva. + +.PARAMETER Find + Numele unui simbol (accepta * si ?) -> unde e DEFINIT (nu si unde e apelat). + +.PARAMETER Class + Numele unei clase -> lantul de mostenire + metodele ei, cu intervalele de linii. + +.PARAMETER Build + Reconstruieste indexul (implicit se reconstruieste automat cand e invechit). + +.EXAMPLE + # construieste/actualizeaza indexul + powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\vfp_symbols.ps1 + +.EXAMPLE + # cautare etichetata, doar in cod + ... vfp_symbols.ps1 -Grep 'calcul_cheiaj' -CodeOnly + +.EXAMPLE + ... vfp_symbols.ps1 -Where 'oacnpro.vc2:14937' + ... vfp_symbols.ps1 -Find 'do_calcul_*' + ... vfp_symbols.ps1 -Class frm_calcul_cheiaj +#> +[CmdletBinding()] +param( + [string]$Grep, + [switch]$CodeOnly, + [string]$Where, + [string]$Find, + [string]$Class, + [string]$CacheRoot = 'D:\ROA\_vfp_textcache\roacnpro', + [string]$ProjectRoot = 'D:\ROA\ROAACNPRO', + [string[]]$PrgDirs = @('Programe', 'COMUN\programe'), + [string]$ExcludePrg = '\\programe\\roa\\', + [string]$IndexFile, + [switch]$Build, + [switch]$Stats +) + +$ErrorActionPreference = 'Stop' +if (-not $IndexFile) { $IndexFile = Join-Path $CacheRoot '_symbols.tsv' } + +# Codepage-ul din antetul .vc2 nu e de incredere (vezi CLAUDE.md), dar aici se citesc doar +# cuvinte-cheie si identificatori ASCII, iar fisierele nu sunt niciodata rescrise. +$cp1252 = [System.Text.Encoding]::GetEncoding(1252) + +# .vc2/.sc2 = clase/forme convertite din binar; .prg = sursa directa (are si Define Class, ex. roa.prg) +$rxClass = [regex]'(?i)^\s*DEFINE\s+CLASS\s+([\w]+)\s+AS\s+([\w\.]+)\s*(?:OF\s+"?([^"]*?)"?\s*)?$' +$rxEndDef = [regex]'(?i)^\s*ENDDEFINE\b' +$rxProc = [regex]'(?i)^\s*(?:(?:HIDDEN|PROTECTED)\s+)*(?:PROCEDURE|FUNCTION)\s+([\w\.]+)' +$rxEndPrc = [regex]'(?i)^\s*(?:ENDPROC|ENDFUNC)\b' + +function Get-SourceFiles { + $files = @() + # -Include e ignorat cu -LiteralPath in PS 5.1 -> filtrarea se face cu Where-Object + if (Test-Path $CacheRoot) { + $files += Get-ChildItem -LiteralPath $CacheRoot -Recurse -File | + Where-Object { $_.Extension -match '(?i)^\.(vc2|sc2)$' } + } + foreach ($d in $PrgDirs) { + $full = Join-Path $ProjectRoot $d + if (Test-Path $full) { + $files += Get-ChildItem -LiteralPath $full -Recurse -File -Filter *.prg | + Where-Object { $_.FullName -notmatch '\\\.svn\\' -and + ($ExcludePrg -eq '' -or $_.FullName -notmatch "(?i)$ExcludePrg") } + } + } + $files +} + +function Parse-File($file) { + $lines = [System.IO.File]::ReadAllLines($file.FullName, $cp1252) + $recs = New-Object System.Collections.ArrayList + + $cls = $null; $clsLine = 0; $clsParent = ''; $clsOf = '' + $prc = $null; $prcLine = 0 + + # inchide procedura curenta la linia $end si o adauga in lista + $closeProc = { + param($end) + if ($prc) { + $q = if ($cls) { "$cls.$prc" } else { $prc } + $k = if ($cls) { 'method' } else { 'proc' } + [void]$recs.Add([pscustomobject]@{ + kind = $k; qname = $q; class = [string]$cls; member = $prc + file = $file.FullName; line = $prcLine; endline = $end + parent = ''; parentfile = '' + }) + $script:prcClosed = $true + } + } + + for ($i = 0; $i -lt $lines.Length; $i++) { + $ln = $lines[$i] + $num = $i + 1 + $t = $ln.TrimStart() + if ($t.Length -eq 0 -or $t[0] -eq '*') { continue } # comentariile nu deschid blocuri + + $m = $rxClass.Match($ln) + if ($m.Success) { + & $closeProc ($num - 1); $prc = $null + if ($cls) { + [void]$recs.Add([pscustomobject]@{ + kind='class'; qname=$cls; class=$cls; member='' + file=$file.FullName; line=$clsLine; endline=($num-1) + parent=$clsParent; parentfile=$clsOf }) + } + $cls = $m.Groups[1].Value; $clsLine = $num + $clsParent = $m.Groups[2].Value; $clsOf = $m.Groups[3].Value + continue + } + if ($rxEndDef.IsMatch($ln)) { + & $closeProc ($num - 1); $prc = $null + if ($cls) { + [void]$recs.Add([pscustomobject]@{ + kind='class'; qname=$cls; class=$cls; member='' + file=$file.FullName; line=$clsLine; endline=$num + parent=$clsParent; parentfile=$clsOf }) + $cls = $null + } + continue + } + $m = $rxProc.Match($ln) + if ($m.Success) { + & $closeProc ($num - 1) # VFP permite proceduri fara ENDPROC + $prc = $m.Groups[1].Value; $prcLine = $num + continue + } + if ($rxEndPrc.IsMatch($ln) -and $prc) { + & $closeProc $num; $prc = $null + continue + } + } + & $closeProc $lines.Length + if ($cls) { + [void]$recs.Add([pscustomobject]@{ + kind='class'; qname=$cls; class=$cls; member='' + file=$file.FullName; line=$clsLine; endline=$lines.Length + parent=$clsParent; parentfile=$clsOf }) + } + $recs +} + +function Build-Index { + $src = Get-SourceFiles + if (-not $src) { throw "Nu am gasit surse. Ruleaza intai vcx2txt.ps1 (cache: $CacheRoot)." } + $all = New-Object System.Collections.ArrayList + foreach ($f in $src) { foreach ($r in (Parse-File $f)) { [void]$all.Add($r) } } + + $dir = Split-Path $IndexFile -Parent + if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } + $all | Select-Object kind, qname, class, member, file, line, endline, parent, parentfile | + Export-Csv -LiteralPath $IndexFile -Delimiter "`t" -NoTypeInformation -Encoding UTF8 + + $nc = ($all | Where-Object kind -eq 'class').Count + $nm = ($all | Where-Object kind -eq 'method').Count + $np = ($all | Where-Object kind -eq 'proc').Count + Write-Host "Index: $($src.Count) fisiere -> $nc clase, $nm metode, $np proceduri" + Write-Host "Scris in: $IndexFile" + $all +} + +function Test-IndexStale { + if (-not (Test-Path $IndexFile)) { return $true } + $idx = (Get-Item $IndexFile).LastWriteTime + foreach ($f in (Get-SourceFiles)) { if ($f.LastWriteTime -gt $idx) { return $true } } + $false +} + +function Get-Index { + if ($Build -or (Test-IndexStale)) { return Build-Index } + Import-Csv -LiteralPath $IndexFile -Delimiter "`t" | ForEach-Object { + $_.line = [int]$_.line; $_.endline = [int]$_.endline; $_ + } +} + +# Indexul se grupeaza o singura data pe fisier: cautarea liniara prin toate inregistrarile +# la fiecare potrivire face -Grep de ~100x mai lent pe simboluri des folosite (ex. goExecutor). +$script:ownerIdx = $null +function Init-OwnerIndex($index) { + $h = @{} + foreach ($r in $index) { + $k = $r.file.ToLower() + if (-not $h.ContainsKey($k)) { $h[$k] = New-Object System.Collections.ArrayList } + [void]$h[$k].Add($r) + } + $script:ownerIdx = $h +} + +# proprietarul unei linii: metoda bate clasa (intervalul cel mai strans) +function Get-Owner($file, $lineNo) { + $k = $file.ToLower() + $recs = $script:ownerIdx[$k] + if (-not $recs) { + foreach ($kk in $script:ownerIdx.Keys) { # accepta si doar numele fisierului + if ($kk.EndsWith('\' + $k)) { $recs = $script:ownerIdx[$kk]; break } + } + } + if (-not $recs) { return $null } + $best = $null; $bestSpan = [int]::MaxValue + foreach ($r in $recs) { + if ($r.line -le $lineNo -and $r.endline -ge $lineNo) { + $span = $r.endline - $r.line + if ($span -lt $bestSpan) { $bestSpan = $span; $best = $r } + } + } + $best +} + +# --- moduri --- + +if ($Where) { + $idx = Get-Index + if ($Where -notmatch '^(?.+?):(?\d+)\s*$') { throw "Format asteptat: 'fisier:linie'" } + Init-OwnerIndex $idx + $o = Get-Owner $Matches['f'] ([int]$Matches['l']) + if ($o) { "{0} [{1}] {2}:{3}-{4}" -f $o.qname, $o.kind, (Split-Path $o.file -Leaf), $o.line, $o.endline } + else { "(in afara oricarei clase/metode - metadata sau cod la nivel de fisier)" } + return +} + +if ($Find) { + $idx = Get-Index + # numele unei clase intoarce definitia ei, nu toti membrii (pentru membri: -Class) + $hits = $idx | Where-Object { $_.member -like $Find -or $_.qname -like $Find -or + ($_.kind -eq 'class' -and $_.class -like $Find) } + if (-not $hits) { Write-Host "Nicio definitie pentru '$Find'."; return } + $hits | Sort-Object kind, qname | ForEach-Object { + "{0,-7} {1,-45} {2}:{3}" -f $_.kind, $_.qname, $_.file.Replace($CacheRoot + '\', '').Replace($ProjectRoot + '\', ''), $_.line + } + return +} + +if ($Class) { + $idx = Get-Index + $c = $idx | Where-Object { $_.kind -eq 'class' -and $_.class -eq $Class } | Select-Object -First 1 + if (-not $c) { Write-Host "Clasa '$Class' nu e in index."; return } + + Write-Host "Lant de mostenire:" + $cur = $c; $depth = 0; $seen = @{} + while ($cur -and $depth -lt 12) { + "{0}{1} ({2}:{3})" -f (' ' * $depth), $cur.class, (Split-Path $cur.file -Leaf), $cur.line + if (-not $cur.parent -or $seen[$cur.parent]) { break } + $seen[$cur.class] = $true + $next = $idx | Where-Object { $_.kind -eq 'class' -and $_.class -eq $cur.parent } | Select-Object -First 1 + if (-not $next) { "{0}{1} (clasa de baza VFP sau librarie neindexata: {2})" -f (' ' * ($depth+1)), $cur.parent, $cur.parentfile; break } + $cur = $next; $depth++ + } + Write-Host '' + Write-Host "Metode proprii ($($c.class)):" + $idx | Where-Object { $_.kind -eq 'method' -and $_.class -eq $Class } | Sort-Object line | + ForEach-Object { " {0,-50} {1}-{2}" -f $_.member, $_.line, $_.endline } + return +} + +if ($Grep) { + $idx = Get-Index + $files = Get-SourceFiles + $res = $files | Select-String -Pattern $Grep -Encoding default + if (-not $res) { Write-Host "Nicio potrivire pentru '$Grep'."; return } + Init-OwnerIndex $idx + $shown = 0; $hidden = 0 + foreach ($r in $res) { + $o = Get-Owner $r.Path $r.LineNumber + if (-not $o -or $o.kind -eq 'class') { + if ($CodeOnly) { $hidden++; continue } + } + $owner = if ($o -and $o.kind -ne 'class') { $o.qname } elseif ($o) { "$($o.qname) (metadata)" } else { '(nivel fisier)' } + $rel = $r.Path.Replace($CacheRoot + '\', '').Replace($ProjectRoot + '\', '') + "{0}:{1}: [{2}] {3}" -f $rel, $r.LineNumber, $owner, $r.Line.Trim() + $shown++ + } + Write-Host '' + Write-Host "$shown potriviri$(if ($hidden) { " ($hidden ascunse: metadata/proprietati)" })" + return +} + +if ($Stats) { + $idx = Get-Index + $idx | Group-Object kind | Sort-Object Count -Descending | ForEach-Object { "{0,-8} {1}" -f $_.Name, $_.Count } + return +} + +Build-Index | Out-Null