Skip to content

Commit b296076

Browse files
committed
feat: Initial API surface validator
1 parent 13ec2fb commit b296076

File tree

3 files changed

+190
-0
lines changed

3 files changed

+190
-0
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#nullable enable
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.IO.Compression;
7+
using System.Linq;
8+
using Mono.Cecil;
9+
10+
namespace Uno.APISurfaceValidator
11+
{
12+
class Program
13+
{
14+
private static (string path, string targetAssembly, string pattern)[] _contractsToValidate = new[]
15+
{
16+
(@"C:\Program Files (x86)\Windows Kits\10\References\10.0.19041.0\Windows.Foundation.FoundationContract\4.0.0.0\Windows.Foundation.FoundationContract.winmd", "Uno.Foundation.dll", "*"),
17+
(@"C:\Program Files (x86)\Windows Kits\10\References\10.0.19041.0\Windows.Foundation.UniversalApiContract\10.0.0.0\Windows.Foundation.UniversalApiContract.winmd", "Uno.dll", "-Windows.UI.Xaml"),
18+
(@"C:\Program Files (x86)\Windows Kits\10\References\10.0.19041.0\Windows.Phone.PhoneContract\1.0.0.0\Windows.Phone.PhoneContract.winmd", "Uno.dll", "*"),
19+
(@"C:\Program Files (x86)\Windows Kits\10\References\10.0.19041.0\Windows.Networking.Connectivity.WwanContract\2.0.0.0\Windows.Networking.Connectivity.WwanContract.winmd", "Uno.dll", "*"),
20+
(@"C:\Program Files (x86)\Windows Kits\10\References\10.0.19041.0\Windows.ApplicationModel.Calls.CallsPhoneContract\5.0.0.0\Windows.ApplicationModel.Calls.CallsPhoneContract.winmd", "Uno.dll", "*"),
21+
(@"C:\Program Files (x86)\Windows Kits\10\References\10.0.19041.0\Windows.Foundation.UniversalApiContract\10.0.0.0\Windows.Foundation.UniversalApiContract.winmd", "Uno.UI.dll", "+Windows.UI.Xaml")
22+
};
23+
24+
static int Main(string[] args)
25+
{
26+
var hasErrors = false;
27+
28+
var filePath = UnpackArchive(args[0]);
29+
30+
Console.WriteLine($"Validating package {args[0]}");
31+
32+
foreach (var contract in _contractsToValidate)
33+
{
34+
Console.WriteLine($"Validating {Path.GetFileName(contract.path)} :");
35+
36+
var referenceAssembly = ReadAssemblyDefinition(contract.path);
37+
var assembly = ReadAssemblyDefinition(Path.Combine(filePath, "lib", "netstandard2.0", contract.targetAssembly));
38+
39+
hasErrors |= CompareAssemblies(referenceAssembly, assembly, contract.pattern, contract.targetAssembly);
40+
}
41+
42+
return hasErrors ? 1 : 0;
43+
}
44+
45+
private static bool CompareAssemblies(AssemblyDefinition referenceAssembly, AssemblyDefinition assembly, string pattern, string identifier)
46+
{
47+
var hasError = false;
48+
49+
var referenceTypes = referenceAssembly.MainModule.GetTypes();
50+
var types = assembly.MainModule.GetTypes().ToDictionary(t => t.FullName);
51+
52+
Func<TypeDefinition, bool> predicate = pattern[0] switch
53+
{
54+
'+' => t => t.IsPublic && t.FullName.StartsWith(pattern[1..]),
55+
'-' => t => t.IsPublic && !t.FullName.StartsWith(pattern[1..]),
56+
_ => t => t.IsPublic
57+
};
58+
59+
foreach (var referenceType in referenceTypes.Where(predicate))
60+
{
61+
if (types.TryGetValue(referenceType.FullName, out var type))
62+
{
63+
if (referenceType.BaseType?.FullName != type.BaseType?.FullName)
64+
{
65+
Console.WriteLine($"{referenceType.FullName} base type is different {referenceType.BaseType?.FullName} in reference, {type.BaseType?.FullName} in {identifier}");
66+
hasError = true;
67+
}
68+
69+
hasError |= CompareMembers(referenceType.Methods.Where(m => m.IsPublic), type.Methods, identifier);
70+
hasError |= CompareMembers(referenceType.Properties.Where(m => m.GetMethod?.IsPublic ?? false), type.Properties, identifier);
71+
hasError |= CompareMembers(referenceType.Fields.Where(m => m.IsPublic), type.Fields, identifier);
72+
hasError |= CompareMembers(referenceType.Events.Where(m => m.AddMethod?.IsPublic ?? false), type.Events, identifier);
73+
}
74+
else
75+
{
76+
hasError = true;
77+
Console.WriteLine($"The type {referenceType} is missing from {identifier}");
78+
}
79+
}
80+
81+
return hasError;
82+
}
83+
84+
private static bool CompareMembers(IEnumerable<MemberReference> referenceMembers, IEnumerable<MemberReference> members, string identifier)
85+
{
86+
var hasError = false;
87+
88+
var membersLookup = members.Select(RewriteMember).ToDictionary(m => m.ToString());
89+
90+
foreach (var referenceMember in referenceMembers.Select(RewriteReferenceMember))
91+
{
92+
if (!membersLookup.ContainsKey(referenceMember))
93+
{
94+
Console.WriteLine($"The member {referenceMember} cannot be found in {identifier}");
95+
hasError = true;
96+
}
97+
}
98+
99+
return hasError;
100+
}
101+
102+
private static AssemblyDefinition ReadAssemblyDefinition(string assemblyPath)
103+
=> AssemblyDefinition.ReadAssembly(assemblyPath, new ReaderParameters() { ApplyWindowsRuntimeProjections = true, AssemblyResolver = new DefaultAssemblyResolver() });
104+
105+
private static MemberReference RewriteMember(MemberReference member)
106+
{
107+
if (member is MethodDefinition methodDefinition)
108+
{
109+
if (methodDefinition.HasOverrides && methodDefinition.Name.EndsWith("IEnumerable.GetEnumerator"))
110+
{
111+
methodDefinition.Name = "GetEnumerator";
112+
}
113+
}
114+
115+
return member;
116+
}
117+
118+
private static string RewriteReferenceMember(MemberReference member)
119+
{
120+
if (member is MethodDefinition methodDefinition)
121+
{
122+
if (methodDefinition.IsAddOn && methodDefinition.ReturnType.FullName == "System.Runtime.InteropServices.WindowsRuntime.EventRegistrationToken")
123+
{
124+
methodDefinition.ReturnType = methodDefinition.Module.TypeSystem.Void;
125+
}
126+
127+
if (methodDefinition.IsRemoveOn && methodDefinition.Parameters.Count == 1 && methodDefinition.Parameters[0].ParameterType.FullName == "System.Runtime.InteropServices.WindowsRuntime.EventRegistrationToken")
128+
{
129+
var parameterType = methodDefinition.DeclaringType.Methods.First(e => e.Name == "add_" + methodDefinition.Name.Substring(7)).Parameters[0].ParameterType;
130+
131+
methodDefinition.Parameters[0].ParameterType = parameterType;
132+
}
133+
134+
if (methodDefinition.IsSetter && methodDefinition.Name.StartsWith("put_"))
135+
{
136+
var suffix = methodDefinition.Name.Substring(4);
137+
138+
return methodDefinition.ToString().Replace("put_" + suffix, "set_" + suffix);
139+
}
140+
}
141+
142+
return member.ToString();
143+
}
144+
145+
private static string UnpackArchive(string packagePath)
146+
{
147+
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
148+
149+
Directory.CreateDirectory(path);
150+
151+
Console.WriteLine($"Extracting {packagePath} -> {path}");
152+
using (var file = File.OpenRead(packagePath))
153+
{
154+
using (var archive = new ZipArchive(file, ZipArchiveMode.Read))
155+
{
156+
archive.ExtractToDirectory(path);
157+
}
158+
}
159+
160+
if (Directory.GetFiles(path, "*.nuspec", SearchOption.AllDirectories).FirstOrDefault() is string)
161+
{
162+
return path;
163+
}
164+
else
165+
{
166+
throw new InvalidOperationException($"Unable to find nuspec file in {packagePath}");
167+
}
168+
}
169+
}
170+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"profiles": {
3+
"Uno.APISurfaceValidator": {
4+
"commandName": "Project"
5+
}
6+
}
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>netcoreapp3.1</TargetFramework>
6+
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Mono.Cecil" Version="0.11.3" />
11+
</ItemGroup>
12+
13+
</Project>

0 commit comments

Comments
 (0)