mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request Adds a new Copilot agent skill (`winmd-api-search`) that lets AI agents discover and explore Windows desktop APIs by searching a local cache of WinMD metadata. The skill covers Windows Platform SDK, WinAppSDK/WinUI, NuGet package WinMDs, and project-output WinMDs — providing full API surface details (types, members, enumeration values, namespaces) without needing external documentation lookups. **Key components:** - `.github/skills/winmd-api-search/SKILL.md` — Skill definition with usage instructions, search/detail workflows, and scoring guidance - `scripts/Invoke-WinMdQuery.ps1` — PowerShell query engine supporting actions: `search`, `type`, `members`, `enums`, `namespaces`, `stats`, `projects` - `scripts/Update-WinMdCache.ps1` — Orchestrator that builds the C# cache generator, discovers project files, and runs the generator - `scripts/cache-generator/CacheGenerator.csproj` + `Program.cs` — .NET console app using `System.Reflection.Metadata` to parse WinMD files from NuGet packages, project references, Windows SDK, and packages.config into per-package JSON caches - `scripts/cache-generator/Directory.Build.props`, `Directory.Build.targets`, `Directory.Packages.props` — Empty isolation files to prevent repo-level Central Package Management and build targets from interfering with this standalone tool <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] Closes: #xxx <!-- Replace with issue number if applicable --> - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass — N/A: This is an offline agent skill (PowerShell + standalone .NET tool) with no integration into the main product build or runtime. Validated manually by running the cache generator across multiple project contexts (ColorPickerUI, CmdPal.UI, runner, ImageResizer, etc.) and exercising all query actions. - [ ] **Localization:** All end-user-facing strings can be localized — N/A: No end-user-facing strings; this is an internal developer/agent tool - [ ] **Dev docs:** Added/updated — The SKILL.md itself serves as the documentation - [ ] **New binaries:** Added on the required places — N/A: The cache generator is a standalone dev-time tool, not shipped in the installer - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: N/A <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments ### Cache Generator (`Program.cs`, ~1000 lines) A self-contained .NET console app that: 1. **Discovers WinMD sources** from four channels: - `project.assets.json` (PackageReference — modern .csproj/.vcxproj) - `packages.config` (legacy NuGet format) - `<ProjectReference>` bin/ output (class libraries producing `.winmd`) - Windows SDK `UnionMetadata/` (highest installed version) 2. **Parses WinMD files** using `System.Reflection.Metadata` / `PEReader` to extract: - Types (classes, structs, interfaces, enums, delegates) with full namespace - Members (methods with decoded signatures/parameters, properties with accessors, events) - Enum values - Base types and type kinds 3. **Outputs per-package JSON** under `Generated Files/winmd-cache/`: - `packages/<Id>/<Version>/meta.json` — package summary (type/member/namespace counts) - `packages/<Id>/<Version>/namespaces.json` — ordered namespace list - `packages/<Id>/<Version>/types/<Namespace>.json` — full type detail per namespace - `projects/<ProjectName>.json` — maps each project to its package set 4. **Deduplicates** at the package level — if a package+version is already cached, it's skipped on subsequent runs. ### Build Isolation Three empty MSBuild files (`Directory.Build.props`, `Directory.Build.targets`, `Directory.Packages.props`) in the cache-generator folder prevent the repo's Central Package Management and shared build configuration from interfering with this standalone tool. ### Query Engine (`Invoke-WinMdQuery.ps1`) Supports seven actions: `search` (fuzzy text search across types/members), `type` (full detail for a specific type), `members` (filtered members of a type), `enums` (enumeration values), `namespaces` (list all namespaces), `stats` (cache statistics), and `projects` (list cached projects with their packages). <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed 1. **Cache generation:** Ran `Update-WinMdCache.ps1` across 310+ project files in the repo — 8 packages parsed, 316 reused from cache, all completed without errors 2. **Query testing on multiple projects:** - `ColorPickerUI` — verified Windows SDK baseline (7,023 types) - `Microsoft.CmdPal.UI.ViewModels` (after restore) — verified 13 packages, 49,799 types, 112,131 members including WinAppSDK, AdaptiveCards, CsWinRT, Win32Metadata - `runner` (C++ vcxproj) — verified packages.config fallback path - `ImageResizerExt` — verified project reference WinMD discovery 3. **All seven query actions validated:** `stats`, `search`, `namespaces`, `type`, `enums`, `members`, `projects` — all returned correct results 4. **Spell-check compliance:** SKILL.md vocabulary reviewed against repo's check-spelling dictionaries; replaced flagged words with standard alternatives --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1223 lines
43 KiB
C#
1223 lines
43 KiB
C#
// 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 <project-dir> <output-dir>
|
|
// CacheGenerator --scan <root-dir> <output-dir>
|
|
|
|
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 <path> 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 <project-dir> <output-dir>");
|
|
Console.Error.WriteLine(" CacheGenerator --scan <root-dir> <output-dir>");
|
|
Console.Error.WriteLine(" CacheGenerator --winappsdk-runtime <path> <project-dir> <output-dir>");
|
|
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<string>();
|
|
|
|
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<ProjectPackageRef>();
|
|
|
|
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<WinMdTypeInfo>();
|
|
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/<Namespace>.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<WinMdMemberInfo> Members { get; init; }
|
|
public List<string>? 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<WinMdParameterInfo>? 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<ProjectPackageRef> 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<string> WinMdFiles);
|
|
|
|
static class NuGetResolver
|
|
{
|
|
public static List<PackageWithWinMd> FindPackagesWithWinMd(string projectDir, string projectFile, string? winAppSdkRuntimePath)
|
|
{
|
|
var result = new List<PackageWithWinMd>();
|
|
|
|
// 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 <ProjectReference> 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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse <ProjectReference> from .csproj/.vcxproj and find .winmd output
|
|
/// from each referenced project's bin/ directory.
|
|
/// </summary>
|
|
internal static List<PackageWithWinMd> FindWinMdFromProjectReferences(string projectFile)
|
|
{
|
|
var result = new List<PackageWithWinMd>();
|
|
|
|
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<string>(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<PackageWithWinMd> FindPackagesFromAssets(string assetsPath)
|
|
{
|
|
var result = new List<PackageWithWinMd>();
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(File.ReadAllText(assetsPath));
|
|
var root = doc.RootElement;
|
|
|
|
var packageFolders = new List<string>();
|
|
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<string>();
|
|
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<string>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
internal static List<PackageWithWinMd> FindPackagesFromConfig(string configPath, string projectDir)
|
|
{
|
|
var result = new List<PackageWithWinMd>();
|
|
|
|
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<string>();
|
|
|
|
// Check solution-level packages/ folder (format: packages/<id>.<version>/)
|
|
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: <id>/<version>/)
|
|
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<string>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Walk up from project dir to find a solution-level "packages/" folder.
|
|
/// </summary>
|
|
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<string> 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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
internal static (List<string> 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<string, object?>
|
|
{
|
|
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<string> 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<string> 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<WinMdTypeInfo> ParseFile(string filePath)
|
|
{
|
|
var types = new List<WinMdTypeInfo>();
|
|
|
|
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 == "<Module>" || 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<WinMdMemberInfo> ParseMembers(
|
|
MetadataReader reader, TypeDefinition typeDef, SimpleTypeProvider typeProvider)
|
|
{
|
|
var members = new List<WinMdMemberInfo>();
|
|
|
|
// Collect property/event accessor methods so we can skip them in the methods loop
|
|
var accessorMethods = new HashSet<MethodDefinitionHandle>();
|
|
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<WinMdParameterInfo> GetMethodParameters(
|
|
MetadataReader reader, MethodDefinition method, MethodSignature<string> sig)
|
|
{
|
|
var parameters = new List<WinMdParameterInfo>();
|
|
var paramHandles = method.GetParameters().ToList();
|
|
var paramNames = new List<string>();
|
|
|
|
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<string> ParseEnumValues(MetadataReader reader, TypeDefinition typeDef)
|
|
{
|
|
var values = new List<string>();
|
|
|
|
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";
|
|
}
|
|
}
|
|
}
|