<# .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, # implicit: fluxul in-tree din ROAACNPRO (textul .vc2/.sc2 sta langa binare, nu intr-un cache) [string]$CacheRoot = 'D:\ROA\ROAACNPRO', [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) { # la fluxul in-tree indexul nu se scrie in repo, ci in zona de cache comuna $IndexFile = if ($CacheRoot -like 'D:\ROA\_vfp_textcache\*') { Join-Path $CacheRoot '_symbols.tsv' } else { Join-Path ('D:\ROA\_vfp_textcache\' + (Split-Path $ProjectRoot -Leaf).ToLower()) '_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 } # acelasi -IndexFile poate veni dintr-o alta radacina (alt -CacheRoot): datele vechi nu par invechite $head = Get-Content -LiteralPath $IndexFile -TotalCount 2 if ($head.Count -ge 2) { $cols = $head[1].Split("`t") if ($cols.Count -gt 4) { $src = $cols[4].Trim('"') if (-not ($src.StartsWith($CacheRoot, 'OrdinalIgnoreCase') -or $src.StartsWith($ProjectRoot, 'OrdinalIgnoreCase'))) { 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