scripts/Get-CollectorResourceTypeMap.ps1
|
#Requires -Version 7.0 <# .SYNOPSIS Derive the authoritative ARM resource-type list the 176 InventoryModules collectors filter `$Resources` by, straight from the AST -- not by grepping for a text pattern. .DESCRIPTION Every one of the 176 `Modules/Public/InventoryModules/*/*.ps1` collectors is a pure function of the shared `$Resources` array: it filters that array down to the rows it cares about with `$_.TYPE -eq/-in/-like '<type>'` and shapes the matches. There was no single list of exactly which ARM types those 176 filters name -- which is what `src/collect/Invoke-Collect.ps1` (Task AB#5639/5641) needs in order to know how much of the legacy extraction surface its own query pack already reproduces. This walks the real PowerShell AST (System.Management.Automation.Language.Parser) of every collector file, looking for a `BinaryExpressionAst` whose left side reads the `TYPE` (or `KIND`) member off the pipeline variable (`$_`, or a `foreach` loop variable the file uses instead, e.g. `$1`) and whose operator is one of `-eq/-ieq/-ceq`, `-in/-iin/-cin`, `-like/-ilike/-clike`, or `-match/-imatch/-cmatch`. AST parsing (not regex over the source text) is deliberate: it survives multi-line `-in @(...)` arrays, ignores types that only appear inside comments, and does not get confused by string interpolation or a `-like`/`-match` wildcard/regex fragment that is not a literal type. .PARAMETER InventoryRoot Path to the InventoryModules directory. Defaults to the repo's own copy. .PARAMETER AsJson Emit the coverage report as JSON instead of the default table + summary text. .OUTPUTS Without -AsJson: a summary count line plus a table of every distinct ARM type, how many collectors reference it, and which collector(s) do. With -AsJson: `[pscustomobject]@{ Type; CollectorCount; Collectors[] }[]`, sorted by Type, plus the `-like`/`-match` fragments recorded separately (they name a pattern, not a literal type, so they are not folded into the same coverage count). .NOTES Tracks ADO AB#5639 (US 5640 / Task 5641, Epic AB#5638). A `-like`/`-match` right-hand side is reported as a *fragment*, not a type: e.g. `'microsoft.recoveryservices/vaults/backupfabrics/protectioncontainers/protecteditems'` used with `-like` here happens to be a literal (no wildcard), but the AST walk cannot assume that in general -- a real wildcard/regex fragment could match many types Resource Graph would need `startswith()`/`matches regex` to reproduce, not `==`. Both are listed so a human reviewing coverage can judge each on its own but only `-eq`/`-in` literals are counted in the type/coverage totals: they are the only forms Invoke-Collect can safely translate as `type =~ "<value>"` in KQL. #> [CmdletBinding()] param( [string] $InventoryRoot = (Join-Path $PSScriptRoot '..' 'Modules' 'Public' 'InventoryModules'), [switch] $AsJson ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $InventoryRoot = (Resolve-Path $InventoryRoot).Path $eqOperators = @('Ieq', 'Ceq', 'Eq') $inOperators = @('Iin', 'Cin', 'In') $patternOperators = @('Ilike', 'Clike', 'Like', 'Imatch', 'Cmatch', 'Match') # member names the collectors read off the pipeline row to decide what a resource is. $typeMembers = @('TYPE', 'KIND') $typeHits = [ordered]@{} # type value -> HashSet[collector name] $patternHits = [System.Collections.Generic.List[object]]::new() function Test-TypeMemberAccess { <# .SYNOPSIS True when $node is a MemberExpressionAst reading .TYPE/.KIND off *some* variable (the pipeline variable in every real case seen in this codebase: $_, or a foreach alias like $1/$vm). #> param($Node) if ($Node -isnot [System.Management.Automation.Language.MemberExpressionAst]) { return $false } if ($Node.Expression -isnot [System.Management.Automation.Language.VariableExpressionAst]) { return $false } $memberName = if ($Node.Member -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $Node.Member.Value } else { $null } return [bool]($memberName -and ($typeMembers -contains $memberName.ToUpperInvariant())) } function Get-StringLiterals { <# .SYNOPSIS Pull every string-constant literal out of an expression: a bare string for -eq, or every element of an array-literal for -in. #> param($Node) switch ($Node) { { $_ -is [System.Management.Automation.Language.StringConstantExpressionAst] } { return , $_.Value } { $_ -is [System.Management.Automation.Language.ArrayLiteralAst] } { return @($_.Elements | ForEach-Object { Get-StringLiterals $_ } | Where-Object { $_ }) } { $_ -is [System.Management.Automation.Language.ArrayExpressionAst] } { $inner = $_.SubExpression.Statements | ForEach-Object { if ($_ -is [System.Management.Automation.Language.PipelineAst]) { $_.PipelineElements | ForEach-Object { if ($_ -is [System.Management.Automation.Language.CommandExpressionAst]) { Get-StringLiterals $_.Expression } } } } return @($inner | Where-Object { $_ }) } default { return @() } } } $files = @(Get-ChildItem -LiteralPath $InventoryRoot -Recurse -Filter '*.ps1' -File | Sort-Object FullName) foreach ($file in $files) { $collectorName = $file.BaseName $tokens = $null $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref] $tokens, [ref] $errors) if ($errors -and @($errors).Count -gt 0) { Write-Warning "Get-CollectorResourceTypeMap: '$($file.Name)' has $(@($errors).Count) parse error(s) -- skipping AST walk for this file." continue } $binaryOps = $ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.BinaryExpressionAst] }, $true) foreach ($bin in $binaryOps) { if (-not (Test-TypeMemberAccess $bin.Left)) { continue } $opText = $bin.Operator.ToString() if ($eqOperators -contains $opText -or $inOperators -contains $opText) { foreach ($value in (Get-StringLiterals $bin.Right)) { $key = $value.ToLowerInvariant() if (-not $typeHits.Contains($key)) { $typeHits[$key] = [System.Collections.Generic.HashSet[string]]::new() } [void] $typeHits[$key].Add($collectorName) } } elseif ($patternOperators -contains $opText) { foreach ($value in (Get-StringLiterals $bin.Right)) { $patternHits.Add([pscustomobject]@{ Collector = $collectorName; Operator = $opText; Fragment = $value }) } } } } $typeReport = @( foreach ($key in ($typeHits.Keys | Sort-Object)) { [pscustomobject]@{ Type = $key CollectorCount = $typeHits[$key].Count Collectors = @($typeHits[$key] | Sort-Object) } } ) if ($AsJson) { [pscustomobject]@{ GeneratedOn = (Get-Date).ToString('o') CollectorRoot = $InventoryRoot FileCount = $files.Count TypeCount = $typeReport.Count Types = $typeReport PatternHits = @($patternHits) } | ConvertTo-Json -Depth 6 return } Write-Host "Scanned $($files.Count) collector files under $InventoryRoot" Write-Host "Distinct ARM types referenced via -eq/-in: $($typeReport.Count)" Write-Host "Pattern (-like/-match) fragments (reported separately, not counted above): $($patternHits.Count)" Write-Host '' $typeReport | Format-Table -AutoSize Type, CollectorCount if ($patternHits.Count -gt 0) { Write-Host '' Write-Host '-- -like / -match fragments (reviewed by hand, not folded into the type count) --' $patternHits | Format-Table -AutoSize } |