diff --git a/.github/skills/winmd-api-search/LICENSE.txt b/.github/skills/winmd-api-search/LICENSE.txt new file mode 100644 index 0000000000..d4b4c80f41 --- /dev/null +++ b/.github/skills/winmd-api-search/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/.github/skills/winmd-api-search/SKILL.md b/.github/skills/winmd-api-search/SKILL.md new file mode 100644 index 0000000000..56110a9507 --- /dev/null +++ b/.github/skills/winmd-api-search/SKILL.md @@ -0,0 +1,192 @@ +--- +name: winmd-api-search +description: 'Find and explore Windows desktop APIs. Use when building features that need platform capabilities — camera, file access, notifications, UI controls, AI/ML, sensors, networking, etc. Discovers the right API for a task and retrieves full type details (methods, properties, events, enumeration values).' +license: Complete terms in LICENSE.txt +--- + +# WinMD API Search + +This skill helps you find the right Windows API for any capability and get its full details. It searches a local cache of all WinMD metadata from: + +- **Windows Platform SDK** — all `Windows.*` WinRT APIs (always available, no restore needed) +- **WinAppSDK / WinUI** — bundled as a baseline in the cache generator (always available, no restore needed) +- **NuGet packages** — any additional packages in restored projects that contain `.winmd` files +- **Project-output WinMD** — class libraries (C++/WinRT, C#) that produce `.winmd` as build output + +Even on a fresh clone with no restore or build, you still get full Platform SDK + WinAppSDK coverage. + +## When to Use This Skill + +- User wants to build a feature and you need to find which API provides that capability +- User asks "how do I do X?" where X involves a platform feature (camera, files, notifications, sensors, AI, etc.) +- You need the exact methods, properties, events, or enumeration values of a type before writing code +- You're unsure which control, class, or interface to use for a UI or system task + +## Prerequisites + +- **.NET SDK 8.0 or later** — required to build the cache generator. Install from [dotnet.microsoft.com](https://dotnet.microsoft.com/download) if not available. + +## Cache Setup (Required Before First Use) + +All query and search commands read from a local JSON cache. **You must generate the cache before running any queries.** + +```powershell +# All projects in the repo (recommended for first run) +.\.github\skills\winmd-api-search\scripts\Update-WinMdCache.ps1 + +# Single project +.\.github\skills\winmd-api-search\scripts\Update-WinMdCache.ps1 -ProjectDir +``` + +No project restore or build is needed for baseline coverage (Platform SDK + WinAppSDK). For additional NuGet packages, the project needs `dotnet restore` (which generates `project.assets.json`) or a `packages.config` file. + +Cache is stored at `Generated Files\winmd-cache\`, deduplicated per-package+version. + +### What gets indexed + +| Source | When available | +|--------|----------------| +| Windows Platform SDK | Always (reads from local SDK install) | +| WinAppSDK (latest) | Always (bundled as baseline in cache generator) | +| WinAppSDK Runtime | When installed on the system (detected via `Get-AppxPackage`) | +| Project NuGet packages | After `dotnet restore` or with `packages.config` | +| Project-output `.winmd` | After project build (class libraries that produce WinMD) | + +> **Note:** This cache directory should be in `.gitignore` — it's generated, not source. + +## How to Use + +Pick the path that matches the situation: + +--- + +### Discover — "I don't know which API to use" + +The user describes a capability in their own words. You need to find the right API. + +**0. Ensure the cache exists** + +If the cache hasn't been generated yet, run `Update-WinMdCache.ps1` first — see [Cache Setup](#cache-setup-required-before-first-use) above. + +**1. Translate user language → search keywords** + +Map the user's daily language to programming terms. Try multiple variations: + +| User says | Search keywords to try (in order) | +|-----------|-----------------------------------| +| "take a picture" | `camera`, `capture`, `photo`, `MediaCapture` | +| "load from disk" | `file open`, `picker`, `FileOpen`, `StorageFile` | +| "describe what's in it" | `image description`, `Vision`, `Recognition` | +| "show a popup" | `dialog`, `flyout`, `popup`, `ContentDialog` | +| "drag and drop" | `drag`, `drop`, `DragDrop` | +| "save settings" | `settings`, `ApplicationData`, `LocalSettings` | + +Start with simple everyday words. If results are weak or irrelevant, try the more technical variation. + +**2. Run searches** + +```powershell +.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action search -Query "" +``` + +This returns ranked namespaces with top matching types and the **JSON file path**. + +If results have **low scores (below 60) or are irrelevant**, fall back to searching online documentation: + +1. Use web search to find the right API on Microsoft Learn, for example: + - `site:learn.microsoft.com/uwp/api ` for `Windows.*` APIs + - `site:learn.microsoft.com/windows/windows-app-sdk/api/winrt ` for `Microsoft.*` WinAppSDK APIs +2. Read the documentation pages to identify which type matches the user's requirement. +3. Once you know the type name, come back and use `-Action members` or `-Action enums` to get the exact local signatures. + +**3. Read the JSON to choose the right API** + +Read the file at the path(s) from the top results. The JSON has all types in that namespace — full members, signatures, parameters, return types, enumeration values. + +Read and decide which types and members fit the user's requirement. + +**4. Look up official documentation for context** + +The cache contains only signatures — no descriptions or usage guidance. For explanations, examples, and remarks, look up the type on Microsoft Learn: + +| Namespace prefix | Documentation base URL | +|-----------------|----------------------| +| `Windows.*` | `https://learn.microsoft.com/uwp/api/{fully.qualified.typename}` | +| `Microsoft.*` (WinAppSDK) | `https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/{fully.qualified.typename}` | + +For example, `Microsoft.UI.Xaml.Controls.NavigationView` maps to: +`https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.navigationview` + +**5. Use the API knowledge to answer or write code** + +--- + +### Lookup — "I know the API, show me the details" + +You already know (or suspect) the type or namespace name. Go direct: + +```powershell +# Get all members of a known type +.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action members -TypeName "Microsoft.UI.Xaml.Controls.NavigationView" + +# Get enum values +.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action enums -TypeName "Microsoft.UI.Xaml.Visibility" + +# List all types in a namespace +.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action types -Namespace "Microsoft.UI.Xaml.Controls" + +# Browse namespaces +.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action namespaces -Filter "Microsoft.UI" +``` + +If you need full detail beyond what `-Action members` shows, use `-Action search` to get the JSON file path, then read the JSON file directly. + +--- + +### Other Commands + +```powershell +# List cached projects +.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action projects + +# List packages for a project +.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action packages + +# Show stats +.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action stats +``` + +> If only one project is cached, `-Project` is auto-selected. +> If multiple projects exist, add `-Project ` (use `-Action projects` to see available names). +> In scan mode, manifest names include a short hash suffix to avoid collisions; you can pass the base project name without the suffix if it's unambiguous. + +## Search Scoring + +The search ranks type names and member names against your query: + +| Score | Match type | Example | +|-------|-----------|---------| +| 100 | Exact name | `Button` → `Button` | +| 80 | Starts with | `Navigation` → `NavigationView` | +| 60 | Contains | `Dialog` → `ContentDialog` | +| 50 | PascalCase initials | `ASB` → `AutoSuggestBox` | +| 40 | Multi-keyword AND | `navigation item` → `NavigationViewItem` | +| 20 | Fuzzy character match | `NavVw` → `NavigationView` | + +Results are grouped by namespace. Higher-scored namespaces appear first. + +## Troubleshooting + +| Issue | Fix | +|-------|-----| +| "Cache not found" | Run `Update-WinMdCache.ps1` | +| "Multiple projects cached" | Add `-Project ` | +| "Namespace not found" | Use `-Action namespaces` to list available ones | +| "Type not found" | Use fully qualified name (e.g., `Microsoft.UI.Xaml.Controls.Button`) | +| Stale after NuGet update | Re-run `Update-WinMdCache.ps1` | +| Cache in git history | Add `Generated Files/` to `.gitignore` | + +## References + +- [Windows Platform SDK API reference](https://learn.microsoft.com/uwp/api/) — documentation for `Windows.*` namespaces +- [Windows App SDK API reference](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/) — documentation for `Microsoft.*` WinAppSDK namespaces diff --git a/.github/skills/winmd-api-search/scripts/Invoke-WinMdQuery.ps1 b/.github/skills/winmd-api-search/scripts/Invoke-WinMdQuery.ps1 new file mode 100644 index 0000000000..4ed9e338d4 --- /dev/null +++ b/.github/skills/winmd-api-search/scripts/Invoke-WinMdQuery.ps1 @@ -0,0 +1,505 @@ +<# +.SYNOPSIS + Query WinMD API metadata from cached JSON files. + +.DESCRIPTION + Reads pre-built JSON cache of WinMD types, members, and namespaces. + The cache is organized per-package (deduplicated) with project manifests + that map each project to its referenced packages. + + Supports listing namespaces, types, members, searching, enum value lookup, + and listing cached projects/packages. + +.PARAMETER Action + The query action to perform: + - projects : List cached projects + - packages : List packages for a project + - stats : Show aggregate statistics for a project + - namespaces : List all namespaces (optional -Filter prefix) + - types : List types in a namespace (-Namespace required) + - members : List members of a type (-TypeName required) + - search : Search types and members by name (-Query required) + - enums : List enum values (-TypeName required) + +.PARAMETER Project + Project name to query. Auto-selected if only one project is cached. + Use -Action projects to list available projects. + +.PARAMETER Namespace + Namespace to query types from (used with -Action types). + +.PARAMETER TypeName + Full type name e.g. "Microsoft.UI.Xaml.Controls.Button" (used with -Action members, enums). + +.PARAMETER Query + Search query string (used with -Action search). + +.PARAMETER Filter + Optional prefix filter for namespaces (used with -Action namespaces). + +.PARAMETER CacheDir + Path to the winmd-cache directory. Defaults to "Generated Files\winmd-cache" + relative to the workspace root. + +.PARAMETER MaxResults + Maximum number of results to return for search. Defaults to 30. + +.EXAMPLE + .\Invoke-WinMdQuery.ps1 -Action projects + .\Invoke-WinMdQuery.ps1 -Action packages -Project BlankWinUI + .\Invoke-WinMdQuery.ps1 -Action stats -Project BlankWinUI + .\Invoke-WinMdQuery.ps1 -Action namespaces -Filter "Microsoft.UI" + .\Invoke-WinMdQuery.ps1 -Action types -Namespace "Microsoft.UI.Xaml.Controls" + .\Invoke-WinMdQuery.ps1 -Action members -TypeName "Microsoft.UI.Xaml.Controls.Button" + .\Invoke-WinMdQuery.ps1 -Action search -Query "NavigationView" + .\Invoke-WinMdQuery.ps1 -Action enums -TypeName "Microsoft.UI.Xaml.Visibility" +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [ValidateSet('projects', 'packages', 'stats', 'namespaces', 'types', 'members', 'search', 'enums')] + [string]$Action, + + [string]$Project, + [string]$Namespace, + [string]$TypeName, + [string]$Query, + [string]$Filter, + [string]$CacheDir, + [int]$MaxResults = 30 +) + +# ─── Resolve cache directory ───────────────────────────────────────────────── + +if (-not $CacheDir) { + # Convention: skill lives at .github/skills/winmd-api-search/scripts/ + # so workspace root is 4 levels up from $PSScriptRoot. + $scriptDir = $PSScriptRoot + $root = (Resolve-Path (Join-Path $scriptDir '..\..\..\..')).Path + $CacheDir = Join-Path $root 'Generated Files\winmd-cache' +} + +if (-not (Test-Path $CacheDir)) { + Write-Error "Cache not found at: $CacheDir`nRun: .\Update-WinMdCache.ps1 (from .github\skills\winmd-api-search\scripts\)" + exit 1 +} + +# ─── Project resolution helpers ────────────────────────────────────────────── + +function Get-CachedProjects { + $projectsDir = Join-Path $CacheDir 'projects' + if (-not (Test-Path $projectsDir)) { return @() } + Get-ChildItem $projectsDir -Filter '*.json' | ForEach-Object { $_.BaseName } +} + +function Resolve-ProjectManifest { + param([string]$Name) + + $projectsDir = Join-Path $CacheDir 'projects' + if (-not (Test-Path $projectsDir)) { + Write-Error "No projects cached. Run Update-WinMdCache.ps1 first." + exit 1 + } + + if ($Name) { + $path = Join-Path $projectsDir "$Name.json" + if (-not (Test-Path $path)) { + # Scan mode appends a hash suffix -- try prefix match + $matching = @(Get-ChildItem $projectsDir -Filter "${Name}_*.json" -ErrorAction SilentlyContinue) + if ($matching.Count -eq 1) { + return Get-Content $matching[0].FullName -Raw | ConvertFrom-Json + } + if ($matching.Count -gt 1) { + $names = ($matching | ForEach-Object { $_.BaseName }) -join ', ' + Write-Error "Multiple projects match '$Name'. Specify the full name: $names" + exit 1 + } + $available = (Get-CachedProjects) -join ', ' + Write-Error "Project '$Name' not found. Available: $available" + exit 1 + } + return Get-Content $path -Raw | ConvertFrom-Json + } + + # Auto-select if only one project + $manifests = Get-ChildItem $projectsDir -Filter '*.json' -ErrorAction SilentlyContinue + if ($manifests.Count -eq 0) { + Write-Error "No projects cached. Run Update-WinMdCache.ps1 first." + exit 1 + } + if ($manifests.Count -eq 1) { + return Get-Content $manifests[0].FullName -Raw | ConvertFrom-Json + } + + $available = ($manifests | ForEach-Object { $_.BaseName }) -join ', ' + Write-Error "Multiple projects cached -- use -Project to specify. Available: $available" + exit 1 +} + +function Get-PackageCacheDirs { + param($Manifest) + $dirs = @() + foreach ($pkg in $Manifest.packages) { + $dir = Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version + if (Test-Path $dir) { + $dirs += $dir + } + } + return $dirs +} + +# ─── Action: projects ──────────────────────────────────────────────────────── + +function Show-Projects { + $projects = Get-CachedProjects + if ($projects.Count -eq 0) { + Write-Output "No projects cached." + return + } + Write-Output "Cached projects ($($projects.Count)):" + foreach ($p in $projects) { + $manifest = Get-Content (Join-Path (Join-Path $CacheDir 'projects') "$p.json") -Raw | ConvertFrom-Json + $pkgCount = $manifest.packages.Count + Write-Output " $p ($pkgCount package(s))" + } +} + +# ─── Action: packages ──────────────────────────────────────────────────────── + +function Show-Packages { + $manifest = Resolve-ProjectManifest -Name $Project + Write-Output "Packages for project '$($manifest.projectName)' ($($manifest.packages.Count)):" + foreach ($pkg in $manifest.packages) { + $metaPath = Join-Path (Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version) 'meta.json' + if (Test-Path $metaPath) { + $meta = Get-Content $metaPath -Raw | ConvertFrom-Json + Write-Output " $($pkg.id)@$($pkg.version) -- $($meta.totalTypes) types, $($meta.totalMembers) members" + } else { + Write-Output " $($pkg.id)@$($pkg.version) -- (cache missing)" + } + } +} + +# ─── Action: stats ─────────────────────────────────────────────────────────── + +function Show-Stats { + $manifest = Resolve-ProjectManifest -Name $Project + $totalTypes = 0 + $totalMembers = 0 + $totalNamespaces = 0 + $totalWinMd = 0 + + foreach ($pkg in $manifest.packages) { + $metaPath = Join-Path (Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version) 'meta.json' + if (Test-Path $metaPath) { + $meta = Get-Content $metaPath -Raw | ConvertFrom-Json + $totalTypes += $meta.totalTypes + $totalMembers += $meta.totalMembers + $totalNamespaces += $meta.totalNamespaces + $totalWinMd += $meta.winMdFiles.Count + } + } + + Write-Output "WinMD Index Statistics -- $($manifest.projectName)" + Write-Output "======================================" + Write-Output " Packages: $($manifest.packages.Count)" + Write-Output " Namespaces: $totalNamespaces (may overlap across packages)" + Write-Output " Types: $totalTypes" + Write-Output " Members: $totalMembers" + Write-Output " WinMD files: $totalWinMd" +} + +# ─── Action: namespaces ────────────────────────────────────────────────────── + +function Get-Namespaces { + param([string]$Prefix) + $manifest = Resolve-ProjectManifest -Name $Project + $dirs = Get-PackageCacheDirs -Manifest $manifest + $allNs = @() + + foreach ($dir in $dirs) { + $nsFile = Join-Path $dir 'namespaces.json' + if (Test-Path $nsFile) { + $allNs += (Get-Content $nsFile -Raw | ConvertFrom-Json) + } + } + + $allNs = $allNs | Sort-Object -Unique + if ($Prefix) { + $allNs = $allNs | Where-Object { $_ -like "$Prefix*" } + } + $allNs | ForEach-Object { Write-Output $_ } +} + +# ─── Action: types ─────────────────────────────────────────────────────────── + +function Get-TypesInNamespace { + param([string]$Ns) + if (-not $Ns) { + Write-Error "-Namespace is required for 'types' action." + exit 1 + } + + $manifest = Resolve-ProjectManifest -Name $Project + $dirs = Get-PackageCacheDirs -Manifest $manifest + $safeFile = $Ns.Replace('.', '_') + '.json' + $found = $false + $seen = @{} + + foreach ($dir in $dirs) { + $filePath = Join-Path $dir "types\$safeFile" + if (-not (Test-Path $filePath)) { continue } + $found = $true + $types = Get-Content $filePath -Raw | ConvertFrom-Json + foreach ($t in $types) { + if ($seen.ContainsKey($t.fullName)) { continue } + $seen[$t.fullName] = $true + Write-Output "$($t.kind) $($t.fullName)$(if ($t.baseType) { " : $($t.baseType)" } else { '' })" + } + } + + if (-not $found) { + Write-Error "Namespace not found: $Ns" + exit 1 + } +} + +# ─── Action: members ───────────────────────────────────────────────────────── + +function Get-MembersOfType { + param([string]$FullName) + if (-not $FullName) { + Write-Error "-TypeName is required for 'members' action." + exit 1 + } + + $lastDot = $FullName.LastIndexOf('.') + if ($lastDot -lt 0) { + Write-Error "-TypeName must include a namespace (for example: 'MyNamespace.MyType'). Provided: $FullName" + exit 1 + } + + $ns = $FullName.Substring(0, $lastDot) + $safeFile = $ns.Replace('.', '_') + '.json' + + $manifest = Resolve-ProjectManifest -Name $Project + $dirs = Get-PackageCacheDirs -Manifest $manifest + + foreach ($dir in $dirs) { + $filePath = Join-Path $dir "types\$safeFile" + if (-not (Test-Path $filePath)) { continue } + + $types = Get-Content $filePath -Raw | ConvertFrom-Json + $type = $types | Where-Object { $_.fullName -eq $FullName } + if (-not $type) { continue } + + Write-Output "$($type.kind) $($type.fullName)" + if ($type.baseType) { Write-Output " Extends: $($type.baseType)" } + Write-Output "" + foreach ($m in $type.members) { + Write-Output " [$($m.kind)] $($m.signature)" + } + return + } + + Write-Error "Type not found: $FullName" + exit 1 +} + +# ─── Action: search ────────────────────────────────────────────────────────── +# Ranks namespaces by best match score on type names and member names. +# Outputs: ranked namespaces with top matching types and the JSON file path. +# The agent can then read the JSON file to inspect all members intelligently. + +function Search-WinMd { + param([string]$SearchQuery, [int]$Max) + if (-not $SearchQuery) { + Write-Error "-Query is required for 'search' action." + exit 1 + } + + $manifest = Resolve-ProjectManifest -Name $Project + $dirs = Get-PackageCacheDirs -Manifest $manifest + + # Collect: namespace -> { bestScore, matchingTypes[], filePath } + $nsResults = @{} + + foreach ($dir in $dirs) { + $nsFile = Join-Path $dir 'namespaces.json' + if (-not (Test-Path $nsFile)) { continue } + $nsList = Get-Content $nsFile -Raw | ConvertFrom-Json + + foreach ($n in $nsList) { + $safeFile = $n.Replace('.', '_') + '.json' + $filePath = Join-Path $dir "types\$safeFile" + if (-not (Test-Path $filePath)) { continue } + + $types = Get-Content $filePath -Raw | ConvertFrom-Json + foreach ($t in $types) { + $typeScore = Get-MatchScore -Name $t.name -FullName $t.fullName -Query $SearchQuery + + # Also search member names for matches + $bestMemberScore = 0 + $matchingMember = $null + if ($t.members) { + foreach ($m in $t.members) { + $memberName = $m.name + $mScore = Get-MatchScore -Name $memberName -FullName "$($t.fullName).$memberName" -Query $SearchQuery + if ($mScore -gt $bestMemberScore) { + $bestMemberScore = $mScore + $matchingMember = $m.signature + } + } + } + + $score = [Math]::Max($typeScore, $bestMemberScore) + if ($score -le 0) { continue } + + if (-not $nsResults.ContainsKey($n)) { + $nsResults[$n] = @{ BestScore = 0; Types = @(); FilePaths = @() } + } + $entry = $nsResults[$n] + if ($score -gt $entry.BestScore) { $entry.BestScore = $score } + if ($entry.FilePaths -notcontains $filePath) { + $entry.FilePaths += $filePath + } + + if ($typeScore -ge $bestMemberScore) { + $entry.Types += @{ Text = "$($t.kind) $($t.fullName) [$typeScore]"; Score = $typeScore } + } else { + $entry.Types += @{ Text = "$($t.kind) $($t.fullName) -> $matchingMember [$bestMemberScore]"; Score = $bestMemberScore } + } + } + } + } + + if ($nsResults.Count -eq 0) { + Write-Output "No results found for: $SearchQuery" + return + } + + $ranked = $nsResults.GetEnumerator() | + Sort-Object { $_.Value.BestScore } -Descending | + Select-Object -First $Max + + foreach ($r in $ranked) { + $ns = $r.Key + $info = $r.Value + Write-Output "[$($info.BestScore)] $ns" + foreach ($fp in $info.FilePaths) { + Write-Output " File: $fp" + } + # Show top 5 highest-scoring matching types in this namespace + $info.Types | Sort-Object { $_.Score } -Descending | + Select-Object -First 5 | + ForEach-Object { Write-Output " $($_.Text)" } + Write-Output "" + } +} + +# ─── Search scoring ────────────────────────────────────────────────────────── +# Simple ranked scoring on type names. Higher = better. +# 100 = exact name 80 = starts-with 60 = substring +# 50 = PascalCase 40 = multi-keyword 20 = fuzzy subsequence + +function Get-MatchScore { + param([string]$Name, [string]$FullName, [string]$Query) + + $q = $Query.Trim() + if (-not $q) { return 0 } + + if ($Name -eq $q) { return 100 } + if ($Name -like "$q*") { return 80 } + if ($Name -like "*$q*" -or $FullName -like "*$q*") { return 60 } + + $initials = ($Name.ToCharArray() | Where-Object { [char]::IsUpper($_) }) -join '' + if ($initials.Length -ge 2 -and $initials -like "*$q*") { return 50 } + + $words = $q -split '\s+' | Where-Object { $_.Length -gt 0 } + if ($words.Count -gt 1) { + $allFound = $true + foreach ($w in $words) { + if ($Name -notlike "*$w*" -and $FullName -notlike "*$w*") { + $allFound = $false + break + } + } + if ($allFound) { return 40 } + } + + if (Test-FuzzySubsequence -Text $Name -Pattern $q) { return 20 } + + return 0 +} + +function Test-FuzzySubsequence { + param([string]$Text, [string]$Pattern) + $ti = 0 + $tLower = $Text.ToLowerInvariant() + $pLower = $Pattern.ToLowerInvariant() + foreach ($ch in $pLower.ToCharArray()) { + $idx = $tLower.IndexOf($ch, $ti) + if ($idx -lt 0) { return $false } + $ti = $idx + 1 + } + return $true +} + +# ─── Action: enums ─────────────────────────────────────────────────────────── + +function Get-EnumValues { + param([string]$FullName) + if (-not $FullName) { + Write-Error "-TypeName is required for 'enums' action." + exit 1 + } + + $lastDot = $FullName.LastIndexOf('.') + if ($lastDot -lt 1) { + Write-Error "-TypeName must be a fully-qualified type name including namespace, e.g. 'Namespace.TypeName'. Provided: $FullName" + exit 1 + } + + $ns = $FullName.Substring(0, $lastDot) + $safeFile = $ns.Replace('.', '_') + '.json' + + $manifest = Resolve-ProjectManifest -Name $Project + $dirs = Get-PackageCacheDirs -Manifest $manifest + + foreach ($dir in $dirs) { + $filePath = Join-Path $dir "types\$safeFile" + if (-not (Test-Path $filePath)) { continue } + + $types = Get-Content $filePath -Raw | ConvertFrom-Json + $type = $types | Where-Object { $_.fullName -eq $FullName } + if (-not $type) { continue } + + if ($type.kind -ne 'Enum') { + Write-Error "$FullName is not an Enum (kind: $($type.kind))" + exit 1 + } + Write-Output "Enum $($type.fullName)" + if ($type.enumValues) { + $type.enumValues | ForEach-Object { Write-Output " $_" } + } else { + Write-Output " (no values)" + } + return + } + + Write-Error "Type not found: $FullName" + exit 1 +} + +# ─── Dispatch ───────────────────────────────────────────────────────────────── + +switch ($Action) { + 'projects' { Show-Projects } + 'packages' { Show-Packages } + 'stats' { Show-Stats } + 'namespaces' { Get-Namespaces -Prefix $Filter } + 'types' { Get-TypesInNamespace -Ns $Namespace } + 'members' { Get-MembersOfType -FullName $TypeName } + 'search' { Search-WinMd -SearchQuery $Query -Max $MaxResults } + 'enums' { Get-EnumValues -FullName $TypeName } +} diff --git a/.github/skills/winmd-api-search/scripts/Update-WinMdCache.ps1 b/.github/skills/winmd-api-search/scripts/Update-WinMdCache.ps1 new file mode 100644 index 0000000000..11cb16a8d8 --- /dev/null +++ b/.github/skills/winmd-api-search/scripts/Update-WinMdCache.ps1 @@ -0,0 +1,208 @@ +<# +.SYNOPSIS + Generate or refresh the WinMD cache for the Agent Skill. + +.DESCRIPTION + Builds and runs the standalone cache generator to export cached JSON files + from all WinMD metadata found in project NuGet packages and Windows SDK. + + The cache is per-package+version: if two projects reference the same + package at the same version, the WinMD data is parsed once and shared. + + Supports single project or recursive scan of an entire repo. + +.PARAMETER ProjectDir + Path to a project directory (contains .csproj/.vcxproj), or a project file itself. + Defaults to scanning the workspace root. + +.PARAMETER Scan + Recursively discover all .csproj/.vcxproj files under ProjectDir. + +.PARAMETER OutputDir + Path to the cache output directory. Defaults to "Generated Files\winmd-cache". + +.EXAMPLE + .\Update-WinMdCache.ps1 + .\Update-WinMdCache.ps1 -ProjectDir BlankWinUI + .\Update-WinMdCache.ps1 -Scan -ProjectDir . + .\Update-WinMdCache.ps1 -ProjectDir "src\MyApp\MyApp.csproj" +#> +[CmdletBinding()] +param( + [string]$ProjectDir, + [switch]$Scan, + [string]$OutputDir = 'Generated Files\winmd-cache' +) + +$ErrorActionPreference = 'Stop' + +# Convention: skill lives at .github/skills/winmd-api-search/scripts/ +# so workspace root is 4 levels up from $PSScriptRoot. +$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')).Path +$generatorProj = Join-Path (Join-Path $PSScriptRoot 'cache-generator') 'CacheGenerator.csproj' + +# --------------------------------------------------------------------------- +# WinAppSDK version detection -- look only at the repo root folder (no recursion) +# --------------------------------------------------------------------------- + +function Get-WinAppSdkVersionFromDirectoryPackagesProps { + <# + .SYNOPSIS + Extract Microsoft.WindowsAppSDK version from a Directory.Packages.props + (Central Package Management) at the repo root. + #> + param([string]$RepoRoot) + $propsFile = Join-Path $RepoRoot 'Directory.Packages.props' + if (-not (Test-Path $propsFile)) { return $null } + try { + [xml]$xml = Get-Content $propsFile -Raw + $node = $xml.SelectNodes('//PackageVersion') | + Where-Object { $_.Include -eq 'Microsoft.WindowsAppSDK' } | + Select-Object -First 1 + if ($node) { return $node.Version } + } catch { + Write-Verbose "Could not parse $propsFile : $_" + } + return $null +} + +function Get-WinAppSdkVersionFromPackagesConfig { + <# + .SYNOPSIS + Extract Microsoft.WindowsAppSDK version from a packages.config at the repo root. + #> + param([string]$RepoRoot) + $configFile = Join-Path $RepoRoot 'packages.config' + if (-not (Test-Path $configFile)) { return $null } + try { + [xml]$xml = Get-Content $configFile -Raw + $node = $xml.SelectNodes('//package') | + Where-Object { $_.id -eq 'Microsoft.WindowsAppSDK' } | + Select-Object -First 1 + if ($node) { return $node.version } + } catch { + Write-Verbose "Could not parse $configFile : $_" + } + return $null +} + +# Try Directory.Packages.props first (CPM), then packages.config +$winAppSdkVersion = Get-WinAppSdkVersionFromDirectoryPackagesProps -RepoRoot $root +if (-not $winAppSdkVersion) { + $winAppSdkVersion = Get-WinAppSdkVersionFromPackagesConfig -RepoRoot $root +} +if ($winAppSdkVersion) { + Write-Host "Detected WinAppSDK version from repo: $winAppSdkVersion" -ForegroundColor Cyan +} else { + Write-Host "No WinAppSDK version found at repo root; will use latest (Version=*)" -ForegroundColor Yellow +} + +# Default: if no ProjectDir, scan the workspace root +if (-not $ProjectDir) { + $ProjectDir = $root + $Scan = $true +} + +Push-Location $root + +try { + # Detect installed .NET SDK -- require >= 8.0, prefer stable over preview + $dotnetSdks = dotnet --list-sdks 2>$null + $bestMajor = $dotnetSdks | + Where-Object { $_ -notmatch 'preview|rc|alpha|beta' } | + ForEach-Object { if ($_ -match '^(\d+)\.') { [int]$Matches[1] } } | + Where-Object { $_ -ge 8 } | + Sort-Object -Descending | + Select-Object -First 1 + + # Fall back to preview SDKs if no stable SDK found + if (-not $bestMajor) { + $bestMajor = $dotnetSdks | + ForEach-Object { if ($_ -match '^(\d+)\.') { [int]$Matches[1] } } | + Where-Object { $_ -ge 8 } | + Sort-Object -Descending | + Select-Object -First 1 + } + + if (-not $bestMajor) { + Write-Error "No .NET SDK >= 8.0 found. Install from https://dotnet.microsoft.com/download" + exit 1 + } + + $targetFramework = "net$bestMajor.0" + Write-Host "Using .NET SDK: $targetFramework" -ForegroundColor Cyan + + # Build MSBuild properties -- pass detected WinAppSDK version when available + $sdkVersionProp = '' + if ($winAppSdkVersion) { + $sdkVersionProp = "-p:WinAppSdkVersion=$winAppSdkVersion" + } + + Write-Host "Building cache generator..." -ForegroundColor Cyan + $restoreArgs = @($generatorProj, "-p:TargetFramework=$targetFramework", '--nologo', '-v', 'q') + if ($sdkVersionProp) { $restoreArgs += $sdkVersionProp } + dotnet restore @restoreArgs + if ($LASTEXITCODE -ne 0) { + Write-Error "Restore failed" + exit 1 + } + $buildArgs = @($generatorProj, '-c', 'Release', '--nologo', '-v', 'q', "-p:TargetFramework=$targetFramework", '--no-restore') + if ($sdkVersionProp) { $buildArgs += $sdkVersionProp } + dotnet build @buildArgs + if ($LASTEXITCODE -ne 0) { + Write-Error "Build failed" + exit 1 + } + + # Run the built executable directly (avoids dotnet run target framework mismatch issues) + $generatorDir = Join-Path $PSScriptRoot 'cache-generator' + $exePath = Join-Path $generatorDir "bin\Release\$targetFramework\CacheGenerator.exe" + if (-not (Test-Path $exePath)) { + # Fallback: try dll with dotnet + $dllPath = Join-Path $generatorDir "bin\Release\$targetFramework\CacheGenerator.dll" + if (Test-Path $dllPath) { + $exePath = $null + } else { + Write-Error "Built executable not found at: $exePath" + exit 1 + } + } + + $runArgs = @() + if ($Scan) { + $runArgs += '--scan' + } + + # Detect installed WinAppSDK runtime via Get-AppxPackage (the WindowsApps + # folder is ACL-restricted so C# cannot enumerate it directly). + # WinMD files are architecture-independent metadata, so pick whichever arch + # matches the current OS to ensure the package is present. + $osArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() + $runtimePkg = Get-AppxPackage -Name 'Microsoft.WindowsAppRuntime.*' -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notmatch 'CBS' -and $_.Architecture -eq $osArch } | + Sort-Object -Property Version -Descending | + Select-Object -First 1 + if ($runtimePkg -and $runtimePkg.InstallLocation -and (Test-Path $runtimePkg.InstallLocation)) { + Write-Host "Detected WinAppSDK runtime: $($runtimePkg.Name) v$($runtimePkg.Version)" -ForegroundColor Cyan + $runArgs += '--winappsdk-runtime' + $runArgs += $runtimePkg.InstallLocation + } + + $runArgs += $ProjectDir + $runArgs += $OutputDir + + Write-Host "Exporting WinMD cache..." -ForegroundColor Cyan + if ($exePath) { + & $exePath @runArgs + } else { + dotnet $dllPath @runArgs + } + if ($LASTEXITCODE -ne 0) { + Write-Error "Cache export failed" + exit 1 + } + + Write-Host "Cache updated at: $OutputDir" -ForegroundColor Green +} finally { + Pop-Location +} diff --git a/.github/skills/winmd-api-search/scripts/cache-generator/CacheGenerator.csproj b/.github/skills/winmd-api-search/scripts/cache-generator/CacheGenerator.csproj new file mode 100644 index 0000000000..63b52471c6 --- /dev/null +++ b/.github/skills/winmd-api-search/scripts/cache-generator/CacheGenerator.csproj @@ -0,0 +1,29 @@ + + + Exe + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/.github/skills/winmd-api-search/scripts/cache-generator/Directory.Build.props b/.github/skills/winmd-api-search/scripts/cache-generator/Directory.Build.props new file mode 100644 index 0000000000..c6262dfde3 --- /dev/null +++ b/.github/skills/winmd-api-search/scripts/cache-generator/Directory.Build.props @@ -0,0 +1,3 @@ + + + diff --git a/.github/skills/winmd-api-search/scripts/cache-generator/Directory.Build.targets b/.github/skills/winmd-api-search/scripts/cache-generator/Directory.Build.targets new file mode 100644 index 0000000000..a776d08ee4 --- /dev/null +++ b/.github/skills/winmd-api-search/scripts/cache-generator/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/.github/skills/winmd-api-search/scripts/cache-generator/Directory.Packages.props b/.github/skills/winmd-api-search/scripts/cache-generator/Directory.Packages.props new file mode 100644 index 0000000000..4c6affab6e --- /dev/null +++ b/.github/skills/winmd-api-search/scripts/cache-generator/Directory.Packages.props @@ -0,0 +1,3 @@ + + + diff --git a/.github/skills/winmd-api-search/scripts/cache-generator/Program.cs b/.github/skills/winmd-api-search/scripts/cache-generator/Program.cs new file mode 100644 index 0000000000..9ce7efcc8e --- /dev/null +++ b/.github/skills/winmd-api-search/scripts/cache-generator/Program.cs @@ -0,0 +1,1222 @@ +// Standalone WinMD cache generator — per-package deduplicate, multi-project support. +// Parses WinMD files from NuGet packages and Windows SDK, exports JSON cache +// keyed by package+version to avoid duplication across projects. +// +// Usage: +// CacheGenerator +// CacheGenerator --scan + +using System.Collections.Immutable; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Security.Cryptography; +using System.Xml.Linq; + +// --- Arg parsing --- + +var scanMode = args.Contains("--scan"); + +// Parse --winappsdk-runtime option +string? winAppSdkRuntimePath = null; +for (int i = 0; i < args.Length - 1; i++) +{ + if (args[i].Equals("--winappsdk-runtime", StringComparison.OrdinalIgnoreCase)) + { + winAppSdkRuntimePath = args[i + 1]; + break; + } +} + +var positionalArgs = args + .Where(a => !a.StartsWith('-')) + .Where(a => a != winAppSdkRuntimePath) // exclude the runtime path value + .ToArray(); + +if (positionalArgs.Length < 2) +{ + Console.Error.WriteLine("Usage:"); + Console.Error.WriteLine(" CacheGenerator "); + Console.Error.WriteLine(" CacheGenerator --scan "); + Console.Error.WriteLine(" CacheGenerator --winappsdk-runtime "); + Console.Error.WriteLine(); + Console.Error.WriteLine(" project-dir: Path containing .csproj/.vcxproj (or a project file itself)"); + Console.Error.WriteLine(" root-dir: Root to scan recursively for project files"); + Console.Error.WriteLine(" output-dir: Cache output (e.g. \"Generated Files\\winmd-cache\")"); + Console.Error.WriteLine(" --winappsdk-runtime: Path to installed WinAppSDK runtime (from Get-AppxPackage)"); + return 1; +} + +var inputPath = Path.GetFullPath(positionalArgs[0]); +var outputDir = Path.GetFullPath(positionalArgs[1]); + +var jsonOptions = new JsonSerializerOptions +{ + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() }, +}; + +// --- Discover project files --- + +var projectFiles = new List(); + +if (scanMode) +{ + if (!Directory.Exists(inputPath)) + { + Console.Error.WriteLine($"Error: Root directory not found: {inputPath}"); + return 1; + } + + var enumerationOptions = new EnumerationOptions + { + RecurseSubdirectories = true, + IgnoreInaccessible = true, + MatchType = MatchType.Simple, + }; + + projectFiles.AddRange(Directory.EnumerateFiles(inputPath, "*.csproj", enumerationOptions)); + projectFiles.AddRange(Directory.EnumerateFiles(inputPath, "*.vcxproj", enumerationOptions)); + + // Exclude common non-source directories + projectFiles = projectFiles + .Where(f => !f.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase)) + .Where(f => !f.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase)) + .Where(f => !f.Contains($"{Path.DirectorySeparatorChar}node_modules{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase)) + .ToList(); +} +else if (File.Exists(inputPath) && (inputPath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) || + inputPath.EndsWith(".vcxproj", StringComparison.OrdinalIgnoreCase))) +{ + projectFiles.Add(inputPath); +} +else if (Directory.Exists(inputPath)) +{ + projectFiles.AddRange(Directory.GetFiles(inputPath, "*.csproj")); + projectFiles.AddRange(Directory.GetFiles(inputPath, "*.vcxproj")); +} +else +{ + Console.Error.WriteLine($"Error: Path not found: {inputPath}"); + return 1; +} + +if (projectFiles.Count == 0) +{ + Console.Error.WriteLine($"No .csproj or .vcxproj files found in: {inputPath}"); + return 1; +} + +// Always include CacheGenerator.csproj as a baseline source of WinAppSDK WinMD files. +// It references Microsoft.WindowsAppSDK with ExcludeAssets="all" so the packages are +// downloaded during restore/build but don't affect the tool's compilation. +var selfCsproj = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "CacheGenerator.csproj"); +selfCsproj = Path.GetFullPath(selfCsproj); +if (File.Exists(selfCsproj) && !projectFiles.Any(f => + Path.GetFullPath(f).Equals(selfCsproj, StringComparison.OrdinalIgnoreCase))) +{ + projectFiles.Add(selfCsproj); +} + +Console.WriteLine($"WinMD Cache Generator (per-package deduplicate)"); +Console.WriteLine($" Output: {outputDir}"); +Console.WriteLine($" Projects: {projectFiles.Count}"); + +// --- Process each project --- + +var totalPackagesCached = 0; +var totalPackagesSkipped = 0; +var totalProjectsProcessed = 0; + +foreach (var projectFile in projectFiles) +{ + var projectDir = Path.GetDirectoryName(projectFile)!; + var projectName = Path.GetFileNameWithoutExtension(projectFile); + + Console.WriteLine($"\n--- {projectName} ({Path.GetFileName(projectFile)}) ---"); + + // Find packages that contain WinMD files + var packages = NuGetResolver.FindPackagesWithWinMd(projectDir, projectFile, winAppSdkRuntimePath); + + if (packages.Count == 0) + { + Console.WriteLine(" No packages with WinMD files (is the project restored?)"); + continue; + } + + Console.WriteLine($" {packages.Count} package(s) with WinMD files"); + totalProjectsProcessed++; + + var projectPackages = new List(); + + foreach (var pkg in packages) + { + var pkgCacheDir = Path.Combine(outputDir, "packages", pkg.Id, pkg.Version); + var metaPath = Path.Combine(pkgCacheDir, "meta.json"); + + if (File.Exists(metaPath)) + { + Console.WriteLine($" [cached] {pkg.Id}@{pkg.Version}"); + totalPackagesSkipped++; + } + else + { + Console.WriteLine($" [parse] {pkg.Id}@{pkg.Version} ({pkg.WinMdFiles.Count} WinMD file(s))"); + ExportPackageCache(pkg, pkgCacheDir); + totalPackagesCached++; + } + + projectPackages.Add(new ProjectPackageRef { Id = pkg.Id, Version = pkg.Version }); + } + + // Write project manifest + var manifest = new ProjectManifest + { + ProjectName = projectName, + ProjectDir = projectDir, + ProjectFile = Path.GetFileName(projectFile), + Packages = projectPackages, + GeneratedAt = DateTime.UtcNow.ToString("o"), + }; + + var projectsDir = Path.Combine(outputDir, "projects"); + Directory.CreateDirectory(projectsDir); + + // In scan mode, different directories may contain same-named projects. + // Append a short path hash to avoid overwriting manifests. + var manifestFileName = projectName; + if (scanMode) + { + var hashBytes = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(projectFile)); + var hashSuffix = Convert.ToHexString(hashBytes)[..8].ToLowerInvariant(); + manifestFileName = $"{projectName}_{hashSuffix}"; + } + + File.WriteAllText( + Path.Combine(projectsDir, $"{manifestFileName}.json"), + JsonSerializer.Serialize(manifest, jsonOptions)); +} + +Console.WriteLine($"\nDone: {totalProjectsProcessed} project(s) processed, " + + $"{totalPackagesCached} package(s) parsed, " + + $"{totalPackagesSkipped} reused from cache"); +return 0; + +// ============================================================================= +// Export a single package's WinMD data to cache +// ============================================================================= + +void ExportPackageCache(PackageWithWinMd pkg, string cacheDir) +{ + var typesDir = Path.Combine(cacheDir, "types"); + Directory.CreateDirectory(typesDir); + + var allTypes = new List(); + foreach (var file in pkg.WinMdFiles) + { + allTypes.AddRange(WinMdParser.ParseFile(file)); + } + + var typesByNamespace = allTypes + .GroupBy(t => t.Namespace) + .ToDictionary(g => g.Key, g => g.ToList()); + + var namespaces = typesByNamespace.Keys + .Where(ns => !string.IsNullOrEmpty(ns)) + .OrderBy(ns => ns) + .ToList(); + + // Include global (empty) namespace types under a reserved bucket name + var hasGlobalNs = typesByNamespace.ContainsKey(string.Empty) + && typesByNamespace[string.Empty].Count > 0; + const string globalNsBucket = "_GlobalNamespace"; + if (hasGlobalNs) + { + namespaces.Insert(0, globalNsBucket); + } + + // meta.json + var meta = new + { + PackageId = pkg.Id, + Version = pkg.Version, + WinMdFiles = pkg.WinMdFiles + .Select(Path.GetFileName) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(), + TotalTypes = allTypes.Count, + TotalMembers = allTypes.Sum(t => t.Members.Count), + TotalNamespaces = namespaces.Count, + GeneratedAt = DateTime.UtcNow.ToString("o"), + }; + + File.WriteAllText( + Path.Combine(cacheDir, "meta.json"), + JsonSerializer.Serialize(meta, jsonOptions)); + + // namespaces.json + File.WriteAllText( + Path.Combine(cacheDir, "namespaces.json"), + JsonSerializer.Serialize(namespaces, jsonOptions)); + + // types/.json + foreach (var ns in namespaces) + { + var lookupKey = ns == globalNsBucket ? string.Empty : ns; + var types = typesByNamespace[lookupKey]; + var safeFileName = ns.Replace('.', '_') + ".json"; + File.WriteAllText( + Path.Combine(typesDir, safeFileName), + JsonSerializer.Serialize(types, jsonOptions)); + } +} + +// ============================================================================= +// Data Models +// ============================================================================= + +enum TypeKind { Class, Struct, Enum, Interface, Delegate } + +enum MemberKind { Method, Property, Event, Field } + +sealed class WinMdTypeInfo +{ + public required string Namespace { get; init; } + public required string Name { get; init; } + public required string FullName { get; init; } + public required TypeKind Kind { get; init; } + public string? BaseType { get; init; } + public required List Members { get; init; } + public List? EnumValues { get; init; } + public required string SourceFile { get; init; } +} + +sealed class WinMdMemberInfo +{ + public required string Name { get; init; } + public required MemberKind Kind { get; init; } + public required string Signature { get; init; } + public string? ReturnType { get; init; } + public List? Parameters { get; init; } +} + +sealed class WinMdParameterInfo +{ + public required string Name { get; init; } + public required string Type { get; init; } +} + +sealed class ProjectPackageRef +{ + public required string Id { get; init; } + public required string Version { get; init; } +} + +sealed class ProjectManifest +{ + public required string ProjectName { get; init; } + public required string ProjectDir { get; init; } + public required string ProjectFile { get; init; } + public required List Packages { get; init; } + public required string GeneratedAt { get; init; } +} + +// ============================================================================= +// NuGet Resolver — finds packages with WinMD files, returns structured data +// ============================================================================= + +record PackageWithWinMd(string Id, string Version, List WinMdFiles); + +static class NuGetResolver +{ + public static List FindPackagesWithWinMd(string projectDir, string projectFile, string? winAppSdkRuntimePath) + { + var result = new List(); + + // 1. Try project.assets.json (PackageReference — .csproj and modern .vcxproj) + var assetsPath = FindProjectAssetsJson(projectDir); + if (assetsPath is not null) + { + result.AddRange(FindPackagesFromAssets(assetsPath)); + } + + // 2. Try packages.config (older .vcxproj / .csproj using NuGet packages.config) + if (result.Count == 0) + { + var packagesConfig = Path.Combine(projectDir, "packages.config"); + if (File.Exists(packagesConfig)) + { + result.AddRange(FindPackagesFromConfig(packagesConfig, projectDir)); + } + } + + // 3. Project references — parse from .csproj/.vcxproj XML, + // then check each referenced project's bin/ for .winmd build output. + // This is the reliable way to find class libraries that generate WinMD. + result.AddRange(FindWinMdFromProjectReferences(projectFile)); + + // 4. Windows SDK as a synthetic "package" + var sdkWinMd = FindWindowsSdkWinMd(); + if (sdkWinMd.Files.Count > 0) + { + result.Add(new PackageWithWinMd("WindowsSDK", sdkWinMd.Version, sdkWinMd.Files)); + } + + // 5. Installed WinAppSDK runtime as a synthetic "package" + // Useful for Electron/Node.js apps that don't reference WinAppSDK via NuGet. + var runtimeWinMd = FindWinAppSdkRuntimeWinMd(winAppSdkRuntimePath); + if (runtimeWinMd.Files.Count > 0) + { + result.Add(new PackageWithWinMd("WinAppSdkRuntime", runtimeWinMd.Version, runtimeWinMd.Files)); + } + + // Deduplicate by (Id, Version), merging WinMdFiles from multiple sources + return result + .GroupBy(p => (p.Id.ToLowerInvariant(), p.Version.ToLowerInvariant())) + .Select(g => + { + var merged = g.SelectMany(p => p.WinMdFiles) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + var first = g.First(); + return new PackageWithWinMd(first.Id, first.Version, merged); + }) + .ToList(); + } + + /// + /// Parse <ProjectReference> from .csproj/.vcxproj and find .winmd output + /// from each referenced project's bin/ directory. + /// + internal static List FindWinMdFromProjectReferences(string projectFile) + { + var result = new List(); + + try + { + var doc = XDocument.Load(projectFile); + var ns = doc.Root?.Name.Namespace ?? XNamespace.None; + var projectRefs = doc.Descendants(ns + "ProjectReference") + .Select(e => e.Attribute("Include")?.Value) + .Where(v => v is not null) + .ToList(); + + if (projectRefs.Count == 0) + { + return result; + } + + var projectDir = Path.GetDirectoryName(projectFile)!; + + foreach (var refPath in projectRefs) + { + var refFullPath = Path.GetFullPath(Path.Combine(projectDir, refPath!)); + if (!File.Exists(refFullPath)) + { + continue; + } + + var refProjectDir = Path.GetDirectoryName(refFullPath)!; + var refProjectName = Path.GetFileNameWithoutExtension(refFullPath); + var refBinDir = Path.Combine(refProjectDir, "bin"); + + if (!Directory.Exists(refBinDir)) + { + continue; + } + + var winmdFiles = Directory.GetFiles(refBinDir, "*.winmd", SearchOption.AllDirectories) + .Where(f => !Path.GetFileName(f).Equals("Windows.winmd", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + // Deduplicate by filename (same WinMD across Debug/Release/x64/etc.) + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + winmdFiles = winmdFiles + .Where(f => seen.Add(Path.GetFileName(f))) + .ToList(); + + if (winmdFiles.Count > 0) + { + result.Add(new PackageWithWinMd($"ProjectRef.{refProjectName}", "local", winmdFiles)); + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Warning: Failed to parse project references: {ex.Message}"); + } + + return result; + } + + internal static string? FindProjectAssetsJson(string projectDir) + { + // Standard location + var assetsPath = Path.Combine(projectDir, "obj", "project.assets.json"); + if (File.Exists(assetsPath)) + { + return assetsPath; + } + + // Sometimes under platform-specific subdirectories + var objDir = Path.Combine(projectDir, "obj"); + if (Directory.Exists(objDir)) + { + var found = Directory.GetFiles(objDir, "project.assets.json", SearchOption.AllDirectories); + if (found.Length > 0) + { + // Pick the most recently written file to avoid non-deterministic + // selection when multi-targeting creates multiple assets files. + string? bestPath = null; + DateTime bestWriteTime = DateTime.MinValue; + + foreach (var path in found) + { + try + { + var writeTime = File.GetLastWriteTimeUtc(path); + if (writeTime > bestWriteTime) + { + bestWriteTime = writeTime; + bestPath = path; + } + } + catch + { + // Ignore files we cannot access metadata for + } + } + + if (bestPath is not null) + { + return bestPath; + } + } + } + + return null; + } + + internal static List FindPackagesFromAssets(string assetsPath) + { + var result = new List(); + + try + { + using var doc = JsonDocument.Parse(File.ReadAllText(assetsPath)); + var root = doc.RootElement; + + var packageFolders = new List(); + if (root.TryGetProperty("packageFolders", out var folders)) + { + foreach (var folder in folders.EnumerateObject()) + { + packageFolders.Add(folder.Name); + } + } + + if (!root.TryGetProperty("libraries", out var libraries)) + { + return result; + } + + foreach (var lib in libraries.EnumerateObject()) + { + // Only treat libraries with type == "package" as NuGet packages; + // skip project references and other entry types. + if (!lib.Value.TryGetProperty("type", out var typeProp) || + !string.Equals(typeProp.GetString(), "package", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // Key format: "PackageId/Version" + var slashIdx = lib.Name.IndexOf('/'); + if (slashIdx < 0) + { + continue; + } + + var packageId = lib.Name[..slashIdx]; + var version = lib.Name[(slashIdx + 1)..]; + + if (!lib.Value.TryGetProperty("path", out var pathProp)) + { + continue; + } + + var libPath = pathProp.GetString(); + if (libPath is null) + { + continue; + } + + var winmdFiles = new List(); + foreach (var folder in packageFolders) + { + var fullPath = Path.Combine(folder, libPath); + if (!Directory.Exists(fullPath)) + { + continue; + } + + winmdFiles.AddRange( + Directory.GetFiles(fullPath, "*.winmd", SearchOption.AllDirectories)); + } + + // Deduplicate by filename (WinMD is arch-neutral metadata) + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + winmdFiles = winmdFiles + .Where(f => seen.Add(Path.GetFileName(f))) + .ToList(); + + if (winmdFiles.Count > 0) + { + result.Add(new PackageWithWinMd(packageId, version, winmdFiles)); + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Warning: Failed to parse project.assets.json: {ex.Message}"); + } + + return result; + } + + /// + /// Parses packages.config (older NuGet format used by some .vcxproj and legacy .csproj). + /// Looks for a solution-level "packages/" folder or the NuGet global cache. + /// + internal static List FindPackagesFromConfig(string configPath, string projectDir) + { + var result = new List(); + + try + { + var doc = System.Xml.Linq.XDocument.Load(configPath); + var packages = doc.Root?.Elements("package"); + if (packages is null) + { + return result; + } + + // packages.config repos typically have a solution-level "packages/" folder. + // Walk up from project dir to find it. + var packagesFolder = FindSolutionPackagesFolder(projectDir); + + // Also check NuGet global packages cache (respect NUGET_PACKAGES override) + var globalPackages = Environment.GetEnvironmentVariable("NUGET_PACKAGES"); + if (string.IsNullOrWhiteSpace(globalPackages)) + { + globalPackages = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".nuget", "packages"); + } + + foreach (var pkg in packages) + { + var id = pkg.Attribute("id")?.Value; + var version = pkg.Attribute("version")?.Value; + if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(version)) + { + continue; + } + + var winmdFiles = new List(); + + // Check solution-level packages/ folder (format: packages/./) + if (packagesFolder is not null) + { + var pkgDir = Path.Combine(packagesFolder, $"{id}.{version}"); + if (Directory.Exists(pkgDir)) + { + winmdFiles.AddRange( + Directory.GetFiles(pkgDir, "*.winmd", SearchOption.AllDirectories)); + } + } + + // Fallback: NuGet global cache (format: //) + if (winmdFiles.Count == 0 && Directory.Exists(globalPackages)) + { + var pkgDir = Path.Combine(globalPackages, id.ToLowerInvariant(), version); + if (Directory.Exists(pkgDir)) + { + winmdFiles.AddRange( + Directory.GetFiles(pkgDir, "*.winmd", SearchOption.AllDirectories)); + } + } + + // Deduplicate by filename + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + winmdFiles = winmdFiles + .Where(f => seen.Add(Path.GetFileName(f))) + .ToList(); + + if (winmdFiles.Count > 0) + { + result.Add(new PackageWithWinMd(id, version, winmdFiles)); + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Warning: Failed to parse packages.config: {ex.Message}"); + } + + return result; + } + + /// + /// Walk up from project dir to find a solution-level "packages/" folder. + /// + internal static string? FindSolutionPackagesFolder(string startDir) + { + var dir = startDir; + for (var i = 0; i < 5; i++) // Walk up at most 5 levels + { + var packagesDir = Path.Combine(dir, "packages"); + if (Directory.Exists(packagesDir)) + { + return packagesDir; + } + + var parent = Directory.GetParent(dir); + if (parent is null) + { + break; + } + + dir = parent.FullName; + } + + return null; + } + + internal static (List Files, string Version) FindWindowsSdkWinMd() + { + var windowsKitsPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), + "Windows Kits", "10", "UnionMetadata"); + + if (!Directory.Exists(windowsKitsPath)) + { + return ([], "unknown"); + } + + // Filter to version-numbered directories only (skip "Facade" etc.) and + // sort by numeric version, not lexicographically, to pick the highest SDK. + var versionDirs = Directory.GetDirectories(windowsKitsPath) + .Select(d => (Dir: d, Name: Path.GetFileName(d))) + .Where(x => !string.IsNullOrEmpty(x.Name) && char.IsDigit(x.Name[0])) + .Select(x => Version.TryParse(x.Name, out var v) + ? (Dir: x.Dir, Version: v) + : (Dir: (string?)null, Version: (Version?)null)) + .Where(x => x.Dir is not null && x.Version is not null) + .OrderByDescending(x => x.Version) + .Select(x => x.Dir!) + .ToList(); + + foreach (var versionDir in versionDirs) + { + var windowsWinMd = Path.Combine(versionDir, "Windows.winmd"); + if (File.Exists(windowsWinMd)) + { + var version = Path.GetFileName(versionDir); + return ([windowsWinMd], version); + } + } + + return ([], "unknown"); + } + + /// + /// Read WinMD files from the installed WinAppSDK runtime path (discovered via + /// Get-AppxPackage in PowerShell and passed as --winappsdk-runtime argument). + /// The WindowsApps folder is ACL-restricted so C# cannot enumerate it directly. + /// + internal static (List Files, string Version) FindWinAppSdkRuntimeWinMd(string? runtimePath) + { + if (string.IsNullOrEmpty(runtimePath) || !Directory.Exists(runtimePath)) + { + return ([], "unknown"); + } + + try + { + var winmdFiles = Directory.EnumerateFiles(runtimePath, "*.winmd", SearchOption.TopDirectoryOnly) + .ToList(); + + if (winmdFiles.Count > 0) + { + // Extract SDK version from path: ...Microsoft.WindowsAppRuntime.1.8_... -> "1.8" + var dirName = Path.GetFileName(runtimePath); + var prefix = dirName.Split('_')[0]; // "Microsoft.WindowsAppRuntime.1.8" + var sdkVersion = prefix.Length > "Microsoft.WindowsAppRuntime.".Length + ? prefix["Microsoft.WindowsAppRuntime.".Length..] + : dirName; + + return (winmdFiles, sdkVersion); + } + } + catch + { + // Path may be inaccessible; degrade gracefully + } + + return ([], "unknown"); + } +} + +// ============================================================================= +// Signature Type Provider — decodes metadata signatures to readable strings +// ============================================================================= + +sealed class SimpleTypeProvider : ISignatureTypeProvider +{ + public string GetPrimitiveType(PrimitiveTypeCode typeCode) => typeCode switch + { + PrimitiveTypeCode.Boolean => "Boolean", + PrimitiveTypeCode.Byte => "Byte", + PrimitiveTypeCode.SByte => "SByte", + PrimitiveTypeCode.Char => "Char", + PrimitiveTypeCode.Int16 => "Int16", + PrimitiveTypeCode.UInt16 => "UInt16", + PrimitiveTypeCode.Int32 => "Int32", + PrimitiveTypeCode.UInt32 => "UInt32", + PrimitiveTypeCode.Int64 => "Int64", + PrimitiveTypeCode.UInt64 => "UInt64", + PrimitiveTypeCode.Single => "Single", + PrimitiveTypeCode.Double => "Double", + PrimitiveTypeCode.String => "String", + PrimitiveTypeCode.Object => "Object", + PrimitiveTypeCode.Void => "void", + PrimitiveTypeCode.IntPtr => "IntPtr", + PrimitiveTypeCode.UIntPtr => "UIntPtr", + PrimitiveTypeCode.TypedReference => "TypedReference", + _ => typeCode.ToString(), + }; + + public string GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + { + var typeDef = reader.GetTypeDefinition(handle); + var name = reader.GetString(typeDef.Name); + var ns = reader.GetString(typeDef.Namespace); + return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + } + + public string GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + { + var typeRef = reader.GetTypeReference(handle); + var name = reader.GetString(typeRef.Name); + var ns = reader.GetString(typeRef.Namespace); + return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + } + + public string GetSZArrayType(string elementType) => $"{elementType}[]"; + + public string GetArrayType(string elementType, ArrayShape shape) => + $"{elementType}[{new string(',', shape.Rank - 1)}]"; + + public string GetByReferenceType(string elementType) => $"ref {elementType}"; + public string GetPointerType(string elementType) => $"{elementType}*"; + public string GetPinnedType(string elementType) => elementType; + + public string GetGenericInstantiation(string genericType, ImmutableArray typeArguments) + { + var name = genericType; + var backtick = name.IndexOf('`'); + if (backtick >= 0) + { + name = name[..backtick]; + } + + return $"{name}<{string.Join(", ", typeArguments)}>"; + } + + public string GetGenericMethodParameter(object? genericContext, int index) => $"TMethod{index}"; + public string GetGenericTypeParameter(object? genericContext, int index) => $"T{index}"; + public string GetModifiedType(string modifier, string unmodifiedType, bool isRequired) => unmodifiedType; + public string GetFunctionPointerType(MethodSignature signature) => "delegate*"; + + public string GetTypeFromSpecification(MetadataReader reader, object? genericContext, + TypeSpecificationHandle handle, byte rawTypeKind) + { + return reader.GetTypeSpecification(handle).DecodeSignature(this, genericContext); + } +} + +// ============================================================================= +// WinMD Parser — reads WinMD files into structured type info +// ============================================================================= + +static class WinMdParser +{ + public static List ParseFile(string filePath) + { + var types = new List(); + + try + { + using var stream = File.OpenRead(filePath); + using var peReader = new PEReader(stream); + + if (!peReader.HasMetadata) + { + return types; + } + + var reader = peReader.GetMetadataReader(); + var typeProvider = new SimpleTypeProvider(); + + foreach (var typeDefHandle in reader.TypeDefinitions) + { + var typeDef = reader.GetTypeDefinition(typeDefHandle); + var name = reader.GetString(typeDef.Name); + var ns = reader.GetString(typeDef.Namespace); + + if (ShouldSkipType(name, typeDef)) + { + continue; + } + + var kind = DetermineTypeKind(reader, typeDef); + var baseType = GetBaseTypeName(reader, typeDef); + var members = ParseMembers(reader, typeDef, typeProvider); + var enumValues = kind == TypeKind.Enum ? ParseEnumValues(reader, typeDef) : null; + var fullName = string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + + types.Add(new WinMdTypeInfo + { + Namespace = ns, + Name = name, + FullName = fullName, + Kind = kind, + BaseType = baseType, + Members = members, + EnumValues = enumValues, + SourceFile = Path.GetFileName(filePath), + }); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Warning: Failed to parse {filePath}: {ex.Message}"); + } + + return types; + } + + internal static bool ShouldSkipType(string name, TypeDefinition typeDef) + { + if (string.IsNullOrEmpty(name) || name == "" || name.StartsWith('<')) + { + return true; + } + + var visibility = typeDef.Attributes & TypeAttributes.VisibilityMask; + return visibility != TypeAttributes.Public && visibility != TypeAttributes.NestedPublic; + } + + internal static TypeKind DetermineTypeKind(MetadataReader reader, TypeDefinition typeDef) + { + if ((typeDef.Attributes & TypeAttributes.Interface) != 0) + { + return TypeKind.Interface; + } + + var baseType = GetBaseTypeName(reader, typeDef); + return baseType switch + { + "System.Enum" => TypeKind.Enum, + "System.ValueType" => TypeKind.Struct, + "System.MulticastDelegate" or "System.Delegate" => TypeKind.Delegate, + _ => TypeKind.Class, + }; + } + + private static string? GetBaseTypeName(MetadataReader reader, TypeDefinition typeDef) + { + if (typeDef.BaseType.IsNil) + { + return null; + } + + return typeDef.BaseType.Kind switch + { + HandleKind.TypeDefinition => GetTypeDefName(reader, (TypeDefinitionHandle)typeDef.BaseType), + HandleKind.TypeReference => GetTypeRefName(reader, (TypeReferenceHandle)typeDef.BaseType), + _ => null, + }; + } + + private static string GetTypeDefName(MetadataReader reader, TypeDefinitionHandle handle) + { + var td = reader.GetTypeDefinition(handle); + var ns = reader.GetString(td.Namespace); + var name = reader.GetString(td.Name); + return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + } + + private static string GetTypeRefName(MetadataReader reader, TypeReferenceHandle handle) + { + var tr = reader.GetTypeReference(handle); + var ns = reader.GetString(tr.Namespace); + var name = reader.GetString(tr.Name); + return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + } + + private static List ParseMembers( + MetadataReader reader, TypeDefinition typeDef, SimpleTypeProvider typeProvider) + { + var members = new List(); + + // Collect property/event accessor methods so we can skip them in the methods loop + var accessorMethods = new HashSet(); + foreach (var propHandle in typeDef.GetProperties()) + { + var accessors = reader.GetPropertyDefinition(propHandle).GetAccessors(); + if (!accessors.Getter.IsNil) accessorMethods.Add(accessors.Getter); + if (!accessors.Setter.IsNil) accessorMethods.Add(accessors.Setter); + } + + foreach (var eventHandle in typeDef.GetEvents()) + { + var accessors = reader.GetEventDefinition(eventHandle).GetAccessors(); + if (!accessors.Adder.IsNil) accessorMethods.Add(accessors.Adder); + if (!accessors.Remover.IsNil) accessorMethods.Add(accessors.Remover); + if (!accessors.Raiser.IsNil) accessorMethods.Add(accessors.Raiser); + } + + // Methods + foreach (var methodHandle in typeDef.GetMethods()) + { + if (accessorMethods.Contains(methodHandle)) + { + continue; + } + + var method = reader.GetMethodDefinition(methodHandle); + var methodName = reader.GetString(method.Name); + + if (methodName.StartsWith('.') || methodName.StartsWith('<')) + { + continue; + } + + if ((method.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public) + { + continue; + } + + try + { + var sig = method.DecodeSignature(typeProvider, null); + var parameters = GetMethodParameters(reader, method, sig); + var paramStr = string.Join(", ", parameters.Select(p => $"{p.Type} {p.Name}")); + + members.Add(new WinMdMemberInfo + { + Name = methodName, + Kind = MemberKind.Method, + Signature = $"{sig.ReturnType} {methodName}({paramStr})", + ReturnType = sig.ReturnType, + Parameters = parameters, + }); + } + catch + { + members.Add(new WinMdMemberInfo + { + Name = methodName, + Kind = MemberKind.Method, + Signature = $"{methodName}(/* signature not decodable */)", + }); + } + } + + // Properties + foreach (var propHandle in typeDef.GetProperties()) + { + var prop = reader.GetPropertyDefinition(propHandle); + var propName = reader.GetString(prop.Name); + + try + { + var propSig = prop.DecodeSignature(typeProvider, null); + var propType = propSig.ReturnType; + var accessors = prop.GetAccessors(); + + var hasGetter = false; + if (!accessors.Getter.IsNil) + { + var getterDef = reader.GetMethodDefinition(accessors.Getter); + if ((getterDef.Attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public) + { + hasGetter = true; + } + } + + var hasSetter = false; + if (!accessors.Setter.IsNil) + { + var setterDef = reader.GetMethodDefinition(accessors.Setter); + if ((setterDef.Attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public) + { + hasSetter = true; + } + } + + // Skip properties where neither accessor is public + if (!hasGetter && !hasSetter) + { + continue; + } + var accessStr = (hasGetter, hasSetter) switch + { + (true, true) => "{ get; set; }", + (true, false) => "{ get; }", + (false, true) => "{ set; }", + _ => "{ }", + }; + + members.Add(new WinMdMemberInfo + { + Name = propName, + Kind = MemberKind.Property, + Signature = $"{propType} {propName} {accessStr}", + ReturnType = propType, + }); + } + catch + { + members.Add(new WinMdMemberInfo + { + Name = propName, + Kind = MemberKind.Property, + Signature = $"/* type not decodable */ {propName}", + }); + } + } + + // Events + foreach (var eventHandle in typeDef.GetEvents()) + { + var evt = reader.GetEventDefinition(eventHandle); + var evtName = reader.GetString(evt.Name); + var accessors = evt.GetAccessors(); + + var isPublicEvent = false; + if (!accessors.Adder.IsNil) + { + var adder = reader.GetMethodDefinition(accessors.Adder); + if ((adder.Attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public) + { + isPublicEvent = true; + } + } + + if (!isPublicEvent && !accessors.Remover.IsNil) + { + var remover = reader.GetMethodDefinition(accessors.Remover); + if ((remover.Attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public) + { + isPublicEvent = true; + } + } + + if (!isPublicEvent) + { + continue; + } + + var evtType = GetHandleTypeName(reader, evt.Type); + + members.Add(new WinMdMemberInfo + { + Name = evtName, + Kind = MemberKind.Event, + Signature = $"event {evtType} {evtName}", + ReturnType = evtType, + }); + } + + return members; + } + + private static List GetMethodParameters( + MetadataReader reader, MethodDefinition method, MethodSignature sig) + { + var parameters = new List(); + var paramHandles = method.GetParameters().ToList(); + var paramNames = new List(); + + foreach (var ph in paramHandles) + { + var param = reader.GetParameter(ph); + if (param.SequenceNumber > 0) + { + paramNames.Add(reader.GetString(param.Name)); + } + } + + for (var i = 0; i < sig.ParameterTypes.Length; i++) + { + parameters.Add(new WinMdParameterInfo + { + Name = i < paramNames.Count ? paramNames[i] : $"arg{i}", + Type = sig.ParameterTypes[i], + }); + } + + return parameters; + } + + internal static List ParseEnumValues(MetadataReader reader, TypeDefinition typeDef) + { + var values = new List(); + + foreach (var fieldHandle in typeDef.GetFields()) + { + var field = reader.GetFieldDefinition(fieldHandle); + var fieldName = reader.GetString(field.Name); + + if (fieldName == "value__") + { + continue; + } + + if ((field.Attributes & FieldAttributes.FieldAccessMask) == FieldAttributes.Public && + (field.Attributes & FieldAttributes.Static) != 0) + { + values.Add(fieldName); + } + } + + return values; + } + + private static string GetHandleTypeName(MetadataReader reader, EntityHandle handle) => handle.Kind switch + { + HandleKind.TypeDefinition => GetTypeDefName(reader, (TypeDefinitionHandle)handle), + HandleKind.TypeReference => GetTypeRefName(reader, (TypeReferenceHandle)handle), + HandleKind.TypeSpecification => DecodeTypeSpecification(reader, (TypeSpecificationHandle)handle), + _ => "unknown", + }; + + private static string DecodeTypeSpecification(MetadataReader reader, TypeSpecificationHandle handle) + { + try + { + var typeSpec = reader.GetTypeSpecification(handle); + return typeSpec.DecodeSignature(new SimpleTypeProvider(), null); + } + catch + { + return "unknown"; + } + } +}