diff --git a/src/Tests/ProjectionWriterTest/Helpers/TypeFilteringMetadata.cs b/src/Tests/ProjectionWriterTest/Helpers/TypeFilteringMetadata.cs
new file mode 100644
index 0000000000..358306a05b
--- /dev/null
+++ b/src/Tests/ProjectionWriterTest/Helpers/TypeFilteringMetadata.cs
@@ -0,0 +1,138 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using System.Linq;
+using AsmResolver.DotNet;
+using AsmResolver.DotNet.Signatures;
+using AsmResolver.PE.DotNet.Metadata.Tables;
+
+namespace ProjectionWriterTest.Helpers;
+
+///
+/// Creates synthetic metadata whose excluded types collide with a selected runtime class's name.
+///
+internal static class TypeFilteringMetadata
+{
+ public static string Create(string directory)
+ {
+ ModuleDefinition module = new("Contoso.winmd")
+ {
+ RuntimeVersion = "WindowsRuntime 1.4"
+ };
+
+ _ = new AssemblyDefinition("Contoso", new Version(255, 255, 255, 255))
+ {
+ Modules = { module },
+ Attributes = AssemblyAttributes.ContentWindowsRuntime
+ };
+
+ AssemblyReference corlib = (AssemblyReference)module.CorLibTypeFactory.CorLibScope;
+ corlib.Version = new Version(255, 255, 255, 255);
+
+ AssemblyReference foundation = new("Windows.Foundation.FoundationContract", new Version(255, 255, 255, 255))
+ {
+ Attributes = AssemblyAttributes.ContentWindowsRuntime
+ };
+
+ TypeSignature systemType = new TypeReference(module, corlib, "System", "Type").ToTypeSignature(false);
+
+ CustomAttribute Attribute(string name, params CustomAttributeArgument[] arguments)
+ {
+ TypeReference attributeType = new(module, foundation, "Windows.Foundation.Metadata", name);
+ MemberReference constructor = new(attributeType, ".ctor", MethodSignature.CreateInstance(
+ module.CorLibTypeFactory.Void, arguments.Select(static argument => argument.ArgumentType).ToArray()));
+
+ return new CustomAttribute(constructor, new CustomAttributeSignature(arguments));
+ }
+
+ void AddGuid(TypeDefinition type, Guid iid)
+ {
+ byte[] bytes = iid.ToByteArray();
+
+ type.CustomAttributes.Add(Attribute("GuidAttribute",
+ new(module.CorLibTypeFactory.UInt32, BitConverter.ToUInt32(bytes, 0)),
+ new(module.CorLibTypeFactory.UInt16, BitConverter.ToUInt16(bytes, 4)),
+ new(module.CorLibTypeFactory.UInt16, BitConverter.ToUInt16(bytes, 6)),
+ new(module.CorLibTypeFactory.Byte, bytes[8]),
+ new(module.CorLibTypeFactory.Byte, bytes[9]),
+ new(module.CorLibTypeFactory.Byte, bytes[10]),
+ new(module.CorLibTypeFactory.Byte, bytes[11]),
+ new(module.CorLibTypeFactory.Byte, bytes[12]),
+ new(module.CorLibTypeFactory.Byte, bytes[13]),
+ new(module.CorLibTypeFactory.Byte, bytes[14]),
+ new(module.CorLibTypeFactory.Byte, bytes[15])));
+ }
+
+ TypeDefinition AddInterface(string ns, string name, Guid iid)
+ {
+ TypeDefinition type = new(ns, name,
+ TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract | TypeAttributes.WindowsRuntime);
+
+ module.TopLevelTypes.Add(type);
+ AddGuid(type, iid);
+
+ return type;
+ }
+
+ void AddMethod(TypeDefinition type, string name, TypeSignature returnType, params TypeSignature[] parameters)
+ {
+ type.Methods.Add(new MethodDefinition(name,
+ MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.NewSlot | MethodAttributes.HideBySig,
+ MethodSignature.CreateInstance(returnType, parameters)));
+ }
+
+ TypeDefinition AddClass(string ns, string name, Guid defaultIid, Guid staticIid)
+ {
+ TypeDefinition type = new(ns, name,
+ TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.WindowsRuntime,
+ module.CorLibTypeFactory.Object.Type);
+
+ module.TopLevelTypes.Add(type);
+
+ TypeDefinition defaultInterface = AddInterface(ns, $"I{name}", defaultIid);
+ defaultInterface.CustomAttributes.Add(Attribute("ExclusiveToAttribute", new CustomAttributeArgument(systemType, type.ToTypeSignature())));
+ AddMethod(defaultInterface, "GetValue", module.CorLibTypeFactory.Object);
+
+ InterfaceImplementation implementation = new(defaultInterface);
+ implementation.CustomAttributes.Add(Attribute("DefaultAttribute"));
+ type.Interfaces.Add(implementation);
+
+ TypeDefinition statics = AddInterface(ns, $"I{name}Statics", staticIid);
+ statics.CustomAttributes.Add(Attribute("ExclusiveToAttribute", new CustomAttributeArgument(systemType, type.ToTypeSignature())));
+ AddMethod(statics, "GetStaticValue", module.CorLibTypeFactory.Int32);
+ type.CustomAttributes.Add(Attribute("StaticAttribute",
+ new(systemType, statics.ToTypeSignature()),
+ new(module.CorLibTypeFactory.UInt32, 1u)));
+
+ return defaultInterface;
+ }
+
+ _ = AddClass("Contoso", "User",
+ new Guid("A3DD8A5E-90C1-4E15-B3A8-95444C622501"),
+ new Guid("A3DD8A5E-90C1-4E15-B3A8-95444C622502"));
+ _ = AddClass("Contoso", "User2",
+ new Guid("A3DD8A5E-90C1-4E15-B3A8-95444C622503"),
+ new Guid("A3DD8A5E-90C1-4E15-B3A8-95444C622504"));
+ TypeDefinition omittedInterface = AddClass("Contoso.UserProfile", "UserSetupManager",
+ new Guid("A3DD8A5E-90C1-4E15-B3A8-95444C622505"),
+ new Guid("A3DD8A5E-90C1-4E15-B3A8-95444C622506"));
+
+ // These signatures are intentionally unsupported. Excluding their declaring types from the
+ // reference projection must also exclude them when regenerating the implementation.
+ TypeDefinition propertyValue = AddInterface("Windows.Foundation", "IPropertyValue",
+ new Guid("4BD682DD-7554-40E9-9A9B-82654EDE7E62"));
+ TypeDefinition asyncOperation = AddInterface("Windows.Foundation", "IAsyncOperation`1",
+ new Guid("9FC2B0BB-E446-44E2-AA61-9CAB8F636AF2"));
+ asyncOperation.GenericParameters.Add(new GenericParameter("T"));
+
+ AddMethod(omittedInterface, "SetProperty", module.CorLibTypeFactory.Void, propertyValue.ToTypeSignature());
+ AddMethod(omittedInterface, "GetPropertyAsync", new GenericInstanceTypeSignature(asyncOperation, false, [propertyValue.ToTypeSignature()]));
+
+ string path = Path.Combine(directory, "Contoso.winmd");
+ module.Write(path);
+
+ return path;
+ }
+}
diff --git a/src/Tests/ProjectionWriterTest/ProjectionWriterTest.csproj b/src/Tests/ProjectionWriterTest/ProjectionWriterTest.csproj
index 303e803f02..157a8426fa 100644
--- a/src/Tests/ProjectionWriterTest/ProjectionWriterTest.csproj
+++ b/src/Tests/ProjectionWriterTest/ProjectionWriterTest.csproj
@@ -52,6 +52,8 @@
match the path published as assembly metadata below.
-->
+
+
+
+
+
+ {
+ ProjectionWriter.Run(new ProjectionWriterOptions
+ {
+ InputPaths = [inputPath],
+ OutputFolder = outputFolder,
+ IncludeTypes = ["Contoso.User"],
+ ReferenceProjection = referenceProjection
+ });
+
+ AssertSelectedTypes(outputFolder);
+ });
+ }
+
+ [TestMethod]
+ public void ReferenceProjection_PrefixExclusionsOmitUnsupportedTypes()
+ {
+ WithMetadata((inputPath, outputFolder) =>
+ {
+ ProjectionWriter.Run(new ProjectionWriterOptions
+ {
+ InputPaths = [inputPath],
+ OutputFolder = outputFolder,
+ Include = ["Contoso"],
+ Exclude = ["Contoso.User2", "Contoso.IUser2", "Contoso.UserProfile"],
+ ReferenceProjection = true
+ });
+
+ AssertSelectedTypes(outputFolder);
+ });
+ }
+
+ [TestMethod]
+ public void ExactTypeIncludes_StillEmitExclusiveFactoryInfrastructure()
+ {
+ WithMetadata((inputPath, outputFolder) =>
+ {
+ ProjectionWriter.Run(new ProjectionWriterOptions
+ {
+ InputPaths = [inputPath],
+ OutputFolder = outputFolder,
+ IncludeTypes = ["Contoso.User"]
+ });
+
+ string source = File.ReadAllText(Path.Combine(outputFolder, "Contoso.cs"));
+ string iids = File.ReadAllText(Path.Combine(outputFolder, "GeneratedInterfaceIIDs.cs"));
+
+ StringAssert.Contains(source, "GetValue");
+ StringAssert.Contains(source, "GetStaticValue");
+ StringAssert.Contains(source, "IUserStaticsMethods");
+ StringAssert.Contains(iids, "IUserStatics");
+ StringAssert.Contains(iids, "IUser");
+ });
+ }
+
+ private static void AssertSelectedTypes(string outputFolder)
+ {
+ string source = File.ReadAllText(Path.Combine(outputFolder, "Contoso.cs"));
+
+ StringAssert.Contains(source, "class User");
+ Assert.IsFalse(source.Contains("User2", StringComparison.Ordinal));
+ Assert.IsFalse(File.Exists(Path.Combine(outputFolder, "Contoso.UserProfile.cs")));
+
+ string allSources = string.Join(Environment.NewLine, Directory.GetFiles(outputFolder, "*.cs").Select(File.ReadAllText));
+
+ Assert.IsFalse(allSources.Contains("UserSetupManager", StringComparison.Ordinal));
+ Assert.IsFalse(allSources.Contains("Windows.Foundation.IPropertyValue", StringComparison.Ordinal));
+ }
+
+ private static void WithMetadata(Action action)
+ {
+ string directory = Directory.CreateTempSubdirectory("ProjectionTypeFilteringTest_").FullName;
+
+ try
+ {
+ action(TypeFilteringMetadata.Create(directory), Path.Combine(directory, "Generated"));
+ }
+ finally
+ {
+ Directory.Delete(directory, recursive: true);
+ }
+ }
+}
diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs
index e3bbc1437b..d453a80efd 100644
--- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs
+++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs
@@ -106,6 +106,7 @@ private static void BuildWriterOptions(
componentAssemblyNames = [];
List includes = [];
+ List includeTypes = [];
List excludes = [];
List winmdInputs = [];
@@ -181,7 +182,7 @@ private static void BuildWriterOptions(
continue;
}
- includes.Add(type.FullName);
+ includeTypes.Add(type.FullName);
hasTypesToProject = true;
}
}
@@ -244,7 +245,8 @@ private static void BuildWriterOptions(
foreach (TypeDefinition exportedType in moduleDefinition.TopLevelTypes)
{
- includes.Add(exportedType.FullName);
+ // Reference assemblies describe exact type identities, not namespace prefixes
+ includeTypes.Add(exportedType.FullName);
hasTypesToProject = true;
}
}
@@ -285,6 +287,7 @@ private static void BuildWriterOptions(
InputPaths = winmdInputs,
OutputFolder = outputFolder,
Include = includes,
+ IncludeTypes = includeTypes,
Exclude = excludes,
Component = componentMode,
ComponentImplementationAssemblyPaths = componentImplementationAssemblies,
diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs
index 71c1ecd44a..c29f7965a3 100644
--- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs
+++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs
@@ -259,7 +259,7 @@ internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGe
// Phase 4: Custom additions to namespaces
_token.ThrowIfCancellationRequested();
- if (_settings.AdditionFilter.Includes(ns))
+ if (_settings.AdditionFilter.IncludesNamespace(ns))
{
foreach (Addition addition in Additions.EnumerateByNamespace(ns))
{
diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Resources.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Resources.cs
index 3e69d3ef89..f411556e9a 100644
--- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Resources.cs
+++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Resources.cs
@@ -35,7 +35,7 @@ private void WriteBaseStrings()
// Skip ComInteropExtensions if Windows is not included
string fileName = resName[(resName.IndexOf(ResourcesBaseSegment, StringComparison.Ordinal) + ResourcesBaseSegment.Length)..];
- if (fileName == "ComInteropExtensions.cs" && !_settings.Filter.Includes("Windows"))
+ if (fileName == "ComInteropExtensions.cs" && !_settings.Filter.IncludesNamespace("Windows"))
{
continue;
}
diff --git a/src/WinRT.Projection.Writer/Generation/Settings.cs b/src/WinRT.Projection.Writer/Generation/Settings.cs
index f5c2dc29c8..4f7ff74b71 100644
--- a/src/WinRT.Projection.Writer/Generation/Settings.cs
+++ b/src/WinRT.Projection.Writer/Generation/Settings.cs
@@ -57,10 +57,15 @@ internal sealed class Settings
public int MaxDegreesOfParallelism { get; init; } = -1;
///
- /// Gets the namespace prefixes to include in projection (when empty, all namespaces are included).
+ /// Gets the namespace or type-name prefixes to include in the projection.
///
public HashSet Include { get; } = [];
+ ///
+ /// Gets the fully qualified type names to include in the projection, matched exactly.
+ ///
+ public HashSet IncludeTypes { get; } = [];
+
///
/// Gets the namespace prefixes to exclude from projection.
///
@@ -72,7 +77,7 @@ internal sealed class Settings
public HashSet AdditionExclude { get; } = [];
///
- /// Gets the compiled type-name filter built from and .
+ /// Gets the compiled type-name filter built from , , and .
/// Only valid after has been called.
///
///
@@ -85,7 +90,8 @@ public TypeFilter Filter
}
///
- /// Gets the compiled type-name filter built from and , used for namespace-additions resources only.
+ /// Gets the compiled filter used for namespace-additions resources only.
+ /// Exact type includes keep an otherwise empty filter from including all namespaces, but do not match namespaces themselves.
/// Only valid after has been called.
///
///
@@ -141,8 +147,8 @@ public void MakeReadOnly()
throw WellKnownProjectionWriterExceptions.SettingsAlreadyReadOnly();
}
- Filter = new TypeFilter(Include, Exclude);
- AdditionFilter = new TypeFilter(Include, AdditionExclude);
+ Filter = new TypeFilter(Include, Exclude, IncludeTypes);
+ AdditionFilter = new TypeFilter(Include, AdditionExclude, IncludeTypes);
_isReadOnly = true;
}
}
diff --git a/src/WinRT.Projection.Writer/Helpers/TypeFilter.cs b/src/WinRT.Projection.Writer/Helpers/TypeFilter.cs
index ef66d12489..c47fddcc73 100644
--- a/src/WinRT.Projection.Writer/Helpers/TypeFilter.cs
+++ b/src/WinRT.Projection.Writer/Helpers/TypeFilter.cs
@@ -10,6 +10,7 @@ namespace WindowsRuntime.ProjectionWriter.Helpers;
///
/// Include/exclude type filter using longest-prefix-match semantics: type/namespace is checked
/// against each prefix in the include/exclude lists, and the longest matching prefix wins.
+/// Exact type includes match only the specified type, not namespaces or other type-name prefixes.
///
///
/// The semantics are:
@@ -26,28 +27,53 @@ internal sealed class TypeFilter
{
private readonly List _include;
private readonly List _exclude;
+ private readonly HashSet _includeTypes;
///
/// Initializes a new with the given include and exclude prefix lists.
///
/// The include prefixes (a type matches if any prefix matches).
/// The exclude prefixes (a type is rejected if any prefix matches and no longer include prefix wins).
- public TypeFilter(IEnumerable include, IEnumerable exclude)
+ /// Optional fully qualified type names to include, matched exactly.
+ public TypeFilter(IEnumerable include, IEnumerable exclude, IEnumerable? includeTypes = null)
{
_include = [.. include.OrderByDescending(s => s.Length)];
_exclude = [.. exclude.OrderByDescending(s => s.Length)];
+ _includeTypes = new HashSet(includeTypes ?? [], StringComparer.Ordinal);
}
///
/// Returns whether the given type name passes the include/exclude filter.
+ /// Exact type includes win over shorter prefixes, but not an identical exclude.
+ ///
+ public bool Includes(string fullName)
+ {
+ if (_includeTypes.Contains(fullName))
+ {
+ return !_exclude.Contains(fullName);
+ }
+
+ return IncludesPrefix(fullName);
+ }
+
+ ///
+ /// Returns whether a namespace passes the prefix filter, without matching exact type includes.
+ ///
+ public bool IncludesNamespace(string ns)
+ {
+ return IncludesPrefix(ns);
+ }
+
+ ///
+ /// Returns whether the given name passes the include/exclude prefix filter.
/// Rules are sorted by descending prefix length (with excludes winning ties over includes);
/// the first matching rule wins. Match semantics split the full type name into
/// namespace.typeName parts and treat the rule prefix as either a namespace-prefix or
/// a namespace + typename-prefix.
///
- public bool Includes(string fullName)
+ private bool IncludesPrefix(string fullName)
{
- if (_include.Count == 0 && _exclude.Count == 0)
+ if (_include.Count == 0 && _exclude.Count == 0 && _includeTypes.Count == 0)
{
return true;
}
@@ -115,7 +141,7 @@ public bool Includes(string fullName)
}
}
- // No rule matched. Since at least one rule exists (the both-empty case returned true
+ // No rule matched. Since at least one rule exists (the all-empty case returned true
// above), default to exclude. This means an excludes-only configuration (no includes)
// projects nothing rather than everything-but-excluded.
return false;
diff --git a/src/WinRT.Projection.Writer/ProjectionWriter.cs b/src/WinRT.Projection.Writer/ProjectionWriter.cs
index e07785cd8b..dc2177ca6f 100644
--- a/src/WinRT.Projection.Writer/ProjectionWriter.cs
+++ b/src/WinRT.Projection.Writer/ProjectionWriter.cs
@@ -55,6 +55,7 @@ public static void Run(ProjectionWriterOptions options)
settings.Input.UnionWith(options.InputPaths);
settings.Include.UnionWith(options.Include);
+ settings.IncludeTypes.UnionWith(options.IncludeTypes);
settings.Exclude.UnionWith(options.Exclude);
settings.AdditionExclude.UnionWith(options.AdditionExclude);
settings.ComponentImplementationAssemblies.UnionWith(options.ComponentImplementationAssemblyPaths);
diff --git a/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs b/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs
index 03a56008c0..6f10285f0e 100644
--- a/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs
+++ b/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs
@@ -27,12 +27,19 @@ public sealed class ProjectionWriterOptions
public required string OutputFolder { get; init; }
///
- /// Optional list of namespace prefixes to include in the projection.
+ /// Optional list of namespace or type-name prefixes to include in the projection.
///
public IReadOnlyList Include { get; init; } = [];
///
- /// Optional list of namespace prefixes to exclude from the projection.
+ /// Optional list of fully qualified type names to include in the projection, matched exactly.
+ /// These entries do not include namespace additions or other types with the same name prefix.
+ /// An exact include wins over shorter exclude prefixes; an identical exclude wins the tie.
+ ///
+ public IReadOnlyList IncludeTypes { get; init; } = [];
+
+ ///
+ /// Optional list of namespace or type-name prefixes to exclude from the projection.
///
public IReadOnlyList Exclude { get; init; } = [];